UNPKG

markdownlint-rule-helpers

Version:

A collection of markdownlint helper functions for custom rules

299 lines (278 loc) 8 kB
// @ts-check "use strict"; const { flatTokensSymbol, htmlFlowSymbol } = require("./shared.cjs"); // eslint-disable-next-line jsdoc/valid-types /** @typedef {import("micromark-util-types", { with: { "resolution-mode": "import" } }).TokenType} TokenType */ // @ts-expect-error https://github.com/microsoft/TypeScript/issues/52529 /** @typedef {import("markdownlint").MicromarkToken} Token */ /** * Determines if a Micromark token is within an htmlFlow type. * * @param {Token} token Micromark token. * @returns {boolean} True iff the token is within an htmlFlow type. */ function inHtmlFlow(token) { return Boolean(token[htmlFlowSymbol]); } /** * Returns whether a token is an htmlFlow type containing an HTML comment. * * @param {Token} token Micromark token. * @returns {boolean} True iff token is htmlFlow containing a comment. */ function isHtmlFlowComment(token) { const { text, type } = token; if ( (type === "htmlFlow") && text.startsWith("<!--") && text.endsWith("-->") ) { const comment = text.slice(4, -3); return ( !comment.startsWith(">") && !comment.startsWith("->") && !comment.endsWith("-") // The following condition from the CommonMark specification is commented // to avoid parsing HTML comments that include "--" because that is NOT a // condition of the HTML specification. // https://spec.commonmark.org/0.30/#raw-html // https://html.spec.whatwg.org/multipage/syntax.html#comments // && !comment.includes("--") ); } return false; } /** * Adds a range of numbers to a set. * * @param {Set<number>} set Set of numbers. * @param {number} start Starting number. * @param {number} end Ending number. * @returns {void} */ function addRangeToSet(set, start, end) { for (let i = start; i <= end; i++) { set.add(i); } } /** * @callback AllowedPredicate * @param {Token} token Micromark token. * @returns {boolean} True iff allowed. */ /** * @callback TransformPredicate * @param {Token} token Micromark token. * @returns {Token[]} Child tokens. */ /** * Filter a list of Micromark tokens by predicate. * * @param {Token[]} tokens Micromark tokens. * @param {AllowedPredicate} [allowed] Allowed token predicate. * @param {TransformPredicate} [transformChildren] Transform predicate. * @returns {Token[]} Filtered tokens. */ function filterByPredicate(tokens, allowed, transformChildren) { allowed = allowed || (() => true); const result = []; const queue = [ { "array": tokens, "index": 0 } ]; while (queue.length > 0) { const current = queue[queue.length - 1]; const { array, index } = current; if (index < array.length) { const token = array[current.index++]; if (allowed(token)) { result.push(token); } const { children } = token; if (children.length > 0) { const transformed = transformChildren ? transformChildren(token) : children; queue.push( { "array": transformed, "index": 0 } ); } } else { queue.pop(); } } return result; } /** * Filter a list of Micromark tokens by type. * * @param {Token[]} tokens Micromark tokens. * @param {TokenType[]} types Types to allow. * @param {boolean} [htmlFlow] Whether to include htmlFlow content. * @returns {Token[]} Filtered tokens. */ function filterByTypes(tokens, types, htmlFlow) { const predicate = (token) => types.includes(token.type) && (htmlFlow || !inHtmlFlow(token)); const flatTokens = tokens[flatTokensSymbol]; if (flatTokens) { return flatTokens.filter(predicate); } return filterByPredicate(tokens, predicate); } /** * Gets the blockquote prefix text (if any) for the specified line number. * * @param {Token[]} tokens Micromark tokens. * @param {number} lineNumber Line number to examine. * @param {number} [count] Number of times to repeat. * @returns {string} Blockquote prefix text. */ function getBlockQuotePrefixText(tokens, lineNumber, count = 1) { return filterByTypes(tokens, [ "blockQuotePrefix", "linePrefix" ]) .filter((prefix) => prefix.startLine === lineNumber) .map((prefix) => prefix.text) .join("") .trimEnd() // eslint-disable-next-line unicorn/prefer-spread .concat("\n") .repeat(count); }; /** * Gets a list of nested Micromark token descendants by type path. * * @param {Token|Token[]} parent Micromark token parent or parents. * @param {(TokenType|TokenType[])[]} typePath Micromark token type path. * @returns {Token[]} Micromark token descendants. */ function getDescendantsByType(parent, typePath) { let tokens = Array.isArray(parent) ? parent : [ parent ]; for (const type of typePath) { const predicate = (token) => Array.isArray(type) ? type.includes(token.type) : (type === token.type); tokens = tokens.flatMap((t) => t.children.filter(predicate)); } return tokens; } /** * Gets the heading level of a Micromark heading tokan. * * @param {Token} heading Micromark heading token. * @returns {number} Heading level. */ function getHeadingLevel(heading) { let level = 1; const headingSequence = heading.children.find( (child) => [ "atxHeadingSequence", "setextHeadingLine" ].includes(child.type) ); // @ts-ignore const { text } = headingSequence; if (text[0] === "#") { level = Math.min(text.length, 6); } else if (text[0] === "-") { level = 2; } return level; } /** * Gets the heading style of a Micromark heading tokan. * * @param {Token} heading Micromark heading token. * @returns {"atx" | "atx_closed" | "setext"} Heading style. */ function getHeadingStyle(heading) { if (heading.type === "setextHeading") { return "setext"; } const atxHeadingSequenceLength = heading.children.filter( (child) => child.type === "atxHeadingSequence" ).length; if (atxHeadingSequenceLength === 1) { return "atx"; } return "atx_closed"; } /** * Gets the heading text of a Micromark heading token. * * @param {Token} heading Micromark heading token. * @returns {string} Heading text. */ function getHeadingText(heading) { const headingTexts = getDescendantsByType(heading, [ [ "atxHeadingText", "setextHeadingText" ] ]); return headingTexts[0]?.text.replace(/[\r\n]+/g, " ") || ""; } /** * HTML tag information. * * @typedef {Object} HtmlTagInfo * @property {boolean} close True iff close tag. * @property {string} name Tag name. */ /** * Gets information about the tag in an HTML token. * * @param {Token} token Micromark token. * @returns {HtmlTagInfo | null} HTML tag information. */ function getHtmlTagInfo(token) { const htmlTagNameRe = /^<([^!>][^/\s>]*)/; if (token.type === "htmlText") { const match = htmlTagNameRe.exec(token.text); if (match) { const name = match[1]; const close = name.startsWith("/"); return { close, "name": close ? name.slice(1) : name }; } } return null; } /** * Gets the nearest parent of the specified type for a Micromark token. * * @param {Token} token Micromark token. * @param {TokenType[]} types Types to allow. * @returns {Token | null} Parent token. */ function getParentOfType(token, types) { /** @type {Token | null} */ let current = token; while ((current = current.parent) && !types.includes(current.type)) { // Empty } return current; } /** * Set containing token types that do not contain content. * * @type {Set<TokenType>} */ const nonContentTokens = new Set([ "blockQuoteMarker", "blockQuotePrefix", "blockQuotePrefixWhitespace", "lineEnding", "lineEndingBlank", "linePrefix", "listItemIndent" ]); module.exports = { addRangeToSet, filterByPredicate, filterByTypes, getBlockQuotePrefixText, getDescendantsByType, getHeadingLevel, getHeadingStyle, getHeadingText, getHtmlTagInfo, getParentOfType, inHtmlFlow, isHtmlFlowComment, nonContentTokens };