From 9739c5feac91ab34f75e3aaf73e12b6447bba50a Mon Sep 17 00:00:00 2001 From: KumJungMin <37934668+KumJungMin@users.noreply.github.com> Date: Mon, 7 Sep 2026 18:22:05 +0900 Subject: [PATCH 1/5] refactor: extract identifier normalization into a shared utility Fixes #731 --- src/rules/no-duplicate-definitions.js | 10 +++--- src/rules/no-empty-definitions.js | 11 +++--- src/rules/no-reference-like-urls.js | 5 ++- src/rules/no-unused-definitions.js | 10 +++--- src/util.js | 10 ++++++ tests/util.test.js | 50 ++++++++++++++++++++++++++- 6 files changed, 73 insertions(+), 23 deletions(-) diff --git a/src/rules/no-duplicate-definitions.js b/src/rules/no-duplicate-definitions.js index 70ef7cd3..0d1d5a40 100644 --- a/src/rules/no-duplicate-definitions.js +++ b/src/rules/no-duplicate-definitions.js @@ -7,7 +7,7 @@ // Imports //----------------------------------------------------------------------------- -import { normalizeIdentifier } from "micromark-util-normalize-identifier"; +import { normalizeIdentifier } from "../util.js"; //----------------------------------------------------------------------------- // Type Definitions @@ -81,13 +81,11 @@ export default /** @satisfies {NoDuplicateDefinitionsRuleDefinition} */ ({ create(context) { const allowDefinitions = new Set( - context.options[0].allowDefinitions.map(identifier => - normalizeIdentifier(identifier).toLowerCase(), - ), + context.options[0].allowDefinitions.map(normalizeIdentifier), ); const allowFootnoteDefinitions = new Set( - context.options[0].allowFootnoteDefinitions.map(identifier => - normalizeIdentifier(identifier).toLowerCase(), + context.options[0].allowFootnoteDefinitions.map( + normalizeIdentifier, ), ); const [{ checkFootnoteDefinitions }] = context.options; diff --git a/src/rules/no-empty-definitions.js b/src/rules/no-empty-definitions.js index 824734bd..9adbb068 100644 --- a/src/rules/no-empty-definitions.js +++ b/src/rules/no-empty-definitions.js @@ -7,8 +7,7 @@ // Imports //----------------------------------------------------------------------------- -import { normalizeIdentifier } from "micromark-util-normalize-identifier"; -import { htmlCommentPattern } from "../util.js"; +import { htmlCommentPattern, normalizeIdentifier } from "../util.js"; //----------------------------------------------------------------------------- // Type Definitions @@ -96,13 +95,11 @@ export default /** @satisfies {NoEmptyDefinitionsRuleDefinition} */ ({ create(context) { const allowDefinitions = new Set( - context.options[0].allowDefinitions.map(identifier => - normalizeIdentifier(identifier).toLowerCase(), - ), + context.options[0].allowDefinitions.map(normalizeIdentifier), ); const allowFootnoteDefinitions = new Set( - context.options[0].allowFootnoteDefinitions.map(identifier => - normalizeIdentifier(identifier).toLowerCase(), + context.options[0].allowFootnoteDefinitions.map( + normalizeIdentifier, ), ); const [{ checkFootnoteDefinitions }] = context.options; diff --git a/src/rules/no-reference-like-urls.js b/src/rules/no-reference-like-urls.js index 68b95203..794e8241 100644 --- a/src/rules/no-reference-like-urls.js +++ b/src/rules/no-reference-like-urls.js @@ -7,7 +7,7 @@ // Imports //----------------------------------------------------------------------------- -import { normalizeIdentifier } from "micromark-util-normalize-identifier"; +import { normalizeIdentifier } from "../util.js"; //----------------------------------------------------------------------------- // Type Definitions @@ -79,8 +79,7 @@ export default /** @satisfies {NoReferenceLikeUrlsRuleDefinition} */ ({ const { label, destination } = match.groups; const { type, title } = node; const prefix = type === "image" ? "!" : ""; - const url = - normalizeIdentifier(destination).toLowerCase(); + const url = normalizeIdentifier(destination); if (definitionIdentifiers.has(url)) { context.report({ diff --git a/src/rules/no-unused-definitions.js b/src/rules/no-unused-definitions.js index 71315032..7a231314 100644 --- a/src/rules/no-unused-definitions.js +++ b/src/rules/no-unused-definitions.js @@ -7,7 +7,7 @@ // Imports //----------------------------------------------------------------------------- -import { normalizeIdentifier } from "micromark-util-normalize-identifier"; +import { normalizeIdentifier } from "../util.js"; //----------------------------------------------------------------------------- // Type Definitions @@ -81,13 +81,11 @@ export default /** @satisfies {NoUnusedDefinitionsRuleDefinition} */ ({ create(context) { const allowDefinitions = new Set( - context.options[0].allowDefinitions.map(identifier => - normalizeIdentifier(identifier).toLowerCase(), - ), + context.options[0].allowDefinitions.map(normalizeIdentifier), ); const allowFootnoteDefinitions = new Set( - context.options[0].allowFootnoteDefinitions.map(identifier => - normalizeIdentifier(identifier).toLowerCase(), + context.options[0].allowFootnoteDefinitions.map( + normalizeIdentifier, ), ); const [{ checkFootnoteDefinitions }] = context.options; diff --git a/src/util.js b/src/util.js index b69e80eb..5c68e052 100644 --- a/src/util.js +++ b/src/util.js @@ -1,3 +1,5 @@ +import { normalizeIdentifier as micromarkUtilNormalizeIdentifier } from "micromark-util-normalize-identifier"; + /** * @fileoverview Utility Library * @author Nicholas C. Zakas @@ -64,3 +66,11 @@ export function stripHtmlComments(value) { match.replace(/[^\r\n]/g, " "), ); } +/** + * Normalizes a Markdown reference identifier. + * @param {string} identifier The identifier to normalize. + * @returns {string} The normalized lowercase identifier. + */ +export function normalizeIdentifier(identifier) { + return micromarkUtilNormalizeIdentifier(identifier).toLowerCase(); +} diff --git a/tests/util.test.js b/tests/util.test.js index ccaaf5f8..a2aa753a 100644 --- a/tests/util.test.js +++ b/tests/util.test.js @@ -8,7 +8,11 @@ //------------------------------------------------------------------------------ import assert from "node:assert"; -import { frontmatterHasTitle, stripHtmlComments } from "../src/util.js"; +import { + frontmatterHasTitle, + normalizeIdentifier, + stripHtmlComments, +} from "../src/util.js"; //------------------------------------------------------------------------------ // Tests @@ -98,4 +102,48 @@ describe("util", () => { assert.strictEqual(result, `Hello${" ".repeat(16)}World`); }); }); + + describe("normalizeIdentifier()", () => { + it("should preserve an already normalized identifier", () => { + const input = "example"; + const result = normalizeIdentifier(input); + assert.strictEqual(result, "example"); + }); + + it("should convert a mixed-case identifier to lowercase", () => { + const input = "ExAmPlE"; + const result = normalizeIdentifier(input); + assert.strictEqual(result, "example"); + }); + + it("should trim leading and trailing spaces", () => { + const input = " example "; + const result = normalizeIdentifier(input); + assert.strictEqual(result, "example"); + }); + + it("should collapse consecutive internal spaces into a single space", () => { + const input = "foo bar"; + const result = normalizeIdentifier(input); + assert.strictEqual(result, "foo bar"); + }); + + it("should normalize tabs and line breaks to a single space", () => { + const input = "foo\t\r\nbar"; + const result = normalizeIdentifier(input); + assert.strictEqual(result, "foo bar"); + }); + + it("should preserve an empty string", () => { + const input = ""; + const result = normalizeIdentifier(input); + assert.strictEqual(result, ""); + }); + + it("should return an empty string for Markdown whitespace only", () => { + const input = " \t\r\n "; + const result = normalizeIdentifier(input); + assert.strictEqual(result, ""); + }); + }); }); From 258a91013af4e9094f1357d7fc9ad5c2ae1c5518 Mon Sep 17 00:00:00 2001 From: KumJungMin <37934668+KumJungMin@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:36:45 +0900 Subject: [PATCH 2/5] chore: add spacing between utility functions --- src/util.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/util.js b/src/util.js index 5c68e052..aae64fd5 100644 --- a/src/util.js +++ b/src/util.js @@ -66,6 +66,7 @@ export function stripHtmlComments(value) { match.replace(/[^\r\n]/g, " "), ); } + /** * Normalizes a Markdown reference identifier. * @param {string} identifier The identifier to normalize. From b4275ede25f9bb8b25752467697e81eb6bebc227 Mon Sep 17 00:00:00 2001 From: KumJungMin <37934668+KumJungMin@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:37:12 +0900 Subject: [PATCH 3/5] chore: place utility import below file overview --- src/util.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/util.js b/src/util.js index aae64fd5..426658fe 100644 --- a/src/util.js +++ b/src/util.js @@ -1,10 +1,14 @@ -import { normalizeIdentifier as micromarkUtilNormalizeIdentifier } from "micromark-util-normalize-identifier"; - /** * @fileoverview Utility Library * @author Nicholas C. Zakas */ +//------------------------------------------------------------------------------ +// Imports +//------------------------------------------------------------------------------ + +import { normalizeIdentifier as micromarkUtilNormalizeIdentifier } from "micromark-util-normalize-identifier"; + //----------------------------------------------------------------------------- // Regex Patterns //----------------------------------------------------------------------------- From f143ddaeda9cfa1069418d164f2eb63e666d19f8 Mon Sep 17 00:00:00 2001 From: KumJungMin <37934668+KumJungMin@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:37:34 +0900 Subject: [PATCH 4/5] chore: sort utility functions alphabetically --- src/util.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/util.js b/src/util.js index 426658fe..314ce671 100644 --- a/src/util.js +++ b/src/util.js @@ -54,6 +54,15 @@ export function frontmatterHasTitle(value, pattern) { return false; } +/** + * Normalizes a Markdown reference identifier. + * @param {string} identifier The identifier to normalize. + * @returns {string} The normalized lowercase identifier. + */ +export function normalizeIdentifier(identifier) { + return micromarkUtilNormalizeIdentifier(identifier).toLowerCase(); +} + /** * Replaces all HTML comments with whitespace. * This preserves offsets and locations of characters @@ -70,12 +79,3 @@ export function stripHtmlComments(value) { match.replace(/[^\r\n]/g, " "), ); } - -/** - * Normalizes a Markdown reference identifier. - * @param {string} identifier The identifier to normalize. - * @returns {string} The normalized lowercase identifier. - */ -export function normalizeIdentifier(identifier) { - return micromarkUtilNormalizeIdentifier(identifier).toLowerCase(); -} From 65c851a321539bca7bacc9bd6300a86108d1a486 Mon Sep 17 00:00:00 2001 From: KumJungMin <37934668+KumJungMin@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:38:19 +0900 Subject: [PATCH 5/5] test: sort utility tests alphabetically --- tests/util.test.js | 54 +++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/tests/util.test.js b/tests/util.test.js index a2aa753a..64e3bb87 100644 --- a/tests/util.test.js +++ b/tests/util.test.js @@ -76,33 +76,6 @@ describe("util", () => { }); }); - describe("stripHtmlComments()", () => { - it("should replace single-line HTML comments with spaces", () => { - const input = "HelloWorld"; - const result = stripHtmlComments(input); - assert.strictEqual(input.length, result.length); - assert.strictEqual(result, `Hello${" ".repeat(14)}World`); - }); - - it("should replace multi-line HTML comments with spaces", () => { - const input = "HelloWorld"; - const result = stripHtmlComments(input); - assert.strictEqual(input.length, result.length); - assert.strictEqual( - result, - `Hello${" ".repeat(4)}\r\n${" ".repeat(5)}\n${" ".repeat(4)}\r${" ".repeat(7)}\r\n${" ".repeat(3)}World`, - ); - }); - - it("should handle surrogate pairs like emojis correctly", () => { - // NOTE: 👍's length is 2, 🚀's length is 2, 🙇‍♂️'s length is 5. - const input = "HelloWorld"; - const result = stripHtmlComments(input); - assert.strictEqual(input.length, result.length); - assert.strictEqual(result, `Hello${" ".repeat(16)}World`); - }); - }); - describe("normalizeIdentifier()", () => { it("should preserve an already normalized identifier", () => { const input = "example"; @@ -146,4 +119,31 @@ describe("util", () => { assert.strictEqual(result, ""); }); }); + + describe("stripHtmlComments()", () => { + it("should replace single-line HTML comments with spaces", () => { + const input = "HelloWorld"; + const result = stripHtmlComments(input); + assert.strictEqual(input.length, result.length); + assert.strictEqual(result, `Hello${" ".repeat(14)}World`); + }); + + it("should replace multi-line HTML comments with spaces", () => { + const input = "HelloWorld"; + const result = stripHtmlComments(input); + assert.strictEqual(input.length, result.length); + assert.strictEqual( + result, + `Hello${" ".repeat(4)}\r\n${" ".repeat(5)}\n${" ".repeat(4)}\r${" ".repeat(7)}\r\n${" ".repeat(3)}World`, + ); + }); + + it("should handle surrogate pairs like emojis correctly", () => { + // NOTE: 👍's length is 2, 🚀's length is 2, 🙇‍♂️'s length is 5. + const input = "HelloWorld"; + const result = stripHtmlComments(input); + assert.strictEqual(input.length, result.length); + assert.strictEqual(result, `Hello${" ".repeat(16)}World`); + }); + }); });