diff options
Diffstat (limited to 'internal/text/goldmark_extension.go')
-rw-r--r-- | internal/text/goldmark_extension.go | 313 |
1 files changed, 0 insertions, 313 deletions
diff --git a/internal/text/goldmark_extension.go b/internal/text/goldmark_extension.go deleted file mode 100644 index a12c618dc..000000000 --- a/internal/text/goldmark_extension.go +++ /dev/null @@ -1,313 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <http://www.gnu.org/licenses/>. - -package text - -import ( - "context" - "fmt" - "strings" - - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/log" - "github.com/superseriousbusiness/gotosocial/internal/regexes" - "github.com/superseriousbusiness/gotosocial/internal/util" - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/ast" - "github.com/yuin/goldmark/parser" - "github.com/yuin/goldmark/renderer" - "github.com/yuin/goldmark/text" - mdutil "github.com/yuin/goldmark/util" -) - -// A goldmark extension that parses potential mentions and hashtags separately from regular -// text, so that they stay as one contiguous text fragment in the AST, and then renders -// them separately too, to avoid scanning normal text for mentions and tags. - -// mention and hashtag fulfil the goldmark ast.Node interface. -type mention struct { - ast.BaseInline - Segment text.Segment -} - -type hashtag struct { - ast.BaseInline - Segment text.Segment -} - -type emoji struct { - ast.BaseInline - Segment text.Segment -} - -var ( - kindMention = ast.NewNodeKind("Mention") - kindHashtag = ast.NewNodeKind("Hashtag") - kindEmoji = ast.NewNodeKind("Emoji") -) - -func (n *mention) Kind() ast.NodeKind { - return kindMention -} - -func (n *hashtag) Kind() ast.NodeKind { - return kindHashtag -} - -func (n *emoji) Kind() ast.NodeKind { - return kindEmoji -} - -// Dump can be used for debugging. -func (n *mention) Dump(source []byte, level int) { - fmt.Printf("%sMention: %s\n", strings.Repeat(" ", level), string(n.Segment.Value(source))) -} - -func (n *hashtag) Dump(source []byte, level int) { - fmt.Printf("%sHashtag: %s\n", strings.Repeat(" ", level), string(n.Segment.Value(source))) -} - -func (n *emoji) Dump(source []byte, level int) { - fmt.Printf("%sEmoji: %s\n", strings.Repeat(" ", level), string(n.Segment.Value(source))) -} - -// newMention and newHashtag create a goldmark ast.Node from a goldmark text.Segment. -// The contained segment is used in rendering. -func newMention(s text.Segment) *mention { - return &mention{ - BaseInline: ast.BaseInline{}, - Segment: s, - } -} - -func newHashtag(s text.Segment) *hashtag { - return &hashtag{ - BaseInline: ast.BaseInline{}, - Segment: s, - } -} - -func newEmoji(s text.Segment) *emoji { - return &emoji{ - BaseInline: ast.BaseInline{}, - Segment: s, - } -} - -// mentionParser and hashtagParser fulfil the goldmark parser.InlineParser interface. -type mentionParser struct{} - -type hashtagParser struct{} - -type emojiParser struct{} - -func (p *mentionParser) Trigger() []byte { - return []byte{'@'} -} - -func (p *hashtagParser) Trigger() []byte { - return []byte{'#'} -} - -func (p *emojiParser) Trigger() []byte { - return []byte{':'} -} - -func (p *mentionParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { - before := block.PrecendingCharacter() - line, segment := block.PeekLine() - - if !util.IsMentionOrHashtagBoundary(before) { - return nil - } - - // unideal for performance but makes use of existing regex - loc := regexes.MentionFinder.FindIndex(line) - switch { - case loc == nil: - fallthrough - case loc[0] != 0: // fail if not found at start - return nil - default: - block.Advance(loc[1]) - return newMention(segment.WithStop(segment.Start + loc[1])) - } -} - -func (p *hashtagParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { - before := block.PrecendingCharacter() - line, segment := block.PeekLine() - s := string(line) - - if !util.IsMentionOrHashtagBoundary(before) || len(s) == 1 { - return nil - } - - for i, r := range s { - switch { - case r == '#' && i == 0: - // ignore initial # - continue - case !util.IsPlausiblyInHashtag(r) && !util.IsMentionOrHashtagBoundary(r): - // Fake hashtag, don't trust it - return nil - case util.IsMentionOrHashtagBoundary(r): - if i <= 1 { - // empty - return nil - } - // End of hashtag - block.Advance(i) - return newHashtag(segment.WithStop(segment.Start + i)) - } - } - // If we don't find invalid characters before the end of the line then it's all hashtag, babey - block.Advance(segment.Len()) - return newHashtag(segment) -} - -func (p *emojiParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { - line, segment := block.PeekLine() - - // unideal for performance but makes use of existing regex - loc := regexes.EmojiFinder.FindIndex(line) - switch { - case loc == nil: - fallthrough - case loc[0] != 0: // fail if not found at start - return nil - default: - block.Advance(loc[1]) - return newEmoji(segment.WithStop(segment.Start + loc[1])) - } -} - -// customRenderer fulfils both the renderer.NodeRenderer and goldmark.Extender interfaces. -// It is created in FromMarkdown and FromPlain to be used as a goldmark extension, and the -// fields are used to report tags and mentions to the caller for use as metadata. -type customRenderer struct { - f *formatter - ctx context.Context - parseMention gtsmodel.ParseMentionFunc - accountID string - statusID string - emojiOnly bool - result *FormatResult -} - -func (r *customRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { - reg.Register(kindMention, r.renderMention) - reg.Register(kindHashtag, r.renderHashtag) - reg.Register(kindEmoji, r.renderEmoji) -} - -func (r *customRenderer) Extend(m goldmark.Markdown) { - // 1000 is set as the lowest priority, but it's arbitrary - m.Parser().AddOptions(parser.WithInlineParsers( - mdutil.Prioritized(&emojiParser{}, 1000), - )) - if !r.emojiOnly { - m.Parser().AddOptions(parser.WithInlineParsers( - mdutil.Prioritized(&mentionParser{}, 1000), - mdutil.Prioritized(&hashtagParser{}, 1000), - )) - } - m.Renderer().AddOptions(renderer.WithNodeRenderers( - mdutil.Prioritized(r, 1000), - )) -} - -// renderMention and renderHashtag take a mention or a hashtag ast.Node and render it as HTML. -func (r *customRenderer) renderMention(w mdutil.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { - if !entering { - return ast.WalkSkipChildren, nil - } - - n, ok := node.(*mention) // this function is only registered for kindMention - if !ok { - log.Panic(r.ctx, "type assertion failed") - } - text := string(n.Segment.Value(source)) - - html := r.replaceMention(text) - - // we don't have much recourse if this fails - if _, err := w.WriteString(html); err != nil { - log.Errorf(r.ctx, "error writing HTML: %s", err) - } - return ast.WalkSkipChildren, nil -} - -func (r *customRenderer) renderHashtag(w mdutil.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { - if !entering { - return ast.WalkSkipChildren, nil - } - - n, ok := node.(*hashtag) // this function is only registered for kindHashtag - if !ok { - log.Panic(r.ctx, "type assertion failed") - } - text := string(n.Segment.Value(source)) - - html := r.replaceHashtag(text) - - _, err := w.WriteString(html) - // we don't have much recourse if this fails - if err != nil { - log.Errorf(r.ctx, "error writing HTML: %s", err) - } - return ast.WalkSkipChildren, nil -} - -// renderEmoji doesn't turn an emoji into HTML, but adds it to the metadata. -func (r *customRenderer) renderEmoji(w mdutil.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { - if !entering { - return ast.WalkSkipChildren, nil - } - - n, ok := node.(*emoji) // this function is only registered for kindEmoji - if !ok { - log.Panic(r.ctx, "type assertion failed") - } - text := string(n.Segment.Value(source)) - shortcode := text[1 : len(text)-1] - - emoji, err := r.f.db.GetEmojiByShortcodeDomain(r.ctx, shortcode, "") - if err != nil { - if err != db.ErrNoEntries { - log.Errorf(nil, "error getting local emoji with shortcode %s: %s", shortcode, err) - } - } else if *emoji.VisibleInPicker && !*emoji.Disabled { - listed := false - for _, e := range r.result.Emojis { - if e.Shortcode == emoji.Shortcode { - listed = true - break - } - } - if !listed { - r.result.Emojis = append(r.result.Emojis, emoji) - } - } - - // we don't have much recourse if this fails - if _, err := w.WriteString(text); err != nil { - log.Errorf(r.ctx, "error writing HTML: %s", err) - } - return ast.WalkSkipChildren, nil -} |