mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
176 lines
4.3 KiB
Go
176 lines
4.3 KiB
Go
// Copyright 2026 Hanzo AI, Inc. 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')
|
|
}
|
|
|
|
func isInMarkdownLinkText(block text.Reader, lineAfter []byte) bool {
|
|
return block.PrecendingCharacter() == '[' && bytes.HasPrefix(lineAfter, []byte("]("))
|
|
}
|
|
|
|
// 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 ||
|
|
succeedingCharacter == '$' ||
|
|
isInMarkdownLinkText(block, line[i+len(stopMark):])
|
|
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)
|
|
}
|