|
|
'use strict';
|
|
|
|
|
|
/* eslint-disable max-params */
|
|
|
|
|
|
var trim = require('trim');
|
|
|
var repeat = require('repeat-string');
|
|
|
var decimal = require('is-decimal');
|
|
|
var getIndent = require('../util/get-indentation');
|
|
|
var removeIndent = require('../util/remove-indentation');
|
|
|
var interrupt = require('../util/interrupt');
|
|
|
|
|
|
module.exports = list;
|
|
|
|
|
|
var C_ASTERISK = '*';
|
|
|
var C_UNDERSCORE = '_';
|
|
|
var C_PLUS = '+';
|
|
|
var C_DASH = '-';
|
|
|
var C_DOT = '.';
|
|
|
var C_SPACE = ' ';
|
|
|
var C_NEWLINE = '\n';
|
|
|
var C_TAB = '\t';
|
|
|
var C_PAREN_CLOSE = ')';
|
|
|
var C_X_LOWER = 'x';
|
|
|
|
|
|
var TAB_SIZE = 4;
|
|
|
var EXPRESSION_LOOSE_LIST_ITEM = /\n\n(?!\s*$)/;
|
|
|
var EXPRESSION_TASK_ITEM = /^\[([ \t]|x|X)][ \t]/;
|
|
|
var EXPRESSION_BULLET = /^([ \t]*)([*+-]|\d+[.)])( {1,4}(?! )| |\t|$|(?=\n))([^\n]*)/;
|
|
|
var EXPRESSION_PEDANTIC_BULLET = /^([ \t]*)([*+-]|\d+[.)])([ \t]+)/;
|
|
|
var EXPRESSION_INITIAL_INDENT = /^( {1,4}|\t)?/gm;
|
|
|
|
|
|
/* Map of characters which can be used to mark
|
|
|
* list-items. */
|
|
|
var LIST_UNORDERED_MARKERS = {};
|
|
|
|
|
|
LIST_UNORDERED_MARKERS[C_ASTERISK] = true;
|
|
|
LIST_UNORDERED_MARKERS[C_PLUS] = true;
|
|
|
LIST_UNORDERED_MARKERS[C_DASH] = true;
|
|
|
|
|
|
/* Map of characters which can be used to mark
|
|
|
* list-items after a digit. */
|
|
|
var LIST_ORDERED_MARKERS = {};
|
|
|
|
|
|
LIST_ORDERED_MARKERS[C_DOT] = true;
|
|
|
|
|
|
/* Map of characters which can be used to mark
|
|
|
* list-items after a digit. */
|
|
|
var LIST_ORDERED_COMMONMARK_MARKERS = {};
|
|
|
|
|
|
LIST_ORDERED_COMMONMARK_MARKERS[C_DOT] = true;
|
|
|
LIST_ORDERED_COMMONMARK_MARKERS[C_PAREN_CLOSE] = true;
|
|
|
|
|
|
function list(eat, value, silent) {
|
|
|
var self = this;
|
|
|
var commonmark = self.options.commonmark;
|
|
|
var pedantic = self.options.pedantic;
|
|
|
var tokenizers = self.blockTokenizers;
|
|
|
var interuptors = self.interruptList;
|
|
|
var markers;
|
|
|
var index = 0;
|
|
|
var length = value.length;
|
|
|
var start = null;
|
|
|
var size = 0;
|
|
|
var queue;
|
|
|
var ordered;
|
|
|
var character;
|
|
|
var marker;
|
|
|
var nextIndex;
|
|
|
var startIndex;
|
|
|
var prefixed;
|
|
|
var currentMarker;
|
|
|
var content;
|
|
|
var line;
|
|
|
var prevEmpty;
|
|
|
var empty;
|
|
|
var items;
|
|
|
var allLines;
|
|
|
var emptyLines;
|
|
|
var item;
|
|
|
var enterTop;
|
|
|
var exitBlockquote;
|
|
|
var isLoose;
|
|
|
var node;
|
|
|
var now;
|
|
|
var end;
|
|
|
var indented;
|
|
|
|
|
|
while (index < length) {
|
|
|
character = value.charAt(index);
|
|
|
|
|
|
if (character === C_TAB) {
|
|
|
size += TAB_SIZE - (size % TAB_SIZE);
|
|
|
} else if (character === C_SPACE) {
|
|
|
size++;
|
|
|
} else {
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
index++;
|
|
|
}
|
|
|
|
|
|
if (size >= TAB_SIZE) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
character = value.charAt(index);
|
|
|
|
|
|
markers = commonmark ?
|
|
|
LIST_ORDERED_COMMONMARK_MARKERS :
|
|
|
LIST_ORDERED_MARKERS;
|
|
|
|
|
|
if (LIST_UNORDERED_MARKERS[character] === true) {
|
|
|
marker = character;
|
|
|
ordered = false;
|
|
|
} else {
|
|
|
ordered = true;
|
|
|
queue = '';
|
|
|
|
|
|
while (index < length) {
|
|
|
character = value.charAt(index);
|
|
|
|
|
|
if (!decimal(character)) {
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
queue += character;
|
|
|
index++;
|
|
|
}
|
|
|
|
|
|
character = value.charAt(index);
|
|
|
|
|
|
if (!queue || markers[character] !== true) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
start = parseInt(queue, 10);
|
|
|
marker = character;
|
|
|
}
|
|
|
|
|
|
character = value.charAt(++index);
|
|
|
|
|
|
if (character !== C_SPACE && character !== C_TAB) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (silent) {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
index = 0;
|
|
|
items = [];
|
|
|
allLines = [];
|
|
|
emptyLines = [];
|
|
|
|
|
|
while (index < length) {
|
|
|
nextIndex = value.indexOf(C_NEWLINE, index);
|
|
|
startIndex = index;
|
|
|
prefixed = false;
|
|
|
indented = false;
|
|
|
|
|
|
if (nextIndex === -1) {
|
|
|
nextIndex = length;
|
|
|
}
|
|
|
|
|
|
end = index + TAB_SIZE;
|
|
|
size = 0;
|
|
|
|
|
|
while (index < length) {
|
|
|
character = value.charAt(index);
|
|
|
|
|
|
if (character === C_TAB) {
|
|
|
size += TAB_SIZE - (size % TAB_SIZE);
|
|
|
} else if (character === C_SPACE) {
|
|
|
size++;
|
|
|
} else {
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
index++;
|
|
|
}
|
|
|
|
|
|
if (size >= TAB_SIZE) {
|
|
|
indented = true;
|
|
|
}
|
|
|
|
|
|
if (item && size >= item.indent) {
|
|
|
indented = true;
|
|
|
}
|
|
|
|
|
|
character = value.charAt(index);
|
|
|
currentMarker = null;
|
|
|
|
|
|
if (!indented) {
|
|
|
if (LIST_UNORDERED_MARKERS[character] === true) {
|
|
|
currentMarker = character;
|
|
|
index++;
|
|
|
size++;
|
|
|
} else {
|
|
|
queue = '';
|
|
|
|
|
|
while (index < length) {
|
|
|
character = value.charAt(index);
|
|
|
|
|
|
if (!decimal(character)) {
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
queue += character;
|
|
|
index++;
|
|
|
}
|
|
|
|
|
|
character = value.charAt(index);
|
|
|
index++;
|
|
|
|
|
|
if (queue && markers[character] === true) {
|
|
|
currentMarker = character;
|
|
|
size += queue.length + 1;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (currentMarker) {
|
|
|
character = value.charAt(index);
|
|
|
|
|
|
if (character === C_TAB) {
|
|
|
size += TAB_SIZE - (size % TAB_SIZE);
|
|
|
index++;
|
|
|
} else if (character === C_SPACE) {
|
|
|
end = index + TAB_SIZE;
|
|
|
|
|
|
while (index < end) {
|
|
|
if (value.charAt(index) !== C_SPACE) {
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
index++;
|
|
|
size++;
|
|
|
}
|
|
|
|
|
|
if (index === end && value.charAt(index) === C_SPACE) {
|
|
|
index -= TAB_SIZE - 1;
|
|
|
size -= TAB_SIZE - 1;
|
|
|
}
|
|
|
} else if (character !== C_NEWLINE && character !== '') {
|
|
|
currentMarker = null;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (currentMarker) {
|
|
|
if (!pedantic && marker !== currentMarker) {
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
prefixed = true;
|
|
|
} else {
|
|
|
if (!commonmark && !indented && value.charAt(startIndex) === C_SPACE) {
|
|
|
indented = true;
|
|
|
} else if (commonmark && item) {
|
|
|
indented = size >= item.indent || size > TAB_SIZE;
|
|
|
}
|
|
|
|
|
|
prefixed = false;
|
|
|
index = startIndex;
|
|
|
}
|
|
|
|
|
|
line = value.slice(startIndex, nextIndex);
|
|
|
content = startIndex === index ? line : value.slice(index, nextIndex);
|
|
|
|
|
|
if (
|
|
|
currentMarker === C_ASTERISK ||
|
|
|
currentMarker === C_UNDERSCORE ||
|
|
|
currentMarker === C_DASH
|
|
|
) {
|
|
|
if (tokenizers.thematicBreak.call(self, eat, line, true)) {
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
prevEmpty = empty;
|
|
|
empty = !trim(content).length;
|
|
|
|
|
|
if (indented && item) {
|
|
|
item.value = item.value.concat(emptyLines, line);
|
|
|
allLines = allLines.concat(emptyLines, line);
|
|
|
emptyLines = [];
|
|
|
} else if (prefixed) {
|
|
|
if (emptyLines.length !== 0) {
|
|
|
item.value.push('');
|
|
|
item.trail = emptyLines.concat();
|
|
|
}
|
|
|
|
|
|
item = {
|
|
|
value: [line],
|
|
|
indent: size,
|
|
|
trail: []
|
|
|
};
|
|
|
|
|
|
items.push(item);
|
|
|
allLines = allLines.concat(emptyLines, line);
|
|
|
emptyLines = [];
|
|
|
} else if (empty) {
|
|
|
if (prevEmpty) {
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
emptyLines.push(line);
|
|
|
} else {
|
|
|
if (prevEmpty) {
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
if (interrupt(interuptors, tokenizers, self, [eat, line, true])) {
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
item.value = item.value.concat(emptyLines, line);
|
|
|
allLines = allLines.concat(emptyLines, line);
|
|
|
emptyLines = [];
|
|
|
}
|
|
|
|
|
|
index = nextIndex + 1;
|
|
|
}
|
|
|
|
|
|
node = eat(allLines.join(C_NEWLINE)).reset({
|
|
|
type: 'list',
|
|
|
ordered: ordered,
|
|
|
start: start,
|
|
|
loose: null,
|
|
|
children: []
|
|
|
});
|
|
|
|
|
|
enterTop = self.enterList();
|
|
|
exitBlockquote = self.enterBlock();
|
|
|
isLoose = false;
|
|
|
index = -1;
|
|
|
length = items.length;
|
|
|
|
|
|
while (++index < length) {
|
|
|
item = items[index].value.join(C_NEWLINE);
|
|
|
now = eat.now();
|
|
|
|
|
|
item = eat(item)(listItem(self, item, now), node);
|
|
|
|
|
|
if (item.loose) {
|
|
|
isLoose = true;
|
|
|
}
|
|
|
|
|
|
item = items[index].trail.join(C_NEWLINE);
|
|
|
|
|
|
if (index !== length - 1) {
|
|
|
item += C_NEWLINE;
|
|
|
}
|
|
|
|
|
|
eat(item);
|
|
|
}
|
|
|
|
|
|
enterTop();
|
|
|
exitBlockquote();
|
|
|
|
|
|
node.loose = isLoose;
|
|
|
|
|
|
return node;
|
|
|
}
|
|
|
|
|
|
function listItem(ctx, value, position) {
|
|
|
var offsets = ctx.offset;
|
|
|
var fn = ctx.options.pedantic ? pedanticListItem : normalListItem;
|
|
|
var checked = null;
|
|
|
var task;
|
|
|
var indent;
|
|
|
|
|
|
value = fn.apply(null, arguments);
|
|
|
|
|
|
if (ctx.options.gfm) {
|
|
|
task = value.match(EXPRESSION_TASK_ITEM);
|
|
|
|
|
|
if (task) {
|
|
|
indent = task[0].length;
|
|
|
checked = task[1].toLowerCase() === C_X_LOWER;
|
|
|
offsets[position.line] += indent;
|
|
|
value = value.slice(indent);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
type: 'listItem',
|
|
|
loose: EXPRESSION_LOOSE_LIST_ITEM.test(value) ||
|
|
|
value.charAt(value.length - 1) === C_NEWLINE,
|
|
|
checked: checked,
|
|
|
children: ctx.tokenizeBlock(value, position)
|
|
|
};
|
|
|
}
|
|
|
|
|
|
/* Create a list-item using overly simple mechanics. */
|
|
|
function pedanticListItem(ctx, value, position) {
|
|
|
var offsets = ctx.offset;
|
|
|
var line = position.line;
|
|
|
|
|
|
/* Remove the list-item’s bullet. */
|
|
|
value = value.replace(EXPRESSION_PEDANTIC_BULLET, replacer);
|
|
|
|
|
|
/* The initial line was also matched by the below, so
|
|
|
* we reset the `line`. */
|
|
|
line = position.line;
|
|
|
|
|
|
return value.replace(EXPRESSION_INITIAL_INDENT, replacer);
|
|
|
|
|
|
/* A simple replacer which removed all matches,
|
|
|
* and adds their length to `offset`. */
|
|
|
function replacer($0) {
|
|
|
offsets[line] = (offsets[line] || 0) + $0.length;
|
|
|
line++;
|
|
|
|
|
|
return '';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* Create a list-item using sane mechanics. */
|
|
|
function normalListItem(ctx, value, position) {
|
|
|
var offsets = ctx.offset;
|
|
|
var line = position.line;
|
|
|
var max;
|
|
|
var bullet;
|
|
|
var rest;
|
|
|
var lines;
|
|
|
var trimmedLines;
|
|
|
var index;
|
|
|
var length;
|
|
|
|
|
|
/* Remove the list-item’s bullet. */
|
|
|
value = value.replace(EXPRESSION_BULLET, replacer);
|
|
|
|
|
|
lines = value.split(C_NEWLINE);
|
|
|
|
|
|
trimmedLines = removeIndent(value, getIndent(max).indent).split(C_NEWLINE);
|
|
|
|
|
|
/* We replaced the initial bullet with something
|
|
|
* else above, which was used to trick
|
|
|
* `removeIndentation` into removing some more
|
|
|
* characters when possible. However, that could
|
|
|
* result in the initial line to be stripped more
|
|
|
* than it should be. */
|
|
|
trimmedLines[0] = rest;
|
|
|
|
|
|
offsets[line] = (offsets[line] || 0) + bullet.length;
|
|
|
line++;
|
|
|
|
|
|
index = 0;
|
|
|
length = lines.length;
|
|
|
|
|
|
while (++index < length) {
|
|
|
offsets[line] = (offsets[line] || 0) +
|
|
|
lines[index].length - trimmedLines[index].length;
|
|
|
line++;
|
|
|
}
|
|
|
|
|
|
return trimmedLines.join(C_NEWLINE);
|
|
|
|
|
|
function replacer($0, $1, $2, $3, $4) {
|
|
|
bullet = $1 + $2 + $3;
|
|
|
rest = $4;
|
|
|
|
|
|
/* Make sure that the first nine numbered list items
|
|
|
* can indent with an extra space. That is, when
|
|
|
* the bullet did not receive an extra final space. */
|
|
|
if (Number($2) < 10 && bullet.length % 2 === 1) {
|
|
|
$2 = C_SPACE + $2;
|
|
|
}
|
|
|
|
|
|
max = $1 + repeat(C_SPACE, $2.length) + $3;
|
|
|
|
|
|
return max + rest;
|
|
|
}
|
|
|
}
|