A fork of Gitea (see branch `mj`) adding Majority Judgment Polls 𐄷 over Issues and Merge Requests. https://git.mieuxvoter.fr
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.

251 lines
5.7 KiB

  1. package parser
  2. import (
  3. "github.com/yuin/goldmark/ast"
  4. "github.com/yuin/goldmark/text"
  5. "github.com/yuin/goldmark/util"
  6. "strconv"
  7. )
  8. type listItemType int
  9. const (
  10. notList listItemType = iota
  11. bulletList
  12. orderedList
  13. )
  14. // Same as
  15. // `^(([ ]*)([\-\*\+]))(\s+.*)?\n?$`.FindSubmatchIndex or
  16. // `^(([ ]*)(\d{1,9}[\.\)]))(\s+.*)?\n?$`.FindSubmatchIndex
  17. func parseListItem(line []byte) ([6]int, listItemType) {
  18. i := 0
  19. l := len(line)
  20. ret := [6]int{}
  21. for ; i < l && line[i] == ' '; i++ {
  22. c := line[i]
  23. if c == '\t' {
  24. return ret, notList
  25. }
  26. }
  27. if i > 3 {
  28. return ret, notList
  29. }
  30. ret[0] = 0
  31. ret[1] = i
  32. ret[2] = i
  33. var typ listItemType
  34. if i < l && (line[i] == '-' || line[i] == '*' || line[i] == '+') {
  35. i++
  36. ret[3] = i
  37. typ = bulletList
  38. } else if i < l {
  39. for ; i < l && util.IsNumeric(line[i]); i++ {
  40. }
  41. ret[3] = i
  42. if ret[3] == ret[2] || ret[3]-ret[2] > 9 {
  43. return ret, notList
  44. }
  45. if i < l && (line[i] == '.' || line[i] == ')') {
  46. i++
  47. ret[3] = i
  48. } else {
  49. return ret, notList
  50. }
  51. typ = orderedList
  52. } else {
  53. return ret, notList
  54. }
  55. if i < l && line[i] != '\n' {
  56. w, _ := util.IndentWidth(line[i:], 0)
  57. if w == 0 {
  58. return ret, notList
  59. }
  60. }
  61. if i >= l {
  62. ret[4] = -1
  63. ret[5] = -1
  64. return ret, typ
  65. }
  66. ret[4] = i
  67. ret[5] = len(line)
  68. if line[ret[5]-1] == '\n' && line[i] != '\n' {
  69. ret[5]--
  70. }
  71. return ret, typ
  72. }
  73. func matchesListItem(source []byte, strict bool) ([6]int, listItemType) {
  74. m, typ := parseListItem(source)
  75. if typ != notList && (!strict || strict && m[1] < 4) {
  76. return m, typ
  77. }
  78. return m, notList
  79. }
  80. func calcListOffset(source []byte, match [6]int) int {
  81. offset := 0
  82. if match[4] < 0 || util.IsBlank(source[match[4]:]) { // list item starts with a blank line
  83. offset = 1
  84. } else {
  85. offset, _ = util.IndentWidth(source[match[4]:], match[4])
  86. if offset > 4 { // offseted codeblock
  87. offset = 1
  88. }
  89. }
  90. return offset
  91. }
  92. func lastOffset(node ast.Node) int {
  93. lastChild := node.LastChild()
  94. if lastChild != nil {
  95. return lastChild.(*ast.ListItem).Offset
  96. }
  97. return 0
  98. }
  99. type listParser struct {
  100. }
  101. var defaultListParser = &listParser{}
  102. // NewListParser returns a new BlockParser that
  103. // parses lists.
  104. // This parser must take precedence over the ListItemParser.
  105. func NewListParser() BlockParser {
  106. return defaultListParser
  107. }
  108. func (b *listParser) Trigger() []byte {
  109. return []byte{'-', '+', '*', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}
  110. }
  111. func (b *listParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
  112. last := pc.LastOpenedBlock().Node
  113. if _, lok := last.(*ast.List); lok || pc.Get(skipListParser) != nil {
  114. pc.Set(skipListParser, nil)
  115. return nil, NoChildren
  116. }
  117. line, _ := reader.PeekLine()
  118. match, typ := matchesListItem(line, true)
  119. if typ == notList {
  120. return nil, NoChildren
  121. }
  122. start := -1
  123. if typ == orderedList {
  124. number := line[match[2] : match[3]-1]
  125. start, _ = strconv.Atoi(string(number))
  126. }
  127. if ast.IsParagraph(last) && last.Parent() == parent {
  128. // we allow only lists starting with 1 to interrupt paragraphs.
  129. if typ == orderedList && start != 1 {
  130. return nil, NoChildren
  131. }
  132. //an empty list item cannot interrupt a paragraph:
  133. if match[5]-match[4] == 1 {
  134. return nil, NoChildren
  135. }
  136. }
  137. marker := line[match[3]-1]
  138. node := ast.NewList(marker)
  139. if start > -1 {
  140. node.Start = start
  141. }
  142. return node, HasChildren
  143. }
  144. func (b *listParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
  145. list := node.(*ast.List)
  146. line, _ := reader.PeekLine()
  147. if util.IsBlank(line) {
  148. // A list item can begin with at most one blank line
  149. if node.ChildCount() == 1 && node.LastChild().ChildCount() == 0 {
  150. return Close
  151. }
  152. return Continue | HasChildren
  153. }
  154. // "offset" means a width that bar indicates.
  155. // - aaaaaaaa
  156. // |----|
  157. //
  158. // If the indent is less than the last offset like
  159. // - a
  160. // - b <--- current line
  161. // it maybe a new child of the list.
  162. offset := lastOffset(node)
  163. indent, _ := util.IndentWidth(line, reader.LineOffset())
  164. if indent < offset {
  165. if indent < 4 {
  166. match, typ := matchesListItem(line, false) // may have a leading spaces more than 3
  167. if typ != notList && match[1]-offset < 4 {
  168. marker := line[match[3]-1]
  169. if !list.CanContinue(marker, typ == orderedList) {
  170. return Close
  171. }
  172. // Thematic Breaks take precedence over lists
  173. if isThematicBreak(line[match[3]-1:], 0) {
  174. isHeading := false
  175. last := pc.LastOpenedBlock().Node
  176. if ast.IsParagraph(last) {
  177. c, ok := matchesSetextHeadingBar(line)
  178. if ok && c == '-' {
  179. isHeading = true
  180. }
  181. }
  182. if !isHeading {
  183. return Close
  184. }
  185. }
  186. return Continue | HasChildren
  187. }
  188. }
  189. return Close
  190. }
  191. return Continue | HasChildren
  192. }
  193. func (b *listParser) Close(node ast.Node, reader text.Reader, pc Context) {
  194. list := node.(*ast.List)
  195. for c := node.FirstChild(); c != nil && list.IsTight; c = c.NextSibling() {
  196. if c.FirstChild() != nil && c.FirstChild() != c.LastChild() {
  197. for c1 := c.FirstChild().NextSibling(); c1 != nil; c1 = c1.NextSibling() {
  198. if bl, ok := c1.(ast.Node); ok && bl.HasBlankPreviousLines() {
  199. list.IsTight = false
  200. break
  201. }
  202. }
  203. }
  204. if c != node.FirstChild() {
  205. if bl, ok := c.(ast.Node); ok && bl.HasBlankPreviousLines() {
  206. list.IsTight = false
  207. }
  208. }
  209. }
  210. if list.IsTight {
  211. for child := node.FirstChild(); child != nil; child = child.NextSibling() {
  212. for gc := child.FirstChild(); gc != nil; gc = gc.NextSibling() {
  213. paragraph, ok := gc.(*ast.Paragraph)
  214. if ok {
  215. textBlock := ast.NewTextBlock()
  216. textBlock.SetLines(paragraph.Lines())
  217. child.ReplaceChild(child, paragraph, textBlock)
  218. }
  219. }
  220. }
  221. }
  222. }
  223. func (b *listParser) CanInterruptParagraph() bool {
  224. return true
  225. }
  226. func (b *listParser) CanAcceptIndentedLine() bool {
  227. return false
  228. }