Skip to content

Refactor markdown math render, add dollor-backquote syntax support #32831

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
Dec 14, 2024
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
28 changes: 15 additions & 13 deletions modules/markup/markdown/markdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,26 +78,23 @@ func (r *GlodmarkRender) Renderer() renderer.Renderer {

func (r *GlodmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
if entering {
language, _ := c.Language()
if language == nil {
language = []byte("text")
}

languageStr := string(language)
languageBytes, _ := c.Language()
languageStr := giteautil.IfZero(string(languageBytes), "text")

preClasses := []string{"code-block"}
preClasses := "code-block"
if languageStr == "mermaid" || languageStr == "math" {
preClasses = append(preClasses, "is-loading")
preClasses += " is-loading"
}

err := r.ctx.RenderInternal.FormatWithSafeAttrs(w, `<pre class="%s">`, strings.Join(preClasses, " "))
err := r.ctx.RenderInternal.FormatWithSafeAttrs(w, `<pre class="%s">`, preClasses)
if err != nil {
return
}

// include language-x class as part of commonmark spec
// the "display" class is used by "js/markup/math.js" to render the code element as a block
err = r.ctx.RenderInternal.FormatWithSafeAttrs(w, `<code class="chroma language-%s display">`, string(language))
// include language-x class as part of commonmark spec, "chroma" class is used to highlight the code
// the "display" class is used by "js/markup/math.ts" to render the code element as a block
// the "math.ts" strictly depends on the structure: <pre class="code-block is-loading"><code class="language-math display">...</code></pre>
err = r.ctx.RenderInternal.FormatWithSafeAttrs(w, `<code class="chroma language-%s display">`, languageStr)
if err != nil {
return
}
Expand Down Expand Up @@ -128,7 +125,12 @@ func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender {
),
highlighting.WithWrapperRenderer(r.highlightingRenderer),
),
math.NewExtension(&ctx.RenderInternal, math.Enabled(setting.Markdown.EnableMath)),
math.NewExtension(&ctx.RenderInternal, math.Options{
Enabled: setting.Markdown.EnableMath,
ParseDollarInline: true,
ParseDollarBlock: true,
ParseSquareBlock: true, // TODO: this is a bad syntax, it should be deprecated in the future (by some config options)
}),
meta.Meta,
),
goldmark.WithParserOptions(
Expand Down
20 changes: 19 additions & 1 deletion modules/markup/markdown/markdown_math_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import (
"github.com/stretchr/testify/assert"
)

const nl = "\n"

func TestMathRender(t *testing.T) {
const nl = "\n"
testcases := []struct {
testcase string
expected string
Expand Down Expand Up @@ -86,6 +87,18 @@ func TestMathRender(t *testing.T) {
`$\text{$b$}$`,
`<p><code class="language-math">\text{$b$}</code></p>` + nl,
},
{
"a$`b`$c",
`<p>a<code class="language-math">b</code>c</p>` + nl,
},
{
"a $`b`$ c",
`<p>a <code class="language-math">b</code> c</p>` + nl,
},
{
"a$``b``$c x$```y```$z",
`<p>a<code class="language-math">b</code>c x<code class="language-math">y</code>z</p>` + nl,
},
}

for _, test := range testcases {
Expand Down Expand Up @@ -215,6 +228,11 @@ x
</ol>
`,
},
{
"inline-non-math",
`\[x]`,
`<p>[x]</p>` + nl,
},
}

for _, test := range testcases {
Expand Down
25 changes: 17 additions & 8 deletions modules/markup/markdown/math/block_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,18 @@ import (

type blockParser struct {
parseDollars bool
parseSquare bool
endBytesDollars []byte
endBytesBracket []byte
endBytesSquare []byte
}

// NewBlockParser creates a new math BlockParser
func NewBlockParser(parseDollarBlocks bool) parser.BlockParser {
func NewBlockParser(parseDollars, parseSquare bool) parser.BlockParser {
return &blockParser{
parseDollars: parseDollarBlocks,
parseDollars: parseDollars,
parseSquare: parseSquare,
endBytesDollars: []byte{'$', '$'},
endBytesBracket: []byte{'\\', ']'},
endBytesSquare: []byte{'\\', ']'},
}
}

Expand All @@ -40,7 +42,7 @@ func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Contex
var dollars bool
if b.parseDollars && line[pos] == '$' && line[pos+1] == '$' {
dollars = true
} else if line[pos] == '\\' && line[pos+1] == '[' {
} else if b.parseSquare && line[pos] == '\\' && line[pos+1] == '[' {
if len(line[pos:]) >= 3 && line[pos+2] == '!' && bytes.Contains(line[pos:], []byte(`\]`)) {
// do not process escaped attention block: "> \[!NOTE\]"
return nil, parser.NoChildren
Expand All @@ -53,10 +55,10 @@ func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Contex
node := NewBlock(dollars, pos)

// Now we need to check if the ending block is on the segment...
endBytes := giteaUtil.Iif(dollars, b.endBytesDollars, b.endBytesBracket)
endBytes := giteaUtil.Iif(dollars, b.endBytesDollars, b.endBytesSquare)
idx := bytes.Index(line[pos+2:], endBytes)
if idx >= 0 {
// for case $$ ... $$ any other text
// for case: "$$ ... $$ any other text" (this case will be handled by the inline parser)
for i := pos + 2 + idx + 2; i < len(line); i++ {
if line[i] != ' ' && line[i] != '\n' {
return nil, parser.NoChildren
Expand All @@ -70,6 +72,13 @@ func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Contex
return node, parser.Close | parser.NoChildren
}

// for case "\[ ... ]" (no close marker on the same line)
for i := pos + 2 + idx + 2; i < len(line); i++ {
if line[i] != ' ' && line[i] != '\n' {
return nil, parser.NoChildren
}
}

segment.Start += pos + 2
node.Lines().Append(segment)
return node, parser.NoChildren
Expand All @@ -85,7 +94,7 @@ func (b *blockParser) Continue(node ast.Node, reader text.Reader, pc parser.Cont
line, segment := reader.PeekLine()
w, pos := util.IndentWidth(line, reader.LineOffset())
if w < 4 {
endBytes := giteaUtil.Iif(block.Dollars, b.endBytesDollars, b.endBytesBracket)
endBytes := giteaUtil.Iif(block.Dollars, b.endBytesDollars, b.endBytesSquare)
if bytes.HasPrefix(line[pos:], endBytes) && util.IsBlank(line[pos+len(endBytes):]) {
if util.IsBlank(line[pos+len(endBytes):]) {
newline := giteaUtil.Iif(line[len(line)-1] != '\n', 0, 1)
Expand Down
31 changes: 0 additions & 31 deletions modules/markup/markdown/math/inline_block_node.go

This file was deleted.

2 changes: 1 addition & 1 deletion modules/markup/markdown/math/inline_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/yuin/goldmark/util"
)

// Inline represents inline math e.g. $...$ or \(...\)
// Inline struct represents inline math e.g. $...$ or \(...\)
type Inline struct {
ast.BaseInline
}
Expand Down
90 changes: 51 additions & 39 deletions modules/markup/markdown/math/inline_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,25 @@ import (
)

type inlineParser struct {
start []byte
end []byte
trigger []byte
endBytesSingleDollar []byte
endBytesDoubleDollar []byte
endBytesBracket []byte
}

var defaultInlineDollarParser = &inlineParser{
start: []byte{'$'},
end: []byte{'$'},
}

var defaultDualDollarParser = &inlineParser{
start: []byte{'$', '$'},
end: []byte{'$', '$'},
trigger: []byte{'$'},
endBytesSingleDollar: []byte{'$'},
endBytesDoubleDollar: []byte{'$', '$'},
}

func NewInlineDollarParser() parser.InlineParser {
return defaultInlineDollarParser
}

func NewInlineDualDollarParser() parser.InlineParser {
return defaultDualDollarParser
}

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

func NewInlineBracketParser() parser.InlineParser {
Expand All @@ -45,7 +39,7 @@ func NewInlineBracketParser() parser.InlineParser {

// Trigger triggers this parser on $ or \
func (parser *inlineParser) Trigger() []byte {
return parser.start
return parser.trigger
}

func isPunctuation(b byte) bool {
Expand All @@ -64,33 +58,60 @@ func isAlphanumeric(b byte) bool {
func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
line, _ := block.PeekLine()

if !bytes.HasPrefix(line, parser.start) {
if !bytes.HasPrefix(line, parser.trigger) {
// We'll catch this one on the next time round
return nil
}

precedingCharacter := block.PrecendingCharacter()
if precedingCharacter < 256 && (isAlphanumeric(byte(precedingCharacter)) || isPunctuation(byte(precedingCharacter))) {
// need to exclude things like `a$` from being considered a start
return nil
var startMarkLen int
var stopMark []byte
checkSurrounding := true
if line[0] == '$' {
startMarkLen = 1
stopMark = parser.endBytesSingleDollar
if len(line) > 1 {
if line[1] == '$' {
startMarkLen = 2
stopMark = parser.endBytesDoubleDollar
} else if line[1] == '`' {
pos := 1
for ; pos < len(line) && line[pos] == '`'; pos++ {
}
startMarkLen = pos
stopMark = bytes.Repeat([]byte{'`'}, pos)
stopMark[len(stopMark)-1] = '$'
checkSurrounding = false
}
}
} else {
startMarkLen = 2
stopMark = parser.endBytesBracket
}

if checkSurrounding {
precedingCharacter := block.PrecendingCharacter()
if precedingCharacter < 256 && (isAlphanumeric(byte(precedingCharacter)) || isPunctuation(byte(precedingCharacter))) {
// need to exclude things like `a$` from being considered a start
return nil
}
}

// move the opener marker point at the start of the text
opener := len(parser.start)
opener := startMarkLen

// Now look for an ending line
depth := 0
ender := -1
for i := opener; i < len(line); i++ {
if depth == 0 && bytes.HasPrefix(line[i:], parser.end) {
if depth == 0 && bytes.HasPrefix(line[i:], stopMark) {
succeedingCharacter := byte(0)
if i+len(parser.end) < len(line) {
succeedingCharacter = line[i+len(parser.end)]
if i+len(stopMark) < len(line) {
succeedingCharacter = line[i+len(stopMark)]
}
// check valid ending character
isValidEndingChar := isPunctuation(succeedingCharacter) || isBracket(succeedingCharacter) ||
succeedingCharacter == ' ' || succeedingCharacter == '\n' || succeedingCharacter == 0
if !isValidEndingChar {
if checkSurrounding && !isValidEndingChar {
break
}
ender = i
Expand All @@ -112,21 +133,12 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.

block.Advance(opener)
_, pos := block.Position()
var node ast.Node
if parser == defaultDualDollarParser {
node = NewInlineBlock()
} else {
node = NewInline()
}
node := NewInline()

segment := pos.WithStop(pos.Start + ender - opener)
node.AppendChild(node, ast.NewRawTextSegment(segment))
block.Advance(ender - opener + len(parser.end))

if parser == defaultDualDollarParser {
trimBlock(&(node.(*InlineBlock)).Inline, block)
} else {
trimBlock(node.(*Inline), block)
}
block.Advance(ender - opener + len(stopMark))
trimBlock(node, block)
return node
}

Expand Down
1 change: 0 additions & 1 deletion modules/markup/markdown/math/inline_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,4 @@ func (r *InlineRenderer) renderInline(w util.BufWriter, source []byte, n ast.Nod
// RegisterFuncs registers the renderer for inline math nodes
func (r *InlineRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(KindInline, r.renderInline)
reg.Register(KindInlineBlock, r.renderInline)
}
Loading
Loading