@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
JavaScript
"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;