gitea源码

inline_parser.go 4.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. // Copyright 2022 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package math
  4. import (
  5. "bytes"
  6. "github.com/yuin/goldmark/ast"
  7. "github.com/yuin/goldmark/parser"
  8. "github.com/yuin/goldmark/text"
  9. )
  10. type inlineParser struct {
  11. trigger []byte
  12. endBytesSingleDollar []byte
  13. endBytesDoubleDollar []byte
  14. endBytesParentheses []byte
  15. enableInlineDollar bool
  16. }
  17. func NewInlineDollarParser(enableInlineDollar bool) parser.InlineParser {
  18. return &inlineParser{
  19. trigger: []byte{'$'},
  20. endBytesSingleDollar: []byte{'$'},
  21. endBytesDoubleDollar: []byte{'$', '$'},
  22. enableInlineDollar: enableInlineDollar,
  23. }
  24. }
  25. var defaultInlineParenthesesParser = &inlineParser{
  26. trigger: []byte{'\\', '('},
  27. endBytesParentheses: []byte{'\\', ')'},
  28. }
  29. func NewInlineParenthesesParser() parser.InlineParser {
  30. return defaultInlineParenthesesParser
  31. }
  32. // Trigger triggers this parser on $ or \
  33. func (parser *inlineParser) Trigger() []byte {
  34. return parser.trigger
  35. }
  36. func isPunctuation(b byte) bool {
  37. return b == '.' || b == '!' || b == '?' || b == ',' || b == ';' || b == ':'
  38. }
  39. func isParenthesesClose(b byte) bool {
  40. return b == ')'
  41. }
  42. func isAlphanumeric(b byte) bool {
  43. return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
  44. }
  45. // Parse parses the current line and returns a result of parsing.
  46. func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
  47. line, _ := block.PeekLine()
  48. if !bytes.HasPrefix(line, parser.trigger) {
  49. // We'll catch this one on the next time round
  50. return nil
  51. }
  52. var startMarkLen int
  53. var stopMark []byte
  54. checkSurrounding := true
  55. if line[0] == '$' {
  56. startMarkLen = 1
  57. stopMark = parser.endBytesSingleDollar
  58. if len(line) > 1 {
  59. switch line[1] {
  60. case '$':
  61. startMarkLen = 2
  62. stopMark = parser.endBytesDoubleDollar
  63. case '`':
  64. pos := 1
  65. for ; pos < len(line) && line[pos] == '`'; pos++ {
  66. }
  67. startMarkLen = pos
  68. stopMark = bytes.Repeat([]byte{'`'}, pos)
  69. stopMark[len(stopMark)-1] = '$'
  70. checkSurrounding = false
  71. }
  72. }
  73. } else {
  74. startMarkLen = 2
  75. stopMark = parser.endBytesParentheses
  76. }
  77. if line[0] == '$' && !parser.enableInlineDollar && (len(line) == 1 || line[1] != '`') {
  78. return nil
  79. }
  80. if checkSurrounding {
  81. precedingCharacter := block.PrecendingCharacter()
  82. if precedingCharacter < 256 && (isAlphanumeric(byte(precedingCharacter)) || isPunctuation(byte(precedingCharacter))) {
  83. // need to exclude things like `a$` from being considered a start
  84. return nil
  85. }
  86. }
  87. // move the opener marker point at the start of the text
  88. opener := startMarkLen
  89. // Now look for an ending line
  90. depth := 0
  91. ender := -1
  92. for i := opener; i < len(line); i++ {
  93. if depth == 0 && bytes.HasPrefix(line[i:], stopMark) {
  94. succeedingCharacter := byte(0)
  95. if i+len(stopMark) < len(line) {
  96. succeedingCharacter = line[i+len(stopMark)]
  97. }
  98. // check valid ending character
  99. isValidEndingChar := isPunctuation(succeedingCharacter) || isParenthesesClose(succeedingCharacter) ||
  100. succeedingCharacter == ' ' || succeedingCharacter == '\n' || succeedingCharacter == 0
  101. if checkSurrounding && !isValidEndingChar {
  102. break
  103. }
  104. ender = i
  105. break
  106. }
  107. if line[i] == '\\' {
  108. i++
  109. continue
  110. }
  111. switch line[i] {
  112. case '{':
  113. depth++
  114. case '}':
  115. depth--
  116. }
  117. }
  118. if ender == -1 {
  119. return nil
  120. }
  121. block.Advance(opener)
  122. _, pos := block.Position()
  123. node := NewInline()
  124. segment := pos.WithStop(pos.Start + ender - opener)
  125. node.AppendChild(node, ast.NewRawTextSegment(segment))
  126. block.Advance(ender - opener + len(stopMark))
  127. trimBlock(node, block)
  128. return node
  129. }
  130. func trimBlock(node *Inline, block text.Reader) {
  131. if node.IsBlank(block.Source()) {
  132. return
  133. }
  134. // trim first space and last space
  135. first := node.FirstChild().(*ast.Text)
  136. if !(!first.Segment.IsEmpty() && block.Source()[first.Segment.Start] == ' ') {
  137. return
  138. }
  139. last := node.LastChild().(*ast.Text)
  140. if !(!last.Segment.IsEmpty() && block.Source()[last.Segment.Stop-1] == ' ') {
  141. return
  142. }
  143. first.Segment = first.Segment.WithStart(first.Segment.Start + 1)
  144. last.Segment = last.Segment.WithStop(last.Segment.Stop - 1)
  145. }