memos/internal/markdown/extensions/mention.go

76 lines
2.4 KiB
Go

package extensions
import (
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
"github.com/usememos/memos/internal/base"
mast "github.com/usememos/memos/internal/markdown/ast"
mparser "github.com/usememos/memos/internal/markdown/parser"
)
type mentionExtension struct{}
// MentionExtension is a goldmark extension for username references using @username syntax.
var MentionExtension = &mentionExtension{}
// Extend extends the goldmark parser with username-reference support.
func (*mentionExtension) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(
parser.WithASTTransformers(
// GFM email recognition runs at 950 and tag recognition at 1000.
util.Prioritized(&mentionASTTransformer{}, 1050),
),
)
}
type mentionASTTransformer struct{}
func (*mentionASTTransformer) Transform(document *ast.Document, reader text.Reader, _ parser.Context) {
source := reader.Source()
textNodes := eligibleLiteralTextNodes(document)
mergeAdjacentLiteralText(textNodes, source)
for _, textNode := range textNodes {
if textNode.Parent() == nil {
continue
}
replaceMentionsInText(textNode, source)
}
}
func replaceMentionsInText(textNode *ast.Text, source []byte) {
segment := textNode.Segment
runHasLeftBoundary := segment.Padding > 0 || segment.Start == 0 || !base.IsUsernameCharacter(source[segment.Start-1])
matches := mparser.FindMentionMatches(segment.Value(source), runHasLeftBoundary)
if len(matches) == 0 {
return
}
parent := textNode.Parent()
cursor := segment.Start
padding := segment.Padding
for _, match := range matches {
start := segment.Start + match.Start - segment.Padding
end := segment.Start + match.End - segment.Padding
if start > cursor || padding > 0 {
insertSplitTextBefore(parent, textNode, textNode, text.NewSegmentPadding(cursor, start, padding), false)
}
mentionNode := &mast.MentionNode{
Username: append([]byte(nil), match.Username...),
Source: append([]byte(nil), source[start:end]...),
}
mentionNode.SetPos(start)
parent.InsertBefore(parent, textNode, mentionNode)
cursor = end
padding = 0
}
if cursor < segment.Stop || textNode.SoftLineBreak() || textNode.HardLineBreak() {
insertSplitTextBefore(parent, textNode, textNode, text.NewSegment(cursor, segment.Stop), true)
}
parent.RemoveChild(parent, textNode)
}