|
|
@@ -1,38 +1,376 @@
|
|
|
-export default class ListConfigurer {
|
|
|
+import { isSpace } from 'markdown-it/lib/common/utils';
|
|
|
|
|
|
|
|
|
- configure(md): void {
|
|
|
+// Search `[-+*][\n ]`, returns next pos after marker on success
|
|
|
+// or -1 on fail.
|
|
|
+function skipBulletListMarker(state, startLine) {
|
|
|
+ let pos; let
|
|
|
+ ch;
|
|
|
|
|
|
- type ListOject = {
|
|
|
- indent: number,
|
|
|
- content: string,
|
|
|
- parent: number | null
|
|
|
+ pos = state.bMarks[startLine] + state.tShift[startLine];
|
|
|
+ const max = state.eMarks[startLine];
|
|
|
+
|
|
|
+ const marker = state.src.charCodeAt(pos++);
|
|
|
+ // Check bullet
|
|
|
+ if (marker !== 0x2A
|
|
|
+ &&/* * */ marker !== 0x2D
|
|
|
+ &&/* - */ marker !== 0x2B/* + */) {
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (pos < max) {
|
|
|
+ ch = state.src.charCodeAt(pos);
|
|
|
+
|
|
|
+ if (!isSpace(ch)) {
|
|
|
+ // " -test " - is not a list item
|
|
|
+ return -1;
|
|
|
}
|
|
|
+ }
|
|
|
+
|
|
|
+ return pos;
|
|
|
+}
|
|
|
+
|
|
|
+// Search `\d+[.)][\n ]`, returns next pos after marker on success
|
|
|
+// or -1 on fail.
|
|
|
+function skipOrderedListMarker(state, startLine) {
|
|
|
+ let ch;
|
|
|
+ const start = state.bMarks[startLine] + state.tShift[startLine];
|
|
|
+ let pos = start;
|
|
|
+ const max = state.eMarks[startLine];
|
|
|
+
|
|
|
+ // List marker should have at least 2 chars (digit + dot)
|
|
|
+ if (pos + 1 >= max) { return -1 }
|
|
|
+
|
|
|
+ ch = state.src.charCodeAt(pos++);
|
|
|
+
|
|
|
+ if (ch < 0x30/* 0 */ || ch > 0x39/* 9 */) { return -1 }
|
|
|
+
|
|
|
+ for (;;) {
|
|
|
+ // EOL -> fail
|
|
|
+ if (pos >= max) { return -1 }
|
|
|
+
|
|
|
+ ch = state.src.charCodeAt(pos++);
|
|
|
|
|
|
- md.renderer.rules.ordered_list_open = function(tokens, idx, options, env, self) {
|
|
|
+ if (ch >= 0x30/* 0 */ && ch <= 0x39/* 9 */) {
|
|
|
|
|
|
- const contents = tokens[idx + 3].content;
|
|
|
+ // List marker should have no more than 9 digits
|
|
|
+ // (prevents integer overflow in browsers)
|
|
|
+ if (pos - start >= 10) { return -1 }
|
|
|
+
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // found valid marker
|
|
|
+ if (ch === 0x29/* ) */ || ch === 0x2e/* . */) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ if (pos < max) {
|
|
|
+ ch = state.src.charCodeAt(pos);
|
|
|
+
|
|
|
+ if (!isSpace(ch)) {
|
|
|
+ // " 1.test " - is not a list item
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return pos;
|
|
|
+}
|
|
|
|
|
|
- const splittedContent = contents.split('\n');
|
|
|
- if (splittedContent.length > 1) {
|
|
|
- const contentList: ListOject[] = [];
|
|
|
- let prevIndent;
|
|
|
- splittedContent.forEach((content) => {
|
|
|
- const indent = content.match(/^\s*/)[0].length;
|
|
|
- const listObject = {
|
|
|
- indent,
|
|
|
- content,
|
|
|
- parent: indent === 0 ? null : prevIndent,
|
|
|
- };
|
|
|
- prevIndent = indent;
|
|
|
- contentList.push(listObject);
|
|
|
- });
|
|
|
+function markTightParagraphs(state, idx) {
|
|
|
+ let i; let l;
|
|
|
+ const level = state.level + 2;
|
|
|
|
|
|
+ for (i = idx + 2, l = state.tokens.length - 2; i < l; i++) {
|
|
|
+ if (state.tokens[i].level === level && state.tokens[i].type === 'paragraph_open') {
|
|
|
+ state.tokens[i + 2].hidden = true;
|
|
|
+ state.tokens[i].hidden = true;
|
|
|
+ i += 2;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export function list(state, startLine, endLine, silent) {
|
|
|
+ let ch;
|
|
|
+ let contentStart;
|
|
|
+ let i;
|
|
|
+ let indent;
|
|
|
+ let indentAfterMarker;
|
|
|
+ let initial;
|
|
|
+ let isOrdered;
|
|
|
+ let itemLines;
|
|
|
+ let l;
|
|
|
+ let listLines;
|
|
|
+ let listTokIdx;
|
|
|
+ let markerCharCode;
|
|
|
+ let markerValue;
|
|
|
+ let max;
|
|
|
+ let nextLine;
|
|
|
+ let offset;
|
|
|
+ let oldListIndent;
|
|
|
+ let oldParentType;
|
|
|
+ let oldSCount;
|
|
|
+ let oldTShift;
|
|
|
+ let oldTight;
|
|
|
+ let pos;
|
|
|
+ let posAfterMarker;
|
|
|
+ let prevEmptyEnd;
|
|
|
+ let start;
|
|
|
+ let terminate;
|
|
|
+ let terminatorRules;
|
|
|
+ let token;
|
|
|
+ let isTerminatingParagraph = false;
|
|
|
+ let tight = true;
|
|
|
+
|
|
|
+ // if it's indented more than 3 spaces, it should be a code block
|
|
|
+ if (state.sCount[startLine] - state.blkIndent >= 4) { return false }
|
|
|
+
|
|
|
+ // Special case:
|
|
|
+ // - item 1
|
|
|
+ // - item 2
|
|
|
+ // - item 3
|
|
|
+ // - item 4
|
|
|
+ // - this one is a paragraph continuation
|
|
|
+ if (state.listIndent >= 0
|
|
|
+ && state.sCount[startLine] - state.listIndent >= 4
|
|
|
+ && state.sCount[startLine] < state.blkIndent) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // limit conditions when list can interrupt
|
|
|
+ // a paragraph (validation mode only)
|
|
|
+ if (silent && state.parentType === 'paragraph') {
|
|
|
+ // Next list item should still terminate previous list item;
|
|
|
+ //
|
|
|
+ // This code can fail if plugins use blkIndent as well as lists,
|
|
|
+ // but I hope the spec gets fixed long before that happens.
|
|
|
+ //
|
|
|
+ if (state.tShift[startLine] >= state.blkIndent) {
|
|
|
+ isTerminatingParagraph = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Detect list type and position after marker
|
|
|
+ // eslint-disable-next-line no-cond-assign
|
|
|
+ if ((posAfterMarker = skipOrderedListMarker(state, startLine)) >= 0) {
|
|
|
+ isOrdered = true;
|
|
|
+ start = state.bMarks[startLine] + state.tShift[startLine];
|
|
|
+ markerValue = Number(state.src.substr(start, posAfterMarker - start - 1));
|
|
|
+
|
|
|
+ // If we're starting a new ordered list right after
|
|
|
+ // a paragraph, it should start with 1.
|
|
|
+ if (isTerminatingParagraph && markerValue !== 1) return false;
|
|
|
+
|
|
|
+ }
|
|
|
+ // eslint-disable-next-line no-cond-assign
|
|
|
+ else if ((posAfterMarker = skipBulletListMarker(state, startLine)) >= 0) {
|
|
|
+ isOrdered = false;
|
|
|
+
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // If we're starting a new unordered list right after
|
|
|
+ // a paragraph, first line should not be empty.
|
|
|
+ if (isTerminatingParagraph) {
|
|
|
+ if (state.skipSpaces(posAfterMarker) >= state.eMarks[startLine]) return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // We should terminate list on style change. Remember first one to compare.
|
|
|
+ // eslint-disable-next-line prefer-const
|
|
|
+ markerCharCode = state.src.charCodeAt(posAfterMarker - 1);
|
|
|
+
|
|
|
+ // For validation mode we can terminate immediately
|
|
|
+ if (silent) { return true }
|
|
|
+
|
|
|
+ // Start list
|
|
|
+ // eslint-disable-next-line prefer-const
|
|
|
+ listTokIdx = state.tokens.length;
|
|
|
+
|
|
|
+ if (isOrdered) {
|
|
|
+ token = state.push('ordered_list_open', 'ol', 1);
|
|
|
+ if (markerValue !== 1) {
|
|
|
+ token.attrs = [['start', markerValue]];
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ token = state.push('bullet_list_open', 'ul', 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ // eslint-disable-next-line no-multi-assign
|
|
|
+ token.map = listLines = [startLine, 0];
|
|
|
+ token.markup = String.fromCharCode(markerCharCode);
|
|
|
+
|
|
|
+ //
|
|
|
+ // Iterate list items
|
|
|
+ //
|
|
|
+
|
|
|
+ nextLine = startLine;
|
|
|
+ prevEmptyEnd = false;
|
|
|
+ // eslint-disable-next-line prefer-const
|
|
|
+ terminatorRules = state.md.block.ruler.getRules('list');
|
|
|
+
|
|
|
+ // eslint-disable-next-line prefer-const
|
|
|
+ oldParentType = state.parentType;
|
|
|
+ state.parentType = 'list';
|
|
|
+
|
|
|
+ while (nextLine < endLine) {
|
|
|
+ pos = posAfterMarker;
|
|
|
+ max = state.eMarks[nextLine];
|
|
|
+
|
|
|
+ // eslint-disable-next-line no-multi-assign
|
|
|
+ initial = offset = state.sCount[nextLine] + posAfterMarker - (state.bMarks[startLine] + state.tShift[startLine]);
|
|
|
+
|
|
|
+ while (pos < max) {
|
|
|
+ ch = state.src.charCodeAt(pos);
|
|
|
+
|
|
|
+ if (ch === 0x09) {
|
|
|
+ // eslint-disable-next-line no-mixed-operators
|
|
|
+ offset += 4 - (offset + state.bsCount[nextLine]) % 4;
|
|
|
+ }
|
|
|
+ else if (ch === 0x20) {
|
|
|
+ offset++;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ pos++;
|
|
|
+ }
|
|
|
+
|
|
|
+ contentStart = pos;
|
|
|
+
|
|
|
+ if (contentStart >= max) {
|
|
|
+ // trimming space in "- \n 3" case, indent is 1 here
|
|
|
+ indentAfterMarker = 1;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ indentAfterMarker = offset - initial;
|
|
|
+ }
|
|
|
+
|
|
|
+ // If we have more than 4 spaces, the indent is 1
|
|
|
+ // (the rest is just indented code block)
|
|
|
+ if (indentAfterMarker > 4) { indentAfterMarker = 1 }
|
|
|
+
|
|
|
+ // " - test"
|
|
|
+ // ^^^^^ - calculating total length of this thing
|
|
|
+ indent = initial + indentAfterMarker;
|
|
|
+
|
|
|
+ // Run subparser & write tokens
|
|
|
+ token = state.push('list_item_open', 'li', 1);
|
|
|
+ token.markup = String.fromCharCode(markerCharCode);
|
|
|
+ // eslint-disable-next-line no-multi-assign
|
|
|
+ token.map = itemLines = [startLine, 0];
|
|
|
+
|
|
|
+ // change current state, then restore it after parser subcall
|
|
|
+ oldTight = state.tight;
|
|
|
+ oldTShift = state.tShift[startLine];
|
|
|
+ oldSCount = state.sCount[startLine];
|
|
|
+
|
|
|
+ // - example list
|
|
|
+ // ^ listIndent position will be here
|
|
|
+ // ^ blkIndent position will be here
|
|
|
+ //
|
|
|
+ oldListIndent = state.listIndent;
|
|
|
+ state.listIndent = state.blkIndent;
|
|
|
+ state.blkIndent = indent;
|
|
|
+
|
|
|
+ state.tight = true;
|
|
|
+ state.tShift[startLine] = contentStart - state.bMarks[startLine];
|
|
|
+ state.sCount[startLine] = offset;
|
|
|
+
|
|
|
+ if (contentStart >= max && state.isEmpty(startLine + 1)) {
|
|
|
+ // workaround for this case
|
|
|
+ // (list item is empty, list terminates before "foo"):
|
|
|
+ // ~~~~~~~~
|
|
|
+ // -
|
|
|
+ //
|
|
|
+ // foo
|
|
|
+ // ~~~~~~~~
|
|
|
+ state.line = Math.min(state.line + 2, endLine);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ state.md.block.tokenize(state, startLine, endLine, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ // If any of list item is tight, mark list as tight
|
|
|
+ if (!state.tight || prevEmptyEnd) {
|
|
|
+ tight = false;
|
|
|
+ }
|
|
|
+ // Item become loose if finish with empty line,
|
|
|
+ // but we should filter last element, because it means list finish
|
|
|
+ prevEmptyEnd = (state.line - startLine) > 1 && state.isEmpty(state.line - 1);
|
|
|
+
|
|
|
+ state.blkIndent = state.listIndent;
|
|
|
+ state.listIndent = oldListIndent;
|
|
|
+ state.tShift[startLine] = oldTShift;
|
|
|
+ state.sCount[startLine] = oldSCount;
|
|
|
+ state.tight = oldTight;
|
|
|
+
|
|
|
+ token = state.push('list_item_close', 'li', -1);
|
|
|
+ token.markup = String.fromCharCode(markerCharCode);
|
|
|
+
|
|
|
+ // eslint-disable-next-line no-multi-assign, no-param-reassign
|
|
|
+ nextLine = startLine = state.line;
|
|
|
+ itemLines[1] = nextLine;
|
|
|
+ contentStart = state.bMarks[startLine];
|
|
|
+
|
|
|
+ if (nextLine >= endLine) { break }
|
|
|
+
|
|
|
+ //
|
|
|
+ // Try to check if list is terminated or continued.
|
|
|
+ //
|
|
|
+ if (state.sCount[nextLine] < state.blkIndent) { break }
|
|
|
+
|
|
|
+ // if it's indented more than 3 spaces, it should be a code block
|
|
|
+ if (state.sCount[startLine] - state.blkIndent >= 4) { break }
|
|
|
+
|
|
|
+ // fail if terminating block found
|
|
|
+ terminate = false;
|
|
|
+ for (i = 0, l = terminatorRules.length; i < l; i++) {
|
|
|
+ if (terminatorRules[i](state, nextLine, endLine, true)) {
|
|
|
+ terminate = true;
|
|
|
+ break;
|
|
|
}
|
|
|
- return self.renderToken(tokens, idx);
|
|
|
+ }
|
|
|
+ if (terminate) { break }
|
|
|
+
|
|
|
+ // fail if list has another type
|
|
|
+ if (isOrdered) {
|
|
|
+ posAfterMarker = skipOrderedListMarker(state, nextLine);
|
|
|
+ if (posAfterMarker < 0) { break }
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ posAfterMarker = skipBulletListMarker(state, nextLine);
|
|
|
+ if (posAfterMarker < 0) { break }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (markerCharCode !== state.src.charCodeAt(posAfterMarker - 1)) { break }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Finalize list
|
|
|
+ if (isOrdered) {
|
|
|
+ token = state.push('ordered_list_close', 'ol', -1);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ token = state.push('bullet_list_close', 'ul', -1);
|
|
|
+ }
|
|
|
+ token.markup = String.fromCharCode(markerCharCode);
|
|
|
+
|
|
|
+ listLines[1] = nextLine;
|
|
|
+ state.line = nextLine;
|
|
|
|
|
|
- };
|
|
|
+ state.parentType = oldParentType;
|
|
|
|
|
|
+ // mark paragraphs tight if needed
|
|
|
+ if (tight) {
|
|
|
+ markTightParagraphs(state, listTokIdx);
|
|
|
}
|
|
|
|
|
|
+ return true;
|
|
|
}
|