From acdd00dbc46134f27d498d5013d4b2363d33ecb5 Mon Sep 17 00:00:00 2001 From: lumir Date: Sat, 1 Aug 2026 15:37:01 +0900 Subject: [PATCH 01/22] feat: support `languageOptions.parser` and test with a Rust-based parser --- package.json | 1 + src/language/markdown-language.js | 25 +++++-- src/types.ts | 46 +++++++++++++ tests/rules/_utils/rule-tester.js | 68 ++++++++++++++++++++ tests/rules/fenced-code-language.test.js | 12 +--- tests/rules/fenced-code-meta.test.js | 18 ++---- tests/rules/no-duplicate-definitions.test.js | 17 ++--- 7 files changed, 149 insertions(+), 38 deletions(-) create mode 100644 tests/rules/_utils/rule-tester.js diff --git a/package.json b/package.json index cd59866b..99b20852 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ }, "devDependencies": { "@arethetypeswrong/cli": "^0.18.3", + "@eslint-markdown/parser": "^0.0.0-canary.0", "@eslint/js": "^10.0.1", "@eslint/json": "^2.0.1", "@types/mdast": "^4.0.4", diff --git a/src/language/markdown-language.js b/src/language/markdown-language.js index 12cc528b..e9974f0d 100644 --- a/src/language/markdown-language.js +++ b/src/language/markdown-language.js @@ -145,6 +145,7 @@ export class MarkdownLanguage { defaultLanguageOptions = { frontmatter: false, math: false, + // TODO }; /** @@ -216,11 +217,25 @@ export class MarkdownLanguage { * problem that ESLint identified just like any other. */ try { - const options = createParserOptions( - this.#mode, - context?.languageOptions, - ); - const root = fromMarkdown(text, options); + const parser = context?.languageOptions?.parser ?? fromMarkdown; + + /** @type {Root} */ + let root; + + if (parser !== fromMarkdown) { + // @ts-expect-error -- TODO + root = parser(text, { + mode: this.#mode, + ...context?.languageOptions, + ...context?.languageOptions?.parserOptions, + // TODO: consider https://github.com/eslint/eslint/pull/20926. + }); + } else { + root = fromMarkdown( + text, + createParserOptions(this.#mode, context?.languageOptions), + ); + } return { ok: true, diff --git a/src/types.ts b/src/types.ts index ff4457cb..a0ee9520 100644 --- a/src/types.ts +++ b/src/types.ts @@ -42,6 +42,7 @@ import type { InlineMath, Math } from "mdast-util-math"; import type { LanguageContext, LanguageOptions, + ObjectMetaProperties, RuleVisitor, } from "@eslint/core"; import type { @@ -118,19 +119,64 @@ export interface JsonData extends Data {} // Exports: Language and Source Code //------------------------------------------------------------------------------ +type NonMdastParser = ObjectMetaProperties & + ( + | { + parse(text: string, options?: any): unknown; + } + | { + parseForESLint( + text: string, + options?: any, + ): { + ast: unknown; + }; + } + ); + +type MdastParser = ObjectMetaProperties & { + parse(text: string, options?: any): Root; + + parseForESLint( + text: string, + options?: any, + ): { + ast: Root; + }; +}; + +type Parser = NonMdastParser | MdastParser; + /** * Language options provided for Markdown files. */ export interface MarkdownLanguageOptions extends LanguageOptions { /** * The options for parsing frontmatter. + * @default false */ frontmatter?: false | "yaml" | "toml" | "json"; /** * The options for parsing math. + * @default false */ math?: boolean; + + /** + * An object containing a `parse()` or `parseForESLint()` method. + * If not configured, the default ESLint Markdown parser + * (`mdast-util-from-markdown`) will be used. + */ + parser?: Parser; + + /** + * An object specifying additional options that are passed directly to the + * `parser()` method on the parser. The available options are parser-dependent. + */ + parserOptions?: { + [key: string]: any; + }; } /** diff --git a/tests/rules/_utils/rule-tester.js b/tests/rules/_utils/rule-tester.js new file mode 100644 index 00000000..4d702722 --- /dev/null +++ b/tests/rules/_utils/rule-tester.js @@ -0,0 +1,68 @@ +/** + * @fileoverview A rule tester helper for running code with both `mdast-util-from-markdown` and `@eslint-markdown/parser`. + * @author lumir(lumirlumir) + */ + +// -------------------------------------------------------------------------------- +// Imports +// -------------------------------------------------------------------------------- + +import { parse } from "@eslint-markdown/parser"; +import { RuleTester } from "eslint"; +import markdown from "../../../src/index.js"; + +//------------------------------------------------------------------------------ +// Typedefs +//------------------------------------------------------------------------------ + +/** + * @import { MarkdownRuleDefinition } from "../../../src/types.ts"; + */ + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Creates a new `RuleTester` instance with the `mdast-util-from-markdown` JS parser. + */ +const ruleTesterJS = new RuleTester({ + plugins: { + markdown, + }, + language: "markdown/commonmark", +}); + +/** + * Creates a new `RuleTester` instance with the `@eslint-markdown/parser` Rust parser. + */ +const ruleTesterRust = new RuleTester({ + plugins: { + markdown, + }, + language: "markdown/commonmark", + languageOptions: { + parser: parse, + }, +}); + +//------------------------------------------------------------------------------ +// Exports +//------------------------------------------------------------------------------ + +/** + * Markdown rule tester. + * @param {string} ruleName Rule name. + * @param {MarkdownRuleDefinition} rule Rule definition. + * @param {Parameters[2]} tests Test cases. + * @returns {void} + */ +export default function ruleTester(ruleName, rule, tests) { + describe("JS: mdast-util-from-markdown", () => { + ruleTesterJS.run(ruleName, rule, tests); + }); + + describe("Rust: @eslint-markdown/parser", () => { + ruleTesterRust.run(ruleName, rule, tests); + }); +} diff --git a/tests/rules/fenced-code-language.test.js b/tests/rules/fenced-code-language.test.js index 8540a6ca..0632c5e9 100644 --- a/tests/rules/fenced-code-language.test.js +++ b/tests/rules/fenced-code-language.test.js @@ -7,22 +7,14 @@ // Imports //------------------------------------------------------------------------------ +import ruleTester from "./_utils/rule-tester.js"; import rule from "../../src/rules/fenced-code-language.js"; -import markdown from "../../src/index.js"; -import { RuleTester } from "eslint"; //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ - plugins: { - markdown, - }, - language: "markdown/commonmark", -}); - -ruleTester.run("fenced-code-language", rule, { +ruleTester("fenced-code-language", rule, { valid: [ // backtick code block `\`\`\`js diff --git a/tests/rules/fenced-code-meta.test.js b/tests/rules/fenced-code-meta.test.js index e7b07c65..1dade856 100644 --- a/tests/rules/fenced-code-meta.test.js +++ b/tests/rules/fenced-code-meta.test.js @@ -7,23 +7,15 @@ // Imports //------------------------------------------------------------------------------ -import rule from "../../src/rules/fenced-code-meta.js"; -import markdown from "../../src/index.js"; -import { RuleTester } from "eslint"; import dedent from "dedent"; +import ruleTester from "./_utils/rule-tester.js"; +import rule from "../../src/rules/fenced-code-meta.js"; //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ - plugins: { - markdown, - }, - language: "markdown/commonmark", -}); - -ruleTester.run("fenced-code-meta", rule, { +ruleTester("fenced-code-meta", rule, { valid: [ dedent`\`\`\` console.log("Hello, world!"); @@ -120,7 +112,7 @@ ruleTester.run("fenced-code-meta", rule, { ], }, { - code: dedent`\`\`\` js + code: dedent`\`\`\` js console.log("Hello, world!"); \`\`\``, errors: [ @@ -134,7 +126,7 @@ ruleTester.run("fenced-code-meta", rule, { ], }, { - code: dedent`~~~ js + code: dedent`~~~ js console.log("Hello, world!"); ~~~`, errors: [ diff --git a/tests/rules/no-duplicate-definitions.test.js b/tests/rules/no-duplicate-definitions.test.js index 47fd4d66..74fb76da 100644 --- a/tests/rules/no-duplicate-definitions.test.js +++ b/tests/rules/no-duplicate-definitions.test.js @@ -7,22 +7,14 @@ // Imports //------------------------------------------------------------------------------ +import ruleTester from "./_utils/rule-tester.js"; import rule from "../../src/rules/no-duplicate-definitions.js"; -import markdown from "../../src/index.js"; -import { RuleTester } from "eslint"; //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ - plugins: { - markdown, - }, - language: "markdown/gfm", -}); - -ruleTester.run("no-duplicate-definitions", rule, { +ruleTester("no-duplicate-definitions", rule, { valid: [ ` [mercury]: https://example.com/mercury/ @@ -348,6 +340,7 @@ ruleTester.run("no-duplicate-definitions", rule, { [^mercury]: Hello, Mercury! [^mercury]: Hello, Venus! `, + language: "markdown/gfm", errors: [ { messageId: "duplicateFootnoteDefinition", @@ -372,6 +365,7 @@ ruleTester.run("no-duplicate-definitions", rule, { [^mercury]: Hello, Earth! [^mercury]: Hello, Mars! `, + language: "markdown/gfm", errors: [ { messageId: "duplicateFootnoteDefinition", @@ -420,6 +414,7 @@ ruleTester.run("no-duplicate-definitions", rule, { [^mercury]: Hello, Mercury! [^Mercury]: Hello, Venus! `, // case insensitive + language: "markdown/gfm", errors: [ { messageId: "duplicateFootnoteDefinition", @@ -442,6 +437,7 @@ ruleTester.run("no-duplicate-definitions", rule, { [^mercury]: Hello, Mercury! [^mercury]: Hello, Venus! `, + language: "markdown/gfm", options: [ { allowDefinitions: ["mercury"], @@ -471,6 +467,7 @@ ruleTester.run("no-duplicate-definitions", rule, { [^mercury]: Hello, Mercury! [^mercury]: Hello, Venus! `, + language: "markdown/gfm", options: [ { checkFootnoteDefinitions: true, From b549e3599d5a8c8ff27eec839a626f1794ad6e9b Mon Sep 17 00:00:00 2001 From: lumir Date: Sat, 1 Aug 2026 16:49:55 +0900 Subject: [PATCH 02/22] wip --- src/language/markdown-language.js | 2 + tests/rules/_utils/rule-tester.js | 45 ++++++- tests/rules/heading-increment.test.js | 2 + tests/rules/no-bare-urls.test.js | 14 +- tests/rules/no-duplicate-definitions.test.js | 21 ++- tests/rules/no-duplicate-headings.test.js | 20 +-- tests/rules/no-empty-definitions.test.js | 93 +++++++++---- tests/rules/no-empty-images.test.js | 12 +- tests/rules/no-empty-links.test.js | 12 +- tests/rules/no-html.test.js | 14 +- tests/rules/no-invalid-label-refs.test.js | 12 +- .../no-missing-atx-heading-space.test.js | 56 +++----- tests/rules/no-missing-label-refs.test.js | 2 + tests/rules/no-missing-link-fragments.test.js | 16 +-- tests/rules/no-multiple-h1.test.js | 2 + tests/rules/no-reference-like-urls.test.js | 14 +- tests/rules/no-reversed-media-syntax.test.js | 2 + tests/rules/no-space-in-emphasis.test.js | 127 +++++++++++------- tests/rules/no-unused-definitions.test.js | 51 ++++--- tests/rules/require-alt-text.test.js | 2 + tests/rules/table-column-count.test.js | 93 +++++++++---- 21 files changed, 377 insertions(+), 235 deletions(-) diff --git a/src/language/markdown-language.js b/src/language/markdown-language.js index e9974f0d..f988c130 100644 --- a/src/language/markdown-language.js +++ b/src/language/markdown-language.js @@ -198,6 +198,8 @@ export class MarkdownLanguage { `Invalid language option value \`${mathOption}\` for math. Expected a boolean.`, ); } + + // TODO } /** diff --git a/tests/rules/_utils/rule-tester.js b/tests/rules/_utils/rule-tester.js index 4d702722..c5967a9c 100644 --- a/tests/rules/_utils/rule-tester.js +++ b/tests/rules/_utils/rule-tester.js @@ -24,9 +24,9 @@ import markdown from "../../../src/index.js"; //------------------------------------------------------------------------------ /** - * Creates a new `RuleTester` instance with the `mdast-util-from-markdown` JS parser. + * Creates a new `RuleTester` instance with the `mdast-util-from-markdown` JS parser and CommonMark syntax. */ -const ruleTesterJS = new RuleTester({ +const ruleTesterJSCommonMark = new RuleTester({ plugins: { markdown, }, @@ -34,9 +34,19 @@ const ruleTesterJS = new RuleTester({ }); /** - * Creates a new `RuleTester` instance with the `@eslint-markdown/parser` Rust parser. + * Creates a new `RuleTester` instance with the `mdast-util-from-markdown` JS parser and GFM syntax. */ -const ruleTesterRust = new RuleTester({ +const ruleTesterJSGFM = new RuleTester({ + plugins: { + markdown, + }, + language: "markdown/gfm", +}); + +/** + * Creates a new `RuleTester` instance with the `@eslint-markdown/parser` Rust parser and CommonMark syntax. + */ +const ruleTesterRustCommonMark = new RuleTester({ plugins: { markdown, }, @@ -46,6 +56,19 @@ const ruleTesterRust = new RuleTester({ }, }); +/** + * Creates a new `RuleTester` instance with the `@eslint-markdown/parser` Rust parser and GFM syntax. + */ +const ruleTesterRustGFM = new RuleTester({ + plugins: { + markdown, + }, + language: "markdown/gfm", + languageOptions: { + parser: parse, + }, +}); + //------------------------------------------------------------------------------ // Exports //------------------------------------------------------------------------------ @@ -59,10 +82,20 @@ const ruleTesterRust = new RuleTester({ */ export default function ruleTester(ruleName, rule, tests) { describe("JS: mdast-util-from-markdown", () => { - ruleTesterJS.run(ruleName, rule, tests); + describe("CommonMark", () => { + ruleTesterJSCommonMark.run(ruleName, rule, tests); + }); + describe("GFM", () => { + ruleTesterJSGFM.run(ruleName, rule, tests); + }); }); describe("Rust: @eslint-markdown/parser", () => { - ruleTesterRust.run(ruleName, rule, tests); + describe("CommonMark", () => { + ruleTesterRustCommonMark.run(ruleName, rule, tests); + }); + describe("GFM", () => { + ruleTesterRustGFM.run(ruleName, rule, tests); + }); }); } diff --git a/tests/rules/heading-increment.test.js b/tests/rules/heading-increment.test.js index adc3b1de..460f7a8e 100644 --- a/tests/rules/heading-increment.test.js +++ b/tests/rules/heading-increment.test.js @@ -16,6 +16,8 @@ import dedent from "dedent"; // Tests //------------------------------------------------------------------------------ +// TODO + const ruleTester = new RuleTester({ plugins: { markdown, diff --git a/tests/rules/no-bare-urls.test.js b/tests/rules/no-bare-urls.test.js index 2583fdc8..aa219cae 100644 --- a/tests/rules/no-bare-urls.test.js +++ b/tests/rules/no-bare-urls.test.js @@ -23,6 +23,8 @@ const ruleTester = new RuleTester({ language: "markdown/gfm", }); +// TODO + ruleTester.run("no-bare-urls", rule, { valid: [ "", @@ -173,12 +175,12 @@ ruleTester.run("no-bare-urls", rule, { { code: dedent` [link that [is-a-valid] link](https://example.com) - + [is-a-valid]: https://example.com `, output: dedent` [link that [is-a-valid] link]() - + [is-a-valid]: https://example.com `, errors: [ @@ -316,12 +318,12 @@ ruleTester.run("no-bare-urls", rule, { { code: dedent`
- + https://example.com
`, output: dedent`
- +
`, errors: [ @@ -337,13 +339,13 @@ ruleTester.run("no-bare-urls", rule, { { code: dedent`
- + https://example.com
`, output: dedent`
- +
`, diff --git a/tests/rules/no-duplicate-definitions.test.js b/tests/rules/no-duplicate-definitions.test.js index 74fb76da..1dcc9b44 100644 --- a/tests/rules/no-duplicate-definitions.test.js +++ b/tests/rules/no-duplicate-definitions.test.js @@ -25,20 +25,29 @@ ruleTester("no-duplicate-definitions", rule, { [venus]: https://example.com/venus/ `, - ` + { + code: ` [^mercury]: Hello, Mercury! `, + language: "markdown/gfm", + }, - ` + { + code: ` [^mercury]: Hello, Mercury! [^venus]: Hello, Venus! `, + language: "markdown/gfm", + }, - ` + { + code: ` [alpha]: bravo [^alpha]: bravo `, + language: "markdown/gfm", + }, ` [//]: # (This is a comment 1) @@ -62,6 +71,7 @@ ruleTester("no-duplicate-definitions", rule, { [^mercury]: Hello, Mercury! [^mercury]: Hello, Venus! `, + language: "markdown/gfm", options: [ { allowFootnoteDefinitions: ["mercury"], @@ -128,6 +138,7 @@ ruleTester("no-duplicate-definitions", rule, { [^MERCURY]: Hello, Mercury! [^mercury]: Hello, Venus! `, + language: "markdown/gfm", options: [ { allowFootnoteDefinitions: ["MERCURY"], @@ -139,6 +150,7 @@ ruleTester("no-duplicate-definitions", rule, { [^mercury]: Hello, Mercury! [^MERCURY]: Hello, Venus! `, + language: "markdown/gfm", options: [ { allowFootnoteDefinitions: ["mercury"], @@ -150,6 +162,7 @@ ruleTester("no-duplicate-definitions", rule, { [^mercury]: Hello, Mercury! [^mercury]: Hello, Venus! `, + language: "markdown/gfm", options: [ { checkFootnoteDefinitions: false, @@ -161,6 +174,7 @@ ruleTester("no-duplicate-definitions", rule, { [^mercury]: Hello, Mercury! [^mercury]: Hello, Venus! `, + language: "markdown/gfm", options: [ { checkFootnoteDefinitions: true, @@ -183,6 +197,7 @@ ruleTester("no-duplicate-definitions", rule, { [^Grüsse]: Grüsse [^Grüsse]: Grüsse `, + language: "markdown/gfm", options: [{ allowFootnoteDefinitions: ["GRÜẞE"] }], }, ] diff --git a/tests/rules/no-duplicate-headings.test.js b/tests/rules/no-duplicate-headings.test.js index 76f4190d..9172a1a0 100644 --- a/tests/rules/no-duplicate-headings.test.js +++ b/tests/rules/no-duplicate-headings.test.js @@ -7,33 +7,27 @@ // Imports //------------------------------------------------------------------------------ -import rule from "../../src/rules/no-duplicate-headings.js"; -import markdown from "../../src/index.js"; -import { Linter, RuleTester } from "eslint"; +import { Linter } from "eslint"; import dedent from "dedent"; +import ruleTester from "./_utils/rule-tester.js"; +import markdown from "../../src/index.js"; +import rule from "../../src/rules/no-duplicate-headings.js"; //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ - plugins: { - markdown, - }, - language: "markdown/commonmark", -}); - -ruleTester.run("no-duplicate-headings", rule, { +ruleTester("no-duplicate-headings", rule, { valid: [ // Using emphasis in headings should not be considered a duplicate by default. dedent` # Heading 1 - + # Heading *1* `, dedent` # ***Heading 1*** - + # Heading 1 `, dedent` diff --git a/tests/rules/no-empty-definitions.test.js b/tests/rules/no-empty-definitions.test.js index f98797f3..7528207a 100644 --- a/tests/rules/no-empty-definitions.test.js +++ b/tests/rules/no-empty-definitions.test.js @@ -7,23 +7,15 @@ // Imports //------------------------------------------------------------------------------ -import rule from "../../src/rules/no-empty-definitions.js"; -import markdown from "../../src/index.js"; -import { RuleTester } from "eslint"; import dedent from "dedent"; +import ruleTester from "./_utils/rule-tester.js"; +import rule from "../../src/rules/no-empty-definitions.js"; //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ - plugins: { - markdown, - }, - language: "markdown/gfm", -}); - -ruleTester.run("no-empty-definitions", rule, { +ruleTester("no-empty-definitions", rule, { valid: [ "[foo]: bar", "[foo]: #bar", @@ -31,24 +23,57 @@ ruleTester.run("no-empty-definitions", rule, { "[foo]: ", "[//]: # (This is a comment 1)", "[//]: <> (This is a comment 2)", - "[^note]: This is a footnote.", - "[^note]: ![]()", - "[^note]: [text](url)", - "[^note]:\n Content", - "[^note]:\n > blockquote", - "[^note]: ", + { + code: "[^note]: This is a footnote.", + language: "markdown/gfm", + }, + { + code: "[^note]: ![]()", + language: "markdown/gfm", + }, + { + code: "[^note]: [text](url)", + language: "markdown/gfm", + }, + { + code: "[^note]:\n Content", + language: "markdown/gfm", + }, + { + code: "[^note]:\n > blockquote", + language: "markdown/gfm", + }, + { + code: "[^note]: ", + language: "markdown/gfm", + }, "\\[^note]:", "[\\^note]:", "[^note\\]:", "[^note]\\:", - "[^foo]: ", - "[^foo]: content ", - "[^foo]: content", - "[^foo]: content ", - dedent` - [^foo]: content - `, + { + code: "[^foo]: ", + language: "markdown/gfm", + }, + { + code: "[^foo]: content ", + language: "markdown/gfm", + }, + { + code: "[^foo]: content", + language: "markdown/gfm", + }, + { + code: "[^foo]: content ", + language: "markdown/gfm", + }, + { + code: dedent` + [^foo]: content + `, + language: "markdown/gfm", + }, { code: "[foo]: #", options: [{ allowDefinitions: ["foo"] }], @@ -63,10 +88,12 @@ ruleTester.run("no-empty-definitions", rule, { }, { code: "[^note]:", + language: "markdown/gfm", options: [{ checkFootnoteDefinitions: false }], }, { code: "[^note]:", + language: "markdown/gfm", options: [ { checkFootnoteDefinitions: true, @@ -124,18 +151,22 @@ ruleTester.run("no-empty-definitions", rule, { }, { code: "[^NOTE]:", + language: "markdown/gfm", options: [{ allowFootnoteDefinitions: ["NOTE"] }], }, { code: "[^note]:", + language: "markdown/gfm", options: [{ allowFootnoteDefinitions: ["NOTE"] }], }, { code: "[^NOTE]:", + language: "markdown/gfm", options: [{ allowFootnoteDefinitions: ["note"] }], }, { code: "[^note]:", + language: "markdown/gfm", options: [{ allowFootnoteDefinitions: [" note "] }], }, // This test case is skipped when running on Bun @@ -151,6 +182,7 @@ ruleTester.run("no-empty-definitions", rule, { }, { code: "[^Grüsse]:", + language: "markdown/gfm", options: [{ allowFootnoteDefinitions: ["GRÜẞE"] }], }, ] @@ -235,6 +267,7 @@ ruleTester.run("no-empty-definitions", rule, { }, { code: "[^note]:", + language: "markdown/gfm", errors: [ { messageId: "emptyFootnoteDefinition", @@ -248,6 +281,7 @@ ruleTester.run("no-empty-definitions", rule, { }, { code: "[^Note]:", + language: "markdown/gfm", errors: [ { messageId: "emptyFootnoteDefinition", @@ -261,6 +295,7 @@ ruleTester.run("no-empty-definitions", rule, { }, { code: "[^note]: ", + language: "markdown/gfm", errors: [ { messageId: "emptyFootnoteDefinition", @@ -274,6 +309,7 @@ ruleTester.run("no-empty-definitions", rule, { }, { code: "[^note]:\n", + language: "markdown/gfm", errors: [ { messageId: "emptyFootnoteDefinition", @@ -287,6 +323,7 @@ ruleTester.run("no-empty-definitions", rule, { }, { code: "[^a]:\n[^b]:", + language: "markdown/gfm", errors: [ { messageId: "emptyFootnoteDefinition", @@ -308,6 +345,7 @@ ruleTester.run("no-empty-definitions", rule, { }, { code: "[foo]: #\n[^note]:", + language: "markdown/gfm", errors: [ { messageId: "emptyDefinition", @@ -329,6 +367,7 @@ ruleTester.run("no-empty-definitions", rule, { }, { code: "[foo]: #\n[^note]:", + language: "markdown/gfm", options: [{ checkFootnoteDefinitions: false }], errors: [ { @@ -343,6 +382,7 @@ ruleTester.run("no-empty-definitions", rule, { }, { code: "[^foo]: ", + language: "markdown/gfm", errors: [ { messageId: "emptyFootnoteDefinition", @@ -358,6 +398,7 @@ ruleTester.run("no-empty-definitions", rule, { code: dedent` [^foo]: `, + language: "markdown/gfm", errors: [ { messageId: "emptyFootnoteDefinition", @@ -373,6 +414,7 @@ ruleTester.run("no-empty-definitions", rule, { code: dedent` [^foo]: `, + language: "markdown/gfm", errors: [ { messageId: "emptyFootnoteDefinition", @@ -418,6 +460,7 @@ ruleTester.run("no-empty-definitions", rule, { }, { code: "[^foo]:", + language: "markdown/gfm", options: [ { allowDefinitions: ["foo"], diff --git a/tests/rules/no-empty-images.test.js b/tests/rules/no-empty-images.test.js index 16baa13c..abba9856 100644 --- a/tests/rules/no-empty-images.test.js +++ b/tests/rules/no-empty-images.test.js @@ -7,22 +7,14 @@ // Imports //------------------------------------------------------------------------------ +import ruleTester from "./_utils/rule-tester.js"; import rule from "../../src/rules/no-empty-images.js"; -import markdown from "../../src/index.js"; -import { RuleTester } from "eslint"; //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ - plugins: { - markdown, - }, - language: "markdown/commonmark", -}); - -ruleTester.run("no-empty-images", rule, { +ruleTester("no-empty-images", rule, { valid: ["![foo](bar)", "![foo](#bar)", "![foo](http://bar.com/image.png)"], invalid: [ { diff --git a/tests/rules/no-empty-links.test.js b/tests/rules/no-empty-links.test.js index 75e950da..de0ab0b3 100644 --- a/tests/rules/no-empty-links.test.js +++ b/tests/rules/no-empty-links.test.js @@ -7,22 +7,14 @@ // Imports //------------------------------------------------------------------------------ +import ruleTester from "./_utils/rule-tester.js"; import rule from "../../src/rules/no-empty-links.js"; -import markdown from "../../src/index.js"; -import { RuleTester } from "eslint"; //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ - plugins: { - markdown, - }, - language: "markdown/commonmark", -}); - -ruleTester.run("no-empty-links", rule, { +ruleTester("no-empty-links", rule, { valid: ["[foo](bar)", "[foo](#bar)", "[foo](http://bar.com)"], invalid: [ { diff --git a/tests/rules/no-html.test.js b/tests/rules/no-html.test.js index 9dcaa189..ecd1cf18 100644 --- a/tests/rules/no-html.test.js +++ b/tests/rules/no-html.test.js @@ -7,23 +7,15 @@ // Imports //------------------------------------------------------------------------------ -import rule from "../../src/rules/no-html.js"; -import markdown from "../../src/index.js"; -import { RuleTester } from "eslint"; import dedent from "dedent"; +import ruleTester from "./_utils/rule-tester.js"; +import rule from "../../src/rules/no-html.js"; //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ - plugins: { - markdown, - }, - language: "markdown/commonmark", -}); - -ruleTester.run("no-html", rule, { +ruleTester("no-html", rule, { valid: [ "Hello world!", " 1 < 5", diff --git a/tests/rules/no-invalid-label-refs.test.js b/tests/rules/no-invalid-label-refs.test.js index 231f1490..5cf67a46 100644 --- a/tests/rules/no-invalid-label-refs.test.js +++ b/tests/rules/no-invalid-label-refs.test.js @@ -7,22 +7,14 @@ // Imports //------------------------------------------------------------------------------ +import ruleTester from "./_utils/rule-tester.js"; import rule from "../../src/rules/no-invalid-label-refs.js"; -import markdown from "../../src/index.js"; -import { RuleTester } from "eslint"; //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ - plugins: { - markdown, - }, - language: "markdown/commonmark", -}); - -ruleTester.run("no-invalid-label-refs", rule, { +ruleTester("no-invalid-label-refs", rule, { valid: [ "[*foo*]", "[foo]\n\n[foo]: http://bar.com", diff --git a/tests/rules/no-missing-atx-heading-space.test.js b/tests/rules/no-missing-atx-heading-space.test.js index 7c854f7a..6a6ed093 100644 --- a/tests/rules/no-missing-atx-heading-space.test.js +++ b/tests/rules/no-missing-atx-heading-space.test.js @@ -7,21 +7,9 @@ // Imports //------------------------------------------------------------------------------ -import rule from "../../src/rules/no-missing-atx-heading-space.js"; -import markdown from "../../src/index.js"; -import { RuleTester } from "eslint"; import dedent from "dedent"; - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -const ruleTester = new RuleTester({ - plugins: { - markdown, - }, - language: "markdown/gfm", -}); +import ruleTester from "./_utils/rule-tester.js"; +import rule from "../../src/rules/no-missing-atx-heading-space.js"; //------------------------------------------------------------------------------ // Valid Test Cases @@ -44,7 +32,7 @@ const validHeadings = [ dedent`# Heading 1 ## Heading 2 - + ### Heading 3`, // 3. Variations on spacing @@ -77,7 +65,7 @@ const validHeadings = [ // 7.4 Code block after paragraph dedent`This is a paragraph followed by code. - + \`\`\` #This is in a code block \`\`\``, @@ -159,7 +147,7 @@ const validHeadings = [ code: dedent`# Heading 1 # ## Heading 2 ## - + ### Heading 3 ###`, options: [{ checkClosedHeadings: true }], }, @@ -333,12 +321,12 @@ const invalidTests = [ code: dedent`# Heading 1 ##Heading 2 - + ### Heading 3`, output: dedent`# Heading 1 ## Heading 2 - + ### Heading 3`, errors: [ { @@ -510,18 +498,18 @@ const invalidTests = [ // 5.2 Multiple incorrect headings in one file { code: dedent`#First heading - + Some text - + ##Second heading - + ###Third heading`, output: dedent`# First heading - + Some text - + ## Second heading - + ### Third heading`, errors: [ { @@ -889,12 +877,12 @@ const invalidTests = [ code: dedent`# Heading 1 ## Heading 2## - + ### Heading 3 ###`, output: dedent`# Heading 1 ## Heading 2 ## - + ### Heading 3 ###`, options: [{ checkClosedHeadings: true }], errors: [ @@ -1005,18 +993,18 @@ const invalidTests = [ }, { code: dedent`# First heading# - + Some text - + ## Second heading## - + ### Third heading###`, output: dedent`# First heading # - + Some text - + ## Second heading ## - + ### Third heading ###`, options: [{ checkClosedHeadings: true }], errors: [ @@ -1170,7 +1158,7 @@ const invalidTests = [ }, ]; -ruleTester.run("no-missing-atx-heading-space", rule, { +ruleTester("no-missing-atx-heading-space", rule, { valid: validHeadings, invalid: invalidTests, }); diff --git a/tests/rules/no-missing-label-refs.test.js b/tests/rules/no-missing-label-refs.test.js index 07ce97e2..bd1788c8 100644 --- a/tests/rules/no-missing-label-refs.test.js +++ b/tests/rules/no-missing-label-refs.test.js @@ -16,6 +16,8 @@ import dedent from "dedent"; // Tests //------------------------------------------------------------------------------ +// TODO + const ruleTester = new RuleTester({ plugins: { markdown, diff --git a/tests/rules/no-missing-link-fragments.test.js b/tests/rules/no-missing-link-fragments.test.js index 573a814a..19e6a0a6 100644 --- a/tests/rules/no-missing-link-fragments.test.js +++ b/tests/rules/no-missing-link-fragments.test.js @@ -7,23 +7,17 @@ // Imports //------------------------------------------------------------------------------ -import rule from "../../src/rules/no-missing-link-fragments.js"; -import markdown from "../../src/index.js"; -import { Linter, RuleTester } from "eslint"; import dedent from "dedent"; +import { Linter } from "eslint"; +import ruleTester from "./_utils/rule-tester.js"; +import markdown from "../../src/index.js"; +import rule from "../../src/rules/no-missing-link-fragments.js"; //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ - plugins: { - markdown, - }, - language: "markdown/gfm", -}); - -ruleTester.run("no-missing-link-fragments", rule, { +ruleTester("no-missing-link-fragments", rule, { valid: [ // Basic heading match with `Link` node dedent` diff --git a/tests/rules/no-multiple-h1.test.js b/tests/rules/no-multiple-h1.test.js index e86cdfd7..e45dcdf2 100644 --- a/tests/rules/no-multiple-h1.test.js +++ b/tests/rules/no-multiple-h1.test.js @@ -16,6 +16,8 @@ import dedent from "dedent"; // Tests //------------------------------------------------------------------------------ +// TODO + const ruleTester = new RuleTester({ plugins: { markdown, diff --git a/tests/rules/no-reference-like-urls.test.js b/tests/rules/no-reference-like-urls.test.js index 095938f7..2a9b8e29 100644 --- a/tests/rules/no-reference-like-urls.test.js +++ b/tests/rules/no-reference-like-urls.test.js @@ -16,6 +16,8 @@ import dedent from "dedent"; // Tests //------------------------------------------------------------------------------ +// TODO + const ruleTester = new RuleTester({ plugins: { markdown, @@ -992,13 +994,13 @@ ruleTester.run("no-reference-like-urls", rule, { code: dedent` [Click Me ](test) - + [test]: https://abc.com `, output: dedent` [Click Me ][test] - + [test]: https://abc.com `, errors: [ @@ -1016,13 +1018,13 @@ ruleTester.run("no-reference-like-urls", rule, { code: dedent` ![Click Me ](test) - + [test]: https://abc.com `, output: dedent` ![Click Me ][test] - + [test]: https://abc.com `, errors: [ @@ -1320,12 +1322,12 @@ ruleTester.run("no-reference-like-urls", rule, { { code: dedent` [link](GRÜẞE) - + [Grüsse]: https://example.com/ `, output: dedent` [link][GRÜẞE] - + [Grüsse]: https://example.com/ `, errors: [ diff --git a/tests/rules/no-reversed-media-syntax.test.js b/tests/rules/no-reversed-media-syntax.test.js index f3de3b8e..35cfa065 100644 --- a/tests/rules/no-reversed-media-syntax.test.js +++ b/tests/rules/no-reversed-media-syntax.test.js @@ -16,6 +16,8 @@ import dedent from "dedent"; // Tests //------------------------------------------------------------------------------ +// TODO + const ruleTester = new RuleTester({ plugins: { markdown, diff --git a/tests/rules/no-space-in-emphasis.test.js b/tests/rules/no-space-in-emphasis.test.js index 72ef6730..f86167de 100644 --- a/tests/rules/no-space-in-emphasis.test.js +++ b/tests/rules/no-space-in-emphasis.test.js @@ -7,23 +7,15 @@ // Imports //------------------------------------------------------------------------------ -import rule from "../../src/rules/no-space-in-emphasis.js"; -import markdown from "../../src/index.js"; -import { RuleTester } from "eslint"; import dedent from "dedent"; +import ruleTester from "./_utils/rule-tester.js"; +import rule from "../../src/rules/no-space-in-emphasis.js"; //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ - plugins: { - markdown, - }, - language: "markdown/gfm", -}); - -ruleTester.run("no-space-in-emphasis", rule, { +ruleTester("no-space-in-emphasis", rule, { valid: [ "Line with *Normal emphasis*", "Line with **Normal strong**", @@ -31,9 +23,18 @@ ruleTester.run("no-space-in-emphasis", rule, { "Line with _Normal emphasis_", "Line with __Normal strong__", "Line with ___Normal strong and emphasis___", - "Line with ~Normal strikethrough~", - "Line with ~~Normal strikethrough~~", - "Line with ~~*strikethrough and emphasis*~~", + { + code: "Line with ~Normal strikethrough~", + language: "markdown/gfm", + }, + { + code: "Line with ~~Normal strikethrough~~", + language: "markdown/gfm", + }, + { + code: "Line with ~~*strikethrough and emphasis*~~", + language: "markdown/gfm", + }, "But not with escaped\\* asterisks\\* \\_and \\_underscores.", "But not with escaped\\~ tildes\\~ either.", "* Emphasis* with left space is recognized as a list", @@ -75,8 +76,14 @@ ruleTester.run("no-space-in-emphasis", rule, { "Mixed `code*span` scenarios are *also* okay.", "Mixed `code*span` scenarios are _also_ okay.", "Mixed `code_span` scenarios are *also* okay.", - "Text ~ strikethrough ~ with spaces", - "Text ~~ strikethrough ~~ with spaces", + { + code: "Text ~ strikethrough ~ with spaces", + language: "markdown/gfm", + }, + { + code: "Text ~~ strikethrough ~~ with spaces", + language: "markdown/gfm", + }, "[Link](under_score) followed by _underscore_", "[Link](un_der_score) followed by _underscore_", "[Link](un_der_sco_re) followed by _underscore_", @@ -108,8 +115,14 @@ ruleTester.run("no-space-in-emphasis", rule, { "text _underscore_*star* text _underscore_**star** text", "text _underscore_**star** text _underscore_*star* text", "text _underscore_**star** text _underscore_**star** text", - "text ~~strike~~*star* text ~strike~**star** text", - "text ~~strike~~_underscore_ text ~strike~__underscore__ text", + { + code: "text ~~strike~~*star* text ~strike~**star** text", + language: "markdown/gfm", + }, + { + code: "text ~~strike~~_underscore_ text ~strike~__underscore__ text", + language: "markdown/gfm", + }, dedent` > * List with *emphasis* in blockquote > @@ -182,35 +195,47 @@ ruleTester.run("no-space-in-emphasis", rule, { **text** *** `, - dedent` - | Table | Table | - | ----- | ----- | - | star | x * y | - | under | x _ y | - `, - dedent` - | Table | Table | - | ----- | ----- | - | star | x * y | - | star | x * y | - | under | x _ y | - | under | x _ y | - `, - dedent` - | Table | Table | - | ----- | ------------------------- | - | star | text *text* text | - | under | text _text_ text | - | strike| text ~text~ text | - `, - dedent` - | Table | Table | - | ----- | ----- | - | x * y | x * y | - | x** y | x** y | - | x _ y | x _ y | - | x__ y | x__ y | - `, + { + code: dedent` + | Table | Table | + | ----- | ----- | + | star | x * y | + | under | x _ y | + `, + language: "markdown/gfm", + }, + { + code: dedent` + | Table | Table | + | ----- | ----- | + | star | x * y | + | star | x * y | + | under | x _ y | + | under | x _ y | + `, + language: "markdown/gfm", + }, + { + code: dedent` + | Table | Table | + | ----- | ------------------------- | + | star | text *text* text | + | under | text _text_ text | + | strike| text ~text~ text | + `, + language: "markdown/gfm", + }, + { + code: dedent` + | Table | Table | + | ----- | ----- | + | x * y | x * y | + | x** y | x** y | + | x _ y | x _ y | + | x__ y | x__ y | + `, + language: "markdown/gfm", + }, dedent` \`\`\`yaml /* autogenerated */ # YAML... @@ -1248,6 +1273,7 @@ ruleTester.run("no-space-in-emphasis", rule, { | under | text _text_ text | | under | text _text_ text | `, + language: "markdown/gfm", errors: [ { messageId: "spaceInEmphasis", @@ -1282,6 +1308,7 @@ ruleTester.run("no-space-in-emphasis", rule, { { code: "Broken ~ strikethrough ~ with spaces", output: "Broken ~strikethrough~ with spaces", + language: "markdown/gfm", options: [{ checkStrikethrough: true }], errors: [ { @@ -1303,6 +1330,7 @@ ruleTester.run("no-space-in-emphasis", rule, { { code: "Broken ~~ strikethrough ~~ with spaces", output: "Broken ~~strikethrough~~ with spaces", + language: "markdown/gfm", options: [{ checkStrikethrough: true }], errors: [ { @@ -1324,6 +1352,7 @@ ruleTester.run("no-space-in-emphasis", rule, { { code: "Mixed ~~ok strikethrough~~ and ~~ broken strikethrough ~~", output: "Mixed ~~ok strikethrough~~ and ~~broken strikethrough~~", + language: "markdown/gfm", options: [{ checkStrikethrough: true }], errors: [ { @@ -1345,6 +1374,7 @@ ruleTester.run("no-space-in-emphasis", rule, { { code: "Mixed ~ strikethrough ~ and * emphasis * with spaces", output: "Mixed ~strikethrough~ and *emphasis* with spaces", + language: "markdown/gfm", options: [{ checkStrikethrough: true }], errors: [ { @@ -1380,6 +1410,7 @@ ruleTester.run("no-space-in-emphasis", rule, { { code: "Mixed ~ok strikethrough~ and ~ broken strikethrough ~", output: "Mixed ~ok strikethrough~ and ~broken strikethrough~", + language: "markdown/gfm", options: [{ checkStrikethrough: true }], errors: [ { @@ -1401,6 +1432,7 @@ ruleTester.run("no-space-in-emphasis", rule, { { code: "# Broken ~ strikethrough ~ with spaces", output: "# Broken ~strikethrough~ with spaces", + language: "markdown/gfm", options: [{ checkStrikethrough: true }], errors: [ { @@ -1432,6 +1464,7 @@ ruleTester.run("no-space-in-emphasis", rule, { | strike | text ~text~ text | | strike | text ~text~ text | `, + language: "markdown/gfm", options: [{ checkStrikethrough: true }], errors: [ { diff --git a/tests/rules/no-unused-definitions.test.js b/tests/rules/no-unused-definitions.test.js index f8ff8096..cfe53195 100644 --- a/tests/rules/no-unused-definitions.test.js +++ b/tests/rules/no-unused-definitions.test.js @@ -7,22 +7,14 @@ // Imports //------------------------------------------------------------------------------ +import ruleTester from "./_utils/rule-tester.js"; import rule from "../../src/rules/no-unused-definitions.js"; -import markdown from "../../src/index.js"; -import { RuleTester } from "eslint"; //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ - plugins: { - markdown, - }, - language: "markdown/gfm", -}); - -ruleTester.run("no-unused-definitions", rule, { +ruleTester("no-unused-definitions", rule, { valid: [ "", " ", @@ -48,7 +40,7 @@ ruleTester.run("no-unused-definitions", rule, { [Mercury]: https://example.com/venus/ `, // case insensitive ` -[Mercury][mercury] +[Mercury][mercury] [mercury]: https://example.com/mercury/ [ mercury ]: https://example.com/venus/ @@ -56,7 +48,7 @@ ruleTester.run("no-unused-definitions", rule, { ` ![Venus Image][venus] -[venus]: https://example.com/venus.jpg +[venus]: https://example.com/venus.jpg `, // Image - full ` ![venus][] @@ -68,34 +60,46 @@ ruleTester.run("no-unused-definitions", rule, { [venus]: https://example.com/venus.jpg `, // Image - shortcut - ` + { + code: ` Mercury[^mercury] [^mercury]: Hello, Mercury! `, - ` + language: "markdown/gfm", + }, + { + code: ` Mercury[^mercury] [^mercury]: Hello, Mercury! [^Mercury]: Hello, Venus! `, // case insensitive - ` + language: "markdown/gfm", + }, + { + code: ` Mercury[^mercury] [^mercury]: https://example.com/mercury/ [ ^mercury ]: https://example.com/venus/ `, // with extra spaces + language: "markdown/gfm", + }, ` [//]: # (This is a comment 1) [//]: <> (This is a comment 2) `, - ` + { + code: ` [Alpha][alpha] and [Alpha][^alpha] [alpha]: bravo [^alpha]: bravo `, + language: "markdown/gfm", + }, { code: ` [mercury]: https://example.com/mercury/ @@ -113,6 +117,7 @@ Mercury[^mercury] [^mercury]: Hello, Mercury! [^mercury]: Hello, Venus! `, + language: "markdown/gfm", options: [ { allowFootnoteDefinitions: ["mercury"], @@ -169,6 +174,7 @@ Mercury[^mercury] }, { code: "[^MERCURY]: Hello, Mercury!", + language: "markdown/gfm", options: [ { allowFootnoteDefinitions: ["MERCURY"], @@ -177,6 +183,7 @@ Mercury[^mercury] }, { code: "[^mercury]: Hello, Mercury!", + language: "markdown/gfm", options: [ { allowFootnoteDefinitions: ["MERCURY"], @@ -185,6 +192,7 @@ Mercury[^mercury] }, { code: "[^MERCURY]: Hello, Mercury!", + language: "markdown/gfm", options: [ { allowFootnoteDefinitions: ["mercury"], @@ -193,6 +201,7 @@ Mercury[^mercury] }, { code: "[^mercury]: Hello, Mercury!", + language: "markdown/gfm", options: [ { allowFootnoteDefinitions: [" mercury "], @@ -201,6 +210,7 @@ Mercury[^mercury] }, { code: "[^mercury]: Hello, Mercury!", + language: "markdown/gfm", options: [ { checkFootnoteDefinitions: false, @@ -209,6 +219,7 @@ Mercury[^mercury] }, { code: "[^mercury]: Hello, Mercury!", + language: "markdown/gfm", options: [ { checkFootnoteDefinitions: true, @@ -229,6 +240,7 @@ Mercury[^mercury] }, { code: "[^Grüsse]: Grüsse", + language: "markdown/gfm", options: [ { allowFootnoteDefinitions: ["GRÜẞE"], @@ -349,6 +361,7 @@ Mercury[^mercury] code: ` [^mercury]: Hello, Mercury! `, + language: "markdown/gfm", errors: [ { messageId: "unusedFootnoteDefinition", @@ -365,6 +378,7 @@ Mercury[^mercury] code: ` [^Mercury]: Hello, Mercury! `, + language: "markdown/gfm", errors: [ { messageId: "unusedFootnoteDefinition", @@ -383,6 +397,7 @@ Mercury[^mercury] [^mercury]: Hello, Venus! [^mercury]: Hello, Earth! `, + language: "markdown/gfm", errors: [ { messageId: "unusedFootnoteDefinition", @@ -415,6 +430,7 @@ Mercury[^mercury] code: ` [^mercury]: Hello, Mercury! `, + language: "markdown/gfm", options: [ { allowDefinitions: ["mercury"], @@ -438,6 +454,7 @@ Mercury[^mercury] code: ` [^mercury]: Hello, Mercury! `, + language: "markdown/gfm", options: [ { checkFootnoteDefinitions: true, @@ -458,7 +475,7 @@ Mercury[^mercury] { code: ` -Hello, [Mercury][mercury]! +Hello, [Mercury][mercury]! I am living on [Earth][earth] and I am going to [Mars][mars]. [mercury]: https://example.com/mercury/ diff --git a/tests/rules/require-alt-text.test.js b/tests/rules/require-alt-text.test.js index 85d7c9b2..387681ec 100644 --- a/tests/rules/require-alt-text.test.js +++ b/tests/rules/require-alt-text.test.js @@ -16,6 +16,8 @@ import dedent from "dedent"; // Tests //------------------------------------------------------------------------------ +// TODO + const ruleTester = new RuleTester({ plugins: { markdown, diff --git a/tests/rules/table-column-count.test.js b/tests/rules/table-column-count.test.js index c6fd71c2..ef754484 100644 --- a/tests/rules/table-column-count.test.js +++ b/tests/rules/table-column-count.test.js @@ -7,53 +7,61 @@ // Imports //------------------------------------------------------------------------------ -import rule from "../../src/rules/table-column-count.js"; -import markdown from "../../src/index.js"; -import { RuleTester } from "eslint"; import dedent from "dedent"; +import ruleTester from "./_utils/rule-tester.js"; +import rule from "../../src/rules/table-column-count.js"; //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ - plugins: { - markdown, - }, - language: "markdown/gfm", -}); - -ruleTester.run("table-column-count", rule, { +ruleTester("table-column-count", rule, { valid: [ - dedent` + { + code: dedent` | Header | Header | | ------ | ------ | | Cell | Cell | | Cell | Cell | `, - dedent` + language: "markdown/gfm", + }, + { + code: dedent` | Header | Header | Header | | ------ | ------ | ------ | | Cell | Cell | | Cell | | `, - dedent` + language: "markdown/gfm", + }, + { + code: dedent` | A | B | |---|---| | | | | C | | `, + language: "markdown/gfm", + }, `Just some text. | not a table |`, - dedent` + { + code: dedent` | Header | Header | | ------ | ------ | ----- | | Cell | Cell | `, - dedent` + language: "markdown/gfm", + }, + { + code: dedent` | Header | Header | | ------ | ------ | `, - dedent` + language: "markdown/gfm", + }, + { + code: dedent` Some text before. | H1 | H2 | @@ -62,48 +70,67 @@ ruleTester.run("table-column-count", rule, { Some text after. `, - dedent` + language: "markdown/gfm", + }, + { + code: dedent` | Valid | Table | | ----- | ----- | | Row | Here | `, - dedent` + language: "markdown/gfm", + }, + { + code: dedent` | abc | defghi | :-: | -----------: bar | baz `, - dedent` + language: "markdown/gfm", + }, + { + code: dedent` | f|oo | | ------ | | b \`|\` az | | b **|** im | `, - dedent` + language: "markdown/gfm", + }, + { + code: dedent` | abc | def | | --- | --- | | bar | baz | > bar `, - dedent` + language: "markdown/gfm", + }, + { + code: dedent` | abc | def | | --- | --- | `, + language: "markdown/gfm", + }, { code: dedent` | Header | Header | | ------ | ------ | - | Cell | Cell | - | Cell | Cell | + | Cell | Cell | + | Cell | Cell | `, + language: "markdown/gfm", options: [{ checkMissingCells: true }], }, { code: dedent` | Header | Header | | ------ | ------ | - | Cell | | - | Cell | Cell | + | Cell | | + | Cell | Cell | `, + language: "markdown/gfm", options: [{ checkMissingCells: true }], }, ], @@ -115,6 +142,7 @@ ruleTester.run("table-column-count", rule, { | ----- | ----- | | R1C1 | R1C2 | R2C3 | `, + language: "markdown/gfm", errors: [ { messageId: "extraCells", @@ -132,6 +160,7 @@ ruleTester.run("table-column-count", rule, { | ----- | ----- | | R1C1 | R1C2 | R2C3 | R3C4 | `, + language: "markdown/gfm", errors: [ { messageId: "extraCells", @@ -149,6 +178,7 @@ ruleTester.run("table-column-count", rule, { | - | | 1 | 2 | `, + language: "markdown/gfm", errors: [ { messageId: "extraCells", @@ -171,6 +201,7 @@ ruleTester.run("table-column-count", rule, { Some concluding text. `, + language: "markdown/gfm", errors: [ { messageId: "extraCells", @@ -189,6 +220,7 @@ ruleTester.run("table-column-count", rule, { bar | baz bar | baz | bad `, + language: "markdown/gfm", errors: [ { messageId: "extraCells", @@ -207,6 +239,7 @@ ruleTester.run("table-column-count", rule, { | bar | baz | Extra | > This is a blockquote after `, + language: "markdown/gfm", errors: [ { messageId: "extraCells", @@ -225,6 +258,7 @@ ruleTester.run("table-column-count", rule, { | bar | baz | Extra1 | | bar | baz | Extra2 | `, + language: "markdown/gfm", errors: [ { messageId: "extraCells", @@ -252,6 +286,7 @@ ruleTester.run("table-column-count", rule, { | bar | baz | | bar | baz | Extra2 | `, + language: "markdown/gfm", errors: [ { messageId: "extraCells", @@ -277,6 +312,7 @@ ruleTester.run("table-column-count", rule, { | ------ | ------ | ------ | | Cell | Cell | `, + language: "markdown/gfm", options: [{ checkMissingCells: true }], errors: [ { @@ -296,6 +332,7 @@ ruleTester.run("table-column-count", rule, { | Cell | | Cell | | Cell | Cell | `, + language: "markdown/gfm", options: [{ checkMissingCells: true }], errors: [ { @@ -316,6 +353,7 @@ ruleTester.run("table-column-count", rule, { | Cell | Cell | | Cell | Cell | Cell | `, + language: "markdown/gfm", options: [{ checkMissingCells: true }], errors: [ { @@ -344,6 +382,7 @@ ruleTester.run("table-column-count", rule, { | Cell | | Cell | Cell | `, + language: "markdown/gfm", options: [{ checkMissingCells: true }], errors: [ { @@ -372,6 +411,7 @@ ruleTester.run("table-column-count", rule, { | Cell | | Cell | Cell | `, + language: "markdown/gfm", options: [{ checkMissingCells: true }], errors: [ { @@ -400,6 +440,7 @@ ruleTester.run("table-column-count", rule, { | Cell | Cell | Cell | Cell | | Cell | `, + language: "markdown/gfm", options: [{ checkMissingCells: true }], errors: [ { From ab3f90c333aaf56caa63c7ca44038a5bb62942dd Mon Sep 17 00:00:00 2001 From: lumir Date: Tue, 4 Aug 2026 07:22:29 +0900 Subject: [PATCH 03/22] wip --- package.json | 2 +- src/language/markdown-language.js | 9 ++++---- src/types.ts | 37 ++++++++----------------------- tests/rules/_utils/rule-tester.js | 6 ++--- 4 files changed, 17 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index 99b20852..0a766f50 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ }, "devDependencies": { "@arethetypeswrong/cli": "^0.18.3", - "@eslint-markdown/parser": "^0.0.0-canary.0", + "@eslint-markdown/parser": "^0.0.0-canary.1", "@eslint/js": "^10.0.1", "@eslint/json": "^2.0.1", "@types/mdast": "^4.0.4", diff --git a/src/language/markdown-language.js b/src/language/markdown-language.js index f988c130..41338398 100644 --- a/src/language/markdown-language.js +++ b/src/language/markdown-language.js @@ -26,7 +26,7 @@ import { math } from "micromark-extension-math"; * @import { Language, File, ParseResult, OkParseResult } from "@eslint/core"; * @import { Root } from "mdast"; * @import { Options } from "mdast-util-from-markdown"; - * @import { MarkdownLanguageOptions, MarkdownLanguageContext } from "../types.js"; + * @import { MarkdownLanguageOptions, MarkdownLanguageContext, MdastParser } from "../types.js"; * @typedef {Options['extensions']} Extensions * @typedef {Options['mdastExtensions']} MdastExtensions * @typedef {"commonmark"|"gfm"} ParserMode @@ -219,14 +219,13 @@ export class MarkdownLanguage { * problem that ESLint identified just like any other. */ try { - const parser = context?.languageOptions?.parser ?? fromMarkdown; + const parser = context?.languageOptions?.parser; /** @type {Root} */ let root; - if (parser !== fromMarkdown) { - // @ts-expect-error -- TODO - root = parser(text, { + if (parser) { + root = /** @type {MdastParser} */ (parser).parse(text, { mode: this.#mode, ...context?.languageOptions, ...context?.languageOptions?.parserOptions, diff --git a/src/types.ts b/src/types.ts index a0ee9520..4eaa0303 100644 --- a/src/types.ts +++ b/src/types.ts @@ -119,33 +119,15 @@ export interface JsonData extends Data {} // Exports: Language and Source Code //------------------------------------------------------------------------------ -type NonMdastParser = ObjectMetaProperties & - ( - | { - parse(text: string, options?: any): unknown; - } - | { - parseForESLint( - text: string, - options?: any, - ): { - ast: unknown; - }; - } - ); - -type MdastParser = ObjectMetaProperties & { - parse(text: string, options?: any): Root; +export type NonMdastParser = ObjectMetaProperties & { + parse(text: string, options?: any): unknown; +}; - parseForESLint( - text: string, - options?: any, - ): { - ast: Root; - }; +export type MdastParser = ObjectMetaProperties & { + parse(text: string, options?: any): Root; }; -type Parser = NonMdastParser | MdastParser; +export type Parser = NonMdastParser | MdastParser; /** * Language options provided for Markdown files. @@ -164,15 +146,14 @@ export interface MarkdownLanguageOptions extends LanguageOptions { math?: boolean; /** - * An object containing a `parse()` or `parseForESLint()` method. - * If not configured, the default ESLint Markdown parser - * (`mdast-util-from-markdown`) will be used. + * An object containing a `parse()` method. If not configured, + * the default ESLint Markdown parser (`mdast-util-from-markdown`) will be used. */ parser?: Parser; /** * An object specifying additional options that are passed directly to the - * `parser()` method on the parser. The available options are parser-dependent. + * `parse()` method on the parser. The available options are parser-dependent. */ parserOptions?: { [key: string]: any; diff --git a/tests/rules/_utils/rule-tester.js b/tests/rules/_utils/rule-tester.js index c5967a9c..ae832a38 100644 --- a/tests/rules/_utils/rule-tester.js +++ b/tests/rules/_utils/rule-tester.js @@ -7,7 +7,7 @@ // Imports // -------------------------------------------------------------------------------- -import { parse } from "@eslint-markdown/parser"; +import parser from "@eslint-markdown/parser"; import { RuleTester } from "eslint"; import markdown from "../../../src/index.js"; @@ -52,7 +52,7 @@ const ruleTesterRustCommonMark = new RuleTester({ }, language: "markdown/commonmark", languageOptions: { - parser: parse, + parser, }, }); @@ -65,7 +65,7 @@ const ruleTesterRustGFM = new RuleTester({ }, language: "markdown/gfm", languageOptions: { - parser: parse, + parser, }, }); From 2f364fa85f1717aacb8e982f291c3be926aba611 Mon Sep 17 00:00:00 2001 From: lumir Date: Tue, 4 Aug 2026 08:18:11 +0900 Subject: [PATCH 04/22] wip --- src/types.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/types.ts b/src/types.ts index 4eaa0303..6020fa64 100644 --- a/src/types.ts +++ b/src/types.ts @@ -127,7 +127,7 @@ export type MdastParser = ObjectMetaProperties & { parse(text: string, options?: any): Root; }; -export type Parser = NonMdastParser | MdastParser; +export type MarkdownParser = NonMdastParser | MdastParser; /** * Language options provided for Markdown files. @@ -149,15 +149,13 @@ export interface MarkdownLanguageOptions extends LanguageOptions { * An object containing a `parse()` method. If not configured, * the default ESLint Markdown parser (`mdast-util-from-markdown`) will be used. */ - parser?: Parser; + parser?: MarkdownParser; /** * An object specifying additional options that are passed directly to the * `parse()` method on the parser. The available options are parser-dependent. */ - parserOptions?: { - [key: string]: any; - }; + parserOptions?: Record; } /** From 2f31eec4f7b720eb46879d64fa121ea6d828598e Mon Sep 17 00:00:00 2001 From: lumir Date: Tue, 4 Aug 2026 08:25:05 +0900 Subject: [PATCH 05/22] wip --- src/language/markdown-language.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/language/markdown-language.js b/src/language/markdown-language.js index 41338398..a0b0131e 100644 --- a/src/language/markdown-language.js +++ b/src/language/markdown-language.js @@ -227,7 +227,8 @@ export class MarkdownLanguage { if (parser) { root = /** @type {MdastParser} */ (parser).parse(text, { mode: this.#mode, - ...context?.languageOptions, + frontmatter: context?.languageOptions?.frontmatter, + math: context?.languageOptions?.math, ...context?.languageOptions?.parserOptions, // TODO: consider https://github.com/eslint/eslint/pull/20926. }); From 45c4852b647256052a91096a2037f238c19ff7d8 Mon Sep 17 00:00:00 2001 From: lumir Date: Thu, 13 Aug 2026 17:14:57 +0900 Subject: [PATCH 06/22] wip --- package.json | 2 +- src/language/markdown-language.js | 11 +++++------ src/types.ts | 28 ++++++++++++++-------------- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 0a766f50..175e5b98 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ }, "devDependencies": { "@arethetypeswrong/cli": "^0.18.3", - "@eslint-markdown/parser": "^0.0.0-canary.1", + "@eslint-markdown/parser": "^0.0.0-canary.2", "@eslint/js": "^10.0.1", "@eslint/json": "^2.0.1", "@types/mdast": "^4.0.4", diff --git a/src/language/markdown-language.js b/src/language/markdown-language.js index a0b0131e..98113970 100644 --- a/src/language/markdown-language.js +++ b/src/language/markdown-language.js @@ -26,7 +26,7 @@ import { math } from "micromark-extension-math"; * @import { Language, File, ParseResult, OkParseResult } from "@eslint/core"; * @import { Root } from "mdast"; * @import { Options } from "mdast-util-from-markdown"; - * @import { MarkdownLanguageOptions, MarkdownLanguageContext, MdastParser } from "../types.js"; + * @import { MarkdownLanguageOptions, MarkdownLanguageContext, MarkdownParser } from "../types.js"; * @typedef {Options['extensions']} Extensions * @typedef {Options['mdastExtensions']} MdastExtensions * @typedef {"commonmark"|"gfm"} ParserMode @@ -219,18 +219,17 @@ export class MarkdownLanguage { * problem that ESLint identified just like any other. */ try { - const parser = context?.languageOptions?.parser; - /** @type {Root} */ let root; + // TODO: use destructuring and consider `__proto__` and rest properties? + const parser = context?.languageOptions?.parser; + if (parser) { - root = /** @type {MdastParser} */ (parser).parse(text, { + root = parser.parse(text, { mode: this.#mode, frontmatter: context?.languageOptions?.frontmatter, math: context?.languageOptions?.math, - ...context?.languageOptions?.parserOptions, - // TODO: consider https://github.com/eslint/eslint/pull/20926. }); } else { root = fromMarkdown( diff --git a/src/types.ts b/src/types.ts index 6020fa64..eb5d6f89 100644 --- a/src/types.ts +++ b/src/types.ts @@ -119,16 +119,22 @@ export interface JsonData extends Data {} // Exports: Language and Source Code //------------------------------------------------------------------------------ -export type NonMdastParser = ObjectMetaProperties & { - parse(text: string, options?: any): unknown; -}; - -export type MdastParser = ObjectMetaProperties & { - parse(text: string, options?: any): Root; +/** + * A parser that converts Markdown source text into an mdast syntax tree. + */ +export type MarkdownParser = ObjectMetaProperties & { + /** + * Parses Markdown source text into an mdast syntax tree. + * @param text The Markdown source text to parse. + * @param options The parser-specific options. + * @returns The root of the mdast syntax tree. + */ + parse( + text: string, + options?: Omit, + ): Root; }; -export type MarkdownParser = NonMdastParser | MdastParser; - /** * Language options provided for Markdown files. */ @@ -150,12 +156,6 @@ export interface MarkdownLanguageOptions extends LanguageOptions { * the default ESLint Markdown parser (`mdast-util-from-markdown`) will be used. */ parser?: MarkdownParser; - - /** - * An object specifying additional options that are passed directly to the - * `parse()` method on the parser. The available options are parser-dependent. - */ - parserOptions?: Record; } /** From e49516df98beffe442c4a0b82c8c4ef5cc97e786 Mon Sep 17 00:00:00 2001 From: lumir Date: Mon, 17 Aug 2026 21:02:47 +0900 Subject: [PATCH 07/22] wip --- src/language/markdown-language.js | 46 +++++++++++++++++-------------- src/types.ts | 14 +++++++++- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/language/markdown-language.js b/src/language/markdown-language.js index 98113970..4df27e8c 100644 --- a/src/language/markdown-language.js +++ b/src/language/markdown-language.js @@ -145,7 +145,14 @@ export class MarkdownLanguage { defaultLanguageOptions = { frontmatter: false, math: false, - // TODO + parser: { + parse(text, { mode, ...languageOptions }) { + return fromMarkdown( + text, + createParserOptions(mode, languageOptions), + ); + }, + }, }; /** @@ -199,7 +206,16 @@ export class MarkdownLanguage { ); } - // TODO + // `parser` option validation + const parserOption = languageOptions?.parser; + + if (parserOption !== undefined && typeof parserOption !== "object") { + throw new Error( + `Invalid language option value \`${parserOption}\` for parser. Expected an object.`, + ); + + // TODO: stricter validation for parser object shape (e.g., check for `parse` method) + } } /** @@ -211,6 +227,10 @@ export class MarkdownLanguage { parse(file, context) { // Note: BOM already removed const text = /** @type {string} */ (file.body); + const languageOptions = { + ...this.defaultLanguageOptions, + ...context?.languageOptions, + }; /* * Check for parsing errors first. If there's a parsing error, nothing @@ -219,24 +239,10 @@ export class MarkdownLanguage { * problem that ESLint identified just like any other. */ try { - /** @type {Root} */ - let root; - - // TODO: use destructuring and consider `__proto__` and rest properties? - const parser = context?.languageOptions?.parser; - - if (parser) { - root = parser.parse(text, { - mode: this.#mode, - frontmatter: context?.languageOptions?.frontmatter, - math: context?.languageOptions?.math, - }); - } else { - root = fromMarkdown( - text, - createParserOptions(this.#mode, context?.languageOptions), - ); - } + const root = languageOptions.parser.parse(text, { + mode: this.#mode, + ...languageOptions, + }); return { ok: true, diff --git a/src/types.ts b/src/types.ts index e5380dc5..3c54a322 100644 --- a/src/types.ts +++ b/src/types.ts @@ -134,6 +134,12 @@ declare module "mdast" { // Exports: Language and Source Code //------------------------------------------------------------------------------ +/** + * The mode of the Markdown parser to use. + * @default "commonmark" + */ +export type MarkdownMode = "commonmark" | "gfm"; + /** * A parser that converts Markdown source text into an mdast syntax tree. */ @@ -146,7 +152,13 @@ export type MarkdownParser = ObjectMetaProperties & { */ parse( text: string, - options?: Omit, + options?: Omit & { + /** + * The mode of the Markdown parser to use. + * @default "commonmark" + */ + mode?: MarkdownMode; + }, ): Root; }; From 965cd5877086e2f3cbd52d787167d1ba22f44cf7 Mon Sep 17 00:00:00 2001 From: lumir Date: Sat, 22 Aug 2026 01:21:48 +0900 Subject: [PATCH 08/22] wip --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 175e5b98..283707fa 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ }, "devDependencies": { "@arethetypeswrong/cli": "^0.18.3", - "@eslint-markdown/parser": "^0.0.0-canary.2", + "@eslint-markdown/parser": "^0.0.0-canary.3", "@eslint/js": "^10.0.1", "@eslint/json": "^2.0.1", "@types/mdast": "^4.0.4", From 37918d824e9dcf63a0d8903dc24d5c650f4ef7b2 Mon Sep 17 00:00:00 2001 From: lumir Date: Sat, 22 Aug 2026 01:29:11 +0900 Subject: [PATCH 09/22] wip: add more compat tests --- tests/rules/heading-increment.test.js | 16 +++------------- tests/rules/no-missing-label-refs.test.js | 18 +++++------------- 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/tests/rules/heading-increment.test.js b/tests/rules/heading-increment.test.js index 460f7a8e..640b2590 100644 --- a/tests/rules/heading-increment.test.js +++ b/tests/rules/heading-increment.test.js @@ -7,25 +7,15 @@ // Imports //------------------------------------------------------------------------------ -import rule from "../../src/rules/heading-increment.js"; -import markdown from "../../src/index.js"; -import { RuleTester } from "eslint"; import dedent from "dedent"; +import ruleTester from "./_utils/rule-tester.js"; +import rule from "../../src/rules/heading-increment.js"; //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ -// TODO - -const ruleTester = new RuleTester({ - plugins: { - markdown, - }, - language: "markdown/commonmark", -}); - -ruleTester.run("heading-increment", rule, { +ruleTester("heading-increment", rule, { valid: [ "# Heading 1", "## Heading 2", diff --git a/tests/rules/no-missing-label-refs.test.js b/tests/rules/no-missing-label-refs.test.js index bd1788c8..61c275bc 100644 --- a/tests/rules/no-missing-label-refs.test.js +++ b/tests/rules/no-missing-label-refs.test.js @@ -7,25 +7,17 @@ // Imports //------------------------------------------------------------------------------ -import rule from "../../src/rules/no-missing-label-refs.js"; -import markdown from "../../src/index.js"; -import { Linter, RuleTester } from "eslint"; import dedent from "dedent"; +import { Linter } from "eslint"; +import ruleTester from "./_utils/rule-tester.js"; +import markdown from "../../src/index.js"; +import rule from "../../src/rules/no-missing-label-refs.js"; //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ -// TODO - -const ruleTester = new RuleTester({ - plugins: { - markdown, - }, - language: "markdown/commonmark", -}); - -ruleTester.run("no-missing-label-refs", rule, { +ruleTester("no-missing-label-refs", rule, { valid: [ "[*foo*]", "[foo]\n\n[foo]: http://bar.com", From 0b627a45cdbf567e857d635671f7693289abdd57 Mon Sep 17 00:00:00 2001 From: lumir Date: Thu, 27 Aug 2026 21:40:46 +0900 Subject: [PATCH 10/22] wip --- src/language/markdown-language.js | 9 ++++----- src/types.ts | 15 ++++++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/language/markdown-language.js b/src/language/markdown-language.js index 4df27e8c..71a0f313 100644 --- a/src/language/markdown-language.js +++ b/src/language/markdown-language.js @@ -26,10 +26,9 @@ import { math } from "micromark-extension-math"; * @import { Language, File, ParseResult, OkParseResult } from "@eslint/core"; * @import { Root } from "mdast"; * @import { Options } from "mdast-util-from-markdown"; - * @import { MarkdownLanguageOptions, MarkdownLanguageContext, MarkdownParser } from "../types.js"; + * @import { MarkdownLanguageOptions, MarkdownLanguageContext, MarkdownParserMode, MarkdownParser } from "../types.js"; * @typedef {Options['extensions']} Extensions * @typedef {Options['mdastExtensions']} MdastExtensions - * @typedef {"commonmark"|"gfm"} ParserMode */ //----------------------------------------------------------------------------- @@ -55,7 +54,7 @@ const jsonFrontmatterConfig = { /** * Create parser options based on `mode` and `languageOptions`. - * @param {ParserMode} mode The markdown parser mode. + * @param {MarkdownParserMode} mode The markdown parser mode. * @param {MarkdownLanguageOptions} languageOptions Language options. * @returns {{extensions: Extensions, mdastExtensions: MdastExtensions}} Parser options for micromark and mdast. */ @@ -157,14 +156,14 @@ export class MarkdownLanguage { /** * The Markdown parser mode. - * @type {ParserMode} + * @type {MarkdownParserMode} */ #mode = "commonmark"; /** * Creates a new instance. * @param {Object} options The options to use for this instance. - * @param {ParserMode} [options.mode] The Markdown parser mode to use. + * @param {MarkdownParserMode} [options.mode] The Markdown parser mode to use. */ constructor({ mode } = {}) { if (mode) { diff --git a/src/types.ts b/src/types.ts index 3c54a322..dd296c7d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -134,18 +134,23 @@ declare module "mdast" { // Exports: Language and Source Code //------------------------------------------------------------------------------ +/** + * @deprecated Use `MarkdownParserMode` instead. + */ +export type ParserMode = MarkdownParserMode; + /** * The mode of the Markdown parser to use. * @default "commonmark" */ -export type MarkdownMode = "commonmark" | "gfm"; +export type MarkdownParserMode = "commonmark" | "gfm"; /** * A parser that converts Markdown source text into an mdast syntax tree. */ export type MarkdownParser = ObjectMetaProperties & { /** - * Parses Markdown source text into an mdast syntax tree. + * Parses Markdown source text into an [mdast](https://github.com/syntax-tree/mdast#readme) syntax tree. * @param text The Markdown source text to parse. * @param options The parser-specific options. * @returns The root of the mdast syntax tree. @@ -157,7 +162,7 @@ export type MarkdownParser = ObjectMetaProperties & { * The mode of the Markdown parser to use. * @default "commonmark" */ - mode?: MarkdownMode; + mode?: MarkdownParserMode; }, ): Root; }; @@ -179,8 +184,8 @@ export interface MarkdownLanguageOptions extends LanguageOptions { math?: boolean; /** - * An object containing a `parse()` method. If not configured, - * the default ESLint Markdown parser (`mdast-util-from-markdown`) will be used. + * An object with a `parse()` method and optional metadata properties. + * If not configured, the default ESLint Markdown parser (`mdast-util-from-markdown`) will be used. */ parser?: MarkdownParser; } From b15456c22ea0fd48ca733f32fa8a0c410b8eb59b Mon Sep 17 00:00:00 2001 From: lumir Date: Thu, 27 Aug 2026 21:43:53 +0900 Subject: [PATCH 11/22] wip --- src/types.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/types.ts b/src/types.ts index dd296c7d..d9496435 100644 --- a/src/types.ts +++ b/src/types.ts @@ -146,11 +146,13 @@ export type ParserMode = MarkdownParserMode; export type MarkdownParserMode = "commonmark" | "gfm"; /** - * A parser that converts Markdown source text into an mdast syntax tree. + * A parser that converts Markdown source text into an + * [mdast](https://github.com/syntax-tree/mdast#readme) syntax tree. */ export type MarkdownParser = ObjectMetaProperties & { /** - * Parses Markdown source text into an [mdast](https://github.com/syntax-tree/mdast#readme) syntax tree. + * Parses Markdown source text into an + * [mdast](https://github.com/syntax-tree/mdast#readme) syntax tree. * @param text The Markdown source text to parse. * @param options The parser-specific options. * @returns The root of the mdast syntax tree. From 3b08c052033ab20357cf2dd9a5f000d675e74407 Mon Sep 17 00:00:00 2001 From: lumir Date: Thu, 27 Aug 2026 21:55:31 +0900 Subject: [PATCH 12/22] wip --- src/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types.ts b/src/types.ts index d9496435..f7202550 100644 --- a/src/types.ts +++ b/src/types.ts @@ -159,12 +159,12 @@ export type MarkdownParser = ObjectMetaProperties & { */ parse( text: string, - options?: Omit & { + options: Omit & { /** * The mode of the Markdown parser to use. * @default "commonmark" */ - mode?: MarkdownParserMode; + mode: MarkdownParserMode; }, ): Root; }; From 0bca2e7649e87a971ab460d60136cf7c81b115d6 Mon Sep 17 00:00:00 2001 From: lumir Date: Thu, 27 Aug 2026 21:57:52 +0900 Subject: [PATCH 13/22] wip --- tests/rules/table-column-count.test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/rules/table-column-count.test.js b/tests/rules/table-column-count.test.js index ef754484..51369ee2 100644 --- a/tests/rules/table-column-count.test.js +++ b/tests/rules/table-column-count.test.js @@ -44,7 +44,10 @@ ruleTester("table-column-count", rule, { `, language: "markdown/gfm", }, - `Just some text. | not a table |`, + { + code: `Just some text. | not a table |`, + language: "markdown/gfm", + }, { code: dedent` | Header | Header | From 433f165fe49fa12bd2797bc19474f4ed9694ac19 Mon Sep 17 00:00:00 2001 From: lumir Date: Thu, 27 Aug 2026 22:30:44 +0900 Subject: [PATCH 14/22] wip --- src/language/markdown-language.js | 16 ++++++++-------- src/types.ts | 12 +++++++++++- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/language/markdown-language.js b/src/language/markdown-language.js index 71a0f313..2c296b4f 100644 --- a/src/language/markdown-language.js +++ b/src/language/markdown-language.js @@ -26,7 +26,7 @@ import { math } from "micromark-extension-math"; * @import { Language, File, ParseResult, OkParseResult } from "@eslint/core"; * @import { Root } from "mdast"; * @import { Options } from "mdast-util-from-markdown"; - * @import { MarkdownLanguageOptions, MarkdownLanguageContext, MarkdownParserMode, MarkdownParser } from "../types.js"; + * @import { MarkdownLanguageOptions, MarkdownLanguageContext, MarkdownParserMode } from "../types.js"; * @typedef {Options['extensions']} Extensions * @typedef {Options['mdastExtensions']} MdastExtensions */ @@ -220,16 +220,16 @@ export class MarkdownLanguage { /** * Parses the given file into an AST. * @param {File} file The virtual file to parse. - * @param {MarkdownLanguageContext} context The options to use for parsing. + * @param {MarkdownLanguageContext} [context] The options to use for parsing. * @returns {ParseResult} The result of parsing. */ parse(file, context) { // Note: BOM already removed const text = /** @type {string} */ (file.body); - const languageOptions = { - ...this.defaultLanguageOptions, - ...context?.languageOptions, - }; + const { + parser = this.defaultLanguageOptions.parser, + ...restLanguageOptions + } = context?.languageOptions ?? {}; /* * Check for parsing errors first. If there's a parsing error, nothing @@ -238,9 +238,9 @@ export class MarkdownLanguage { * problem that ESLint identified just like any other. */ try { - const root = languageOptions.parser.parse(text, { + const root = parser.parse(text, { mode: this.#mode, - ...languageOptions, + ...restLanguageOptions, }); return { diff --git a/src/types.ts b/src/types.ts index f7202550..12b561d2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -159,12 +159,22 @@ export type MarkdownParser = ObjectMetaProperties & { */ parse( text: string, - options: Omit & { + options: MarkdownLanguageOptions & { /** * The mode of the Markdown parser to use. * @default "commonmark" */ mode: MarkdownParserMode; + + /* + * Don't use `Omit` here. + * Because `MarkdownLanguageOptions` has a string index signature, + * `Omit` would lose its named option properties. + */ + /** + * Parsers cannot override the `parser` used by the language. + */ + parser?: never; }, ): Root; }; From ff8dc22cc55df16ac60e212a105d4cbf2d8730dc Mon Sep 17 00:00:00 2001 From: lumir Date: Thu, 27 Aug 2026 22:37:20 +0900 Subject: [PATCH 15/22] wip --- src/language/markdown-language.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/language/markdown-language.js b/src/language/markdown-language.js index 2c296b4f..aafe2fb6 100644 --- a/src/language/markdown-language.js +++ b/src/language/markdown-language.js @@ -226,6 +226,7 @@ export class MarkdownLanguage { parse(file, context) { // Note: BOM already removed const text = /** @type {string} */ (file.body); + const { parser = this.defaultLanguageOptions.parser, ...restLanguageOptions From 99a12f0179228b5da68678db331ba76b6414af53 Mon Sep 17 00:00:00 2001 From: lumir Date: Thu, 27 Aug 2026 22:48:24 +0900 Subject: [PATCH 16/22] wip --- src/language/markdown-language.js | 20 +++++++++++++------- src/processor.js | 4 ++-- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/language/markdown-language.js b/src/language/markdown-language.js index aafe2fb6..4a1d7d12 100644 --- a/src/language/markdown-language.js +++ b/src/language/markdown-language.js @@ -162,7 +162,7 @@ export class MarkdownLanguage { /** * Creates a new instance. - * @param {Object} options The options to use for this instance. + * @param {Object} [options] The options to use for this instance. * @param {MarkdownParserMode} [options.mode] The Markdown parser mode to use. */ constructor({ mode } = {}) { @@ -173,7 +173,7 @@ export class MarkdownLanguage { /** * Validates the language options. - * @param {MarkdownLanguageOptions} languageOptions The language options to validate. + * @param {MarkdownLanguageOptions} [languageOptions] The language options to validate. * @returns {void} * @throws {Error} When the language options are invalid. */ @@ -208,12 +208,18 @@ export class MarkdownLanguage { // `parser` option validation const parserOption = languageOptions?.parser; - if (parserOption !== undefined && typeof parserOption !== "object") { - throw new Error( - `Invalid language option value \`${parserOption}\` for parser. Expected an object.`, - ); + if (parserOption !== undefined) { + if (typeof parserOption !== "object" || parserOption === null) { + throw new Error( + `Invalid language option value \`${parserOption}\` for parser. Expected a non-null object.`, + ); + } - // TODO: stricter validation for parser object shape (e.g., check for `parse` method) + if (typeof parserOption.parse !== "function") { + throw new Error( + "Invalid language option `parser`. Expected an object with a `parse` method.", + ); + } } } diff --git a/src/processor.js b/src/processor.js index 38e9ef56..2740b392 100644 --- a/src/processor.js +++ b/src/processor.js @@ -260,7 +260,7 @@ const languageToFileExtension = { /** * Extracts lintable code blocks from Markdown text. * @param {string} sourceText The text of the file. - * @param {string} filename The filename of the file. + * @param {string} [filename] The filename of the file. * @returns {Array<{ filename: string, text: string }>} Source code blocks to lint. */ function preprocess(sourceText, filename) { @@ -453,7 +453,7 @@ function excludeUnsatisfiableRules(message) { * Transforms generated messages for output. * @param {Array} messages An array containing one array of messages * for each code block returned from `preprocess`. - * @param {string} filename The filename of the file + * @param {string} [filename] The filename of the file. * @returns {LintMessage[]} A flattened array of messages with mapped locations. */ function postprocess(messages, filename) { From f1f3aee774ef12693cb91fac706a4774ee22f8c8 Mon Sep 17 00:00:00 2001 From: lumir Date: Thu, 27 Aug 2026 23:01:23 +0900 Subject: [PATCH 17/22] wip --- src/language/markdown-language.js | 3 +++ tests/plugin.test.js | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/language/markdown-language.js b/src/language/markdown-language.js index 4a1d7d12..452a9bea 100644 --- a/src/language/markdown-language.js +++ b/src/language/markdown-language.js @@ -145,6 +145,9 @@ export class MarkdownLanguage { frontmatter: false, math: false, parser: { + meta: { + name: "mdast-util-from-markdown", + }, parse(text, { mode, ...languageOptions }) { return fromMarkdown( text, diff --git a/tests/plugin.test.js b/tests/plugin.test.js index ac88c6d4..759264be 100644 --- a/tests/plugin.test.js +++ b/tests/plugin.test.js @@ -1228,6 +1228,25 @@ describe("FlatESLint", () => { .map(([name]) => `markdown/${name}`); assert.deepStrictEqual(actualRuleIds, expectedRuleIds); }); + + it("should serialize the default Markdown parser", async () => { + const markdownESLint = new ESLint({ + overrideConfigFile: true, + overrideConfig: { + files: ["**/*.md"], + plugins: { markdown: plugin }, + language: "markdown/commonmark", + }, + }); + const config = + await markdownESLint.calculateConfigForFile("test.md"); + const serializedConfig = JSON.parse(JSON.stringify(config)); + + assert.strictEqual( + serializedConfig.languageOptions.parser, + "mdast-util-from-markdown", + ); + }); }); describe("plugin", () => { From 7df23545ee24cc09deb540c884c709068cd2c44f Mon Sep 17 00:00:00 2001 From: lumir Date: Thu, 27 Aug 2026 23:03:11 +0900 Subject: [PATCH 18/22] wip --- src/processor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/processor.js b/src/processor.js index 2740b392..3334151a 100644 --- a/src/processor.js +++ b/src/processor.js @@ -260,7 +260,7 @@ const languageToFileExtension = { /** * Extracts lintable code blocks from Markdown text. * @param {string} sourceText The text of the file. - * @param {string} [filename] The filename of the file. + * @param {string} filename The filename of the file. * @returns {Array<{ filename: string, text: string }>} Source code blocks to lint. */ function preprocess(sourceText, filename) { @@ -453,7 +453,7 @@ function excludeUnsatisfiableRules(message) { * Transforms generated messages for output. * @param {Array} messages An array containing one array of messages * for each code block returned from `preprocess`. - * @param {string} [filename] The filename of the file. + * @param {string} filename The filename of the file. * @returns {LintMessage[]} A flattened array of messages with mapped locations. */ function postprocess(messages, filename) { From 2fc3cd8782593650a34a67d6e9a76ab737b0f41a Mon Sep 17 00:00:00 2001 From: lumir Date: Thu, 27 Aug 2026 23:08:54 +0900 Subject: [PATCH 19/22] wip --- src/language/markdown-language.js | 2 +- tests/language/markdown-language.test.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/language/markdown-language.js b/src/language/markdown-language.js index 452a9bea..0e44596c 100644 --- a/src/language/markdown-language.js +++ b/src/language/markdown-language.js @@ -214,7 +214,7 @@ export class MarkdownLanguage { if (parserOption !== undefined) { if (typeof parserOption !== "object" || parserOption === null) { throw new Error( - `Invalid language option value \`${parserOption}\` for parser. Expected a non-null object.`, + `Invalid language option value \`${String(parserOption)}\` for parser. Expected a non-null object.`, ); } diff --git a/tests/language/markdown-language.test.js b/tests/language/markdown-language.test.js index b9c5c4cf..df714a58 100644 --- a/tests/language/markdown-language.test.js +++ b/tests/language/markdown-language.test.js @@ -139,6 +139,22 @@ describe("MarkdownLanguage", () => { language.validateLanguageOptions({ math: false }); }); }); + + it("should throw the expected error when `parser` is a symbol", () => { + const language = new MarkdownLanguage(); + + assert.throws( + () => { + language.validateLanguageOptions({ + parser: Symbol("parser"), + }); + }, + { + message: + "Invalid language option value `Symbol(parser)` for parser. Expected a non-null object.", + }, + ); + }); }); describe("parse()", () => { From 64985ad5b6e1ce929193e59f9cee6f3c6ba616dc Mon Sep 17 00:00:00 2001 From: lumir Date: Thu, 27 Aug 2026 23:36:01 +0900 Subject: [PATCH 20/22] wip --- src/language/markdown-language.js | 2 +- tests/language/markdown-language.test.js | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/language/markdown-language.js b/src/language/markdown-language.js index 0e44596c..96ccff78 100644 --- a/src/language/markdown-language.js +++ b/src/language/markdown-language.js @@ -249,8 +249,8 @@ export class MarkdownLanguage { */ try { const root = parser.parse(text, { - mode: this.#mode, ...restLanguageOptions, + mode: this.#mode, }); return { diff --git a/tests/language/markdown-language.test.js b/tests/language/markdown-language.test.js index df714a58..ca9ea148 100644 --- a/tests/language/markdown-language.test.js +++ b/tests/language/markdown-language.test.js @@ -418,6 +418,26 @@ describe("MarkdownLanguage", () => { assert.strictEqual(result.ast.children[1].children[0].type, "text"); assert.strictEqual(result.ast.children[2].type, "math"); }); + + it("should not allow language options to override commonmark mode", () => { + const language = new MarkdownLanguage({ + mode: "commonmark", + }); + const result = language.parse( + { + body: "| Column 1 | Column 2 |\n| -------- | -------- |\n| Cell 1 | Cell 2 |", + path: "test.md", + }, + { + languageOptions: { + mode: "gfm", + }, + }, + ); + + assert.strictEqual(result.ok, true); + assert.strictEqual(result.ast.children[0].type, "paragraph"); + }); }); describe("createSourceCode()", () => { From 7c6b6e2f02bc9b9fa9ac016bd6da73e25e916117 Mon Sep 17 00:00:00 2001 From: lumir Date: Fri, 28 Aug 2026 20:43:11 +0900 Subject: [PATCH 21/22] wip --- src/processor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/processor.js b/src/processor.js index 3334151a..38e9ef56 100644 --- a/src/processor.js +++ b/src/processor.js @@ -453,7 +453,7 @@ function excludeUnsatisfiableRules(message) { * Transforms generated messages for output. * @param {Array} messages An array containing one array of messages * for each code block returned from `preprocess`. - * @param {string} filename The filename of the file. + * @param {string} filename The filename of the file * @returns {LintMessage[]} A flattened array of messages with mapped locations. */ function postprocess(messages, filename) { From 1a28ef600b44ff8b6b0796738af122281dbf8fea Mon Sep 17 00:00:00 2001 From: lumir Date: Sat, 5 Sep 2026 18:47:43 +0900 Subject: [PATCH 22/22] wip --- tests/fixtures/parser.js | 21 +++++++++++++++++ tests/plugin.test.js | 50 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 tests/fixtures/parser.js diff --git a/tests/fixtures/parser.js b/tests/fixtures/parser.js new file mode 100644 index 00000000..ebbc7234 --- /dev/null +++ b/tests/fixtures/parser.js @@ -0,0 +1,21 @@ +import markdown from "../../src/index.js"; + +export default [ + { + files: ["**/*.md"], + plugins: { markdown }, + language: "markdown/commonmark", + languageOptions: { + parser: { + parse() { + throw new Error( + "The configured Markdown parser was called.", + ); + }, + }, + }, + rules: { + "markdown/heading-increment": "error", + }, + }, +]; diff --git a/tests/plugin.test.js b/tests/plugin.test.js index 759264be..86a5c600 100644 --- a/tests/plugin.test.js +++ b/tests/plugin.test.js @@ -8,6 +8,7 @@ //----------------------------------------------------------------------------- import assert from "node:assert"; +import { spawnSync } from "node:child_process"; import path from "node:path"; import fs from "node:fs"; import { fileURLToPath, pathToFileURL } from "node:url"; @@ -2459,4 +2460,53 @@ describe("FlatESLint", () => { } }); }); + + describe("CLI", () => { + it("should override the Markdown parser with `--parser @eslint-markdown/parser`", () => { + const eslintPath = fileURLToPath( + new URL( + "bin/eslint.js", + import.meta.resolve("eslint/package.json"), + ), + ); + const args = [ + eslintPath, + "--config=parser.js", + "--stdin", + "--stdin-filename=test.md", + "--format=json", + ]; + const options = { + cwd: path.resolve(__dirname, "fixtures"), + input: "# Hello\n\n### Skipped level\n", + encoding: "utf8", + }; + + const withoutParser = spawnSync(process.execPath, args, options); + + assert.strictEqual(withoutParser.status, 1); + const [configuredResult] = JSON.parse(withoutParser.stdout); + + assert.strictEqual(configuredResult.messages.length, 1); + assert.strictEqual( + configuredResult.messages[0].message, + "Parsing error: The configured Markdown parser was called.", + ); + + const withParser = spawnSync( + process.execPath, + [...args, "--parser", "@eslint-markdown/parser"], + options, + ); + + assert.strictEqual(withParser.status, 1); + const [overriddenResult] = JSON.parse(withParser.stdout); + + assert.strictEqual(overriddenResult.messages.length, 1); + assert.strictEqual( + overriddenResult.messages[0].ruleId, + "markdown/heading-increment", + ); + }); + }); });