2021-06-16 04:04:01 +00:00
|
|
|
package parsing
|
|
|
|
|
|
|
|
import (
|
2021-06-23 03:52:04 +00:00
|
|
|
"bytes"
|
2021-06-20 20:46:42 +00:00
|
|
|
"regexp"
|
2021-06-23 03:52:04 +00:00
|
|
|
"strings"
|
2021-06-16 04:04:01 +00:00
|
|
|
|
2021-06-20 22:40:33 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/hmnurl"
|
2021-06-23 03:52:04 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/oops"
|
|
|
|
"github.com/alecthomas/chroma"
|
|
|
|
chromahtml "github.com/alecthomas/chroma/formatters/html"
|
|
|
|
"github.com/alecthomas/chroma/lexers"
|
|
|
|
"github.com/alecthomas/chroma/styles"
|
2021-06-20 20:46:42 +00:00
|
|
|
"github.com/frustra/bbcode"
|
2021-06-16 04:04:01 +00:00
|
|
|
"github.com/yuin/goldmark"
|
|
|
|
gast "github.com/yuin/goldmark/ast"
|
|
|
|
"github.com/yuin/goldmark/parser"
|
2021-06-20 20:46:42 +00:00
|
|
|
"github.com/yuin/goldmark/renderer"
|
|
|
|
"github.com/yuin/goldmark/renderer/html"
|
2021-06-16 04:04:01 +00:00
|
|
|
"github.com/yuin/goldmark/text"
|
|
|
|
"github.com/yuin/goldmark/util"
|
|
|
|
)
|
|
|
|
|
2021-08-28 17:07:45 +00:00
|
|
|
var BBCodePriority = 1
|
2021-06-16 04:04:01 +00:00
|
|
|
|
2021-07-18 02:42:52 +00:00
|
|
|
var reOpenTag = regexp.MustCompile(`^\[\s*(?P<name>[a-zA-Z0-9]+)`)
|
|
|
|
var reTag = regexp.MustCompile(`\[\s*(?P<opentagname>[a-zA-Z0-9]+)|\[\s*\/\s*(?P<closetagname>[a-zA-Z0-9]+)\s*\]`)
|
2021-06-16 04:04:01 +00:00
|
|
|
|
2021-06-20 21:44:07 +00:00
|
|
|
var previewBBCodeCompiler = bbcode.NewCompiler(false, false)
|
|
|
|
var realBBCodeCompiler = bbcode.NewCompiler(false, false)
|
|
|
|
|
|
|
|
var REYoutubeVidOnly = regexp.MustCompile(`^[a-zA-Z0-9_-]{11}$`)
|
|
|
|
|
|
|
|
func init() {
|
2021-06-20 22:21:58 +00:00
|
|
|
type attr struct {
|
|
|
|
Name, Value string
|
|
|
|
}
|
2021-06-20 22:40:33 +00:00
|
|
|
|
2021-06-20 22:21:58 +00:00
|
|
|
addSimpleTag := func(name, tag string, notext bool, attrs ...attr) {
|
|
|
|
var tagFunc bbcode.TagCompilerFunc = func(bn *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) {
|
|
|
|
if notext {
|
|
|
|
var newChildren []*bbcode.BBCodeNode
|
|
|
|
for _, child := range bn.Children {
|
|
|
|
if child.ID != bbcode.TEXT {
|
|
|
|
newChildren = append(newChildren, child)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
bn.Children = newChildren
|
|
|
|
}
|
|
|
|
|
|
|
|
out := bbcode.NewHTMLTag("")
|
|
|
|
out.Name = tag
|
|
|
|
for _, a := range attrs {
|
|
|
|
out.Attrs[a.Name] = a.Value
|
|
|
|
}
|
|
|
|
return out, true
|
|
|
|
}
|
|
|
|
previewBBCodeCompiler.SetTag(name, tagFunc)
|
|
|
|
realBBCodeCompiler.SetTag(name, tagFunc)
|
|
|
|
}
|
2021-06-20 22:40:33 +00:00
|
|
|
addTag := func(name string, f bbcode.TagCompilerFunc) {
|
|
|
|
previewBBCodeCompiler.SetTag(name, f)
|
|
|
|
realBBCodeCompiler.SetTag(name, f)
|
|
|
|
}
|
2021-06-20 22:21:58 +00:00
|
|
|
|
2021-06-20 21:44:07 +00:00
|
|
|
previewBBCodeCompiler.SetTag("youtube", makeYoutubeBBCodeFunc(true))
|
|
|
|
realBBCodeCompiler.SetTag("youtube", makeYoutubeBBCodeFunc(false))
|
2021-06-20 22:21:58 +00:00
|
|
|
|
|
|
|
addSimpleTag("h1", "h1", false)
|
|
|
|
addSimpleTag("h2", "h3", false)
|
|
|
|
addSimpleTag("h3", "h3", false)
|
|
|
|
addSimpleTag("m", "span", false, attr{"class", "monospace"})
|
|
|
|
addSimpleTag("ol", "ol", true)
|
|
|
|
addSimpleTag("ul", "ul", true)
|
|
|
|
addSimpleTag("li", "li", false)
|
|
|
|
addSimpleTag("spoiler", "span", false, attr{"class", "spoiler"})
|
|
|
|
addSimpleTag("table", "table", true)
|
|
|
|
addSimpleTag("tr", "tr", true)
|
|
|
|
addSimpleTag("th", "th", false)
|
|
|
|
addSimpleTag("td", "td", false)
|
2021-06-20 22:40:33 +00:00
|
|
|
|
|
|
|
addTag("quote", func(bn *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) {
|
|
|
|
cite := bn.GetOpeningTag().Value
|
|
|
|
if cite == "" {
|
|
|
|
out := bbcode.NewHTMLTag("")
|
|
|
|
out.Name = "blockquote"
|
|
|
|
return out, true
|
|
|
|
} else {
|
|
|
|
out := bbcode.NewHTMLTag("")
|
|
|
|
out.Name = "blockquote"
|
|
|
|
out.Attrs["cite"] = cite
|
|
|
|
|
|
|
|
a := bbcode.NewHTMLTag("")
|
|
|
|
a.Name = "a"
|
|
|
|
a.Attrs = map[string]string{
|
2021-06-24 04:16:36 +00:00
|
|
|
"href": hmnurl.BuildUserProfile(cite),
|
2021-06-20 22:40:33 +00:00
|
|
|
"class": "quotewho",
|
|
|
|
}
|
|
|
|
a.AppendChild(bbcode.NewHTMLTag(cite))
|
|
|
|
|
|
|
|
br := bbcode.NewHTMLTag("")
|
|
|
|
br.Name = "br"
|
|
|
|
|
|
|
|
out.AppendChild(a)
|
|
|
|
out.AppendChild(br)
|
|
|
|
|
|
|
|
return out, true
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
addTag("code", func(bn *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) {
|
|
|
|
lang := ""
|
|
|
|
if tagvalue := bn.GetOpeningTag().Value; tagvalue != "" {
|
|
|
|
lang = tagvalue
|
|
|
|
} else if arglang, ok := bn.GetOpeningTag().Args["language"]; ok {
|
|
|
|
lang = arglang
|
|
|
|
}
|
|
|
|
|
2021-06-23 03:52:04 +00:00
|
|
|
text := bbcode.CompileText(bn)
|
|
|
|
text = strings.TrimPrefix(text, "\n")
|
|
|
|
|
|
|
|
var lexer chroma.Lexer
|
|
|
|
if lang != "" {
|
|
|
|
lexer = lexers.Get(lang)
|
|
|
|
}
|
|
|
|
if lexer == nil {
|
|
|
|
lexer = lexers.Analyse(text)
|
|
|
|
}
|
|
|
|
if lexer == nil {
|
|
|
|
lexer = lexers.Fallback
|
|
|
|
}
|
|
|
|
|
|
|
|
iterator, err := lexer.Tokenise(nil, text)
|
|
|
|
if err != nil {
|
|
|
|
panic(oops.New(err, "failed to tokenize bbcode"))
|
|
|
|
}
|
|
|
|
|
|
|
|
var result bytes.Buffer
|
|
|
|
formatter := chromahtml.New(HMNChromaOptions...)
|
|
|
|
formatter.Format(&result, styles.Monokai, iterator)
|
|
|
|
formatted := result.String()
|
|
|
|
|
2021-06-20 22:40:33 +00:00
|
|
|
out := bbcode.NewHTMLTag("")
|
|
|
|
out.Name = "pre"
|
2021-06-23 03:52:04 +00:00
|
|
|
out.Attrs["class"] = "hmn-code"
|
2021-06-20 22:40:33 +00:00
|
|
|
|
2021-06-23 03:52:04 +00:00
|
|
|
child := bbcode.NewHTMLTag(formatted)
|
|
|
|
child.Raw = true
|
|
|
|
out.AppendChild(child)
|
2021-06-20 22:40:33 +00:00
|
|
|
|
2021-06-23 03:52:04 +00:00
|
|
|
return out, false
|
2021-06-20 22:40:33 +00:00
|
|
|
})
|
2021-06-20 21:44:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func makeYoutubeBBCodeFunc(preview bool) bbcode.TagCompilerFunc {
|
|
|
|
return func(bn *bbcode.BBCodeNode) (*bbcode.HTMLTag, bool) {
|
2021-06-23 03:52:04 +00:00
|
|
|
contents := bbcode.CompileText(bn)
|
|
|
|
if contents == "" {
|
2021-06-20 21:44:07 +00:00
|
|
|
return bbcode.NewHTMLTag("<missing video URL>"), false
|
|
|
|
}
|
|
|
|
|
|
|
|
vid := ""
|
|
|
|
|
|
|
|
if m := REYoutubeLong.FindStringSubmatch(contents); m != nil {
|
|
|
|
vid = m[REYoutubeLong.SubexpIndex("vid")]
|
|
|
|
} else if m := REYoutubeShort.FindStringSubmatch(contents); m != nil {
|
|
|
|
vid = m[REYoutubeShort.SubexpIndex("vid")]
|
|
|
|
} else if m := REYoutubeVidOnly.MatchString(contents); m {
|
|
|
|
vid = contents
|
|
|
|
}
|
|
|
|
|
|
|
|
if vid == "" {
|
|
|
|
return bbcode.NewHTMLTag("<bad video URL>"), false
|
|
|
|
}
|
|
|
|
|
|
|
|
if preview {
|
|
|
|
/*
|
|
|
|
<div class="mw6">
|
|
|
|
<img src="https://img.youtube.com/vi/` + vid + `/hqdefault.jpg">
|
|
|
|
</div>
|
|
|
|
*/
|
|
|
|
|
|
|
|
out := bbcode.NewHTMLTag("")
|
|
|
|
out.Name = "div"
|
|
|
|
out.Attrs["class"] = "mw6"
|
|
|
|
|
|
|
|
img := bbcode.NewHTMLTag("")
|
|
|
|
img.Name = "img"
|
|
|
|
img.Attrs = map[string]string{
|
|
|
|
"src": "https://img.youtube.com/vi/" + vid + "/hqdefault.jpg",
|
|
|
|
}
|
|
|
|
|
|
|
|
out.AppendChild(img)
|
|
|
|
|
|
|
|
return out, false
|
|
|
|
} else {
|
|
|
|
/*
|
|
|
|
<div class="mw6">
|
|
|
|
<div class="aspect-ratio aspect-ratio--16x9">
|
|
|
|
<iframe class="aspect-ratio--object" src="https://www.youtube-nocookie.com/embed/` + vid + `" frameborder="0" allowfullscreen></iframe>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
*/
|
|
|
|
|
|
|
|
out := bbcode.NewHTMLTag("")
|
|
|
|
out.Name = "div"
|
|
|
|
out.Attrs["class"] = "mw6"
|
|
|
|
|
|
|
|
aspect := bbcode.NewHTMLTag("")
|
|
|
|
aspect.Name = "div"
|
|
|
|
aspect.Attrs["class"] = "aspect-ratio aspect-ratio--16x9"
|
|
|
|
|
|
|
|
iframe := bbcode.NewHTMLTag("")
|
|
|
|
iframe.Name = "iframe"
|
|
|
|
iframe.Attrs = map[string]string{
|
|
|
|
"class": "aspect-ratio--object",
|
|
|
|
"src": "https://www.youtube-nocookie.com/embed/" + vid,
|
|
|
|
"frameborder": "0",
|
|
|
|
"allowfullscreen": "",
|
|
|
|
}
|
|
|
|
iframe.AppendChild(nil) // render a closing tag lol
|
|
|
|
|
|
|
|
aspect.AppendChild(iframe)
|
|
|
|
out.AppendChild(aspect)
|
|
|
|
|
|
|
|
return out, false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-06-16 04:04:01 +00:00
|
|
|
|
2021-06-20 20:46:42 +00:00
|
|
|
// ----------------------
|
|
|
|
// Parser and delimiters
|
|
|
|
// ----------------------
|
|
|
|
|
2021-06-20 21:44:07 +00:00
|
|
|
type bbcodeParser struct {
|
|
|
|
Preview bool
|
2021-06-16 04:04:01 +00:00
|
|
|
}
|
|
|
|
|
2021-06-20 21:44:07 +00:00
|
|
|
var _ parser.InlineParser = &bbcodeParser{}
|
|
|
|
|
2021-06-20 20:46:42 +00:00
|
|
|
func (s bbcodeParser) Trigger() []byte {
|
|
|
|
return []byte{'['}
|
|
|
|
}
|
2021-06-16 04:04:01 +00:00
|
|
|
|
2021-06-20 20:46:42 +00:00
|
|
|
func (s bbcodeParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node {
|
|
|
|
_, pos := block.Position()
|
|
|
|
restOfSource := block.Source()[pos.Start:]
|
2021-06-16 04:04:01 +00:00
|
|
|
|
2021-07-18 02:42:52 +00:00
|
|
|
openMatch := reOpenTag.FindSubmatch(restOfSource)
|
|
|
|
if openMatch == nil {
|
|
|
|
// not a bbcode tag
|
2021-06-20 20:46:42 +00:00
|
|
|
return nil
|
|
|
|
}
|
2021-06-16 04:04:01 +00:00
|
|
|
|
2021-06-20 20:46:42 +00:00
|
|
|
otIndex := reTag.SubexpIndex("opentagname")
|
|
|
|
ctIndex := reTag.SubexpIndex("closetagname")
|
2021-06-16 04:04:01 +00:00
|
|
|
|
2021-07-18 02:42:52 +00:00
|
|
|
tagName := string(openMatch[reOpenTag.SubexpIndex("name")])
|
2021-06-20 20:46:42 +00:00
|
|
|
depth := 0
|
|
|
|
endIndex := -1
|
2021-07-18 02:42:52 +00:00
|
|
|
|
|
|
|
searchStartIndex := 0
|
|
|
|
|
|
|
|
for {
|
|
|
|
searchText := restOfSource[searchStartIndex:]
|
|
|
|
|
|
|
|
match := reTag.FindSubmatchIndex(searchText)
|
|
|
|
if match == nil {
|
|
|
|
// no more tags
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
if openName := extractStringBySubmatchIndices(searchText, match, otIndex); openName == tagName {
|
|
|
|
depth++
|
|
|
|
} else if closeName := extractStringBySubmatchIndices(searchText, match, ctIndex); closeName == tagName {
|
|
|
|
depth--
|
|
|
|
if depth == 0 {
|
|
|
|
// We have balanced out!
|
|
|
|
endIndex = searchStartIndex + match[1] // the end index of this closing tag (exclusive)
|
|
|
|
break
|
2021-06-20 20:46:42 +00:00
|
|
|
}
|
|
|
|
}
|
2021-07-18 02:42:52 +00:00
|
|
|
|
|
|
|
searchStartIndex = searchStartIndex + match[1]
|
2021-06-20 20:46:42 +00:00
|
|
|
}
|
|
|
|
if endIndex < 0 {
|
|
|
|
// Unbalanced, too many opening tags
|
2021-06-16 04:04:01 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-06-20 20:46:42 +00:00
|
|
|
unparsedBBCode := restOfSource[:endIndex]
|
|
|
|
block.Advance(len(unparsedBBCode))
|
2021-06-16 04:04:01 +00:00
|
|
|
|
2021-06-20 21:44:07 +00:00
|
|
|
compiler := realBBCodeCompiler
|
|
|
|
if s.Preview {
|
|
|
|
compiler = previewBBCodeCompiler
|
|
|
|
}
|
|
|
|
|
|
|
|
return NewBBCode(compiler.Compile(string(unparsedBBCode)))
|
2021-06-16 04:04:01 +00:00
|
|
|
}
|
|
|
|
|
2021-06-20 20:46:42 +00:00
|
|
|
func extractStringBySubmatchIndices(src []byte, m []int, subexpIndex int) string {
|
|
|
|
srcIndices := m[2*subexpIndex : 2*subexpIndex+1+1]
|
|
|
|
if srcIndices[0] < 0 {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return string(src[srcIndices[0]:srcIndices[1]])
|
|
|
|
}
|
|
|
|
|
|
|
|
// ----------------------
|
|
|
|
// AST node
|
|
|
|
// ----------------------
|
|
|
|
|
|
|
|
type BBCodeNode struct {
|
|
|
|
gast.BaseInline
|
|
|
|
HTML string
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ gast.Node = &BBCodeNode{}
|
|
|
|
|
|
|
|
func (n *BBCodeNode) Dump(source []byte, level int) {
|
|
|
|
gast.DumpHelper(n, source, level, nil, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
var KindBBCode = gast.NewNodeKind("BBCode")
|
|
|
|
|
|
|
|
func (n *BBCodeNode) Kind() gast.NodeKind {
|
|
|
|
return KindBBCode
|
|
|
|
}
|
2021-06-16 04:04:01 +00:00
|
|
|
|
2021-06-20 20:46:42 +00:00
|
|
|
func NewBBCode(html string) gast.Node {
|
|
|
|
return &BBCodeNode{
|
|
|
|
HTML: html,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ----------------------
|
|
|
|
// Renderer
|
|
|
|
// ----------------------
|
|
|
|
|
|
|
|
type BBCodeHTMLRenderer struct {
|
|
|
|
html.Config
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewBBCodeHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
|
|
|
|
r := &BBCodeHTMLRenderer{
|
|
|
|
Config: html.NewConfig(),
|
|
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
|
|
opt.SetHTMLOption(&r.Config)
|
|
|
|
}
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *BBCodeHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
|
|
|
reg.Register(KindBBCode, r.renderBBCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *BBCodeHTMLRenderer) renderBBCode(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
|
|
|
if entering {
|
|
|
|
w.WriteString(n.(*BBCodeNode).HTML)
|
|
|
|
}
|
|
|
|
return gast.WalkContinue, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ----------------------
|
|
|
|
// Extension
|
|
|
|
// ----------------------
|
|
|
|
|
2021-06-20 21:44:07 +00:00
|
|
|
type BBCodeExtension struct {
|
|
|
|
Preview bool
|
|
|
|
}
|
2021-06-20 20:46:42 +00:00
|
|
|
|
|
|
|
func (e BBCodeExtension) Extend(m goldmark.Markdown) {
|
2021-06-16 04:04:01 +00:00
|
|
|
m.Parser().AddOptions(parser.WithInlineParsers(
|
2021-06-20 21:44:07 +00:00
|
|
|
util.Prioritized(bbcodeParser{Preview: e.Preview}, BBCodePriority),
|
2021-06-20 20:46:42 +00:00
|
|
|
))
|
|
|
|
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
|
|
|
util.Prioritized(NewBBCodeHTMLRenderer(), BBCodePriority),
|
2021-06-16 04:04:01 +00:00
|
|
|
))
|
|
|
|
}
|