From 013878c21034b491a00b820190efdf64ed428ef5 Mon Sep 17 00:00:00 2001 From: lumir Date: Wed, 12 Aug 2026 15:21:31 +0900 Subject: [PATCH 1/4] feat: add `no-emphasis-as-heading` rule --- README.md | 1 + docs/rules/no-emphasis-as-heading.md | 79 ++++++++++++++ src/rules/no-emphasis-as-heading.js | 67 ++++++++++++ tests/rules/no-emphasis-as-heading.test.js | 115 +++++++++++++++++++++ 4 files changed, 262 insertions(+) create mode 100644 docs/rules/no-emphasis-as-heading.md create mode 100644 src/rules/no-emphasis-as-heading.js create mode 100644 tests/rules/no-emphasis-as-heading.test.js diff --git a/README.md b/README.md index a2236073..2077e7b3 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ export default defineConfig([ | [`no-bare-urls`](./docs/rules/no-bare-urls.md) | Disallow bare URLs | no | | [`no-duplicate-definitions`](./docs/rules/no-duplicate-definitions.md) | Disallow duplicate definitions | yes | | [`no-duplicate-headings`](./docs/rules/no-duplicate-headings.md) | Disallow duplicate headings in the same document | no | +| [`no-emphasis-as-heading`](./docs/rules/no-emphasis-as-heading.md) | Disallow using fully bolded paragraphs as headings | yes | | [`no-empty-definitions`](./docs/rules/no-empty-definitions.md) | Disallow empty definitions | yes | | [`no-empty-images`](./docs/rules/no-empty-images.md) | Disallow empty images | yes | | [`no-empty-links`](./docs/rules/no-empty-links.md) | Disallow empty links | yes | diff --git a/docs/rules/no-emphasis-as-heading.md b/docs/rules/no-emphasis-as-heading.md new file mode 100644 index 00000000..5471154d --- /dev/null +++ b/docs/rules/no-emphasis-as-heading.md @@ -0,0 +1,79 @@ +# no-emphasis-as-heading + +TODO + +## Rule Details + +This rule disallows using fully bolded paragraphs as headings. The use of fully bolded paragraphs as headings is a common anti-pattern that reduces document semantics and accessibility. Instead, proper heading elements (`#`, `##`, etc.) should be used. + +### Why This Is Important + +Using proper headings instead of bolded paragraphs: + +- Improves document structure and semantics +- Enhances accessibility for screen readers +- Creates proper document outline +- Makes navigation of the document easier + +### What This Rule Checks + +This rule identifies paragraphs that: + +- Consist entirely of a bold element (`**text**` or `__text__`) +- Are contained on a single line +- Are not within list items +- Have bold markup that spans the entire paragraph content + +## Examples + +### :x: Incorrect + +Examples of **incorrect** code for this rule: + +```md /**First Chapter**/ /__Second Chapter__/ + + +# Book + +**First Chapter** + +Content of the first chapter + +__Second Chapter__ + +Content of the second chapter +``` + +### :white_check_mark: Correct + +Examples of **correct** code for this rule: + +```md + + +# Book + +## First Chapter + +Content of the first chapter + +## Second Chapter + +Content of the second chapter + +--- + +**Bold text** with normal text in the paragraph. + +Text with **bold parts** is fine. + +- **Bold text in a list item** is allowed. +``` + +## Options + +No options are available for this rule. + +## Prior Art + +- [textlint-rule-no-bold-paragraph](https://github.com/aborazmeh/textlint-rule-no-bold-paragraph) diff --git a/src/rules/no-emphasis-as-heading.js b/src/rules/no-emphasis-as-heading.js new file mode 100644 index 00000000..6aa15aed --- /dev/null +++ b/src/rules/no-emphasis-as-heading.js @@ -0,0 +1,67 @@ +/** + * @fileoverview Rule to disallow using fully bolded paragraphs as headings. + * @author lumir(lumirlumir) + */ + +// @ts-nocheck -- TODO + +//----------------------------------------------------------------------------- +// Type Definitions +//----------------------------------------------------------------------------- + +/** + * @import { MarkdownRuleDefinition } from "../types.js"; + * @typedef {"no-emphasis-as-heading"} NoEmphasisAsHeadingMessageIds + * @typedef {[]} NoEmphasisAsHeadingOptions + * @typedef {MarkdownRuleDefinition<{ RuleOptions: NoEmphasisAsHeadingOptions, MessageIds: NoEmphasisAsHeadingMessageIds }>} NoEmphasisAsHeadingRuleDefinition + */ + +// -------------------------------------------------------------------------------- +// Rule Definition +// -------------------------------------------------------------------------------- + +export default /** @satisfies {NoEmphasisAsHeadingRuleDefinition} */ ({ + meta: { + type: "problem", + + docs: { + recommended: true, + description: "Disallow using fully bolded paragraphs as headings", + url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-emphasis-as-heading.md", + }, + + messages: { + noEmphasisAsHeading: + "Fully bolded paragraphs should not be used as headings. Please use a heading instead.", + }, + + // language: "markdown", + + // dialects: ["commonmark", "gfm"], + }, + + create(context) { + return { + strong(node) { + const parentNode = context.sourceCode.getParent(node); + const ancestorNode = context.sourceCode.getParent(parentNode); + + if ( + parentNode.type === "paragraph" && + ancestorNode.type !== "listItem" && + parentNode.position.start.line === + parentNode.position.end.line && // Should be a single line. + parentNode.position.start.offset === + node.position.start.offset && // Should have the same start offset. + parentNode.position.end.offset === node.position.end.offset // Should have the same end offset. + ) { + context.report({ + node, + + messageId: "noEmphasisAsHeading", + }); + } + }, + }; + }, +}); diff --git a/tests/rules/no-emphasis-as-heading.test.js b/tests/rules/no-emphasis-as-heading.test.js new file mode 100644 index 00000000..64849f36 --- /dev/null +++ b/tests/rules/no-emphasis-as-heading.test.js @@ -0,0 +1,115 @@ +/** + * @fileoverview Test for `no-emphasis-as-heading.ts`. + * @author lumir(lumirlumir) + */ + +//------------------------------------------------------------------------------ +// Imports +//------------------------------------------------------------------------------ + +import rule from "../../src/rules/no-emphasis-as-heading.js"; +import markdown from "../../src/index.js"; +import { RuleTester } from "eslint"; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + plugins: { + markdown, + }, + language: "markdown/commonmark", +}); + +ruleTester.run("no-emphasis-as-heading", rule, { + valid: [ + // Basic + { + name: "Empty", + code: "", + }, + { + name: "Empty string", + code: " ", + }, + { + name: "Paragraph without bolded text", + code: "#Book\n\nFirst Chapter\n\nContent\n\nSecond Chapter\n\nContent", + }, + { + name: "Paragraph with bolded text and other text", + code: "**Hello** World.\n\nHello **World**.", + }, + { + name: "Paragraph with bolded text and other multiline text", + code: "**Hello**\nWorld.\n\nHello\n**World**.", + }, + { + name: "Bold with whitespace inside", + code: "** Not fully bolded paragraph **", + }, + { + name: "Multiple bold elements comprising entire paragraph", + code: "**First part** **second part**", + }, + + // ListItem + { + name: "Bold text in list item", + code: "- **List item**\n- __List item__", + }, + ], + + invalid: [ + // Basic + { + name: "Paragraph with fully bolded text", + code: "#Book\n\n**First Chapter**\n\nContent\n\n__Second Chapter__\n\nContent", + errors: [ + { + messageId: "noEmphasisAsHeading", + line: 3, + column: 1, + endLine: 3, + endColumn: 18, + }, + { + messageId: "noEmphasisAsHeading", + line: 7, + column: 1, + endLine: 7, + endColumn: 19, + }, + ], + }, + { + name: "Paragraph with fully bolded single character", + code: "**X**", + errors: [ + { + messageId: "noEmphasisAsHeading", + line: 1, + column: 1, + endLine: 1, + endColumn: 6, + }, + ], + }, + + // Blockquote + { + name: "Bold text in blockquote", + code: "> **Blockquote**", + errors: [ + { + messageId: "noEmphasisAsHeading", + line: 1, + column: 3, + endLine: 1, + endColumn: 17, + }, + ], + }, + ], +}); From 7a14f48dbfb84b9a5db9ac3e1870a128a6d43584 Mon Sep 17 00:00:00 2001 From: lumir Date: Mon, 14 Sep 2026 22:54:21 +0900 Subject: [PATCH 2/4] wip --- README.md | 2 +- ...-heading.md => no-emphasis-as-headings.md} | 0 src/rules/no-emphasis-as-heading.js | 67 ---- src/rules/no-emphasis-as-headings.js | 90 ++++++ tests/rules/no-emphasis-as-heading.test.js | 115 ------- tests/rules/no-emphasis-as-headings.test.js | 303 ++++++++++++++++++ 6 files changed, 394 insertions(+), 183 deletions(-) rename docs/rules/{no-emphasis-as-heading.md => no-emphasis-as-headings.md} (100%) delete mode 100644 src/rules/no-emphasis-as-heading.js create mode 100644 src/rules/no-emphasis-as-headings.js delete mode 100644 tests/rules/no-emphasis-as-heading.test.js create mode 100644 tests/rules/no-emphasis-as-headings.test.js diff --git a/README.md b/README.md index db95eb93..4726dfac 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ export default defineConfig([ | [`no-bare-urls`](./docs/rules/no-bare-urls.md) | Disallow bare URLs | no | | [`no-duplicate-definitions`](./docs/rules/no-duplicate-definitions.md) | Disallow duplicate definitions | yes | | [`no-duplicate-headings`](./docs/rules/no-duplicate-headings.md) | Disallow duplicate headings in the same document | no | -| [`no-emphasis-as-heading`](./docs/rules/no-emphasis-as-heading.md) | Disallow using fully bolded paragraphs as headings | yes | +| [`no-emphasis-as-headings`](./docs/rules/no-emphasis-as-headings.md) | Disallow using emphasis or strong as headings | yes | | [`no-empty-definitions`](./docs/rules/no-empty-definitions.md) | Disallow empty definitions | yes | | [`no-empty-images`](./docs/rules/no-empty-images.md) | Disallow empty images | yes | | [`no-empty-links`](./docs/rules/no-empty-links.md) | Disallow empty links | yes | diff --git a/docs/rules/no-emphasis-as-heading.md b/docs/rules/no-emphasis-as-headings.md similarity index 100% rename from docs/rules/no-emphasis-as-heading.md rename to docs/rules/no-emphasis-as-headings.md diff --git a/src/rules/no-emphasis-as-heading.js b/src/rules/no-emphasis-as-heading.js deleted file mode 100644 index 6aa15aed..00000000 --- a/src/rules/no-emphasis-as-heading.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @fileoverview Rule to disallow using fully bolded paragraphs as headings. - * @author lumir(lumirlumir) - */ - -// @ts-nocheck -- TODO - -//----------------------------------------------------------------------------- -// Type Definitions -//----------------------------------------------------------------------------- - -/** - * @import { MarkdownRuleDefinition } from "../types.js"; - * @typedef {"no-emphasis-as-heading"} NoEmphasisAsHeadingMessageIds - * @typedef {[]} NoEmphasisAsHeadingOptions - * @typedef {MarkdownRuleDefinition<{ RuleOptions: NoEmphasisAsHeadingOptions, MessageIds: NoEmphasisAsHeadingMessageIds }>} NoEmphasisAsHeadingRuleDefinition - */ - -// -------------------------------------------------------------------------------- -// Rule Definition -// -------------------------------------------------------------------------------- - -export default /** @satisfies {NoEmphasisAsHeadingRuleDefinition} */ ({ - meta: { - type: "problem", - - docs: { - recommended: true, - description: "Disallow using fully bolded paragraphs as headings", - url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-emphasis-as-heading.md", - }, - - messages: { - noEmphasisAsHeading: - "Fully bolded paragraphs should not be used as headings. Please use a heading instead.", - }, - - // language: "markdown", - - // dialects: ["commonmark", "gfm"], - }, - - create(context) { - return { - strong(node) { - const parentNode = context.sourceCode.getParent(node); - const ancestorNode = context.sourceCode.getParent(parentNode); - - if ( - parentNode.type === "paragraph" && - ancestorNode.type !== "listItem" && - parentNode.position.start.line === - parentNode.position.end.line && // Should be a single line. - parentNode.position.start.offset === - node.position.start.offset && // Should have the same start offset. - parentNode.position.end.offset === node.position.end.offset // Should have the same end offset. - ) { - context.report({ - node, - - messageId: "noEmphasisAsHeading", - }); - } - }, - }; - }, -}); diff --git a/src/rules/no-emphasis-as-headings.js b/src/rules/no-emphasis-as-headings.js new file mode 100644 index 00000000..ec360532 --- /dev/null +++ b/src/rules/no-emphasis-as-headings.js @@ -0,0 +1,90 @@ +/** + * @fileoverview Rule to disallow using emphasis or strong as headings. + * @author lumir(lumirlumir) + */ + +//----------------------------------------------------------------------------- +// Type Definitions +//----------------------------------------------------------------------------- + +/** + * @import { Emphasis, Strong } from "mdast"; + * @import { MarkdownRuleDefinition } from "../types.js"; + * @typedef {"noEmphasisAsHeadings"} NoEmphasisAsHeadingsMessageIds + * @typedef {[]} NoEmphasisAsHeadingsOptions + * @typedef {MarkdownRuleDefinition<{ RuleOptions: NoEmphasisAsHeadingsOptions, MessageIds: NoEmphasisAsHeadingsMessageIds }>} NoEmphasisAsHeadingsRuleDefinition + */ + +// -------------------------------------------------------------------------------- +// Rule Definition +// -------------------------------------------------------------------------------- + +export default /** @satisfies {NoEmphasisAsHeadingsRuleDefinition} */ ({ + meta: { + type: "problem", + languages: ["markdown/commonmark", "markdown/gfm"], + + docs: { + recommended: true, + description: "Disallow using emphasis or strong as headings", + dialects: ["CommonMark", "GFM"], + url: "https://github.com/eslint/markdown/blob/main/docs/rules/no-emphasis-as-headings.md", + }, + + messages: { + noEmphasisAsHeadings: + "Unexpected emphasis or strong used as a heading.", + }, + + // option: TODO + }, + + create(context) { + const { sourceCode } = context; + + let isInBlockquote = false; + let isInListItem = false; + + return { + blockquote() { + isInBlockquote = true; + }, + + listItem() { + isInListItem = true; + }, + + "emphasis, strong"(/** @type {Emphasis | Strong} */ node) { + if (isInBlockquote || isInListItem) { + // Early return if inside a blockquote or list item. + return; + } + + const parentNode = sourceCode.getParent(node); + + if ( + parentNode.type === "paragraph" && + parentNode.position.start.line === + parentNode.position.end.line && // Should be a single line. + parentNode.position.start.offset === + node.position.start.offset && // Should have the same start offset. + parentNode.position.end.offset === node.position.end.offset // Should have the same end offset. + ) { + context.report({ + node, + + messageId: "noEmphasisAsHeadings", + }); + } + }, + + "blockquote:exit"() { + isInBlockquote = false; + }, + + "listItem:exit"() { + isInListItem = false; + }, + }; + }, +}); diff --git a/tests/rules/no-emphasis-as-heading.test.js b/tests/rules/no-emphasis-as-heading.test.js deleted file mode 100644 index 64849f36..00000000 --- a/tests/rules/no-emphasis-as-heading.test.js +++ /dev/null @@ -1,115 +0,0 @@ -/** - * @fileoverview Test for `no-emphasis-as-heading.ts`. - * @author lumir(lumirlumir) - */ - -//------------------------------------------------------------------------------ -// Imports -//------------------------------------------------------------------------------ - -import rule from "../../src/rules/no-emphasis-as-heading.js"; -import markdown from "../../src/index.js"; -import { RuleTester } from "eslint"; - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -const ruleTester = new RuleTester({ - plugins: { - markdown, - }, - language: "markdown/commonmark", -}); - -ruleTester.run("no-emphasis-as-heading", rule, { - valid: [ - // Basic - { - name: "Empty", - code: "", - }, - { - name: "Empty string", - code: " ", - }, - { - name: "Paragraph without bolded text", - code: "#Book\n\nFirst Chapter\n\nContent\n\nSecond Chapter\n\nContent", - }, - { - name: "Paragraph with bolded text and other text", - code: "**Hello** World.\n\nHello **World**.", - }, - { - name: "Paragraph with bolded text and other multiline text", - code: "**Hello**\nWorld.\n\nHello\n**World**.", - }, - { - name: "Bold with whitespace inside", - code: "** Not fully bolded paragraph **", - }, - { - name: "Multiple bold elements comprising entire paragraph", - code: "**First part** **second part**", - }, - - // ListItem - { - name: "Bold text in list item", - code: "- **List item**\n- __List item__", - }, - ], - - invalid: [ - // Basic - { - name: "Paragraph with fully bolded text", - code: "#Book\n\n**First Chapter**\n\nContent\n\n__Second Chapter__\n\nContent", - errors: [ - { - messageId: "noEmphasisAsHeading", - line: 3, - column: 1, - endLine: 3, - endColumn: 18, - }, - { - messageId: "noEmphasisAsHeading", - line: 7, - column: 1, - endLine: 7, - endColumn: 19, - }, - ], - }, - { - name: "Paragraph with fully bolded single character", - code: "**X**", - errors: [ - { - messageId: "noEmphasisAsHeading", - line: 1, - column: 1, - endLine: 1, - endColumn: 6, - }, - ], - }, - - // Blockquote - { - name: "Bold text in blockquote", - code: "> **Blockquote**", - errors: [ - { - messageId: "noEmphasisAsHeading", - line: 1, - column: 3, - endLine: 1, - endColumn: 17, - }, - ], - }, - ], -}); diff --git a/tests/rules/no-emphasis-as-headings.test.js b/tests/rules/no-emphasis-as-headings.test.js new file mode 100644 index 00000000..5615adad --- /dev/null +++ b/tests/rules/no-emphasis-as-headings.test.js @@ -0,0 +1,303 @@ +/** + * @fileoverview Tests for no-emphasis-as-headings rule. + * @author lumir(lumirlumir) + */ + +//------------------------------------------------------------------------------ +// Imports +//------------------------------------------------------------------------------ + +import rule from "../../src/rules/no-emphasis-as-headings.js"; +import markdown from "../../src/index.js"; +import { RuleTester } from "eslint"; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + plugins: { + markdown, + }, + language: "markdown/commonmark", +}); + +ruleTester.run("no-emphasis-as-headings", rule, { + valid: [ + "", + " ", + "foo\n\nbar\n\nbaz\n\nqux", + "*foo*\nbar\nbaz", + "_foo_\nbar\nbaz", + "**foo**\nbar\nbaz", + "__foo__\nbar\nbaz", + "***foo***\nbar\nbaz", + "___foo___\nbar\nbaz", + "foo\nbar\nbaz", + "foo\n*bar*\nbaz", + "foo\n_bar_\nbaz", + "foo\n**bar**\nbaz", + "foo\n__bar__\nbaz", + "foo\n***bar***\nbaz", + "foo\n___bar___\nbaz", + "**foo** bar\n\nfoo **bar**", + "**foo** **bar**", + "** foo **", // This is not a `strong` node. + + // Multiline + "*foo\nbar*", + "_foo\nbar_", + "**foo\nbar**", + "__foo\nbar__", + + // `blockquote` is not checked by this rule. + // This behavior aligns with `markdownlint`. + "> *foo*", + "> _foo_", + "> **foo**", + "> __foo__", + "> ***foo***", + "> ___foo___", + + "> > *foo*", + "> > _foo_", + "> > **foo**", + "> > __foo__", + "> > ***foo***", + "> > ___foo___", + + "> - *foo*", + "> - _foo_", + "> - **foo**", + "> - __foo__", + "> - ***foo***", + "> - ___foo___", + + // `heading` is not checked by this rule. + // This behavior aligns with `markdownlint`. + "# *foo*", + "# _foo_", + "# **foo**", + "# __foo__", + "# ***foo***", + "# ___foo___", + + // `listItem` is not checked by this rule. + // This behavior aligns with `markdownlint`. + "- *foo*", + "- _foo_", + "- **foo**", + "- __foo__", + "- ***foo***", + "- ___foo___", + + "* *foo*", + "* _foo_", + "* **foo**", + "* __foo__", + "* ***foo***", + "* ___foo___", + + "- > *foo*", + "- > _foo_", + "- > **foo**", + "- > __foo__", + "- > ***foo***", + "- > ___foo___", + + // `strikethrough` is not checked by this rule. + // This behavior aligns with `markdownlint`. + { + code: "~foo~", + language: "markdown/gfm", + }, + { + code: "~~foo~~", + language: "markdown/gfm", + }, + + // `tableCell` is not checked by this rule because it cannot contain headings. + // This behavior aligns with `markdownlint`. + { + code: ` +| foo | bar | +| --- | --- | +| *baz* | *qux* |`, + language: "markdown/gfm", + }, + { + code: ` +| foo | bar | +| --- | --- | +| _baz_ | _qux_ |`, + language: "markdown/gfm", + }, + { + code: ` +| foo | bar | +| --- | --- | +| **baz** | **qux** |`, + language: "markdown/gfm", + }, + { + code: ` +| foo | bar | +| --- | --- | +| __baz__ | __qux__ |`, + language: "markdown/gfm", + }, + + // TODO: + // "*foo _bar_ baz*", + // "_foo *bar* baz_", + ], + + invalid: [ + { + code: "*foo*", + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 6, + }, + ], + }, + { + code: "_foo_", + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 6, + }, + ], + }, + { + code: "**foo**", + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 8, + }, + ], + }, + { + code: "__foo__", + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 8, + }, + ], + }, + { + // `markdownlint` does not flag triple emphasis as a heading, but this rule does. + code: "***foo***", + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 10, + }, + ], + }, + { + // `markdownlint` does not flag triple emphasis as a heading, but this rule does. + code: "___foo___", + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 10, + }, + ], + }, + { + // `markdownlint` does not flag triple emphasis as a heading, but this rule does. + code: "*__foo__*", + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 10, + }, + ], + }, + { + // `markdownlint` does not flag triple emphasis as a heading, but this rule does. + code: "__*foo*__", + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 10, + }, + ], + }, + { + // `markdownlint` does not flag triple emphasis as a heading, but this rule does. + code: "_**foo**_", + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 10, + }, + ], + }, + { + // `markdownlint` does not flag triple emphasis as a heading, but this rule does. + code: "**_foo_**", + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 10, + }, + ], + }, + + { + code: "foo\n\n**bar**\n\nbaz\n\n__qux__", + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 3, + column: 1, + endLine: 3, + endColumn: 8, + }, + { + messageId: "noEmphasisAsHeadings", + line: 7, + column: 1, + endLine: 7, + endColumn: 8, + }, + ], + }, + ], +}); From 235d04d4f3fee1267aa43ac0f4c3dd64835d6607 Mon Sep 17 00:00:00 2001 From: lumir Date: Mon, 14 Sep 2026 23:15:49 +0900 Subject: [PATCH 3/4] wip --- docs/rules/no-emphasis-as-headings.md | 2 + src/rules/no-emphasis-as-headings.js | 48 ++++++++++++++++++++- tests/rules/no-emphasis-as-headings.test.js | 14 ++++++ 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/docs/rules/no-emphasis-as-headings.md b/docs/rules/no-emphasis-as-headings.md index 5471154d..086701e2 100644 --- a/docs/rules/no-emphasis-as-headings.md +++ b/docs/rules/no-emphasis-as-headings.md @@ -76,4 +76,6 @@ No options are available for this rule. ## Prior Art +- [MD036 - no-emphasis-as-heading](https://github.com/DavidAnson/markdownlint/blob/main/doc/md036.md#md036---emphasis-used-instead-of-a-heading) +- [remark-lint-no-emphasis-as-heading](https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-no-emphasis-as-heading#remark-lint-no-emphasis-as-heading) - [textlint-rule-no-bold-paragraph](https://github.com/aborazmeh/textlint-rule-no-bold-paragraph) diff --git a/src/rules/no-emphasis-as-headings.js b/src/rules/no-emphasis-as-headings.js index ec360532..9f0570f6 100644 --- a/src/rules/no-emphasis-as-headings.js +++ b/src/rules/no-emphasis-as-headings.js @@ -11,7 +11,7 @@ * @import { Emphasis, Strong } from "mdast"; * @import { MarkdownRuleDefinition } from "../types.js"; * @typedef {"noEmphasisAsHeadings"} NoEmphasisAsHeadingsMessageIds - * @typedef {[]} NoEmphasisAsHeadingsOptions + * @typedef {[{ punctuation?: string[] }]} NoEmphasisAsHeadingsOptions * @typedef {MarkdownRuleDefinition<{ RuleOptions: NoEmphasisAsHeadingsOptions, MessageIds: NoEmphasisAsHeadingsMessageIds }>} NoEmphasisAsHeadingsRuleDefinition */ @@ -36,11 +36,48 @@ export default /** @satisfies {NoEmphasisAsHeadingsRuleDefinition} */ ({ "Unexpected emphasis or strong used as a heading.", }, - // option: TODO + schema: [ + { + type: "object", + properties: { + punctuation: { + type: "array", + items: { + type: "string", + minLength: 1, + maxLength: 1, + }, + minItems: 1, + uniqueItems: true, + }, + }, + additionalProperties: false, + }, + ], + + defaultOptions: [ + { + punctuation: [ + ".", + ",", + ";", + ":", + "!", + "?", + "。", + "\uFF0C", // `,` + "\uFF1B", // `;` + "\uFF1A", // `:` + "\uFF01", // `!` + "\uFF1F", // `?` + ], + }, + ], }, create(context) { const { sourceCode } = context; + const [{ punctuation }] = context.options; let isInBlockquote = false; let isInListItem = false; @@ -60,6 +97,13 @@ export default /** @satisfies {NoEmphasisAsHeadingsRuleDefinition} */ ({ return; } + const count = node.type === "emphasis" ? 1 : 2; + const text = sourceCode.getText(node, -count, -count); + + if (punctuation.includes(text.at(-1))) { + return; + } + const parentNode = sourceCode.getParent(node); if ( diff --git a/tests/rules/no-emphasis-as-headings.test.js b/tests/rules/no-emphasis-as-headings.test.js index 5615adad..384770f6 100644 --- a/tests/rules/no-emphasis-as-headings.test.js +++ b/tests/rules/no-emphasis-as-headings.test.js @@ -44,6 +44,20 @@ ruleTester.run("no-emphasis-as-headings", rule, { "**foo** **bar**", "** foo **", // This is not a `strong` node. + // Punctuation + "*foo.*", + "*foo,*", + "*foo;*", + "*foo:*", + "*foo!*", + "*foo?*", + "*foo。*", + "*foo\uFF0C*", // `,` + "*foo\uFF1B*", // `;` + "*foo\uFF1A*", // `:` + "*foo\uFF01*", // `!` + "*foo\uFF1F*", // `?` + // Multiline "*foo\nbar*", "_foo\nbar_", From de3ba26b3c4b1fbcfd9bbdb37ea182d050173d16 Mon Sep 17 00:00:00 2001 From: lumir Date: Mon, 14 Sep 2026 23:29:31 +0900 Subject: [PATCH 4/4] wip --- src/rules/no-emphasis-as-headings.js | 11 +- tests/rules/no-emphasis-as-headings.test.js | 189 +++++++++++++++++++- 2 files changed, 198 insertions(+), 2 deletions(-) diff --git a/src/rules/no-emphasis-as-headings.js b/src/rules/no-emphasis-as-headings.js index 9f0570f6..551efd26 100644 --- a/src/rules/no-emphasis-as-headings.js +++ b/src/rules/no-emphasis-as-headings.js @@ -80,6 +80,7 @@ export default /** @satisfies {NoEmphasisAsHeadingsRuleDefinition} */ ({ const [{ punctuation }] = context.options; let isInBlockquote = false; + let isInFootnoteDefinition = false; let isInListItem = false; return { @@ -87,12 +88,16 @@ export default /** @satisfies {NoEmphasisAsHeadingsRuleDefinition} */ ({ isInBlockquote = true; }, + footnoteDefinition() { + isInFootnoteDefinition = true; + }, + listItem() { isInListItem = true; }, "emphasis, strong"(/** @type {Emphasis | Strong} */ node) { - if (isInBlockquote || isInListItem) { + if (isInBlockquote || isInFootnoteDefinition || isInListItem) { // Early return if inside a blockquote or list item. return; } @@ -126,6 +131,10 @@ export default /** @satisfies {NoEmphasisAsHeadingsRuleDefinition} */ ({ isInBlockquote = false; }, + "footnoteDefinition:exit"() { + isInFootnoteDefinition = false; + }, + "listItem:exit"() { isInListItem = false; }, diff --git a/tests/rules/no-emphasis-as-headings.test.js b/tests/rules/no-emphasis-as-headings.test.js index 384770f6..377e661d 100644 --- a/tests/rules/no-emphasis-as-headings.test.js +++ b/tests/rules/no-emphasis-as-headings.test.js @@ -26,20 +26,24 @@ ruleTester.run("no-emphasis-as-headings", rule, { valid: [ "", " ", + "foo\nbar\nbaz", "foo\n\nbar\n\nbaz\n\nqux", + "`*foo*`", + "*foo*\nbar\nbaz", "_foo_\nbar\nbaz", "**foo**\nbar\nbaz", "__foo__\nbar\nbaz", "***foo***\nbar\nbaz", "___foo___\nbar\nbaz", - "foo\nbar\nbaz", + "foo\n*bar*\nbaz", "foo\n_bar_\nbaz", "foo\n**bar**\nbaz", "foo\n__bar__\nbaz", "foo\n***bar***\nbaz", "foo\n___bar___\nbaz", + "**foo** bar\n\nfoo **bar**", "**foo** **bar**", "** foo **", // This is not a `strong` node. @@ -161,6 +165,43 @@ ruleTester.run("no-emphasis-as-headings", rule, { language: "markdown/gfm", }, + // `footnoteDefinition` is not checked by this rule. + // This behavior aligns with `markdownlint`. + { + code: "[^1]: *foo*", + language: "markdown/gfm", + }, + { + code: "[^1]: _foo_", + language: "markdown/gfm", + }, + { + code: "[^1]: **foo**", + language: "markdown/gfm", + }, + { + code: "[^1]: __foo__", + language: "markdown/gfm", + }, + + // Options + { + code: "*foo)*", + options: [ + { + punctuation: [")"], + }, + ], + }, + { + code: "*foo$*", + options: [ + { + punctuation: ["$"], + }, + ], + }, + // TODO: // "*foo _bar_ baz*", // "_foo *bar* baz_", @@ -294,6 +335,152 @@ ruleTester.run("no-emphasis-as-headings", rule, { ], }, + // Punctuation + { + code: "*foo.b*", + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 8, + }, + ], + }, + { + code: "*foo,b*", + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 8, + }, + ], + }, + { + code: "*foo;b*", + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 8, + }, + ], + }, + { + code: "*foo:b*", + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 8, + }, + ], + }, + { + code: "*foo!b*", + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 8, + }, + ], + }, + { + code: "*foo?b*", + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 8, + }, + ], + }, + { + code: "*foo。b*", + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 8, + }, + ], + }, + { + code: "*foo\uFF0Cb*", // `,` + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 8, + }, + ], + }, + { + code: "*foo\uFF1Bb*", // `;` + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 8, + }, + ], + }, + { + code: "*foo\uFF1Ab*", // `:` + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 8, + }, + ], + }, + { + code: "*foo\uFF01b*", // `!` + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 8, + }, + ], + }, + { + code: "*foo\uFF1Fb*", // `?` + errors: [ + { + messageId: "noEmphasisAsHeadings", + line: 1, + column: 1, + endLine: 1, + endColumn: 8, + }, + ], + }, + { code: "foo\n\n**bar**\n\nbaz\n\n__qux__", errors: [