list.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  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. oldIndent,
  91. oldLIndent,
  92. oldParentType,
  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. // limit conditions when list can interrupt
  107. // a paragraph (validation mode only)
  108. if (silent && state.parentType === 'paragraph') {
  109. // Next list item should still terminate previous list item;
  110. //
  111. // This code can fail if plugins use blkIndent as well as lists,
  112. // but I hope the spec gets fixed long before that happens.
  113. //
  114. if (state.tShift[startLine] >= state.blkIndent) {
  115. isTerminatingParagraph = true;
  116. }
  117. }
  118. // Detect list type and position after marker
  119. if ((posAfterMarker = skipOrderedListMarker(state, startLine)) >= 0) {
  120. isOrdered = true;
  121. start = state.bMarks[startLine] + state.tShift[startLine];
  122. markerValue = Number(state.src.substr(start, posAfterMarker - start - 1));
  123. // If we're starting a new ordered list right after
  124. // a paragraph, it should start with 1.
  125. if (isTerminatingParagraph && markerValue !== 1) return false;
  126. } else if ((posAfterMarker = skipBulletListMarker(state, startLine)) >= 0) {
  127. isOrdered = false;
  128. } else {
  129. return false;
  130. }
  131. // If we're starting a new unordered list right after
  132. // a paragraph, first line should not be empty.
  133. if (isTerminatingParagraph) {
  134. if (state.skipSpaces(posAfterMarker) >= state.eMarks[startLine]) return false;
  135. }
  136. // We should terminate list on style change. Remember first one to compare.
  137. markerCharCode = state.src.charCodeAt(posAfterMarker - 1);
  138. // For validation mode we can terminate immediately
  139. if (silent) { return true; }
  140. // Start list
  141. listTokIdx = state.tokens.length;
  142. if (isOrdered) {
  143. token = state.push('ordered_list_open', 'ol', 1);
  144. if (markerValue !== 1) {
  145. token.attrs = [ [ 'start', markerValue ] ];
  146. }
  147. } else {
  148. token = state.push('bullet_list_open', 'ul', 1);
  149. }
  150. token.map = listLines = [ startLine, 0 ];
  151. token.markup = String.fromCharCode(markerCharCode);
  152. //
  153. // Iterate list items
  154. //
  155. nextLine = startLine;
  156. prevEmptyEnd = false;
  157. terminatorRules = state.md.block.ruler.getRules('list');
  158. oldParentType = state.parentType;
  159. state.parentType = 'list';
  160. while (nextLine < endLine) {
  161. pos = posAfterMarker;
  162. max = state.eMarks[nextLine];
  163. initial = offset = state.sCount[nextLine] + posAfterMarker - (state.bMarks[startLine] + state.tShift[startLine]);
  164. while (pos < max) {
  165. ch = state.src.charCodeAt(pos);
  166. if (ch === 0x09) {
  167. offset += 4 - (offset + state.bsCount[nextLine]) % 4;
  168. } else if (ch === 0x20) {
  169. offset++;
  170. } else {
  171. break;
  172. }
  173. pos++;
  174. }
  175. contentStart = pos;
  176. if (contentStart >= max) {
  177. // trimming space in "- \n 3" case, indent is 1 here
  178. indentAfterMarker = 1;
  179. } else {
  180. indentAfterMarker = offset - initial;
  181. }
  182. // If we have more than 4 spaces, the indent is 1
  183. // (the rest is just indented code block)
  184. if (indentAfterMarker > 4) { indentAfterMarker = 1; }
  185. // " - test"
  186. // ^^^^^ - calculating total length of this thing
  187. indent = initial + indentAfterMarker;
  188. // Run subparser & write tokens
  189. token = state.push('list_item_open', 'li', 1);
  190. token.markup = String.fromCharCode(markerCharCode);
  191. token.map = itemLines = [ startLine, 0 ];
  192. oldIndent = state.blkIndent;
  193. oldTight = state.tight;
  194. oldTShift = state.tShift[startLine];
  195. oldLIndent = state.sCount[startLine];
  196. state.blkIndent = indent;
  197. state.tight = true;
  198. state.tShift[startLine] = contentStart - state.bMarks[startLine];
  199. state.sCount[startLine] = offset;
  200. if (contentStart >= max && state.isEmpty(startLine + 1)) {
  201. // workaround for this case
  202. // (list item is empty, list terminates before "foo"):
  203. // ~~~~~~~~
  204. // -
  205. //
  206. // foo
  207. // ~~~~~~~~
  208. state.line = Math.min(state.line + 2, endLine);
  209. } else {
  210. state.md.block.tokenize(state, startLine, endLine, true);
  211. }
  212. // If any of list item is tight, mark list as tight
  213. if (!state.tight || prevEmptyEnd) {
  214. tight = false;
  215. }
  216. // Item become loose if finish with empty line,
  217. // but we should filter last element, because it means list finish
  218. prevEmptyEnd = (state.line - startLine) > 1 && state.isEmpty(state.line - 1);
  219. state.blkIndent = oldIndent;
  220. state.tShift[startLine] = oldTShift;
  221. state.sCount[startLine] = oldLIndent;
  222. state.tight = oldTight;
  223. token = state.push('list_item_close', 'li', -1);
  224. token.markup = String.fromCharCode(markerCharCode);
  225. nextLine = startLine = state.line;
  226. itemLines[1] = nextLine;
  227. contentStart = state.bMarks[startLine];
  228. if (nextLine >= endLine) { break; }
  229. //
  230. // Try to check if list is terminated or continued.
  231. //
  232. if (state.sCount[nextLine] < state.blkIndent) { break; }
  233. // fail if terminating block found
  234. terminate = false;
  235. for (i = 0, l = terminatorRules.length; i < l; i++) {
  236. if (terminatorRules[i](state, nextLine, endLine, true)) {
  237. terminate = true;
  238. break;
  239. }
  240. }
  241. if (terminate) { break; }
  242. // fail if list has another type
  243. if (isOrdered) {
  244. posAfterMarker = skipOrderedListMarker(state, nextLine);
  245. if (posAfterMarker < 0) { break; }
  246. } else {
  247. posAfterMarker = skipBulletListMarker(state, nextLine);
  248. if (posAfterMarker < 0) { break; }
  249. }
  250. if (markerCharCode !== state.src.charCodeAt(posAfterMarker - 1)) { break; }
  251. }
  252. // Finalize list
  253. if (isOrdered) {
  254. token = state.push('ordered_list_close', 'ol', -1);
  255. } else {
  256. token = state.push('bullet_list_close', 'ul', -1);
  257. }
  258. token.markup = String.fromCharCode(markerCharCode);
  259. listLines[1] = nextLine;
  260. state.line = nextLine;
  261. state.parentType = oldParentType;
  262. // mark paragraphs tight if needed
  263. if (tight) {
  264. markTightParagraphs(state, listTokIdx);
  265. }
  266. return true;
  267. };