Skip to content

Commit e1c2d05

Browse files
authored
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
1 parent ee6929d commit e1c2d05

33 files changed

+419
-223
lines changed

custom/conf/app.example.ini

+14-8
Original file line numberDiff line numberDiff line change
@@ -1413,14 +1413,14 @@ LEVEL = Info
14131413
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
14141414
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
14151415
;;
1416-
;; Render soft line breaks as hard line breaks, which means a single newline character between
1417-
;; paragraphs will cause a line break and adding trailing whitespace to paragraphs is not
1418-
;; necessary to force a line break.
1419-
;; Render soft line breaks as hard line breaks for comments
1420-
;ENABLE_HARD_LINE_BREAK_IN_COMMENTS = true
1421-
;;
1422-
;; Render soft line breaks as hard line breaks for markdown documents
1423-
;ENABLE_HARD_LINE_BREAK_IN_DOCUMENTS = false
1416+
;; Customize render options for different contexts. Set to "none" to disable the defaults, or use comma separated list:
1417+
;; * short-issue-pattern: recognized "#123" issue reference and render it as a link to the issue
1418+
;; * new-line-hard-break: render soft line breaks as hard line breaks, which means a single newline character between
1419+
;; paragraphs will cause a line break and adding trailing whitespace to paragraphs is not
1420+
;; necessary to force a line break.
1421+
;RENDER_OPTIONS_COMMENT = short-issue-pattern, new-line-hard-break
1422+
;RENDER_OPTIONS_WIKI = short-issue-pattern
1423+
;RENDER_OPTIONS_REPO_FILE =
14241424
;;
14251425
;; Comma separated list of custom URL-Schemes that are allowed as links when rendering Markdown
14261426
;; for example git,magnet,ftp (more at https://en.wikipedia.org/wiki/List_of_URI_schemes)
@@ -1434,6 +1434,12 @@ LEVEL = Info
14341434
;;
14351435
;; Enables math inline and block detection
14361436
;ENABLE_MATH = true
1437+
;;
1438+
;; Enable delimiters for math code block detection. Set to "none" to disable all,
1439+
;; or use comma separated list: inline-dollar, inline-parentheses, block-dollar, block-square-brackets
1440+
;; Defaults to "inline-dollar,block-dollar" to follow GitHub's behavior.
1441+
;MATH_CODE_BLOCK_DETECTION =
1442+
;;
14371443

14381444
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
14391445
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

models/renderhelper/repo_comment.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,15 @@ func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repositor
5656
if repo != nil {
5757
helper.repoLink = repo.Link()
5858
helper.commitChecker = newCommitChecker(ctx, repo)
59-
rctx = rctx.WithMetas(repo.ComposeMetas(ctx))
59+
rctx = rctx.WithMetas(repo.ComposeCommentMetas(ctx))
6060
} else {
6161
// this is almost dead code, only to pass the incorrect tests
6262
helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
6363
rctx = rctx.WithMetas(map[string]string{
6464
"user": helper.opts.DeprecatedOwnerName,
6565
"repo": helper.opts.DeprecatedRepoName,
6666

67-
"markdownLineBreakStyle": "comment",
67+
"markdownNewLineHardBreak": "true",
6868
"markupAllowShortIssuePattern": "true",
6969
})
7070
}

models/renderhelper/repo_file.go

+1-3
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,13 @@ func NewRenderContextRepoFile(ctx context.Context, repo *repo_model.Repository,
6161
if repo != nil {
6262
helper.repoLink = repo.Link()
6363
helper.commitChecker = newCommitChecker(ctx, repo)
64-
rctx = rctx.WithMetas(repo.ComposeDocumentMetas(ctx))
64+
rctx = rctx.WithMetas(repo.ComposeRepoFileMetas(ctx))
6565
} else {
6666
// this is almost dead code, only to pass the incorrect tests
6767
helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
6868
rctx = rctx.WithMetas(map[string]string{
6969
"user": helper.opts.DeprecatedOwnerName,
7070
"repo": helper.opts.DeprecatedRepoName,
71-
72-
"markdownLineBreakStyle": "document",
7371
})
7472
}
7573
rctx = rctx.WithHelper(helper)

models/renderhelper/repo_wiki.go

-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ func NewRenderContextRepoWiki(ctx context.Context, repo *repo_model.Repository,
6868
"user": helper.opts.DeprecatedOwnerName,
6969
"repo": helper.opts.DeprecatedRepoName,
7070

71-
"markdownLineBreakStyle": "document",
7271
"markupAllowShortIssuePattern": "true",
7372
})
7473
}

