From e1c2d05bde6e42a86cb90c1c07e882bda313cd8f Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 5 Apr 2025 11:56:48 +0800 Subject: [PATCH] Fix markdown render behaviors (#34122) * Fix #27645 * Add config options `MATH_CODE_BLOCK_DETECTION`, problematic syntaxes are disabled by default * Fix #33639 * Add config options `RENDER_OPTIONS_*`, old behaviors are kept --- custom/conf/app.example.ini | 22 ++- models/renderhelper/repo_comment.go | 4 +- models/renderhelper/repo_file.go | 4 +- models/renderhelper/repo_wiki.go | 1 - models/repo/repo.go | 27 ++-- models/repo/repo_test.go | 6 +- modules/markup/markdown/goldmark.go | 13 +- modules/markup/markdown/markdown.go | 10 +- modules/markup/markdown/markdown_math_test.go | 67 ++++++++- modules/markup/markdown/math/inline_parser.go | 38 ++--- modules/markup/markdown/math/math.go | 21 +-- modules/markup/render.go | 6 +- modules/setting/markup.go | 84 +++++++++-- modules/setting/markup_test.go | 51 +++++++ modules/templates/util_render_test.go | 2 +- modules/util/util.go | 7 + routers/web/repo/actions/view.go | 4 +- routers/web/repo/issue_view.go | 2 +- routers/web/repo/view_file.go | 2 +- templates/repo/branch/list.tmpl | 4 +- templates/repo/commit_page.tmpl | 4 +- templates/repo/commits_list.tmpl | 4 +- templates/repo/commits_list_small.tmpl | 4 +- templates/repo/diff/compare.tmpl | 2 +- templates/repo/graph/commits.tmpl | 2 +- templates/repo/issue/view_title.tmpl | 2 +- templates/repo/latest_commit.tmpl | 4 +- templates/repo/view_list.tmpl | 2 +- templates/user/dashboard/feeds.tmpl | 2 +- web_src/js/markup/asciicast.ts | 25 ++-- web_src/js/markup/codecopy.ts | 16 +- web_src/js/markup/math.ts | 57 +++---- web_src/js/markup/mermaid.ts | 141 +++++++++--------- 33 files changed, 418 insertions(+), 222 deletions(-) create mode 100644 modules/setting/markup_test.go diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 05b7494f96..07a6ebdcf2 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1413,14 +1413,14 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; -;; Render soft line breaks as hard line breaks, which means a single newline character between -;; paragraphs will cause a line break and adding trailing whitespace to paragraphs is not -;; necessary to force a line break. -;; Render soft line breaks as hard line breaks for comments -;ENABLE_HARD_LINE_BREAK_IN_COMMENTS = true -;; -;; Render soft line breaks as hard line breaks for markdown documents -;ENABLE_HARD_LINE_BREAK_IN_DOCUMENTS = false +;; Customize render options for different contexts. Set to "none" to disable the defaults, or use comma separated list: +;; * short-issue-pattern: recognized "#123" issue reference and render it as a link to the issue +;; * new-line-hard-break: render soft line breaks as hard line breaks, which means a single newline character between +;; paragraphs will cause a line break and adding trailing whitespace to paragraphs is not +;; necessary to force a line break. +;RENDER_OPTIONS_COMMENT = short-issue-pattern, new-line-hard-break +;RENDER_OPTIONS_WIKI = short-issue-pattern +;RENDER_OPTIONS_REPO_FILE = ;; ;; Comma separated list of custom URL-Schemes that are allowed as links when rendering Markdown ;; for example git,magnet,ftp (more at https://en.wikipedia.org/wiki/List_of_URI_schemes) @@ -1434,6 +1434,12 @@ LEVEL = Info ;; ;; Enables math inline and block detection ;ENABLE_MATH = true +;; +;; Enable delimiters for math code block detection. Set to "none" to disable all, +;; or use comma separated list: inline-dollar, inline-parentheses, block-dollar, block-square-brackets +;; Defaults to "inline-dollar,block-dollar" to follow GitHub's behavior. +;MATH_CODE_BLOCK_DETECTION = +;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/models/renderhelper/repo_comment.go b/models/renderhelper/repo_comment.go index eab85bf6d4..7c40eded44 100644 --- a/models/renderhelper/repo_comment.go +++ b/models/renderhelper/repo_comment.go @@ -56,7 +56,7 @@ func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repositor if repo != nil { helper.repoLink = repo.Link() helper.commitChecker = newCommitChecker(ctx, repo) - rctx = rctx.WithMetas(repo.ComposeMetas(ctx)) + rctx = rctx.WithMetas(repo.ComposeCommentMetas(ctx)) } else { // this is almost dead code, only to pass the incorrect tests helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName) @@ -64,7 +64,7 @@ func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repositor "user": helper.opts.DeprecatedOwnerName, "repo": helper.opts.DeprecatedRepoName, - "markdownLineBreakStyle": "comment", + "markdownNewLineHardBreak": "true", "markupAllowShortIssuePattern": "true", }) } diff --git a/models/renderhelper/repo_file.go b/models/renderhelper/repo_file.go index 5bf754bf20..e0375ed280 100644 --- a/models/renderhelper/repo_file.go +++ b/models/renderhelper/repo_file.go @@ -61,15 +61,13 @@ func NewRenderContextRepoFile(ctx context.Context, repo *repo_model.Repository, if repo != nil { helper.repoLink = repo.Link() helper.commitChecker = newCommitChecker(ctx, repo) - rctx = rctx.WithMetas(repo.ComposeDocumentMetas(ctx)) + rctx = rctx.WithMetas(repo.ComposeRepoFileMetas(ctx)) } else { // this is almost dead code, only to pass the incorrect tests helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName) rctx = rctx.WithMetas(map[string]string{ "user": helper.opts.DeprecatedOwnerName, "repo": helper.opts.DeprecatedRepoName, - - "markdownLineBreakStyle": "document", }) } rctx = rctx.WithHelper(helper) diff --git a/models/renderhelper/repo_wiki.go b/models/renderhelper/repo_wiki.go index 1e3e07295c..b75f1b9701 100644 --- a/models/renderhelper/repo_wiki.go +++ b/models/renderhelper/repo_wiki.go @@ -68,7 +68,6 @@ func NewRenderContextRepoWiki(ctx context.Context, repo *repo_model.Repository, "user": helper.opts.DeprecatedOwnerName, "repo": helper.opts.DeprecatedRepoName, - "markdownLineBreakStyle": "document", "markupAllowShortIssuePattern": "true", }) } diff --git a/models/repo/repo.go b/models/repo/repo.go index 050be760d5..2977dfb9f1 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -512,15 +512,15 @@ func (repo *Repository) composeCommonMetas(ctx context.Context) map[string]strin "repo": repo.Name, } - unit, err := repo.GetUnit(ctx, unit.TypeExternalTracker) + unitExternalTracker, err := repo.GetUnit(ctx, unit.TypeExternalTracker) if err == nil { - metas["format"] = unit.ExternalTrackerConfig().ExternalTrackerFormat - switch unit.ExternalTrackerConfig().ExternalTrackerStyle { + metas["format"] = unitExternalTracker.ExternalTrackerConfig().ExternalTrackerFormat + switch unitExternalTracker.ExternalTrackerConfig().ExternalTrackerStyle { case markup.IssueNameStyleAlphanumeric: metas["style"] = markup.IssueNameStyleAlphanumeric case markup.IssueNameStyleRegexp: metas["style"] = markup.IssueNameStyleRegexp - metas["regexp"] = unit.ExternalTrackerConfig().ExternalTrackerRegexpPattern + metas["regexp"] = unitExternalTracker.ExternalTrackerConfig().ExternalTrackerRegexpPattern default: metas["style"] = markup.IssueNameStyleNumeric } @@ -544,11 +544,11 @@ func (repo *Repository) composeCommonMetas(ctx context.Context) map[string]strin return repo.commonRenderingMetas } -// ComposeMetas composes a map of metas for properly rendering comments or comment-like contents (commit message) -func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string { +// ComposeCommentMetas composes a map of metas for properly rendering comments or comment-like contents (commit message) +func (repo *Repository) ComposeCommentMetas(ctx context.Context) map[string]string { metas := maps.Clone(repo.composeCommonMetas(ctx)) - metas["markdownLineBreakStyle"] = "comment" - metas["markupAllowShortIssuePattern"] = "true" + metas["markdownNewLineHardBreak"] = strconv.FormatBool(setting.Markdown.RenderOptionsComment.NewLineHardBreak) + metas["markupAllowShortIssuePattern"] = strconv.FormatBool(setting.Markdown.RenderOptionsComment.ShortIssuePattern) return metas } @@ -556,16 +556,17 @@ func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string { func (repo *Repository) ComposeWikiMetas(ctx context.Context) map[string]string { // does wiki need the "teams" and "org" from common metas? metas := maps.Clone(repo.composeCommonMetas(ctx)) - metas["markdownLineBreakStyle"] = "document" - metas["markupAllowShortIssuePattern"] = "true" + metas["markdownNewLineHardBreak"] = strconv.FormatBool(setting.Markdown.RenderOptionsWiki.NewLineHardBreak) + metas["markupAllowShortIssuePattern"] = strconv.FormatBool(setting.Markdown.RenderOptionsWiki.ShortIssuePattern) return metas } -// ComposeDocumentMetas composes a map of metas for properly rendering documents (repo files) -func (repo *Repository) ComposeDocumentMetas(ctx context.Context) map[string]string { +// ComposeRepoFileMetas composes a map of metas for properly rendering documents (repo files) +func (repo *Repository) ComposeRepoFileMetas(ctx context.Context) map[string]string { // does document(file) need the "teams" and "org" from common metas? metas := maps.Clone(repo.composeCommonMetas(ctx)) - metas["markdownLineBreakStyle"] = "document" + metas["markdownNewLineHardBreak"] = strconv.FormatBool(setting.Markdown.RenderOptionsRepoFile.NewLineHardBreak) + metas["markupAllowShortIssuePattern"] = strconv.FormatBool(setting.Markdown.RenderOptionsRepoFile.ShortIssuePattern) return metas } diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go index dffbd18261..b2604ab575 100644 --- a/models/repo/repo_test.go +++ b/models/repo/repo_test.go @@ -86,7 +86,7 @@ func TestMetas(t *testing.T) { repo.Units = nil - metas := repo.ComposeMetas(db.DefaultContext) + metas := repo.ComposeCommentMetas(db.DefaultContext) assert.Equal(t, "testRepo", metas["repo"]) assert.Equal(t, "testOwner", metas["user"]) @@ -100,7 +100,7 @@ func TestMetas(t *testing.T) { testSuccess := func(expectedStyle string) { repo.Units = []*RepoUnit{&externalTracker} repo.commonRenderingMetas = nil - metas := repo.ComposeMetas(db.DefaultContext) + metas := repo.ComposeCommentMetas(db.DefaultContext) assert.Equal(t, expectedStyle, metas["style"]) assert.Equal(t, "testRepo", metas["repo"]) assert.Equal(t, "testOwner", metas["user"]) @@ -121,7 +121,7 @@ func TestMetas(t *testing.T) { repo, err := GetRepositoryByID(db.DefaultContext, 3) assert.NoError(t, err) - metas = repo.ComposeMetas(db.DefaultContext) + metas = repo.ComposeCommentMetas(db.DefaultContext) assert.Contains(t, metas, "org") assert.Contains(t, metas, "teams") assert.Equal(t, "org3", metas["org"]) diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index c2b3389245..b28fa9824e 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -9,7 +9,6 @@ import ( "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/internal" - "code.gitea.io/gitea/modules/setting" "github.com/yuin/goldmark/ast" east "github.com/yuin/goldmark/extension/ast" @@ -69,16 +68,8 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa g.transformList(ctx, v, rc) case *ast.Text: if v.SoftLineBreak() && !v.HardLineBreak() { - // TODO: this was a quite unclear part, old code: `if metas["mode"] != "document" { use comment link break setting }` - // many places render non-comment contents with no mode=document, then these contents also use comment's hard line break setting - // especially in many tests. - markdownLineBreakStyle := ctx.RenderOptions.Metas["markdownLineBreakStyle"] - switch markdownLineBreakStyle { - case "comment": - v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments) - case "document": - v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments) - } + newLineHardBreak := ctx.RenderOptions.Metas["markdownNewLineHardBreak"] == "true" + v.SetHardLineBreak(newLineHardBreak) } case *ast.CodeSpan: g.transformCodeSpan(ctx, v, reader) diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index b102fdac7d..0d7180c6b1 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -126,11 +126,11 @@ func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender { highlighting.WithWrapperRenderer(r.highlightingRenderer), ), math.NewExtension(&ctx.RenderInternal, math.Options{ - Enabled: setting.Markdown.EnableMath, - ParseDollarInline: true, - ParseDollarBlock: true, - ParseSquareBlock: true, // TODO: this is a bad syntax "\[ ... \]", it conflicts with normal markdown escaping, it should be deprecated in the future (by some config options) - // ParseBracketInline: true, // TODO: this is also a bad syntax "\( ... \)", it also conflicts, it should be deprecated in the future + Enabled: setting.Markdown.EnableMath, + ParseInlineDollar: setting.Markdown.MathCodeBlockOptions.ParseInlineDollar, + ParseInlineParentheses: setting.Markdown.MathCodeBlockOptions.ParseInlineParentheses, // this is a bad syntax "\( ... \)", it conflicts with normal markdown escaping + ParseBlockDollar: setting.Markdown.MathCodeBlockOptions.ParseBlockDollar, + ParseBlockSquareBrackets: setting.Markdown.MathCodeBlockOptions.ParseBlockSquareBrackets, // this is a bad syntax "\[ ... \]", it conflicts with normal markdown escaping }), meta.Meta, ), diff --git a/modules/markup/markdown/markdown_math_test.go b/modules/markup/markdown/markdown_math_test.go index 813f050965..a75f18d36a 100644 --- a/modules/markup/markdown/markdown_math_test.go +++ b/modules/markup/markdown/markdown_math_test.go @@ -8,6 +8,8 @@ import ( "testing" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" ) @@ -15,6 +17,7 @@ import ( const nl = "\n" func TestMathRender(t *testing.T) { + setting.Markdown.MathCodeBlockOptions = setting.MarkdownMathCodeBlockOptions{ParseInlineDollar: true, ParseInlineParentheses: true} testcases := []struct { testcase string expected string @@ -69,7 +72,7 @@ func TestMathRender(t *testing.T) { }, { "$$a$$", - `a` + nl, + `

a

` + nl, }, { "$$a$$ test", @@ -111,6 +114,7 @@ func TestMathRender(t *testing.T) { } func TestMathRenderBlockIndent(t *testing.T) { + setting.Markdown.MathCodeBlockOptions = setting.MarkdownMathCodeBlockOptions{ParseBlockDollar: true, ParseBlockSquareBrackets: true} testcases := []struct { name string testcase string @@ -243,3 +247,64 @@ x }) } } + +func TestMathRenderOptions(t *testing.T) { + setting.Markdown.MathCodeBlockOptions = setting.MarkdownMathCodeBlockOptions{} + defer test.MockVariableValue(&setting.Markdown.MathCodeBlockOptions) + test := func(t *testing.T, expected, input string) { + res, err := RenderString(markup.NewTestRenderContext(), input) + assert.NoError(t, err) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(res)), "input: %s", input) + } + + // default (non-conflict) inline syntax + test(t, `

a

`, "$`a`$") + + // ParseInlineDollar + test(t, `

$a$

`, `$a$`) + setting.Markdown.MathCodeBlockOptions.ParseInlineDollar = true + test(t, `

a

`, `$a$`) + + // ParseInlineParentheses + test(t, `

(a)

`, `\(a\)`) + setting.Markdown.MathCodeBlockOptions.ParseInlineParentheses = true + test(t, `

a

`, `\(a\)`) + + // ParseBlockDollar + test(t, `

$$ +a +$$

+`, ` +$$ +a +$$ +`) + setting.Markdown.MathCodeBlockOptions.ParseBlockDollar = true + test(t, `

+a
+
+`, ` +$$ +a +$$ +`) + + // ParseBlockSquareBrackets + test(t, `

[ +a +]

+`, ` +\[ +a +\] +`) + setting.Markdown.MathCodeBlockOptions.ParseBlockSquareBrackets = true + test(t, `

+a
+
+`, ` +\[ +a +\] +`) +} diff --git a/modules/markup/markdown/math/inline_parser.go b/modules/markup/markdown/math/inline_parser.go index d24fd50955..a711d1e1cd 100644 --- a/modules/markup/markdown/math/inline_parser.go +++ b/modules/markup/markdown/math/inline_parser.go @@ -15,26 +15,26 @@ type inlineParser struct { trigger []byte endBytesSingleDollar []byte endBytesDoubleDollar []byte - endBytesBracket []byte + endBytesParentheses []byte + enableInlineDollar bool } -var defaultInlineDollarParser = &inlineParser{ - trigger: []byte{'$'}, - endBytesSingleDollar: []byte{'$'}, - endBytesDoubleDollar: []byte{'$', '$'}, +func NewInlineDollarParser(enableInlineDollar bool) parser.InlineParser { + return &inlineParser{ + trigger: []byte{'$'}, + endBytesSingleDollar: []byte{'$'}, + endBytesDoubleDollar: []byte{'$', '$'}, + enableInlineDollar: enableInlineDollar, + } } -func NewInlineDollarParser() parser.InlineParser { - return defaultInlineDollarParser +var defaultInlineParenthesesParser = &inlineParser{ + trigger: []byte{'\\', '('}, + endBytesParentheses: []byte{'\\', ')'}, } -var defaultInlineBracketParser = &inlineParser{ - trigger: []byte{'\\', '('}, - endBytesBracket: []byte{'\\', ')'}, -} - -func NewInlineBracketParser() parser.InlineParser { - return defaultInlineBracketParser +func NewInlineParenthesesParser() parser.InlineParser { + return defaultInlineParenthesesParser } // Trigger triggers this parser on $ or \ @@ -46,7 +46,7 @@ func isPunctuation(b byte) bool { return b == '.' || b == '!' || b == '?' || b == ',' || b == ';' || b == ':' } -func isBracket(b byte) bool { +func isParenthesesClose(b byte) bool { return b == ')' } @@ -86,7 +86,11 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser. } } else { startMarkLen = 2 - stopMark = parser.endBytesBracket + stopMark = parser.endBytesParentheses + } + + if line[0] == '$' && !parser.enableInlineDollar && (len(line) == 1 || line[1] != '`') { + return nil } if checkSurrounding { @@ -110,7 +114,7 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser. succeedingCharacter = line[i+len(stopMark)] } // check valid ending character - isValidEndingChar := isPunctuation(succeedingCharacter) || isBracket(succeedingCharacter) || + isValidEndingChar := isPunctuation(succeedingCharacter) || isParenthesesClose(succeedingCharacter) || succeedingCharacter == ' ' || succeedingCharacter == '\n' || succeedingCharacter == 0 if checkSurrounding && !isValidEndingChar { break diff --git a/modules/markup/markdown/math/math.go b/modules/markup/markdown/math/math.go index a6ff593d62..4b74db2d76 100644 --- a/modules/markup/markdown/math/math.go +++ b/modules/markup/markdown/math/math.go @@ -14,10 +14,11 @@ import ( ) type Options struct { - Enabled bool - ParseDollarInline bool - ParseDollarBlock bool - ParseSquareBlock bool + Enabled bool + ParseInlineDollar bool // inline $$ xxx $$ text + ParseInlineParentheses bool // inline \( xxx \) text + ParseBlockDollar bool // block $$ multiple-line $$ text + ParseBlockSquareBrackets bool // block \[ multiple-line \] text } // Extension is a math extension @@ -42,16 +43,16 @@ func (e *Extension) Extend(m goldmark.Markdown) { return } - inlines := []util.PrioritizedValue{util.Prioritized(NewInlineBracketParser(), 501)} - if e.options.ParseDollarInline { - inlines = append(inlines, util.Prioritized(NewInlineDollarParser(), 502)) + var inlines []util.PrioritizedValue + if e.options.ParseInlineParentheses { + inlines = append(inlines, util.Prioritized(NewInlineParenthesesParser(), 501)) } + inlines = append(inlines, util.Prioritized(NewInlineDollarParser(e.options.ParseInlineDollar), 502)) + m.Parser().AddOptions(parser.WithInlineParsers(inlines...)) - m.Parser().AddOptions(parser.WithBlockParsers( - util.Prioritized(NewBlockParser(e.options.ParseDollarBlock, e.options.ParseSquareBlock), 701), + util.Prioritized(NewBlockParser(e.options.ParseBlockDollar, e.options.ParseBlockSquareBrackets), 701), )) - m.Renderer().AddOptions(renderer.WithNodeRenderers( util.Prioritized(NewBlockRenderer(e.renderInternal), 501), util.Prioritized(NewInlineRenderer(e.renderInternal), 502), diff --git a/modules/markup/render.go b/modules/markup/render.go index eb621b30a7..79f1f473c2 100644 --- a/modules/markup/render.go +++ b/modules/markup/render.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "net/url" + "strconv" "strings" "time" @@ -46,7 +47,7 @@ type RenderOptions struct { // user&repo, format&style®exp (for external issue pattern), teams&org (for mention) // RefTypeNameSubURL (for iframe&asciicast) // markupAllowShortIssuePattern - // markdownLineBreakStyle (comment, document) + // markdownNewLineHardBreak Metas map[string]string // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page @@ -247,7 +248,8 @@ func Init(renderHelpFuncs *RenderHelperFuncs) { } func ComposeSimpleDocumentMetas() map[string]string { - return map[string]string{"markdownLineBreakStyle": "document"} + // TODO: there is no separate config option for "simple document" rendering, so temporarily use the same config as "repo file" + return map[string]string{"markdownNewLineHardBreak": strconv.FormatBool(setting.Markdown.RenderOptionsRepoFile.NewLineHardBreak)} } type TestRenderHelper struct { diff --git a/modules/setting/markup.go b/modules/setting/markup.go index dfce8afa77..3bd368f831 100644 --- a/modules/setting/markup.go +++ b/modules/setting/markup.go @@ -8,6 +8,7 @@ import ( "strings" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" ) // ExternalMarkupRenderers represents the external markup renderers @@ -23,18 +24,33 @@ const ( RenderContentModeIframe = "iframe" ) +type MarkdownRenderOptions struct { + NewLineHardBreak bool + ShortIssuePattern bool // Actually it is a "markup" option because it is used in "post processor" +} + +type MarkdownMathCodeBlockOptions struct { + ParseInlineDollar bool + ParseInlineParentheses bool + ParseBlockDollar bool + ParseBlockSquareBrackets bool +} + // Markdown settings var Markdown = struct { - EnableHardLineBreakInComments bool - EnableHardLineBreakInDocuments bool - CustomURLSchemes []string `ini:"CUSTOM_URL_SCHEMES"` - FileExtensions []string - EnableMath bool + RenderOptionsComment MarkdownRenderOptions `ini:"-"` + RenderOptionsWiki MarkdownRenderOptions `ini:"-"` + RenderOptionsRepoFile MarkdownRenderOptions `ini:"-"` + + CustomURLSchemes []string `ini:"CUSTOM_URL_SCHEMES"` // Actually it is a "markup" option because it is used in "post processor" + FileExtensions []string + + EnableMath bool + MathCodeBlockDetection []string + MathCodeBlockOptions MarkdownMathCodeBlockOptions `ini:"-"` }{ - EnableHardLineBreakInComments: true, - EnableHardLineBreakInDocuments: false, - FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd,.livemd", ","), - EnableMath: true, + FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd,.livemd", ","), + EnableMath: true, } // MarkupRenderer defines the external parser configured in ini @@ -60,6 +76,56 @@ type MarkupSanitizerRule struct { func loadMarkupFrom(rootCfg ConfigProvider) { mustMapSetting(rootCfg, "markdown", &Markdown) + const none = "none" + + const renderOptionShortIssuePattern = "short-issue-pattern" + const renderOptionNewLineHardBreak = "new-line-hard-break" + cfgMarkdown := rootCfg.Section("markdown") + parseMarkdownRenderOptions := func(key string, defaults []string) (ret MarkdownRenderOptions) { + options := cfgMarkdown.Key(key).Strings(",") + options = util.IfEmpty(options, defaults) + for _, opt := range options { + switch opt { + case renderOptionShortIssuePattern: + ret.ShortIssuePattern = true + case renderOptionNewLineHardBreak: + ret.NewLineHardBreak = true + case none: + ret = MarkdownRenderOptions{} + case "": + default: + log.Error("Unknown markdown render option in %s: %s", key, opt) + } + } + return ret + } + Markdown.RenderOptionsComment = parseMarkdownRenderOptions("RENDER_OPTIONS_COMMENT", []string{renderOptionShortIssuePattern, renderOptionNewLineHardBreak}) + Markdown.RenderOptionsWiki = parseMarkdownRenderOptions("RENDER_OPTIONS_WIKI", []string{renderOptionShortIssuePattern}) + Markdown.RenderOptionsRepoFile = parseMarkdownRenderOptions("RENDER_OPTIONS_REPO_FILE", nil) + + const mathCodeInlineDollar = "inline-dollar" + const mathCodeInlineParentheses = "inline-parentheses" + const mathCodeBlockDollar = "block-dollar" + const mathCodeBlockSquareBrackets = "block-square-brackets" + Markdown.MathCodeBlockDetection = util.IfEmpty(Markdown.MathCodeBlockDetection, []string{mathCodeInlineDollar, mathCodeBlockDollar}) + Markdown.MathCodeBlockOptions = MarkdownMathCodeBlockOptions{} + for _, s := range Markdown.MathCodeBlockDetection { + switch s { + case mathCodeInlineDollar: + Markdown.MathCodeBlockOptions.ParseInlineDollar = true + case mathCodeInlineParentheses: + Markdown.MathCodeBlockOptions.ParseInlineParentheses = true + case mathCodeBlockDollar: + Markdown.MathCodeBlockOptions.ParseBlockDollar = true + case mathCodeBlockSquareBrackets: + Markdown.MathCodeBlockOptions.ParseBlockSquareBrackets = true + case none: + Markdown.MathCodeBlockOptions = MarkdownMathCodeBlockOptions{} + case "": + default: + log.Error("Unknown math code block detection option: %s", s) + } + } MermaidMaxSourceCharacters = rootCfg.Section("markup").Key("MERMAID_MAX_SOURCE_CHARACTERS").MustInt(5000) ExternalMarkupRenderers = make([]*MarkupRenderer, 0, 10) diff --git a/modules/setting/markup_test.go b/modules/setting/markup_test.go new file mode 100644 index 0000000000..c47a38ce15 --- /dev/null +++ b/modules/setting/markup_test.go @@ -0,0 +1,51 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLoadMarkup(t *testing.T) { + cfg, _ := NewConfigProviderFromData(``) + loadMarkupFrom(cfg) + assert.Equal(t, MarkdownMathCodeBlockOptions{ParseInlineDollar: true, ParseBlockDollar: true}, Markdown.MathCodeBlockOptions) + assert.Equal(t, MarkdownRenderOptions{NewLineHardBreak: true, ShortIssuePattern: true}, Markdown.RenderOptionsComment) + assert.Equal(t, MarkdownRenderOptions{ShortIssuePattern: true}, Markdown.RenderOptionsWiki) + assert.Equal(t, MarkdownRenderOptions{}, Markdown.RenderOptionsRepoFile) + + t.Run("Math", func(t *testing.T) { + cfg, _ = NewConfigProviderFromData(` +[markdown] +MATH_CODE_BLOCK_DETECTION = none +`) + loadMarkupFrom(cfg) + assert.Equal(t, MarkdownMathCodeBlockOptions{}, Markdown.MathCodeBlockOptions) + + cfg, _ = NewConfigProviderFromData(` +[markdown] +MATH_CODE_BLOCK_DETECTION = inline-dollar, inline-parentheses, block-dollar, block-square-brackets +`) + loadMarkupFrom(cfg) + assert.Equal(t, MarkdownMathCodeBlockOptions{ParseInlineDollar: true, ParseInlineParentheses: true, ParseBlockDollar: true, ParseBlockSquareBrackets: true}, Markdown.MathCodeBlockOptions) + }) + + t.Run("Render", func(t *testing.T) { + cfg, _ = NewConfigProviderFromData(` +[markdown] +RENDER_OPTIONS_COMMENT = none +`) + loadMarkupFrom(cfg) + assert.Equal(t, MarkdownRenderOptions{}, Markdown.RenderOptionsComment) + + cfg, _ = NewConfigProviderFromData(` +[markdown] +RENDER_OPTIONS_REPO_FILE = short-issue-pattern, new-line-hard-break +`) + loadMarkupFrom(cfg) + assert.Equal(t, MarkdownRenderOptions{NewLineHardBreak: true, ShortIssuePattern: true}, Markdown.RenderOptionsRepoFile) + }) +} diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go index 7b53fdb1e2..26cd1eb348 100644 --- a/modules/templates/util_render_test.go +++ b/modules/templates/util_render_test.go @@ -51,7 +51,7 @@ var testMetas = map[string]string{ "user": "user13", "repo": "repo11", "repoPath": "../../tests/gitea-repositories-meta/user13/repo11.git/", - "markdownLineBreakStyle": "comment", + "markdownNewLineHardBreak": "true", "markupAllowShortIssuePattern": "true", } diff --git a/modules/util/util.go b/modules/util/util.go index 72fcddbe13..dd8e073888 100644 --- a/modules/util/util.go +++ b/modules/util/util.go @@ -219,6 +219,13 @@ func IfZero[T comparable](v, def T) T { return v } +func IfEmpty[T any](v, def []T) []T { + if len(v) == 0 { + return def + } + return v +} + // OptionalArg helps the "optional argument" in Golang: // // func foo(optArg ...int) { return OptionalArg(optArg) } diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index ec06c9233a..2ec6389263 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -200,9 +200,9 @@ func ViewPost(ctx *context_module.Context) { } } - // TODO: "ComposeMetas" (usually for comment) is not quite right, but it is still the same as what template "RenderCommitMessage" does. + // TODO: "ComposeCommentMetas" (usually for comment) is not quite right, but it is still the same as what template "RenderCommitMessage" does. // need to be refactored together in the future - metas := ctx.Repo.Repository.ComposeMetas(ctx) + metas := ctx.Repo.Repository.ComposeCommentMetas(ctx) // the title for the "run" is from the commit message resp.State.Run.Title = run.Title diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go index b312f1260a..3ffcdfe676 100644 --- a/routers/web/repo/issue_view.go +++ b/routers/web/repo/issue_view.go @@ -278,7 +278,7 @@ func ViewIssue(ctx *context.Context) { extIssueUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker) if err == nil && extIssueUnit != nil { if extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == markup.IssueNameStyleNumeric || extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == "" { - metas := ctx.Repo.Repository.ComposeMetas(ctx) + metas := ctx.Repo.Repository.ComposeCommentMetas(ctx) metas["index"] = ctx.PathParam("index") res, err := vars.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas) if err != nil { diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index 3edba8e06e..12083a1ced 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -176,7 +176,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { if markupType != "" && !shouldRenderSource { ctx.Data["IsMarkup"] = true ctx.Data["MarkupType"] = markupType - metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx) + metas := ctx.Repo.Repository.ComposeRepoFileMetas(ctx) metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL() rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ CurrentRefPath: ctx.Repo.RefTypeNameSubURL(), diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index c0accf16fa..19797229bf 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -27,7 +27,7 @@ {{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DefaultBranchBranch.DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DefaultBranchBranch.DBBranch.CommitID)}} -

{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}} · {{ctx.RenderUtils.RenderCommitMessage .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeMetas ctx)}} · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DefaultBranchBranch.DBBranch.CommitTime}}{{if .DefaultBranchBranch.DBBranch.Pusher}}  {{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}

+

{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}} · {{ctx.RenderUtils.RenderCommitMessage .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeCommentMetas ctx)}} · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DefaultBranchBranch.DBBranch.CommitTime}}{{if .DefaultBranchBranch.DBBranch.Pusher}}  {{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}

{{/* FIXME: here and below, the tw-overflow-visible is not quite right but it is still needed the moment: to show the important buttons when the width is narrow */}} @@ -103,7 +103,7 @@ {{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}} -

{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .DBBranch.CommitID}} · {{ctx.RenderUtils.RenderCommitMessage .DBBranch.CommitMessage ($.Repository.ComposeMetas ctx)}} · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DBBranch.CommitTime}}{{if .DBBranch.Pusher}}  {{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}}  {{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}

+

{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .DBBranch.CommitID}} · {{ctx.RenderUtils.RenderCommitMessage .DBBranch.CommitMessage ($.Repository.ComposeCommentMetas ctx)}} · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DBBranch.CommitTime}}{{if .DBBranch.Pusher}}  {{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}}  {{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}

{{end}} diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index ff3dccd534..5639c87a82 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -5,7 +5,7 @@
-

{{ctx.RenderUtils.RenderCommitMessage .Commit.Message ($.Repository.ComposeMetas ctx)}}{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}

+

{{ctx.RenderUtils.RenderCommitMessage .Commit.Message ($.Repository.ComposeCommentMetas ctx)}}{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}

{{if not $.PageIsWiki}} {{if IsMultilineCommitMessage .Commit.Message}} -
{{ctx.RenderUtils.RenderCommitBody .Commit.Message ($.Repository.ComposeMetas ctx)}}
+
{{ctx.RenderUtils.RenderCommitBody .Commit.Message ($.Repository.ComposeCommentMetas ctx)}}
{{end}} {{template "repo/commit_load_branches_and_tags" .}}
diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl index e1bd6b73ae..50e6708dcf 100644 --- a/templates/repo/commits_list.tmpl +++ b/templates/repo/commits_list.tmpl @@ -44,7 +44,7 @@ {{.Summary | ctx.RenderUtils.RenderEmoji}} {{else}} {{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String)}} - {{ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.Repository.ComposeMetas ctx)}} + {{ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.Repository.ComposeCommentMetas ctx)}} {{end}} {{if IsMultilineCommitMessage .Message}} @@ -52,7 +52,7 @@ {{end}} {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} {{if IsMultilineCommitMessage .Message}} -
{{ctx.RenderUtils.RenderCommitBody .Message ($.Repository.ComposeMetas ctx)}}
+
{{ctx.RenderUtils.RenderCommitBody .Message ($.Repository.ComposeCommentMetas ctx)}}
{{end}} {{if $.CommitsTagsMap}} {{range (index $.CommitsTagsMap .ID.String)}} diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl index 2acf7c58b8..b054ce19a5 100644 --- a/templates/repo/commits_list_small.tmpl +++ b/templates/repo/commits_list_small.tmpl @@ -15,7 +15,7 @@ {{$commitLink:= printf "%s/%s" $commitBaseLink (PathEscape .ID.String)}} - {{- ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx) -}} + {{- ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeCommentMetas ctx) -}} {{if IsMultilineCommitMessage .Message}} @@ -29,7 +29,7 @@
{{if IsMultilineCommitMessage .Message}}
-		{{- ctx.RenderUtils.RenderCommitBody .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx) -}}
+		{{- ctx.RenderUtils.RenderCommitBody .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeCommentMetas ctx) -}}
 	
