mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 19:06:18 +01:00 
			
		
		
		
	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
This commit is contained in:
		| @@ -1413,14 +1413,14 @@ LEVEL = Info | |||||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||||
| ;; | ;; | ||||||
| ;; Render soft line breaks as hard line breaks, which means a single newline character between | ;; Customize render options for different contexts. Set to "none" to disable the defaults, or use comma separated list: | ||||||
| ;; paragraphs will cause a line break and adding trailing whitespace to paragraphs is not | ;; * short-issue-pattern: recognized "#123" issue reference and render it as a link to the issue | ||||||
| ;; necessary to force a line break. | ;; * new-line-hard-break: render soft line breaks as hard line breaks, which means a single newline character between | ||||||
| ;; Render soft line breaks as hard line breaks for comments | ;;   paragraphs will cause a line break and adding trailing whitespace to paragraphs is not | ||||||
| ;ENABLE_HARD_LINE_BREAK_IN_COMMENTS = true | ;;   necessary to force a line break. | ||||||
| ;; | ;RENDER_OPTIONS_COMMENT = short-issue-pattern, new-line-hard-break | ||||||
| ;; Render soft line breaks as hard line breaks for markdown documents | ;RENDER_OPTIONS_WIKI = short-issue-pattern | ||||||
| ;ENABLE_HARD_LINE_BREAK_IN_DOCUMENTS = false | ;RENDER_OPTIONS_REPO_FILE = | ||||||
| ;; | ;; | ||||||
| ;; Comma separated list of custom URL-Schemes that are allowed as links when rendering Markdown | ;; 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) | ;; 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 | ;; Enables math inline and block detection | ||||||
| ;ENABLE_MATH = true | ;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 = | ||||||
|  | ;; | ||||||
|  |  | ||||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||||
|   | |||||||
| @@ -56,7 +56,7 @@ func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repositor | |||||||
| 	if repo != nil { | 	if repo != nil { | ||||||
| 		helper.repoLink = repo.Link() | 		helper.repoLink = repo.Link() | ||||||
| 		helper.commitChecker = newCommitChecker(ctx, repo) | 		helper.commitChecker = newCommitChecker(ctx, repo) | ||||||
| 		rctx = rctx.WithMetas(repo.ComposeMetas(ctx)) | 		rctx = rctx.WithMetas(repo.ComposeCommentMetas(ctx)) | ||||||
| 	} else { | 	} else { | ||||||
| 		// this is almost dead code, only to pass the incorrect tests | 		// this is almost dead code, only to pass the incorrect tests | ||||||
| 		helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName) | 		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, | 			"user": helper.opts.DeprecatedOwnerName, | ||||||
| 			"repo": helper.opts.DeprecatedRepoName, | 			"repo": helper.opts.DeprecatedRepoName, | ||||||
|  |  | ||||||
| 			"markdownLineBreakStyle":       "comment", | 			"markdownNewLineHardBreak":     "true", | ||||||
| 			"markupAllowShortIssuePattern": "true", | 			"markupAllowShortIssuePattern": "true", | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -61,15 +61,13 @@ func NewRenderContextRepoFile(ctx context.Context, repo *repo_model.Repository, | |||||||
| 	if repo != nil { | 	if repo != nil { | ||||||
| 		helper.repoLink = repo.Link() | 		helper.repoLink = repo.Link() | ||||||
| 		helper.commitChecker = newCommitChecker(ctx, repo) | 		helper.commitChecker = newCommitChecker(ctx, repo) | ||||||
| 		rctx = rctx.WithMetas(repo.ComposeDocumentMetas(ctx)) | 		rctx = rctx.WithMetas(repo.ComposeRepoFileMetas(ctx)) | ||||||
| 	} else { | 	} else { | ||||||
| 		// this is almost dead code, only to pass the incorrect tests | 		// this is almost dead code, only to pass the incorrect tests | ||||||
| 		helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName) | 		helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName) | ||||||
| 		rctx = rctx.WithMetas(map[string]string{ | 		rctx = rctx.WithMetas(map[string]string{ | ||||||
| 			"user": helper.opts.DeprecatedOwnerName, | 			"user": helper.opts.DeprecatedOwnerName, | ||||||
| 			"repo": helper.opts.DeprecatedRepoName, | 			"repo": helper.opts.DeprecatedRepoName, | ||||||
|  |  | ||||||
| 			"markdownLineBreakStyle": "document", |  | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| 	rctx = rctx.WithHelper(helper) | 	rctx = rctx.WithHelper(helper) | ||||||
|   | |||||||
| @@ -68,7 +68,6 @@ func NewRenderContextRepoWiki(ctx context.Context, repo *repo_model.Repository, | |||||||
| 			"user": helper.opts.DeprecatedOwnerName, | 			"user": helper.opts.DeprecatedOwnerName, | ||||||
| 			"repo": helper.opts.DeprecatedRepoName, | 			"repo": helper.opts.DeprecatedRepoName, | ||||||
|  |  | ||||||
| 			"markdownLineBreakStyle":       "document", |  | ||||||
| 			"markupAllowShortIssuePattern": "true", | 			"markupAllowShortIssuePattern": "true", | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -512,15 +512,15 @@ func (repo *Repository) composeCommonMetas(ctx context.Context) map[string]strin | |||||||
| 			"repo": repo.Name, | 			"repo": repo.Name, | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		unit, err := repo.GetUnit(ctx, unit.TypeExternalTracker) | 		unitExternalTracker, err := repo.GetUnit(ctx, unit.TypeExternalTracker) | ||||||
| 		if err == nil { | 		if err == nil { | ||||||
| 			metas["format"] = unit.ExternalTrackerConfig().ExternalTrackerFormat | 			metas["format"] = unitExternalTracker.ExternalTrackerConfig().ExternalTrackerFormat | ||||||
| 			switch unit.ExternalTrackerConfig().ExternalTrackerStyle { | 			switch unitExternalTracker.ExternalTrackerConfig().ExternalTrackerStyle { | ||||||
| 			case markup.IssueNameStyleAlphanumeric: | 			case markup.IssueNameStyleAlphanumeric: | ||||||
| 				metas["style"] = markup.IssueNameStyleAlphanumeric | 				metas["style"] = markup.IssueNameStyleAlphanumeric | ||||||
| 			case markup.IssueNameStyleRegexp: | 			case markup.IssueNameStyleRegexp: | ||||||
| 				metas["style"] = markup.IssueNameStyleRegexp | 				metas["style"] = markup.IssueNameStyleRegexp | ||||||
| 				metas["regexp"] = unit.ExternalTrackerConfig().ExternalTrackerRegexpPattern | 				metas["regexp"] = unitExternalTracker.ExternalTrackerConfig().ExternalTrackerRegexpPattern | ||||||
| 			default: | 			default: | ||||||
| 				metas["style"] = markup.IssueNameStyleNumeric | 				metas["style"] = markup.IssueNameStyleNumeric | ||||||
| 			} | 			} | ||||||
| @@ -544,11 +544,11 @@ func (repo *Repository) composeCommonMetas(ctx context.Context) map[string]strin | |||||||
| 	return repo.commonRenderingMetas | 	return repo.commonRenderingMetas | ||||||
| } | } | ||||||
|  |  | ||||||
| // ComposeMetas composes a map of metas for properly rendering comments or comment-like contents (commit message) | // ComposeCommentMetas 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 { | func (repo *Repository) ComposeCommentMetas(ctx context.Context) map[string]string { | ||||||
| 	metas := maps.Clone(repo.composeCommonMetas(ctx)) | 	metas := maps.Clone(repo.composeCommonMetas(ctx)) | ||||||
| 	metas["markdownLineBreakStyle"] = "comment" | 	metas["markdownNewLineHardBreak"] = strconv.FormatBool(setting.Markdown.RenderOptionsComment.NewLineHardBreak) | ||||||
| 	metas["markupAllowShortIssuePattern"] = "true" | 	metas["markupAllowShortIssuePattern"] = strconv.FormatBool(setting.Markdown.RenderOptionsComment.ShortIssuePattern) | ||||||
| 	return metas | 	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 { | func (repo *Repository) ComposeWikiMetas(ctx context.Context) map[string]string { | ||||||
| 	// does wiki need the "teams" and "org" from common metas? | 	// does wiki need the "teams" and "org" from common metas? | ||||||
| 	metas := maps.Clone(repo.composeCommonMetas(ctx)) | 	metas := maps.Clone(repo.composeCommonMetas(ctx)) | ||||||
| 	metas["markdownLineBreakStyle"] = "document" | 	metas["markdownNewLineHardBreak"] = strconv.FormatBool(setting.Markdown.RenderOptionsWiki.NewLineHardBreak) | ||||||
| 	metas["markupAllowShortIssuePattern"] = "true" | 	metas["markupAllowShortIssuePattern"] = strconv.FormatBool(setting.Markdown.RenderOptionsWiki.ShortIssuePattern) | ||||||
| 	return metas | 	return metas | ||||||
| } | } | ||||||
|  |  | ||||||
| // ComposeDocumentMetas composes a map of metas for properly rendering documents (repo files) | // ComposeRepoFileMetas composes a map of metas for properly rendering documents (repo files) | ||||||
| func (repo *Repository) ComposeDocumentMetas(ctx context.Context) map[string]string { | func (repo *Repository) ComposeRepoFileMetas(ctx context.Context) map[string]string { | ||||||
| 	// does document(file) need the "teams" and "org" from common metas? | 	// does document(file) need the "teams" and "org" from common metas? | ||||||
| 	metas := maps.Clone(repo.composeCommonMetas(ctx)) | 	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 | 	return metas | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -86,7 +86,7 @@ func TestMetas(t *testing.T) { | |||||||
|  |  | ||||||
| 	repo.Units = nil | 	repo.Units = nil | ||||||
|  |  | ||||||
| 	metas := repo.ComposeMetas(db.DefaultContext) | 	metas := repo.ComposeCommentMetas(db.DefaultContext) | ||||||
| 	assert.Equal(t, "testRepo", metas["repo"]) | 	assert.Equal(t, "testRepo", metas["repo"]) | ||||||
| 	assert.Equal(t, "testOwner", metas["user"]) | 	assert.Equal(t, "testOwner", metas["user"]) | ||||||
|  |  | ||||||
| @@ -100,7 +100,7 @@ func TestMetas(t *testing.T) { | |||||||
| 	testSuccess := func(expectedStyle string) { | 	testSuccess := func(expectedStyle string) { | ||||||
| 		repo.Units = []*RepoUnit{&externalTracker} | 		repo.Units = []*RepoUnit{&externalTracker} | ||||||
| 		repo.commonRenderingMetas = nil | 		repo.commonRenderingMetas = nil | ||||||
| 		metas := repo.ComposeMetas(db.DefaultContext) | 		metas := repo.ComposeCommentMetas(db.DefaultContext) | ||||||
| 		assert.Equal(t, expectedStyle, metas["style"]) | 		assert.Equal(t, expectedStyle, metas["style"]) | ||||||
| 		assert.Equal(t, "testRepo", metas["repo"]) | 		assert.Equal(t, "testRepo", metas["repo"]) | ||||||
| 		assert.Equal(t, "testOwner", metas["user"]) | 		assert.Equal(t, "testOwner", metas["user"]) | ||||||
| @@ -121,7 +121,7 @@ func TestMetas(t *testing.T) { | |||||||
| 	repo, err := GetRepositoryByID(db.DefaultContext, 3) | 	repo, err := GetRepositoryByID(db.DefaultContext, 3) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
|  |  | ||||||
| 	metas = repo.ComposeMetas(db.DefaultContext) | 	metas = repo.ComposeCommentMetas(db.DefaultContext) | ||||||
| 	assert.Contains(t, metas, "org") | 	assert.Contains(t, metas, "org") | ||||||
| 	assert.Contains(t, metas, "teams") | 	assert.Contains(t, metas, "teams") | ||||||
| 	assert.Equal(t, "org3", metas["org"]) | 	assert.Equal(t, "org3", metas["org"]) | ||||||
|   | |||||||
| @@ -9,7 +9,6 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/container" | 	"code.gitea.io/gitea/modules/container" | ||||||
| 	"code.gitea.io/gitea/modules/markup" | 	"code.gitea.io/gitea/modules/markup" | ||||||
| 	"code.gitea.io/gitea/modules/markup/internal" | 	"code.gitea.io/gitea/modules/markup/internal" | ||||||
| 	"code.gitea.io/gitea/modules/setting" |  | ||||||
|  |  | ||||||
| 	"github.com/yuin/goldmark/ast" | 	"github.com/yuin/goldmark/ast" | ||||||
| 	east "github.com/yuin/goldmark/extension/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) | 			g.transformList(ctx, v, rc) | ||||||
| 		case *ast.Text: | 		case *ast.Text: | ||||||
| 			if v.SoftLineBreak() && !v.HardLineBreak() { | 			if v.SoftLineBreak() && !v.HardLineBreak() { | ||||||
| 				// TODO: this was a quite unclear part, old code: `if metas["mode"] != "document" { use comment link break setting }` | 				newLineHardBreak := ctx.RenderOptions.Metas["markdownNewLineHardBreak"] == "true" | ||||||
| 				// many places render non-comment contents with no mode=document, then these contents also use comment's hard line break setting | 				v.SetHardLineBreak(newLineHardBreak) | ||||||
| 				// 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) |  | ||||||
| 				} |  | ||||||
| 			} | 			} | ||||||
| 		case *ast.CodeSpan: | 		case *ast.CodeSpan: | ||||||
| 			g.transformCodeSpan(ctx, v, reader) | 			g.transformCodeSpan(ctx, v, reader) | ||||||
|   | |||||||
| @@ -126,11 +126,11 @@ func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender { | |||||||
| 				highlighting.WithWrapperRenderer(r.highlightingRenderer), | 				highlighting.WithWrapperRenderer(r.highlightingRenderer), | ||||||
| 			), | 			), | ||||||
| 			math.NewExtension(&ctx.RenderInternal, math.Options{ | 			math.NewExtension(&ctx.RenderInternal, math.Options{ | ||||||
| 				Enabled:           setting.Markdown.EnableMath, | 				Enabled:                  setting.Markdown.EnableMath, | ||||||
| 				ParseDollarInline: true, | 				ParseInlineDollar:        setting.Markdown.MathCodeBlockOptions.ParseInlineDollar, | ||||||
| 				ParseDollarBlock:  true, | 				ParseInlineParentheses:   setting.Markdown.MathCodeBlockOptions.ParseInlineParentheses, // this is a bad syntax "\( ... \)", it conflicts with normal markdown escaping | ||||||
| 				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) | 				ParseBlockDollar:         setting.Markdown.MathCodeBlockOptions.ParseBlockDollar, | ||||||
| 				// ParseBracketInline: true, // TODO: this is also a bad syntax "\( ... \)", it also conflicts, it should be deprecated in the future | 				ParseBlockSquareBrackets: setting.Markdown.MathCodeBlockOptions.ParseBlockSquareBrackets, //  this is a bad syntax "\[ ... \]", it conflicts with normal markdown escaping | ||||||
| 			}), | 			}), | ||||||
| 			meta.Meta, | 			meta.Meta, | ||||||
| 		), | 		), | ||||||
|   | |||||||
| @@ -8,6 +8,8 @@ import ( | |||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/modules/markup" | 	"code.gitea.io/gitea/modules/markup" | ||||||
|  | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/test" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| ) | ) | ||||||
| @@ -15,6 +17,7 @@ import ( | |||||||
| const nl = "\n" | const nl = "\n" | ||||||
|  |  | ||||||
| func TestMathRender(t *testing.T) { | func TestMathRender(t *testing.T) { | ||||||
|  | 	setting.Markdown.MathCodeBlockOptions = setting.MarkdownMathCodeBlockOptions{ParseInlineDollar: true, ParseInlineParentheses: true} | ||||||
| 	testcases := []struct { | 	testcases := []struct { | ||||||
| 		testcase string | 		testcase string | ||||||
| 		expected string | 		expected string | ||||||
| @@ -69,7 +72,7 @@ func TestMathRender(t *testing.T) { | |||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"$$a$$", | 			"$$a$$", | ||||||
| 			`<code class="language-math display">a</code>` + nl, | 			`<p><code class="language-math">a</code></p>` + nl, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"$$a$$ test", | 			"$$a$$ test", | ||||||
| @@ -111,6 +114,7 @@ func TestMathRender(t *testing.T) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func TestMathRenderBlockIndent(t *testing.T) { | func TestMathRenderBlockIndent(t *testing.T) { | ||||||
|  | 	setting.Markdown.MathCodeBlockOptions = setting.MarkdownMathCodeBlockOptions{ParseBlockDollar: true, ParseBlockSquareBrackets: true} | ||||||
| 	testcases := []struct { | 	testcases := []struct { | ||||||
| 		name     string | 		name     string | ||||||
| 		testcase 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, `<p><code class="language-math">a</code></p>`, "$`a`$") | ||||||
|  |  | ||||||
|  | 	// ParseInlineDollar | ||||||
|  | 	test(t, `<p>$a$</p>`, `$a$`) | ||||||
|  | 	setting.Markdown.MathCodeBlockOptions.ParseInlineDollar = true | ||||||
|  | 	test(t, `<p><code class="language-math">a</code></p>`, `$a$`) | ||||||
|  |  | ||||||
|  | 	// ParseInlineParentheses | ||||||
|  | 	test(t, `<p>(a)</p>`, `\(a\)`) | ||||||
|  | 	setting.Markdown.MathCodeBlockOptions.ParseInlineParentheses = true | ||||||
|  | 	test(t, `<p><code class="language-math">a</code></p>`, `\(a\)`) | ||||||
|  |  | ||||||
|  | 	// ParseBlockDollar | ||||||
|  | 	test(t, `<p>$$ | ||||||
|  | a | ||||||
|  | $$</p> | ||||||
|  | `, ` | ||||||
|  | $$ | ||||||
|  | a | ||||||
|  | $$ | ||||||
|  | `) | ||||||
|  | 	setting.Markdown.MathCodeBlockOptions.ParseBlockDollar = true | ||||||
|  | 	test(t, `<pre class="code-block is-loading"><code class="language-math display"> | ||||||
|  | a | ||||||
|  | </code></pre> | ||||||
|  | `, ` | ||||||
|  | $$ | ||||||
|  | a | ||||||
|  | $$ | ||||||
|  | `) | ||||||
|  |  | ||||||
|  | 	// ParseBlockSquareBrackets | ||||||
|  | 	test(t, `<p>[ | ||||||
|  | a | ||||||
|  | ]</p> | ||||||
|  | `, ` | ||||||
|  | \[ | ||||||
|  | a | ||||||
|  | \] | ||||||
|  | `) | ||||||
|  | 	setting.Markdown.MathCodeBlockOptions.ParseBlockSquareBrackets = true | ||||||
|  | 	test(t, `<pre class="code-block is-loading"><code class="language-math display"> | ||||||
|  | a | ||||||
|  | </code></pre> | ||||||
|  | `, ` | ||||||
|  | \[ | ||||||
|  | a | ||||||
|  | \] | ||||||
|  | `) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -15,26 +15,26 @@ type inlineParser struct { | |||||||
| 	trigger              []byte | 	trigger              []byte | ||||||
| 	endBytesSingleDollar []byte | 	endBytesSingleDollar []byte | ||||||
| 	endBytesDoubleDollar []byte | 	endBytesDoubleDollar []byte | ||||||
| 	endBytesBracket      []byte | 	endBytesParentheses  []byte | ||||||
|  | 	enableInlineDollar   bool | ||||||
| } | } | ||||||
|  |  | ||||||
| var defaultInlineDollarParser = &inlineParser{ | func NewInlineDollarParser(enableInlineDollar bool) parser.InlineParser { | ||||||
| 	trigger:              []byte{'$'}, | 	return &inlineParser{ | ||||||
| 	endBytesSingleDollar: []byte{'$'}, | 		trigger:              []byte{'$'}, | ||||||
| 	endBytesDoubleDollar: []byte{'$', '$'}, | 		endBytesSingleDollar: []byte{'$'}, | ||||||
|  | 		endBytesDoubleDollar: []byte{'$', '$'}, | ||||||
|  | 		enableInlineDollar:   enableInlineDollar, | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func NewInlineDollarParser() parser.InlineParser { | var defaultInlineParenthesesParser = &inlineParser{ | ||||||
| 	return defaultInlineDollarParser | 	trigger:             []byte{'\\', '('}, | ||||||
|  | 	endBytesParentheses: []byte{'\\', ')'}, | ||||||
| } | } | ||||||
|  |  | ||||||
| var defaultInlineBracketParser = &inlineParser{ | func NewInlineParenthesesParser() parser.InlineParser { | ||||||
| 	trigger:         []byte{'\\', '('}, | 	return defaultInlineParenthesesParser | ||||||
| 	endBytesBracket: []byte{'\\', ')'}, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func NewInlineBracketParser() parser.InlineParser { |  | ||||||
| 	return defaultInlineBracketParser |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // Trigger triggers this parser on $ or \ | // Trigger triggers this parser on $ or \ | ||||||
| @@ -46,7 +46,7 @@ func isPunctuation(b byte) bool { | |||||||
| 	return b == '.' || b == '!' || b == '?' || b == ',' || b == ';' || b == ':' | 	return b == '.' || b == '!' || b == '?' || b == ',' || b == ';' || b == ':' | ||||||
| } | } | ||||||
|  |  | ||||||
| func isBracket(b byte) bool { | func isParenthesesClose(b byte) bool { | ||||||
| 	return b == ')' | 	return b == ')' | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -86,7 +86,11 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser. | |||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		startMarkLen = 2 | 		startMarkLen = 2 | ||||||
| 		stopMark = parser.endBytesBracket | 		stopMark = parser.endBytesParentheses | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if line[0] == '$' && !parser.enableInlineDollar && (len(line) == 1 || line[1] != '`') { | ||||||
|  | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if checkSurrounding { | 	if checkSurrounding { | ||||||
| @@ -110,7 +114,7 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser. | |||||||
| 				succeedingCharacter = line[i+len(stopMark)] | 				succeedingCharacter = line[i+len(stopMark)] | ||||||
| 			} | 			} | ||||||
| 			// check valid ending character | 			// check valid ending character | ||||||
| 			isValidEndingChar := isPunctuation(succeedingCharacter) || isBracket(succeedingCharacter) || | 			isValidEndingChar := isPunctuation(succeedingCharacter) || isParenthesesClose(succeedingCharacter) || | ||||||
| 				succeedingCharacter == ' ' || succeedingCharacter == '\n' || succeedingCharacter == 0 | 				succeedingCharacter == ' ' || succeedingCharacter == '\n' || succeedingCharacter == 0 | ||||||
| 			if checkSurrounding && !isValidEndingChar { | 			if checkSurrounding && !isValidEndingChar { | ||||||
| 				break | 				break | ||||||
|   | |||||||
| @@ -14,10 +14,11 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| type Options struct { | type Options struct { | ||||||
| 	Enabled           bool | 	Enabled                  bool | ||||||
| 	ParseDollarInline bool | 	ParseInlineDollar        bool // inline $$ xxx $$ text | ||||||
| 	ParseDollarBlock  bool | 	ParseInlineParentheses   bool // inline \( xxx \) text | ||||||
| 	ParseSquareBlock  bool | 	ParseBlockDollar         bool // block $$ multiple-line $$ text | ||||||
|  | 	ParseBlockSquareBrackets bool // block \[ multiple-line \] text | ||||||
| } | } | ||||||
|  |  | ||||||
| // Extension is a math extension | // Extension is a math extension | ||||||
| @@ -42,16 +43,16 @@ func (e *Extension) Extend(m goldmark.Markdown) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	inlines := []util.PrioritizedValue{util.Prioritized(NewInlineBracketParser(), 501)} | 	var inlines []util.PrioritizedValue | ||||||
| 	if e.options.ParseDollarInline { | 	if e.options.ParseInlineParentheses { | ||||||
| 		inlines = append(inlines, util.Prioritized(NewInlineDollarParser(), 502)) | 		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.WithInlineParsers(inlines...)) | ||||||
|  |  | ||||||
| 	m.Parser().AddOptions(parser.WithBlockParsers( | 	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( | 	m.Renderer().AddOptions(renderer.WithNodeRenderers( | ||||||
| 		util.Prioritized(NewBlockRenderer(e.renderInternal), 501), | 		util.Prioritized(NewBlockRenderer(e.renderInternal), 501), | ||||||
| 		util.Prioritized(NewInlineRenderer(e.renderInternal), 502), | 		util.Prioritized(NewInlineRenderer(e.renderInternal), 502), | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/url" | 	"net/url" | ||||||
|  | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| @@ -46,7 +47,7 @@ type RenderOptions struct { | |||||||
| 	// user&repo, format&style®exp (for external issue pattern), teams&org (for mention) | 	// user&repo, format&style®exp (for external issue pattern), teams&org (for mention) | ||||||
| 	// RefTypeNameSubURL (for iframe&asciicast) | 	// RefTypeNameSubURL (for iframe&asciicast) | ||||||
| 	// markupAllowShortIssuePattern | 	// markupAllowShortIssuePattern | ||||||
| 	// markdownLineBreakStyle (comment, document) | 	// markdownNewLineHardBreak | ||||||
| 	Metas map[string]string | 	Metas map[string]string | ||||||
|  |  | ||||||
| 	// used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page | 	// 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 { | 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 { | type TestRenderHelper struct { | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
|  | 	"code.gitea.io/gitea/modules/util" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // ExternalMarkupRenderers represents the external markup renderers | // ExternalMarkupRenderers represents the external markup renderers | ||||||
| @@ -23,18 +24,33 @@ const ( | |||||||
| 	RenderContentModeIframe      = "iframe" | 	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 | // Markdown settings | ||||||
| var Markdown = struct { | var Markdown = struct { | ||||||
| 	EnableHardLineBreakInComments  bool | 	RenderOptionsComment  MarkdownRenderOptions `ini:"-"` | ||||||
| 	EnableHardLineBreakInDocuments bool | 	RenderOptionsWiki     MarkdownRenderOptions `ini:"-"` | ||||||
| 	CustomURLSchemes               []string `ini:"CUSTOM_URL_SCHEMES"` | 	RenderOptionsRepoFile MarkdownRenderOptions `ini:"-"` | ||||||
| 	FileExtensions                 []string |  | ||||||
| 	EnableMath                     bool | 	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, | 	FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd,.livemd", ","), | ||||||
| 	EnableHardLineBreakInDocuments: false, | 	EnableMath:     true, | ||||||
| 	FileExtensions:                 strings.Split(".md,.markdown,.mdown,.mkd,.livemd", ","), |  | ||||||
| 	EnableMath:                     true, |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // MarkupRenderer defines the external parser configured in ini | // MarkupRenderer defines the external parser configured in ini | ||||||
| @@ -60,6 +76,56 @@ type MarkupSanitizerRule struct { | |||||||
|  |  | ||||||
| func loadMarkupFrom(rootCfg ConfigProvider) { | func loadMarkupFrom(rootCfg ConfigProvider) { | ||||||
| 	mustMapSetting(rootCfg, "markdown", &Markdown) | 	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) | 	MermaidMaxSourceCharacters = rootCfg.Section("markup").Key("MERMAID_MAX_SOURCE_CHARACTERS").MustInt(5000) | ||||||
| 	ExternalMarkupRenderers = make([]*MarkupRenderer, 0, 10) | 	ExternalMarkupRenderers = make([]*MarkupRenderer, 0, 10) | ||||||
|   | |||||||
							
								
								
									
										51
									
								
								modules/setting/markup_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								modules/setting/markup_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
| @@ -51,7 +51,7 @@ var testMetas = map[string]string{ | |||||||
| 	"user":                         "user13", | 	"user":                         "user13", | ||||||
| 	"repo":                         "repo11", | 	"repo":                         "repo11", | ||||||
| 	"repoPath":                     "../../tests/gitea-repositories-meta/user13/repo11.git/", | 	"repoPath":                     "../../tests/gitea-repositories-meta/user13/repo11.git/", | ||||||
| 	"markdownLineBreakStyle":       "comment", | 	"markdownNewLineHardBreak":     "true", | ||||||
| 	"markupAllowShortIssuePattern": "true", | 	"markupAllowShortIssuePattern": "true", | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -219,6 +219,13 @@ func IfZero[T comparable](v, def T) T { | |||||||
| 	return v | 	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: | // OptionalArg helps the "optional argument" in Golang: | ||||||
| // | // | ||||||
| //	func foo(optArg ...int) { return OptionalArg(optArg) } | //	func foo(optArg ...int) { return OptionalArg(optArg) } | ||||||
|   | |||||||
| @@ -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 | 	// 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 | 	// the title for the "run" is from the commit message | ||||||
| 	resp.State.Run.Title = run.Title | 	resp.State.Run.Title = run.Title | ||||||
|   | |||||||
| @@ -278,7 +278,7 @@ func ViewIssue(ctx *context.Context) { | |||||||
| 		extIssueUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker) | 		extIssueUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker) | ||||||
| 		if err == nil && extIssueUnit != nil { | 		if err == nil && extIssueUnit != nil { | ||||||
| 			if extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == markup.IssueNameStyleNumeric || extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == "" { | 			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") | 				metas["index"] = ctx.PathParam("index") | ||||||
| 				res, err := vars.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas) | 				res, err := vars.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
|   | |||||||
| @@ -176,7 +176,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { | |||||||
| 		if markupType != "" && !shouldRenderSource { | 		if markupType != "" && !shouldRenderSource { | ||||||
| 			ctx.Data["IsMarkup"] = true | 			ctx.Data["IsMarkup"] = true | ||||||
| 			ctx.Data["MarkupType"] = markupType | 			ctx.Data["MarkupType"] = markupType | ||||||
| 			metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx) | 			metas := ctx.Repo.Repository.ComposeRepoFileMetas(ctx) | ||||||
| 			metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL() | 			metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL() | ||||||
| 			rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ | 			rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ | ||||||
| 				CurrentRefPath:  ctx.Repo.RefTypeNameSubURL(), | 				CurrentRefPath:  ctx.Repo.RefTypeNameSubURL(), | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ | |||||||
| 									<button class="btn interact-fg tw-px-1" data-clipboard-text="{{.DefaultBranchBranch.DBBranch.Name}}" data-tooltip-content="{{ctx.Locale.Tr "copy_branch"}}">{{svg "octicon-copy" 14}}</button> | 									<button class="btn interact-fg tw-px-1" data-clipboard-text="{{.DefaultBranchBranch.DBBranch.Name}}" data-tooltip-content="{{ctx.Locale.Tr "copy_branch"}}">{{svg "octicon-copy" 14}}</button> | ||||||
| 									{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DefaultBranchBranch.DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DefaultBranchBranch.DBBranch.CommitID)}} | 									{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DefaultBranchBranch.DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DefaultBranchBranch.DBBranch.CommitID)}} | ||||||
| 								</div> | 								</div> | ||||||
| 								<p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{ctx.RenderUtils.RenderCommitMessage .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeMetas ctx)}}</span> · {{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}}</p> | 								<p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{ctx.RenderUtils.RenderCommitMessage .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeCommentMetas ctx)}}</span> · {{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}}</p> | ||||||
| 							</td> | 							</td> | ||||||
| 							{{/* 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 */}} | 							{{/* 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 */}} | ||||||
| 							<td class="tw-text-right tw-overflow-visible"> | 							<td class="tw-text-right tw-overflow-visible"> | ||||||
| @@ -103,7 +103,7 @@ | |||||||
| 									<button class="btn interact-fg tw-px-1" data-clipboard-text="{{.DBBranch.Name}}" data-tooltip-content="{{ctx.Locale.Tr "copy_branch"}}">{{svg "octicon-copy" 14}}</button> | 									<button class="btn interact-fg tw-px-1" data-clipboard-text="{{.DBBranch.Name}}" data-tooltip-content="{{ctx.Locale.Tr "copy_branch"}}">{{svg "octicon-copy" 14}}</button> | ||||||
| 									{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}} | 									{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}} | ||||||
| 								</div> | 								</div> | ||||||
| 								<p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{ctx.RenderUtils.RenderCommitMessage .DBBranch.CommitMessage ($.Repository.ComposeMetas ctx)}}</span> · {{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}}</p> | 								<p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{ctx.RenderUtils.RenderCommitMessage .DBBranch.CommitMessage ($.Repository.ComposeCommentMetas ctx)}}</span> · {{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}}</p> | ||||||
| 							{{end}} | 							{{end}} | ||||||
| 							</td> | 							</td> | ||||||
| 							<td class="two wide ui"> | 							<td class="two wide ui"> | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ | |||||||
| 	<div class="ui container fluid padded"> | 	<div class="ui container fluid padded"> | ||||||
| 		<div class="ui top attached header clearing segment tw-relative commit-header"> | 		<div class="ui top attached header clearing segment tw-relative commit-header"> | ||||||
| 			<div class="tw-flex tw-mb-4 tw-gap-1"> | 			<div class="tw-flex tw-mb-4 tw-gap-1"> | ||||||
| 				<h3 class="tw-mb-0 tw-flex-1"><span class="commit-summary" title="{{.Commit.Summary}}">{{ctx.RenderUtils.RenderCommitMessage .Commit.Message ($.Repository.ComposeMetas ctx)}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3> | 				<h3 class="tw-mb-0 tw-flex-1"><span class="commit-summary" title="{{.Commit.Summary}}">{{ctx.RenderUtils.RenderCommitMessage .Commit.Message ($.Repository.ComposeCommentMetas ctx)}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3> | ||||||
| 				{{if not $.PageIsWiki}} | 				{{if not $.PageIsWiki}} | ||||||
| 					<div class="commit-header-buttons"> | 					<div class="commit-header-buttons"> | ||||||
| 						<a class="ui primary tiny button" href="{{.SourcePath}}"> | 						<a class="ui primary tiny button" href="{{.SourcePath}}"> | ||||||
| @@ -122,7 +122,7 @@ | |||||||
| 				{{end}} | 				{{end}} | ||||||
| 			</div> | 			</div> | ||||||
| 			{{if IsMultilineCommitMessage .Commit.Message}} | 			{{if IsMultilineCommitMessage .Commit.Message}} | ||||||
| 				<pre class="commit-body">{{ctx.RenderUtils.RenderCommitBody .Commit.Message ($.Repository.ComposeMetas ctx)}}</pre> | 				<pre class="commit-body">{{ctx.RenderUtils.RenderCommitBody .Commit.Message ($.Repository.ComposeCommentMetas ctx)}}</pre> | ||||||
| 			{{end}} | 			{{end}} | ||||||
| 			{{template "repo/commit_load_branches_and_tags" .}} | 			{{template "repo/commit_load_branches_and_tags" .}} | ||||||
| 		</div> | 		</div> | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ | |||||||
| 							<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{.Summary | ctx.RenderUtils.RenderEmoji}}</span> | 							<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{.Summary | ctx.RenderUtils.RenderEmoji}}</span> | ||||||
| 						{{else}} | 						{{else}} | ||||||
| 							{{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String)}} | 							{{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String)}} | ||||||
| 							<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.Repository.ComposeMetas ctx)}}</span> | 							<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.Repository.ComposeCommentMetas ctx)}}</span> | ||||||
| 						{{end}} | 						{{end}} | ||||||
| 						</span> | 						</span> | ||||||
| 						{{if IsMultilineCommitMessage .Message}} | 						{{if IsMultilineCommitMessage .Message}} | ||||||
| @@ -52,7 +52,7 @@ | |||||||
| 						{{end}} | 						{{end}} | ||||||
| 						{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} | 						{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} | ||||||
| 						{{if IsMultilineCommitMessage .Message}} | 						{{if IsMultilineCommitMessage .Message}} | ||||||
| 						<pre class="commit-body tw-hidden">{{ctx.RenderUtils.RenderCommitBody .Message ($.Repository.ComposeMetas ctx)}}</pre> | 						<pre class="commit-body tw-hidden">{{ctx.RenderUtils.RenderCommitBody .Message ($.Repository.ComposeCommentMetas ctx)}}</pre> | ||||||
| 						{{end}} | 						{{end}} | ||||||
| 						{{if $.CommitsTagsMap}} | 						{{if $.CommitsTagsMap}} | ||||||
| 							{{range (index $.CommitsTagsMap .ID.String)}} | 							{{range (index $.CommitsTagsMap .ID.String)}} | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ | |||||||
| 		{{$commitLink:= printf "%s/%s" $commitBaseLink (PathEscape .ID.String)}} | 		{{$commitLink:= printf "%s/%s" $commitBaseLink (PathEscape .ID.String)}} | ||||||
|  |  | ||||||
| 		<span class="tw-flex-1 tw-font-mono gt-ellipsis" title="{{.Summary}}"> | 		<span class="tw-flex-1 tw-font-mono gt-ellipsis" title="{{.Summary}}"> | ||||||
| 			{{- ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx) -}} | 			{{- ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeCommentMetas ctx) -}} | ||||||
| 		</span> | 		</span> | ||||||
|  |  | ||||||
| 		{{if IsMultilineCommitMessage .Message}} | 		{{if IsMultilineCommitMessage .Message}} | ||||||
| @@ -29,7 +29,7 @@ | |||||||
| 	</div> | 	</div> | ||||||
| 	{{if IsMultilineCommitMessage .Message}} | 	{{if IsMultilineCommitMessage .Message}} | ||||||
| 	<pre class="commit-body tw-ml-[33px] tw-hidden" data-singular-commit-body-for="{{$tag}}"> | 	<pre class="commit-body tw-ml-[33px] tw-hidden" data-singular-commit-body-for="{{$tag}}"> | ||||||
| 		{{- ctx.RenderUtils.RenderCommitBody .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx) -}} | 		{{- ctx.RenderUtils.RenderCommitBody .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeCommentMetas ctx) -}} | ||||||
| 	</pre> | 	</pre> | ||||||
| 	{{end}} | 	{{end}} | ||||||
| {{end}} | {{end}} | ||||||
|   | |||||||
| @@ -189,7 +189,7 @@ | |||||||
| 				<div class="ui segment flex-text-block tw-gap-4"> | 				<div class="ui segment flex-text-block tw-gap-4"> | ||||||
| 					{{template "shared/issueicon" .}} | 					{{template "shared/issueicon" .}} | ||||||
| 					<div class="issue-title tw-break-anywhere"> | 					<div class="issue-title tw-break-anywhere"> | ||||||
| 						{{ctx.RenderUtils.RenderIssueTitle .PullRequest.Issue.Title ($.Repository.ComposeMetas ctx)}} | 						{{ctx.RenderUtils.RenderIssueTitle .PullRequest.Issue.Title ($.Repository.ComposeCommentMetas ctx)}} | ||||||
| 						<span class="index">#{{.PullRequest.Issue.Index}}</span> | 						<span class="index">#{{.PullRequest.Issue.Index}}</span> | ||||||
| 					</div> | 					</div> | ||||||
| 					<a href="{{$.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui compact button primary"> | 					<a href="{{$.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui compact button primary"> | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ | |||||||
| 					{{template "repo/commit_sign_badge" dict "Commit" $commit.Commit "CommitBaseLink" (print $.RepoLink "/commit") "CommitSignVerification" $commit.Verification}} | 					{{template "repo/commit_sign_badge" dict "Commit" $commit.Commit "CommitBaseLink" (print $.RepoLink "/commit") "CommitSignVerification" $commit.Verification}} | ||||||
|  |  | ||||||
| 					<span class="message tw-inline-block gt-ellipsis"> | 					<span class="message tw-inline-block gt-ellipsis"> | ||||||
| 						<span>{{ctx.RenderUtils.RenderCommitMessage $commit.Subject ($.Repository.ComposeMetas ctx)}}</span> | 						<span>{{ctx.RenderUtils.RenderCommitMessage $commit.Subject ($.Repository.ComposeCommentMetas ctx)}}</span> | ||||||
| 					</span> | 					</span> | ||||||
|  |  | ||||||
| 					<span class="commit-refs flex-text-inline"> | 					<span class="commit-refs flex-text-inline"> | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ | |||||||
| 	{{$canEditIssueTitle := and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}} | 	{{$canEditIssueTitle := and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}} | ||||||
| 	<div class="issue-title" id="issue-title-display"> | 	<div class="issue-title" id="issue-title-display"> | ||||||
| 		<h1 class="tw-break-anywhere"> | 		<h1 class="tw-break-anywhere"> | ||||||
| 			{{ctx.RenderUtils.RenderIssueTitle .Issue.Title ($.Repository.ComposeMetas ctx)}} | 			{{ctx.RenderUtils.RenderIssueTitle .Issue.Title ($.Repository.ComposeCommentMetas ctx)}} | ||||||
| 			<span class="index">#{{.Issue.Index}}</span> | 			<span class="index">#{{.Issue.Index}}</span> | ||||||
| 		</h1> | 		</h1> | ||||||
| 		<div class="issue-title-buttons"> | 		<div class="issue-title-buttons"> | ||||||
|   | |||||||
| @@ -21,10 +21,10 @@ | |||||||
| 	{{template "repo/commit_statuses" dict "Status" .LatestCommitStatus "Statuses" .LatestCommitStatuses}} | 	{{template "repo/commit_statuses" dict "Status" .LatestCommitStatus "Statuses" .LatestCommitStatuses}} | ||||||
|  |  | ||||||
| 	{{$commitLink:= printf "%s/commit/%s" .RepoLink (PathEscape .LatestCommit.ID.String)}} | 	{{$commitLink:= printf "%s/commit/%s" .RepoLink (PathEscape .LatestCommit.ID.String)}} | ||||||
| 	<span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{ctx.RenderUtils.RenderCommitMessageLinkSubject .LatestCommit.Message $commitLink ($.Repository.ComposeMetas ctx)}}</span> | 	<span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{ctx.RenderUtils.RenderCommitMessageLinkSubject .LatestCommit.Message $commitLink ($.Repository.ComposeCommentMetas ctx)}}</span> | ||||||
| 		{{if IsMultilineCommitMessage .LatestCommit.Message}} | 		{{if IsMultilineCommitMessage .LatestCommit.Message}} | ||||||
| 			<button class="ui button ellipsis-button" aria-expanded="false" data-global-click="onRepoEllipsisButtonClick">...</button> | 			<button class="ui button ellipsis-button" aria-expanded="false" data-global-click="onRepoEllipsisButtonClick">...</button> | ||||||
| 			<pre class="commit-body tw-hidden">{{ctx.RenderUtils.RenderCommitBody .LatestCommit.Message ($.Repository.ComposeMetas ctx)}}</pre> | 			<pre class="commit-body tw-hidden">{{ctx.RenderUtils.RenderCommitBody .LatestCommit.Message ($.Repository.ComposeCommentMetas ctx)}}</pre> | ||||||
| 		{{end}} | 		{{end}} | ||||||
| 	</span> | 	</span> | ||||||
| {{end}} | {{end}} | ||||||
|   | |||||||
| @@ -46,7 +46,7 @@ | |||||||
| 			<div class="repo-file-cell message loading-icon-2px"> | 			<div class="repo-file-cell message loading-icon-2px"> | ||||||
| 				{{if $commit}} | 				{{if $commit}} | ||||||
| 					{{$commitLink := printf "%s/commit/%s" $.RepoLink (PathEscape $commit.ID.String)}} | 					{{$commitLink := printf "%s/commit/%s" $.RepoLink (PathEscape $commit.ID.String)}} | ||||||
| 					{{ctx.RenderUtils.RenderCommitMessageLinkSubject $commit.Message $commitLink ($.Repository.ComposeMetas ctx)}} | 					{{ctx.RenderUtils.RenderCommitMessageLinkSubject $commit.Message $commitLink ($.Repository.ComposeCommentMetas ctx)}} | ||||||
| 				{{else}} | 				{{else}} | ||||||
| 					… {{/* will be loaded again by LastCommitLoaderURL */}} | 					… {{/* will be loaded again by LastCommitLoaderURL */}} | ||||||
| 				{{end}} | 				{{end}} | ||||||
|   | |||||||
| @@ -94,7 +94,7 @@ | |||||||
| 								<img class="ui avatar" src="{{$push.AvatarLink ctx .AuthorEmail}}" title="{{.AuthorName}}" width="16" height="16"> | 								<img class="ui avatar" src="{{$push.AvatarLink ctx .AuthorEmail}}" title="{{.AuthorName}}" width="16" height="16"> | ||||||
| 								<a class="ui sha label" href="{{$commitLink}}">{{ShortSha .Sha1}}</a> | 								<a class="ui sha label" href="{{$commitLink}}">{{ShortSha .Sha1}}</a> | ||||||
| 								<span class="text truncate"> | 								<span class="text truncate"> | ||||||
| 									{{ctx.RenderUtils.RenderCommitMessage .Message ($repo.ComposeMetas ctx)}} | 									{{ctx.RenderUtils.RenderCommitMessage .Message ($repo.ComposeCommentMetas ctx)}} | ||||||
| 								</span> | 								</span> | ||||||
| 							</div> | 							</div> | ||||||
| 						{{end}} | 						{{end}} | ||||||
|   | |||||||
| @@ -1,16 +1,17 @@ | |||||||
|  | import {queryElems} from '../utils/dom.ts'; | ||||||
|  |  | ||||||
| export async function initMarkupRenderAsciicast(elMarkup: HTMLElement): Promise<void> { | export async function initMarkupRenderAsciicast(elMarkup: HTMLElement): Promise<void> { | ||||||
|   const el = elMarkup.querySelector('.asciinema-player-container'); |   queryElems(elMarkup, '.asciinema-player-container', async (el) => { | ||||||
|   if (!el) return; |     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([ |     player.create(el.getAttribute('data-asciinema-player-src'), el, { | ||||||
|     // @ts-expect-error: module exports no types |       // poster (a preview frame) to display until the playback is started. | ||||||
|     import(/* webpackChunkName: "asciinema-player" */'asciinema-player'), |       // Set it to 1 hour (also means the end if the video is shorter) to make the preview frame show more. | ||||||
|     import(/* webpackChunkName: "asciinema-player" */'asciinema-player/dist/bundle/asciinema-player.css'), |       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', |  | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import {svg} from '../svg.ts'; | import {svg} from '../svg.ts'; | ||||||
|  | import {queryElems} from '../utils/dom.ts'; | ||||||
|  |  | ||||||
| export function makeCodeCopyButton(): HTMLButtonElement { | export function makeCodeCopyButton(): HTMLButtonElement { | ||||||
|   const button = document.createElement('button'); |   const button = document.createElement('button'); | ||||||
| @@ -8,11 +9,12 @@ export function makeCodeCopyButton(): HTMLButtonElement { | |||||||
| } | } | ||||||
|  |  | ||||||
| export function initMarkupCodeCopy(elMarkup: HTMLElement): void { | export function initMarkupCodeCopy(elMarkup: HTMLElement): void { | ||||||
|   const el = elMarkup.querySelector('.code-block code'); // .markup .code-block code |   // .markup .code-block code | ||||||
|   if (!el || !el.textContent) return; |   queryElems(elMarkup, '.code-block code', (el) => { | ||||||
|  |     if (!el.textContent) return; | ||||||
|   const btn = makeCodeCopyButton(); |     const btn = makeCodeCopyButton(); | ||||||
|   // remove final trailing newline introduced during HTML rendering |     // remove final trailing newline introduced during HTML rendering | ||||||
|   btn.setAttribute('data-clipboard-text', el.textContent.replace(/\r?\n$/, '')); |     btn.setAttribute('data-clipboard-text', el.textContent.replace(/\r?\n$/, '')); | ||||||
|   el.after(btn); |     el.after(btn); | ||||||
|  |   }); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import {displayError} from './common.ts'; | import {displayError} from './common.ts'; | ||||||
|  | import {queryElems} from '../utils/dom.ts'; | ||||||
|  |  | ||||||
| function targetElement(el: Element): {target: Element, displayAsBlock: boolean} { | function targetElement(el: Element): {target: Element, displayAsBlock: boolean} { | ||||||
|   // The target element is either the parent "code block with loading indicator", or itself |   // 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<void> { | export async function initMarkupCodeMath(elMarkup: HTMLElement): Promise<void> { | ||||||
|   const el = elMarkup.querySelector('code.language-math'); // .markup code.language-math' |   // .markup code.language-math' | ||||||
|   if (!el) return; |   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([ |     const MAX_CHARS = 1000; | ||||||
|     import(/* webpackChunkName: "katex" */'katex'), |     const MAX_SIZE = 25; | ||||||
|     import(/* webpackChunkName: "katex" */'katex/dist/katex.css'), |     const MAX_EXPAND = 1000; | ||||||
|   ]); |  | ||||||
|  |  | ||||||
|   const MAX_CHARS = 1000; |     const {target, displayAsBlock} = targetElement(el); | ||||||
|   const MAX_SIZE = 25; |     if (target.hasAttribute('data-render-done')) return; | ||||||
|   const MAX_EXPAND = 1000; |     const source = el.textContent; | ||||||
|  |  | ||||||
|   const {target, displayAsBlock} = targetElement(el); |     if (source.length > MAX_CHARS) { | ||||||
|   if (target.hasAttribute('data-render-done')) return; |       displayError(target, new Error(`Math source of ${source.length} characters exceeds the maximum allowed length of ${MAX_CHARS}.`)); | ||||||
|   const source = el.textContent; |       return; | ||||||
|  |     } | ||||||
|   if (source.length > MAX_CHARS) { |     try { | ||||||
|     displayError(target, new Error(`Math source of ${source.length} characters exceeds the maximum allowed length of ${MAX_CHARS}.`)); |       const tempEl = document.createElement(displayAsBlock ? 'p' : 'span'); | ||||||
|     return; |       katex.render(source, tempEl, { | ||||||
|   } |         maxSize: MAX_SIZE, | ||||||
|   try { |         maxExpand: MAX_EXPAND, | ||||||
|     const tempEl = document.createElement(displayAsBlock ? 'p' : 'span'); |         displayMode: displayAsBlock, // katex: true for display (block) mode, false for inline mode | ||||||
|     katex.render(source, tempEl, { |       }); | ||||||
|       maxSize: MAX_SIZE, |       target.replaceWith(tempEl); | ||||||
|       maxExpand: MAX_EXPAND, |     } catch (error) { | ||||||
|       displayMode: displayAsBlock, // katex: true for display (block) mode, false for inline mode |       displayError(target, error); | ||||||
|     }); |     } | ||||||
|     target.replaceWith(tempEl); |   }); | ||||||
|   } catch (error) { |  | ||||||
|     displayError(target, error); |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import {isDarkTheme} from '../utils.ts'; | import {isDarkTheme} from '../utils.ts'; | ||||||
| import {makeCodeCopyButton} from './codecopy.ts'; | import {makeCodeCopyButton} from './codecopy.ts'; | ||||||
| import {displayError} from './common.ts'; | import {displayError} from './common.ts'; | ||||||
|  | import {queryElems} from '../utils/dom.ts'; | ||||||
|  |  | ||||||
| const {mermaidMaxSourceCharacters} = window.config; | 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}`; | blockquote, dd, dl, figure, h1, h2, h3, h4, h5, h6, hr, p, pre {margin: 0}`; | ||||||
|  |  | ||||||
| export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void> { | export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void> { | ||||||
|   const el = elMarkup.querySelector('code.language-mermaid'); // .markup code.language-mermaid |   // .markup code.language-mermaid | ||||||
|   if (!el) return; |   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, | ||||||
|   mermaid.initialize({ |       theme: isDarkTheme() ? 'dark' : 'neutral', | ||||||
|     startOnLoad: false, |       securityLevel: 'strict', | ||||||
|     theme: isDarkTheme() ? 'dark' : 'neutral', |       suppressErrorRendering: true, | ||||||
|     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 = `<html><head><style>${iframeCss}</style></head><body>${svg}</body></html>`; |  | ||||||
|  |  | ||||||
|     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 <details> + <summary> block and the <details> 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); |     const pre = el.closest('pre'); | ||||||
|   } catch (err) { |     if (pre.hasAttribute('data-render-done')) return; | ||||||
|     displayError(pre, err); |  | ||||||
|   } |     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 = `<html><head><style>${iframeCss}</style></head><body>${svg}</body></html>`; | ||||||
|  |  | ||||||
|  |       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 <details> + <summary> block and the <details> 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); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user