From 0278c7e70cfb53b9e8f76f1af71a8c58f244732a Mon Sep 17 00:00:00 2001 From: Juraj Kapsz Date: Sat, 11 Jan 2025 18:28:37 +0100 Subject: [PATCH 1/3] feat: improve heading id slug format --- .changeset/tiny-parrots-unite.md | 5 ++ .../remark/src/rehype-collect-headings.ts | 10 ++- .../test/rehype-collect-headings.test.js | 68 +++++++++++++++++++ 3 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 .changeset/tiny-parrots-unite.md create mode 100644 packages/markdown/remark/test/rehype-collect-headings.test.js diff --git a/.changeset/tiny-parrots-unite.md b/.changeset/tiny-parrots-unite.md new file mode 100644 index 000000000000..56a65d9e56d3 --- /dev/null +++ b/.changeset/tiny-parrots-unite.md @@ -0,0 +1,5 @@ +--- +'@astrojs/markdown-remark': patch +--- + +Makes generated heading ID slug values more kebab-case friendly diff --git a/packages/markdown/remark/src/rehype-collect-headings.ts b/packages/markdown/remark/src/rehype-collect-headings.ts index ab2113f494ef..9637fa84ffb1 100644 --- a/packages/markdown/remark/src/rehype-collect-headings.ts +++ b/packages/markdown/remark/src/rehype-collect-headings.ts @@ -55,13 +55,11 @@ export function rehypeHeadingIds(): ReturnType { } }); - node.properties = node.properties || {}; + node.properties ??= {}; if (typeof node.properties.id !== 'string') { - let slug = slugger.slug(text); - - if (slug.endsWith('-')) slug = slug.slice(0, -1); - - node.properties.id = slug; + const NBSP = /[\u00A0\u2007\u202F]/g; + const EXTRA_HYPHENS = /^-+|(?<=-)-+|-+$/g; + node.properties.id = slugger.slug(text.replace(NBSP, ' ')).replace(EXTRA_HYPHENS, ''); } headings.push({ depth, slug: node.properties.id, text }); diff --git a/packages/markdown/remark/test/rehype-collect-headings.test.js b/packages/markdown/remark/test/rehype-collect-headings.test.js new file mode 100644 index 000000000000..a107f1e96036 --- /dev/null +++ b/packages/markdown/remark/test/rehype-collect-headings.test.js @@ -0,0 +1,68 @@ +import { createMarkdownProcessor } from '../dist/index.js'; + +import * as assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; + +describe('rehypeHeadingIds()', () => { + let processor; + + before(async () => { + processor = await createMarkdownProcessor(); + }); + + describe('generated ID slug', () => { + it('treats non-breaking space as space', async () => { + /** + * NO-BREAK SPACE ` `, FIGURE SPACE ` `, NARROW NO-BREAK SPACE + * */ + const NON_BREAKING_SPACES = [' ', ' ', ' ']; + + const { + metadata: { + headings: [heading], + }, + } = await processor.render(markdownHeading()); + + assert.equal(heading.slug, expectedSlug()); + + /** + * Value function + * @returns {string} markdown heading with defined `NON_BREAKING_SPACES` in text + * */ + function markdownHeading() { + return `## text${NON_BREAKING_SPACES.join('text')}text`; + } + + /** + * Value function + * @returns {string} expected slug value of `markdownHeading()` + * */ + function expectedSlug() { + return `text-${NON_BREAKING_SPACES.map(() => 'text').join('-')}`; + } + }); + + it('is in kebab-case', async () => { + const EXPECTED_SLUG = 'lorem-ipsum-dolor-sit-amet'; + const MARKDOWN_HEADING = '## 😄 😄 Lorem 😄 ipsum 😄 😄 😄 dolor ✓ sit   amet 😄 😄'; + + const { + metadata: { + headings: [heading], + }, + } = await processor.render(MARKDOWN_HEADING); + + assert.equal(heading.slug, EXPECTED_SLUG); + }); + + it('has unique value within given set of headings', async () => { + const repeatedMarkdownHeadings = `${'## Lorem ipsum dolor sit amet\n\n'.repeat(3)}`; + + const { + metadata: { headings }, + } = await processor.render(repeatedMarkdownHeadings); + + assert.equal(headings.length, new Set(headings.map(({ slug }) => slug)).size); + }); + }); +}); From d2c1112c393c85bc25ade7cbacf6a047f4a7f76f Mon Sep 17 00:00:00 2001 From: Juraj Kapsz Date: Sun, 12 Jan 2025 15:09:06 +0100 Subject: [PATCH 2/3] minor: code cleanup --- .../test/rehype-collect-headings.test.js | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/markdown/remark/test/rehype-collect-headings.test.js b/packages/markdown/remark/test/rehype-collect-headings.test.js index a107f1e96036..a79e18ba18e9 100644 --- a/packages/markdown/remark/test/rehype-collect-headings.test.js +++ b/packages/markdown/remark/test/rehype-collect-headings.test.js @@ -1,7 +1,6 @@ -import { createMarkdownProcessor } from '../dist/index.js'; - import * as assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; +import { createMarkdownProcessor } from '../dist/index.js'; describe('rehypeHeadingIds()', () => { let processor; @@ -21,24 +20,26 @@ describe('rehypeHeadingIds()', () => { metadata: { headings: [heading], }, - } = await processor.render(markdownHeading()); + } = await processor.render(markdownHeading(NON_BREAKING_SPACES)); - assert.equal(heading.slug, expectedSlug()); + assert.equal(heading.slug, expectedSlug(NON_BREAKING_SPACES)); /** - * Value function - * @returns {string} markdown heading with defined `NON_BREAKING_SPACES` in text + * Helper function + * @param {string[]} nbsp - list of non-breaking spaces + * @returns {string} markdown heading with given `nbsp` list * */ - function markdownHeading() { - return `## text${NON_BREAKING_SPACES.join('text')}text`; + function markdownHeading(nbsp = []) { + return `## text${nbsp.join('text')}text`; } /** - * Value function - * @returns {string} expected slug value of `markdownHeading()` + * Helper function + * @param {string[]} nbsp - list of non-breaking spaces + * @returns {string} expected slug value of `markdownHeading()` with given `nbsp` list * */ - function expectedSlug() { - return `text-${NON_BREAKING_SPACES.map(() => 'text').join('-')}`; + function expectedSlug(nbsp = []) { + return `text-${nbsp.map(() => 'text').join('-')}`; } }); From 7229c396ecec4fdcfbb080067bbc242abcf6ec33 Mon Sep 17 00:00:00 2001 From: Juraj Kapsz Date: Sun, 12 Jan 2025 16:21:45 +0100 Subject: [PATCH 3/3] minor: improve test assertion readability --- .../markdown/remark/test/rehype-collect-headings.test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/markdown/remark/test/rehype-collect-headings.test.js b/packages/markdown/remark/test/rehype-collect-headings.test.js index a79e18ba18e9..e439799a923b 100644 --- a/packages/markdown/remark/test/rehype-collect-headings.test.js +++ b/packages/markdown/remark/test/rehype-collect-headings.test.js @@ -63,7 +63,10 @@ describe('rehypeHeadingIds()', () => { metadata: { headings }, } = await processor.render(repeatedMarkdownHeadings); - assert.equal(headings.length, new Set(headings.map(({ slug }) => slug)).size); + const numberOfHeadings = headings.length; + const numberOfUniqueIdValues = new Set(headings.map(({ slug }) => slug)).size; + + assert.equal(numberOfHeadings, numberOfUniqueIdValues); }); }); });