2018-01-21 21:44:25 -08:00
|
|
|
// @ts-check
|
|
|
|
|
2024-11-28 20:36:44 -08:00
|
|
|
import { addErrorContext, frontMatterHasTitle } from "../helpers/helpers.cjs";
|
|
|
|
import { filterByTypes, getHeadingLevel, getHtmlTagInfo, isHtmlFlowComment, nonContentTokens } from "../helpers/micromark-helpers.cjs";
|
2018-01-21 21:44:25 -08:00
|
|
|
|
2025-03-22 16:15:59 -07:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2024-12-03 19:58:28 -08:00
|
|
|
/** @type {import("markdownlint").Rule} */
|
2024-11-28 20:36:44 -08:00
|
|
|
export default {
|
2019-03-13 21:39:15 -07:00
|
|
|
"names": [ "MD041", "first-line-heading", "first-line-h1" ],
|
2020-12-28 13:28:38 -08:00
|
|
|
"description": "First line in a file should be a top-level heading",
|
2023-11-09 20:05:30 -08:00
|
|
|
"tags": [ "headings" ],
|
2024-03-18 20:48:22 -07:00
|
|
|
"parser": "micromark",
|
2018-01-21 21:44:25 -08:00
|
|
|
"function": function MD041(params, onError) {
|
2025-03-22 16:15:59 -07:00
|
|
|
const allowPreamble = !!params.config.allow_preamble;
|
2020-01-25 18:40:39 -08:00
|
|
|
const level = Number(params.config.level || 1);
|
2025-03-16 20:03:58 -07:00
|
|
|
const { tokens } = params.parsers.micromark;
|
|
|
|
if (
|
|
|
|
!frontMatterHasTitle(
|
|
|
|
params.frontMatterLines,
|
|
|
|
params.config.front_matter_title
|
|
|
|
)
|
|
|
|
) {
|
2025-03-22 16:15:59 -07:00
|
|
|
let errorLineNumber = 0;
|
2025-03-16 20:03:58 -07:00
|
|
|
for (const token of tokens) {
|
2025-03-22 16:15:59 -07:00
|
|
|
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;
|
2024-03-18 20:48:22 -07:00
|
|
|
}
|
2025-03-16 20:03:58 -07:00
|
|
|
}
|
|
|
|
}
|
2025-03-22 16:15:59 -07:00
|
|
|
if (errorLineNumber > 0) {
|
|
|
|
addErrorContext(onError, errorLineNumber, params.lines[errorLineNumber - 1]);
|
|
|
|
}
|
2019-03-10 22:10:33 -07:00
|
|
|
}
|
2018-01-21 21:44:25 -08:00
|
|
|
}
|
|
|
|
};
|