Update MD041/first-line-heading to add an allow_preamble parameter (fixes #1416).

This commit is contained in:
David Anson 2025-03-22 16:15:59 -07:00
parent c061888937
commit 62dc79864d
17 changed files with 538 additions and 163 deletions

View file

@ -406,14 +406,14 @@ export interface ConfigurationStrict {
MD025?:
| boolean
| {
/**
* Heading level
*/
level?: number;
/**
* RegExp for matching title in front matter
*/
front_matter_title?: string;
/**
* Heading level
*/
level?: number;
};
/**
* MD025/single-title/single-h1 : Multiple top-level headings in the same document : https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md025.md
@ -421,14 +421,14 @@ export interface ConfigurationStrict {
"single-title"?:
| boolean
| {
/**
* Heading level
*/
level?: number;
/**
* RegExp for matching title in front matter
*/
front_matter_title?: string;
/**
* Heading level
*/
level?: number;
};
/**
* MD025/single-title/single-h1 : Multiple top-level headings in the same document : https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md025.md
@ -436,14 +436,14 @@ export interface ConfigurationStrict {
"single-h1"?:
| boolean
| {
/**
* Heading level
*/
level?: number;
/**
* RegExp for matching title in front matter
*/
front_matter_title?: string;
/**
* Heading level
*/
level?: number;
};
/**
* MD026/no-trailing-punctuation : Trailing punctuation in heading : https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md026.md
@ -730,13 +730,17 @@ export interface ConfigurationStrict {
| boolean
| {
/**
* Heading level
* Allow content before first heading
*/
level?: number;
allow_preamble?: boolean;
/**
* RegExp for matching title in front matter
*/
front_matter_title?: string;
/**
* Heading level
*/
level?: number;
};
/**
* MD041/first-line-heading/first-line-h1 : First line in a file should be a top-level heading : https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md041.md
@ -745,13 +749,17 @@ export interface ConfigurationStrict {
| boolean
| {
/**
* Heading level
* Allow content before first heading
*/
level?: number;
allow_preamble?: boolean;
/**
* RegExp for matching title in front matter
*/
front_matter_title?: string;
/**
* Heading level
*/
level?: number;
};
/**
* MD041/first-line-heading/first-line-h1 : First line in a file should be a top-level heading : https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md041.md
@ -760,13 +768,17 @@ export interface ConfigurationStrict {
| boolean
| {
/**
* Heading level
* Allow content before first heading
*/
level?: number;
allow_preamble?: boolean;
/**
* RegExp for matching title in front matter
*/
front_matter_title?: string;
/**
* Heading level
*/
level?: number;
};
/**
* MD042/no-empty-links : No empty links : https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md042.md

View file

@ -3,6 +3,26 @@
import { addErrorContext, frontMatterHasTitle } from "../helpers/helpers.cjs";
import { filterByTypes, getHeadingLevel, getHtmlTagInfo, isHtmlFlowComment, nonContentTokens } from "../helpers/micromark-helpers.cjs";
const headingTagNameRe = /^h[1-6]$/;
/**
* Gets the HTML tag name of an htmlFlow token.
*
* @param {import("markdownlint").MicromarkToken} token Micromark Token.
* @returns {string | null} Tag name.
*/
function getHtmlFlowTagName(token) {
const { children, type } = token;
if (type === "htmlFlow") {
const htmlTexts = filterByTypes(children, [ "htmlText" ], true);
const tagInfo = (htmlTexts.length > 0) && getHtmlTagInfo(htmlTexts[0]);
if (tagInfo) {
return tagInfo.name.toLowerCase();
}
}
return null;
}
/** @type {import("markdownlint").Rule} */
export default {
"names": [ "MD041", "first-line-heading", "first-line-h1" ],
@ -10,6 +30,7 @@ export default {
"tags": [ "headings" ],
"parser": "micromark",
"function": function MD041(params, onError) {
const allowPreamble = !!params.config.allow_preamble;
const level = Number(params.config.level || 1);
const { tokens } = params.parsers.micromark;
if (
@ -18,22 +39,33 @@ export default {
params.config.front_matter_title
)
) {
let errorLineNumber = 0;
for (const token of tokens) {
if (!nonContentTokens.has(token.type) && !isHtmlFlowComment(token)) {
let isError = true;
if ((token.type === "atxHeading") || (token.type === "setextHeading")) {
isError = (getHeadingLevel(token) !== level);
} else if (token.type === "htmlFlow") {
const htmlTexts = filterByTypes(token.children, [ "htmlText" ], true);
const tagInfo = (htmlTexts.length > 0) && getHtmlTagInfo(htmlTexts[0]);
isError = !tagInfo || (tagInfo.name.toLowerCase() !== `h${level}`);
const { startLine, type } = token;
if (!nonContentTokens.has(type) && !isHtmlFlowComment(token)) {
let tagName = null;
if ((type === "atxHeading") || (type === "setextHeading")) {
// First heading needs to have the expected level
if (getHeadingLevel(token) !== level) {
errorLineNumber = startLine;
}
break;
} else if ((tagName = getHtmlFlowTagName(token)) && headingTagNameRe.test(tagName)) {
// First HTML element needs to have an <h?> with the expected level
if (tagName !== `h${level}`) {
errorLineNumber = startLine;
}
break;
} else if (!allowPreamble) {
// First non-content needs to be a heading with the expected level
errorLineNumber = startLine;
break;
}
if (isError) {
addErrorContext(onError, token.startLine, params.lines[token.startLine - 1]);
}
break;
}
}
if (errorLineNumber > 0) {
addErrorContext(onError, errorLineNumber, params.lines[errorLineNumber - 1]);
}
}
}
};