list.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. 'use strict';
  2. /* eslint-disable max-params */
  3. var trim = require('trim');
  4. var repeat = require('repeat-string');
  5. var decimal = require('is-decimal');
  6. var getIndent = require('../util/get-indentation');
  7. var removeIndent = require('../util/remove-indentation');
  8. var interrupt = require('../util/interrupt');
  9. module.exports = list;
  10. var C_ASTERISK = '*';
  11. var C_UNDERSCORE = '_';
  12. var C_PLUS = '+';
  13. var C_DASH = '-';
  14. var C_DOT = '.';
  15. var C_SPACE = ' ';
  16. var C_NEWLINE = '\n';
  17. var C_TAB = '\t';
  18. var C_PAREN_CLOSE = ')';
  19. var C_X_LOWER = 'x';
  20. var TAB_SIZE = 4;
  21. var EXPRESSION_LOOSE_LIST_ITEM = /\n\n(?!\s*$)/;
  22. var EXPRESSION_TASK_ITEM = /^\[([ \t]|x|X)][ \t]/;
  23. var EXPRESSION_BULLET = /^([ \t]*)([*+-]|\d+[.)])( {1,4}(?! )| |\t|$|(?=\n))([^\n]*)/;
  24. var EXPRESSION_PEDANTIC_BULLET = /^([ \t]*)([*+-]|\d+[.)])([ \t]+)/;
  25. var EXPRESSION_INITIAL_INDENT = /^( {1,4}|\t)?/gm;
  26. /* Map of characters which can be used to mark
  27. * list-items. */
  28. var LIST_UNORDERED_MARKERS = {};
  29. LIST_UNORDERED_MARKERS[C_ASTERISK] = true;
  30. LIST_UNORDERED_MARKERS[C_PLUS] = true;
  31. LIST_UNORDERED_MARKERS[C_DASH] = true;
  32. /* Map of characters which can be used to mark
  33. * list-items after a digit. */
  34. var LIST_ORDERED_MARKERS = {};
  35. LIST_ORDERED_MARKERS[C_DOT] = true;
  36. /* Map of characters which can be used to mark
  37. * list-items after a digit. */
  38. var LIST_ORDERED_COMMONMARK_MARKERS = {};
  39. LIST_ORDERED_COMMONMARK_MARKERS[C_DOT] = true;
  40. LIST_ORDERED_COMMONMARK_MARKERS[C_PAREN_CLOSE] = true;
  41. function list(eat, value, silent) {
  42. var self = this;
  43. var commonmark = self.options.commonmark;
  44. var pedantic = self.options.pedantic;
  45. var tokenizers = self.blockTokenizers;
  46. var interuptors = self.interruptList;
  47. var markers;
  48. var index = 0;
  49. var length = value.length;
  50. var start = null;
  51. var size = 0;
  52. var queue;
  53. var ordered;
  54. var character;
  55. var marker;
  56. var nextIndex;
  57. var startIndex;
  58. var prefixed;
  59. var currentMarker;
  60. var content;
  61. var line;
  62. var prevEmpty;
  63. var empty;
  64. var items;
  65. var allLines;
  66. var emptyLines;
  67. var item;
  68. var enterTop;
  69. var exitBlockquote;
  70. var isLoose;
  71. var node;
  72. var now;
  73. var end;
  74. var indented;
  75. while (index < length) {
  76. character = value.charAt(index);
  77. if (character === C_TAB) {
  78. size += TAB_SIZE - (size % TAB_SIZE);
  79. } else if (character === C_SPACE) {
  80. size++;
  81. } else {
  82. break;
  83. }
  84. index++;
  85. }
  86. if (size >= TAB_SIZE) {
  87. return;
  88. }
  89. character = value.charAt(index);
  90. markers = commonmark ?
  91. LIST_ORDERED_COMMONMARK_MARKERS :
  92. LIST_ORDERED_MARKERS;
  93. if (LIST_UNORDERED_MARKERS[character] === true) {
  94. marker = character;
  95. ordered = false;
  96. } else {
  97. ordered = true;
  98. queue = '';
  99. while (index < length) {
  100. character = value.charAt(index);
  101. if (!decimal(character)) {
  102. break;
  103. }
  104. queue += character;
  105. index++;
  106. }
  107. character = value.charAt(index);
  108. if (!queue || markers[character] !== true) {
  109. return;
  110. }
  111. start = parseInt(queue, 10);
  112. marker = character;
  113. }
  114. character = value.charAt(++index);
  115. if (character !== C_SPACE && character !== C_TAB) {
  116. return;
  117. }
  118. if (silent) {
  119. return true;
  120. }
  121. index = 0;
  122. items = [];
  123. allLines = [];
  124. emptyLines = [];
  125. while (index < length) {
  126. nextIndex = value.indexOf(C_NEWLINE, index);
  127. startIndex = index;
  128. prefixed = false;
  129. indented = false;
  130. if (nextIndex === -1) {
  131. nextIndex = length;
  132. }
  133. end = index + TAB_SIZE;
  134. size = 0;
  135. while (index < length) {
  136. character = value.charAt(index);
  137. if (character === C_TAB) {
  138. size += TAB_SIZE - (size % TAB_SIZE);
  139. } else if (character === C_SPACE) {
  140. size++;
  141. } else {
  142. break;
  143. }
  144. index++;
  145. }
  146. if (size >= TAB_SIZE) {
  147. indented = true;
  148. }
  149. if (item && size >= item.indent) {
  150. indented = true;
  151. }
  152. character = value.charAt(index);
  153. currentMarker = null;
  154. if (!indented) {
  155. if (LIST_UNORDERED_MARKERS[character] === true) {
  156. currentMarker = character;
  157. index++;
  158. size++;
  159. } else {
  160. queue = '';
  161. while (index < length) {
  162. character = value.charAt(index);
  163. if (!decimal(character)) {
  164. break;
  165. }
  166. queue += character;
  167. index++;
  168. }
  169. character = value.charAt(index);
  170. index++;
  171. if (queue && markers[character] === true) {
  172. currentMarker = character;
  173. size += queue.length + 1;
  174. }
  175. }
  176. if (currentMarker) {
  177. character = value.charAt(index);
  178. if (character === C_TAB) {
  179. size += TAB_SIZE - (size % TAB_SIZE);
  180. index++;
  181. } else if (character === C_SPACE) {
  182. end = index + TAB_SIZE;
  183. while (index < end) {
  184. if (value.charAt(index) !== C_SPACE) {
  185. break;
  186. }
  187. index++;
  188. size++;
  189. }
  190. if (index === end && value.charAt(index) === C_SPACE) {
  191. index -= TAB_SIZE - 1;
  192. size -= TAB_SIZE - 1;
  193. }
  194. } else if (character !== C_NEWLINE && character !== '') {
  195. currentMarker = null;
  196. }
  197. }
  198. }
  199. if (currentMarker) {
  200. if (!pedantic && marker !== currentMarker) {
  201. break;
  202. }
  203. prefixed = true;
  204. } else {
  205. if (!commonmark && !indented && value.charAt(startIndex) === C_SPACE) {
  206. indented = true;
  207. } else if (commonmark && item) {
  208. indented = size >= item.indent || size > TAB_SIZE;
  209. }
  210. prefixed = false;
  211. index = startIndex;
  212. }
  213. line = value.slice(startIndex, nextIndex);
  214. content = startIndex === index ? line : value.slice(index, nextIndex);
  215. if (
  216. currentMarker === C_ASTERISK ||
  217. currentMarker === C_UNDERSCORE ||
  218. currentMarker === C_DASH
  219. ) {
  220. if (tokenizers.thematicBreak.call(self, eat, line, true)) {
  221. break;
  222. }
  223. }
  224. prevEmpty = empty;
  225. empty = !trim(content).length;
  226. if (indented && item) {
  227. item.value = item.value.concat(emptyLines, line);
  228. allLines = allLines.concat(emptyLines, line);
  229. emptyLines = [];
  230. } else if (prefixed) {
  231. if (emptyLines.length !== 0) {
  232. item.value.push('');
  233. item.trail = emptyLines.concat();
  234. }
  235. item = {
  236. value: [line],
  237. indent: size,
  238. trail: []
  239. };
  240. items.push(item);
  241. allLines = allLines.concat(emptyLines, line);
  242. emptyLines = [];
  243. } else if (empty) {
  244. if (prevEmpty) {
  245. break;
  246. }
  247. emptyLines.push(line);
  248. } else {
  249. if (prevEmpty) {
  250. break;
  251. }
  252. if (interrupt(interuptors, tokenizers, self, [eat, line, true])) {
  253. break;
  254. }
  255. item.value = item.value.concat(emptyLines, line);
  256. allLines = allLines.concat(emptyLines, line);
  257. emptyLines = [];
  258. }
  259. index = nextIndex + 1;
  260. }
  261. node = eat(allLines.join(C_NEWLINE)).reset({
  262. type: 'list',
  263. ordered: ordered,
  264. start: start,
  265. loose: null,
  266. children: []
  267. });
  268. enterTop = self.enterList();
  269. exitBlockquote = self.enterBlock();
  270. isLoose = false;
  271. index = -1;
  272. length = items.length;
  273. while (++index < length) {
  274. item = items[index].value.join(C_NEWLINE);
  275. now = eat.now();
  276. item = eat(item)(listItem(self, item, now), node);
  277. if (item.loose) {
  278. isLoose = true;
  279. }
  280. item = items[index].trail.join(C_NEWLINE);
  281. if (index !== length - 1) {
  282. item += C_NEWLINE;
  283. }
  284. eat(item);
  285. }
  286. enterTop();
  287. exitBlockquote();
  288. node.loose = isLoose;
  289. return node;
  290. }
  291. function listItem(ctx, value, position) {
  292. var offsets = ctx.offset;
  293. var fn = ctx.options.pedantic ? pedanticListItem : normalListItem;
  294. var checked = null;
  295. var task;
  296. var indent;
  297. value = fn.apply(null, arguments);
  298. if (ctx.options.gfm) {
  299. task = value.match(EXPRESSION_TASK_ITEM);
  300. if (task) {
  301. indent = task[0].length;
  302. checked = task[1].toLowerCase() === C_X_LOWER;
  303. offsets[position.line] += indent;
  304. value = value.slice(indent);
  305. }
  306. }
  307. return {
  308. type: 'listItem',
  309. loose: EXPRESSION_LOOSE_LIST_ITEM.test(value) ||
  310. value.charAt(value.length - 1) === C_NEWLINE,
  311. checked: checked,
  312. children: ctx.tokenizeBlock(value, position)
  313. };
  314. }
  315. /* Create a list-item using overly simple mechanics. */
  316. function pedanticListItem(ctx, value, position) {
  317. var offsets = ctx.offset;
  318. var line = position.line;
  319. /* Remove the list-item’s bullet. */
  320. value = value.replace(EXPRESSION_PEDANTIC_BULLET, replacer);
  321. /* The initial line was also matched by the below, so
  322. * we reset the `line`. */
  323. line = position.line;
  324. return value.replace(EXPRESSION_INITIAL_INDENT, replacer);
  325. /* A simple replacer which removed all matches,
  326. * and adds their length to `offset`. */
  327. function replacer($0) {
  328. offsets[line] = (offsets[line] || 0) + $0.length;
  329. line++;
  330. return '';
  331. }
  332. }
  333. /* Create a list-item using sane mechanics. */
  334. function normalListItem(ctx, value, position) {
  335. var offsets = ctx.offset;
  336. var line = position.line;
  337. var max;
  338. var bullet;
  339. var rest;
  340. var lines;
  341. var trimmedLines;
  342. var index;
  343. var length;
  344. /* Remove the list-item’s bullet. */
  345. value = value.replace(EXPRESSION_BULLET, replacer);
  346. lines = value.split(C_NEWLINE);
  347. trimmedLines = removeIndent(value, getIndent(max).indent).split(C_NEWLINE);
  348. /* We replaced the initial bullet with something
  349. * else above, which was used to trick
  350. * `removeIndentation` into removing some more
  351. * characters when possible. However, that could
  352. * result in the initial line to be stripped more
  353. * than it should be. */
  354. trimmedLines[0] = rest;
  355. offsets[line] = (offsets[line] || 0) + bullet.length;
  356. line++;
  357. index = 0;
  358. length = lines.length;
  359. while (++index < length) {
  360. offsets[line] = (offsets[line] || 0) +
  361. lines[index].length - trimmedLines[index].length;
  362. line++;
  363. }
  364. return trimmedLines.join(C_NEWLINE);
  365. function replacer($0, $1, $2, $3, $4) {
  366. bullet = $1 + $2 + $3;
  367. rest = $4;
  368. /* Make sure that the first nine numbered list items
  369. * can indent with an extra space. That is, when
  370. * the bullet did not receive an extra final space. */
  371. if (Number($2) < 10 && bullet.length % 2 === 1) {
  372. $2 = C_SPACE + $2;
  373. }
  374. max = $1 + repeat(C_SPACE, $2.length) + $3;
  375. return max + rest;
  376. }
  377. }