From e7615ef4be5f6cbbc31349ceffa589099a62b9d9 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 30 Jan 2023 00:42:20 +0200 Subject: [PATCH] Refactor tag rendering to avoid recreating goldmark instance for each message --- formatter.go | 39 +++++++++++++++++++++++---------------- formatter_everyone.go | 6 ++++-- formatter_tag.go | 32 ++++++++++++++++++-------------- 3 files changed, 45 insertions(+), 32 deletions(-) diff --git a/formatter.go b/formatter.go index 406806e..f040173 100644 --- a/formatter.go +++ b/formatter.go @@ -33,9 +33,16 @@ import ( "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(`\\(__[^_]|\*\*[^*])`) +func escapeReplacement(s string) string { + return s[:2] + `\` + s[2:] +} + // indentableParagraphParser is the default paragraph parser with CanAcceptIndentedLine. // Used when disabling CodeBlockParser (as disabling it without a replacement will make indented blocks disappear). type indentableParagraphParser struct { @@ -48,24 +55,24 @@ func (b *indentableParagraphParser) CanAcceptIndentedLine() bool { return true } -func (portal *Portal) renderDiscordMarkdownOnlyHTML(text string) string { - text = escapeFixer.ReplaceAllStringFunc(text, func(s string) string { - return s[:2] + `\` + s[2:] - }) +var discordRenderer = goldmark.New( + goldmark.WithParser(mdext.ParserWithoutFeatures( + 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( - goldmark.WithParser(mdext.ParserWithoutFeatures( - 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}), - ) +func (portal *Portal) renderDiscordMarkdownOnlyHTML(text string) string { + text = escapeFixer.ReplaceAllStringFunc(text, escapeReplacement) 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 { panic(fmt.Errorf("markdown parser errored: %w", err)) } diff --git a/formatter_everyone.go b/formatter_everyone.go index a3a5cf8..b1aed5a 100644 --- a/formatter_everyone.go +++ b/formatter_everyone.go @@ -96,9 +96,11 @@ func (r *discordEveryoneHTMLRenderer) renderDiscordEveryone(w util.BufWriter, so 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( util.Prioritized(defaultDiscordEveryoneParser, 600), )) diff --git a/formatter_tag.go b/formatter_tag.go index 0326d5f..46701e5 100644 --- a/formatter_tag.go +++ b/formatter_tag.go @@ -37,7 +37,8 @@ import ( type astDiscordTag struct { ast.BaseInline - id int64 + portal *Portal + id int64 } var _ ast.Node = (*astDiscordTag)(nil) @@ -143,7 +144,10 @@ func (s *discordTagParser) Trigger() []byte { return []byte{'<'} } +var parserContextPortal = parser.NewContextKey() + func (s *discordTagParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { + portal := pc.Get(parserContextPortal).(*Portal) //before := block.PrecendingCharacter() line, _ := block.PeekLine() match := discordTagRegex.FindSubmatch(line) @@ -157,7 +161,7 @@ func (s *discordTagParser) Parse(parent ast.Node, block text.Reader, pc parser.C if err != nil { return nil } - tag := astDiscordTag{id: id} + tag := astDiscordTag{id: id, portal: portal} tagName := string(match[1]) switch { case tagName == "@": @@ -199,9 +203,9 @@ func (s *discordTagParser) CloseBlock(parent ast.Node, pc parser.Context) { // nothing to do } -type discordTagHTMLRenderer struct { - portal *Portal -} +type discordTagHTMLRenderer struct{} + +var defaultDiscordTagHTMLRenderer = &discordTagHTMLRenderer{} func (r *discordTagHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { reg.Register(astKindDiscordTag, r.renderDiscordMention) @@ -259,17 +263,17 @@ func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source [ } switch node := n.(type) { 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, `%s`, puppet.MXID, puppet.Name) return 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 { _, _ = fmt.Fprintf(w, `@%s`, role.Color, role.Name) return } case *astDiscordChannelMention: - portal := r.portal.bridge.GetExistingPortalByID(database.PortalKey{ + portal := node.portal.bridge.GetExistingPortalByID(database.PortalKey{ ChannelID: strconv.FormatInt(node.id, 10), Receiver: "", }) @@ -282,7 +286,7 @@ func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source [ return } 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() { _, _ = fmt.Fprintf(w, `%[2]s`, reactionMXC.String(), node.name) return @@ -310,15 +314,15 @@ func (r *discordTagHTMLRenderer) renderDiscordMention(w util.BufWriter, source [ return } -type DiscordTag struct { - Portal *Portal -} +type discordTag struct{} -func (e *DiscordTag) Extend(m goldmark.Markdown) { +var ExtDiscordTag = &discordTag{} + +func (e *discordTag) Extend(m goldmark.Markdown) { m.Parser().AddOptions(parser.WithInlineParsers( util.Prioritized(defaultDiscordTagParser, 600), )) m.Renderer().AddOptions(renderer.WithNodeRenderers( - util.Prioritized(&discordTagHTMLRenderer{e.Portal}, 600), + util.Prioritized(defaultDiscordTagHTMLRenderer, 600), )) }