Skip to content

Fix markdown render behaviors #34122

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 =
;;

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Expand Down
4 changes: 2 additions & 2 deletions models/renderhelper/repo_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ 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)
rctx = rctx.WithMetas(map[string]string{
"user": helper.opts.DeprecatedOwnerName,
"repo": helper.opts.DeprecatedRepoName,

"markdownLineBreakStyle": "comment",
"markdownNewLineHardBreak": "true",
"markupAllowShortIssuePattern": "true",
})
}
Expand Down
4 changes: 1 addition & 3 deletions models/renderhelper/repo_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 0 additions & 1 deletion models/renderhelper/repo_wiki.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
})
}
Expand Down
27 changes: 14 additions & 13 deletions models/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -544,28 +544,29 @@ 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
}

// ComposeWikiMetas composes a map of metas for properly rendering wikis
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
}

Expand Down
6 changes: 3 additions & 3 deletions models/repo/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"])

Expand All @@ -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"])
Expand All @@ -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"])
Expand Down
13 changes: 2 additions & 11 deletions modules/markup/markdown/goldmark.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 5 additions & 5 deletions modules/markup/markdown/markdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
Expand Down
67 changes: 66 additions & 1 deletion modules/markup/markdown/markdown_math_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ 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"
)

const nl = "\n"

func TestMathRender(t *testing.T) {
setting.Markdown.MathCodeBlockOptions = setting.MarkdownMathCodeBlockOptions{ParseInlineDollar: true, ParseInlineParentheses: true}
testcases := []struct {
testcase string
expected string
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
\]
`)
}
38 changes: 21 additions & 17 deletions modules/markup/markdown/math/inline_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() parser.InlineParser {
return defaultInlineDollarParser
func NewInlineDollarParser(enableInlineDollar bool) parser.InlineParser {
return &inlineParser{
trigger: []byte{'$'},
endBytesSingleDollar: []byte{'$'},
endBytesDoubleDollar: []byte{'$', '$'},
enableInlineDollar: enableInlineDollar,
}
}

var defaultInlineBracketParser = &inlineParser{
trigger: []byte{'\\', '('},
endBytesBracket: []byte{'\\', ')'},
var defaultInlineParenthesesParser = &inlineParser{
trigger: []byte{'\\', '('},
endBytesParentheses: []byte{'\\', ')'},
}

func NewInlineBracketParser() parser.InlineParser {
return defaultInlineBracketParser
func NewInlineParenthesesParser() parser.InlineParser {
return defaultInlineParenthesesParser
}

// Trigger triggers this parser on $ or \
Expand All @@ -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 == ')'
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down
Loading