diff --git a/packages/repl-sdk/README.md b/packages/repl-sdk/README.md index 47bc65d07..781336cd0 100644 --- a/packages/repl-sdk/README.md +++ b/packages/repl-sdk/README.md @@ -13,3 +13,34 @@ Features: ## Usage +### Heading ids + +Every markdown heading gets an `id`, so in-page anchors can link to sections. + +Ids match what GitHub generates for the same markdown, via +[`github-slugger`][github-slugger]: + +``` +### `setupMirage` -> #setupmirage +### V2 JSON:API -> #v2-jsonapi +``` + +A `.md` file is typically read in two places — a rendered site, and the repo on +GitHub — and an in-page `#anchor` only resolves in both if the two agree on how +the id is derived. + +Like GitHub, repeated headings within a document are de-duplicated (`#usage`, +`#usage-1`, `#usage-2`), and the numbering restarts for each document. +Whitespace the author wrote is preserved rather than collapsed, also matching +GitHub: `## Hello World` becomes `#hello----world`. + +A heading with an explicit `{#custom-id}` suffix keeps that id instead. + +> [!NOTE] +> Heading anchors are not part of the [GFM spec][gfm-spec], which covers +> autolink literals, footnotes, strikethrough, tables and tasklists. GitHub +> generates them in its rendering layer — `github-slugger` is that behavior, +> extracted. + +[github-slugger]: https://github.com/Flet/github-slugger +[gfm-spec]: https://github.github.com/gfm/ diff --git a/packages/repl-sdk/package.json b/packages/repl-sdk/package.json index 2e1735ce4..909c1d003 100644 --- a/packages/repl-sdk/package.json +++ b/packages/repl-sdk/package.json @@ -86,7 +86,6 @@ "@lezer/markdown": "^1.6.3", "@replit/codemirror-lang-svelte": "^6.0.0", "@shikijs/rehype": "^3.22.0", - "change-case": "^5.4.4", "codemirror": "^6.0.2", "codemirror-lang-glimdown": "workspace:^", "codemirror-lang-glimmer": "workspace:^", @@ -95,7 +94,9 @@ "codemirror-languageserver": "^1.12.1", "comlink": "^4.4.2", "es-module-shims": "^2.8.0", + "github-slugger": "^2.0.0", "mdast": "^3.0.0", + "mdast-util-to-string": "^4.0.0", "mime": "^4.0.7", "package-name-regex": "^5.0.0", "rehype-autolink-headings": "^7.1.0", diff --git a/packages/repl-sdk/src/compilers/markdown/heading-id.js b/packages/repl-sdk/src/compilers/markdown/heading-id.js index c60f4d14e..c062cedd4 100644 --- a/packages/repl-sdk/src/compilers/markdown/heading-id.js +++ b/packages/repl-sdk/src/compilers/markdown/heading-id.js @@ -1,46 +1,7 @@ -import { kebabCase } from 'change-case'; +import GithubSlugger from 'github-slugger'; +import { toString } from 'mdast-util-to-string'; import { visit } from 'unist-util-visit'; -/** - * @param {import('mdast').PhrasingContent[]} children - * @return {string} - */ -function getDefaultId(children) { - return formatDefaultId(extractText(children)); -} - -/** - * @param {import('mdast').PhrasingContent[]} children - * @return {string} - */ -function extractText(children) { - return children - .map( - /** - * @param {any} child - */ - (child) => { - const isEmpty = !child.value?.trim(); - - if (!isEmpty) { - return child.value; - } else if (child.children && child.children.length > 0) { - return extractText(child.children); - } else { - return ''; - } - } - ) - .join(' '); -} - -/** - * @param {string} value - */ -function formatDefaultId(value) { - return kebabCase(value.replaceAll(/\s+/g, ' ').trim()); -} - /** * @param {import('mdast').Heading} node * @param {string} id @@ -52,11 +13,13 @@ function setNodeId(node, id) { /** @type {any} */ (node.data).id = node.data.hProperties.id = id; } -export function headingId(options = { defaults: false }) { +export function headingId() { /** * @param {import('mdast').Root} node */ return function (node) { + const slugger = new GithubSlugger(); + visit(node, 'heading', (node) => { const lastChild = node.children[node.children.length - 1]; @@ -81,7 +44,7 @@ export function headingId(options = { defaults: false }) { } } - setNodeId(node, getDefaultId(node.children)); + setNodeId(node, slugger.slug(toString(node))); }); }; } diff --git a/packages/repl-sdk/src/compilers/markdown/parse.test.ts b/packages/repl-sdk/src/compilers/markdown/parse.test.ts index 87815ba62..6cb32c1df 100644 --- a/packages/repl-sdk/src/compilers/markdown/parse.test.ts +++ b/packages/repl-sdk/src/compilers/markdown/parse.test.ts @@ -230,30 +230,66 @@ describe('default features', () => { expect(result).toMatchInlineSnapshot(` { "codeBlocks": [], - "text": "
<Hello @foo="two" /><Hello @foo="two" />