UNPKG

mdclint

Version:

Markdown Linting

1,560 lines (1,465 loc) 525 kB
import { loadConfig } from 'c12'; import { glob } from 'glob'; import { access, accessSync, readFile, readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { EOL, homedir } from 'node:os'; import { dirname, resolve } from 'node:path'; import { micromarkExtension } from 'remark-mdc'; var helpers = {}; var shared = {}; var hasRequiredShared; function requireShared () { if (hasRequiredShared) return shared; hasRequiredShared = 1; // Symbol for identifing the flat tokens array from micromark parse shared.flatTokensSymbol = Symbol("flat-tokens"); // Symbol for identifying the htmlFlow token from micromark parse shared.htmlFlowSymbol = Symbol("html-flow"); // Regular expression for matching common newline characters // See NEWLINES_RE in markdown-it/lib/rules_core/normalize.js shared.newLineRe = /\r\n?|\n/g; // Regular expression for matching next lines shared.nextLinesRe = /[\r\n][\s\S]*$/; return shared; } var micromarkHelpers; var hasRequiredMicromarkHelpers; function requireMicromarkHelpers () { if (hasRequiredMicromarkHelpers) return micromarkHelpers; hasRequiredMicromarkHelpers = 1; const { flatTokensSymbol, htmlFlowSymbol } = requireShared(); // 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("../lib/exports.mjs").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" ]); micromarkHelpers = { addRangeToSet, filterByPredicate, filterByTypes, getBlockQuotePrefixText, getDescendantsByType, getHeadingLevel, getHeadingStyle, getHeadingText, getHtmlTagInfo, getParentOfType, inHtmlFlow, isHtmlFlowComment, nonContentTokens }; return micromarkHelpers; } var hasRequiredHelpers; function requireHelpers () { if (hasRequiredHelpers) return helpers; hasRequiredHelpers = 1; const micromark = requireMicromarkHelpers(); const { newLineRe, nextLinesRe } = requireShared(); helpers.newLineRe = newLineRe; helpers.nextLinesRe = nextLinesRe; // @ts-expect-error https://github.com/microsoft/TypeScript/issues/52529 /** @typedef {import("../lib/exports.mjs").RuleOnError} RuleOnError */ // @ts-expect-error https://github.com/microsoft/TypeScript/issues/52529 /** @typedef {import("../lib/exports.mjs").RuleOnErrorFixInfo} RuleOnErrorFixInfo */ // @ts-expect-error https://github.com/microsoft/TypeScript/issues/52529 /** @typedef {import("../lib/exports.mjs").MicromarkToken} MicromarkToken */ // eslint-disable-next-line jsdoc/valid-types /** @typedef {import("micromark-extension-gfm-footnote", { with: { "resolution-mode": "import" } })} */ // eslint-disable-next-line jsdoc/valid-types /** @typedef {import("../lib/micromark-types.d.mts", { with: { "resolution-mode": "import" } })} */ // Regular expression for matching common front matter (YAML and TOML) // @ts-ignore helpers.frontMatterRe = /((^---[^\S\r\n\u2028\u2029]*$[\s\S]+?^---\s*)|(^\+\+\+[^\S\r\n\u2028\u2029]*$[\s\S]+?^(\+\+\+|\.\.\.)\s*)|(^\{[^\S\r\n\u2028\u2029]*$[\s\S]+?^\}\s*))(\r\n|\r|\n|$)/m; // Regular expression for matching the start of inline disable/enable comments const inlineCommentStartRe = /(<!--\s*markdownlint-(disable|enable|capture|restore|disable-file|enable-file|disable-line|disable-next-line|configure-file))(?:\s|-->)/gi; helpers.inlineCommentStartRe = inlineCommentStartRe; // Regular expression for identifying an HTML entity at the end of a line helpers.endOfLineHtmlEntityRe = /&(?:#\d+|#[xX][\da-fA-F]+|[a-zA-Z]{2,31}|blk\d{2}|emsp1[34]|frac\d{2}|sup\d|there4);$/; // Regular expression for identifying a GitHub emoji code at the end of a line helpers.endOfLineGemojiCodeRe = /:(?:[abmovx]|[-+]1|100|1234|(?:1st|2nd|3rd)_place_medal|8ball|clock\d{1,4}|e-mail|non-potable_water|o2|t-rex|u5272|u5408|u55b6|u6307|u6708|u6709|u6e80|u7121|u7533|u7981|u7a7a|[a-z]{2,15}2?|[a-z]{1,14}(?:_[a-z\d]{1,16})+):$/; // All punctuation characters (normal and full-width) const allPunctuation = ".,;:!?。,;:!?"; helpers.allPunctuation = allPunctuation; // All punctuation characters without question mark (normal and full-width) helpers.allPunctuationNoQuestion = allPunctuation.replace(/[??]/gu, ""); /** * Returns true iff the input is a Number. * * @param {Object} obj Object of unknown type. * @returns {boolean} True iff obj is a Number. */ function isNumber(obj) { return typeof obj === "number"; } helpers.isNumber = isNumber; /** * Returns true iff the input is a String. * * @param {Object} obj Object of unknown type. * @returns {boolean} True iff obj is a String. */ function isString(obj) { return typeof obj === "string"; } helpers.isString = isString; /** * Returns true iff the input String is empty. * * @param {string} str String of unknown length. * @returns {boolean} True iff the input String is empty. */ function isEmptyString(str) { return str.length === 0; } helpers.isEmptyString = isEmptyString; /** * Returns true iff the input is an Object. * * @param {Object} obj Object of unknown type. * @returns {boolean} True iff obj is an Object. */ function isObject(obj) { return !!obj && (typeof obj === "object") && !Array.isArray(obj); } helpers.isObject = isObject; /** * Returns true iff the input is a URL. * * @param {Object} obj Object of unknown type. * @returns {boolean} True iff obj is a URL. */ function isUrl(obj) { return !!obj && (Object.getPrototypeOf(obj) === URL.prototype); } helpers.isUrl = isUrl; /** * Clones the input if it is an Array. * * @param {Object} arr Object of unknown type. * @returns {Object} Clone of obj iff obj is an Array. */ function cloneIfArray(arr) { return Array.isArray(arr) ? [ ...arr ] : arr; } helpers.cloneIfArray = cloneIfArray; /** * Clones the input if it is a URL. * * @param {Object} url Object of unknown type. * @returns {Object} Clone of obj iff obj is a URL. */ function cloneIfUrl(url) { return isUrl(url) ? new URL(url) : url; } helpers.cloneIfUrl = cloneIfUrl; /** * Gets a Regular Expression for matching the specified HTML attribute. * * @param {string} name HTML attribute name. * @returns {RegExp} Regular Expression for matching. */ helpers.getHtmlAttributeRe = function getHtmlAttributeRe(name) { return new RegExp(`\\s${name}\\s*=\\s*['"]?([^'"\\s>]*)`, "iu"); }; /** * Returns true iff the input line is blank (contains nothing, whitespace, or * comments (unclosed start/end comments allowed)). * * @param {string} line Input line. * @returns {boolean} True iff line is blank. */ function isBlankLine(line) { const startComment = "<!--"; const endComment = "-->"; const removeComments = (s) => { while (true) { const start = s.indexOf(startComment); const end = s.indexOf(endComment); if ((end !== -1) && ((start === -1) || (end < start))) { // Unmatched end comment is first s = s.slice(end + endComment.length); } else if ((start !== -1) && (end !== -1)) { // Start comment is before end comment s = s.slice(0, start) + s.slice(end + endComment.length); } else if ((start !== -1) && (end === -1)) { // Unmatched start comment is last s = s.slice(0, start); } else { // No more comments to remove return s; } } }; return ( !line || !line.trim() || !removeComments(line).replace(/>/g, "").trim() ); } helpers.isBlankLine = isBlankLine; // Replaces the content of properly-formatted CommonMark comments with "." // This preserves the line/column information for the rest of the document // https://spec.commonmark.org/0.29/#html-blocks // https://spec.commonmark.org/0.29/#html-comment const htmlCommentBegin = "<!--"; const htmlCommentEnd = "-->"; const safeCommentCharacter = "."; const startsWithPipeRe = /^ *\|/; const notCrLfRe = /[^\r\n]/g; const notSpaceCrLfRe = /[^ \r\n]/g; const trailingSpaceRe = / +[\r\n]/g; const replaceTrailingSpace = (s) => s.replace(notCrLfRe, safeCommentCharacter); helpers.clearHtmlCommentText = function clearHtmlCommentText(text) { let i = 0; while ((i = text.indexOf(htmlCommentBegin, i)) !== -1) { const j = text.indexOf(htmlCommentEnd, i + 2); if (j === -1) { // Un-terminated comments are treated as text break; } // If the comment has content... if (j > i + htmlCommentBegin.length) { const content = text.slice(i + htmlCommentBegin.length, j); const lastLf = text.lastIndexOf("\n", i) + 1; const preText = text.slice(lastLf, i); const isBlock = preText.trim().length === 0; const couldBeTable = startsWithPipeRe.test(preText); const spansTableCells = couldBeTable && content.includes("\n"); const isValid = isBlock || !( spansTableCells || content.startsWith(">") || content.startsWith("->") || content.endsWith("-") || content.includes("--") ); // If a valid block/inline comment... if (isValid) { const clearedContent = content .replace(notSpaceCrLfRe, safeCommentCharacter) .replace(trailingSpaceRe, replaceTrailingSpace); text = text.slice(0, i + htmlCommentBegin.length) + clearedContent + text.slice(j); } } i = j + htmlCommentEnd.length; } return text; }; // Escapes a string for use in a RegExp helpers.escapeForRegExp = function escapeForRegExp(str) { return str.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); }; /** * Adds ellipsis to the left/right/middle of the specified text. * * @param {string} text Text to ellipsify. * @param {boolean} [start] True iff the start of the text is important. * @param {boolean} [end] True iff the end of the text is important. * @returns {string} Ellipsified text. */ function ellipsify(text, start, end) { if (text.length <= 30) ; else if (start && end) { text = text.slice(0, 15) + "..." + text.slice(-15); } else if (end) { text = "..." + text.slice(-30); } else { text = text.slice(0, 30) + "..."; } return text; } helpers.ellipsify = ellipsify; /** * Adds a generic error object via the onError callback. * * @param {RuleOnError} onError RuleOnError instance. * @param {number} lineNumber Line number. * @param {string} [detail] Error details. * @param {string} [context] Error context. * @param {number[]} [range] Column and length of error. * @param {RuleOnErrorFixInfo} [fixInfo] RuleOnErrorFixInfo instance. * @returns {void} */ function addError(onError, lineNumber, detail, context, range, fixInfo) { onError({ lineNumber, detail, context, range, fixInfo }); } helpers.addError = addError; /** * Adds an error object with details conditionally via the onError callback. * * @param {RuleOnError} onError RuleOnError instance. * @param {number} lineNumber Line number. * @param {Object} expected Expected value. * @param {Object} actual Actual value. * @param {string} [detail] Error details. * @param {string} [context] Error context. * @param {number[]} [range] Column and length of error. * @param {RuleOnErrorFixInfo} [fixInfo] RuleOnErrorFixInfo instance. * @returns {void} */ function addErrorDetailIf( onError, lineNumber, expected, actual, detail, context, range, fixInfo) { if (expected !== actual) { addError( onError, lineNumber, "Expected: " + expected + "; Actual: " + actual + (detail ? "; " + detail : ""), context, range, fixInfo); } } helpers.addErrorDetailIf = addErrorDetailIf; /** * Adds an error object with context via the onError callback. * * @param {RuleOnError} onError RuleOnError instance. * @param {number} lineNumber Line number. * @param {string} context Error context. * @param {boolean} [start] True iff the start of the text is important. * @param {boolean} [end] True iff the end of the text is important. * @param {number[]} [range] Column and length of error. * @param {RuleOnErrorFixInfo} [fixInfo] RuleOnErrorFixInfo instance. * @returns {void} */ function addErrorContext( onError, lineNumber, context, start, end, range, fixInfo) { context = ellipsify(context, start, end); addError(onError, lineNumber, undefined, context, range, fixInfo); } helpers.addErrorContext = addErrorContext; /** * Defines a range within a file (start line/column to end line/column, subset of MicromarkToken). * * @typedef {Object} FileRange * @property {number} startLine Start line (1-based). * @property {number} startColumn Start column (1-based). * @property {number} endLine End line (1-based). * @property {number} endColumn End column (1-based). */ /** * Returns whether line/column A is less than or equal to line/column B. * * @param {number} lineA Line A. * @param {number} columnA Column A. * @param {number} lineB Line B. * @param {number} columnB Column B. * @returns {boolean} True iff A is less than or equal to B. */ const positionLessThanOrEqual = (lineA, columnA, lineB, columnB) => ( (lineA < lineB) || ((lineA === lineB) && (columnA <= columnB)) ); /** * Returns whether two ranges (or MicromarkTokens) overlap anywhere. * * @param {FileRange|MicromarkToken} rangeA Range A. * @param {FileRange|MicromarkToken} rangeB Range B. * @returns {boolean} True iff the two ranges overlap. */ helpers.hasOverlap = function hasOverlap(rangeA, rangeB) { const lte = positionLessThanOrEqual(rangeA.startLine, rangeA.startColumn, rangeB.startLine, rangeB.startColumn); const first = lte ? rangeA : rangeB; const second = lte ? rangeB : rangeA; return positionLessThanOrEqual(second.startLine, second.startColumn, first.endLine, first.endColumn); }; // Determines if the front matter includes a title helpers.frontMatterHasTitle = function frontMatterHasTitle(frontMatterLines, frontMatterTitlePattern) { const ignoreFrontMatter = (frontMatterTitlePattern !== undefined) && !frontMatterTitlePattern; const frontMatterTitleRe = new RegExp( String(frontMatterTitlePattern || "^\\s*\"?title\"?\\s*[:=]"), "i" ); return !ignoreFrontMatter && frontMatterLines.some((line) => frontMatterTitleRe.test(line)); }; /** * Returns an object with information about reference links and images. * * @param {MicromarkToken[]} tokens Micromark tokens. * @returns {Object} Reference link/image data. */ function getReferenceLinkImageData(tokens) { const normalizeReference = (s) => s.toLowerCase().trim().replace(/\s+/g, " "); const references = new Map(); const shortcuts = new Map(); const addReferenceToDictionary = (token, label, isShortcut) => { const referenceDatum = [ token.startLine - 1, token.startColumn - 1, token.text.length ]; const reference = normalizeReference(label); const dictionary = isShortcut ? shortcuts : references; const referenceData = dictionary.get(reference) || []; referenceData.push(referenceDatum); dictionary.set(reference, referenceData); }; const definitions = new Map(); const definitionLineIndices = []; const duplicateDefinitions = []; const filteredTokens = micromark.filterByTypes( tokens, [ // definitionLineIndices "definition", "gfmFootnoteDefinition", // definitions and definitionLineIndices "definitionLabelString", "gfmFootnoteDefinitionLabelString", // references and shortcuts "gfmFootnoteCall", "image", "link", // undefined link labels "undefinedReferenceCollapsed", "undefinedReferenceFull", "undefinedReferenceShortcut" ] ); for (const token of filteredTokens) { let labelPrefix = ""; // eslint-disable-next-line default-case switch (token.type) { case "definition": case "gfmFootnoteDefinition": // definitionLineIndices for (let i = token.startLine; i <= token.endLine; i++) { definitionLineIndices.push(i - 1); } break; case "gfmFootnoteDefinitionLabelString": labelPrefix = "^"; case "definitionLabelString": // eslint-disable-line no-fallthrough { // definitions and definitionLineIndices const reference = normalizeReference(`${labelPrefix}${token.text}`); if (definitions.has(reference)) { duplicateDefinitions.push([ reference, token.startLine - 1 ]); } else { const parent = micromark.getParentOfType(token, [ "definition" ]); const destinationString = parent && micromark.getDescendantsByType(parent, [ "definitionDestination", "definitionDestinationRaw", "definitionDestinationString" ])[0]?.text; definitions.set( reference, [ token.startLine - 1, destinationString ] ); } } break; case "gfmFootnoteCall": case "image": case "link": { // Identify if shortcut or full/collapsed let isShortcut = (token.children.length === 1); const isFullOrCollapsed = (token.children.length === 2) && !token.children.some((t) => t.type === "resource"); const [ labelText ] = micromark.getDescendantsByType(token, [ "label", "labelText" ]); const [ referenceString ] = micromark.getDescendantsByType(token, [ "reference", "referenceString" ]); let label = labelText?.text; // Identify if footnote if (!isShortcut && !isFullOrCollapsed) { const [ footnoteCallMarker, footnoteCallString ] = token.children.filter( (t) => [ "gfmFootnoteCallMarker", "gfmFootnoteCallString" ].includes(t.type) ); if (footnoteCallMarker && footnoteCallString) { label = `${footnoteCallMarker.text}${footnoteCallString.text}`; isShortcut = true; } } // Track link (handle shortcuts separately due to ambiguity in "text [text] text") if (isShortcut || isFullOrCollapsed) { addReferenceToDictionary(token, referenceString?.text || label, isShortcut); } } break; case "undefinedReferenceCollapsed": case "undefinedReferenceFull": case "undefinedReferenceShortcut": { const undefinedReference = micromark.getDescendantsByType(token, [ "undefinedReference" ])[0]; const label = undefinedReference.children.map((t) => t.text).join(""); const isShortcut = (token.type === "undefinedReferenceShortcut"); addReferenceToDictionary(token, label, isShortcut); } break; } } return { references, shortcuts, definitions, duplicateDefinitions, definitionLineIndices }; } helpers.getReferenceLinkImageData = getReferenceLinkImageData; /** * Gets the most common line ending, falling back to the platform default. * * @param {string} input Markdown content to analyze. * @param {Object} [os] Node.js "os" module. * @returns {string} Preferred line ending. */ function getPreferredLineEnding(input, os) { let cr = 0; let lf = 0; let crlf = 0; const endings = input.match(newLineRe) || []; for (const ending of endings) { // eslint-disable-next-line default-case switch (ending) { case "\r": cr++; break; case "\n": lf++; break; case "\r\n": crlf++; break; } } let preferredLineEnding = null; if (!cr && !lf && !crlf) { preferredLineEnding = (os && os.EOL) || "\n"; } else if ((lf >= crlf) && (lf >= cr)) { preferredLineEnding = "\n"; } else if (crlf >= cr) { preferredLineEnding = "\r\n"; } else { preferredLineEnding = "\r"; } return preferredLineEnding; } helpers.getPreferredLineEnding = getPreferredLineEnding; /** * Expands a path with a tilde to an absolute path. * * @param {string} file Path that may begin with a tilde. * @param {Object} os Node.js "os" module. * @returns {string} Absolute path (or original path). */ function expandTildePath(file, os) { const homedir = os && os.homedir && os.homedir(); return homedir ? file.replace(/^~($|\/|\\)/, `${homedir}$1`) : file; } helpers.expandTildePath = expandTildePath; return helpers; } var helpersExports = requireHelpers(); const flatTokensSymbol = Symbol("flat-tokens"); const htmlFlowSymbol = Symbol("html-flow"); function inHtmlFlow(token) { return Boolean(token[htmlFlowSymbol]); } function addRangeToSet(set, start, end) { for (let i = start; i <= end; i++) { set.add(i); } } 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; } function filterByTypes(tokens, types, htmlFlow = false) { const predicate = (token) => types.includes(token.type) && (htmlFlow || !inHtmlFlow(token)); const flatTokens = tokens[flatTokensSymbol]; if (flatTokens) { return flatTokens.filter(predicate); } return filterByPredicate(tokens, predicate); } function getBlockQuotePrefixText(tokens, lineNumber, count = 1) { return filterByTypes(tokens, ["blockQuotePrefix", "linePrefix"]).filter((prefix) => prefix.startLine === lineNumber).map((prefix) => prefix.text).join("").trimEnd().concat("\n").repeat(count); } 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; } function getParentOfType(token, types) { let current = token; while ((current = current.parent) && !types.includes(current.type)) { } return current; } const nonContentTokens = /* @__PURE__ */ new Set([ "blockQuoteMarker", "blockQuotePrefix", "blockQuotePrefixWhitespace", "lineEnding", "lineEndingBlank", "linePrefix", "listItemIndent" ]); const MDC018 = { "names": ["MDC018", "mdc-no-missing-space-atx"], "description": "No space after hash on atx style heading", "tags": ["headings", "atx", "spaces"], "parser": "micromark", "function": function MDC0182(params, onError) { const { lines, parsers: { micromark: { tokens } } } = params; const ignoreBlockLineNumbers = /* @__PURE__ */ new Set(); for (const ignoreBlock of filterByTypes(tokens, ["codeFenced", "codeIndented", "htmlFlow", "componentContainer"])) { addRangeToSet(ignoreBlockLineNumbers, ignoreBlock.startLine, ignoreBlock.endLine); } for (const [lineIndex, line] of lines.entries()) { if (!ignoreBlockLineNumbers.has(lineIndex + 1) && /^#+[^# \t]/.test(line) && !/#\s*$/.test(line) && !line.startsWith("#\uFE0F\u20E3")) { const hashCount = /^#+/.exec(line)[0].length; helpersExports.addErrorContext( onError, lineIndex + 1, line.trim(), void 0, void 0, [1, hashCount + 1], { "editColumn": hashCount + 1, "insertText": " " } ); } } } }; const isList$1 = (token) => token.type === "listOrdered" || token.type === "listUnordered"; const MDC032 = { "names": ["MDD032", "mdc-blanks-around-lists"], "description": "Lists should be surrounded by blank lines", "tags": ["bullet", "ul", "ol", "blank_lines"], "parser": "micromark", "function": function MDD032(params, onError) { const { lines, parsers } = params; const blockQuotePrefixes = filterByTypes(parsers.micromark.tokens, ["blockQuotePrefix", "linePrefix"]); const topLevelLists = filterByPredicate( parsers.micromark.tokens, isList$1, (token) => isList$1(token) || token.type === "htmlFlow" || token.type === "componentContainerDataSection" ? [] : token.children ); for (const list of topLevelLists) { const firstLineNumber = list.startLine; if (!helpersExports.isBlankLine(lines[firstLineNumber - 2])) { helpersExports.addErrorContext( onError, firstLineNumber, lines[firstLineNumber - 1].trim(), void 0, void 0, void 0, { "insertText": getBlockQuotePrefixText(blockQuotePrefixes, firstLineNumber) } ); } let endLine = list.endLine; const flattenedChildren = filterByPredicate(list.children); for (const child of flattenedChildren.reverse()) { if (!nonContentTokens.has(child.type)) { endLine = child.endLine; break; } } const lastLineNumber = endLine; if (!helpersExports.isBlankLine(lines[lastLineNumber])) { helpersExports.addErrorContext( onError, lastLineNumber, lines[lastLineNumber - 1].trim(), void 0, void 0, void 0, { "lineNumber": lastLineNumber + 1, "insertText": getBlockQuotePrefixText(blockQuotePrefixes, lastLineNumber) } ); } } } }; const unorderedListTypes$1 = ["blockQuotePrefix", "listItemPrefix", "listUnordered"]; const unorderedParentTypes$1 = ["blockQuote", "listOrdered", "listUnordered"]; const MDC007 = { "names": ["MDC007", "mdc-ul-indent"], "description": "Unordered list indentation", "tags": ["bullet", "ul", "indentation"], "parser": "micromark", "function": function MDC0072(params, onError) { const indent = Number(params.config.indent || 2); const startIndented = !!params.config.start_indented; const startIndent = Number(params.config.start_indent || indent); const unorderedListNesting = /* @__PURE__ */ new Map(); let lastBlockQuotePrefix = null; const tokens = filterByPredicate( params.parsers.micromark.tokens, (token) => unorderedListTypes$1.includes(token.type), (token) => token.type === "htmlFlow" || token.type === "componentContainerDataSection" ? [] : token.children ); for (const token of tokens) { const { endColumn, parent, startColumn, startLine, type } = token; if (type === "blockQuotePrefix") { lastBlockQuotePrefix = token; } else if (type === "listUnordered") { let nesting = 0; let current = token; while ( // @ts-ignore current = getParentOfType(current, unorderedParentTypes$1) ) { if (current.type === "listUnordered") { nesting++; continue; } else if (current.type === "listOrdered") { nesting = -1; } break; } if (nesting >= 0) { unorderedListNesting.set(token, nesting); } } else { const nesting = unorderedListNesting.get(parent); if (nesting !== void 0) { const expectedIndent = (startIndented ? startIndent : 0) + nesting * indent; const blockQuoteAdjustment = lastBlockQuotePrefix?.endLine === startLine ? lastBlockQuotePrefix.endColumn - 1 : 0; const actualIndent = startColumn - 1 - blockQuoteAdjustment; const range = [1, endColumn - 1]; const fixInfo = { "editColumn": startColumn - actualIndent, "deleteCount": Math.max(actualIndent - expectedIndent, 0), "insertText": "".padEnd(Math.max(expectedIndent - actualIndent, 0)) }; helpersExports.addErrorDetailIf( onError, startLine, expectedIndent, actualIndent, void 0, void 0, range, fixInfo ); } } } } }; const MDC034 = { "names": ["MDC034", "mdc-no-bare-urls"], "description": "Bare URL used", "tags": ["links", "url"], "parser": "micromark", "function": function MDC0342(params, onError) { const literalAutolinks = (tokens) => filterByPredicate( tokens, (token) => { if (token.type === "literalAutolink" && !inHtmlFlow(token)) { const siblings = token.parent?.children; const index = siblings?.indexOf(token); const prev = siblings?.at(index - 1); const next = siblings?.at(index + 1); return !(prev && next && prev.type === "data" && next.type === "data" && prev.text.endsWith("<") && next.text.startsWith(">")); } return false; }, (token) => { if (token.type === "componentContainerDataSection") { return []; } const { children } = token; const result = []; for (let i = 0; i < children.length; i++) { const current = children[i]; const openTagInfo = getHtmlTagInfo(current); if (openTagInfo && !openTagInfo.close) { let count = 1; for (let j = i + 1; j < children.length; j++) { const candidate = children[j]; const closeTagInfo = getHtmlTagInfo(candidate); if (closeTagInfo && openTagInfo.name === closeTagInfo.name) { if (closeTagInfo.close) { count--; if (count === 0) { i = j; break; } } else { count++; } } } } else { result.push(current); } } return result; } ); for (const token of literalAutolinks(params.parsers.micromark.tokens)) { const range = [ token.startColumn, token.endColumn - token.startColumn ]; const fixInfo = { "editColumn": range[0], "deleteCount": range[1], "insertText": `<${token.text}>` }; helpersExports.addErrorContext( onError, token.startLine, token.text, void 0, void 0, range, fixInfo ); } } }; const isBlockComponentStart = (line) => line.match(/^\s*:{2,}\w+/); const isBlockComponentEnd = (line) => line.match(/^\s*:{2,}\s*$/); const isBlockComponentSlot = (line) => line.match(/^\s*#\w+/); const codeFencePrefixRe$1 = /^(.*?)[`~]/; function addError$1(onError, lines, lineNumber, top) { const line = lines[lineNumber - 1]; const [, prefix] = line.match(codeFencePrefixRe$1) || []; const fixInfo = prefix === void 0 ? void 0 : { "lineNumber": lineNumber + (top ? 0 : 1), "insertText": `${prefix.replace(/[^>]/g, " ").trim()} ` }; helpersExports.addErrorContext( onError, lineNumber, line.trim(), void 0, void 0, void 0, fixInfo ); } const MDC031 = { "names": ["MDC031", "mdc-blanks-around-fences"], "description": "Fenced code blocks should be surrounded by blank lines", "tags": ["code", "blank_lines"], "parser": "micromark", "function": function MD031(params, onError) { const listItems = params.config.list_items; const includeListItems = listItems === void 0 ? true : !!listItems; const { lines } = params; for (const codeBlock of filterByTypes(params.parsers.micromark.tokens, ["codeFenced"])) { if (includeListItems || !getParentOfType(codeBlock, ["listOrdered", "listUnordered"])) { if (!helpersExports.isBlankLine(lines[codeBlock.startLine - 2]) && !isBlockComponentStart(lines[codeBlock.startLine - 2]) && !isBlockComponentSlot(lines[codeBlock.startLine - 2])) { addError$1(onError, lines, codeBlock.startLine, true); } if (!helpersExports.isBlankLine(lines[codeBlock.endLine]) && !helpersExports.isBlankLine(lines[codeBlock.endLine - 1])) { if (!isBlockComponentEnd(lines[codeBlock.endLine]) && !isBlockComponentStart(lines[codeBlock.endLine - 1])) { addError$1(onError, lines, codeBlock.endLine, false); } } } } } }; async function getConfig(root, overrides) { const configFiles = await glob(".markdownlint.*", { cwd: root, dot: true }); const { config } = await loadConfig({ configFile: configFiles?.[0] ?? ".markdownlint.yaml", cwd: root, defaultConfig: { // Overridden rules "MD018": false, // No extra spaces after blockquote "MD032": false, // Lists should be surrounded by blank lines "MD007": false, // Unordered list indentation "MD034": false, // No bare URL used "MD031": false, // Fenced code blocks should be surrounded by blank lines // Configured rules "MD025": { "front_matter_title": "" // Ignore front matter title }, // Disabled rules "MD041": false // First line in file should be a top level header } }); if (overrides) { return { ...config, ...overrides }; } return config; } async function getMarkdownlintOptions(root, opts) { const config = await getConfig(root, opts.overrides); const options = { config }; if (opts.preset === "mdc") { options.customRules = [ MDC018, MDC032, MDC007, MDC034, MDC031 ]; } return options; } // @ts-check const fs = { access, accessSync, readFile, readFileSync }; const module = { createRequire }; const os = { EOL, homedir }; const path$1 = { dirname, resolve }; var micromarkHelpersExports = requireMicromarkHelpers(); // @ts-check /** @type {Map<string, object>} */ const map = new Map(); let params = undefined; /** * Initializes (resets) the cache. * * @param {import("markdownlint").RuleParams} [p] Rule parameters object. * @returns {void} */ function initialize(p) { map.clear(); params = p; } /** * Gets a cached object value - computes it and caches it. * * @param {string} name Cache object name. * @param {Function} getValue Getter for object value. * @returns {Object} Object value. */ function getCached(name, getValue) { if (map.has(name)) { return map.get(name); } const value = getValue(); map.set(name, value); return value; } /** * Filters a list of Micromark tokens by type and caches the result. * * @param {import("markdownlint").MicromarkTokenType[]} types Types to allow. * @param {boolean} [htmlFlow] Whether to include htmlFlow content. * @returns {import("markdownlint").MicromarkToken[]} Filtered tokens. */ function filterByTypesCached(types, htmlFlow) { return getCached( // eslint-disable-next-line prefer-rest-params JSON.stringify(arguments), () => micromarkHelpersExports.filterByTypes(params.parsers.micromark.tokens, types, htmlFlow) ); } /** * Gets a reference link and image data object. * * @returns {Object} Reference link and image data object. */ function getReferenceLinkImageData() { return getCached( getReferenceLinkImageData.name, () => helpersExports.getReferenceLinkImageData(params.parsers.micromark.tokens) ); } // @ts-check const homepage = "https://github.com/DavidAnson/markdownlint"; const version = "0.37.1"; // @ts-check /** @type {import("markdownlint").Rule} */ const md001 = { "names": [ "MD001", "heading-increment" ], "description": "Heading levels should only increment by one level at a time", "tags": [ "headings" ], "parser": "micromark", "function": function MD001(params, onError) { let prevLevel = Number.MAX_SAFE_INTEGER; for (const heading of filterByTypesCached([ "atxHeading", "setextHeading" ])) { const level = micromarkHelpersExports.getHeadingLevel(heading); if (level > prevLevel) { helpersExports.addErrorDetailIf( onError, heading.startLine, `h${prevLevel + 1}`, `h${level}` ); } prevLevel = level; } } }; // @ts-check /** @type {import("markdownlint").Rule} */ const md003 = { "names": [ "MD003", "heading-style" ], "description": "Heading style", "tags": [ "headings" ], "parser": "micromark", "function": function MD003(params, onError) { let style = String(params.config.style || "consistent"); for (const heading of filterByTypesCached([ "atxHeading", "setextHeading" ])) { const styleForToken = micromarkHelpersExports.getHeadingStyle(heading); if (style === "consistent") { style = styleForToken; } if (styleForToken !== style) { const h12 = micromarkHelpersExports.getHeadingLevel(heading) <= 2; const setextWithAtx = (style === "setext_with_atx") && ((h12 && (styleForToken === "setext")) || (!h12 && (styleForToken === "atx"))); const setextWithAtxClosed = (style === "setext_with_atx_closed") && ((h12 && (styleForToken === "setext")) || (!h12 && (styleForToken === "atx_closed"))); if (!setextWithAtx && !setextWithAtxClosed) { let expected = style; if (style === "setext_with_atx") { expected = h12 ? "setext" : "atx"; } else if (style === "setext_with_atx_closed") { expected = h12 ? "setext" : "atx_closed"; } helpersExports.addErrorDetailIf( onError, heading.startLine, expected, styleForToken ); } } } } }; // @ts-check const markerToStyle = { "-": "dash", "+": "plus", "*": "asterisk" }; const styleToMarker = { "dash": "-", "plus": "+", "asterisk": "*" }; const differentItemStyle = { "dash": "plus", "plus": "asterisk", "asterisk": "dash" }; const validStyles = new Set([ "asterisk", "consistent", "dash", "plus", "sublist" ]); /** @type {import("markdownlint").Rule} */ const md004 = { "names": [ "MD004", "ul-style" ], "description": "Unordered list style", "tags": [ "bullet", "ul" ], "parser": "micromark", "function": function MD004(params, onError) { const style = String(params.config.style || "consistent"); let expectedStyle = validStyles.has(style) ? style : "dash"; const nestingStyles = []; for (const listUnordered of filterByTypesCached([ "listUnordered" ])) { let nesting = 0; if (style === "sublist") { /** @type {import("markdownlint").MicromarkToken | null} */ let parent = listUnordered; // @ts-ignore while ((parent = micromarkHelpersExports.getParentOfType(parent, [ "listOrdered", "listUnordered" ]))) { nesting++; } } const listItemMarkers = micromarkHelpersExports.getDescendantsByType(listUnordered, [ "listItemPrefix", "listItemMarker" ]); for (const listItemMarker of listItemMarkers) { const itemStyle = markerToStyle[listItemMarker.text]; if (style === "sublist") { if (!nestingStyles[nesting]) { nestingStyles[nesting] = (itemStyle === nestingStyles[nesting - 1]) ? differentItemStyle[itemStyle] : itemStyle; } expectedStyle = nestingStyles[nesting]; } else if (expectedStyle === "consistent") { expectedStyle = itemStyle; } const column = listItemMarker.startColumn; const length = listItemMarker.endColumn - listItemMarker.startColumn; helpersExports.addErrorDetailIf( onError, listItemMarker.startLine, expectedStyle, itemStyle, undefined, undefined, [ column, length ], { "editColumn": column, "deleteCount": length, "insertText": styleToMarker[expectedStyle] } ); } } } }; // @ts-check /** @type {import("markdownlint").Rule} */ const md005 = { "names": [ "MD005", "list-indent" ], "description": "Inconsistent indentation for list items at the same level", "tags": [ "bullet", "ul", "indentation" ], "parser": "micromark", "function": function MD005(params, onError) { for (const list of filterByTypesCached([ "listOrdered", "listUnordered" ])) { const expectedIndent = list.startColumn - 1; let expectedEnd = 0; let endMatching = false; const listItemPrefixes = list.children.filter((token) => (token.type === "listItemPrefix")); for (const listItemPrefix of listItemPrefixes) { const lineNumber = listItemPrefix.startLine; const actualIndent = listItemPrefix.startColumn - 1; const range = [ 1, listItemPrefix.endColumn - 1 ]; if (list.type === "listUnordered") { helpersExports.addErrorDetailIf( onError, lineNumber, expectedIndent, actualIndent, undefined, undefined, range // No fixInfo; MD007 handles this scenario better ); } else { const markerLength = listItemPrefix.text.trim().length; const actualEnd = listItemPrefix.startColumn + markerLength - 1; expectedEnd = expectedEnd || actualEnd; if ((expectedIndent !== actualIndent) || endMatching) { if (expectedEnd === actualEnd) { endMatching = true; } else { const detail = endMatching ? `Expected: