summaryrefslogtreecommitdiffstats
path: root/modules/markup/markdown/math
diff options
context:
space:
mode:
Diffstat (limited to 'modules/markup/markdown/math')
-rw-r--r--modules/markup/markdown/math/block_node.go41
-rw-r--r--modules/markup/markdown/math/block_parser.go125
-rw-r--r--modules/markup/markdown/math/block_renderer.go42
-rw-r--r--modules/markup/markdown/math/inline_block_node.go31
-rw-r--r--modules/markup/markdown/math/inline_node.go48
-rw-r--r--modules/markup/markdown/math/inline_parser.go153
-rw-r--r--modules/markup/markdown/math/inline_renderer.go51
-rw-r--r--modules/markup/markdown/math/math.go108
8 files changed, 599 insertions, 0 deletions
diff --git a/modules/markup/markdown/math/block_node.go b/modules/markup/markdown/math/block_node.go
new file mode 100644
index 0000000..10d17ff
--- /dev/null
+++ b/modules/markup/markdown/math/block_node.go
@@ -0,0 +1,41 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package math
+
+import "github.com/yuin/goldmark/ast"
+
+// Block represents a display math block e.g. $$...$$ or \[...\]
+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(dollars bool, indent int) *Block {
+ return &Block{
+ Dollars: dollars,
+ Indent: indent,
+ }
+}
+
+// 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 0000000..527df84
--- /dev/null
+++ b/modules/markup/markdown/math/block_parser.go
@@ -0,0 +1,125 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+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 blockParser struct {
+ parseDollars bool
+}
+
+// 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, segment := 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
+ }
+
+ 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 {
+ // for case $$ ... $$ any other text
+ for i := pos + idx + 4; i < len(line); i++ {
+ if line[i] != ' ' && line[i] != '\n' {
+ return nil, parser.NoChildren
+ }
+ }
+ 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
+ }
+
+ return nil, 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()
+ w, pos := util.IndentWidth(line, 0)
+ if w < 4 {
+ 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, block.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) {
+ // noop
+}
+
+// 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 0000000..84817ef
--- /dev/null
+++ b/modules/markup/markdown/math/block_renderer.go
@@ -0,0 +1,42 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+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{}
+
+// NewBlockRenderer creates a new renderer for math Blocks
+func NewBlockRenderer() renderer.NodeRenderer {
+ return &BlockRenderer{}
+}
+
+// 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(`<pre class="code-block is-loading"><code class="chroma language-math display">`)
+ r.writeLines(w, source, n)
+ } else {
+ _, _ = w.WriteString(`</code></pre>` + "\n")
+ }
+ return gast.WalkContinue, nil
+}
diff --git a/modules/markup/markdown/math/inline_block_node.go b/modules/markup/markdown/math/inline_block_node.go
new file mode 100644
index 0000000..c92d0c8
--- /dev/null
+++ b/modules/markup/markdown/math/inline_block_node.go
@@ -0,0 +1,31 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package math
+
+import (
+ "github.com/yuin/goldmark/ast"
+)
+
+// InlineBlock represents inline math e.g. $$...$$
+type InlineBlock struct {
+ Inline
+}
+
+// InlineBlock implements InlineBlock.
+func (n *InlineBlock) InlineBlock() {}
+
+// KindInlineBlock is the kind for math inline block
+var KindInlineBlock = ast.NewNodeKind("MathInlineBlock")
+
+// Kind returns KindInlineBlock
+func (n *InlineBlock) Kind() ast.NodeKind {
+ return KindInlineBlock
+}
+
+// NewInlineBlock creates a new ast math inline block node
+func NewInlineBlock() *InlineBlock {
+ return &InlineBlock{
+ Inline{},
+ }
+}
diff --git a/modules/markup/markdown/math/inline_node.go b/modules/markup/markdown/math/inline_node.go
new file mode 100644
index 0000000..2221a25
--- /dev/null
+++ b/modules/markup/markdown/math/inline_node.go
@@ -0,0 +1,48 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package math
+
+import (
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/util"
+)
+
+// Inline represents inline math e.g. $...$ or \(...\)
+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 0000000..b11195d
--- /dev/null
+++ b/modules/markup/markdown/math/inline_parser.go
@@ -0,0 +1,153 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package math
+
+import (
+ "bytes"
+
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/text"
+)
+
+type inlineParser struct {
+ start []byte
+ end []byte
+}
+
+var defaultInlineDollarParser = &inlineParser{
+ start: []byte{'$'},
+ end: []byte{'$'},
+}
+
+var defaultDualDollarParser = &inlineParser{
+ start: []byte{'$', '$'},
+ end: []byte{'$', '$'},
+}
+
+// NewInlineDollarParser returns a new inline parser
+func NewInlineDollarParser() parser.InlineParser {
+ return defaultInlineDollarParser
+}
+
+func NewInlineDualDollarParser() parser.InlineParser {
+ return defaultDualDollarParser
+}
+
+var defaultInlineBracketParser = &inlineParser{
+ start: []byte{'\\', '('},
+ end: []byte{'\\', ')'},
+}
+
+// NewInlineDollarParser returns a new inline parser
+func NewInlineBracketParser() parser.InlineParser {
+ return defaultInlineBracketParser
+}
+
+// Trigger triggers this parser on $ or \
+func (parser *inlineParser) Trigger() []byte {
+ return parser.start
+}
+
+func isPunctuation(b byte) bool {
+ return b == '.' || b == '!' || b == '?' || b == ',' || b == ';' || b == ':'
+}
+
+func isBracket(b byte) bool {
+ return b == ')'
+}
+
+func isAlphanumeric(b byte) bool {
+ return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
+}
+
+// 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, _ := block.PeekLine()
+
+ if !bytes.HasPrefix(line, parser.start) {
+ // We'll catch this one on the next time round
+ return nil
+ }
+
+ precedingCharacter := block.PrecendingCharacter()
+ if precedingCharacter < 256 && (isAlphanumeric(byte(precedingCharacter)) || isPunctuation(byte(precedingCharacter))) {
+ // need to exclude things like `a$` from being considered a start
+ return nil
+ }
+
+ // move the opener marker point at the start of the text
+ opener := len(parser.start)
+
+ // Now look for an ending line
+ ender := opener
+ for {
+ pos := bytes.Index(line[ender:], parser.end)
+ if pos < 0 {
+ return nil
+ }
+
+ ender += pos
+
+ // Now we want to check the character at the end of our parser section
+ // that is ender + len(parser.end) and check if char before ender is '\'
+ pos = ender + len(parser.end)
+ if len(line) <= pos {
+ break
+ }
+ suceedingCharacter := line[pos]
+ // check valid ending character
+ if !isPunctuation(suceedingCharacter) &&
+ !(suceedingCharacter == ' ') &&
+ !(suceedingCharacter == '\n') &&
+ !isBracket(suceedingCharacter) {
+ return nil
+ }
+ if line[ender-1] != '\\' {
+ break
+ }
+
+ // move the pointer onwards
+ ender += len(parser.end)
+ }
+
+ block.Advance(opener)
+ _, pos := block.Position()
+ var node ast.Node
+ if parser == defaultDualDollarParser {
+ node = NewInlineBlock()
+ } else {
+ node = NewInline()
+ }
+ segment := pos.WithStop(pos.Start + ender - opener)
+ node.AppendChild(node, ast.NewRawTextSegment(segment))
+ block.Advance(ender - opener + len(parser.end))
+
+ if parser == defaultDualDollarParser {
+ trimBlock(&(node.(*InlineBlock)).Inline, block)
+ } else {
+ trimBlock(node.(*Inline), block)
+ }
+ 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 0000000..9684809
--- /dev/null
+++ b/modules/markup/markdown/math/inline_renderer.go
@@ -0,0 +1,51 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+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{}
+
+// NewInlineRenderer returns a new renderer for inline math
+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 {
+ extraClass := ""
+ if _, ok := n.(*InlineBlock); ok {
+ extraClass = "display "
+ }
+ _, _ = w.WriteString(`<code class="language-math ` + extraClass + `is-loading">`)
+ 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(`</code>`)
+ return ast.WalkContinue, nil
+}
+
+// RegisterFuncs registers the renderer for inline math nodes
+func (r *InlineRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+ reg.Register(KindInline, r.renderInline)
+ reg.Register(KindInlineBlock, r.renderInline)
+}
diff --git a/modules/markup/markdown/math/math.go b/modules/markup/markdown/math/math.go
new file mode 100644
index 0000000..3d9f376
--- /dev/null
+++ b/modules/markup/markdown/math/math.go
@@ -0,0 +1,108 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+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
+ 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
+ })
+}
+
+// Math represents a math extension with default rendered delimiters
+var Math = &Extension{
+ 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,
+ parseDollarInline: 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(), 503),
+ util.Prioritized(NewInlineDualDollarParser(), 502))
+ }
+ m.Parser().AddOptions(parser.WithInlineParsers(inlines...))
+
+ m.Renderer().AddOptions(renderer.WithNodeRenderers(
+ util.Prioritized(NewBlockRenderer(), 501),
+ util.Prioritized(NewInlineRenderer(), 502),
+ ))
+}