76 lines
2.4 KiB
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)
|
|
}
|