Files
LearnNode/node-course/node_modules/@eslint/markdown/dist/rules/no-missing-atx-heading-space.js
T
2026-08-06 12:00:39 +10:00

102 lines
4.3 KiB
JavaScript

/**
* @fileoverview Rule to ensure there is a space after hash on ATX style headings in Markdown.
* @author Sweta Tanwar (@SwetaTanwar)
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { MarkdownRuleDefinition } from "../types.js";
* @typedef {"missingSpace"} NoMissingAtxHeadingSpaceMessageIds
* @typedef {[{ checkClosedHeadings?: boolean }]} NoMissingAtxHeadingSpaceOptions
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoMissingAtxHeadingSpaceOptions, MessageIds: NoMissingAtxHeadingSpaceMessageIds }>} NoMissingAtxHeadingSpaceRuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const leadingAtxHeadingHashPattern = /(?:^|(?<=[\r\n]))(?<hashes>#{1,6})(?:[^# \t]|$)/gu;
const trailingAtxHeadingHashPattern = /(?<![ \t])(?<spaces>[ \t]*)(?<=(?<!\\)(?:\\{2})*)(?<hashes>#+)[ \t]*$/u;
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {NoMissingAtxHeadingSpaceRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
recommended: true,
description: "Disallow headings without a space after the hash characters",
url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-missing-atx-heading-space.md",
},
fixable: "whitespace",
messages: {
missingSpace: "Missing space {{position}} hash(es) on ATX style heading.",
},
schema: [
{
type: "object",
properties: {
checkClosedHeadings: {
type: "boolean",
},
},
additionalProperties: false,
},
],
defaultOptions: [{ checkClosedHeadings: false }],
},
create(context) {
const { sourceCode } = context;
const [{ checkClosedHeadings }] = context.options;
return {
heading(node) {
if (!checkClosedHeadings) {
return;
}
const text = sourceCode.getText(node);
const match = trailingAtxHeadingHashPattern.exec(text);
if (match === null) {
return;
}
const { spaces, hashes } = match.groups;
if (spaces.length > 0) {
return;
}
const startOffset = node.position.start.offset + match.index;
const endOffset = startOffset + hashes.length;
context.report({
loc: {
start: sourceCode.getLocFromIndex(startOffset - 1),
end: sourceCode.getLocFromIndex(endOffset),
},
messageId: "missingSpace",
data: { position: "before" },
fix(fixer) {
return fixer.insertTextBeforeRange([startOffset, startOffset + 1], " ");
},
});
},
paragraph(node) {
const text = sourceCode.getText(node);
/** @type {RegExpExecArray | null} */
let match;
while ((match = leadingAtxHeadingHashPattern.exec(text)) !== null) {
const { hashes } = match.groups;
const startOffset = node.position.start.offset + match.index;
const endOffset = startOffset + hashes.length;
context.report({
loc: {
start: sourceCode.getLocFromIndex(startOffset),
end: sourceCode.getLocFromIndex(endOffset + 1),
},
messageId: "missingSpace",
data: { position: "after" },
fix(fixer) {
return fixer.insertTextAfterRange([endOffset - 1, endOffset], " ");
},
});
}
},
};
},
});