// Lists 'use strict'; var isSpace = require('../common/utils').isSpace; // Search `[-+*][\n ]`, returns next pos after marker on success // or -1 on fail. function skipBulletListMarker(state, startLine) { var marker, pos, max, ch; pos = state.bMarks[startLine] + state.tShift[startLine]; max = state.eMarks[startLine]; 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) { var ch, start = state.bMarks[startLine] + state.tShift[startLine], pos = start, 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++); if (ch >= 0x30/* 0 */ && ch <= 0x39/* 9 */) { // 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; } function markTightParagraphs(state, idx) { var i, l, 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; } } } module.exports = function list(state, startLine, endLine, silent) { var ch, contentStart, i, indent, indentAfterMarker, initial, isOrdered, itemLines, l, listLines, listTokIdx, markerCharCode, markerValue, max, nextLine, offset, oldListIndent, oldParentType, oldSCount, oldTShift, oldTight, pos, posAfterMarker, prevEmptyEnd, start, terminate, terminatorRules, token, isTerminatingParagraph = false, 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 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; } 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. markerCharCode = state.src.charCodeAt(posAfterMarker - 1); // For validation mode we can terminate immediately if (silent) { return true; } // Start list 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); } token.map = listLines = [ startLine, 0 ]; token.markup = String.fromCharCode(markerCharCode); // // Iterate list items // nextLine = startLine; prevEmptyEnd = false; terminatorRules = state.md.block.ruler.getRules('list'); oldParentType = state.parentType; state.parentType = 'list'; while (nextLine < endLine) { pos = posAfterMarker; max = state.eMarks[nextLine]; initial = offset = state.sCount[nextLine] + posAfterMarker - (state.bMarks[startLine] + state.tShift[startLine]); while (pos < max) { ch = state.src.charCodeAt(pos); if (ch === 0x09) { 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); 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); 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; } } 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; };