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$$",
-			`<code class="language-math display">a</code>` + nl,
+			`<p><code class="language-math">a</code></p>` + 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, `<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
+\]
+`)
+}
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&regexp (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 @@
 									<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)}}
 								</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}} &nbsp;{{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}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p>
 							</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 */}}
 							<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>
 									{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}}
 								</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}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} &nbsp;{{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}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} &nbsp;{{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p>
 							{{end}}
 							</td>
 							<td class="two wide ui">
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 @@
 	<div class="ui container fluid padded">
 		<div class="ui top attached header clearing segment tw-relative commit-header">
 			<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}}
 					<div class="commit-header-buttons">
 						<a class="ui primary tiny button" href="{{.SourcePath}}">
@@ -122,7 +122,7 @@
 				{{end}}
 			</div>
 			{{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}}
 			{{template "repo/commit_load_branches_and_tags" .}}
 		</div>
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 @@
 							<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{.Summary | ctx.RenderUtils.RenderEmoji}}</span>
 						{{else}}
 							{{$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}}
 						</span>
 						{{if IsMultilineCommitMessage .Message}}
@@ -52,7 +52,7 @@
 						{{end}}
 						{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}}
 						{{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}}
 						{{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)}}
 
 		<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>
 
 		{{if IsMultilineCommitMessage .Message}}
@@ -29,7 +29,7 @@
 	</div>
 	{{if IsMultilineCommitMessage .Message}}
 	<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>
 	{{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 @@
 				<div class="ui segment flex-text-block tw-gap-4">
 					{{template "shared/issueicon" .}}
 					<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>
 					</div>
 					<a href="{{$.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui compact button primary">
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}}
 
 					<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 class="commit-refs flex-text-inline">
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)}}
 	<div class="issue-title" id="issue-title-display">
 		<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>
 		</h1>
 		<div class="issue-title-buttons">
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)}}
-	<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}}
 			<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}}
 	</span>
 {{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 @@
 			<div class="repo-file-cell message loading-icon-2px">
 				{{if $commit}}
 					{{$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}}
 					… {{/* will be loaded again by LastCommitLoaderURL */}}
 				{{end}}
diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl
index 47686dd442..366bb80b42 100644
--- a/templates/user/dashboard/feeds.tmpl
+++ b/templates/user/dashboard/feeds.tmpl
@@ -94,7 +94,7 @@
 								<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>
 								<span class="text truncate">
-									{{ctx.RenderUtils.RenderCommitMessage .Message ($repo.ComposeMetas ctx)}}
+									{{ctx.RenderUtils.RenderCommitMessage .Message ($repo.ComposeCommentMetas ctx)}}
 								</span>
 							</div>
 						{{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<void> {
-  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<void> {
-  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<void> {
-  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 = `<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);
+    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 = `<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);
+    }
+  });
 }