{{end}} {{end}} diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl index 05cfffd2b7..0c9e3f0698 100644 --- a/templates/repo/diff/compare.tmpl +++ b/templates/repo/diff/compare.tmpl @@ -189,7 +189,7 @@
{{template "shared/issueicon" .}}
- {{ctx.RenderUtils.RenderIssueTitle .PullRequest.Issue.Title ($.Repository.ComposeMetas ctx)}} + {{ctx.RenderUtils.RenderIssueTitle .PullRequest.Issue.Title ($.Repository.ComposeCommentMetas ctx)}} #{{.PullRequest.Issue.Index}}
diff --git a/templates/repo/graph/commits.tmpl b/templates/repo/graph/commits.tmpl index 6af0ba1f0f..55a91ac195 100644 --- a/templates/repo/graph/commits.tmpl +++ b/templates/repo/graph/commits.tmpl @@ -8,7 +8,7 @@ {{template "repo/commit_sign_badge" dict "Commit" $commit.Commit "CommitBaseLink" (print $.RepoLink "/commit") "CommitSignVerification" $commit.Verification}} - {{ctx.RenderUtils.RenderCommitMessage $commit.Subject ($.Repository.ComposeMetas ctx)}} + {{ctx.RenderUtils.RenderCommitMessage $commit.Subject ($.Repository.ComposeCommentMetas ctx)}} diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl index dcc1f48c2c..a4be598540 100644 --- a/templates/repo/issue/view_title.tmpl +++ b/templates/repo/issue/view_title.tmpl @@ -13,7 +13,7 @@ {{$canEditIssueTitle := and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}

- {{ctx.RenderUtils.RenderIssueTitle .Issue.Title ($.Repository.ComposeMetas ctx)}} + {{ctx.RenderUtils.RenderIssueTitle .Issue.Title ($.Repository.ComposeCommentMetas ctx)}} #{{.Issue.Index}}

diff --git a/templates/repo/latest_commit.tmpl b/templates/repo/latest_commit.tmpl index 0341d60eb2..da457e423a 100644 --- a/templates/repo/latest_commit.tmpl +++ b/templates/repo/latest_commit.tmpl @@ -21,10 +21,10 @@ {{template "repo/commit_statuses" dict "Status" .LatestCommitStatus "Statuses" .LatestCommitStatuses}} {{$commitLink:= printf "%s/commit/%s" .RepoLink (PathEscape .LatestCommit.ID.String)}} - {{ctx.RenderUtils.RenderCommitMessageLinkSubject .LatestCommit.Message $commitLink ($.Repository.ComposeMetas ctx)}} + {{ctx.RenderUtils.RenderCommitMessageLinkSubject .LatestCommit.Message $commitLink ($.Repository.ComposeCommentMetas ctx)}} {{if IsMultilineCommitMessage .LatestCommit.Message}} -
{{ctx.RenderUtils.RenderCommitBody .LatestCommit.Message ($.Repository.ComposeMetas ctx)}}
+
{{ctx.RenderUtils.RenderCommitBody .LatestCommit.Message ($.Repository.ComposeCommentMetas ctx)}}
{{end}}
{{end}} diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index 4745110dd2..612a2405e4 100644 --- a/templates/repo/view_list.tmpl +++ b/templates/repo/view_list.tmpl @@ -46,7 +46,7 @@
{{end}} diff --git a/web_src/js/markup/asciicast.ts b/web_src/js/markup/asciicast.ts index 22dbff2d46..125bba447b 100644 --- a/web_src/js/markup/asciicast.ts +++ b/web_src/js/markup/asciicast.ts @@ -1,16 +1,17 @@ +import {queryElems} from '../utils/dom.ts'; + export async function initMarkupRenderAsciicast(elMarkup: HTMLElement): Promise { - const el = elMarkup.querySelector('.asciinema-player-container'); - if (!el) return; + queryElems(elMarkup, '.asciinema-player-container', async (el) => { + const [player] = await Promise.all([ + // @ts-expect-error: module exports no types + import(/* webpackChunkName: "asciinema-player" */'asciinema-player'), + import(/* webpackChunkName: "asciinema-player" */'asciinema-player/dist/bundle/asciinema-player.css'), + ]); - const [player] = await Promise.all([ - // @ts-expect-error: module exports no types - import(/* webpackChunkName: "asciinema-player" */'asciinema-player'), - import(/* webpackChunkName: "asciinema-player" */'asciinema-player/dist/bundle/asciinema-player.css'), - ]); - - player.create(el.getAttribute('data-asciinema-player-src'), el, { - // poster (a preview frame) to display until the playback is started. - // Set it to 1 hour (also means the end if the video is shorter) to make the preview frame show more. - poster: 'npt:1:0:0', + player.create(el.getAttribute('data-asciinema-player-src'), el, { + // poster (a preview frame) to display until the playback is started. + // Set it to 1 hour (also means the end if the video is shorter) to make the preview frame show more. + poster: 'npt:1:0:0', + }); }); } diff --git a/web_src/js/markup/codecopy.ts b/web_src/js/markup/codecopy.ts index 4430256848..67284bad55 100644 --- a/web_src/js/markup/codecopy.ts +++ b/web_src/js/markup/codecopy.ts @@ -1,4 +1,5 @@ import {svg} from '../svg.ts'; +import {queryElems} from '../utils/dom.ts'; export function makeCodeCopyButton(): HTMLButtonElement { const button = document.createElement('button'); @@ -8,11 +9,12 @@ export function makeCodeCopyButton(): HTMLButtonElement { } export function initMarkupCodeCopy(elMarkup: HTMLElement): void { - const el = elMarkup.querySelector('.code-block code'); // .markup .code-block code - if (!el || !el.textContent) return; - - const btn = makeCodeCopyButton(); - // remove final trailing newline introduced during HTML rendering - btn.setAttribute('data-clipboard-text', el.textContent.replace(/\r?\n$/, '')); - el.after(btn); + // .markup .code-block code + queryElems(elMarkup, '.code-block code', (el) => { + if (!el.textContent) return; + const btn = makeCodeCopyButton(); + // remove final trailing newline introduced during HTML rendering + btn.setAttribute('data-clipboard-text', el.textContent.replace(/\r?\n$/, '')); + el.after(btn); + }); } diff --git a/web_src/js/markup/math.ts b/web_src/js/markup/math.ts index 2a4468bf2e..bc118137a1 100644 --- a/web_src/js/markup/math.ts +++ b/web_src/js/markup/math.ts @@ -1,4 +1,5 @@ import {displayError} from './common.ts'; +import {queryElems} from '../utils/dom.ts'; function targetElement(el: Element): {target: Element, displayAsBlock: boolean} { // The target element is either the parent "code block with loading indicator", or itself @@ -12,35 +13,35 @@ function targetElement(el: Element): {target: Element, displayAsBlock: boolean} } export async function initMarkupCodeMath(elMarkup: HTMLElement): Promise { - const el = elMarkup.querySelector('code.language-math'); // .markup code.language-math' - if (!el) return; + // .markup code.language-math' + queryElems(elMarkup, 'code.language-math', async (el) => { + const [{default: katex}] = await Promise.all([ + import(/* webpackChunkName: "katex" */'katex'), + import(/* webpackChunkName: "katex" */'katex/dist/katex.css'), + ]); - const [{default: katex}] = await Promise.all([ - import(/* webpackChunkName: "katex" */'katex'), - import(/* webpackChunkName: "katex" */'katex/dist/katex.css'), - ]); + const MAX_CHARS = 1000; + const MAX_SIZE = 25; + const MAX_EXPAND = 1000; - const MAX_CHARS = 1000; - const MAX_SIZE = 25; - const MAX_EXPAND = 1000; + const {target, displayAsBlock} = targetElement(el); + if (target.hasAttribute('data-render-done')) return; + const source = el.textContent; - const {target, displayAsBlock} = targetElement(el); - if (target.hasAttribute('data-render-done')) return; - const source = el.textContent; - - if (source.length > MAX_CHARS) { - displayError(target, new Error(`Math source of ${source.length} characters exceeds the maximum allowed length of ${MAX_CHARS}.`)); - return; - } - try { - const tempEl = document.createElement(displayAsBlock ? 'p' : 'span'); - katex.render(source, tempEl, { - maxSize: MAX_SIZE, - maxExpand: MAX_EXPAND, - displayMode: displayAsBlock, // katex: true for display (block) mode, false for inline mode - }); - target.replaceWith(tempEl); - } catch (error) { - displayError(target, error); - } + if (source.length > MAX_CHARS) { + displayError(target, new Error(`Math source of ${source.length} characters exceeds the maximum allowed length of ${MAX_CHARS}.`)); + return; + } + try { + const tempEl = document.createElement(displayAsBlock ? 'p' : 'span'); + katex.render(source, tempEl, { + maxSize: MAX_SIZE, + maxExpand: MAX_EXPAND, + displayMode: displayAsBlock, // katex: true for display (block) mode, false for inline mode + }); + target.replaceWith(tempEl); + } catch (error) { + displayError(target, error); + } + }); } diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index b4bf3153ea..ac24b3bcba 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -1,6 +1,7 @@ import {isDarkTheme} from '../utils.ts'; import {makeCodeCopyButton} from './codecopy.ts'; import {displayError} from './common.ts'; +import {queryElems} from '../utils/dom.ts'; const {mermaidMaxSourceCharacters} = window.config; @@ -11,77 +12,77 @@ body {margin: 0; padding: 0; overflow: hidden} blockquote, dd, dl, figure, h1, h2, h3, h4, h5, h6, hr, p, pre {margin: 0}`; export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise { - const el = elMarkup.querySelector('code.language-mermaid'); // .markup code.language-mermaid - if (!el) return; + // .markup code.language-mermaid + queryElems(elMarkup, 'code.language-mermaid', async (el) => { + const {default: mermaid} = await import(/* webpackChunkName: "mermaid" */'mermaid'); - const {default: mermaid} = await import(/* webpackChunkName: "mermaid" */'mermaid'); - - mermaid.initialize({ - startOnLoad: false, - theme: isDarkTheme() ? 'dark' : 'neutral', - securityLevel: 'strict', - suppressErrorRendering: true, - }); - - const pre = el.closest('pre'); - if (pre.hasAttribute('data-render-done')) return; - - const source = el.textContent; - if (mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters) { - displayError(pre, new Error(`Mermaid source of ${source.length} characters exceeds the maximum allowed length of ${mermaidMaxSourceCharacters}.`)); - return; - } - - try { - await mermaid.parse(source); - } catch (err) { - displayError(pre, err); - return; - } - - try { - // can't use bindFunctions here because we can't cross the iframe boundary. This - // means js-based interactions won't work but they aren't intended to work either - const {svg} = await mermaid.render('mermaid', source); - - const iframe = document.createElement('iframe'); - iframe.classList.add('markup-content-iframe', 'tw-invisible'); - iframe.srcdoc = `${svg}`; - - const mermaidBlock = document.createElement('div'); - mermaidBlock.classList.add('mermaid-block', 'is-loading', 'tw-hidden'); - mermaidBlock.append(iframe); - - const btn = makeCodeCopyButton(); - btn.setAttribute('data-clipboard-text', source); - mermaidBlock.append(btn); - - const updateIframeHeight = () => { - const body = iframe.contentWindow?.document?.body; - if (body) { - iframe.style.height = `${body.clientHeight}px`; - } - }; - - iframe.addEventListener('load', () => { - pre.replaceWith(mermaidBlock); - mermaidBlock.classList.remove('tw-hidden'); - updateIframeHeight(); - setTimeout(() => { // avoid flash of iframe background - mermaidBlock.classList.remove('is-loading'); - iframe.classList.remove('tw-invisible'); - }, 0); - - // update height when element's visibility state changes, for example when the diagram is inside - // a
+ block and the
block becomes visible upon user interaction, it - // would initially set a incorrect height and the correct height is set during this callback. - (new IntersectionObserver(() => { - updateIframeHeight(); - }, {root: document.documentElement})).observe(iframe); + mermaid.initialize({ + startOnLoad: false, + theme: isDarkTheme() ? 'dark' : 'neutral', + securityLevel: 'strict', + suppressErrorRendering: true, }); - document.body.append(mermaidBlock); - } catch (err) { - displayError(pre, err); - } + const pre = el.closest('pre'); + if (pre.hasAttribute('data-render-done')) return; + + const source = el.textContent; + if (mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters) { + displayError(pre, new Error(`Mermaid source of ${source.length} characters exceeds the maximum allowed length of ${mermaidMaxSourceCharacters}.`)); + return; + } + + try { + await mermaid.parse(source); + } catch (err) { + displayError(pre, err); + return; + } + + try { + // can't use bindFunctions here because we can't cross the iframe boundary. This + // means js-based interactions won't work but they aren't intended to work either + const {svg} = await mermaid.render('mermaid', source); + + const iframe = document.createElement('iframe'); + iframe.classList.add('markup-content-iframe', 'tw-invisible'); + iframe.srcdoc = `${svg}`; + + const mermaidBlock = document.createElement('div'); + mermaidBlock.classList.add('mermaid-block', 'is-loading', 'tw-hidden'); + mermaidBlock.append(iframe); + + const btn = makeCodeCopyButton(); + btn.setAttribute('data-clipboard-text', source); + mermaidBlock.append(btn); + + const updateIframeHeight = () => { + const body = iframe.contentWindow?.document?.body; + if (body) { + iframe.style.height = `${body.clientHeight}px`; + } + }; + + iframe.addEventListener('load', () => { + pre.replaceWith(mermaidBlock); + mermaidBlock.classList.remove('tw-hidden'); + updateIframeHeight(); + setTimeout(() => { // avoid flash of iframe background + mermaidBlock.classList.remove('is-loading'); + iframe.classList.remove('tw-invisible'); + }, 0); + + // update height when element's visibility state changes, for example when the diagram is inside + // a
+ block and the
block becomes visible upon user interaction, it + // would initially set a incorrect height and the correct height is set during this callback. + (new IntersectionObserver(() => { + updateIframeHeight(); + }, {root: document.documentElement})).observe(iframe); + }); + + document.body.append(mermaidBlock); + } catch (err) { + displayError(pre, err); + } + }); }