diff --git a/go.mod b/go.mod index 76e0d9dc..2edee626 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/sprig v2.22.0+incompatible + github.com/frustra/bbcode v0.0.0-20201127003707-6ef347fbe1c8 // indirect github.com/go-stack/stack v1.8.0 github.com/google/uuid v1.2.0 github.com/huandu/xstrings v1.3.2 // indirect diff --git a/go.sum b/go.sum index b85bbf47..f21765df 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/frustra/bbcode v0.0.0-20201127003707-6ef347fbe1c8 h1:sdIsYe6Vv7KIWZWp8KqSeTl+XlF17d+wHCC4lbxFcYs= +github.com/frustra/bbcode v0.0.0-20201127003707-6ef347fbe1c8/go.mod h1:0QBxkXxN+o4FyZgLI9FHY/oUizheze3+bNY/kgCKL+4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= diff --git a/public/parsing.wasm b/public/parsing.wasm index 4bf2bc09..3df7a3f9 100644 Binary files a/public/parsing.wasm and b/public/parsing.wasm differ diff --git a/src/parsing/bbcode.go b/src/parsing/bbcode.go index 5d4d6ea0..e5562379 100644 --- a/src/parsing/bbcode.go +++ b/src/parsing/bbcode.go @@ -1,67 +1,160 @@ package parsing import ( - "strings" + "regexp" + "github.com/frustra/bbcode" "github.com/yuin/goldmark" - "github.com/yuin/goldmark/ast" gast "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/renderer/html" "github.com/yuin/goldmark/text" "github.com/yuin/goldmark/util" ) -const BBCodePriority = 1 // TODO: Pick something more reasonable? +var BBCodePriority = 1 // TODO: This is maybe too high a priority? -type bParser struct{} +var reTag = regexp.MustCompile(`(?P\[\s*(?P[a-zA-Z]+))|(?P\[\s*\/\s*(?P[a-zA-Z]+)\s*\])`) -var _ parser.InlineParser = bParser{} +var bbcodeCompiler = bbcode.NewCompiler(false, false) -func (s bParser) Trigger() []byte { +// ---------------------- +// Parser and delimiters +// ---------------------- + +type bbcodeParser struct{} + +func NewBBCodeParser() parser.InlineParser { + return bbcodeParser{} +} + +func (s bbcodeParser) Trigger() []byte { return []byte{'['} } -func (s bParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node { - // _, segment := block.PeekLine() - // start := segment.Start +func (s bbcodeParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node { + _, pos := block.Position() + restOfSource := block.Source()[pos.Start:] - // block.Advance(3) - // n := ast.NewTextSegment(text.NewSegment(start, start+4)) - // bold := ast.NewText() - // bold.Segment - // link := ast.NewAutoLink(typ, n) - // link.Protocol = protocol - // return link - - lineBytes, _ := block.PeekLine() - - line := string(lineBytes) - - if !strings.HasPrefix(line, "[b]") { + matches := reTag.FindAllSubmatchIndex(restOfSource, -1) + if matches == nil { + // No tags anywhere return nil } - start := 0 - closingIndex := strings.Index(line, "[/b]") - if closingIndex < 0 { + otIndex := reTag.SubexpIndex("opentagname") + ctIndex := reTag.SubexpIndex("closetagname") + + tagName := extractStringBySubmatchIndices(restOfSource, matches[0], otIndex) + if tagName == "" { + // Not an opening tag return nil } - end := closingIndex + 4 - n := ast.NewEmphasis(2) - n.AppendChild(n, ast.NewString([]byte("wow bold text"))) + depth := 0 + endIndex := -1 + for _, m := range matches { + if openName := extractStringBySubmatchIndices(restOfSource, m, otIndex); openName != "" { + if openName == tagName { + depth++ + } + } else if closeName := extractStringBySubmatchIndices(restOfSource, m, ctIndex); closeName != "" { + if closeName == tagName { + depth-- + if depth == 0 { + // We have balanced out! + endIndex = m[1] // the end index of this closing tag (exclusive) + } + } + } + } + if endIndex < 0 { + // Unbalanced, too many opening tags + return nil + } - block.Advance(end - start) - return n + unparsedBBCode := restOfSource[:endIndex] + block.Advance(len(unparsedBBCode)) + + return NewBBCode(bbcodeCompiler.Compile(string(unparsedBBCode))) } -type bTag struct{} +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]]) +} -func (e bTag) Extend(m goldmark.Markdown) { +// ---------------------- +// 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 +} + +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 +// ---------------------- + +type BBCodeExtension struct{} + +func (e BBCodeExtension) Extend(m goldmark.Markdown) { m.Parser().AddOptions(parser.WithInlineParsers( - util.Prioritized(bParser{}, BBCodePriority), + util.Prioritized(NewBBCodeParser(), BBCodePriority), + )) + m.Renderer().AddOptions(renderer.WithNodeRenderers( + util.Prioritized(NewBBCodeHTMLRenderer(), BBCodePriority), )) - // m.Renderer().AddOptions(renderer.WithNodeRenderers( - // util.Prioritized(NewStrikethroughHTMLRenderer(), 500), - // )) } diff --git a/src/parsing/parsing.go b/src/parsing/parsing.go index acff981a..e0724ddc 100644 --- a/src/parsing/parsing.go +++ b/src/parsing/parsing.go @@ -2,94 +2,11 @@ package parsing import ( "bytes" - "regexp" - "sort" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" ) -func StripNames(regexStr string) string { - return regexp.MustCompile(`\(\?P<[a-zA-Z0-9_]+>`).ReplaceAllString(regexStr, `(?:`) -} - -var reArgStr = `(?P[a-zA-Z0-9]+)(?:\s*=\s*(?:'(?P.*?)'|"(?P.*?)"|(?P[^\s\]]+)))?` -var reTagOpenStr = `\[\s*(?P(?:` + StripNames(reArgStr) + `)(?:\s+(?:` + StripNames(reArgStr) + `))*)\s*\]` -var reTagCloseStr = `\[/\s*(?P[a-zA-Z0-9]+)?\s*\]` - -var reArg = regexp.MustCompile(reArgStr) -var reTagOpen = regexp.MustCompile(reTagOpenStr) -var reTagClose = regexp.MustCompile(reTagCloseStr) - -const tokenTypeString = "string" -const tokenTypeOpenTag = "openTag" -const tokenTypeCloseTag = "closeTag" - -type token struct { - Type string - StartIndex int - EndIndex int - Contents string -} - -func ParseBBCode(input string) string { - return input -} - -func tokenizeBBCode(input string) []token { - openMatches := reTagOpen.FindAllStringIndex(input, -1) - closeMatches := reTagClose.FindAllStringIndex(input, -1) - - // Build tokens from regex matches - var tagTokens []token - for _, match := range openMatches { - tagTokens = append(tagTokens, token{ - Type: tokenTypeOpenTag, - StartIndex: match[0], - EndIndex: match[1], - Contents: input[match[0]:match[1]], - }) - } - for _, match := range closeMatches { - tagTokens = append(tagTokens, token{ - Type: tokenTypeCloseTag, - StartIndex: match[0], - EndIndex: match[1], - Contents: input[match[0]:match[1]], - }) - } - - // Sort those tokens together - sort.Slice(tagTokens, func(i, j int) bool { - return tagTokens[i].StartIndex < tagTokens[j].StartIndex - }) - - // Make a new list of tokens that fills in the gaps with plain old text - var tokens []token - for i, tagToken := range tagTokens { - prevEnd := 0 - if i > 0 { - prevEnd = tagTokens[i-1].EndIndex - } - - tokens = append(tokens, token{ - Type: tokenTypeString, - StartIndex: prevEnd, - EndIndex: tagToken.StartIndex, - Contents: input[prevEnd:tagToken.StartIndex], - }) - tokens = append(tokens, tagToken) - } - tokens = append(tokens, token{ - Type: tokenTypeString, - StartIndex: tokens[len(tokens)-1].EndIndex, - EndIndex: len(input), - Contents: input[tokens[len(tokens)-1].EndIndex:], - }) - - return tokens -} - var previewMarkdown = goldmark.New( goldmark.WithExtensions( extension.GFM, @@ -98,7 +15,7 @@ var previewMarkdown = goldmark.New( Preview: true, }, MathjaxExtension{}, - bTag{}, + BBCodeExtension{}, ), ) var realMarkdown = goldmark.New( @@ -107,7 +24,7 @@ var realMarkdown = goldmark.New( SpoilerExtension{}, EmbedExtension{}, MathjaxExtension{}, - bTag{}, + BBCodeExtension{}, ), )