2022-09-13 18:33:37 +02:00
|
|
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
2022-11-27 19:20:29 +01:00
|
|
|
// SPDX-License-Identifier: MIT
|
2022-09-13 18:33:37 +02:00
|
|
|
|
|
|
|
package math
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
|
|
|
|
"github.com/yuin/goldmark/ast"
|
|
|
|
"github.com/yuin/goldmark/parser"
|
|
|
|
"github.com/yuin/goldmark/text"
|
|
|
|
)
|
|
|
|
|
|
|
|
type inlineParser struct {
|
2024-12-14 06:43:05 +01:00
|
|
|
trigger []byte
|
|
|
|
endBytesSingleDollar []byte
|
|
|
|
endBytesDoubleDollar []byte
|
|
|
|
endBytesBracket []byte
|
2022-09-13 18:33:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
var defaultInlineDollarParser = &inlineParser{
|
2024-12-14 06:43:05 +01:00
|
|
|
trigger: []byte{'$'},
|
|
|
|
endBytesSingleDollar: []byte{'$'},
|
|
|
|
endBytesDoubleDollar: []byte{'$', '$'},
|
2024-06-30 01:23:47 +02:00
|
|
|
}
|
|
|
|
|
2022-09-13 18:33:37 +02:00
|
|
|
func NewInlineDollarParser() parser.InlineParser {
|
|
|
|
return defaultInlineDollarParser
|
|
|
|
}
|
|
|
|
|
|
|
|
var defaultInlineBracketParser = &inlineParser{
|
2024-12-14 06:43:05 +01:00
|
|
|
trigger: []byte{'\\', '('},
|
|
|
|
endBytesBracket: []byte{'\\', ')'},
|
2022-09-13 18:33:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewInlineBracketParser() parser.InlineParser {
|
|
|
|
return defaultInlineBracketParser
|
|
|
|
}
|
|
|
|
|
2022-10-05 20:55:36 +02:00
|
|
|
// Trigger triggers this parser on $ or \
|
2022-09-13 18:33:37 +02:00
|
|
|
func (parser *inlineParser) Trigger() []byte {
|
2024-12-14 06:43:05 +01:00
|
|
|
return parser.trigger
|
2022-09-13 18:33:37 +02:00
|
|
|
}
|
|
|
|
|
2024-04-02 20:15:40 +02:00
|
|
|
func isPunctuation(b byte) bool {
|
|
|
|
return b == '.' || b == '!' || b == '?' || b == ',' || b == ';' || b == ':'
|
|
|
|
}
|
|
|
|
|
2024-06-20 04:12:54 +02:00
|
|
|
func isBracket(b byte) bool {
|
|
|
|
return b == ')'
|
|
|
|
}
|
|
|
|
|
2022-09-13 18:33:37 +02:00
|
|
|
func isAlphanumeric(b byte) bool {
|
2024-04-02 20:15:40 +02:00
|
|
|
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
|
2022-09-13 18:33:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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()
|
2022-10-05 20:55:36 +02:00
|
|
|
|
2024-12-14 06:43:05 +01:00
|
|
|
if !bytes.HasPrefix(line, parser.trigger) {
|
2022-10-05 20:55:36 +02:00
|
|
|
// We'll catch this one on the next time round
|
2022-09-13 18:33:37 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-12-14 06:43:05 +01:00
|
|
|
var startMarkLen int
|
|
|
|
var stopMark []byte
|
|
|
|
checkSurrounding := true
|
|
|
|
if line[0] == '$' {
|
|
|
|
startMarkLen = 1
|
|
|
|
stopMark = parser.endBytesSingleDollar
|
|
|
|
if len(line) > 1 {
|
|
|
|
if line[1] == '$' {
|
|
|
|
startMarkLen = 2
|
|
|
|
stopMark = parser.endBytesDoubleDollar
|
|
|
|
} else if line[1] == '`' {
|
|
|
|
pos := 1
|
|
|
|
for ; pos < len(line) && line[pos] == '`'; pos++ {
|
|
|
|
}
|
|
|
|
startMarkLen = pos
|
|
|
|
stopMark = bytes.Repeat([]byte{'`'}, pos)
|
|
|
|
stopMark[len(stopMark)-1] = '$'
|
|
|
|
checkSurrounding = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
startMarkLen = 2
|
|
|
|
stopMark = parser.endBytesBracket
|
|
|
|
}
|
|
|
|
|
|
|
|
if checkSurrounding {
|
|
|
|
precedingCharacter := block.PrecendingCharacter()
|
|
|
|
if precedingCharacter < 256 && (isAlphanumeric(byte(precedingCharacter)) || isPunctuation(byte(precedingCharacter))) {
|
|
|
|
// need to exclude things like `a$` from being considered a start
|
|
|
|
return nil
|
|
|
|
}
|
2022-09-13 18:33:37 +02:00
|
|
|
}
|
2022-10-05 20:55:36 +02:00
|
|
|
|
|
|
|
// move the opener marker point at the start of the text
|
2024-12-14 06:43:05 +01:00
|
|
|
opener := startMarkLen
|
2022-10-05 20:55:36 +02:00
|
|
|
|
|
|
|
// Now look for an ending line
|
2024-12-06 13:00:24 +01:00
|
|
|
depth := 0
|
2024-12-06 05:29:09 +01:00
|
|
|
ender := -1
|
|
|
|
for i := opener; i < len(line); i++ {
|
2024-12-14 06:43:05 +01:00
|
|
|
if depth == 0 && bytes.HasPrefix(line[i:], stopMark) {
|
2024-12-06 05:29:09 +01:00
|
|
|
succeedingCharacter := byte(0)
|
2024-12-14 06:43:05 +01:00
|
|
|
if i+len(stopMark) < len(line) {
|
|
|
|
succeedingCharacter = line[i+len(stopMark)]
|
2024-12-06 05:29:09 +01:00
|
|
|
}
|
|
|
|
// check valid ending character
|
|
|
|
isValidEndingChar := isPunctuation(succeedingCharacter) || isBracket(succeedingCharacter) ||
|
|
|
|
succeedingCharacter == ' ' || succeedingCharacter == '\n' || succeedingCharacter == 0
|
2024-12-14 06:43:05 +01:00
|
|
|
if checkSurrounding && !isValidEndingChar {
|
2024-12-06 05:29:09 +01:00
|
|
|
break
|
|
|
|
}
|
|
|
|
ender = i
|
2022-10-05 20:55:36 +02:00
|
|
|
break
|
|
|
|
}
|
2024-12-06 05:29:09 +01:00
|
|
|
if line[i] == '\\' {
|
|
|
|
i++
|
|
|
|
continue
|
2022-10-05 20:55:36 +02:00
|
|
|
}
|
2024-12-06 13:00:24 +01:00
|
|
|
if line[i] == '{' {
|
|
|
|
depth++
|
|
|
|
} else if line[i] == '}' {
|
|
|
|
depth--
|
|
|
|
}
|
2024-12-06 05:29:09 +01:00
|
|
|
}
|
|
|
|
if ender == -1 {
|
|
|
|
return nil
|
2022-09-13 18:33:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
block.Advance(opener)
|
|
|
|
_, pos := block.Position()
|
2024-12-14 06:43:05 +01:00
|
|
|
node := NewInline()
|
|
|
|
|
2022-10-05 20:55:36 +02:00
|
|
|
segment := pos.WithStop(pos.Start + ender - opener)
|
2022-09-13 18:33:37 +02:00
|
|
|
node.AppendChild(node, ast.NewRawTextSegment(segment))
|
2024-12-14 06:43:05 +01:00
|
|
|
block.Advance(ender - opener + len(stopMark))
|
|
|
|
trimBlock(node, block)
|
2022-09-13 18:33:37 +02:00
|
|
|
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)
|
|
|
|
}
|