UNPKG

@lbevilacqua/markdown-it-fancy-lists

Version:

Extension for markdown-it to support additional numbering types for ordered lists

386 lines (385 loc) 16.2 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.markdownItFancyListPlugin = void 0; var utils_1 = require("markdown-it/lib/common/utils"); var roman_numerals_1 = require("roman-numerals"); // Search `[-+*][\n ]`, returns next pos after marker on success // or -1 on fail. function parseUnorderedListMarker(state, startLine) { var pos = state.bMarks[startLine] + state.tShift[startLine]; var max = state.eMarks[startLine]; var marker = state.src.charCodeAt(pos); pos += 1; // Check bullet if (marker !== 0x2A /* * */ && marker !== 0x2D /* - */ && marker !== 0x2B /* + */) { return null; } if (pos < max) { var ch = state.src.charCodeAt(pos); if (!utils_1.isSpace(ch)) { // " -test " - is not a list item return null; } } return { type: state.src.charAt(pos - 1), posAfterMarker: pos, }; } // Search `^(\d{1,9}|[a-z]|[A-Z]|[ivxlcdm]+|[IVXLCDM]+|#)([\u00BA\u00B0\u02DA\u1D52]?)([.)])`, returns next pos after marker on success // or -1 on fail. function parseOrderedListMarker(state, startLine) { var start = state.bMarks[startLine] + state.tShift[startLine]; var max = state.eMarks[startLine]; // List marker should have at least 2 chars (digit + dot) if (start + 1 >= max) { return null; } var stringContainingNumberAndMarker = state.src.substring(start, Math.min(max, start + 10)); var match = /^(\d{1,9}|[a-z]|[A-Z]|[ivxlcdm]+|[IVXLCDM]+|#)([\u00BA\u00B0\u02DA\u1D52]?)([.)])/.exec(stringContainingNumberAndMarker); if (match === null) { return null; } var markerPos = start + match[1].length; var markerChar = state.src.charAt(markerPos); var finalPos = start + match[0].length; var finalChar = state.src.charCodeAt(finalPos); // requires once space after marker if (utils_1.isSpace(finalChar) === false) { return null; } // requires two spaces after a capital letter and a period if (isCharCodeUppercaseAlpha(match[1].charCodeAt(0)) && match[1].length === 1 && markerChar === ".") { finalPos += 1; // consume another space var finalChar_1 = state.src.charCodeAt(finalPos); if (utils_1.isSpace(finalChar_1) === false) { return null; } } return { bulletChar: match[1], hasOrdinalIndicator: match[2] !== "", delimiter: match[3], posAfterMarker: finalPos, }; } function markTightParagraphs(state, idx) { var i, l; var level = state.level + 2; for (i = idx + 2, l = state.tokens.length - 2; i < l; i += 1) { 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; } } } function isCharCodeDigit(charChode) { return charChode >= 0x30 /* 0 */ && charChode <= 0x39 /* 9 */; } function isCharCodeLowercaseAlpha(charChode) { return charChode >= 0x61 /* a */ && charChode <= 0x7A /* z */; } function isCharCodeUppercaseAlpha(charChode) { return charChode >= 0x41 /* A */ && charChode <= 0x5A /* Z */; } var analyzeRoman = function (romanNumeralString) { var parsedRomanNumber = 1; var isValidRoman = true; try { parsedRomanNumber = roman_numerals_1.toArabic(romanNumeralString); } catch (e) { isValidRoman = false; } return { parsedRomanNumber: parsedRomanNumber, isValidRoman: isValidRoman, }; }; function analyseMarker(state, startLine, endLine, previousMarker) { var orderedListMarker = parseOrderedListMarker(state, startLine); if (orderedListMarker !== null) { var bulletChar = orderedListMarker.bulletChar; var charCode = orderedListMarker.bulletChar.charCodeAt(0); if (isCharCodeDigit(charCode)) { return __assign({ isOrdered: true, isRoman: false, isAlpha: false, type: "0", start: Number.parseInt(bulletChar) }, orderedListMarker); } else if (isCharCodeLowercaseAlpha(charCode)) { var isValidAlpha = bulletChar.length === 1; var preferRoman = ((previousMarker !== null && previousMarker.isRoman === true) || ((previousMarker === null || previousMarker.isAlpha === false) && bulletChar === "i")); var _a = analyzeRoman(bulletChar), parsedRomanNumber = _a.parsedRomanNumber, isValidRoman = _a.isValidRoman; if (isValidRoman === true && (isValidAlpha === false || preferRoman === true)) { return __assign({ isOrdered: true, isRoman: true, isAlpha: false, type: "i", start: parsedRomanNumber }, orderedListMarker); } else if (isValidAlpha === true) { return __assign({ isOrdered: true, isRoman: false, isAlpha: true, type: "a", start: bulletChar.charCodeAt(0) - "a".charCodeAt(0) + 1 }, orderedListMarker); } return null; } else if (isCharCodeUppercaseAlpha(charCode)) { var isValidAlpha = bulletChar.length === 1; var preferRoman = ((previousMarker !== null && previousMarker.isRoman === true) || ((previousMarker === null || previousMarker.isAlpha === false) && bulletChar === "I")); var _b = analyzeRoman(bulletChar), parsedRomanNumber = _b.parsedRomanNumber, isValidRoman = _b.isValidRoman; if (isValidRoman === true && (isValidAlpha === false || preferRoman === true)) { return __assign({ isOrdered: true, isRoman: true, isAlpha: false, type: "I", start: parsedRomanNumber }, orderedListMarker); } else if (isValidAlpha === true) { return __assign({ isOrdered: true, isRoman: false, isAlpha: true, type: "A", start: bulletChar.charCodeAt(0) - "A".charCodeAt(0) + 1 }, orderedListMarker); } return null; } else { return __assign({ isOrdered: true, isRoman: false, isAlpha: false, type: "#", start: 1 }, orderedListMarker); } } var unorderedListMarker = parseUnorderedListMarker(state, startLine); if (unorderedListMarker !== null) { var start = state.bMarks[startLine] + state.tShift[startLine]; return __assign({ isOrdered: false, isRoman: false, isAlpha: false, bulletChar: "", hasOrdinalIndicator: false, delimiter: "", start: 1 }, unorderedListMarker); } else { return null; } } function areMarkersCompatible(previousMarker, currentMarker) { return previousMarker.isOrdered === currentMarker.isOrdered && (previousMarker.type === currentMarker.type || currentMarker.type === "#") && previousMarker.delimiter === currentMarker.delimiter && previousMarker.hasOrdinalIndicator === currentMarker.hasOrdinalIndicator; } var createFancyList = function (options) { return function (state, startLine, endLine, silent) { // 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; } var isTerminatingParagraph = 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; } } var marker = analyseMarker(state, startLine, endLine, null); if (marker === null) { return false; } if (marker.hasOrdinalIndicator === true && options.allowOrdinal !== true) { return false; } var posAfterMarker = marker.bulletChar.length + 2; // do not allow subsequent numbers to interrupt paragraphs if (isTerminatingParagraph && marker.start !== 1) { 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. var markerCharCode = state.src.charCodeAt(marker.posAfterMarker - 1); // For validation mode we can terminate immediately if (silent) { return true; } // Start list var listTokIdx = state.tokens.length; var token; if (marker.isOrdered === true) { token = state.push("ordered_list_open", "ol", 1); var attrs = []; if (marker.type !== "0" && marker.type !== "#") { attrs.push(["type", marker.type]); } if (marker.start !== 1) { attrs.push(["start", marker.start.toString(10)]); } if (marker.hasOrdinalIndicator === true) { attrs.push(["class", "ordinal"]); } token.attrs = attrs; } else { token = state.push("bullet_list_open", "ul", 1); } var listLines = [startLine, 0]; token.map = listLines; token.markup = String.fromCharCode(markerCharCode); // // Iterate list items // var nextLine = startLine; var prevEmptyEnd = false; var terminatorRules = state.md.block.ruler.getRules("list"); var oldParentType = state.parentType; state.parentType = "list"; var tight = true; while (nextLine < endLine) { var nextMarker = analyseMarker(state, nextLine, endLine, marker); if (nextMarker === null || areMarkersCompatible(marker, nextMarker) === false) { break; } var pos = nextMarker.posAfterMarker; var max = state.eMarks[nextLine]; var initial = state.sCount[nextLine] + pos - (state.bMarks[startLine] + state.tShift[startLine]); var offset = initial; while (pos < max) { var ch = state.src.charCodeAt(pos); if (ch === 0x09) { offset += 4 - (offset + state.bsCount[nextLine]) % 4; } else if (ch === 0x20) { offset += 1; } else { break; } pos += 1; } var contentStart = pos; var indentAfterMarker = void 0; 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 var indent = initial + indentAfterMarker; // Run subparser & write tokens token = state.push("list_item_open", "li", 1); token.markup = String.fromCharCode(markerCharCode); var itemLines = [startLine, 0]; token.map = itemLines; // change current state, then restore it after parser subcall var oldTight = state.tight; var oldTShift = state.tShift[startLine]; var oldSCount = state.sCount[startLine]; // - example list // ^ listIndent position will be here // ^ blkIndent position will be here // var 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); } // 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 var terminate = false; for (var i = 0, l = terminatorRules.length; i < l; i += 1) { if (terminatorRules[i](state, nextLine, endLine, true)) { terminate = true; break; } } if (terminate) { break; } marker = nextMarker; } // Finalize list if (marker.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; }; }; var markdownItFancyListPlugin = function (markdownIt, options) { markdownIt.block.ruler.at("list", createFancyList(options !== null && options !== void 0 ? options : {}), { alt: ["paragraph", "reference", "blockquote"] }); }; exports.markdownItFancyListPlugin = markdownItFancyListPlugin;