list.js 9.3 KB

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