// 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 . package text import ( "fmt" "strings" "github.com/superseriousbusiness/gotosocial/internal/regexes" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/text" ) /* MENTION PARSER STUFF */ // mention fulfils the goldmark // ast.Node interface. type mention struct { ast.BaseInline Segment text.Segment } var kindMention = ast.NewNodeKind("Mention") func (n *mention) Kind() ast.NodeKind { return kindMention } func (n *mention) Dump(source []byte, level int) { fmt.Printf("%sMention: %s\n", strings.Repeat(" ", level), string(n.Segment.Value(source))) } // newMention creates a goldmark ast.Node // from a text.Segment. The contained segment // is used in rendering. func newMention(s text.Segment) *mention { return &mention{ BaseInline: ast.BaseInline{}, Segment: s, } } // mentionParser fulfils the goldmark // parser.InlineParser interface. type mentionParser struct{} // Mention parsing is triggered by the `@` symbol // which appears at the beginning of a mention. func (p *mentionParser) Trigger() []byte { return []byte{'@'} } func (p *mentionParser) Parse( _ ast.Node, block text.Reader, _ parser.Context, ) ast.Node { // If preceding character is not a valid boundary // character, then this cannot be a valid mention. if !isMentionBoundary(block.PrecendingCharacter()) { return nil } line, segment := block.PeekLine() // Ascertain location of mention in the line // that starts with the trigger character. loc := regexes.MentionFinder.FindIndex(line) if loc == nil || loc[0] != 0 { // Noop if not found or // not found at start. return nil } // Advance the block to // the end of the mention. block.Advance(loc[1]) // mention ast.Node spans from the // beginning of this segment up to // the last character of the mention. return newMention( segment.WithStop( segment.Start + loc[1], ), ) } /* HASHTAG PARSER STUFF */ // hashtag fulfils the goldmark // ast.Node interface. type hashtag struct { ast.BaseInline Segment text.Segment } var kindHashtag = ast.NewNodeKind("Hashtag") func (n *hashtag) Kind() ast.NodeKind { return kindHashtag } func (n *hashtag) Dump(source []byte, level int) { fmt.Printf("%sHashtag: %s\n", strings.Repeat(" ", level), string(n.Segment.Value(source))) } // newHashtag creates a goldmark ast.Node // from a text.Segment. The contained segment // is used in rendering. func newHashtag(s text.Segment) *hashtag { return &hashtag{ BaseInline: ast.BaseInline{}, Segment: s, } } type hashtagParser struct{} // Hashtag parsing is triggered by a '#' symbol // which appears at the beginning of a hashtag. func (p *hashtagParser) Trigger() []byte { return []byte{'#'} } func (p *hashtagParser) Parse( _ ast.Node, block text.Reader, _ parser.Context, ) ast.Node { // If preceding character is not a valid boundary // character, then this cannot be a valid hashtag. if !isHashtagBoundary(block.PrecendingCharacter()) { return nil } var ( line, segment = block.PeekLine() lineStr = string(line) lineStrLen = len(lineStr) ) if lineStrLen <= 1 { // This is probably just // a lonely '#' char. return nil } // Iterate through the runes in the detected // hashtag string until we reach either: // - A weird character (bad). // - The end of the hashtag (ok). // - The end of the string (also ok). for i, r := range lineStr { switch { case r == '#' && i == 0: // Ignore initial '#'. continue case !isPlausiblyInHashtag(r) && !isHashtagBoundary(r): // Weird non-boundary character // in the hashtag. Don't trust it. return nil case isHashtagBoundary(r): // Reached closing hashtag // boundary. Advance block // to the end of the hashtag. block.Advance(i) // hashtag ast.Node spans from // the beginning of this segment // up to the boundary character. return newHashtag( segment.WithStop( segment.Start + i, ), ) } } // No invalid or boundary characters before the // end of the line: it's all hashtag, baby 😎 // // Advance block to the end of the segment. block.Advance(segment.Len()) // hashtag ast.Node spans // the entire segment. return newHashtag(segment) } /* EMOJI PARSER STUFF */ // emoji fulfils the goldmark // ast.Node interface. type emoji struct { ast.BaseInline Segment text.Segment } var kindEmoji = ast.NewNodeKind("Emoji") func (n *emoji) Kind() ast.NodeKind { return kindEmoji } func (n *emoji) Dump(source []byte, level int) { fmt.Printf("%sEmoji: %s\n", strings.Repeat(" ", level), string(n.Segment.Value(source))) } // newEmoji creates a goldmark ast.Node // from a text.Segment. The contained // segment is used in rendering. func newEmoji(s text.Segment) *emoji { return &emoji{ BaseInline: ast.BaseInline{}, Segment: s, } } type emojiParser struct{} // Emoji parsing is triggered by a ':' char // which appears at the start of the emoji. func (p *emojiParser) Trigger() []byte { return []byte{':'} } func (p *emojiParser) Parse( _ ast.Node, block text.Reader, _ parser.Context, ) ast.Node { line, segment := block.PeekLine() // Ascertain location of emoji in the line // that starts with the trigger character. loc := regexes.EmojiFinder.FindIndex(line) if loc == nil || loc[0] != 0 { // Noop if not found or // not found at start. return nil } // Advance the block to // the end of the emoji. block.Advance(loc[1]) // emoji ast.Node spans from the // beginning of this segment up to // the last character of the emoji. return newEmoji( segment.WithStop( segment.Start + loc[1], ), ) }