code.gitea.io/gitea@v1.22.3/modules/markup/markdown/math/inline_parser.go (about) 1 // Copyright 2022 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package math 5 6 import ( 7 "bytes" 8 9 "github.com/yuin/goldmark/ast" 10 "github.com/yuin/goldmark/parser" 11 "github.com/yuin/goldmark/text" 12 ) 13 14 type inlineParser struct { 15 start []byte 16 end []byte 17 } 18 19 var defaultInlineDollarParser = &inlineParser{ 20 start: []byte{'$'}, 21 end: []byte{'$'}, 22 } 23 24 // NewInlineDollarParser returns a new inline parser 25 func NewInlineDollarParser() parser.InlineParser { 26 return defaultInlineDollarParser 27 } 28 29 var defaultInlineBracketParser = &inlineParser{ 30 start: []byte{'\\', '('}, 31 end: []byte{'\\', ')'}, 32 } 33 34 // NewInlineDollarParser returns a new inline parser 35 func NewInlineBracketParser() parser.InlineParser { 36 return defaultInlineBracketParser 37 } 38 39 // Trigger triggers this parser on $ or \ 40 func (parser *inlineParser) Trigger() []byte { 41 return parser.start[0:1] 42 } 43 44 func isPunctuation(b byte) bool { 45 return b == '.' || b == '!' || b == '?' || b == ',' || b == ';' || b == ':' 46 } 47 48 func isBracket(b byte) bool { 49 return b == ')' 50 } 51 52 func isAlphanumeric(b byte) bool { 53 return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') 54 } 55 56 // Parse parses the current line and returns a result of parsing. 57 func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { 58 line, _ := block.PeekLine() 59 60 if !bytes.HasPrefix(line, parser.start) { 61 // We'll catch this one on the next time round 62 return nil 63 } 64 65 precedingCharacter := block.PrecendingCharacter() 66 if precedingCharacter < 256 && (isAlphanumeric(byte(precedingCharacter)) || isPunctuation(byte(precedingCharacter))) { 67 // need to exclude things like `a$` from being considered a start 68 return nil 69 } 70 71 // move the opener marker point at the start of the text 72 opener := len(parser.start) 73 74 // Now look for an ending line 75 ender := opener 76 for { 77 pos := bytes.Index(line[ender:], parser.end) 78 if pos < 0 { 79 return nil 80 } 81 82 ender += pos 83 84 // Now we want to check the character at the end of our parser section 85 // that is ender + len(parser.end) and check if char before ender is '\' 86 pos = ender + len(parser.end) 87 if len(line) <= pos { 88 break 89 } 90 suceedingCharacter := line[pos] 91 if !isPunctuation(suceedingCharacter) && !(suceedingCharacter == ' ') && !isBracket(suceedingCharacter) { 92 return nil 93 } 94 if line[ender-1] != '\\' { 95 break 96 } 97 98 // move the pointer onwards 99 ender += len(parser.end) 100 } 101 102 block.Advance(opener) 103 _, pos := block.Position() 104 node := NewInline() 105 segment := pos.WithStop(pos.Start + ender - opener) 106 node.AppendChild(node, ast.NewRawTextSegment(segment)) 107 block.Advance(ender - opener + len(parser.end)) 108 109 trimBlock(node, block) 110 return node 111 } 112 113 func trimBlock(node *Inline, block text.Reader) { 114 if node.IsBlank(block.Source()) { 115 return 116 } 117 118 // trim first space and last space 119 first := node.FirstChild().(*ast.Text) 120 if !(!first.Segment.IsEmpty() && block.Source()[first.Segment.Start] == ' ') { 121 return 122 } 123 124 last := node.LastChild().(*ast.Text) 125 if !(!last.Segment.IsEmpty() && block.Source()[last.Segment.Stop-1] == ' ') { 126 return 127 } 128 129 first.Segment = first.Segment.WithStart(first.Segment.Start + 1) 130 last.Segment = last.Segment.WithStop(last.Segment.Stop - 1) 131 }