2024-09-28 15:57:25 -07:00
|
|
|
// @ts-check
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
2024-09-28 16:26:38 -07:00
|
|
|
const { flatTokensSymbol, htmlFlowSymbol } = require("./shared.js");
|
2024-09-28 15:57:25 -07:00
|
|
|
|
|
|
|
/** @typedef {import("markdownlint-micromark").TokenType} TokenType */
|
|
|
|
/** @typedef {import("../lib/markdownlint.js").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) {
|
2024-10-23 21:44:36 -07:00
|
|
|
const predicate = (token) => types.includes(token.type) && (htmlFlow || !inHtmlFlow(token));
|
2024-09-28 15:57:25 -07:00
|
|
|
const flatTokens = tokens[flatTokensSymbol];
|
|
|
|
if (flatTokens) {
|
|
|
|
return flatTokens.filter(predicate);
|
|
|
|
}
|
|
|
|
return filterByPredicate(tokens, predicate);
|
|
|
|
}
|
|
|
|
|
2024-10-04 22:41:34 -07:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
};
|
|
|
|
|
2024-09-28 15:57:25 -07:00
|
|
|
/**
|
|
|
|
* 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) {
|
2024-10-23 22:05:44 -07:00
|
|
|
const predicate = (token) => Array.isArray(type) ? type.includes(token.type) : (type === token.type);
|
|
|
|
tokens = tokens.flatMap((t) => t.children.filter(predicate));
|
2024-09-28 15:57:25 -07:00
|
|
|
}
|
|
|
|
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;
|
2024-10-09 22:42:36 -07:00
|
|
|
const headingSequence = heading.children.find(
|
|
|
|
(child) => [ "atxHeadingSequence", "setextHeadingLine" ].includes(child.type)
|
|
|
|
);
|
|
|
|
// @ts-ignore
|
|
|
|
const { text } = headingSequence;
|
2024-09-28 15:57:25 -07:00
|
|
|
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";
|
|
|
|
}
|
2024-10-09 22:42:36 -07:00
|
|
|
const atxHeadingSequenceLength = heading.children.filter(
|
|
|
|
(child) => child.type === "atxHeadingSequence"
|
2024-09-28 15:57:25 -07:00
|
|
|
).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,
|
2024-10-04 22:41:34 -07:00
|
|
|
getBlockQuotePrefixText,
|
2024-09-28 15:57:25 -07:00
|
|
|
getDescendantsByType,
|
|
|
|
getHeadingLevel,
|
|
|
|
getHeadingStyle,
|
|
|
|
getHeadingText,
|
|
|
|
getHtmlTagInfo,
|
|
|
|
getParentOfType,
|
|
|
|
inHtmlFlow,
|
|
|
|
isHtmlFlowComment,
|
|
|
|
nonContentTokens
|
|
|
|
};
|