From f57fb785902f8b12f8982385cc34bc9848e12d4b Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Thu, 22 Jul 2021 22:32:25 +0100 Subject: [PATCH 01/16] Add KaTeX rendering to Markdown. 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 --- custom/conf/app.example.ini | 6 + .../doc/advanced/config-cheat-sheet.en-us.md | 2 + modules/context/context.go | 1 + modules/markup/markdown/markdown.go | 5 + modules/markup/markdown/math/block_node.go | 36 +++++ modules/markup/markdown/math/block_parser.go | 104 ++++++++++++++ .../markup/markdown/math/block_renderer.go | 46 ++++++ modules/markup/markdown/math/inline_node.go | 49 +++++++ modules/markup/markdown/math/inline_parser.go | 103 ++++++++++++++ .../markup/markdown/math/inline_renderer.go | 50 +++++++ modules/markup/markdown/math/math.go | 134 ++++++++++++++++++ modules/markup/sanitizer.go | 2 +- modules/setting/setting.go | 4 + package-lock.json | 39 +++++ package.json | 1 + templates/base/footer.tmpl | 4 + templates/base/head.tmpl | 3 + web_src/js/katex.js | 41 ++++++ webpack.config.js | 6 + 19 files changed, 635 insertions(+), 1 deletion(-) create mode 100644 modules/markup/markdown/math/block_node.go create mode 100644 modules/markup/markdown/math/block_parser.go create mode 100644 modules/markup/markdown/math/block_renderer.go create mode 100644 modules/markup/markdown/math/inline_node.go create mode 100644 modules/markup/markdown/math/inline_parser.go create mode 100644 modules/markup/markdown/math/inline_renderer.go create mode 100644 modules/markup/markdown/math/math.go create mode 100644 web_src/js/katex.js diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 1c6a7e3b7c61..327effdfcaaf 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1225,6 +1225,12 @@ ROUTER = console ;; List of file extensions that should be rendered/edited as Markdown ;; Separate the extensions with a comma. To render files without any extension as markdown, just put a comma ;FILE_EXTENSIONS = .md,.markdown,.mdown,.mkd +;; +;; Enables math inline and block detection +;ENABLE_MATH = true +;; +;; Enables in addition inline block detection using single dollars +;ENABLE_INLINE_DOLLAR_MATH = false ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index cb2b9526d7db..cae1c79bc89d 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -233,6 +233,8 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `CUSTOM_URL_SCHEMES`: Use a comma separated list (ftp,git,svn) to indicate additional URL hyperlinks to be rendered in Markdown. URLs beginning in http and https are always displayed +- `ENABLE_MATH`: **true**: Enables detection of `\(...\)`, `\[...\]` and `$$...$$` blocks as math blocks +- `ENABLE_INLINE_DOLLAR_MATH`: **false**: In addition enables detection of `$...$` as inline math. ## Server (`server`) diff --git a/modules/context/context.go b/modules/context/context.go index 882491161992..098a4e4e270d 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -710,6 +710,7 @@ func Contexter() func(next http.Handler) http.Handler { ctx.PageData = map[string]interface{}{} ctx.Data["PageData"] = ctx.PageData ctx.Data["Context"] = &ctx + ctx.Data["MathEnabled"] = setting.Markdown.EnableMath ctx.Req = WithContext(req, &ctx) ctx.csrf = PrepareCSRFProtector(csrfOpts, &ctx) diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index 4ce85dfc3187..48748eec3618 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/common" + "code.gitea.io/gitea/modules/markup/markdown/math" "code.gitea.io/gitea/modules/setting" giteautil "code.gitea.io/gitea/modules/util" @@ -120,6 +121,10 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) } }), ), + math.NewExtension( + math.Enabled(setting.Markdown.EnableMath), + math.WithInlineDollarParser(setting.Markdown.EnableInlineDollarMath), + ), meta.Meta, ), goldmark.WithParserOptions( diff --git a/modules/markup/markdown/math/block_node.go b/modules/markup/markdown/math/block_node.go new file mode 100644 index 000000000000..535b2900f7e4 --- /dev/null +++ b/modules/markup/markdown/math/block_node.go @@ -0,0 +1,36 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package math + +import "github.com/yuin/goldmark/ast" + +// Block represents a math Block +type Block struct { + ast.BaseBlock +} + +// KindBlock is the node kind for math blocks +var KindBlock = ast.NewNodeKind("MathBlock") + +// NewBlock creates a new math Block +func NewBlock() *Block { + return &Block{} +} + +// Dump dumps the block to a string +func (n *Block) Dump(source []byte, level int) { + m := map[string]string{} + ast.DumpHelper(n, source, level, m, nil) +} + +// Kind returns KindBlock for math Blocks +func (n *Block) Kind() ast.NodeKind { + return KindBlock +} + +// IsRaw returns true as this block should not be processed further +func (n *Block) IsRaw() bool { + return true +} diff --git a/modules/markup/markdown/math/block_parser.go b/modules/markup/markdown/math/block_parser.go new file mode 100644 index 000000000000..9d6e16672bd7 --- /dev/null +++ b/modules/markup/markdown/math/block_parser.go @@ -0,0 +1,104 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package math + +import ( + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" +) + +type blockParser struct { + parseDollars bool +} + +type blockData struct { + dollars bool + indent int +} + +var blockInfoKey = parser.NewContextKey() + +// NewBlockParser creates a new math BlockParser +func NewBlockParser(parseDollarBlocks bool) parser.BlockParser { + return &blockParser{ + parseDollars: parseDollarBlocks, + } +} + +// Open parses the current line and returns a result of parsing. +func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { + line, _ := reader.PeekLine() + pos := pc.BlockOffset() + if pos == -1 || len(line[pos:]) < 2 { + return nil, parser.NoChildren + } + + dollars := false + if b.parseDollars && line[pos] == '$' && line[pos+1] == '$' { + dollars = true + } else if line[pos] != '\\' || line[pos+1] != '[' { + return nil, parser.NoChildren + } + + pc.Set(blockInfoKey, &blockData{dollars: dollars, indent: pos}) + node := NewBlock() + return node, parser.NoChildren +} + +// Continue parses the current line and returns a result of parsing. +func (b *blockParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State { + line, segment := reader.PeekLine() + data := pc.Get(blockInfoKey).(*blockData) + w, pos := util.IndentWidth(line, 0) + if w < 4 { + if data.dollars { + i := pos + for ; i < len(line) && line[i] == '$'; i++ { + } + length := i - pos + if length >= 2 && util.IsBlank(line[i:]) { + reader.Advance(segment.Stop - segment.Start - segment.Padding) + return parser.Close + } + } else if len(line[pos:]) > 1 && line[pos] == '\\' && line[pos+1] == ']' && util.IsBlank(line[pos+2:]) { + reader.Advance(segment.Stop - segment.Start - segment.Padding) + return parser.Close + } + } + + pos, padding := util.IndentPosition(line, 0, data.indent) + seg := text.NewSegmentPadding(segment.Start+pos, segment.Stop, padding) + node.Lines().Append(seg) + reader.AdvanceAndSetPadding(segment.Stop-segment.Start-pos-1, padding) + return parser.Continue | parser.NoChildren +} + +// Close will be called when the parser returns Close. +func (b *blockParser) Close(node ast.Node, reader text.Reader, pc parser.Context) { + pc.Set(blockInfoKey, nil) +} + +// CanInterruptParagraph returns true if the parser can interrupt paragraphs, +// otherwise false. +func (b *blockParser) CanInterruptParagraph() bool { + return true +} + +// CanAcceptIndentedLine returns true if the parser can open new node when +// the given line is being indented more than 3 spaces. +func (b *blockParser) CanAcceptIndentedLine() bool { + return false +} + +// Trigger returns a list of characters that triggers Parse method of +// this parser. +// If Trigger returns a nil, Open will be called with any lines. +// +// We leave this as nil as our parse method is quick enough +func (b *blockParser) Trigger() []byte { + return nil +} diff --git a/modules/markup/markdown/math/block_renderer.go b/modules/markup/markdown/math/block_renderer.go new file mode 100644 index 000000000000..b17eada5abe7 --- /dev/null +++ b/modules/markup/markdown/math/block_renderer.go @@ -0,0 +1,46 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package math + +import ( + gast "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/util" +) + +// BlockRenderer represents a renderer for math Blocks +type BlockRenderer struct { + startDelim string + endDelim string +} + +// NewBlockRenderer creates a new renderer for math Blocks +func NewBlockRenderer(start, end string) renderer.NodeRenderer { + return &BlockRenderer{start, end} +} + +// RegisterFuncs registers the renderer for math Blocks +func (r *BlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(KindBlock, r.renderBlock) +} + +func (r *BlockRenderer) writeLines(w util.BufWriter, source []byte, n gast.Node) { + l := n.Lines().Len() + for i := 0; i < l; i++ { + line := n.Lines().At(i) + _, _ = w.Write(util.EscapeHTML(line.Value(source))) + } +} + +func (r *BlockRenderer) renderBlock(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { + n := node.(*Block) + if entering { + _, _ = w.WriteString(`

` + r.startDelim) + r.writeLines(w, source, n) + } else { + _, _ = w.WriteString(r.endDelim + `