models/repo/repo.go

+14-13
Original file line numberDiff line numberDiff line change
@@ -512,15 +512,15 @@ func (repo *Repository) composeCommonMetas(ctx context.Context) map[string]strin
512512
"repo": repo.Name,
513513
}
514514

515-
unit, err := repo.GetUnit(ctx, unit.TypeExternalTracker)
515+
unitExternalTracker, err := repo.GetUnit(ctx, unit.TypeExternalTracker)
516516
if err == nil {
517-
metas["format"] = unit.ExternalTrackerConfig().ExternalTrackerFormat
518-
switch unit.ExternalTrackerConfig().ExternalTrackerStyle {
517+
metas["format"] = unitExternalTracker.ExternalTrackerConfig().ExternalTrackerFormat
518+
switch unitExternalTracker.ExternalTrackerConfig().ExternalTrackerStyle {
519519
case markup.IssueNameStyleAlphanumeric:
520520
metas["style"] = markup.IssueNameStyleAlphanumeric
521521
case markup.IssueNameStyleRegexp:
522522
metas["style"] = markup.IssueNameStyleRegexp
523-
metas["regexp"] = unit.ExternalTrackerConfig().ExternalTrackerRegexpPattern
523+
metas["regexp"] = unitExternalTracker.ExternalTrackerConfig().ExternalTrackerRegexpPattern
524524
default:
525525
metas["style"] = markup.IssueNameStyleNumeric
526526
}
@@ -544,28 +544,29 @@ func (repo *Repository) composeCommonMetas(ctx context.Context) map[string]strin
544544
return repo.commonRenderingMetas
545545
}
546546

547-
// ComposeMetas composes a map of metas for properly rendering comments or comment-like contents (commit message)
548-
func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string {
547+
// ComposeCommentMetas composes a map of metas for properly rendering comments or comment-like contents (commit message)
548+
func (repo *Repository) ComposeCommentMetas(ctx context.Context) map[string]string {
549549
metas := maps.Clone(repo.composeCommonMetas(ctx))
550-
metas["markdownLineBreakStyle"] = "comment"
551-
metas["markupAllowShortIssuePattern"] = "true"
550+
metas["markdownNewLineHardBreak"] = strconv.FormatBool(setting.Markdown.RenderOptionsComment.NewLineHardBreak)
551+
metas["markupAllowShortIssuePattern"] = strconv.FormatBool(setting.Markdown.RenderOptionsComment.ShortIssuePattern)
552552
return metas
553553
}
554554

555555
// ComposeWikiMetas composes a map of metas for properly rendering wikis
556556
func (repo *Repository) ComposeWikiMetas(ctx context.Context) map[string]string {
557557
// does wiki need the "teams" and "org" from common metas?
558558
metas := maps.Clone(repo.composeCommonMetas(ctx))
559-
metas["markdownLineBreakStyle"] = "document"
560-
metas["markupAllowShortIssuePattern"] = "true"
559+
metas["markdownNewLineHardBreak"] = strconv.FormatBool(setting.Markdown.RenderOptionsWiki.NewLineHardBreak)
560+
metas["markupAllowShortIssuePattern"] = strconv.FormatBool(setting.Markdown.RenderOptionsWiki.ShortIssuePattern)
561561
return metas
562562
}
563563

564-
// ComposeDocumentMetas composes a map of metas for properly rendering documents (repo files)
565-
func (repo *Repository) ComposeDocumentMetas(ctx context.Context) map[string]string {
564+
// ComposeRepoFileMetas composes a map of metas for properly rendering documents (repo files)
565+
func (repo *Repository) ComposeRepoFileMetas(ctx context.Context) map[string]string {
566566
// does document(file) need the "teams" and "org" from common metas?
567567
metas := maps.Clone(repo.composeCommonMetas(ctx))
568-
metas["markdownLineBreakStyle"] = "document"
568+
metas["markdownNewLineHardBreak"] = strconv.FormatBool(setting.Markdown.RenderOptionsRepoFile.NewLineHardBreak)
569+
metas["markupAllowShortIssuePattern"] = strconv.FormatBool(setting.Markdown.RenderOptionsRepoFile.ShortIssuePattern)
569570
return metas
570571
}
571572

models/repo/repo_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func TestMetas(t *testing.T) {
8686

8787
repo.Units = nil
8888

89-
metas := repo.ComposeMetas(db.DefaultContext)
89+
metas := repo.ComposeCommentMetas(db.DefaultContext)
9090
assert.Equal(t, "testRepo", metas["repo"])
9191
assert.Equal(t, "testOwner", metas["user"])
9292

@@ -100,7 +100,7 @@ func TestMetas(t *testing.T) {
100100
testSuccess := func(expectedStyle string) {
101101
repo.Units = []*RepoUnit{&externalTracker}
102102
repo.commonRenderingMetas = nil
103-
metas := repo.ComposeMetas(db.DefaultContext)
103+
metas := repo.ComposeCommentMetas(db.DefaultContext)
104104
assert.Equal(t, expectedStyle, metas["style"])
105105
assert.Equal(t, "testRepo", metas["repo"])
106106
assert.Equal(t, "testOwner", metas["user"])
@@ -121,7 +121,7 @@ func TestMetas(t *testing.T) {
121121
repo, err := GetRepositoryByID(db.DefaultContext, 3)
122122
assert.NoError(t, err)
123123

124-
metas = repo.ComposeMetas(db.DefaultContext)
124+
metas = repo.ComposeCommentMetas(db.DefaultContext)
125125
assert.Contains(t, metas, "org")
126126
assert.Contains(t, metas, "teams")
127127
assert.Equal(t, "org3", metas["org"])

modules/markup/markdown/goldmark.go

+2-11
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"code.gitea.io/gitea/modules/container"
1010
"code.gitea.io/gitea/modules/markup"
1111
"code.gitea.io/gitea/modules/markup/internal"
12-
"code.gitea.io/gitea/modules/setting"
1312

1413
"github.com/yuin/goldmark/ast"
1514
east "github.com/yuin/goldmark/extension/ast"
@@ -69,16 +68,8 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
6968
g.transformList(ctx, v, rc)
7069
case *ast.Text:
7170
if v.SoftLineBreak() && !v.HardLineBreak() {
72-
// TODO: this was a quite unclear part, old code: `if metas["mode"] != "document" { use comment link break setting }`
73-
// many places render non-comment contents with no mode=document, then these contents also use comment's hard line break setting
74-
// especially in many tests.
75-
markdownLineBreakStyle := ctx.RenderOptions.Metas["markdownLineBreakStyle"]
76-
switch markdownLineBreakStyle {
77-
case "comment":
78-
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments)
79-
case "document":
80-
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments)
81-
}
71+
newLineHardBreak := ctx.RenderOptions.Metas["markdownNewLineHardBreak"] == "true"
72+
v.SetHardLineBreak(newLineHardBreak)
8273
}
8374
case *ast.CodeSpan:
8475
g.transformCodeSpan(ctx, v, reader)

modules/markup/markdown/markdown.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,11 @@ func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender {
126126
highlighting.WithWrapperRenderer(r.highlightingRenderer),
127127
),
128128
math.NewExtension(&ctx.RenderInternal, math.Options{
129-
Enabled: setting.Markdown.EnableMath,
130-
ParseDollarInline: true,
131-
ParseDollarBlock: true,
132-
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)
133-
// ParseBracketInline: true, // TODO: this is also a bad syntax "\( ... \)", it also conflicts, it should be deprecated in the future
129+
Enabled: setting.Markdown.EnableMath,
130+
ParseInlineDollar: setting.Markdown.MathCodeBlockOptions.ParseInlineDollar,
131+
ParseInlineParentheses: setting.Markdown.MathCodeBlockOptions.ParseInlineParentheses, // this is a bad syntax "\( ... \)", it conflicts with normal markdown escaping
132+
ParseBlockDollar: setting.Markdown.MathCodeBlockOptions.ParseBlockDollar,
133+
ParseBlockSquareBrackets: setting.Markdown.MathCodeBlockOptions.ParseBlockSquareBrackets, // this is a bad syntax "\[ ... \]", it conflicts with normal markdown escaping
134134
}),
135135
meta.Meta,
136136
),

modules/markup/markdown/markdown_math_test.go

+66-1
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ import (
88
"testing"
99

1010
"code.gitea.io/gitea/modules/markup"
11+
"code.gitea.io/gitea/modules/setting"
12+
"code.gitea.io/gitea/modules/test"
1113

1214
"github.com/stretchr/testify/assert"
1315
)
1416

1517
const nl = "\n"
1618

1719
func TestMathRender(t *testing.T) {
20+
setting.Markdown.MathCodeBlockOptions = setting.MarkdownMathCodeBlockOptions{ParseInlineDollar: true, ParseInlineParentheses: true}
1821
testcases := []struct {
1922
testcase string
2023
expected string
@@ -69,7 +72,7 @@ func TestMathRender(t *testing.T) {
6972
},
7073
{
7174
"$$a$$",
72-
`<code class="language-math display">a</code>` + nl,
75+
`<p><code class="language-math">a</code></p>` + nl,
7376
},
7477
{
7578
"$$a$$ test",
@@ -111,6 +114,7 @@ func TestMathRender(t *testing.T) {
111114
}
112115

113116
func TestMathRenderBlockIndent(t *testing.T) {
117+
setting.Markdown.MathCodeBlockOptions = setting.MarkdownMathCodeBlockOptions{ParseBlockDollar: true, ParseBlockSquareBrackets: true}
114118
testcases := []struct {
115119
name string
116120
testcase string
@@ -243,3 +247,64 @@ x
243247
})
244248
}
245249
}
250+
251+
func TestMathRenderOptions(t *testing.T) {
252+
setting.Markdown.MathCodeBlockOptions = setting.MarkdownMathCodeBlockOptions{}
253+
defer test.MockVariableValue(&setting.Markdown.MathCodeBlockOptions)
254+
test := func(t *testing.T, expected, input string) {
255+
res, err := RenderString(markup.NewTestRenderContext(), input)
256+
assert.NoError(t, err)
257+
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(res)), "input: %s", input)
258+
}
259+
260+
// default (non-conflict) inline syntax
261+
test(t, `<p><code class="language-math">a</code></p>`, "$`a`$")
262+
263+
// ParseInlineDollar
264+
test(t, `<p>$a$</p>`, `$a$`)
265+
setting.Markdown.MathCodeBlockOptions.ParseInlineDollar = true
266+
test(t, `<p><code class="language-math">a</code></p>`, `$a$`)
267+
268+
// ParseInlineParentheses
269+
test(t, `<p>(a)</p>`, `\(a\)`)
270+
setting.Markdown.MathCodeBlockOptions.ParseInlineParentheses = true
271+
test(t, `<p><code class="language-math">a</code></p>`, `\(a\)`)
272+
273+
// ParseBlockDollar
274+
test(t, `<p>$$
275+
a
276+
$$</p>
277+
`, `
278+
$$
279+
a
280+
$$
281+
`)
282+
setting.Markdown.MathCodeBlockOptions.ParseBlockDollar = true
283+
test(t, `<pre class="code-block is-loading"><code class="language-math display">
284+
a
285+
</code></pre>
286+
`, `
287+
$$
288+
a
289+
$$
290+
`)
291+
292+
// ParseBlockSquareBrackets
293+
test(t, `<p>[
294+
a
295+
]</p>
296+
`, `
297+
\[
298+
a
299+
\]
300+
`)
301+
setting.Markdown.MathCodeBlockOptions.ParseBlockSquareBrackets = true
302+
test(t, `<pre class="code-block is-loading"><code class="language-math display">
303+
a
304+
</code></pre>
305+
`, `
306+
\[
307+
a
308+
\]
309+
`)
310+
}

modules/markup/markdown/math/inline_parser.go

+21-17
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,26 @@ type inlineParser struct {
1515
trigger []byte
1616
endBytesSingleDollar []byte
1717
endBytesDoubleDollar []byte
18-
endBytesBracket []byte
18+
endBytesParentheses []byte
19+
enableInlineDollar bool
1920
}
2021

21-
var defaultInlineDollarParser = &inlineParser{
22-
trigger: []byte{'$'},
23-
endBytesSingleDollar: []byte{'$'},
24-
endBytesDoubleDollar: []byte{'$', '$'},
25-
}
26-
27-
func NewInlineDollarParser() parser.InlineParser {
28-
return defaultInlineDollarParser
22+
func NewInlineDollarParser(enableInlineDollar bool) parser.InlineParser {
23+
return &inlineParser{
24+
trigger: []byte{'$'},
25+
endBytesSingleDollar: []byte{'$'},
26+
endBytesDoubleDollar: []byte{'$', '$'},
27+
enableInlineDollar: enableInlineDollar,
28+
}
2929
}
3030

31-
var defaultInlineBracketParser = &inlineParser{
32-
trigger: []byte{'\\', '('},
33-
endBytesBracket: []byte{'\\', ')'},
31+
var defaultInlineParenthesesParser = &inlineParser{
32+
trigger: []byte{'\\', '('},
33+
endBytesParentheses: []byte{'\\', ')'},
3434
}
3535

36-
func NewInlineBracketParser() parser.InlineParser {
37-
return defaultInlineBracketParser
36+
func NewInlineParenthesesParser() parser.InlineParser {
37+
return defaultInlineParenthesesParser
3838
}
3939

4040
// Trigger triggers this parser on $ or \
@@ -46,7 +46,7 @@ func isPunctuation(b byte) bool {
4646
return b == '.' || b == '!' || b == '?' || b == ',' || b == ';' || b == ':'
4747
}
4848

49-
func isBracket(b byte) bool {
49+
func isParenthesesClose(b byte) bool {
5050
return b == ')'
5151
}
5252

@@ -86,7 +86,11 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.
8686
}
8787
} else {
8888
startMarkLen = 2
89-
stopMark = parser.endBytesBracket
89+
stopMark = parser.endBytesParentheses
90+
}
91+
92+
if line[0] == '$' && !parser.enableInlineDollar && (len(line) == 1 || line[1] != '`') {
93+
return nil
9094
}
9195

9296
if checkSurrounding {
@@ -110,7 +114,7 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.
110114
succeedingCharacter = line[i+len(stopMark)]
111115
}
112116
// check valid ending character
113-
isValidEndingChar := isPunctuation(succeedingCharacter) || isBracket(succeedingCharacter) ||
117+
isValidEndingChar := isPunctuation(succeedingCharacter) || isParenthesesClose(succeedingCharacter) ||
114118
succeedingCharacter == ' ' || succeedingCharacter == '\n' || succeedingCharacter == 0
115119
if checkSurrounding && !isValidEndingChar {
116120
break

0 commit comments

Comments
 (0)