markdownlint/helpers/micromark-parse.cjs

191 lines
5.5 KiB
JavaScript
Raw Normal View History

// @ts-check
"use strict";
const micromark = require("markdownlint-micromark");
const { isHtmlFlowComment } = require("./micromark-helpers.cjs");
const { flatTokensSymbol, htmlFlowSymbol, newLineRe } = require("./shared.js");
/** @typedef {import("markdownlint-micromark").Event} Event */
/** @typedef {import("markdownlint-micromark").ParseOptions} ParseOptions */
/** @typedef {import("../lib/markdownlint.js").MicromarkToken} Token */
/**
* Parses a Markdown document and returns Micromark events.
*
* @param {string} markdown Markdown document.
* @param {ParseOptions} [micromarkOptions] Options for micromark.
* @param {boolean} [referencesDefined] Treat references as defined.
* @returns {Event[]} Micromark events.
*/
function getEvents(
markdown,
micromarkOptions = {},
referencesDefined = true
) {
// Customize options object to add useful extensions
micromarkOptions.extensions = micromarkOptions.extensions || [];
micromarkOptions.extensions.push(
micromark.directive(),
micromark.gfmAutolinkLiteral(),
micromark.gfmFootnote(),
micromark.gfmTable(),
micromark.math()
);
// Use micromark to parse document into Events
const encoding = undefined;
const eol = true;
const parseContext = micromark.parse(micromarkOptions);
if (referencesDefined) {
// Customize ParseContext to treat all references as defined
parseContext.defined.includes = (searchElement) => searchElement.length > 0;
}
const chunks = micromark.preprocess()(markdown, encoding, eol);
const events = micromark.postprocess(parseContext.document().write(chunks));
return events;
}
/**
* Parses a Markdown document and returns (frozen) tokens.
*
* @param {string} markdown Markdown document.
* @param {ParseOptions} micromarkOptions Options for micromark.
* @param {boolean} referencesDefined Treat references as defined.
* @param {number} lineDelta Offset to apply to start/end line.
* @param {Token} [ancestor] Parent of top-most tokens.
* @returns {Token[]} Micromark tokens (frozen).
*/
function parseWithOffset(
markdown,
micromarkOptions,
referencesDefined,
lineDelta,
ancestor
) {
// Use micromark to parse document into Events
const events = getEvents(
markdown, micromarkOptions, referencesDefined
);
// Create Token objects
const document = [];
let flatTokens = [];
/** @type {Token} */
const root = {
"type": "data",
"startLine": -1,
"startColumn": -1,
"endLine": -1,
"endColumn": -1,
"text": "ROOT",
"children": document,
"parent": null
};
const history = [ root ];
let current = root;
// eslint-disable-next-line jsdoc/valid-types
/** @type ParseOptions | null */
let reparseOptions = null;
let lines = null;
let skipHtmlFlowChildren = false;
for (const event of events) {
const [ kind, token, context ] = event;
const { type, start, end } = token;
const { "column": startColumn, "line": startLine } = start;
const { "column": endColumn, "line": endLine } = end;
const text = context.sliceSerialize(token);
if ((kind === "enter") && !skipHtmlFlowChildren) {
const previous = current;
history.push(previous);
current = {
type,
"startLine": startLine + lineDelta,
startColumn,
"endLine": endLine + lineDelta,
endColumn,
text,
"children": [],
"parent": ((previous === root) ? (ancestor || null) : previous)
};
if (ancestor) {
Object.defineProperty(current, htmlFlowSymbol, { "value": true });
}
previous.children.push(current);
flatTokens.push(current);
if ((current.type === "htmlFlow") && !isHtmlFlowComment(current)) {
skipHtmlFlowChildren = true;
if (!reparseOptions || !lines) {
reparseOptions = {
...micromarkOptions,
"extensions": [
{
"disable": {
"null": [ "codeIndented", "htmlFlow" ]
}
}
]
};
lines = markdown.split(newLineRe);
}
const reparseMarkdown = lines
.slice(current.startLine - 1, current.endLine)
.join("\n");
const tokens = parseWithOffset(
reparseMarkdown,
reparseOptions,
referencesDefined,
current.startLine - 1,
current
);
current.children = tokens;
// Avoid stack overflow of Array.push(...spread)
// eslint-disable-next-line unicorn/prefer-spread
flatTokens = flatTokens.concat(tokens[flatTokensSymbol]);
}
} else if (kind === "exit") {
if (type === "htmlFlow") {
skipHtmlFlowChildren = false;
}
if (!skipHtmlFlowChildren) {
Object.freeze(current.children);
Object.freeze(current);
// @ts-ignore
current = history.pop();
}
}
}
// Return document
Object.defineProperty(document, flatTokensSymbol, { "value": flatTokens });
Object.freeze(document);
return document;
}
/**
* Parses a Markdown document and returns (frozen) tokens.
*
* @param {string} markdown Markdown document.
* @param {ParseOptions} [micromarkOptions] Options for micromark.
* @param {boolean} [referencesDefined] Treat references as defined.
* @returns {Token[]} Micromark tokens (frozen).
*/
function parse(
markdown,
micromarkOptions = {},
referencesDefined = true
) {
return parseWithOffset(
markdown,
micromarkOptions,
referencesDefined,
0
);
}
module.exports = {
getEvents,
parse
};