list.mjs 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. // Lists
  2. import { isSpace } from '../common/utils.mjs'
  3. // Search `[-+*][\n ]`, returns next pos after marker on success
  4. // or -1 on fail.
  5. function skipBulletListMarker (state, startLine) {
  6. const max = state.eMarks[startLine]
  7. let pos = state.bMarks[startLine] + state.tShift[startLine]
  8. const marker = state.src.charCodeAt(pos++)
  9. // Check bullet
  10. if (marker !== 0x2A/* * */ &&
  11. marker !== 0x2D/* - */ &&
  12. marker !== 0x2B/* + */) {
  13. return -1
  14. }
  15. if (pos < max) {
  16. const ch = state.src.charCodeAt(pos)
  17. if (!isSpace(ch)) {
  18. // " -test " - is not a list item
  19. return -1
  20. }
  21. }
  22. return pos
  23. }
  24. // Search `\d+[.)][\n ]`, returns next pos after marker on success
  25. // or -1 on fail.
  26. function skipOrderedListMarker (state, startLine) {
  27. const start = state.bMarks[startLine] + state.tShift[startLine]
  28. const max = state.eMarks[startLine]
  29. let pos = start
  30. // List marker should have at least 2 chars (digit + dot)
  31. if (pos + 1 >= max) { return -1 }
  32. let ch = state.src.charCodeAt(pos++)
  33. if (ch < 0x30/* 0 */ || ch > 0x39/* 9 */) { return -1 }
  34. for (;;) {
  35. // EOL -> fail
  36. if (pos >= max) { return -1 }
  37. ch = state.src.charCodeAt(pos++)
  38. if (ch >= 0x30/* 0 */ && ch <= 0x39/* 9 */) {
  39. // List marker should have no more than 9 digits
  40. // (prevents integer overflow in browsers)
  41. if (pos - start >= 10) { return -1 }
  42. continue
  43. }
  44. // found valid marker
  45. if (ch === 0x29/* ) */ || ch === 0x2e/* . */) {
  46. break
  47. }
  48. return -1
  49. }
  50. if (pos < max) {
  51. ch = state.src.charCodeAt(pos)
  52. if (!isSpace(ch)) {
  53. // " 1.test " - is not a list item
  54. return -1
  55. }
  56. }
  57. return pos
  58. }
  59. function markTightParagraphs (state, idx) {
  60. const level = state.level + 2
  61. for (let i = idx + 2, l = state.tokens.length - 2; i < l; i++) {
  62. if (state.tokens[i].level === level && state.tokens[i].type === 'paragraph_open') {
  63. state.tokens[i + 2].hidden = true
  64. state.tokens[i].hidden = true
  65. i += 2
  66. }
  67. }
  68. }
  69. export default function list (state, startLine, endLine, silent) {
  70. let max, pos, start, token
  71. let nextLine = startLine
  72. let tight = true
  73. // if it's indented more than 3 spaces, it should be a code block
  74. if (state.sCount[nextLine] - state.blkIndent >= 4) { return false }
  75. // Special case:
  76. // - item 1
  77. // - item 2
  78. // - item 3
  79. // - item 4
  80. // - this one is a paragraph continuation
  81. if (state.listIndent >= 0 &&
  82. state.sCount[nextLine] - state.listIndent >= 4 &&
  83. state.sCount[nextLine] < state.blkIndent) {
  84. return false
  85. }
  86. let isTerminatingParagraph = false
  87. // limit conditions when list can interrupt
  88. // a paragraph (validation mode only)
  89. if (silent && state.parentType === 'paragraph') {
  90. // Next list item should still terminate previous list item;
  91. //
  92. // This code can fail if plugins use blkIndent as well as lists,
  93. // but I hope the spec gets fixed long before that happens.
  94. //
  95. if (state.sCount[nextLine] >= state.blkIndent) {
  96. isTerminatingParagraph = true
  97. }
  98. }
  99. // Detect list type and position after marker
  100. let isOrdered
  101. let markerValue
  102. let posAfterMarker
  103. if ((posAfterMarker = skipOrderedListMarker(state, nextLine)) >= 0) {
  104. isOrdered = true
  105. start = state.bMarks[nextLine] + state.tShift[nextLine]
  106. markerValue = Number(state.src.slice(start, posAfterMarker - 1))
  107. // If we're starting a new ordered list right after
  108. // a paragraph, it should start with 1.
  109. if (isTerminatingParagraph && markerValue !== 1) return false
  110. } else if ((posAfterMarker = skipBulletListMarker(state, nextLine)) >= 0) {
  111. isOrdered = false
  112. } else {
  113. return false
  114. }
  115. // If we're starting a new unordered list right after
  116. // a paragraph, first line should not be empty.
  117. if (isTerminatingParagraph) {
  118. if (state.skipSpaces(posAfterMarker) >= state.eMarks[nextLine]) return false
  119. }
  120. // For validation mode we can terminate immediately
  121. if (silent) { return true }
  122. // We should terminate list on style change. Remember first one to compare.
  123. const markerCharCode = state.src.charCodeAt(posAfterMarker - 1)
  124. // Start list
  125. const listTokIdx = state.tokens.length
  126. if (isOrdered) {
  127. token = state.push('ordered_list_open', 'ol', 1)
  128. if (markerValue !== 1) {
  129. token.attrs = [['start', markerValue]]
  130. }
  131. } else {
  132. token = state.push('bullet_list_open', 'ul', 1)
  133. }
  134. const listLines = [nextLine, 0]
  135. token.map = listLines
  136. token.markup = String.fromCharCode(markerCharCode)
  137. //
  138. // Iterate list items
  139. //
  140. let prevEmptyEnd = false
  141. const terminatorRules = state.md.block.ruler.getRules('list')
  142. const oldParentType = state.parentType
  143. state.parentType = 'list'
  144. while (nextLine < endLine) {
  145. pos = posAfterMarker
  146. max = state.eMarks[nextLine]
  147. const initial = state.sCount[nextLine] + posAfterMarker - (state.bMarks[nextLine] + state.tShift[nextLine])
  148. let offset = initial
  149. while (pos < max) {
  150. const ch = state.src.charCodeAt(pos)
  151. if (ch === 0x09) {
  152. offset += 4 - (offset + state.bsCount[nextLine]) % 4
  153. } else if (ch === 0x20) {
  154. offset++
  155. } else {
  156. break
  157. }
  158. pos++
  159. }
  160. const contentStart = pos
  161. let indentAfterMarker
  162. if (contentStart >= max) {
  163. // trimming space in "- \n 3" case, indent is 1 here
  164. indentAfterMarker = 1
  165. } else {
  166. indentAfterMarker = offset - initial
  167. }
  168. // If we have more than 4 spaces, the indent is 1
  169. // (the rest is just indented code block)
  170. if (indentAfterMarker > 4) { indentAfterMarker = 1 }
  171. // " - test"
  172. // ^^^^^ - calculating total length of this thing
  173. const indent = initial + indentAfterMarker
  174. // Run subparser & write tokens
  175. token = state.push('list_item_open', 'li', 1)
  176. token.markup = String.fromCharCode(markerCharCode)
  177. const itemLines = [nextLine, 0]
  178. token.map = itemLines
  179. if (isOrdered) {
  180. token.info = state.src.slice(start, posAfterMarker - 1)
  181. }
  182. // change current state, then restore it after parser subcall
  183. const oldTight = state.tight
  184. const oldTShift = state.tShift[nextLine]
  185. const oldSCount = state.sCount[nextLine]
  186. // - example list
  187. // ^ listIndent position will be here
  188. // ^ blkIndent position will be here
  189. //
  190. const oldListIndent = state.listIndent
  191. state.listIndent = state.blkIndent
  192. state.blkIndent = indent
  193. state.tight = true
  194. state.tShift[nextLine] = contentStart - state.bMarks[nextLine]
  195. state.sCount[nextLine] = offset
  196. if (contentStart >= max && state.isEmpty(nextLine + 1)) {
  197. // workaround for this case
  198. // (list item is empty, list terminates before "foo"):
  199. // ~~~~~~~~
  200. // -
  201. //
  202. // foo
  203. // ~~~~~~~~
  204. state.line = Math.min(state.line + 2, endLine)
  205. } else {
  206. state.md.block.tokenize(state, nextLine, endLine, true)
  207. }
  208. // If any of list item is tight, mark list as tight
  209. if (!state.tight || prevEmptyEnd) {
  210. tight = false
  211. }
  212. // Item become loose if finish with empty line,
  213. // but we should filter last element, because it means list finish
  214. prevEmptyEnd = (state.line - nextLine) > 1 && state.isEmpty(state.line - 1)
  215. state.blkIndent = state.listIndent
  216. state.listIndent = oldListIndent
  217. state.tShift[nextLine] = oldTShift
  218. state.sCount[nextLine] = oldSCount
  219. state.tight = oldTight
  220. token = state.push('list_item_close', 'li', -1)
  221. token.markup = String.fromCharCode(markerCharCode)
  222. nextLine = state.line
  223. itemLines[1] = nextLine
  224. if (nextLine >= endLine) { break }
  225. //
  226. // Try to check if list is terminated or continued.
  227. //
  228. if (state.sCount[nextLine] < state.blkIndent) { break }
  229. // if it's indented more than 3 spaces, it should be a code block
  230. if (state.sCount[nextLine] - state.blkIndent >= 4) { break }
  231. // fail if terminating block found
  232. let terminate = false
  233. for (let i = 0, l = terminatorRules.length; i < l; i++) {
  234. if (terminatorRules[i](state, nextLine, endLine, true)) {
  235. terminate = true
  236. break
  237. }
  238. }
  239. if (terminate) { break }
  240. // fail if list has another type
  241. if (isOrdered) {
  242. posAfterMarker = skipOrderedListMarker(state, nextLine)
  243. if (posAfterMarker < 0) { break }
  244. start = state.bMarks[nextLine] + state.tShift[nextLine]
  245. } else {
  246. posAfterMarker = skipBulletListMarker(state, nextLine)
  247. if (posAfterMarker < 0) { break }
  248. }
  249. if (markerCharCode !== state.src.charCodeAt(posAfterMarker - 1)) { break }
  250. }
  251. // Finalize list
  252. if (isOrdered) {
  253. token = state.push('ordered_list_close', 'ol', -1)
  254. } else {
  255. token = state.push('bullet_list_close', 'ul', -1)
  256. }
  257. token.markup = String.fromCharCode(markerCharCode)
  258. listLines[1] = nextLine
  259. state.line = nextLine
  260. state.parentType = oldParentType
  261. // mark paragraphs tight if needed
  262. if (tight) {
  263. markTightParagraphs(state, listTokIdx)
  264. }
  265. return true
  266. }