markdownlint/lib/md044.mjs

110 lines
4 KiB
JavaScript

// @ts-check
import { addErrorDetailIf, escapeForRegExp, hasOverlap } from "../helpers/helpers.cjs";
import { filterByPredicate, filterByTypes } from "../helpers/micromark-helpers.cjs";
import { parse } from "./micromark-parse.mjs";
const ignoredChildTypes = new Set(
[ "codeFencedFence", "definition", "reference", "resource" ]
);
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD044", "proper-names" ],
"description": "Proper names should have the correct capitalization",
"tags": [ "spelling" ],
"parser": "micromark",
"function": function MD044(params, onError) {
let names = params.config.names;
names = Array.isArray(names) ? names : [];
names.sort((a, b) => (b.length - a.length) || a.localeCompare(b));
if (names.length === 0) {
// Nothing to check; avoid doing any work
return;
}
const codeBlocks = params.config.code_blocks;
const includeCodeBlocks =
(codeBlocks === undefined) ? true : !!codeBlocks;
const htmlElements = params.config.html_elements;
const includeHtmlElements =
(htmlElements === undefined) ? true : !!htmlElements;
const scannedTypes = new Set([ "data" ]);
if (includeCodeBlocks) {
scannedTypes.add("codeFlowValue");
scannedTypes.add("codeTextData");
}
if (includeHtmlElements) {
scannedTypes.add("htmlFlowData");
scannedTypes.add("htmlTextData");
}
const contentTokens =
filterByPredicate(
params.parsers.micromark.tokens,
(token) => scannedTypes.has(token.type),
(token) => (
token.children.filter((t) => !ignoredChildTypes.has(t.type))
)
);
/** @type {import("../helpers/helpers.cjs").FileRange[]} */
const exclusions = [];
const scannedTokens = new Set();
for (const name of names) {
const escapedName = escapeForRegExp(name);
const startNamePattern = /^\W/.test(name) ? "" : "\\b_*";
const endNamePattern = /\W$/.test(name) ? "" : "_*\\b";
const namePattern = `(${startNamePattern})(${escapedName})${endNamePattern}`;
const nameRe = new RegExp(namePattern, "gi");
for (const token of contentTokens) {
let match = null;
while ((match = nameRe.exec(token.text)) !== null) {
const [ , leftMatch, nameMatch ] = match;
const column = token.startColumn + match.index + leftMatch.length;
const length = nameMatch.length;
const lineNumber = token.startLine;
/** @type {import("../helpers/helpers.cjs").FileRange} */
const nameRange = {
"startLine": lineNumber,
"startColumn": column,
"endLine": lineNumber,
"endColumn": column + length - 1
};
if (
!names.includes(nameMatch) &&
!exclusions.some((exclusion) => hasOverlap(exclusion, nameRange))
) {
/** @type {import("../helpers/helpers.cjs").FileRange[]} */
let autolinkRanges = [];
if (!scannedTokens.has(token)) {
autolinkRanges = filterByTypes(parse(token.text), [ "literalAutolink" ])
.map((tok) => ({
"startLine": lineNumber,
"startColumn": token.startColumn + tok.startColumn - 1,
"endLine": lineNumber,
"endColumn": token.endColumn + tok.endColumn - 1
}));
exclusions.push(...autolinkRanges);
scannedTokens.add(token);
}
if (!autolinkRanges.some((autolinkRange) => hasOverlap(autolinkRange, nameRange))) {
addErrorDetailIf(
onError,
token.startLine,
name,
nameMatch,
undefined,
undefined,
[ column, length ],
{
"editColumn": column,
"deleteCount": length,
"insertText": name
}
);
}
}
exclusions.push(nameRange);
}
}
}
}
};