Skip to content

Commit 88c2e24

Browse files
zeripathsilverwindlunny
authored
Add KaTeX rendering to Markdown. (#20571)
This PR adds mathematical rendering with KaTeX. The first step is to add a Goldmark extension that detects the latex (and tex) mathematics delimiters. The second step to make this extension only run if math support is enabled. The second step is to then add KaTeX CSS and JS to the head which will load after the dom is rendered. Fix #3445 Signed-off-by: Andrew Thornton <[email protected]> Signed-off-by: Andrew Thornton <[email protected]> Co-authored-by: silverwind <[email protected]> Co-authored-by: Lunny Xiao <[email protected]>
1 parent eaa5611 commit 88c2e24

28 files changed

+1077
-172
lines changed

custom/conf/app.example.ini

+3
Original file line numberDiff line numberDiff line change
@@ -1262,6 +1262,9 @@ ROUTER = console
12621262
;; List of file extensions that should be rendered/edited as Markdown
12631263
;; Separate the extensions with a comma. To render files without any extension as markdown, just put a comma
12641264
;FILE_EXTENSIONS = .md,.markdown,.mdown,.mkd
1265+
;;
1266+
;; Enables math inline and block detection
1267+
;ENABLE_MATH = true
12651268

12661269
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
12671270
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

docs/content/doc/advanced/config-cheat-sheet.en-us.md

+1
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a
236236
- `CUSTOM_URL_SCHEMES`: Use a comma separated list (ftp,git,svn) to indicate additional
237237
URL hyperlinks to be rendered in Markdown. URLs beginning in http and https are
238238
always displayed
239+
- `ENABLE_MATH`: **true**: Enables detection of `\(...\)`, `\[...\]`, `$...$` and `$$...$$` blocks as math blocks.
239240

240241
## Server (`server`)
241242

docs/content/doc/advanced/external-renderers.en-us.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,13 @@ RENDER_COMMAND = "timeout 30s pandoc +RTS -M512M -RTS -f rst"
7474
IS_INPUT_FILE = false
7575
```
7676

77-
If your external markup relies on additional classes and attributes on the generated HTML elements, you might need to enable custom sanitizer policies. Gitea uses the [`bluemonday`](https://godoc.org/github.com/microcosm-cc/bluemonday) package as our HTML sanitizier. The example below will support [KaTeX](https://katex.org/) output from [`pandoc`](https://pandoc.org/).
77+
If your external markup relies on additional classes and attributes on the generated HTML elements, you might need to enable custom sanitizer policies. Gitea uses the [`bluemonday`](https://godoc.org/github.com/microcosm-cc/bluemonday) package as our HTML sanitizer. The example below could be used to support server-side [KaTeX](https://katex.org/) rendering output from [`pandoc`](https://pandoc.org/).
7878

7979
```ini
8080
[markup.sanitizer.TeX]
8181
; Pandoc renders TeX segments as <span>s with the "math" class, optionally
8282
; with "inline" or "display" classes depending on context.
83+
; - note this is different from the built-in math support in our markdown parser which uses <code>
8384
ELEMENT = span
8485
ALLOW_ATTR = class
8586
REGEXP = ^\s*((math(\s+|$)|inline(\s+|$)|display(\s+|$)))+

docs/content/doc/features/comparison.en-us.md

+2
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ _Symbols used in table:_
5353
| WebAuthn (2FA) ||||||| ? |
5454
| Built-in CI/CD ||||||||
5555
| Subgroups: groups within groups | [](https://github.com/go-gitea/gitea/issues/1872) |||||||
56+
| Mermaid diagrams in Markdown ||||||||
57+
| Math syntax in Markdown ||||||||
5658

5759
## Code management
5860

docs/content/page/index.en-us.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ You can try it out using [the online demo](https://try.gitea.io/).
131131
- Environment variables
132132
- Command line options
133133
- Multi-language support ([21 languages](https://github.com/go-gitea/gitea/tree/main/options/locale))
134-
- [Mermaid](https://mermaidjs.github.io/) Diagram support
134+
- [Mermaid](https://mermaidjs.github.io/) diagrams in Markdown
135+
- Math syntax in Markdown
135136
- Mail service
136137
- Notifications
137138
- Registration confirmation

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ require (
103103
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
104104
gopkg.in/ini.v1 v1.67.0
105105
gopkg.in/yaml.v2 v2.4.0
106+
gopkg.in/yaml.v3 v3.0.1
106107
mvdan.cc/xurls/v2 v2.4.0
107108
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251
108109
xorm.io/builder v0.3.11
@@ -290,7 +291,6 @@ require (
290291
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
291292
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
292293
gopkg.in/warnings.v0 v0.1.2 // indirect
293-
gopkg.in/yaml.v3 v3.0.1 // indirect
294294
sigs.k8s.io/yaml v1.2.0 // indirect
295295
)
296296

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package markdown
6+
7+
import (
8+
"github.com/yuin/goldmark/ast"
9+
east "github.com/yuin/goldmark/extension/ast"
10+
"gopkg.in/yaml.v3"
11+
)
12+
13+
func nodeToTable(meta *yaml.Node) ast.Node {
14+
for {
15+
if meta == nil {
16+
return nil
17+
}
18+
switch meta.Kind {
19+
case yaml.DocumentNode:
20+
meta = meta.Content[0]
21+
continue
22+
default:
23+
}
24+
break
25+
}
26+
switch meta.Kind {
27+
case yaml.MappingNode:
28+
return mappingNodeToTable(meta)
29+
case yaml.SequenceNode:
30+
return sequenceNodeToTable(meta)
31+
default:
32+
return ast.NewString([]byte(meta.Value))
33+
}
34+
}
35+
36+
func mappingNodeToTable(meta *yaml.Node) ast.Node {
37+
table := east.NewTable()
38+
alignments := []east.Alignment{}
39+
for i := 0; i < len(meta.Content); i += 2 {
40+
alignments = append(alignments, east.AlignNone)
41+
}
42+
43+
headerRow := east.NewTableRow(alignments)
44+
valueRow := east.NewTableRow(alignments)
45+
for i := 0; i < len(meta.Content); i += 2 {
46+
cell := east.NewTableCell()
47+
48+
cell.AppendChild(cell, nodeToTable(meta.Content[i]))
49+
headerRow.AppendChild(headerRow, cell)
50+
51+
if i+1 < len(meta.Content) {
52+
cell = east.NewTableCell()
53+
cell.AppendChild(cell, nodeToTable(meta.Content[i+1]))
54+
valueRow.AppendChild(valueRow, cell)
55+
}
56+
}
57+
58+
table.AppendChild(table, east.NewTableHeader(headerRow))
59+
table.AppendChild(table, valueRow)
60+
return table
61+
}
62+
63+
func sequenceNodeToTable(meta *yaml.Node) ast.Node {
64+
table := east.NewTable()
65+
alignments := []east.Alignment{east.AlignNone}
66+
for _, item := range meta.Content {
67+
row := east.NewTableRow(alignments)
68+
cell := east.NewTableCell()
69+
cell.AppendChild(cell, nodeToTable(item))
70+
row.AppendChild(row, cell)
71+
table.AppendChild(table, row)
72+
}
73+
return table
74+
}
75+
76+
func nodeToDetails(meta *yaml.Node, icon string) ast.Node {
77+
details := NewDetails()
78+
summary := NewSummary()
79+
summary.AppendChild(summary, NewIcon(icon))
80+
details.AppendChild(details, summary)
81+
details.AppendChild(details, nodeToTable(meta))
82+
83+
return details
84+
}

modules/markup/markdown/goldmark.go

+3-12
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
"code.gitea.io/gitea/modules/setting"
1616
giteautil "code.gitea.io/gitea/modules/util"
1717

18-
meta "github.com/yuin/goldmark-meta"
1918
"github.com/yuin/goldmark/ast"
2019
east "github.com/yuin/goldmark/extension/ast"
2120
"github.com/yuin/goldmark/parser"
@@ -32,20 +31,12 @@ type ASTTransformer struct{}
3231

3332
// Transform transforms the given AST tree.
3433
func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
35-
metaData := meta.GetItems(pc)
3634
firstChild := node.FirstChild()
3735
createTOC := false
3836
ctx := pc.Get(renderContextKey).(*markup.RenderContext)
39-
rc := &RenderConfig{
40-
Meta: "table",
41-
Icon: "table",
42-
Lang: "",
43-
}
44-
45-
if metaData != nil {
46-
rc.ToRenderConfig(metaData)
47-
48-
metaNode := rc.toMetaNode(metaData)
37+
rc := pc.Get(renderConfigKey).(*RenderConfig)
38+
if rc.yamlNode != nil {
39+
metaNode := rc.toMetaNode()
4940
if metaNode != nil {
5041
node.InsertBefore(node, firstChild, metaNode)
5142
}

modules/markup/markdown/markdown.go

+18-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"code.gitea.io/gitea/modules/log"
1515
"code.gitea.io/gitea/modules/markup"
1616
"code.gitea.io/gitea/modules/markup/common"
17+
"code.gitea.io/gitea/modules/markup/markdown/math"
1718
"code.gitea.io/gitea/modules/setting"
1819
giteautil "code.gitea.io/gitea/modules/util"
1920

@@ -38,6 +39,7 @@ var (
3839
isWikiKey = parser.NewContextKey()
3940
renderMetasKey = parser.NewContextKey()
4041
renderContextKey = parser.NewContextKey()
42+
renderConfigKey = parser.NewContextKey()
4143
)
4244

4345
type limitWriter struct {
@@ -98,7 +100,7 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer)
98100
languageStr := string(language)
99101

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

@@ -120,6 +122,9 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer)
120122
}
121123
}),
122124
),
125+
math.NewExtension(
126+
math.Enabled(setting.Markdown.EnableMath),
127+
),
123128
meta.Meta,
124129
),
125130
goldmark.WithParserOptions(
@@ -167,7 +172,18 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer)
167172
log.Error("Unable to ReadAll: %v", err)
168173
return err
169174
}
170-
if err := converter.Convert(giteautil.NormalizeEOL(buf), lw, parser.WithContext(pc)); err != nil {
175+
buf = giteautil.NormalizeEOL(buf)
176+
177+
rc := &RenderConfig{
178+
Meta: "table",
179+
Icon: "table",
180+
Lang: "",
181+
}
182+
buf, _ = ExtractMetadataBytes(buf, rc)
183+
184+
pc.Set(renderConfigKey, rc)
185+
186+
if err := converter.Convert(buf, lw, parser.WithContext(pc)); err != nil {
171187
log.Error("Unable to render: %v", err)
172188
return err
173189
}
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package math
6+
7+
import "github.com/yuin/goldmark/ast"
8+
9+
// Block represents a display math block e.g. $$...$$ or \[...\]
10+
type Block struct {
11+
ast.BaseBlock
12+
Dollars bool
13+
Indent int
14+
Closed bool
15+
}
16+
17+
// KindBlock is the node kind for math blocks
18+
var KindBlock = ast.NewNodeKind("MathBlock")
19+
20+
// NewBlock creates a new math Block
21+
func NewBlock(dollars bool, indent int) *Block {
22+
return &Block{
23+
Dollars: dollars,
24+
Indent: indent,
25+
}
26+
}
27+
28+
// Dump dumps the block to a string
29+
func (n *Block) Dump(source []byte, level int) {
30+
m := map[string]string{}
31+
ast.DumpHelper(n, source, level, m, nil)
32+
}
33+
34+
// Kind returns KindBlock for math Blocks
35+
func (n *Block) Kind() ast.NodeKind {
36+
return KindBlock
37+
}
38+
39+
// IsRaw returns true as this block should not be processed further
40+
func (n *Block) IsRaw() bool {
41+
return true
42+
}

0 commit comments

Comments
 (0)