Refactor tag rendering to avoid recreating goldmark instance for each message

This commit is contained in:
Tulir Asokan 2023-01-30 00:42:20 +02:00
parent 694733a4e9
commit e7615ef4be
3 changed files with 45 additions and 32 deletions

View file

@ -33,9 +33,16 @@ import (
"maunium.net/go/mautrix/util/variationselector" "maunium.net/go/mautrix/util/variationselector"
) )
var discordExtensions = goldmark.WithExtensions(extension.Strikethrough, mdext.SimpleSpoiler, mdext.DiscordUnderline, &DiscordEveryone{}) // escapeFixer is a hacky partial fix for the difference in escaping markdown, used with escapeReplacement
//
// Discord allows escaping with just one backslash, e.g. \__a__,
// but standard markdown requires both to be escaped (\_\_a__)
var escapeFixer = regexp.MustCompile(`\\(__[^_]|\*\*[^*])`) var escapeFixer = regexp.MustCompile(`\\(__[^_]|\*\*[^*])`)
func escapeReplacement(s string) string {
return s[:2] + `\` + s[2:]
}
// indentableParagraphParser is the default paragraph parser with CanAcceptIndentedLine. // indentableParagraphParser is the default paragraph parser with CanAcceptIndentedLine.
// Used when disabling CodeBlockParser (as disabling it without a replacement will make indented blocks disappear). // Used when disabling CodeBlockParser (as disabling it without a replacement will make indented blocks disappear).
type indentableParagraphParser struct { type indentableParagraphParser struct {
@ -48,24 +55,24 @@ func (b *indentableParagraphParser) CanAcceptIndentedLine() bool {
return true return true
} }
func (portal *Portal) renderDiscordMarkdownOnlyHTML(text string) string { var discordRenderer = goldmark.New(
text = escapeFixer.ReplaceAllStringFunc(text, func(s string) string { goldmark.WithParser(mdext.ParserWithoutFeatures(
return s[:2] + `\` + s[2:] parser.NewListParser(), parser.NewListItemParser(), parser.NewHTMLBlockParser(), parser.NewRawHTMLParser(),
}) parser.NewSetextHeadingParser(), parser.NewATXHeadingParser(), parser.NewThematicBreakParser(),
parser.NewLinkParser(), parser.NewCodeBlockParser(),
)),
goldmark.WithParserOptions(parser.WithBlockParsers(util.Prioritized(defaultIndentableParagraphParser, 500))),
format.HTMLOptions,
goldmark.WithExtensions(extension.Strikethrough, mdext.SimpleSpoiler, mdext.DiscordUnderline, ExtDiscordEveryone, ExtDiscordTag),
)
mdRenderer := goldmark.New( func (portal *Portal) renderDiscordMarkdownOnlyHTML(text string) string {
goldmark.WithParser(mdext.ParserWithoutFeatures( text = escapeFixer.ReplaceAllStringFunc(text, escapeReplacement)
parser.NewListParser(), parser.NewListItemParser(), parser.NewHTMLBlockParser(), parser.NewRawHTMLParser(),
parser.NewSetextHeadingParser(), parser.NewATXHeadingParser(), parser.NewThematicBreakParser(),
parser.NewLinkParser(), parser.NewCodeBlockParser(),
)),
goldmark.WithParserOptions(parser.WithBlockParsers(util.Prioritized(defaultIndentableParagraphParser, 500))),
format.HTMLOptions, discordExtensions,
goldmark.WithExtensions(&DiscordTag{portal}),
)
var buf strings.Builder var buf strings.Builder
err := mdRenderer.Convert([]byte(text), &buf) ctx := parser.NewContext()
ctx.Set(parserContextPortal, portal)
err := discordRenderer.Convert([]byte(text), &buf, parser.WithContext(ctx))
if err != nil { if err != nil {
panic(fmt.Errorf("markdown parser errored: %w", err)) panic(fmt.Errorf("markdown parser errored: %w", err))
} }

View file

@ -96,9 +96,11 @@ func (r *discordEveryoneHTMLRenderer) renderDiscordEveryone(w util.BufWriter, so
return return
} }
type DiscordEveryone struct{} type discordEveryone struct{}
func (e *DiscordEveryone) Extend(m goldmark.Markdown) { var ExtDiscordEveryone = &discordEveryone{}
func (e *discordEveryone) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(parser.WithInlineParsers( m.Parser().AddOptions(parser.WithInlineParsers(
util.Prioritized(defaultDiscordEveryoneParser, 600), util.Prioritized(defaultDiscordEveryoneParser, 600),
)) ))

View file

@ -37,7 +37,8 @@ import (
type astDiscordTag struct { type astDiscordTag struct {
ast.BaseInline ast.BaseInline
id int64 portal *Portal
id int64
} }
var _ ast.Node = (*astDiscordTag)(nil) var _ ast.Node = (*astDiscordTag)(nil)
@ -143,7 +144,10 @@ func (s *discordTagParser) Trigger() []byte {
return []byte{'<'} return []byte{'<'}
} }
var parserContextPortal = parser.NewContextKey()
func (s *discordTagParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { func (s *discordTagParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
portal := pc.Get(parserContextPortal).(*Portal)
//before := block.PrecendingCharacter() //before := block.PrecendingCharacter()
line, _ := block.PeekLine() line, _ := block.PeekLine()
match := discordTagRegex.FindSubmatch(line) match := discordTagRegex.FindSubmatch(line)
@ -157,7 +161,7 @@ func (s *discordTagParser) Parse(parent ast.Node, block text.Reader, pc parser.C
if err != nil { if err != nil {
return nil return nil
} }
tag := astDiscordTag{id: id} tag := astDiscordTag{id: id, portal: portal}
tagName := string(match[1]) tagName := string(match[1])
switch { switch {
case tagName == "@": case tagName == "@":
@ -199,9 +203,9 @@ func (s *discordTagParser) CloseBlock(parent ast.Node, pc parser.Context) {
// nothing to do // nothing to do
} }
type discordTagHTMLRenderer struct { type discordTagHTMLRenderer struct{}
portal *Portal
} var defaultDiscordTagHTMLRenderer = &discordTagHTMLRenderer{}
func (r *discordTagHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { func (r *discordTagHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(astKindDiscordTag, r.renderDiscordMention) reg.Register(astKindDiscordTag, r.renderDiscordMention)
@ -259,17 +263,17 @@ func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source [
} }
switch node := n.(type) { switch node := n.(type) {
case *astDiscordUserMention: case *astDiscordUserMention:
puppet := r.portal.bridge.GetPuppetByID(strconv.FormatInt(node.id, 10)) puppet := node.portal.bridge.GetPuppetByID(strconv.FormatInt(node.id, 10))
_, _ = fmt.Fprintf(w, `<a href="https://matrix.to/#/%s">%s</a>`, puppet.MXID, puppet.Name) _, _ = fmt.Fprintf(w, `<a href="https://matrix.to/#/%s">%s</a>`, puppet.MXID, puppet.Name)
return return
case *astDiscordRoleMention: case *astDiscordRoleMention:
role := r.portal.bridge.DB.Role.GetByID(r.portal.GuildID, strconv.FormatInt(node.id, 10)) role := node.portal.bridge.DB.Role.GetByID(node.portal.GuildID, strconv.FormatInt(node.id, 10))
if role != nil { if role != nil {
_, _ = fmt.Fprintf(w, `<font color="#%06x"><strong>@%s</strong></font>`, role.Color, role.Name) _, _ = fmt.Fprintf(w, `<font color="#%06x"><strong>@%s</strong></font>`, role.Color, role.Name)
return return
} }
case *astDiscordChannelMention: case *astDiscordChannelMention:
portal := r.portal.bridge.GetExistingPortalByID(database.PortalKey{ portal := node.portal.bridge.GetExistingPortalByID(database.PortalKey{
ChannelID: strconv.FormatInt(node.id, 10), ChannelID: strconv.FormatInt(node.id, 10),
Receiver: "", Receiver: "",
}) })
@ -282,7 +286,7 @@ func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source [
return return
} }
case *astDiscordCustomEmoji: case *astDiscordCustomEmoji:
reactionMXC := r.portal.getEmojiMXCByDiscordID(strconv.FormatInt(node.id, 10), node.name, node.animated) reactionMXC := node.portal.getEmojiMXCByDiscordID(strconv.FormatInt(node.id, 10), node.name, node.animated)
if !reactionMXC.IsEmpty() { if !reactionMXC.IsEmpty() {
_, _ = fmt.Fprintf(w, `<img data-mx-emoticon src="%[1]s" alt="%[2]s" title="%[2]s" height="32"/>`, reactionMXC.String(), node.name) _, _ = fmt.Fprintf(w, `<img data-mx-emoticon src="%[1]s" alt="%[2]s" title="%[2]s" height="32"/>`, reactionMXC.String(), node.name)
return return
@ -310,15 +314,15 @@ func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source [
return return
} }
type DiscordTag struct { type discordTag struct{}
Portal *Portal
}
func (e *DiscordTag) Extend(m goldmark.Markdown) { var ExtDiscordTag = &discordTag{}
func (e *discordTag) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(parser.WithInlineParsers( m.Parser().AddOptions(parser.WithInlineParsers(
util.Prioritized(defaultDiscordTagParser, 600), util.Prioritized(defaultDiscordTagParser, 600),
)) ))
m.Renderer().AddOptions(renderer.WithNodeRenderers( m.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(&discordTagHTMLRenderer{e.Portal}, 600), util.Prioritized(defaultDiscordTagHTMLRenderer, 600),
)) ))
} }