diff --git a/lib/md025.mjs b/lib/md025.mjs index f0b83a34..aa1b88e4 100644 --- a/lib/md025.mjs +++ b/lib/md025.mjs @@ -1,7 +1,7 @@ // @ts-check import { addErrorContext, frontMatterHasTitle } from "../helpers/helpers.cjs"; -import { getHeadingLevel, getHeadingText } from "../helpers/micromark-helpers.cjs"; +import { getHeadingLevel, getHeadingText, isHtmlFlowComment, nonContentTokens } from "../helpers/micromark-helpers.cjs"; import { filterByTypesCached } from "./cache.mjs"; /** @type {import("markdownlint").Rule} */ @@ -12,24 +12,32 @@ export default { "parser": "micromark", "function": function MD025(params, onError) { const level = Number(params.config.level || 1); - const foundFrontMatterTitle = - frontMatterHasTitle( - params.frontMatterLines, - params.config.front_matter_title - ); - let hasTopLevelHeading = false; - for (const heading of filterByTypesCached([ "atxHeading", "setextHeading" ])) { - const headingLevel = getHeadingLevel(heading); - if (headingLevel === level) { - if (hasTopLevelHeading || foundFrontMatterTitle) { - const headingText = getHeadingText(heading); + const { tokens } = params.parsers.micromark; + const matchingHeadings = filterByTypesCached([ "atxHeading", "setextHeading" ]) + .filter((heading) => level === getHeadingLevel(heading)); + if (matchingHeadings.length > 0) { + const foundFrontMatterTitle = + frontMatterHasTitle( + params.frontMatterLines, + params.config.front_matter_title + ); + // Front matter title counts as a top-level heading if present + let hasTopLevelHeading = foundFrontMatterTitle; + if (!hasTopLevelHeading) { + // Check if the first matching heading is a top-level heading + const previousTokens = tokens.slice(0, tokens.indexOf(matchingHeadings[0])); + hasTopLevelHeading = previousTokens.every( + (token) => nonContentTokens.has(token.type) || isHtmlFlowComment(token) + ); + } + if (hasTopLevelHeading) { + // All other matching headings are violations + for (const heading of matchingHeadings.slice(foundFrontMatterTitle ? 0 : 1)) { addErrorContext( onError, heading.startLine, - headingText + getHeadingText(heading) ); - } else if (heading.startLine === 1) { - hasTopLevelHeading = true; } } } diff --git a/test/heading-multiple-top-level-preceding-blank-and-comment.md b/test/heading-multiple-top-level-preceding-blank-and-comment.md new file mode 100644 index 00000000..3e6e23a4 --- /dev/null +++ b/test/heading-multiple-top-level-preceding-blank-and-comment.md @@ -0,0 +1,6 @@ + + + +# Heading 1 + +# Heading 2 {MD025} diff --git a/test/heading-multiple-top-level-preceding-blank.md b/test/heading-multiple-top-level-preceding-blank.md new file mode 100644 index 00000000..3170bcfc --- /dev/null +++ b/test/heading-multiple-top-level-preceding-blank.md @@ -0,0 +1,4 @@ + +# Heading 1 + +# Heading 2 {MD025} diff --git a/test/heading-multiple-top-level-preceding-comment.md b/test/heading-multiple-top-level-preceding-comment.md new file mode 100644 index 00000000..9bf24d7b --- /dev/null +++ b/test/heading-multiple-top-level-preceding-comment.md @@ -0,0 +1,4 @@ + +# Heading 1 + +# Heading 2 {MD025} diff --git a/test/snapshots/markdownlint-test-scenarios.mjs.md b/test/snapshots/markdownlint-test-scenarios.mjs.md index 34cd122c..a2c6b8d7 100644 --- a/test/snapshots/markdownlint-test-scenarios.mjs.md +++ b/test/snapshots/markdownlint-test-scenarios.mjs.md @@ -15750,6 +15750,92 @@ Generated by [AVA](https://avajs.dev). `, } +## heading-multiple-top-level-preceding-blank-and-comment.md + +> Snapshot 1 + + { + errors: [ + { + errorContext: 'Heading 2 {MD025}', + errorDetail: null, + errorRange: null, + fixInfo: null, + lineNumber: 6, + ruleDescription: 'Multiple top-level headings in the same document', + ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md025.md', + ruleNames: [ + 'MD025', + 'single-title', + 'single-h1', + ], + }, + ], + fixed: `␊ + ␊ + ␊ + # Heading 1␊ + ␊ + # Heading 2 {MD025}␊ + `, + } + +## heading-multiple-top-level-preceding-blank.md + +> Snapshot 1 + + { + errors: [ + { + errorContext: 'Heading 2 {MD025}', + errorDetail: null, + errorRange: null, + fixInfo: null, + lineNumber: 4, + ruleDescription: 'Multiple top-level headings in the same document', + ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md025.md', + ruleNames: [ + 'MD025', + 'single-title', + 'single-h1', + ], + }, + ], + fixed: `␊ + # Heading 1␊ + ␊ + # Heading 2 {MD025}␊ + `, + } + +## heading-multiple-top-level-preceding-comment.md + +> Snapshot 1 + + { + errors: [ + { + errorContext: 'Heading 2 {MD025}', + errorDetail: null, + errorRange: null, + fixInfo: null, + lineNumber: 4, + ruleDescription: 'Multiple top-level headings in the same document', + ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md025.md', + ruleNames: [ + 'MD025', + 'single-title', + 'single-h1', + ], + }, + ], + fixed: `␊ + # Heading 1␊ + ␊ + # Heading 2 {MD025}␊ + `, + } + ## heading_duplicate_content.md > Snapshot 1 diff --git a/test/snapshots/markdownlint-test-scenarios.mjs.snap b/test/snapshots/markdownlint-test-scenarios.mjs.snap index c39a3eb2..ca0a29dd 100644 Binary files a/test/snapshots/markdownlint-test-scenarios.mjs.snap and b/test/snapshots/markdownlint-test-scenarios.mjs.snap differ