markdownlint/lib/md013.mjs

93 lines
4 KiB
JavaScript
Raw Normal View History

// @ts-check
import { addErrorDetailIf } from "../helpers/helpers.cjs";
import { filterByTypesCached, getReferenceLinkImageData } from "./cache.mjs";
import { addRangeToSet, getDescendantsByType } from "../helpers/micromark-helpers.cjs";
2025-11-27 19:45:22 -08:00
import stringWidth from "string-width";
// Regular expression for a line that is not wrappable
const notWrappableRe = /^(?:[#>\s]*\s)?\S*$/;
/** @typedef {import("micromark-extension-gfm-autolink-literal")} */
/** @typedef {import("micromark-extension-gfm-table")} */
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD013", "line-length" ],
"description": "Line length",
"tags": [ "line_length" ],
"parser": "micromark",
"function": function MD013(params, onError) {
const lineLength = Number(params.config.line_length || 80);
const headingLineLength = Number(params.config.heading_line_length || lineLength);
const codeLineLength = Number(params.config.code_block_line_length || lineLength);
const strict = !!params.config.strict;
const stern = !!params.config.stern;
const codeBlocks = params.config.code_blocks;
const includeCodeBlocks = (codeBlocks === undefined) ? true : !!codeBlocks;
const tables = params.config.tables;
const includeTables = (tables === undefined) ? true : !!tables;
const headings = params.config.headings;
const includeHeadings = (headings === undefined) ? true : !!headings;
2025-11-27 19:45:22 -08:00
const wideCharacters = !!params.config.wide_characters;
const headingLineNumbers = new Set();
for (const heading of filterByTypesCached([ "atxHeading", "setextHeading" ])) {
addRangeToSet(headingLineNumbers, heading.startLine, heading.endLine);
}
const codeBlockLineNumbers = new Set();
for (const codeBlock of filterByTypesCached([ "codeFenced", "codeIndented" ])) {
addRangeToSet(codeBlockLineNumbers, codeBlock.startLine, codeBlock.endLine);
}
const tableLineNumbers = new Set();
for (const table of filterByTypesCached([ "table" ])) {
addRangeToSet(tableLineNumbers, table.startLine, table.endLine);
}
const linkLineNumbers = new Set();
for (const link of filterByTypesCached([ "autolink", "image", "link", "literalAutolink" ])) {
addRangeToSet(linkLineNumbers, link.startLine, link.endLine);
}
const paragraphDataLineNumbers = new Set();
for (const paragraph of filterByTypesCached([ "paragraph" ])) {
for (const data of getDescendantsByType(paragraph, [ "data" ])) {
addRangeToSet(paragraphDataLineNumbers, data.startLine, data.endLine);
}
}
const linkOnlyLineNumbers = new Set();
for (const lineNumber of linkLineNumbers) {
if (!paragraphDataLineNumbers.has(lineNumber)) {
linkOnlyLineNumbers.add(lineNumber);
}
}
const definitionLineIndices = new Set(getReferenceLinkImageData().definitionLineIndices);
for (let lineIndex = 0; lineIndex < params.lines.length; lineIndex++) {
const line = params.lines[lineIndex];
const lineNumber = lineIndex + 1;
const isHeading = headingLineNumbers.has(lineNumber);
const inCode = codeBlockLineNumbers.has(lineNumber);
const inTable = tableLineNumbers.has(lineNumber);
const maxLength = inCode ? codeLineLength : (isHeading ? headingLineLength : lineLength);
const text = (strict || stern) ? line : line.replace(/\S*$/u, "");
2025-11-27 19:45:22 -08:00
const textLength = wideCharacters ? stringWidth(text) : text.length;
if ((maxLength > 0) &&
(includeCodeBlocks || !inCode) &&
(includeTables || !inTable) &&
(includeHeadings || !isHeading) &&
!definitionLineIndices.has(lineIndex) &&
(strict ||
(!(stern && notWrappableRe.test(line)) &&
!linkOnlyLineNumbers.has(lineNumber))) &&
2025-11-27 19:45:22 -08:00
(textLength > maxLength)) {
addErrorDetailIf(
onError,
lineNumber,
maxLength,
2025-11-27 19:45:22 -08:00
wideCharacters ? stringWidth(line) : line.length,
undefined,
undefined,
2025-11-27 19:45:22 -08:00
wideCharacters ? undefined : [ maxLength + 1, line.length - maxLength ]
);
}
}
}
};