` + "\n") + } + return gast.WalkContinue, nil +} diff --git a/modules/markup/markdown/math/inline_node.go b/modules/markup/markdown/math/inline_node.go new file mode 100644 index 000000000000..877a94d53cbe --- /dev/null +++ b/modules/markup/markdown/math/inline_node.go @@ -0,0 +1,49 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package math + +import ( + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/util" +) + +// Inline represents inline math +type Inline struct { + ast.BaseInline +} + +// Inline implements Inline.Inline. +func (n *Inline) Inline() {} + +// IsBlank returns if this inline node is empty +func (n *Inline) IsBlank(source []byte) bool { + for c := n.FirstChild(); c != nil; c = c.NextSibling() { + text := c.(*ast.Text).Segment + if !util.IsBlank(text.Value(source)) { + return false + } + } + return true +} + +// Dump renders this inline math as debug +func (n *Inline) Dump(source []byte, level int) { + ast.DumpHelper(n, source, level, nil, nil) +} + +// KindInline is the kind for math inline +var KindInline = ast.NewNodeKind("MathInline") + +// Kind returns KindInline +func (n *Inline) Kind() ast.NodeKind { + return KindInline +} + +// NewInline creates a new ast math inline node +func NewInline() *Inline { + return &Inline{ + BaseInline: ast.BaseInline{}, + } +} diff --git a/modules/markup/markdown/math/inline_parser.go b/modules/markup/markdown/math/inline_parser.go new file mode 100644 index 000000000000..0222b0342433 --- /dev/null +++ b/modules/markup/markdown/math/inline_parser.go @@ -0,0 +1,103 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package math + +import ( + "bytes" + + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" +) + +type inlineParser struct { + start []byte + end []byte +} + +var defaultInlineDollarParser = &inlineParser{ + start: []byte{'$'}, + end: []byte{'$'}, +} + +// NewInlineDollarParser returns a new inline parser +func NewInlineDollarParser() parser.InlineParser { + return defaultInlineDollarParser +} + +var defaultInlineBracketParser = &inlineParser{ + start: []byte{'\\', '('}, + end: []byte{'\\', ')'}, +} + +// NewInlineDollarParser returns a new inline parser +func NewInlineBracketParser() parser.InlineParser { + return defaultInlineBracketParser +} + +// Trigger triggers this parser on $ +func (parser *inlineParser) Trigger() []byte { + return parser.start[0:1] +} + +// Parse parses the current line and returns a result of parsing. +func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { + line, startSegment := block.PeekLine() + opener := bytes.Index(line, parser.start) + if opener < 0 { + return nil + } + opener += len(parser.start) + block.Advance(opener) + l, pos := block.Position() + node := NewInline() + + for { + line, segment := block.PeekLine() + if line == nil { + block.SetPosition(l, pos) + return ast.NewTextSegment(startSegment.WithStop(startSegment.Start + opener)) + } + + closer := bytes.Index(line, parser.end) + if closer < 0 { + if !util.IsBlank(line) { + node.AppendChild(node, ast.NewRawTextSegment(segment)) + } + block.AdvanceLine() + continue + } + segment = segment.WithStop(segment.Start + closer) + if !segment.IsEmpty() { + node.AppendChild(node, ast.NewRawTextSegment(segment)) + } + block.Advance(closer + len(parser.end)) + break + } + + trimBlock(node, block) + return node +} + +func trimBlock(node *Inline, block text.Reader) { + if node.IsBlank(block.Source()) { + return + } + + // trim first space and last space + first := node.FirstChild().(*ast.Text) + if !(!first.Segment.IsEmpty() && block.Source()[first.Segment.Start] == ' ') { + return + } + + last := node.LastChild().(*ast.Text) + if !(!last.Segment.IsEmpty() && block.Source()[last.Segment.Stop-1] == ' ') { + return + } + + first.Segment = first.Segment.WithStart(first.Segment.Start + 1) + last.Segment = last.Segment.WithStop(last.Segment.Stop - 1) +} diff --git a/modules/markup/markdown/math/inline_renderer.go b/modules/markup/markdown/math/inline_renderer.go new file mode 100644 index 000000000000..ce6aafdca2a4 --- /dev/null +++ b/modules/markup/markdown/math/inline_renderer.go @@ -0,0 +1,50 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package math + +import ( + "bytes" + + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/util" +) + +// InlineRenderer is an inline renderer +type InlineRenderer struct { + startDelim string + endDelim string +} + +// NewInlineRenderer returns a new renderer for inline math +func NewInlineRenderer(start, end string) renderer.NodeRenderer { + return &InlineRenderer{start, end} +} + +func (r *InlineRenderer) renderInline(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { + if entering { + _, _ = w.WriteString(`` + r.startDelim) + for c := n.FirstChild(); c != nil; c = c.NextSibling() { + segment := c.(*ast.Text).Segment + value := util.EscapeHTML(segment.Value(source)) + if bytes.HasSuffix(value, []byte("\n")) { + _, _ = w.Write(value[:len(value)-1]) + if c != n.LastChild() { + _, _ = w.Write([]byte(" ")) + } + } else { + _, _ = w.Write(value) + } + } + return ast.WalkSkipChildren, nil + } + _, _ = w.WriteString(r.endDelim + ``) + return ast.WalkContinue, nil +} + +// RegisterFuncs registers the renderer for inline math nodes +func (r *InlineRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(KindInline, r.renderInline) +} diff --git a/modules/markup/markdown/math/math.go b/modules/markup/markdown/math/math.go new file mode 100644 index 000000000000..6eb20b54c50d --- /dev/null +++ b/modules/markup/markdown/math/math.go @@ -0,0 +1,134 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package math + +import ( + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/util" +) + +// Extension is a math extension +type Extension struct { + enabled bool + inlineStartDelimRender string + inlineEndDelimRender string + blockStartDelimRender string + blockEndDelimRender string + parseDollarInline bool + parseDollarBlock bool +} + +// Option is the interface Options should implement +type Option interface { + SetOption(e *Extension) +} + +type extensionFunc func(e *Extension) + +func (fn extensionFunc) SetOption(e *Extension) { + fn(e) +} + +// Enabled enables or disables this extension +func Enabled(enable ...bool) Option { + value := true + if len(enable) > 0 { + value = enable[0] + } + return extensionFunc(func(e *Extension) { + e.enabled = value + }) +} + +// WithInlineDollarParser enables or disables the parsing of $...$ +func WithInlineDollarParser(enable ...bool) Option { + value := true + if len(enable) > 0 { + value = enable[0] + } + return extensionFunc(func(e *Extension) { + e.parseDollarInline = value + }) +} + +// WithBlockDollarParser enables or disables the parsing of $$...$$ +func WithBlockDollarParser(enable ...bool) Option { + value := true + if len(enable) > 0 { + value = enable[0] + } + return extensionFunc(func(e *Extension) { + e.parseDollarBlock = value + }) +} + +// WithInlineDelimRender sets the start and end strings for the rendered inline delimiters +func WithInlineDelimRender(start, end string) Option { + return extensionFunc(func(e *Extension) { + e.inlineStartDelimRender = start + e.inlineEndDelimRender = end + }) +} + +// WithBlockDelimRender sets the start and end strings for the rendered block delimiters +func WithBlockDelimRender(start, end string) Option { + return extensionFunc(func(e *Extension) { + e.blockStartDelimRender = start + e.blockEndDelimRender = end + }) +} + +// Math represents a math extension with default rendered delimiters +var Math = &Extension{ + enabled: true, + inlineStartDelimRender: `\(`, + inlineEndDelimRender: `\)`, + blockStartDelimRender: `\[`, + blockEndDelimRender: `\]`, + parseDollarBlock: true, +} + +// NewExtension creates a new math extension with the provided options +func NewExtension(opts ...Option) *Extension { + r := &Extension{ + enabled: true, + inlineStartDelimRender: `\(`, + inlineEndDelimRender: `\)`, + blockStartDelimRender: `\[`, + blockEndDelimRender: `\]`, + parseDollarBlock: true, + } + + for _, o := range opts { + o.SetOption(r) + } + return r +} + +// Extend extends goldmark with our parsers and renderers +func (e *Extension) Extend(m goldmark.Markdown) { + if !e.enabled { + return + } + + m.Parser().AddOptions(parser.WithBlockParsers( + util.Prioritized(NewBlockParser(e.parseDollarBlock), 701), + )) + + inlines := []util.PrioritizedValue{ + util.Prioritized(NewInlineBracketParser(), 501), + } + if e.parseDollarInline { + inlines = append(inlines, util.Prioritized(NewInlineDollarParser(), 501)) + } + m.Parser().AddOptions(parser.WithInlineParsers(inlines...)) + + m.Renderer().AddOptions(renderer.WithNodeRenderers( + util.Prioritized(NewBlockRenderer(e.blockStartDelimRender, e.blockEndDelimRender), 501), + util.Prioritized(NewInlineRenderer(e.inlineStartDelimRender, e.inlineEndDelimRender), 502), + )) +} diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index 57e88fdabc81..ba8ecd445c20 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -83,7 +83,7 @@ func createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img") // Allow icons, emojis, chroma syntax and keyword markup on span - policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(math display)|(math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span") // Allow 'style' attribute on text elements. policy.AllowAttrs("style").OnElements("span", "p") diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 23e3280dc9f1..80b046628a1e 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -332,10 +332,14 @@ var ( EnableHardLineBreakInDocuments bool CustomURLSchemes []string `ini:"CUSTOM_URL_SCHEMES"` FileExtensions []string + EnableMath bool + EnableInlineDollarMath bool }{ EnableHardLineBreakInComments: true, EnableHardLineBreakInDocuments: false, FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd", ","), + EnableMath: true, + EnableInlineDollarMath: false, } // Admin settings diff --git a/package-lock.json b/package-lock.json index 8ebff450e2a5..cb9e21fd2b1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "font-awesome": "4.7.0", "jquery": "3.6.0", "jquery.are-you-sure": "1.9.0", + "katex": "0.16.0", "less": "4.1.3", "less-loader": "11.0.0", "license-checker-webpack-plugin": "0.2.1", @@ -8673,6 +8674,29 @@ "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz", "integrity": "sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ==" }, + "node_modules/katex": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.0.tgz", + "integrity": "sha512-wPRB4iUPysfH97wTgG5/tRLYxmKVq6Q4jRAWRVOUxXB1dsiv4cvcNjqabHkrOvJHM1Bpk3WrgmllSO1vIvP24w==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "dependencies": { + "commander": "^8.0.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "engines": { + "node": ">= 12" + } + }, "node_modules/khroma": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz", @@ -19465,6 +19489,21 @@ "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz", "integrity": "sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ==" }, + "katex": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.0.tgz", + "integrity": "sha512-wPRB4iUPysfH97wTgG5/tRLYxmKVq6Q4jRAWRVOUxXB1dsiv4cvcNjqabHkrOvJHM1Bpk3WrgmllSO1vIvP24w==", + "requires": { + "commander": "^8.0.0" + }, + "dependencies": { + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==" + } + } + }, "khroma": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz", diff --git a/package.json b/package.json index e4741f98fec8..2b8ef5c791a1 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "font-awesome": "4.7.0", "jquery": "3.6.0", "jquery.are-you-sure": "1.9.0", + "katex": "0.16.0", "less": "4.1.3", "less-loader": "11.0.0", "license-checker-webpack-plugin": "0.2.1", diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl index 9bf16f8aa5b5..91263241ec88 100644 --- a/templates/base/footer.tmpl +++ b/templates/base/footer.tmpl @@ -23,6 +23,10 @@ {{end}} {{end}} +{{if .MathEnabled}} + +{{end}} + {{template "custom/footer" .}} diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index e0d2b26f2cdb..e811db8fa7ab 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -72,6 +72,9 @@ {{else if ne DefaultTheme "gitea"}} {{end}} +{{if .MathEnabled}} + +{{end}} {{template "custom/header" .}} diff --git a/web_src/js/katex.js b/web_src/js/katex.js new file mode 100644 index 000000000000..7234ef10d755 --- /dev/null +++ b/web_src/js/katex.js @@ -0,0 +1,41 @@ +import renderMathInElement from 'katex/dist/contrib/auto-render.js'; + +const mathNodes = document.querySelectorAll('.math'); + +const ourRender = (nodes) => { + for (const element of nodes) { + if (element.hasAttribute('katex-rendered') || !element.textContent) { + continue; + } + + renderMathInElement(element, { + delimiters: [ + {left: '\\[', right: '\\]', display: true}, + {left: '\\(', right: '\\)', display: false} + ], + errorCallback: (_, stack) => { + element.setAttribute('title', stack); + }, + }); + element.setAttribute('katex-rendered', 'yes'); + } +}; + +ourRender(mathNodes); + +// Options for the observer (which mutations to observe) +const config = {childList: true, subtree: true}; + +// Callback function to execute when mutations are observed +const callback = (records) => { + for (const record of records) { + const mathNodes = record.target.querySelectorAll('.math'); + ourRender(mathNodes); + } +}; + +// Create an observer instance linked to the callback function +const observer = new MutationObserver(callback); + +// Start observing the target node for configured mutations +observer.observe(document, config); diff --git a/webpack.config.js b/webpack.config.js index 5109103f7faf..0763a26fd41b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -62,6 +62,12 @@ export default { 'eventsource.sharedworker': [ fileURLToPath(new URL('web_src/js/features/eventsource.sharedworker.js', import.meta.url)), ], + 'katex': [ + fileURLToPath(new URL('node_modules/katex/dist/katex.min.js', import.meta.url)), + fileURLToPath(new URL('node_modules/katex/dist/contrib/auto-render.min.js', import.meta.url)), + fileURLToPath(new URL('web_src/js/katex.js', import.meta.url)), + fileURLToPath(new URL('node_modules/katex/dist/katex.min.css', import.meta.url)), + ], ...themes, }, devtool: false, From 3ac5fb518620701e1ca153737ed3261398e068e9 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Tue, 2 Aug 2022 15:34:14 +0100 Subject: [PATCH 02/16] Switch to always available rendering with ```math Signed-off-by: Andrew Thornton --- modules/context/context.go | 1 - modules/markup/markdown/markdown.go | 2 +- .../markup/markdown/math/block_renderer.go | 13 ++--- .../markup/markdown/math/inline_renderer.go | 13 ++--- modules/markup/markdown/math/math.go | 46 ++++-------------- modules/markup/sanitizer.go | 4 +- templates/base/footer.tmpl | 3 -- templates/base/head.tmpl | 3 -- web_src/js/katex.js | 4 +- web_src/js/markup/content.js | 2 + web_src/js/markup/katex.js | 47 +++++++++++++++++++ webpack.config.js | 7 +-- 12 files changed, 74 insertions(+), 71 deletions(-) create mode 100644 web_src/js/markup/katex.js diff --git a/modules/context/context.go b/modules/context/context.go index 098a4e4e270d..882491161992 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -710,7 +710,6 @@ func Contexter() func(next http.Handler) http.Handler { ctx.PageData = map[string]interface{}{} ctx.Data["PageData"] = ctx.PageData ctx.Data["Context"] = &ctx - ctx.Data["MathEnabled"] = setting.Markdown.EnableMath ctx.Req = WithContext(req, &ctx) ctx.csrf = PrepareCSRFProtector(csrfOpts, &ctx) diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index 48748eec3618..cd72f6068848 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -99,7 +99,7 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) languageStr := string(language) preClasses := []string{"code-block"} - if languageStr == "mermaid" { + if languageStr == "mermaid" || languageStr == "math" { preClasses = append(preClasses, "is-loading") } diff --git a/modules/markup/markdown/math/block_renderer.go b/modules/markup/markdown/math/block_renderer.go index b17eada5abe7..d502065259ec 100644 --- a/modules/markup/markdown/math/block_renderer.go +++ b/modules/markup/markdown/math/block_renderer.go @@ -11,14 +11,11 @@ import ( ) // BlockRenderer represents a renderer for math Blocks -type BlockRenderer struct { - startDelim string - endDelim string -} +type BlockRenderer struct{} // NewBlockRenderer creates a new renderer for math Blocks -func NewBlockRenderer(start, end string) renderer.NodeRenderer { - return &BlockRenderer{start, end} +func NewBlockRenderer() renderer.NodeRenderer { + return &BlockRenderer{} } // RegisterFuncs registers the renderer for math Blocks @@ -37,10 +34,10 @@ func (r *BlockRenderer) writeLines(w util.BufWriter, source []byte, n gast.Node) func (r *BlockRenderer) renderBlock(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { n := node.(*Block) if entering { - _, _ = w.WriteString(`

` + r.startDelim) + _, _ = w.WriteString(`

`)
 		r.writeLines(w, source, n)
 	} else {
-		_, _ = w.WriteString(r.endDelim + `

` + "\n") + _, _ = w.WriteString(`
` + "\n") } return gast.WalkContinue, nil } diff --git a/modules/markup/markdown/math/inline_renderer.go b/modules/markup/markdown/math/inline_renderer.go index ce6aafdca2a4..e4c0f3761dac 100644 --- a/modules/markup/markdown/math/inline_renderer.go +++ b/modules/markup/markdown/math/inline_renderer.go @@ -13,19 +13,16 @@ import ( ) // InlineRenderer is an inline renderer -type InlineRenderer struct { - startDelim string - endDelim string -} +type InlineRenderer struct{} // NewInlineRenderer returns a new renderer for inline math -func NewInlineRenderer(start, end string) renderer.NodeRenderer { - return &InlineRenderer{start, end} +func NewInlineRenderer() renderer.NodeRenderer { + return &InlineRenderer{} } func (r *InlineRenderer) renderInline(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { if entering { - _, _ = w.WriteString(`` + r.startDelim) + _, _ = w.WriteString(``) for c := n.FirstChild(); c != nil; c = c.NextSibling() { segment := c.(*ast.Text).Segment value := util.EscapeHTML(segment.Value(source)) @@ -40,7 +37,7 @@ func (r *InlineRenderer) renderInline(w util.BufWriter, source []byte, n ast.Nod } return ast.WalkSkipChildren, nil } - _, _ = w.WriteString(r.endDelim + ``) + _, _ = w.WriteString(``) return ast.WalkContinue, nil } diff --git a/modules/markup/markdown/math/math.go b/modules/markup/markdown/math/math.go index 6eb20b54c50d..e896084fb8a4 100644 --- a/modules/markup/markdown/math/math.go +++ b/modules/markup/markdown/math/math.go @@ -13,13 +13,9 @@ import ( // Extension is a math extension type Extension struct { - enabled bool - inlineStartDelimRender string - inlineEndDelimRender string - blockStartDelimRender string - blockEndDelimRender string - parseDollarInline bool - parseDollarBlock bool + enabled bool + parseDollarInline bool + parseDollarBlock bool } // Option is the interface Options should implement @@ -66,41 +62,17 @@ func WithBlockDollarParser(enable ...bool) Option { }) } -// WithInlineDelimRender sets the start and end strings for the rendered inline delimiters -func WithInlineDelimRender(start, end string) Option { - return extensionFunc(func(e *Extension) { - e.inlineStartDelimRender = start - e.inlineEndDelimRender = end - }) -} - -// WithBlockDelimRender sets the start and end strings for the rendered block delimiters -func WithBlockDelimRender(start, end string) Option { - return extensionFunc(func(e *Extension) { - e.blockStartDelimRender = start - e.blockEndDelimRender = end - }) -} - // Math represents a math extension with default rendered delimiters var Math = &Extension{ - enabled: true, - inlineStartDelimRender: `\(`, - inlineEndDelimRender: `\)`, - blockStartDelimRender: `\[`, - blockEndDelimRender: `\]`, - parseDollarBlock: true, + enabled: true, + parseDollarBlock: true, } // NewExtension creates a new math extension with the provided options func NewExtension(opts ...Option) *Extension { r := &Extension{ - enabled: true, - inlineStartDelimRender: `\(`, - inlineEndDelimRender: `\)`, - blockStartDelimRender: `\[`, - blockEndDelimRender: `\]`, - parseDollarBlock: true, + enabled: true, + parseDollarBlock: true, } for _, o := range opts { @@ -128,7 +100,7 @@ func (e *Extension) Extend(m goldmark.Markdown) { m.Parser().AddOptions(parser.WithInlineParsers(inlines...)) m.Renderer().AddOptions(renderer.WithNodeRenderers( - util.Prioritized(NewBlockRenderer(e.blockStartDelimRender, e.blockEndDelimRender), 501), - util.Prioritized(NewInlineRenderer(e.inlineStartDelimRender, e.inlineEndDelimRender), 502), + util.Prioritized(NewBlockRenderer(), 501), + util.Prioritized(NewInlineRenderer(), 502), )) } diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index ba8ecd445c20..807a8a7892b3 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -56,7 +56,7 @@ func createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre") // For Chroma markdown plugin - policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+$`)).OnElements("code") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+( display)?( is-loading)?$`)).OnElements("code") // Checkboxes policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input") @@ -83,7 +83,7 @@ func createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img") // Allow icons, emojis, chroma syntax and keyword markup on span - policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(math display)|(math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span") // Allow 'style' attribute on text elements. policy.AllowAttrs("style").OnElements("span", "p") diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl index 91263241ec88..cd783d92c537 100644 --- a/templates/base/footer.tmpl +++ b/templates/base/footer.tmpl @@ -23,9 +23,6 @@ {{end}} {{end}} -{{if .MathEnabled}} - -{{end}} {{template "custom/footer" .}} diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index e811db8fa7ab..e0d2b26f2cdb 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -72,9 +72,6 @@ {{else if ne DefaultTheme "gitea"}} {{end}} -{{if .MathEnabled}} - -{{end}} {{template "custom/header" .}} diff --git a/web_src/js/katex.js b/web_src/js/katex.js index 7234ef10d755..38bc6c89e4ba 100644 --- a/web_src/js/katex.js +++ b/web_src/js/katex.js @@ -1,6 +1,6 @@ import renderMathInElement from 'katex/dist/contrib/auto-render.js'; -const mathNodes = document.querySelectorAll('.math'); +const mathNodes = document.querySelectorAll('.language-math'); const ourRender = (nodes) => { for (const element of nodes) { @@ -29,7 +29,7 @@ const config = {childList: true, subtree: true}; // Callback function to execute when mutations are observed const callback = (records) => { for (const record of records) { - const mathNodes = record.target.querySelectorAll('.math'); + const mathNodes = record.target.querySelectorAll('.language-math'); ourRender(mathNodes); } }; diff --git a/web_src/js/markup/content.js b/web_src/js/markup/content.js index ef5067fd6652..5c0428c69692 100644 --- a/web_src/js/markup/content.js +++ b/web_src/js/markup/content.js @@ -1,10 +1,12 @@ import {renderMermaid} from './mermaid.js'; +import {renderMath} from './katex.js'; import {renderCodeCopy} from './codecopy.js'; import {initMarkupTasklist} from './tasklist.js'; // code that runs for all markup content export function initMarkupContent() { renderMermaid(); + renderMath(); renderCodeCopy(); } diff --git a/web_src/js/markup/katex.js b/web_src/js/markup/katex.js new file mode 100644 index 000000000000..9b4eb13e94a0 --- /dev/null +++ b/web_src/js/markup/katex.js @@ -0,0 +1,47 @@ +function displayError(el, err) { + let target = el; + if (el.classList.contains('is-loading')) { + // assume no pre + el.classList.remove('is-loading'); + } else { + target = el.closest('pre'); + target.classList.remove('is-loading'); + } + const errorNode = document.createElement('div'); + errorNode.setAttribute('class', 'ui message error markup-block-error mono'); + errorNode.textContent = err.str || err.message || String(err); + target.before(errorNode); +} + +// eslint-disable-next-line import/no-unused-modules +export async function renderMath() { + const els = document.querySelectorAll('.markup code.language-math'); + if (!els.length) return; + + const {default: katex} = await import(/* webpackChunkName: "katex" */'katex'); + + for (const el of els) { + const source = el.textContent; + + const options = {}; + options.display = el.classList.contains('display'); + + try { + const markup = katex.renderToString(source, options); + let target; + if (options.display) { + target = document.createElement('p'); + } else { + target = document.createElement('span'); + } + target.innerHTML = markup; + if (el.classList.contains('is-loading')) { + el.replaceWith(target); + } else { + el.closest('pre').replaceWith(target); + } + } catch (error) { + displayError(el, error); + } + } +} diff --git a/webpack.config.js b/webpack.config.js index 0763a26fd41b..b69d276d32d7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -50,6 +50,7 @@ export default { fileURLToPath(new URL('web_src/js/index.js', import.meta.url)), fileURLToPath(new URL('node_modules/easymde/dist/easymde.min.css', import.meta.url)), fileURLToPath(new URL('web_src/fomantic/build/semantic.css', import.meta.url)), + fileURLToPath(new URL('node_modules/katex/dist/katex.css', import.meta.url)), fileURLToPath(new URL('web_src/less/index.less', import.meta.url)), ], swagger: [ @@ -62,12 +63,6 @@ export default { 'eventsource.sharedworker': [ fileURLToPath(new URL('web_src/js/features/eventsource.sharedworker.js', import.meta.url)), ], - 'katex': [ - fileURLToPath(new URL('node_modules/katex/dist/katex.min.js', import.meta.url)), - fileURLToPath(new URL('node_modules/katex/dist/contrib/auto-render.min.js', import.meta.url)), - fileURLToPath(new URL('web_src/js/katex.js', import.meta.url)), - fileURLToPath(new URL('node_modules/katex/dist/katex.min.css', import.meta.url)), - ], ...themes, }, devtool: false, From 1f8100608526b701976731b11ab75aaa2fd36766 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Tue, 2 Aug 2022 19:45:35 +0100 Subject: [PATCH 03/16] remove goldmark meta and handle yaml frontmatter ourselves Signed-off-by: Andrew Thornton --- go.mod | 2 +- modules/markup/markdown/convertyaml.go | 84 +++++++ modules/markup/markdown/goldmark.go | 15 +- modules/markup/markdown/markdown.go | 14 +- modules/markup/markdown/meta.go | 96 ++++++-- modules/markup/markdown/meta_test.go | 32 +++ modules/markup/markdown/renderconfig.go | 219 ++++++++----------- modules/markup/markdown/renderconfig_test.go | 162 ++++++++++++++ 8 files changed, 457 insertions(+), 167 deletions(-) create mode 100644 modules/markup/markdown/convertyaml.go create mode 100644 modules/markup/markdown/renderconfig_test.go diff --git a/go.mod b/go.mod index 6d41af507d35..30f877c880bb 100644 --- a/go.mod +++ b/go.mod @@ -101,6 +101,7 @@ require ( gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/ini.v1 v1.66.4 gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b mvdan.cc/xurls/v2 v2.4.0 strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 xorm.io/builder v0.3.11 @@ -286,7 +287,6 @@ require ( gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect sigs.k8s.io/yaml v1.2.0 // indirect ) diff --git a/modules/markup/markdown/convertyaml.go b/modules/markup/markdown/convertyaml.go new file mode 100644 index 000000000000..3f5ebec90899 --- /dev/null +++ b/modules/markup/markdown/convertyaml.go @@ -0,0 +1,84 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package markdown + +import ( + "github.com/yuin/goldmark/ast" + east "github.com/yuin/goldmark/extension/ast" + "gopkg.in/yaml.v3" +) + +func nodeToTable(meta *yaml.Node) ast.Node { + for { + if meta == nil { + return nil + } + switch meta.Kind { + case yaml.DocumentNode: + meta = meta.Content[0] + continue + default: + } + break + } + switch meta.Kind { + case yaml.MappingNode: + return mappingNodeToTable(meta) + case yaml.SequenceNode: + return sequenceNodeToTable(meta) + default: + return ast.NewString([]byte(meta.Value)) + } +} + +func mappingNodeToTable(meta *yaml.Node) ast.Node { + table := east.NewTable() + alignments := []east.Alignment{} + for i := 0; i < len(meta.Content); i += 2 { + alignments = append(alignments, east.AlignNone) + } + + headerRow := east.NewTableRow(alignments) + valueRow := east.NewTableRow(alignments) + for i := 0; i < len(meta.Content); i += 2 { + cell := east.NewTableCell() + + cell.AppendChild(cell, nodeToTable(meta.Content[i])) + headerRow.AppendChild(headerRow, cell) + + if i+1 < len(meta.Content) { + cell = east.NewTableCell() + cell.AppendChild(cell, nodeToTable(meta.Content[i+1])) + valueRow.AppendChild(valueRow, cell) + } + } + + table.AppendChild(table, east.NewTableHeader(headerRow)) + table.AppendChild(table, valueRow) + return table +} + +func sequenceNodeToTable(meta *yaml.Node) ast.Node { + table := east.NewTable() + alignments := []east.Alignment{east.AlignNone} + for _, item := range meta.Content { + row := east.NewTableRow(alignments) + cell := east.NewTableCell() + cell.AppendChild(cell, nodeToTable(item)) + row.AppendChild(row, cell) + table.AppendChild(table, row) + } + return table +} + +func nodeToDetails(meta *yaml.Node, icon string) ast.Node { + details := NewDetails() + summary := NewSummary() + summary.AppendChild(summary, NewIcon(icon)) + details.AppendChild(details, summary) + details.AppendChild(details, nodeToTable(meta)) + + return details +} diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index 1750128dec85..24f1ab7a0167 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -15,7 +15,6 @@ import ( "code.gitea.io/gitea/modules/setting" giteautil "code.gitea.io/gitea/modules/util" - meta "github.com/yuin/goldmark-meta" "github.com/yuin/goldmark/ast" east "github.com/yuin/goldmark/extension/ast" "github.com/yuin/goldmark/parser" @@ -32,20 +31,12 @@ type ASTTransformer struct{} // Transform transforms the given AST tree. func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { - metaData := meta.GetItems(pc) firstChild := node.FirstChild() createTOC := false ctx := pc.Get(renderContextKey).(*markup.RenderContext) - rc := &RenderConfig{ - Meta: "table", - Icon: "table", - Lang: "", - } - - if metaData != nil { - rc.ToRenderConfig(metaData) - - metaNode := rc.toMetaNode(metaData) + rc := pc.Get(renderConfigKey).(*RenderConfig) + if rc.yamlNode != nil { + metaNode := rc.toMetaNode() if metaNode != nil { node.InsertBefore(node, firstChild, metaNode) } diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index cd72f6068848..c10922f7679d 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -39,6 +39,7 @@ var ( isWikiKey = parser.NewContextKey() renderMetasKey = parser.NewContextKey() renderContextKey = parser.NewContextKey() + renderConfigKey = parser.NewContextKey() ) type limitWriter struct { @@ -172,7 +173,18 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) log.Error("Unable to ReadAll: %v", err) return err } - if err := converter.Convert(giteautil.NormalizeEOL(buf), lw, parser.WithContext(pc)); err != nil { + buf = giteautil.NormalizeEOL(buf) + + rc := &RenderConfig{ + Meta: "table", + Icon: "table", + Lang: "", + } + buf, _ = ExtractMetadataBytes(buf, rc) + + pc.Set(renderConfigKey, rc) + + if err := converter.Convert(buf, lw, parser.WithContext(pc)); err != nil { log.Error("Unable to render: %v", err) return err } diff --git a/modules/markup/markdown/meta.go b/modules/markup/markdown/meta.go index faf92ae2c6b7..28913fd68457 100644 --- a/modules/markup/markdown/meta.go +++ b/modules/markup/markdown/meta.go @@ -5,47 +5,101 @@ package markdown import ( + "bytes" "errors" - "strings" + "unicode" + "unicode/utf8" - "gopkg.in/yaml.v2" + "code.gitea.io/gitea/modules/log" + "gopkg.in/yaml.v3" ) -func isYAMLSeparator(line string) bool { - line = strings.TrimSpace(line) - for i := 0; i < len(line); i++ { - if line[i] != '-' { +func isYAMLSeparator(line []byte) bool { + idx := 0 + for ; idx < len(line); idx++ { + if line[idx] >= utf8.RuneSelf { + r, sz := utf8.DecodeRune(line[idx:]) + if !unicode.IsSpace(r) { + return false + } + idx += sz + continue + } + if line[idx] != ' ' { + break + } + } + dashCount := 0 + for ; idx < len(line); idx++ { + if line[idx] != '-' { + break + } + dashCount++ + } + if dashCount < 3 { + return false + } + for ; idx < len(line); idx++ { + if line[idx] >= utf8.RuneSelf { + r, sz := utf8.DecodeRune(line[idx:]) + if !unicode.IsSpace(r) { + return false + } + idx += sz + continue + } + if line[idx] != ' ' { return false } } - return len(line) > 2 + return true } // ExtractMetadata consumes a markdown file, parses YAML frontmatter, // and returns the frontmatter metadata separated from the markdown content func ExtractMetadata(contents string, out interface{}) (string, error) { - var front, body []string - lines := strings.Split(contents, "\n") - for idx, line := range lines { - if idx == 0 { - // First line has to be a separator - if !isYAMLSeparator(line) { - return "", errors.New("frontmatter must start with a separator line") - } - continue + body, err := ExtractMetadataBytes([]byte(contents), out) + return string(body), err +} + +// ExtractMetadata consumes a markdown file, parses YAML frontmatter, +// and returns the frontmatter metadata separated from the markdown content +func ExtractMetadataBytes(contents []byte, out interface{}) ([]byte, error) { + var front, body []byte + + start, end := 0, len(contents) + idx := bytes.IndexByte(contents[start:], '\n') + if idx >= 0 { + end = start + idx + } + line := contents[start:end] + + if !isYAMLSeparator(line) { + return contents, errors.New("frontmatter must start with a separator line") + } + frontMatterStart := end + 1 + for start = frontMatterStart; start < len(contents); start = end + 1 { + end = len(contents) + idx := bytes.IndexByte(contents[start:], '\n') + if idx >= 0 { + end = start + idx } + line := contents[start:end] if isYAMLSeparator(line) { - front, body = lines[1:idx], lines[idx+1:] + front = contents[frontMatterStart:start] + body = contents[end+1:] break } } if len(front) == 0 { - return "", errors.New("could not determine metadata") + return contents, errors.New("could not determine metadata") } - if err := yaml.Unmarshal([]byte(strings.Join(front, "\n")), out); err != nil { - return "", err + log.Info("%s", string(front)) + + if err := yaml.Unmarshal(front, out); err != nil { + return contents, err } - return strings.Join(body, "\n"), nil + return body, nil } diff --git a/modules/markup/markdown/meta_test.go b/modules/markup/markdown/meta_test.go index f525777a54c1..c9bb7e1af235 100644 --- a/modules/markup/markdown/meta_test.go +++ b/modules/markup/markdown/meta_test.go @@ -45,6 +45,38 @@ func TestExtractMetadata(t *testing.T) { }) } +func TestExtractMetadataBytes(t *testing.T) { + t.Run("ValidFrontAndBody", func(t *testing.T) { + var meta structs.IssueTemplate + body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s\n%s", sepTest, frontTest, sepTest, bodyTest)), &meta) + assert.NoError(t, err) + assert.Equal(t, bodyTest, body) + assert.Equal(t, metaTest, meta) + assert.True(t, meta.Valid()) + }) + + t.Run("NoFirstSeparator", func(t *testing.T) { + var meta structs.IssueTemplate + _, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", frontTest, sepTest, bodyTest)), &meta) + assert.Error(t, err) + }) + + t.Run("NoLastSeparator", func(t *testing.T) { + var meta structs.IssueTemplate + _, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, bodyTest)), &meta) + assert.Error(t, err) + }) + + t.Run("NoBody", func(t *testing.T) { + var meta structs.IssueTemplate + body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, sepTest)), &meta) + assert.NoError(t, err) + assert.Equal(t, "", body) + assert.Equal(t, metaTest, meta) + assert.True(t, meta.Valid()) + }) +} + var ( sepTest = "-----" frontTest = `name: Test diff --git a/modules/markup/markdown/renderconfig.go b/modules/markup/markdown/renderconfig.go index bef67e9e59bf..6a3b3a1bde8b 100644 --- a/modules/markup/markdown/renderconfig.go +++ b/modules/markup/markdown/renderconfig.go @@ -5,159 +5,114 @@ package markdown import ( - "fmt" "strings" + "code.gitea.io/gitea/modules/log" "github.com/yuin/goldmark/ast" - east "github.com/yuin/goldmark/extension/ast" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) // RenderConfig represents rendering configuration for this file type RenderConfig struct { - Meta string - Icon string - TOC bool - Lang string + Meta string + Icon string + TOC bool + Lang string + yamlNode *yaml.Node } -// ToRenderConfig converts a yaml.MapSlice to a RenderConfig -func (rc *RenderConfig) ToRenderConfig(meta yaml.MapSlice) { - if meta == nil { - return - } - found := false - var giteaMetaControl yaml.MapItem - for _, item := range meta { - strKey, ok := item.Key.(string) - if !ok { - continue - } - strKey = strings.TrimSpace(strings.ToLower(strKey)) - switch strKey { - case "gitea": - giteaMetaControl = item - found = true - case "include_toc": - val, ok := item.Value.(bool) - if !ok { - continue - } - rc.TOC = val - case "lang": - val, ok := item.Value.(string) - if !ok { - continue - } - val = strings.TrimSpace(val) - if len(val) == 0 { - continue - } - rc.Lang = val +// UnmarshalYAML implement yaml.v3 UnmarshalYAML +func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error { + if rc == nil { + rc = &RenderConfig{ + Meta: "table", + Icon: "table", + Lang: "", } } + rc.yamlNode = value - if found { - switch v := giteaMetaControl.Value.(type) { - case string: - switch v { - case "none": - rc.Meta = "none" - case "table": - rc.Meta = "table" - default: // "details" - rc.Meta = "details" - } - case yaml.MapSlice: - for _, item := range v { - strKey, ok := item.Key.(string) - if !ok { - continue - } - strKey = strings.TrimSpace(strings.ToLower(strKey)) - switch strKey { - case "meta": - val, ok := item.Value.(string) - if !ok { - continue - } - switch strings.TrimSpace(strings.ToLower(val)) { - case "none": - rc.Meta = "none" - case "table": - rc.Meta = "table" - default: // "details" - rc.Meta = "details" - } - case "details_icon": - val, ok := item.Value.(string) - if !ok { - continue - } - rc.Icon = strings.TrimSpace(strings.ToLower(val)) - case "include_toc": - val, ok := item.Value.(bool) - if !ok { - continue - } - rc.TOC = val - case "lang": - val, ok := item.Value.(string) - if !ok { - continue - } - val = strings.TrimSpace(val) - if len(val) == 0 { - continue - } - rc.Lang = val - } - } - } + type basicRenderConfig struct { + Gitea *yaml.Node `yaml:"gitea"` + TOC bool `yaml:"include_toc"` + Lang string `yaml:"lang"` } -} -func (rc *RenderConfig) toMetaNode(meta yaml.MapSlice) ast.Node { - switch rc.Meta { - case "table": - return metaToTable(meta) - case "details": - return metaToDetails(meta, rc.Icon) - default: + var basic basicRenderConfig + + err := value.Decode(&basic) + if err != nil { + return err + } + + if basic.Lang != "" { + rc.Lang = basic.Lang + } + + rc.TOC = basic.TOC + if basic.Gitea == nil { return nil } -} -func metaToTable(meta yaml.MapSlice) ast.Node { - table := east.NewTable() - alignments := []east.Alignment{} - for range meta { - alignments = append(alignments, east.AlignNone) + var control *string + if err := basic.Gitea.Decode(&control); err == nil && control != nil { + log.Info("control %v", control) + switch strings.TrimSpace(strings.ToLower(*control)) { + case "none": + rc.Meta = "none" + case "table": + rc.Meta = "table" + default: // "details" + rc.Meta = "details" + } + return nil } - row := east.NewTableRow(alignments) - for _, item := range meta { - cell := east.NewTableCell() - cell.AppendChild(cell, ast.NewString([]byte(fmt.Sprintf("%v", item.Key)))) - row.AppendChild(row, cell) + + type giteaControl struct { + Meta string `yaml:"meta"` + Icon string `yaml:"details_icon"` + TOC *yaml.Node `yaml:"include_toc"` + Lang string `yaml:"lang"` } - table.AppendChild(table, east.NewTableHeader(row)) - row = east.NewTableRow(alignments) - for _, item := range meta { - cell := east.NewTableCell() - cell.AppendChild(cell, ast.NewString([]byte(fmt.Sprintf("%v", item.Value)))) - row.AppendChild(row, cell) + var controlStruct *giteaControl + if err := basic.Gitea.Decode(controlStruct); err != nil || controlStruct == nil { + return err } - table.AppendChild(table, row) - return table -} -func metaToDetails(meta yaml.MapSlice, icon string) ast.Node { - details := NewDetails() - summary := NewSummary() - summary.AppendChild(summary, NewIcon(icon)) - details.AppendChild(details, summary) - details.AppendChild(details, metaToTable(meta)) + switch strings.TrimSpace(strings.ToLower(controlStruct.Meta)) { + case "none": + rc.Meta = "none" + case "table": + rc.Meta = "table" + default: // "details" + rc.Meta = "details" + } + + rc.Icon = strings.TrimSpace(strings.ToLower(controlStruct.Icon)) + + if controlStruct.Lang != "" { + rc.Lang = controlStruct.Lang + } + + var toc bool + if err := controlStruct.TOC.Decode(&toc); err == nil { + rc.TOC = toc + } + + return nil +} - return details +func (rc *RenderConfig) toMetaNode() ast.Node { + if rc.yamlNode == nil { + return nil + } + switch rc.Meta { + case "table": + return nodeToTable(rc.yamlNode) + case "details": + return nodeToDetails(rc.yamlNode, rc.Icon) + default: + return nil + } } diff --git a/modules/markup/markdown/renderconfig_test.go b/modules/markup/markdown/renderconfig_test.go new file mode 100644 index 000000000000..1027035cda5a --- /dev/null +++ b/modules/markup/markdown/renderconfig_test.go @@ -0,0 +1,162 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package markdown + +import ( + "testing" + + "gopkg.in/yaml.v3" +) + +func TestRenderConfig_UnmarshalYAML(t *testing.T) { + tests := []struct { + name string + expected *RenderConfig + args string + }{ + { + "empty", &RenderConfig{ + Meta: "table", + Icon: "table", + Lang: "", + }, "", + }, + { + "lang", &RenderConfig{ + Meta: "table", + Icon: "table", + Lang: "test", + }, "lang: test", + }, + { + "metatable", &RenderConfig{ + Meta: "table", + Icon: "table", + Lang: "", + }, "gitea: table", + }, + { + "metanone", &RenderConfig{ + Meta: "none", + Icon: "table", + Lang: "", + }, "gitea: none", + }, + { + "metadetails", &RenderConfig{ + Meta: "details", + Icon: "table", + Lang: "", + }, "gitea: details", + }, + { + "metawrong", &RenderConfig{ + Meta: "details", + Icon: "table", + Lang: "", + }, "gitea: wrong", + }, + { + "toc", &RenderConfig{ + TOC: true, + Meta: "table", + Icon: "table", + Lang: "", + }, "include_toc: true", + }, + { + "tocfalse", &RenderConfig{ + TOC: false, + Meta: "table", + Icon: "table", + Lang: "", + }, "include_toc: false", + }, + { + "toclang", &RenderConfig{ + Meta: "table", + Icon: "table", + TOC: true, + Lang: "testlang", + }, ` + include_toc: true + lang: testlang +`, + }, + { + "complexlang", &RenderConfig{ + Meta: "table", + Icon: "table", + Lang: "testlang", + }, ` + gitea: + lang: testlang +`, + }, + { + "complexlang2", &RenderConfig{ + Meta: "table", + Icon: "table", + Lang: "testlang", + }, ` + lang: notright + gitea: + lang: testlang +`, + }, + { + "complexlang", &RenderConfig{ + Meta: "table", + Icon: "table", + Lang: "testlang", + }, ` + gitea: + lang: testlang +`, + }, + { + "complex2", &RenderConfig{ + Lang: "two", + Meta: "table", + TOC: true, + Icon: "smiley", + }, ` + lang: one + include_toc: true + gitea: + details_icon: smiley + meta: table + include_toc: true + lang: two +`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := &RenderConfig{ + Meta: "table", + Icon: "table", + Lang: "", + } + if err := yaml.Unmarshal([]byte(tt.args), got); err != nil { + t.Errorf("RenderConfig.UnmarshalYAML() error = %v", err) + return + } + + if got.Meta != tt.expected.Meta { + t.Errorf("Meta Expected %s Got %s", tt.expected.Meta, got.Meta) + } + if got.Icon != tt.expected.Icon { + t.Errorf("Icon Expected %s Got %s", tt.expected.Icon, got.Icon) + } + if got.Lang != tt.expected.Lang { + t.Errorf("Lang Expected %s Got %s", tt.expected.Lang, got.Lang) + } + if got.TOC != tt.expected.TOC { + t.Errorf("TOC Expected %t Got %t", tt.expected.TOC, got.TOC) + } + }) + } +} From fa26c5821cdd81846debac78682e02fbe3aa754b Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Tue, 2 Aug 2022 22:10:04 +0100 Subject: [PATCH 04/16] missedremoval Signed-off-by: Andrew Thornton --- web_src/js/katex.js | 41 ----------------------------------------- 1 file changed, 41 deletions(-) delete mode 100644 web_src/js/katex.js diff --git a/web_src/js/katex.js b/web_src/js/katex.js deleted file mode 100644 index 38bc6c89e4ba..000000000000 --- a/web_src/js/katex.js +++ /dev/null @@ -1,41 +0,0 @@ -import renderMathInElement from 'katex/dist/contrib/auto-render.js'; - -const mathNodes = document.querySelectorAll('.language-math'); - -const ourRender = (nodes) => { - for (const element of nodes) { - if (element.hasAttribute('katex-rendered') || !element.textContent) { - continue; - } - - renderMathInElement(element, { - delimiters: [ - {left: '\\[', right: '\\]', display: true}, - {left: '\\(', right: '\\)', display: false} - ], - errorCallback: (_, stack) => { - element.setAttribute('title', stack); - }, - }); - element.setAttribute('katex-rendered', 'yes'); - } -}; - -ourRender(mathNodes); - -// Options for the observer (which mutations to observe) -const config = {childList: true, subtree: true}; - -// Callback function to execute when mutations are observed -const callback = (records) => { - for (const record of records) { - const mathNodes = record.target.querySelectorAll('.language-math'); - ourRender(mathNodes); - } -}; - -// Create an observer instance linked to the callback function -const observer = new MutationObserver(callback); - -// Start observing the target node for configured mutations -observer.observe(document, config); From 1d6a332669c7d17fff590c42b563cd0cf01582c0 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Tue, 2 Aug 2022 22:24:18 +0100 Subject: [PATCH 05/16] placate lint Signed-off-by: Andrew Thornton --- web_src/js/markup/katex.js | 1 - 1 file changed, 1 deletion(-) diff --git a/web_src/js/markup/katex.js b/web_src/js/markup/katex.js index 9b4eb13e94a0..35f97f69df52 100644 --- a/web_src/js/markup/katex.js +++ b/web_src/js/markup/katex.js @@ -13,7 +13,6 @@ function displayError(el, err) { target.before(errorNode); } -// eslint-disable-next-line import/no-unused-modules export async function renderMath() { const els = document.querySelectorAll('.markup code.language-math'); if (!els.length) return; From a775a9c2824c65c8fbb7431045ba9c8e6385b7ab Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Fri, 2 Sep 2022 23:24:31 +0100 Subject: [PATCH 06/16] as per silverwind Signed-off-by: Andrew Thornton Co-authored-by: silverwind --- .../doc/advanced/config-cheat-sheet.en-us.md | 2 +- package-lock.json | 14 +++++++------- package.json | 2 +- web_src/js/markup/{katex.js => math.js} | 15 ++++++--------- web_src/less/animations.less | 7 +++++++ webpack.config.js | 4 ++++ 6 files changed, 26 insertions(+), 18 deletions(-) rename web_src/js/markup/{katex.js => math.js} (75%) diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index c74a2829f88f..096bb7e06999 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -236,7 +236,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `CUSTOM_URL_SCHEMES`: Use a comma separated list (ftp,git,svn) to indicate additional URL hyperlinks to be rendered in Markdown. URLs beginning in http and https are always displayed -- `ENABLE_MATH`: **true**: Enables detection of `\(...\)`, `\[...\]` and `$$...$$` blocks as math blocks +- `ENABLE_MATH`: **true**: Enables detection of `\(...\)`, `\[...\]` and `$$...$$` blocks as math blocks. - `ENABLE_INLINE_DOLLAR_MATH`: **false**: In addition enables detection of `$...$` as inline math. ## Server (`server`) diff --git a/package-lock.json b/package-lock.json index b8e12d434554..c2e3a819c741 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "font-awesome": "4.7.0", "jquery": "3.6.0", "jquery.are-you-sure": "1.9.0", - "katex": "0.16.0", + "katex": "0.16.2", "less": "4.1.3", "less-loader": "11.0.0", "license-checker-webpack-plugin": "0.2.1", @@ -7735,9 +7735,9 @@ "integrity": "sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ==" }, "node_modules/katex": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.0.tgz", - "integrity": "sha512-wPRB4iUPysfH97wTgG5/tRLYxmKVq6Q4jRAWRVOUxXB1dsiv4cvcNjqabHkrOvJHM1Bpk3WrgmllSO1vIvP24w==", + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.2.tgz", + "integrity": "sha512-70DJdQAyh9EMsthw3AaQlDyFf54X7nWEUIa5W+rq8XOpEk//w5Th7/8SqFqpvi/KZ2t6MHUj4f9wLmztBmAYQA==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -17694,9 +17694,9 @@ "integrity": "sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ==" }, "katex": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.0.tgz", - "integrity": "sha512-wPRB4iUPysfH97wTgG5/tRLYxmKVq6Q4jRAWRVOUxXB1dsiv4cvcNjqabHkrOvJHM1Bpk3WrgmllSO1vIvP24w==", + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.2.tgz", + "integrity": "sha512-70DJdQAyh9EMsthw3AaQlDyFf54X7nWEUIa5W+rq8XOpEk//w5Th7/8SqFqpvi/KZ2t6MHUj4f9wLmztBmAYQA==", "requires": { "commander": "^8.0.0" }, diff --git a/package.json b/package.json index f3d1911aa59b..a43a7a82f033 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "font-awesome": "4.7.0", "jquery": "3.6.0", "jquery.are-you-sure": "1.9.0", - "katex": "0.16.0", + "katex": "0.16.2", "less": "4.1.3", "less-loader": "11.0.0", "license-checker-webpack-plugin": "0.2.1", diff --git a/web_src/js/markup/katex.js b/web_src/js/markup/math.js similarity index 75% rename from web_src/js/markup/katex.js rename to web_src/js/markup/math.js index 35f97f69df52..422b10989a23 100644 --- a/web_src/js/markup/katex.js +++ b/web_src/js/markup/math.js @@ -17,22 +17,19 @@ export async function renderMath() { const els = document.querySelectorAll('.markup code.language-math'); if (!els.length) return; - const {default: katex} = await import(/* webpackChunkName: "katex" */'katex'); + const [{default: katex}] = await Promise.all([ + import(/* webpackChunkName: "katex" */'katex'), + import(/* webpackChunkName: "katex" */'katex/dist/katex.css'), + ]); for (const el of els) { const source = el.textContent; - const options = {}; - options.display = el.classList.contains('display'); + const options = {display: el.classList.contains('display')}; try { const markup = katex.renderToString(source, options); - let target; - if (options.display) { - target = document.createElement('p'); - } else { - target = document.createElement('span'); - } + const target = document.createElement(options.display ? 'p' : 'span') target.innerHTML = markup; if (el.classList.contains('is-loading')) { el.replaceWith(target); diff --git a/web_src/less/animations.less b/web_src/less/animations.less index 92a3052a1f36..ea31d53bfea7 100644 --- a/web_src/less/animations.less +++ b/web_src/less/animations.less @@ -33,6 +33,13 @@ height: var(--height-loading); } +code.language-math.is-loading::after { + padding: 0; + border-width: 2px; + width: 1.25rem; + height: 1.25rem; +} + @keyframes fadein { 0% { opacity: 0; diff --git a/webpack.config.js b/webpack.config.js index 974af15c01df..7590256cda7e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -34,6 +34,10 @@ const filterCssImport = (url, ...args) => { if (/(eot|ttf|otf|woff|svg)$/.test(importedFile)) return false; } + if (cssFile.includes('katex') && /(ttf|woff)$/.test(importedFile)) { + return false; + } + if (cssFile.includes('font-awesome') && /(eot|ttf|otf|woff|svg)$/.test(importedFile)) { return false; } From ca3573dbca95935181a81ecb747647c7f68f50e9 Mon Sep 17 00:00:00 2001 From: zeripath Date: Sat, 3 Sep 2022 08:11:26 +0100 Subject: [PATCH 07/16] Apply suggestions from code review Co-authored-by: silverwind --- web_src/js/markup/math.js | 1 - webpack.config.js | 1 - 2 files changed, 2 deletions(-) diff --git a/web_src/js/markup/math.js b/web_src/js/markup/math.js index 422b10989a23..ce6955484214 100644 --- a/web_src/js/markup/math.js +++ b/web_src/js/markup/math.js @@ -24,7 +24,6 @@ export async function renderMath() { for (const el of els) { const source = el.textContent; - const options = {display: el.classList.contains('display')}; try { diff --git a/webpack.config.js b/webpack.config.js index 7590256cda7e..c6c115d97d27 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -54,7 +54,6 @@ export default { fileURLToPath(new URL('web_src/js/index.js', import.meta.url)), fileURLToPath(new URL('node_modules/easymde/dist/easymde.min.css', import.meta.url)), fileURLToPath(new URL('web_src/fomantic/build/semantic.css', import.meta.url)), - fileURLToPath(new URL('node_modules/katex/dist/katex.css', import.meta.url)), fileURLToPath(new URL('web_src/less/index.less', import.meta.url)), ], swagger: [ From 810c790a2728b26d87788b97a8d36794f1c0c56f Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sat, 3 Sep 2022 08:49:48 +0100 Subject: [PATCH 08/16] placate lint Signed-off-by: Andrew Thornton --- docs/content/doc/advanced/external-renderers.en-us.md | 3 ++- web_src/js/markup/content.js | 2 +- web_src/js/markup/math.js | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/content/doc/advanced/external-renderers.en-us.md b/docs/content/doc/advanced/external-renderers.en-us.md index 4e5e72554d9d..5418ffb98d3b 100644 --- a/docs/content/doc/advanced/external-renderers.en-us.md +++ b/docs/content/doc/advanced/external-renderers.en-us.md @@ -74,12 +74,13 @@ RENDER_COMMAND = "timeout 30s pandoc +RTS -M512M -RTS -f rst" IS_INPUT_FILE = false ``` -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/). +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 could be used to support server-side [KaTeX](https://katex.org/) rendering output from [`pandoc`](https://pandoc.org/). ```ini [markup.sanitizer.TeX] ; Pandoc renders TeX segments as s with the "math" class, optionally ; with "inline" or "display" classes depending on context. +; - note this is different from the built-in math support in our markdown parser which uses ELEMENT = span ALLOW_ATTR = class REGEXP = ^\s*((math(\s+|$)|inline(\s+|$)|display(\s+|$)))+ diff --git a/web_src/js/markup/content.js b/web_src/js/markup/content.js index 5c0428c69692..319c229385a3 100644 --- a/web_src/js/markup/content.js +++ b/web_src/js/markup/content.js @@ -1,5 +1,5 @@ import {renderMermaid} from './mermaid.js'; -import {renderMath} from './katex.js'; +import {renderMath} from './math.js'; import {renderCodeCopy} from './codecopy.js'; import {initMarkupTasklist} from './tasklist.js'; diff --git a/web_src/js/markup/math.js b/web_src/js/markup/math.js index ce6955484214..f44d48c2cbbf 100644 --- a/web_src/js/markup/math.js +++ b/web_src/js/markup/math.js @@ -28,7 +28,7 @@ export async function renderMath() { try { const markup = katex.renderToString(source, options); - const target = document.createElement(options.display ? 'p' : 'span') + const target = document.createElement(options.display ? 'p' : 'span'); target.innerHTML = markup; if (el.classList.contains('is-loading')) { el.replaceWith(target); From 3b361e2a22fe940130c62badab2923f55e24fc83 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sat, 3 Sep 2022 08:57:42 +0100 Subject: [PATCH 09/16] Handle conflict from #20987 Signed-off-by: Andrew Thornton --- modules/markup/markdown/meta_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/markup/markdown/meta_test.go b/modules/markup/markdown/meta_test.go index 6e864a0c38b4..9332b35b4278 100644 --- a/modules/markup/markdown/meta_test.go +++ b/modules/markup/markdown/meta_test.go @@ -63,7 +63,7 @@ func TestExtractMetadataBytes(t *testing.T) { assert.NoError(t, err) assert.Equal(t, bodyTest, body) assert.Equal(t, metaTest, meta) - assert.True(t, meta.Valid()) + assert.True(t, validateMetadata(meta)) }) t.Run("NoFirstSeparator", func(t *testing.T) { @@ -84,7 +84,7 @@ func TestExtractMetadataBytes(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "", body) assert.Equal(t, metaTest, meta) - assert.True(t, meta.Valid()) + assert.True(t, validateMetadata(meta)) }) } From 438f9fa8b4911b7f55783ad4e839d6cc19db2759 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sun, 4 Sep 2022 08:46:36 +0100 Subject: [PATCH 10/16] Set inline dollar math to true - to match github Signed-off-by: Andrew Thornton --- custom/conf/app.example.ini | 2 +- docs/content/doc/advanced/config-cheat-sheet.en-us.md | 2 +- modules/setting/setting.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 7df6e0fb7d00..2e7e00f8d8fb 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1267,7 +1267,7 @@ ROUTER = console ;ENABLE_MATH = true ;; ;; Enables in addition inline block detection using single dollars -;ENABLE_INLINE_DOLLAR_MATH = false +;ENABLE_INLINE_DOLLAR_MATH = true ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 096bb7e06999..ea3740ed962d 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -237,7 +237,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a URL hyperlinks to be rendered in Markdown. URLs beginning in http and https are always displayed - `ENABLE_MATH`: **true**: Enables detection of `\(...\)`, `\[...\]` and `$$...$$` blocks as math blocks. -- `ENABLE_INLINE_DOLLAR_MATH`: **false**: In addition enables detection of `$...$` as inline math. +- `ENABLE_INLINE_DOLLAR_MATH`: **true**: In addition enables detection of `$...$` as inline math. ## Server (`server`) diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 271bbfdba2d6..2a73482eacf7 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -351,7 +351,7 @@ var ( EnableHardLineBreakInDocuments: false, FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd", ","), EnableMath: true, - EnableInlineDollarMath: false, + EnableInlineDollarMath: true, } // Admin settings From 056ab7057d9c3aa190fc8391d2f6d0d8d1f8742e Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sun, 4 Sep 2022 14:39:38 +0100 Subject: [PATCH 11/16] fix inline placement of display math Signed-off-by: Andrew Thornton --- modules/markup/markdown/math/block_node.go | 10 ++++- modules/markup/markdown/math/block_parser.go | 47 ++++++++++++++------ 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/modules/markup/markdown/math/block_node.go b/modules/markup/markdown/math/block_node.go index 535b2900f7e4..e2de513658f1 100644 --- a/modules/markup/markdown/math/block_node.go +++ b/modules/markup/markdown/math/block_node.go @@ -9,14 +9,20 @@ import "github.com/yuin/goldmark/ast" // Block represents a math Block type Block struct { ast.BaseBlock + Dollars bool + Indent int + Closed bool } // KindBlock is the node kind for math blocks var KindBlock = ast.NewNodeKind("MathBlock") // NewBlock creates a new math Block -func NewBlock() *Block { - return &Block{} +func NewBlock(dollars bool, indent int) *Block { + return &Block{ + Dollars: dollars, + Indent: indent, + } } // Dump dumps the block to a string diff --git a/modules/markup/markdown/math/block_parser.go b/modules/markup/markdown/math/block_parser.go index 9d6e16672bd7..f86512288672 100644 --- a/modules/markup/markdown/math/block_parser.go +++ b/modules/markup/markdown/math/block_parser.go @@ -5,6 +5,8 @@ package math import ( + "bytes" + "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/text" @@ -15,13 +17,6 @@ type blockParser struct { parseDollars bool } -type blockData struct { - dollars bool - indent int -} - -var blockInfoKey = parser.NewContextKey() - // NewBlockParser creates a new math BlockParser func NewBlockParser(parseDollarBlocks bool) parser.BlockParser { return &blockParser{ @@ -31,7 +26,7 @@ func NewBlockParser(parseDollarBlocks bool) parser.BlockParser { // Open parses the current line and returns a result of parsing. func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { - line, _ := reader.PeekLine() + line, segment := reader.PeekLine() pos := pc.BlockOffset() if pos == -1 || len(line[pos:]) < 2 { return nil, parser.NoChildren @@ -44,33 +39,57 @@ func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Contex return nil, parser.NoChildren } - pc.Set(blockInfoKey, &blockData{dollars: dollars, indent: pos}) - node := NewBlock() + node := NewBlock(dollars, pos) + + // Now we need to check if the ending block is on the segment... + endBytes := []byte{'\\', ']'} + if dollars { + endBytes = []byte{'$', '$'} + } + idx := bytes.Index(line[pos+2:], endBytes) + if idx >= 0 { + segment.Stop = segment.Start + idx + 2 + reader.Advance(segment.Len() - 1) + segment.Start += 2 + node.Lines().Append(segment) + node.Closed = true + return node, parser.Close | parser.NoChildren + } + + reader.Advance(segment.Len() - 1) + segment.Start += 2 + node.Lines().Append(segment) return node, parser.NoChildren } // Continue parses the current line and returns a result of parsing. func (b *blockParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State { + block := node.(*Block) + if block.Closed { + return parser.Close + } + line, segment := reader.PeekLine() - data := pc.Get(blockInfoKey).(*blockData) w, pos := util.IndentWidth(line, 0) if w < 4 { - if data.dollars { + if block.Dollars { i := pos for ; i < len(line) && line[i] == '$'; i++ { } length := i - pos if length >= 2 && util.IsBlank(line[i:]) { reader.Advance(segment.Stop - segment.Start - segment.Padding) + block.Closed = true return parser.Close } } else if len(line[pos:]) > 1 && line[pos] == '\\' && line[pos+1] == ']' && util.IsBlank(line[pos+2:]) { reader.Advance(segment.Stop - segment.Start - segment.Padding) + block.Closed = true return parser.Close } } - pos, padding := util.IndentPosition(line, 0, data.indent) + pos, padding := util.IndentPosition(line, 0, block.Indent) seg := text.NewSegmentPadding(segment.Start+pos, segment.Stop, padding) node.Lines().Append(seg) reader.AdvanceAndSetPadding(segment.Stop-segment.Start-pos-1, padding) @@ -79,7 +98,7 @@ func (b *blockParser) Continue(node ast.Node, reader text.Reader, pc parser.Cont // Close will be called when the parser returns Close. func (b *blockParser) Close(node ast.Node, reader text.Reader, pc parser.Context) { - pc.Set(blockInfoKey, nil) + // noop } // CanInterruptParagraph returns true if the parser can interrupt paragraphs, From f114106c6fe2eea7b5237bfd8158df87bd8a65ec Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sun, 11 Sep 2022 20:44:35 +0100 Subject: [PATCH 12/16] remove inline dollar option Signed-off-by: Andrew Thornton --- custom/conf/app.example.ini | 3 -- .../doc/advanced/config-cheat-sheet.en-us.md | 3 +- modules/markup/markdown/markdown.go | 1 - modules/markup/markdown/math/inline_parser.go | 48 +++++++++---------- modules/markup/markdown/math/math.go | 10 ++-- modules/setting/setting.go | 2 - 6 files changed, 29 insertions(+), 38 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 2e7e00f8d8fb..6e1eca7a6279 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1265,9 +1265,6 @@ ROUTER = console ;; ;; Enables math inline and block detection ;ENABLE_MATH = true -;; -;; Enables in addition inline block detection using single dollars -;ENABLE_INLINE_DOLLAR_MATH = true ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index ea3740ed962d..459e42ac2469 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -236,8 +236,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `CUSTOM_URL_SCHEMES`: Use a comma separated list (ftp,git,svn) to indicate additional URL hyperlinks to be rendered in Markdown. URLs beginning in http and https are always displayed -- `ENABLE_MATH`: **true**: Enables detection of `\(...\)`, `\[...\]` and `$$...$$` blocks as math blocks. -- `ENABLE_INLINE_DOLLAR_MATH`: **true**: In addition enables detection of `$...$` as inline math. +- `ENABLE_MATH`: **true**: Enables detection of `\(...\)`, `\[...\]`, `$...$` and `$$...$$` blocks as math blocks. ## Server (`server`) diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index c10922f7679d..c0e72fd6ce51 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -124,7 +124,6 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) ), math.NewExtension( math.Enabled(setting.Markdown.EnableMath), - math.WithInlineDollarParser(setting.Markdown.EnableInlineDollarMath), ), meta.Meta, ), diff --git a/modules/markup/markdown/math/inline_parser.go b/modules/markup/markdown/math/inline_parser.go index 0222b0342433..0339674b6ca5 100644 --- a/modules/markup/markdown/math/inline_parser.go +++ b/modules/markup/markdown/math/inline_parser.go @@ -10,7 +10,6 @@ import ( "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/text" - "github.com/yuin/goldmark/util" ) type inlineParser struct { @@ -43,40 +42,37 @@ func (parser *inlineParser) Trigger() []byte { return parser.start[0:1] } +func isAlphanumeric(b byte) bool { + // Github only cares about 0-9A-Za-z + return (b >= '0' && b <= '9') || (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z') +} + // Parse parses the current line and returns a result of parsing. func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { - line, startSegment := block.PeekLine() + line, _ := block.PeekLine() opener := bytes.Index(line, parser.start) if opener < 0 { return nil } + if opener != 0 && isAlphanumeric(line[opener-1]) { + return nil + } + opener += len(parser.start) + ender := bytes.Index(line[opener:], parser.end) + if ender < 0 { + return nil + } + if len(line) > opener+ender+len(parser.end) && isAlphanumeric(line[opener+ender+len(parser.end)]) { + return nil + } + block.Advance(opener) - l, pos := block.Position() + _, pos := block.Position() node := NewInline() - - for { - line, segment := block.PeekLine() - if line == nil { - block.SetPosition(l, pos) - return ast.NewTextSegment(startSegment.WithStop(startSegment.Start + opener)) - } - - closer := bytes.Index(line, parser.end) - if closer < 0 { - if !util.IsBlank(line) { - node.AppendChild(node, ast.NewRawTextSegment(segment)) - } - block.AdvanceLine() - continue - } - segment = segment.WithStop(segment.Start + closer) - if !segment.IsEmpty() { - node.AppendChild(node, ast.NewRawTextSegment(segment)) - } - block.Advance(closer + len(parser.end)) - break - } + segment := pos.WithStop(pos.Start + ender) + node.AppendChild(node, ast.NewRawTextSegment(segment)) + block.Advance(ender + len(parser.end)) trimBlock(node, block) return node diff --git a/modules/markup/markdown/math/math.go b/modules/markup/markdown/math/math.go index e896084fb8a4..7854ac84dbb1 100644 --- a/modules/markup/markdown/math/math.go +++ b/modules/markup/markdown/math/math.go @@ -64,15 +64,17 @@ func WithBlockDollarParser(enable ...bool) Option { // Math represents a math extension with default rendered delimiters var Math = &Extension{ - enabled: true, - parseDollarBlock: true, + enabled: true, + parseDollarBlock: true, + parseDollarInline: true, } // NewExtension creates a new math extension with the provided options func NewExtension(opts ...Option) *Extension { r := &Extension{ - enabled: true, - parseDollarBlock: true, + enabled: true, + parseDollarBlock: true, + parseDollarInline: true, } for _, o := range opts { diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 2a73482eacf7..1ac13cb5feea 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -345,13 +345,11 @@ var ( CustomURLSchemes []string `ini:"CUSTOM_URL_SCHEMES"` FileExtensions []string EnableMath bool - EnableInlineDollarMath bool }{ EnableHardLineBreakInComments: true, EnableHardLineBreakInDocuments: false, FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd", ","), EnableMath: true, - EnableInlineDollarMath: true, } // Admin settings From 4b138dbcfa68253ec728c26b3741cc1b6ec96969 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sun, 11 Sep 2022 20:45:29 +0100 Subject: [PATCH 13/16] fix spelling bug Signed-off-by: Andrew Thornton --- docs/content/doc/advanced/external-renderers.en-us.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/doc/advanced/external-renderers.en-us.md b/docs/content/doc/advanced/external-renderers.en-us.md index 5418ffb98d3b..f40c23dd842a 100644 --- a/docs/content/doc/advanced/external-renderers.en-us.md +++ b/docs/content/doc/advanced/external-renderers.en-us.md @@ -74,7 +74,7 @@ RENDER_COMMAND = "timeout 30s pandoc +RTS -M512M -RTS -f rst" IS_INPUT_FILE = false ``` -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 could be used to support server-side [KaTeX](https://katex.org/) rendering output from [`pandoc`](https://pandoc.org/). +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/). ```ini [markup.sanitizer.TeX] From 92d37487a2094ad34db9f8f5e1698566db6b52b3 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sun, 11 Sep 2022 20:47:20 +0100 Subject: [PATCH 14/16] comments Signed-off-by: Andrew Thornton --- modules/markup/markdown/math/block_node.go | 2 +- modules/markup/markdown/math/inline_node.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/markup/markdown/math/block_node.go b/modules/markup/markdown/math/block_node.go index e2de513658f1..bd8449babfed 100644 --- a/modules/markup/markdown/math/block_node.go +++ b/modules/markup/markdown/math/block_node.go @@ -6,7 +6,7 @@ package math import "github.com/yuin/goldmark/ast" -// Block represents a math Block +// Block represents a display math block e.g. $$...$$ or \[...\] type Block struct { ast.BaseBlock Dollars bool diff --git a/modules/markup/markdown/math/inline_node.go b/modules/markup/markdown/math/inline_node.go index 877a94d53cbe..245ff8dab0ba 100644 --- a/modules/markup/markdown/math/inline_node.go +++ b/modules/markup/markdown/math/inline_node.go @@ -9,7 +9,7 @@ import ( "github.com/yuin/goldmark/util" ) -// Inline represents inline math +// Inline represents inline math e.g. $...$ or \(...\) type Inline struct { ast.BaseInline } From e7851cae04aea496cfcb56252b60ac42dba4c02e Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sun, 11 Sep 2022 20:59:13 +0100 Subject: [PATCH 15/16] as per delvh Signed-off-by: Andrew Thornton --- web_src/js/markup/math.js | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/web_src/js/markup/math.js b/web_src/js/markup/math.js index f44d48c2cbbf..5790a327a51f 100644 --- a/web_src/js/markup/math.js +++ b/web_src/js/markup/math.js @@ -1,18 +1,17 @@ function displayError(el, err) { - let target = el; - if (el.classList.contains('is-loading')) { - // assume no pre - el.classList.remove('is-loading'); - } else { - target = el.closest('pre'); - target.classList.remove('is-loading'); - } + const target = targetElement(el); + target.remove('is-loading'); const errorNode = document.createElement('div'); errorNode.setAttribute('class', 'ui message error markup-block-error mono'); errorNode.textContent = err.str || err.message || String(err); target.before(errorNode); } +function targetElement(el) { + // The target element is either the current element if it has the `is-loading` class or the pre that contains it + return el.classList.contains('is-loading') ? el : el.closest('pre'); +} + export async function renderMath() { const els = document.querySelectorAll('.markup code.language-math'); if (!els.length) return; @@ -28,13 +27,9 @@ export async function renderMath() { try { const markup = katex.renderToString(source, options); - const target = document.createElement(options.display ? 'p' : 'span'); - target.innerHTML = markup; - if (el.classList.contains('is-loading')) { - el.replaceWith(target); - } else { - el.closest('pre').replaceWith(target); - } + const tempEl = document.createElement(options.display ? 'p' : 'span'); + tempEl.innerHTML = markup; + targetElement(el).replaceWith(tempEl); } catch (error) { displayError(el, error); } From 59e0b7fd4fe3bd2a582d12058c008a1958924a64 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 12 Sep 2022 19:39:14 +0200 Subject: [PATCH 16/16] docs update --- docs/content/doc/features/comparison.en-us.md | 2 ++ docs/content/page/index.en-us.md | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/content/doc/features/comparison.en-us.md b/docs/content/doc/features/comparison.en-us.md index 3c2a3f916235..89d92b256518 100644 --- a/docs/content/doc/features/comparison.en-us.md +++ b/docs/content/doc/features/comparison.en-us.md @@ -53,6 +53,8 @@ _Symbols used in table:_ | WebAuthn (2FA) | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ? | | Built-in CI/CD | ✘ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | | Subgroups: groups within groups | [✘](https://github.com/go-gitea/gitea/issues/1872) | ✘ | ✘ | ✓ | ✓ | ✘ | ✓ | +| Mermaid diagrams in Markdown | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Math syntax in Markdown | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | ## Code management diff --git a/docs/content/page/index.en-us.md b/docs/content/page/index.en-us.md index 8e2e36223ad0..c3ee996f0b4c 100644 --- a/docs/content/page/index.en-us.md +++ b/docs/content/page/index.en-us.md @@ -131,7 +131,8 @@ You can try it out using [the online demo](https://try.gitea.io/). - Environment variables - Command line options - Multi-language support ([21 languages](https://github.com/go-gitea/gitea/tree/main/options/locale)) -- [Mermaid](https://mermaidjs.github.io/) Diagram support +- [Mermaid](https://mermaidjs.github.io/) diagrams in Markdown +- Math syntax in Markdown - Mail service - Notifications - Registration confirmation