mirror of https://github.com/go-gitea/gitea.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
169 lines
4.1 KiB
169 lines
4.1 KiB
// 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 { |
|
trigger []byte |
|
endBytesSingleDollar []byte |
|
endBytesDoubleDollar []byte |
|
endBytesParentheses []byte |
|
enableInlineDollar bool |
|
} |
|
|
|
func NewInlineDollarParser(enableInlineDollar bool) parser.InlineParser { |
|
return &inlineParser{ |
|
trigger: []byte{'$'}, |
|
endBytesSingleDollar: []byte{'$'}, |
|
endBytesDoubleDollar: []byte{'$', '$'}, |
|
enableInlineDollar: enableInlineDollar, |
|
} |
|
} |
|
|
|
var defaultInlineParenthesesParser = &inlineParser{ |
|
trigger: []byte{'\\', '('}, |
|
endBytesParentheses: []byte{'\\', ')'}, |
|
} |
|
|
|
func NewInlineParenthesesParser() parser.InlineParser { |
|
return defaultInlineParenthesesParser |
|
} |
|
|
|
// Trigger triggers this parser on $ or \ |
|
func (parser *inlineParser) Trigger() []byte { |
|
return parser.trigger |
|
} |
|
|
|
func isPunctuation(b byte) bool { |
|
return b == '.' || b == '!' || b == '?' || b == ',' || b == ';' || b == ':' |
|
} |
|
|
|
func isParenthesesClose(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.trigger) { |
|
// We'll catch this one on the next time round |
|
return nil |
|
} |
|
|
|
var startMarkLen int |
|
var stopMark []byte |
|
checkSurrounding := true |
|
if line[0] == '$' { |
|
startMarkLen = 1 |
|
stopMark = parser.endBytesSingleDollar |
|
if len(line) > 1 { |
|
switch line[1] { |
|
case '$': |
|
startMarkLen = 2 |
|
stopMark = parser.endBytesDoubleDollar |
|
case '`': |
|
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.endBytesParentheses |
|
} |
|
|
|
if line[0] == '$' && !parser.enableInlineDollar && (len(line) == 1 || line[1] != '`') { |
|
return nil |
|
} |
|
|
|
if checkSurrounding { |
|
precedingCharacter := block.PrecendingCharacter() |
|
if precedingCharacter < 256 && (isAlphanumeric(byte(precedingCharacter)) || isPunctuation(byte(precedingCharacter))) { |
|
// need to exclude things like `a$` from being considered a start |
|
return nil |
|
} |
|
} |
|
|
|
// move the opener marker point at the start of the text |
|
opener := startMarkLen |
|
|
|
// Now look for an ending line |
|
depth := 0 |
|
ender := -1 |
|
for i := opener; i < len(line); i++ { |
|
if depth == 0 && bytes.HasPrefix(line[i:], stopMark) { |
|
succeedingCharacter := byte(0) |
|
if i+len(stopMark) < len(line) { |
|
succeedingCharacter = line[i+len(stopMark)] |
|
} |
|
// check valid ending character |
|
isValidEndingChar := isPunctuation(succeedingCharacter) || isParenthesesClose(succeedingCharacter) || |
|
succeedingCharacter == ' ' || succeedingCharacter == '\n' || succeedingCharacter == 0 |
|
if checkSurrounding && !isValidEndingChar { |
|
break |
|
} |
|
ender = i |
|
break |
|
} |
|
if line[i] == '\\' { |
|
i++ |
|
continue |
|
} |
|
switch line[i] { |
|
case '{': |
|
depth++ |
|
case '}': |
|
depth-- |
|
} |
|
} |
|
if ender == -1 { |
|
return nil |
|
} |
|
|
|
block.Advance(opener) |
|
_, pos := block.Position() |
|
node := NewInline() |
|
|
|
segment := pos.WithStop(pos.Start + ender - opener) |
|
node.AppendChild(node, ast.NewRawTextSegment(segment)) |
|
block.Advance(ender - opener + len(stopMark)) |
|
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) |
|
}
|
|
|