From 91187369697e88bc57987f57079f1c3b17bd3637 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Fri, 14 Aug 2026 16:07:57 -0700 Subject: [PATCH 1/9] feat(repl-sdk): let consumers choose how heading ids are slugged `headingId` always kebab-cases the heading text. That is a reasonable default, but it is not what GitHub generates for the same markdown, so an in-page `#anchor` can only resolve in one place at a time: ### `setupMirage` kebabCase: #setup-mirage GitHub: #setupmirage ### V2 JSON:API kebabCase: #v2-json-api GitHub: #v2-jsonapi For docs that are read both on a rendered site and as `.md` files in the repo, that split is the whole problem -- links authored against one are dead in the other. Adds an optional `headingId.slug` to the markdown compiler options. The default is unchanged, so this is not breaking; consumers who want GitHub-compatible anchors can pass `github-slugger`: buildCompiler({ headingId: { slug: (text) => slugger.slug(text) } }) Also normalizes the extracted text before slugging. `extractText` joins a heading's children with a space on top of the spacing the text nodes already carry, so `## Hello *there*` extracts as `'Hello there'`. `kebabCase` happens not to care, but any slugger that maps runs of space to `-` emits `hello--there` -- github-slugger does exactly that. Normalizing at the boundary keeps the option usable. Tests cover the default, a custom slug, the text handed to it, the whitespace case, and that an explicit `{#custom-id}` still wins. --- .../src/compilers/markdown/build-compiler.js | 5 +- .../src/compilers/markdown/heading-id.js | 46 ++++++++++++---- .../src/compilers/markdown/parse.test.ts | 54 +++++++++++++++++++ .../repl-sdk/src/compilers/markdown/types.ts | 9 ++++ 4 files changed, 103 insertions(+), 11 deletions(-) diff --git a/packages/repl-sdk/src/compilers/markdown/build-compiler.js b/packages/repl-sdk/src/compilers/markdown/build-compiler.js index 44072dc60..eae675822 100644 --- a/packages/repl-sdk/src/compilers/markdown/build-compiler.js +++ b/packages/repl-sdk/src/compilers/markdown/build-compiler.js @@ -20,7 +20,10 @@ import { sanitizeForGlimmer } from './sanitize-for-glimmer.js'; * @returns {import('unified').Processor} */ export function buildCompiler(options) { - let compiler = unified().use(remarkParse).use(remarkGfm, { singleTilde: true }).use(headingId); + let compiler = unified() + .use(remarkParse) + .use(remarkGfm, { singleTilde: true }) + .use(headingId, options.headingId); /** * If this were "use"d after `remarkRehype`, diff --git a/packages/repl-sdk/src/compilers/markdown/heading-id.js b/packages/repl-sdk/src/compilers/markdown/heading-id.js index 4f2647d9e..e1baa98ce 100644 --- a/packages/repl-sdk/src/compilers/markdown/heading-id.js +++ b/packages/repl-sdk/src/compilers/markdown/heading-id.js @@ -1,14 +1,6 @@ import { kebabCase } from 'change-case'; 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} @@ -34,6 +26,18 @@ function extractText(children) { .join(' '); } +/** + * `extractText` joins a heading's children with a space, on top of whatever + * spacing the text nodes already carry, so `## Hello *there*` extracts as + * `'Hello there'`. Collapse that before slugging: `kebabCase` happens not to + * care, but a slugger that maps space to `-` would emit `hello--there`. + * + * @param {string} value + */ +function normalizeText(value) { + return value.replaceAll(/\s+/g, ' ').trim(); +} + /** * @param {string} value */ @@ -52,7 +56,29 @@ function setNodeId(node, id) { /** @type {any} */ (node.data).id = node.data.hProperties.id = id; } -export function headingId(options = { defaults: false }) { +/** + * Assign an `id` to every heading, so anchors can link to sections. + * + * By default the id is the heading's text in kebab-case. Pass `slug` to + * generate it some other way -- for example, matching what GitHub produces for + * the same markdown, so an in-page `#anchor` resolves both in a rendered site + * and in the `.md` file on GitHub: + * + * ```js + * import GithubSlugger from 'github-slugger'; + * + * const slugger = new GithubSlugger(); + * + * headingId({ slug: (text) => slugger.slug(text) }); + * ``` + * + * A heading with an explicit `{#custom-id}` suffix is left alone either way. + * + * @param {{ slug?: (text: string) => string }} [options] + */ +export function headingId(options) { + const slug = typeof options?.slug === 'function' ? options.slug : formatDefaultId; + /** * @param {import('mdast').Root} node */ @@ -69,7 +95,7 @@ export function headingId(options = { defaults: false }) { } } - setNodeId(node, getDefaultId(node.children)); + setNodeId(node, slug(normalizeText(extractText(node.children)))); }); }; } diff --git a/packages/repl-sdk/src/compilers/markdown/parse.test.ts b/packages/repl-sdk/src/compilers/markdown/parse.test.ts index 213679e87..7abcde8f8 100644 --- a/packages/repl-sdk/src/compilers/markdown/parse.test.ts +++ b/packages/repl-sdk/src/compilers/markdown/parse.test.ts @@ -238,6 +238,60 @@ describe('default features', () => { }); describe('options', () => { + describe('headingId', () => { + it('defaults to kebab-case', async () => { + const result = await parseMarkdown(`## setupMirage`, { ...defaults }); + + expect(result.text).toBe('

setupMirage

'); + }); + + it('uses a custom slug function when given', async () => { + const result = await parseMarkdown(`## setupMirage`, { + ...defaults, + headingId: { slug: (text: string) => text.toLowerCase() }, + }); + + expect(result.text).toBe('

setupMirage

'); + }); + + it('passes the heading text, not the node', async () => { + const seen: string[] = []; + + await parseMarkdown(`## Hello *there*`, { + ...defaults, + headingId: { + slug: (text: string) => { + seen.push(text); + + return 'x'; + }, + }, + }); + + expect(seen).to.deep.equal(['Hello there']); + }); + + it('collapses the whitespace joining introduces, so space-to-dash sluggers behave', async () => { + // `## Hello *there*` extracts as 'Hello there' before normalizing; a + // slugger that maps runs of space to '-' would otherwise emit 'hello--there'. + const result = await parseMarkdown(`## Hello *there*`, { + ...defaults, + headingId: { slug: (text: string) => text.toLowerCase().replaceAll(' ', '-') }, + }); + + expect(result.text).toContain('id="hello-there"'); + }); + + it('leaves an explicit {#custom-id} heading alone', async () => { + const result = await parseMarkdown(`## Title {#custom}`, { + ...defaults, + headingId: { slug: () => 'should-not-be-used' }, + }); + + expect(result.text).not.toContain('should-not-be-used'); + }); + }); + describe('remarkPlugins', () => { it('works', async () => { const result = await parseMarkdown(`# Title`, { diff --git a/packages/repl-sdk/src/compilers/markdown/types.ts b/packages/repl-sdk/src/compilers/markdown/types.ts index 238cf0280..ba71d96ec 100644 --- a/packages/repl-sdk/src/compilers/markdown/types.ts +++ b/packages/repl-sdk/src/compilers/markdown/types.ts @@ -14,6 +14,15 @@ export interface PublicOptions { demo?: { classList?: string[]; }; + headingId?: { + /** + * How a heading's text becomes its `id`. Defaults to kebab-case. + * + * Pass a `github-slugger` instance's `slug` to match the anchors GitHub + * generates for the same markdown. + */ + slug?: (text: string) => string; + }; remarkPlugins?: unknown[]; rehypePlugins?: unknown[]; compiler?: { process: (text: string) => Promise<{ data: { liveCode: Array } }> }; From f76b5b6fb25fb58cb81d513a1670494a514d7f83 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Fri, 14 Aug 2026 16:42:54 -0700 Subject: [PATCH 2/9] feat(repl-sdk): add a named 'gfm' heading-id mode Follows the custom-function support in the previous commit with the mode most consumers actually want, so they don't have to wire up a slugger themselves: headingId: { slug: 'gfm' } `'gfm'` produces the anchor GitHub generates for the same markdown, via github-slugger, which repl-sdk already had in its tree through rehype-slug. `'kebab'` remains the default and can now be named explicitly. Two details worth calling out: - The slugger is created per document, inside the transformer, not in the plugin factory. github-slugger carries dedupe state (`usage`, `usage-1`), and a compiler can be reused across documents -- building it once would leak counts between them. Per document also matches GitHub, where numbering restarts per file. - `filterOptions` in compilers/markdown.js only forwarded remarkPlugins and rehypePlugins, so `headingId` was dropped between the public `md` compiler and parseMarkdown -- the option would only have worked for callers reaching into buildCompiler directly. It is now forwarded, with a per-compile value taking precedence over the compiler-level one. Documents the option in the repl-sdk README, including the note that heading anchors are not actually part of the GFM spec -- GitHub generates them in its rendering layer -- so `'gfm'` is named for the expectation, not the spec. Adds 9 tests: the named modes, an unknown name throwing, per-document dedupe, numbering restarting across documents, and the filterOptions forwarding. --- packages/repl-sdk/README.md | 49 +++ packages/repl-sdk/package.json | 1 + packages/repl-sdk/src/compilers/markdown.js | 7 +- .../repl-sdk/src/compilers/markdown.test.ts | 30 ++ .../src/compilers/markdown/heading-id.js | 64 +++- .../src/compilers/markdown/parse.test.ts | 49 +++ .../repl-sdk/src/compilers/markdown/types.ts | 25 +- pnpm-lock.yaml | 319 ++++++++---------- 8 files changed, 349 insertions(+), 195 deletions(-) create mode 100644 packages/repl-sdk/src/compilers/markdown.test.ts diff --git a/packages/repl-sdk/README.md b/packages/repl-sdk/README.md index 47bc65d07..d502cbdd5 100644 --- a/packages/repl-sdk/README.md +++ b/packages/repl-sdk/README.md @@ -13,3 +13,52 @@ Features: ## Usage +### Markdown options + +#### `headingId` + +Every heading gets an `id`, so anchors can link to sections. `headingId.slug` +controls how the heading's text becomes that id. + +| `slug` | `### setupMirage` | `### V2 JSON:API` | +| --- | --- | --- | +| `'kebab'` (default) | `#setup-mirage` | `#v2-json-api` | +| `'gfm'` | `#setupmirage` | `#v2-jsonapi` | + +Set it for the compiler, or per compile: + +```js +const compiler = new Compiler({ + options: { + md: { headingId: { slug: 'gfm' } }, + }, +}); + +// or, overriding for one document +await compiler.compile('md', text, { headingId: { slug: 'gfm' } }); +``` + +Use `'gfm'` when the same `.md` file is read both in a rendered site and on +GitHub — an in-page `#anchor` can only resolve in both if the two agree on how +ids are generated, and GitHub's rule is [`github-slugger`][github-slugger]. + +Like GitHub, `'gfm'` also de-duplicates repeated headings within a document +(`#usage`, `#usage-1`, `#usage-2`). The counter restarts for each document. + +A function is accepted for anything neither mode covers. It receives the +heading's text with whitespace already collapsed: + +```js +{ headingId: { slug: (text) => text.toUpperCase() } } +``` + +A heading with an explicit `{#custom-id}` suffix is left alone in every mode. + +> [!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. `'gfm'` is named for the behavior +> people expect from GitHub-flavored markdown, not for a spec requirement. + +[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..4426006e4 100644 --- a/packages/repl-sdk/package.json +++ b/packages/repl-sdk/package.json @@ -95,6 +95,7 @@ "codemirror-languageserver": "^1.12.1", "comlink": "^4.4.2", "es-module-shims": "^2.8.0", + "github-slugger": "^2.0.0", "mdast": "^3.0.0", "mime": "^4.0.7", "package-name-regex": "^5.0.0", diff --git a/packages/repl-sdk/src/compilers/markdown.js b/packages/repl-sdk/src/compilers/markdown.js index a0088f943..21d7c6e45 100644 --- a/packages/repl-sdk/src/compilers/markdown.js +++ b/packages/repl-sdk/src/compilers/markdown.js @@ -6,7 +6,7 @@ import { buildCodeFenceMetaUtils } from './markdown/utils.js'; /** * @param {unknown} [ options ] - * @returns {{ remarkPlugins: Plugin[], rehypePlugins: Plugin[] }} + * @returns {{ remarkPlugins: Plugin[], rehypePlugins: Plugin[], headingId?: import('./markdown/types.ts').HeadingIdOptions }} */ export function filterOptions(options) { if (!isRecord(options)) { @@ -16,6 +16,9 @@ export function filterOptions(options) { return { remarkPlugins: /** @type {Plugin[]}*/ (options?.remarkPlugins || []), rehypePlugins: /** @type {Plugin[]}*/ (options?.rehypePlugins || []), + headingId: /** @type {import('./markdown/types.ts').HeadingIdOptions | undefined}*/ ( + options?.headingId + ), }; } @@ -55,6 +58,8 @@ export const md = { const result = await parseMarkdown(text, { remarkPlugins: [...userOptions.remarkPlugins, ...compileOptions.remarkPlugins], rehypePlugins: [...userOptions.rehypePlugins, ...compileOptions.rehypePlugins], + // A per-compile `headingId` wins over the one configured for the compiler. + headingId: compileOptions.headingId ?? userOptions.headingId, isLive, isPreview, isBelow, diff --git a/packages/repl-sdk/src/compilers/markdown.test.ts b/packages/repl-sdk/src/compilers/markdown.test.ts new file mode 100644 index 000000000..379ccefe5 --- /dev/null +++ b/packages/repl-sdk/src/compilers/markdown.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; + +import { filterOptions } from './markdown.js'; + +describe('filterOptions', () => { + it('returns empty plugin lists for a non-record', () => { + expect(filterOptions(undefined)).to.deep.equal({ remarkPlugins: [], rehypePlugins: [] }); + }); + + it('passes plugin lists through', () => { + const remark = () => {}; + const rehype = () => {}; + + const result = filterOptions({ remarkPlugins: [remark], rehypePlugins: [rehype] }); + + expect(result.remarkPlugins).to.deep.equal([remark]); + expect(result.rehypePlugins).to.deep.equal([rehype]); + }); + + it('passes headingId through, so it reaches the compiler', () => { + // Without this, `headingId` is silently dropped between the public `md` + // compiler and `parseMarkdown`, and the option only works for callers + // reaching into `buildCompiler` directly. + expect(filterOptions({ headingId: { slug: 'gfm' } }).headingId).to.deep.equal({ slug: 'gfm' }); + }); + + it('leaves headingId undefined when not given', () => { + expect(filterOptions({}).headingId).toBe(undefined); + }); +}); diff --git a/packages/repl-sdk/src/compilers/markdown/heading-id.js b/packages/repl-sdk/src/compilers/markdown/heading-id.js index e1baa98ce..7c6df33b6 100644 --- a/packages/repl-sdk/src/compilers/markdown/heading-id.js +++ b/packages/repl-sdk/src/compilers/markdown/heading-id.js @@ -1,4 +1,5 @@ import { kebabCase } from 'change-case'; +import GithubSlugger from 'github-slugger'; import { visit } from 'unist-util-visit'; /** @@ -56,33 +57,74 @@ function setNodeId(node, id) { /** @type {any} */ (node.data).id = node.data.hProperties.id = id; } +/** + * Slug strategies that can be named instead of passed as a function. + * + * `gfm` matches the anchors GitHub generates for the same markdown, via + * `github-slugger`. A fresh slugger is created per document so its + * duplicate-heading counter (`usage`, `usage-1`) restarts each time, the way it + * does on GitHub -- sharing one across documents would leak counts between them. + */ +const NAMED_SLUGGERS = { + gfm: () => { + const slugger = new GithubSlugger(); + + return (/** @type {string} */ text) => slugger.slug(text); + }, + kebab: () => formatDefaultId, +}; + +/** + * @param {import('./types').HeadingIdOptions['slug']} slug + * @returns {(text: string) => string} + */ +function resolveSlugger(slug) { + if (typeof slug === 'function') return slug; + + const named = slug ? NAMED_SLUGGERS[slug] : undefined; + + if (slug && !named) { + throw new Error( + `Unknown headingId.slug: ${JSON.stringify(slug)}. ` + + `Expected a function or one of: ${Object.keys(NAMED_SLUGGERS).join(', ')}.` + ); + } + + return (named ?? NAMED_SLUGGERS.kebab)(); +} + /** * Assign an `id` to every heading, so anchors can link to sections. * - * By default the id is the heading's text in kebab-case. Pass `slug` to - * generate it some other way -- for example, matching what GitHub produces for - * the same markdown, so an in-page `#anchor` resolves both in a rendered site - * and in the `.md` file on GitHub: + * The id defaults to the heading's text in kebab-case (`'kebab'`). Pass + * `slug: 'gfm'` to generate the anchors GitHub generates for the same markdown + * instead -- which matters when the same `.md` is read both in a rendered site + * and on GitHub, since an in-page `#anchor` has to resolve in both: * * ```js - * import GithubSlugger from 'github-slugger'; + * headingId({ slug: 'gfm' }); + * // ### `setupMirage` -> #setupmirage (kebab gives #setup-mirage) + * ``` * - * const slugger = new GithubSlugger(); + * A function is also accepted, for anything neither mode covers: * - * headingId({ slug: (text) => slugger.slug(text) }); + * ```js + * headingId({ slug: (text) => text.toUpperCase() }); * ``` * - * A heading with an explicit `{#custom-id}` suffix is left alone either way. + * A heading with an explicit `{#custom-id}` suffix is left alone in every mode. * - * @param {{ slug?: (text: string) => string }} [options] + * @param {import('./types').HeadingIdOptions} [options] */ export function headingId(options) { - const slug = typeof options?.slug === 'function' ? options.slug : formatDefaultId; - /** * @param {import('mdast').Root} node */ return function (node) { + // Per document, not per plugin: a named slugger carries dedupe state, and a + // compiler can be reused across documents. + const slug = resolveSlugger(options?.slug); + visit(node, 'heading', (node) => { const lastChild = node.children[node.children.length - 1]; diff --git a/packages/repl-sdk/src/compilers/markdown/parse.test.ts b/packages/repl-sdk/src/compilers/markdown/parse.test.ts index 7abcde8f8..4a822a39d 100644 --- a/packages/repl-sdk/src/compilers/markdown/parse.test.ts +++ b/packages/repl-sdk/src/compilers/markdown/parse.test.ts @@ -245,6 +245,55 @@ describe('options', () => { expect(result.text).toBe('

setupMirage

'); }); + it("slug: 'gfm' matches the anchor GitHub generates", async () => { + const result = await parseMarkdown(`## setupMirage`, { + ...defaults, + headingId: { slug: 'gfm' }, + }); + + expect(result.text).toBe('

setupMirage

'); + }); + + it("slug: 'gfm' de-duplicates repeated headings within a document", async () => { + const result = await parseMarkdown(`## Usage\n\n## Usage\n\n## Usage`, { + ...defaults, + headingId: { slug: 'gfm' }, + }); + + expect(result.text).toContain('id="usage"'); + expect(result.text).toContain('id="usage-1"'); + expect(result.text).toContain('id="usage-2"'); + }); + + it("slug: 'gfm' restarts numbering for each document", async () => { + const options = { ...defaults, headingId: { slug: 'gfm' as const } }; + + await parseMarkdown(`## Usage`, options); + + const second = await parseMarkdown(`## Usage`, options); + + expect(second.text).toBe('

Usage

'); + }); + + it("slug: 'kebab' is the default and can be named explicitly", async () => { + const result = await parseMarkdown(`## setupMirage`, { + ...defaults, + headingId: { slug: 'kebab' }, + }); + + expect(result.text).toBe('

setupMirage

'); + }); + + it('throws on an unknown named slugger', async () => { + await errorExpect( + parseMarkdown(`## Title`, { + ...defaults, + // @ts-expect-error -- deliberately invalid + headingId: { slug: 'nope' }, + }) + ).rejects.toThrow(/Unknown headingId.slug/); + }); + it('uses a custom slug function when given', async () => { const result = await parseMarkdown(`## setupMirage`, { ...defaults, diff --git a/packages/repl-sdk/src/compilers/markdown/types.ts b/packages/repl-sdk/src/compilers/markdown/types.ts index ba71d96ec..47fcf5ae5 100644 --- a/packages/repl-sdk/src/compilers/markdown/types.ts +++ b/packages/repl-sdk/src/compilers/markdown/types.ts @@ -7,6 +7,21 @@ export interface LiveCodeExtractionOptions { getFlavorFromMeta?: (meta: string, lang: string) => string | undefined; } +export interface HeadingIdOptions { + /** + * How a heading's text becomes its `id`. + * + * - `'kebab'` (default) — kebab-case, e.g. `### setupMirage` → `#setup-mirage` + * - `'gfm'` — the anchor GitHub generates for the same markdown, e.g. + * `### setupMirage` → `#setupmirage`. Use this when the same `.md` is read + * both in a rendered site and on GitHub, so in-page links resolve in both. + * + * A function is also accepted for anything neither mode covers. It receives + * the heading's text with whitespace already collapsed. + */ + slug?: 'kebab' | 'gfm' | ((text: string) => string); +} + export interface PublicOptions { code?: { classList?: string[]; @@ -14,15 +29,7 @@ export interface PublicOptions { demo?: { classList?: string[]; }; - headingId?: { - /** - * How a heading's text becomes its `id`. Defaults to kebab-case. - * - * Pass a `github-slugger` instance's `slug` to match the anchors GitHub - * generates for the same markdown. - */ - slug?: (text: string) => string; - }; + headingId?: HeadingIdOptions; remarkPlugins?: unknown[]; rehypePlugins?: unknown[]; compiler?: { process: (text: string) => Promise<{ data: { liveCode: Array } }> }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e92562af..a109557e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -235,7 +235,7 @@ importers: version: 0.1.3 '@embroider/vite': specifier: ^1.7.2 - version: 1.7.2(97aaab996a93266663833be1dd991207) + version: 1.7.2(994f87776d275bddf9bc421f787b0ba3) '@fortawesome/ember-fontawesome': specifier: ^3.0.0 version: 3.1.0(b8564c087c2abe66190fe4148dad7724) @@ -283,7 +283,7 @@ importers: version: 6.1.0(2feedaebee37a40334972cdc05053654) '@tailwindcss/typography': specifier: ^0.5.19 - version: 0.5.19(tailwindcss@3.4.19(yaml@2.8.3)) + version: 0.5.19(tailwindcss@3.4.19(yaml@2.9.0)) '@tsconfig/ember': specifier: ^3.0.9 version: 3.0.12 @@ -400,7 +400,7 @@ importers: version: 3.5.1 tailwindcss: specifier: ^3.0.0 - version: 3.4.19(yaml@2.8.3) + version: 3.4.19(yaml@2.9.0) terser: specifier: ^5.44.1 version: 5.46.0 @@ -424,19 +424,19 @@ importers: version: 6.0.3 vite: specifier: 8.0.8 - version: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) + version: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) vite-bundle-analyzer: specifier: ^1.3.7 version: 1.3.7 vite-ember-ssr: specifier: 0.1.0-alpha.13 - version: 0.1.0-alpha.13(cc3e84ea8bf61933c19981acdb6d64f0) + version: 0.1.0-alpha.13(3a4ac714ec0686fc376fb28470713e65) vite-plugin-circular-dependency: specifier: ^0.5.0 version: 0.5.0(rollup@4.57.1) vite-plugin-mkcert: specifier: ^1.17.9 - version: 1.17.9(cc3e84ea8bf61933c19981acdb6d64f0) + version: 1.17.9(3a4ac714ec0686fc376fb28470713e65) zimmerframe: specifier: ^1.1.4 version: 1.1.4 @@ -466,13 +466,13 @@ importers: version: 0.61.1(a64917a33376b7be2998bd4d1c9f349f) ember-repl: specifier: latest - version: 8.1.0(97a2ca31a8e4cf9a385bac208eb0c5f1) + version: 8.2.2(b124149995ac6fe1e0820550d6115f50) ember-resources: specifier: ^7.1.0 version: 7.1.0(5417512ae382abcafe5679ee7b1ac481) kolay: specifier: github:universal-ember/kolay#dist - version: https://codeload.github.com/universal-ember/kolay/tar.gz/757d6a0ec85ebe6244c1e8c53a15f3840392536f(0d7798950681c8d6f11c89df9fde55ef) + version: https://codeload.github.com/universal-ember/kolay/tar.gz/2485d3f44037017d35c4c4a059a75a512f736edd(4303bcd233b82c5146db71a8b074d6a0) limber-ui: specifier: workspace:* version: link:../../packages/limber-ui @@ -530,7 +530,7 @@ importers: version: 1.20.2(c8332dc3f90c0e60eb130a3a85f980b9) '@embroider/vite': specifier: ^1.7.2 - version: 1.7.2(97aaab996a93266663833be1dd991207) + version: 1.7.2(994f87776d275bddf9bc421f787b0ba3) '@fortawesome/ember-fontawesome': specifier: ^3.0.0 version: 3.1.0(b8564c087c2abe66190fe4148dad7724) @@ -632,7 +632,7 @@ importers: version: 3.5.1 tailwindcss: specifier: ^3.0.0 - version: 3.4.19(yaml@2.8.3) + version: 3.4.19(yaml@2.9.0) terser: specifier: ^5.44.1 version: 5.46.0 @@ -647,7 +647,7 @@ importers: version: 2.3.11 vite: specifier: ^8.0.8 - version: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) + version: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) dev: dependencies: @@ -832,7 +832,7 @@ importers: dependencies: '@tailwindcss/typography': specifier: ^0.5.16 - version: 0.5.19(tailwindcss@3.4.19(yaml@2.8.3)) + version: 0.5.19(tailwindcss@3.4.19(yaml@2.9.0)) broccoli-funnel: specifier: ^3.0.8 version: 3.0.8 @@ -863,7 +863,7 @@ importers: version: 2.1.2(prettier@3.8.1) tailwindcss: specifier: ^3.0.0 - version: 3.4.19(yaml@2.8.3) + version: 3.4.19(yaml@2.9.0) packages/consts: devDependencies: @@ -957,7 +957,7 @@ importers: version: 1.20.2(c8332dc3f90c0e60eb130a3a85f980b9) '@embroider/vite': specifier: ^1.7.2 - version: 1.7.2(97aaab996a93266663833be1dd991207) + version: 1.7.2(994f87776d275bddf9bc421f787b0ba3) '@eslint/js': specifier: ^9.39.1 version: 9.39.2 @@ -1095,10 +1095,10 @@ importers: version: 13.0.0 vite: specifier: ^8.0.8 - version: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) + version: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) vite-plugin-mkcert: specifier: ^1.17.9 - version: 1.17.9(cc3e84ea8bf61933c19981acdb6d64f0) + version: 1.17.9(3a4ac714ec0686fc376fb28470713e65) packages/horizon-theme: devDependencies: @@ -1234,7 +1234,7 @@ importers: version: 5.9.3 vite: specifier: ^8.0.8 - version: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) + version: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) packages/repl-sdk: dependencies: @@ -1322,6 +1322,9 @@ importers: es-module-shims: specifier: ^2.8.0 version: 2.8.0 + github-slugger: + specifier: ^2.0.0 + version: 2.0.0 mdast: specifier: ^3.0.0 version: 3.0.0 @@ -1406,13 +1409,13 @@ importers: version: 5.9.3 vite: specifier: ^8.0.8 - version: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) + version: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) vite-plugin-dts: specifier: 4.5.4 - version: 4.5.4(8ff8d5b5f4292de1ffd303963911e09b) + version: 4.5.4(40d780d8e1564ef6a9c3e74a5a7db6b7) vitest: specifier: ^4.0.18 - version: 4.0.18(f35b124e478be927e5c31cf57dd7ef77) + version: 4.0.18(3f5b38b2ba6f5a8623730cd6c6aa5611) packages/repl-sdk/tests-browser: dependencies: @@ -1446,13 +1449,13 @@ importers: version: 4.0.4 '@vitest/browser': specifier: ^3.2.4 - version: 3.2.4(d10d1473c0c51b1ece3e87404f191532) + version: 3.2.4(bb9d26120721aeee32f0e6e10380de87) '@vitest/browser-playwright': specifier: ^4.0.18 - version: 4.0.18(beb02c5d0877ed4a855a5dbf53682eda) + version: 4.0.18(a822696f60557b9c5a75ed597ed66851) '@vitest/browser-webdriverio': specifier: ^4.0.18 - version: 4.0.18(82de5a8281df24df283ca5b9c3bb8729) + version: 4.0.18(08833fb9beb09b339566be3b8fab4608) '@vitest/ui': specifier: 3.2.4 version: 3.2.4(vitest@4.0.18) @@ -1500,13 +1503,13 @@ importers: version: 5.1.0 vite: specifier: ^8.0.8 - version: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) + version: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) vite-plugin-wasm: specifier: ^3.5.0 - version: 3.5.0(cc3e84ea8bf61933c19981acdb6d64f0) + version: 3.5.0(3a4ac714ec0686fc376fb28470713e65) vitest: specifier: ^4.0.18 - version: 4.0.18(f35b124e478be927e5c31cf57dd7ef77) + version: 4.0.18(3f5b38b2ba6f5a8623730cd6c6aa5611) vue: specifier: ^3.5.17 version: 3.5.27(typescript@5.9.3) @@ -1713,7 +1716,7 @@ importers: version: 5.9.3 vite: specifier: ^8.0.8 - version: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) + version: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) packages/syntax/glimdown/codemirror/tests: devDependencies: @@ -1880,7 +1883,7 @@ importers: version: 5.9.3 vite: specifier: ^8.0.8 - version: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) + version: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) packages/syntax/glimmer-s-expression/lezer: dependencies: @@ -2026,7 +2029,7 @@ importers: version: 5.9.3 vite: specifier: ^8.0.8 - version: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) + version: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) packages/syntax/glimmer/codemirror: dependencies: @@ -2147,7 +2150,7 @@ importers: version: 5.9.3 vite: specifier: ^8.0.8 - version: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) + version: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) packages/syntax/glimmer/lezer: dependencies: @@ -2269,7 +2272,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.2.4 - version: 3.2.4(c3ff36e9605173de7c7f8d7b5e3812c6) + version: 3.2.4(58efc7f29c65c378bec41e1d061beee1) packages: @@ -3455,8 +3458,8 @@ packages: '@gar/promisify@1.1.3': resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} - '@gerrit0/mini-shiki@3.21.0': - resolution: {integrity: sha512-9PrsT5DjZA+w3lur/aOIx3FlDeHdyCEFlv9U+fmsVyjPZh61G5SYURQ/1ebe2U63KbDmI2V8IhIUegWb8hjOyg==} + '@gerrit0/mini-shiki@3.23.0': + resolution: {integrity: sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==} '@glimdown/lezer-glimmer-expression@2.0.3': resolution: {integrity: sha512-E7yiSfaL4qkbW+zfbVMGZ9G81MCzst0R02wPK5u2Vt1wWxrpo+mZkCA2fFJuKwvjdcPFt4X04KSRmBQdnL/qVw==} @@ -6862,22 +6865,6 @@ packages: '@glint/template': optional: true - ember-primitives@0.60.1: - resolution: {integrity: sha512-OLUSUM4eqGVaxWGnq3oV1FIBE8JyYUTDTXYWDHd8OHdiFbU10CUJ4A7k6FbpCfBXO7k0Ce+aualiyuIpY28mmg==} - hasBin: true - peerDependencies: - '@ember/test-helpers': '>= 3.2.0' - '@ember/test-waiters': '>= 3.0.2' - '@glimmer/component': ^2.0.0 - '@glint/template': 1.7.7 - ember-modifier: '>= 4.1.0' - ember-resources: '>= 6.1.0' - peerDependenciesMeta: - '@ember/test-helpers': - optional: true - '@glint/template': - optional: true - ember-primitives@0.61.1: resolution: {integrity: sha512-bnr9XEQCqYb86a0yBn9hP5xDuWZwmyT12oPzZrNR8GZJmRjVanlvk64NjVbYVKs56/xxs/zYk4/oct5S9MmsIw==} hasBin: true @@ -6900,8 +6887,8 @@ packages: '@ember/test-helpers': '>=3.0.3' qunit: ^2.13.0 - ember-repl@8.1.0: - resolution: {integrity: sha512-XO3ryrvnik72AZrhRcGjlAJe6IjI9Vi7EaZghW8y/a1oSdiP2SL/j1DP/e5zOfDvTOxvAcPwHJVWOKH2KTyrCw==} + ember-repl@8.2.2: + resolution: {integrity: sha512-Zh2OosBRdctmyq7mzKdoWAU0hIRIv5B7GNNSOY3AiiK8XVj4jQ2VelZvW6xyV3EWwUpgd8VKPpZAdaKTx7gQoQ==} peerDependencies: '@glint/template': 1.7.7 peerDependenciesMeta: @@ -7729,8 +7716,8 @@ packages: resolution: {integrity: sha512-+A4Hq7m7Ze592k9gZRy4gJ27DrXRNnC1vPjxTt1qQxEY8RxagBkBxivkCwg7FxSTG0iLLEMaUx13oOr0R2/qcQ==} engines: {node: '>=20'} - globby@16.2.2: - resolution: {integrity: sha512-NLvV9ubZ6NDsJaOpKPy3cQeJpKi9DcWiyCiFUpJPA0YihRqiE6RWaLUmgNNPr8MgPpLZjnBjSmou7uZBRJv9wA==} + globby@16.2.3: + resolution: {integrity: sha512-VZX7TV7jmd/pn71vdnLKtgwy1IWqc3KjI9x1/UtPkwoKk5fKrNLY30ltDe3cAM5xruIN7YuuaulFt133jRrKZg==} engines: {node: '>=20'} globrex@0.1.2: @@ -8274,8 +8261,8 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} - kolay@https://codeload.github.com/universal-ember/kolay/tar.gz/757d6a0ec85ebe6244c1e8c53a15f3840392536f: - resolution: {gitHosted: true, integrity: sha512-jMfOl5d8NP0Crjdd7EU/Mn5NCSre99nh87SX56Ze3N9xC2Pj5UTHbDBLH+fRJGdRcUGo5U/kDp6vDJ59eaRnuw==, tarball: https://codeload.github.com/universal-ember/kolay/tar.gz/757d6a0ec85ebe6244c1e8c53a15f3840392536f} + kolay@https://codeload.github.com/universal-ember/kolay/tar.gz/2485d3f44037017d35c4c4a059a75a512f736edd: + resolution: {gitHosted: true, integrity: sha512-TaaA5fa1DH1zuHRQTUbtPgu6jxqZE1Bxgi+rHJGYlAva3TdIkV9hj1HW1o3QfzJs+daS1PU82tqk3B49tpfTIA==, tarball: https://codeload.github.com/universal-ember/kolay/tar.gz/2485d3f44037017d35c4c4a059a75a512f736edd} version: 5.4.0 engines: {node: '>= 18'} @@ -8398,8 +8385,8 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - linkify-it@5.0.0: - resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + linkify-it@5.0.2: + resolution: {integrity: sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==} loader.js@4.7.0: resolution: {integrity: sha512-9M2KvGT6duzGMgkOcTkWb+PR/Q2Oe54df/tLgHGVmFpAmtqJ553xJh6N63iFYI2yjo2PeJXbS5skHi/QpJq4vA==} @@ -8533,8 +8520,8 @@ packages: resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} engines: {node: '>=6'} - markdown-it@14.1.0: - resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + markdown-it@14.3.0: + resolution: {integrity: sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==} hasBin: true markdown-table@3.0.4: @@ -9740,8 +9727,8 @@ packages: remove-types@1.0.0: resolution: {integrity: sha512-G7Hk1Q+UJ5DvlNAoJZObxANkBZGiGdp589rVcTW/tYqJWJ5rwfraSnKSQaETN8Epaytw8J40nS/zC7bcHGv36w==} - repl-sdk@1.6.0: - resolution: {integrity: sha512-zgf2dJy/Vnei15WZ9keI/YR/g1LRwk6AKD6F96oEQiHQdnqpQEWH1CS6Ua0iA89Iyo1SGM/Qmufp67rNvNGKnQ==} + repl-sdk@1.6.2: + resolution: {integrity: sha512-jGQv4s2IYZmW102DCWWwd14QpkmQdx2QpwiW7axPUsHDN8CPZXVbQVUguAIWpfHhoIJCn+QU+AKq0I8nv3CMMQ==} request-light@0.7.0: resolution: {integrity: sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==} @@ -10665,12 +10652,12 @@ packages: peerDependencies: typedoc: '>=0.22.x <0.29.x' - typedoc@0.28.16: - resolution: {integrity: sha512-x4xW77QC3i5DUFMBp0qjukOTnr/sSg+oEs86nB3LjDslvAmwe/PUGDWbe3GrIqt59oTqoXK5GRK9tAa0sYMiog==} + typedoc@0.28.20: + resolution: {integrity: sha512-uSKqkh8Cr48vllnEy+jdaAgOeR6Y+QCBW7usgUsKj7gJEfR7stw9U/fE49LBnj2tPRKPY0c0EBJSWe9Appmplg==} engines: {node: '>= 18', pnpm: '>= 10'} hasBin: true peerDependencies: - typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x + typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x typesafe-path@0.2.2: resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} @@ -11396,6 +11383,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -12948,7 +12940,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@embroider/vite@1.7.2(97aaab996a93266663833be1dd991207)': + '@embroider/vite@1.7.2(994f87776d275bddf9bc421f787b0ba3)': dependencies: '@babel/core': 7.29.0 '@embroider/core': 4.4.7(@glint/template@1.7.7) @@ -12966,7 +12958,7 @@ snapshots: send: 0.18.0 source-map-url: 0.4.1 terser: 5.46.0 - vite: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) + vite: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) transitivePeerDependencies: - '@glint/template' - bufferutil @@ -13156,7 +13148,7 @@ snapshots: '@gar/promisify@1.1.3': {} - '@gerrit0/mini-shiki@3.21.0': + '@gerrit0/mini-shiki@3.23.0': dependencies: '@shikijs/engine-oniguruma': 3.23.0 '@shikijs/langs': 3.23.0 @@ -14244,10 +14236,10 @@ snapshots: dependencies: acorn: 8.16.0 - '@tailwindcss/typography@0.5.19(tailwindcss@3.4.19(yaml@2.8.3))': + '@tailwindcss/typography@0.5.19(tailwindcss@3.4.19(yaml@2.9.0))': dependencies: postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.19(yaml@2.8.3) + tailwindcss: 3.4.19(yaml@2.9.0) '@testing-library/dom@10.4.1': dependencies: @@ -14719,23 +14711,23 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitest/browser-playwright@4.0.18(beb02c5d0877ed4a855a5dbf53682eda)': + '@vitest/browser-playwright@4.0.18(a822696f60557b9c5a75ed597ed66851)': dependencies: - '@vitest/browser': 4.0.18(a756b5983c87fcbd13c59e78394f6ad4) - '@vitest/mocker': 4.0.18(cc3e84ea8bf61933c19981acdb6d64f0) + '@vitest/browser': 4.0.18(2614c19a9db521911c99ed2a491c14d3) + '@vitest/mocker': 4.0.18(3a4ac714ec0686fc376fb28470713e65) playwright: 1.58.1 tinyrainbow: 3.0.3 - vitest: 4.0.18(f35b124e478be927e5c31cf57dd7ef77) + vitest: 4.0.18(3f5b38b2ba6f5a8623730cd6c6aa5611) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser-webdriverio@4.0.18(82de5a8281df24df283ca5b9c3bb8729)': + '@vitest/browser-webdriverio@4.0.18(08833fb9beb09b339566be3b8fab4608)': dependencies: - '@vitest/browser': 4.0.18(a756b5983c87fcbd13c59e78394f6ad4) - vitest: 4.0.18(f35b124e478be927e5c31cf57dd7ef77) + '@vitest/browser': 4.0.18(2614c19a9db521911c99ed2a491c14d3) + vitest: 4.0.18(3f5b38b2ba6f5a8623730cd6c6aa5611) webdriverio: 9.23.3 transitivePeerDependencies: - bufferutil @@ -14743,16 +14735,16 @@ snapshots: - utf-8-validate - vite - '@vitest/browser@3.2.4(4fd95ed8106a190e5d70d04cbde3b222)': + '@vitest/browser@3.2.4(5e31ff89691a6aa19bc5de8e97aa636e)': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/mocker': 3.2.4(449e7d7b1644d13515d67bb1a50e94e2) + '@vitest/mocker': 3.2.4(2e088714ecb038fa5c6f5f197b01307a) '@vitest/utils': 3.2.4 magic-string: 0.30.21 sirv: 3.0.2 tinyrainbow: 2.0.0 - vitest: 3.2.4(c3ff36e9605173de7c7f8d7b5e3812c6) + vitest: 3.2.4(58efc7f29c65c378bec41e1d061beee1) ws: 8.19.0 optionalDependencies: playwright: 1.58.1 @@ -14764,16 +14756,16 @@ snapshots: - vite optional: true - '@vitest/browser@3.2.4(d10d1473c0c51b1ece3e87404f191532)': + '@vitest/browser@3.2.4(bb9d26120721aeee32f0e6e10380de87)': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/mocker': 3.2.4(cc3e84ea8bf61933c19981acdb6d64f0) + '@vitest/mocker': 3.2.4(3a4ac714ec0686fc376fb28470713e65) '@vitest/utils': 3.2.4 magic-string: 0.30.21 sirv: 3.0.2 tinyrainbow: 2.0.0 - vitest: 4.0.18(f35b124e478be927e5c31cf57dd7ef77) + vitest: 4.0.18(3f5b38b2ba6f5a8623730cd6c6aa5611) ws: 8.19.0 optionalDependencies: playwright: 1.58.1 @@ -14784,16 +14776,16 @@ snapshots: - utf-8-validate - vite - '@vitest/browser@4.0.18(a756b5983c87fcbd13c59e78394f6ad4)': + '@vitest/browser@4.0.18(2614c19a9db521911c99ed2a491c14d3)': dependencies: - '@vitest/mocker': 4.0.18(cc3e84ea8bf61933c19981acdb6d64f0) + '@vitest/mocker': 4.0.18(3a4ac714ec0686fc376fb28470713e65) '@vitest/utils': 4.0.18 magic-string: 0.30.21 pixelmatch: 7.1.0 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.0.3 - vitest: 4.0.18(f35b124e478be927e5c31cf57dd7ef77) + vitest: 4.0.18(3f5b38b2ba6f5a8623730cd6c6aa5611) ws: 8.19.0 transitivePeerDependencies: - bufferutil @@ -14818,37 +14810,37 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@3.2.4(449e7d7b1644d13515d67bb1a50e94e2)': + '@vitest/mocker@3.2.4(2e088714ecb038fa5c6f5f197b01307a)': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(c15aa76a89bd46a8955d1cc0ec2e331d) + vite: 7.3.1(bb1b215ab7298e4ef4d4b8cdd426c297) - '@vitest/mocker@3.2.4(cc3e84ea8bf61933c19981acdb6d64f0)': + '@vitest/mocker@3.2.4(3a4ac714ec0686fc376fb28470713e65)': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) + vite: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) - '@vitest/mocker@4.0.18(449e7d7b1644d13515d67bb1a50e94e2)': + '@vitest/mocker@4.0.18(2e088714ecb038fa5c6f5f197b01307a)': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(c15aa76a89bd46a8955d1cc0ec2e331d) + vite: 7.3.1(bb1b215ab7298e4ef4d4b8cdd426c297) - '@vitest/mocker@4.0.18(cc3e84ea8bf61933c19981acdb6d64f0)': + '@vitest/mocker@4.0.18(3a4ac714ec0686fc376fb28470713e65)': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) + vite: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) '@vitest/pretty-format@3.2.4': dependencies: @@ -14896,7 +14888,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.4(c3ff36e9605173de7c7f8d7b5e3812c6) + vitest: 3.2.4(58efc7f29c65c378bec41e1d061beee1) optional: true '@vitest/ui@3.2.4(vitest@4.0.18)': @@ -14908,7 +14900,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 4.0.18(f35b124e478be927e5c31cf57dd7ef77) + vitest: 4.0.18(3f5b38b2ba6f5a8623730cd6c6aa5611) '@vitest/utils@3.2.4': dependencies: @@ -17056,31 +17048,6 @@ snapshots: - '@babel/core' - supports-color - ember-primitives@0.60.1(a64917a33376b7be2998bd4d1c9f349f): - dependencies: - '@babel/runtime': 7.29.2 - '@ember/test-waiters': 4.1.2 - '@embroider/addon-shim': 1.10.2 - '@floating-ui/dom': 1.7.5 - '@glimmer/component': 2.1.1 - decorator-transforms: 2.3.2(@babel/core@7.29.0) - ember-element-helper: 0.8.8 - ember-modifier: 4.3.0(@babel/core@7.29.0) - ember-resources: 7.1.0(5417512ae382abcafe5679ee7b1ac481) - form-data-utils: 0.6.0 - reactiveweb: 1.9.1(9f91e21669edde5a0835948ba5900801) - should-handle-link: 1.3.0 - tabster: 8.7.0 - tracked-built-ins: 4.1.2(@babel/core@7.29.0) - tracked-toolbox: 3.0.0(@babel/core@7.29.0) - which-heading-do-i-need: 0.4.1 - optionalDependencies: - '@ember/test-helpers': 5.4.1(c8332dc3f90c0e60eb130a3a85f980b9) - '@glint/template': 1.7.7 - transitivePeerDependencies: - - '@babel/core' - - supports-color - ember-primitives@0.61.1(a64917a33376b7be2998bd4d1c9f349f): dependencies: '@babel/runtime': 7.29.2 @@ -17118,17 +17085,17 @@ snapshots: - '@glint/template' - supports-color - ember-repl@8.1.0(97a2ca31a8e4cf9a385bac208eb0c5f1): + ember-repl@8.2.2(b124149995ac6fe1e0820550d6115f50): dependencies: '@ember/test-helpers': 5.4.1(c8332dc3f90c0e60eb130a3a85f980b9) '@ember/test-waiters': 4.1.2 codemirror: 6.0.2 content-tag: 4.2.0 decorator-transforms: 2.3.1(@babel/core@7.29.0) - ember-primitives: 0.51.0(a64917a33376b7be2998bd4d1c9f349f) + ember-primitives: 0.61.1(a64917a33376b7be2998bd4d1c9f349f) ember-resolver: 13.1.1 ember-resources: 7.1.0(5417512ae382abcafe5679ee7b1ac481) - repl-sdk: 1.6.0(93d4969b5698d0d4464d33ccc0660007) + repl-sdk: 1.6.2(6f4bb085e066dcd1e8cba157a733f3a5) optionalDependencies: '@glint/template': 1.7.7 transitivePeerDependencies: @@ -18218,7 +18185,7 @@ snapshots: slash: 5.1.0 unicorn-magic: 0.4.0 - globby@16.2.2: + globby@16.2.3: dependencies: '@sindresorhus/merge-streams': 4.0.0 fast-glob: 3.3.3 @@ -18832,10 +18799,10 @@ snapshots: kind-of@6.0.3: {} - kolay@https://codeload.github.com/universal-ember/kolay/tar.gz/757d6a0ec85ebe6244c1e8c53a15f3840392536f(0d7798950681c8d6f11c89df9fde55ef): + kolay@https://codeload.github.com/universal-ember/kolay/tar.gz/2485d3f44037017d35c4c4a059a75a512f736edd(4303bcd233b82c5146db71a8b074d6a0): dependencies: '@ember/test-waiters': 4.1.2 - '@embroider/addon-shim': 1.10.2 + '@embroider/addon-shim': 1.10.3 '@glimmer/component': 2.1.1 '@glint/template': 1.7.7 change-case: 5.4.4 @@ -18843,11 +18810,12 @@ snapshots: content-tag: 4.2.0 decorator-transforms: 2.4.0(@babel/core@7.29.0) ember-modifier: 4.3.0(@babel/core@7.29.0) - ember-primitives: 0.60.1(a64917a33376b7be2998bd4d1c9f349f) - ember-repl: 8.1.0(97a2ca31a8e4cf9a385bac208eb0c5f1) + ember-primitives: 0.61.1(a64917a33376b7be2998bd4d1c9f349f) + ember-repl: 8.2.2(b124149995ac6fe1e0820550d6115f50) ember-resources: 7.1.0(5417512ae382abcafe5679ee7b1ac481) - globby: 16.2.2 + globby: 16.2.3 json5: 2.2.3 + lilconfig: 3.1.3 package-up: 5.0.0 reactiveweb: 1.9.1(9f91e21669edde5a0835948ba5900801) rehype-raw: 7.0.0 @@ -18855,13 +18823,14 @@ snapshots: remark-gfm: 4.0.1 remark-parse: 11.0.0 remark-rehype: 11.1.2 - repl-sdk: 1.6.0(93d4969b5698d0d4464d33ccc0660007) + repl-sdk: 1.6.2(6f4bb085e066dcd1e8cba157a733f3a5) + resolve.exports: 2.0.3 send: 1.2.1 tracked-built-ins: 4.1.2(@babel/core@7.29.0) - typedoc: 0.28.16(typescript@5.9.3) - typedoc-plugin-rename-defaults: 0.7.3(typedoc@0.28.16(typescript@5.9.3)) + typedoc: 0.28.20(typescript@5.9.3) + typedoc-plugin-rename-defaults: 0.7.3(typedoc@0.28.20(typescript@5.9.3)) unist-util-visit: 5.1.0 - unplugin: 3.3.0(90af3858bcb41b7217b92ee30358691a) + unplugin: 3.3.0(c09f9af6a20ab73299230aff0ce4a374) transitivePeerDependencies: - '@babel/core' - '@codemirror/lang-css' @@ -18983,7 +18952,7 @@ snapshots: lines-and-columns@1.2.4: {} - linkify-it@5.0.0: + linkify-it@5.0.2: dependencies: uc.micro: 2.1.0 @@ -19123,11 +19092,11 @@ snapshots: dependencies: p-defer: 1.0.0 - markdown-it@14.1.0: + markdown-it@14.3.0: dependencies: argparse: 2.0.1 entities: 4.5.0 - linkify-it: 5.0.0 + linkify-it: 5.0.2 mdurl: 2.0.0 punycode.js: 2.3.1 uc.micro: 2.1.0 @@ -20175,13 +20144,13 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.5.9 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.9)(yaml@2.8.3): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.9)(yaml@2.9.0): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 postcss: 8.5.9 - yaml: 2.8.3 + yaml: 2.9.0 postcss-nested@6.2.0(postcss@8.5.9): dependencies: @@ -20589,7 +20558,7 @@ snapshots: transitivePeerDependencies: - supports-color - repl-sdk@1.6.0(93d4969b5698d0d4464d33ccc0660007): + repl-sdk@1.6.2(6f4bb085e066dcd1e8cba157a733f3a5): dependencies: '@codemirror/autocomplete': 6.20.0 '@codemirror/commands': 6.10.1 @@ -21406,7 +21375,7 @@ snapshots: tagged-tag@1.0.0: {} - tailwindcss@3.4.19(yaml@2.8.3): + tailwindcss@3.4.19(yaml@2.9.0): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -21425,7 +21394,7 @@ snapshots: postcss: 8.5.9 postcss-import: 15.1.0(postcss@8.5.9) postcss-js: 4.1.0(postcss@8.5.9) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.9)(yaml@2.8.3) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.9)(yaml@2.9.0) postcss-nested: 6.2.0(postcss@8.5.9) postcss-selector-parser: 6.1.2 resolve: 1.22.12 @@ -21838,19 +21807,19 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 - typedoc-plugin-rename-defaults@0.7.3(typedoc@0.28.16(typescript@5.9.3)): + typedoc-plugin-rename-defaults@0.7.3(typedoc@0.28.20(typescript@5.9.3)): dependencies: camelcase: 8.0.0 - typedoc: 0.28.16(typescript@5.9.3) + typedoc: 0.28.20(typescript@5.9.3) - typedoc@0.28.16(typescript@5.9.3): + typedoc@0.28.20(typescript@5.9.3): dependencies: - '@gerrit0/mini-shiki': 3.21.0 + '@gerrit0/mini-shiki': 3.23.0 lunr: 2.3.9 - markdown-it: 14.1.0 - minimatch: 9.0.5 + markdown-it: 14.3.0 + minimatch: 10.2.5 typescript: 5.9.3 - yaml: 2.8.3 + yaml: 2.9.0 typesafe-path@0.2.2: {} @@ -21989,7 +21958,7 @@ snapshots: picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 - unplugin@3.3.0(90af3858bcb41b7217b92ee30358691a): + unplugin@3.3.0(c09f9af6a20ab73299230aff0ce4a374): dependencies: '@jridgewell/remapping': 2.3.5 picomatch: 4.0.4 @@ -21997,7 +21966,7 @@ snapshots: optionalDependencies: esbuild: 0.27.2 rollup: 4.57.1 - vite: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) + vite: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) unrs-resolver@1.11.1: dependencies: @@ -22079,22 +22048,22 @@ snapshots: vite-bundle-analyzer@1.3.7: {} - vite-ember-ssr@0.1.0-alpha.13(cc3e84ea8bf61933c19981acdb6d64f0): + vite-ember-ssr@0.1.0-alpha.13(3a4ac714ec0686fc376fb28470713e65): dependencies: happy-dom: 20.8.9 tinypool: 2.1.0 - vite: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) + vite: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) transitivePeerDependencies: - bufferutil - utf-8-validate - vite-node@3.2.4(c15aa76a89bd46a8955d1cc0ec2e331d): + vite-node@3.2.4(bb1b215ab7298e4ef4d4b8cdd426c297): dependencies: cac: 6.7.14 debug: 4.4.3(supports-color@8.1.1) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.1(c15aa76a89bd46a8955d1cc0ec2e331d) + vite: 7.3.1(bb1b215ab7298e4ef4d4b8cdd426c297) transitivePeerDependencies: - '@types/node' - jiti @@ -22116,7 +22085,7 @@ snapshots: transitivePeerDependencies: - rollup - vite-plugin-dts@4.5.4(8ff8d5b5f4292de1ffd303963911e09b): + vite-plugin-dts@4.5.4(40d780d8e1564ef6a9c3e74a5a7db6b7): dependencies: '@microsoft/api-extractor': 7.56.0(@types/node@24.10.9) '@rollup/pluginutils': 5.3.0(rollup@4.57.1) @@ -22129,26 +22098,26 @@ snapshots: magic-string: 0.30.21 typescript: 5.9.3 optionalDependencies: - vite: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) + vite: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite-plugin-mkcert@1.17.9(cc3e84ea8bf61933c19981acdb6d64f0): + vite-plugin-mkcert@1.17.9(3a4ac714ec0686fc376fb28470713e65): dependencies: axios: 1.13.4(debug@4.4.3) debug: 4.4.3(supports-color@8.1.1) picocolors: 1.1.1 - vite: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) + vite: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) transitivePeerDependencies: - supports-color - vite-plugin-wasm@3.5.0(cc3e84ea8bf61933c19981acdb6d64f0): + vite-plugin-wasm@3.5.0(3a4ac714ec0686fc376fb28470713e65): dependencies: - vite: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) + vite: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) - vite@7.3.1(c15aa76a89bd46a8955d1cc0ec2e331d): + vite@7.3.1(bb1b215ab7298e4ef4d4b8cdd426c297): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.4) @@ -22162,9 +22131,9 @@ snapshots: jiti: 2.6.1 lightningcss: 1.32.0 terser: 5.46.0 - yaml: 2.8.3 + yaml: 2.9.0 - vite@8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7): + vite@8.0.8(0958e47a27b9dbd3a2ad515effef1b96): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -22177,13 +22146,13 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 terser: 5.46.0 - yaml: 2.8.3 + yaml: 2.9.0 - vitest@3.2.4(c3ff36e9605173de7c7f8d7b5e3812c6): + vitest@3.2.4(58efc7f29c65c378bec41e1d061beee1): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(449e7d7b1644d13515d67bb1a50e94e2) + '@vitest/mocker': 3.2.4(2e088714ecb038fa5c6f5f197b01307a) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -22201,13 +22170,13 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.1(c15aa76a89bd46a8955d1cc0ec2e331d) - vite-node: 3.2.4(c15aa76a89bd46a8955d1cc0ec2e331d) + vite: 7.3.1(bb1b215ab7298e4ef4d4b8cdd426c297) + vite-node: 3.2.4(bb1b215ab7298e4ef4d4b8cdd426c297) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 '@types/node': 24.10.9 - '@vitest/browser': 3.2.4(4fd95ed8106a190e5d70d04cbde3b222) + '@vitest/browser': 3.2.4(5e31ff89691a6aa19bc5de8e97aa636e) '@vitest/ui': 3.2.4(vitest@3.2.4) happy-dom: 20.8.9 jsdom: 26.1.0 @@ -22225,10 +22194,10 @@ snapshots: - tsx - yaml - vitest@4.0.18(f35b124e478be927e5c31cf57dd7ef77): + vitest@4.0.18(3f5b38b2ba6f5a8623730cd6c6aa5611): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(449e7d7b1644d13515d67bb1a50e94e2) + '@vitest/mocker': 4.0.18(2e088714ecb038fa5c6f5f197b01307a) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -22245,12 +22214,12 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(c15aa76a89bd46a8955d1cc0ec2e331d) + vite: 7.3.1(bb1b215ab7298e4ef4d4b8cdd426c297) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.10.9 - '@vitest/browser-playwright': 4.0.18(beb02c5d0877ed4a855a5dbf53682eda) - '@vitest/browser-webdriverio': 4.0.18(82de5a8281df24df283ca5b9c3bb8729) + '@vitest/browser-playwright': 4.0.18(a822696f60557b9c5a75ed597ed66851) + '@vitest/browser-webdriverio': 4.0.18(08833fb9beb09b339566be3b8fab4608) '@vitest/ui': 3.2.4(vitest@4.0.18) happy-dom: 20.8.9 jsdom: 26.1.0 @@ -22564,6 +22533,8 @@ snapshots: yaml@2.8.3: {} + yaml@2.9.0: {} + yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} From ba0cabe08e983d9238007d24a9f7648800c93088 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Fri, 14 Aug 2026 17:37:18 -0700 Subject: [PATCH 3/9] feat(repl-sdk)!: generate heading ids GitHub's way, always Replaces the configurable slugger from the previous commits with the simpler thing @NullVoxPopuli asked for: no option, github-slugger is the behavior. BREAKING CHANGE: heading ids change for any heading whose text isn't already identical under both sluggers -- camelCase and punctuation are where they diverge: ### `setupMirage` before: #setup-mirage after: #setupmirage ### V2 JSON:API before: #v2-json-api after: #v2-jsonapi Links authored against the old ids need updating. Anchors are also now de-duplicated per document (`usage`, `usage-1`), matching GitHub. Drops `change-case`, which was only used for this. Keeps the two pieces that were never about configurability: whitespace is normalized before slugging (`extractText` joins children with a space on top of existing spacing, so `## Hello *there*` would otherwise slug as `hello--there`), and the slugger is built per document rather than per plugin, so its dedupe counter doesn't leak between documents. Updates the snapshot for a heading of ``, which moves from `#hello-foo-two` to `#hello-footwo-` -- github-slugger's real output for that text, and a fair illustration of the break. --- packages/repl-sdk/README.md | 50 +++------- packages/repl-sdk/package.json | 1 - packages/repl-sdk/src/compilers/markdown.js | 7 +- .../repl-sdk/src/compilers/markdown.test.ts | 30 ------ .../src/compilers/markdown/build-compiler.js | 2 +- .../src/compilers/markdown/heading-id.js | 84 ++++------------- .../src/compilers/markdown/parse.test.ts | 93 ++++--------------- .../repl-sdk/src/compilers/markdown/types.ts | 16 ---- pnpm-lock.yaml | 21 ++--- 9 files changed, 59 insertions(+), 245 deletions(-) delete mode 100644 packages/repl-sdk/src/compilers/markdown.test.ts diff --git a/packages/repl-sdk/README.md b/packages/repl-sdk/README.md index d502cbdd5..83c3b9535 100644 --- a/packages/repl-sdk/README.md +++ b/packages/repl-sdk/README.md @@ -13,52 +13,32 @@ Features: ## Usage -### Markdown options +### Heading ids -#### `headingId` +Every markdown heading gets an `id`, so in-page anchors can link to sections. -Every heading gets an `id`, so anchors can link to sections. `headingId.slug` -controls how the heading's text becomes that id. +Ids match what GitHub generates for the same markdown, via +[`github-slugger`][github-slugger]: -| `slug` | `### setupMirage` | `### V2 JSON:API` | -| --- | --- | --- | -| `'kebab'` (default) | `#setup-mirage` | `#v2-json-api` | -| `'gfm'` | `#setupmirage` | `#v2-jsonapi` | - -Set it for the compiler, or per compile: - -```js -const compiler = new Compiler({ - options: { - md: { headingId: { slug: 'gfm' } }, - }, -}); - -// or, overriding for one document -await compiler.compile('md', text, { headingId: { slug: 'gfm' } }); +``` +### `setupMirage` -> #setupmirage +### V2 JSON:API -> #v2-jsonapi ``` -Use `'gfm'` when the same `.md` file is read both in a rendered site and on -GitHub — an in-page `#anchor` can only resolve in both if the two agree on how -ids are generated, and GitHub's rule is [`github-slugger`][github-slugger]. - -Like GitHub, `'gfm'` also de-duplicates repeated headings within a document -(`#usage`, `#usage-1`, `#usage-2`). The counter restarts for each document. - -A function is accepted for anything neither mode covers. It receives the -heading's text with whitespace already collapsed: +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. -```js -{ headingId: { slug: (text) => text.toUpperCase() } } -``` +Like GitHub, repeated headings within a document are de-duplicated (`#usage`, +`#usage-1`, `#usage-2`), and the numbering restarts for each document. -A heading with an explicit `{#custom-id}` suffix is left alone in every mode. +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. `'gfm'` is named for the behavior -> people expect from GitHub-flavored markdown, not for a spec requirement. +> 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 4426006e4..e4d90340d 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:^", diff --git a/packages/repl-sdk/src/compilers/markdown.js b/packages/repl-sdk/src/compilers/markdown.js index 21d7c6e45..a0088f943 100644 --- a/packages/repl-sdk/src/compilers/markdown.js +++ b/packages/repl-sdk/src/compilers/markdown.js @@ -6,7 +6,7 @@ import { buildCodeFenceMetaUtils } from './markdown/utils.js'; /** * @param {unknown} [ options ] - * @returns {{ remarkPlugins: Plugin[], rehypePlugins: Plugin[], headingId?: import('./markdown/types.ts').HeadingIdOptions }} + * @returns {{ remarkPlugins: Plugin[], rehypePlugins: Plugin[] }} */ export function filterOptions(options) { if (!isRecord(options)) { @@ -16,9 +16,6 @@ export function filterOptions(options) { return { remarkPlugins: /** @type {Plugin[]}*/ (options?.remarkPlugins || []), rehypePlugins: /** @type {Plugin[]}*/ (options?.rehypePlugins || []), - headingId: /** @type {import('./markdown/types.ts').HeadingIdOptions | undefined}*/ ( - options?.headingId - ), }; } @@ -58,8 +55,6 @@ export const md = { const result = await parseMarkdown(text, { remarkPlugins: [...userOptions.remarkPlugins, ...compileOptions.remarkPlugins], rehypePlugins: [...userOptions.rehypePlugins, ...compileOptions.rehypePlugins], - // A per-compile `headingId` wins over the one configured for the compiler. - headingId: compileOptions.headingId ?? userOptions.headingId, isLive, isPreview, isBelow, diff --git a/packages/repl-sdk/src/compilers/markdown.test.ts b/packages/repl-sdk/src/compilers/markdown.test.ts deleted file mode 100644 index 379ccefe5..000000000 --- a/packages/repl-sdk/src/compilers/markdown.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { filterOptions } from './markdown.js'; - -describe('filterOptions', () => { - it('returns empty plugin lists for a non-record', () => { - expect(filterOptions(undefined)).to.deep.equal({ remarkPlugins: [], rehypePlugins: [] }); - }); - - it('passes plugin lists through', () => { - const remark = () => {}; - const rehype = () => {}; - - const result = filterOptions({ remarkPlugins: [remark], rehypePlugins: [rehype] }); - - expect(result.remarkPlugins).to.deep.equal([remark]); - expect(result.rehypePlugins).to.deep.equal([rehype]); - }); - - it('passes headingId through, so it reaches the compiler', () => { - // Without this, `headingId` is silently dropped between the public `md` - // compiler and `parseMarkdown`, and the option only works for callers - // reaching into `buildCompiler` directly. - expect(filterOptions({ headingId: { slug: 'gfm' } }).headingId).to.deep.equal({ slug: 'gfm' }); - }); - - it('leaves headingId undefined when not given', () => { - expect(filterOptions({}).headingId).toBe(undefined); - }); -}); diff --git a/packages/repl-sdk/src/compilers/markdown/build-compiler.js b/packages/repl-sdk/src/compilers/markdown/build-compiler.js index eae675822..d6408b58f 100644 --- a/packages/repl-sdk/src/compilers/markdown/build-compiler.js +++ b/packages/repl-sdk/src/compilers/markdown/build-compiler.js @@ -23,7 +23,7 @@ export function buildCompiler(options) { let compiler = unified() .use(remarkParse) .use(remarkGfm, { singleTilde: true }) - .use(headingId, options.headingId); + .use(headingId); /** * If this were "use"d after `remarkRehype`, diff --git a/packages/repl-sdk/src/compilers/markdown/heading-id.js b/packages/repl-sdk/src/compilers/markdown/heading-id.js index 8da1c1f84..1bed0855d 100644 --- a/packages/repl-sdk/src/compilers/markdown/heading-id.js +++ b/packages/repl-sdk/src/compilers/markdown/heading-id.js @@ -1,4 +1,3 @@ -import { kebabCase } from 'change-case'; import GithubSlugger from 'github-slugger'; import { visit } from 'unist-util-visit'; @@ -30,8 +29,8 @@ function extractText(children) { /** * `extractText` joins a heading's children with a space, on top of whatever * spacing the text nodes already carry, so `## Hello *there*` extracts as - * `'Hello there'`. Collapse that before slugging: `kebabCase` happens not to - * care, but a slugger that maps space to `-` would emit `hello--there`. + * `'Hello there'`. Collapse that before slugging, or the gap survives into the + * id: `hello--there` rather than `hello-there`. * * @param {string} value */ @@ -39,13 +38,6 @@ function normalizeText(value) { return value.replaceAll(/\s+/g, ' ').trim(); } -/** - * @param {string} value - */ -function formatDefaultId(value) { - return kebabCase(value.replaceAll(/\s+/g, ' ').trim()); -} - /** * @param {import('mdast').Heading} node * @param {string} id @@ -57,73 +49,29 @@ function setNodeId(node, id) { /** @type {any} */ (node.data).id = node.data.hProperties.id = id; } -/** - * Slug strategies that can be named instead of passed as a function. - * - * `gfm` matches the anchors GitHub generates for the same markdown, via - * `github-slugger`. A fresh slugger is created per document so its - * duplicate-heading counter (`usage`, `usage-1`) restarts each time, the way it - * does on GitHub -- sharing one across documents would leak counts between them. - */ -const NAMED_SLUGGERS = { - gfm: () => { - const slugger = new GithubSlugger(); - - return (/** @type {string} */ text) => slugger.slug(text); - }, - kebab: () => formatDefaultId, -}; - -/** - * @param {import('./types').HeadingIdOptions['slug']} slug - * @returns {(text: string) => string} - */ -function resolveSlugger(slug) { - if (typeof slug === 'function') return slug; - - const named = slug ? NAMED_SLUGGERS[slug] : undefined; - - if (slug && !named) { - throw new Error( - `Unknown headingId.slug: ${JSON.stringify(slug)}. ` + - `Expected a function or one of: ${Object.keys(NAMED_SLUGGERS).join(', ')}.` - ); - } - - return (named ?? NAMED_SLUGGERS.kebab)(); -} - /** * Assign an `id` to every heading, so anchors can link to sections. * - * The id defaults to the heading's text in kebab-case (`'kebab'`). Pass - * `slug: 'gfm'` to generate the anchors GitHub generates for the same markdown - * instead -- which matters when the same `.md` is read both in a rendered site - * and on GitHub, since an in-page `#anchor` has to resolve in both: - * - * ```js - * headingId({ slug: 'gfm' }); - * // ### `setupMirage` -> #setupmirage (kebab gives #setup-mirage) - * ``` - * - * A function is also accepted, for anything neither mode covers: + * Ids match what GitHub generates for the same markdown, via `github-slugger`. + * That matters because 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: * - * ```js - * headingId({ slug: (text) => text.toUpperCase() }); - * ``` + * ### `setupMirage` -> #setupmirage + * ### V2 JSON:API -> #v2-jsonapi * - * A heading with an explicit `{#custom-id}` suffix is left alone in every mode. - * - * @param {import('./types').HeadingIdOptions} [options] + * A heading with an explicit `{#custom-id}` suffix is left alone. */ -export function headingId(options) { +export function headingId() { /** * @param {import('mdast').Root} node */ return function (node) { - // Per document, not per plugin: a named slugger carries dedupe state, and a - // compiler can be reused across documents. - const slug = resolveSlugger(options?.slug); + // Per document, not per plugin: the slugger carries the duplicate-heading + // counter (`usage`, `usage-1`), and a compiler can be reused across + // documents. Building it here restarts numbering per document, which is + // also what GitHub does. + const slugger = new GithubSlugger(); visit(node, 'heading', (node) => { const lastChild = node.children[node.children.length - 1]; @@ -137,7 +85,7 @@ export function headingId(options) { } } - setNodeId(node, slug(normalizeText(extractText(node.children)))); + setNodeId(node, slugger.slug(normalizeText(extractText(node.children)))); }); }; } diff --git a/packages/repl-sdk/src/compilers/markdown/parse.test.ts b/packages/repl-sdk/src/compilers/markdown/parse.test.ts index 34c154479..94bcb9798 100644 --- a/packages/repl-sdk/src/compilers/markdown/parse.test.ts +++ b/packages/repl-sdk/src/compilers/markdown/parse.test.ts @@ -230,7 +230,7 @@ describe('default features', () => { expect(result).toMatchInlineSnapshot(` { "codeBlocks": [], - "text": "

<Hello @foo="two" />

", + "text": "

<Hello @foo="two" />

", } `); }); @@ -257,105 +257,46 @@ describe('heading ids', () => { describe('options', () => { describe('headingId', () => { - it('defaults to kebab-case', async () => { + it('matches the anchor GitHub generates', async () => { const result = await parseMarkdown(`## setupMirage`, { ...defaults }); - expect(result.text).toBe('

setupMirage

'); + expect(result.text).toBe('

setupMirage

'); }); - it("slug: 'gfm' matches the anchor GitHub generates", async () => { - const result = await parseMarkdown(`## setupMirage`, { - ...defaults, - headingId: { slug: 'gfm' }, - }); + it('keeps a colon out of the id, the way GitHub does', async () => { + const result = await parseMarkdown(`## V2 JSON:API`, { ...defaults }); - expect(result.text).toBe('

setupMirage

'); + expect(result.text).toBe('

V2 JSON:API

'); }); - it("slug: 'gfm' de-duplicates repeated headings within a document", async () => { - const result = await parseMarkdown(`## Usage\n\n## Usage\n\n## Usage`, { - ...defaults, - headingId: { slug: 'gfm' }, - }); + it('de-duplicates repeated headings within a document', async () => { + const result = await parseMarkdown(`## Usage\n\n## Usage\n\n## Usage`, { ...defaults }); expect(result.text).toContain('id="usage"'); expect(result.text).toContain('id="usage-1"'); expect(result.text).toContain('id="usage-2"'); }); - it("slug: 'gfm' restarts numbering for each document", async () => { - const options = { ...defaults, headingId: { slug: 'gfm' as const } }; - - await parseMarkdown(`## Usage`, options); + it('restarts numbering for each document', async () => { + await parseMarkdown(`## Usage`, { ...defaults }); - const second = await parseMarkdown(`## Usage`, options); + const second = await parseMarkdown(`## Usage`, { ...defaults }); expect(second.text).toBe('

Usage

'); }); - it("slug: 'kebab' is the default and can be named explicitly", async () => { - const result = await parseMarkdown(`## setupMirage`, { - ...defaults, - headingId: { slug: 'kebab' }, - }); - - expect(result.text).toBe('

setupMirage

'); - }); - - it('throws on an unknown named slugger', async () => { - await errorExpect( - parseMarkdown(`## Title`, { - ...defaults, - // @ts-expect-error -- deliberately invalid - headingId: { slug: 'nope' }, - }) - ).rejects.toThrow(/Unknown headingId.slug/); - }); - - it('uses a custom slug function when given', async () => { - const result = await parseMarkdown(`## setupMirage`, { - ...defaults, - headingId: { slug: (text: string) => text.toLowerCase() }, - }); - - expect(result.text).toBe('

setupMirage

'); - }); - - it('passes the heading text, not the node', async () => { - const seen: string[] = []; - - await parseMarkdown(`## Hello *there*`, { - ...defaults, - headingId: { - slug: (text: string) => { - seen.push(text); - - return 'x'; - }, - }, - }); - - expect(seen).to.deep.equal(['Hello there']); - }); - - it('collapses the whitespace joining introduces, so space-to-dash sluggers behave', async () => { - // `## Hello *there*` extracts as 'Hello there' before normalizing; a - // slugger that maps runs of space to '-' would otherwise emit 'hello--there'. - const result = await parseMarkdown(`## Hello *there*`, { - ...defaults, - headingId: { slug: (text: string) => text.toLowerCase().replaceAll(' ', '-') }, - }); + it('collapses the whitespace that joining children introduces', async () => { + // `## Hello *there*` extracts as 'Hello there'; without normalizing, the + // gap survives into the id as `hello--there`. + const result = await parseMarkdown(`## Hello *there*`, { ...defaults }); expect(result.text).toContain('id="hello-there"'); }); it('leaves an explicit {#custom-id} heading alone', async () => { - const result = await parseMarkdown(`## Title {#custom}`, { - ...defaults, - headingId: { slug: () => 'should-not-be-used' }, - }); + const result = await parseMarkdown(`## Title {#custom}`, { ...defaults }); - expect(result.text).not.toContain('should-not-be-used'); + expect(result.text).not.toContain('id="title"'); }); }); diff --git a/packages/repl-sdk/src/compilers/markdown/types.ts b/packages/repl-sdk/src/compilers/markdown/types.ts index 47fcf5ae5..238cf0280 100644 --- a/packages/repl-sdk/src/compilers/markdown/types.ts +++ b/packages/repl-sdk/src/compilers/markdown/types.ts @@ -7,21 +7,6 @@ export interface LiveCodeExtractionOptions { getFlavorFromMeta?: (meta: string, lang: string) => string | undefined; } -export interface HeadingIdOptions { - /** - * How a heading's text becomes its `id`. - * - * - `'kebab'` (default) — kebab-case, e.g. `### setupMirage` → `#setup-mirage` - * - `'gfm'` — the anchor GitHub generates for the same markdown, e.g. - * `### setupMirage` → `#setupmirage`. Use this when the same `.md` is read - * both in a rendered site and on GitHub, so in-page links resolve in both. - * - * A function is also accepted for anything neither mode covers. It receives - * the heading's text with whitespace already collapsed. - */ - slug?: 'kebab' | 'gfm' | ((text: string) => string); -} - export interface PublicOptions { code?: { classList?: string[]; @@ -29,7 +14,6 @@ export interface PublicOptions { demo?: { classList?: string[]; }; - headingId?: HeadingIdOptions; remarkPlugins?: unknown[]; rehypePlugins?: unknown[]; compiler?: { process: (text: string) => Promise<{ data: { liveCode: Array } }> }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a109557e1..69dd31370 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -466,13 +466,13 @@ importers: version: 0.61.1(a64917a33376b7be2998bd4d1c9f349f) ember-repl: specifier: latest - version: 8.2.2(b124149995ac6fe1e0820550d6115f50) + version: 8.2.2(97a2ca31a8e4cf9a385bac208eb0c5f1) ember-resources: specifier: ^7.1.0 version: 7.1.0(5417512ae382abcafe5679ee7b1ac481) kolay: specifier: github:universal-ember/kolay#dist - version: https://codeload.github.com/universal-ember/kolay/tar.gz/2485d3f44037017d35c4c4a059a75a512f736edd(4303bcd233b82c5146db71a8b074d6a0) + version: https://codeload.github.com/universal-ember/kolay/tar.gz/2485d3f44037017d35c4c4a059a75a512f736edd(57fc87d098bbfe8820010d56435c776e) limber-ui: specifier: workspace:* version: link:../../packages/limber-ui @@ -1295,9 +1295,6 @@ importers: '@shikijs/rehype': specifier: ^3.22.0 version: 3.23.0 - change-case: - specifier: ^5.4.4 - version: 5.4.4 codemirror: specifier: ^6.0.2 version: 6.0.2 @@ -8262,7 +8259,7 @@ packages: engines: {node: '>=0.10.0'} kolay@https://codeload.github.com/universal-ember/kolay/tar.gz/2485d3f44037017d35c4c4a059a75a512f736edd: - resolution: {gitHosted: true, integrity: sha512-TaaA5fa1DH1zuHRQTUbtPgu6jxqZE1Bxgi+rHJGYlAva3TdIkV9hj1HW1o3QfzJs+daS1PU82tqk3B49tpfTIA==, tarball: https://codeload.github.com/universal-ember/kolay/tar.gz/2485d3f44037017d35c4c4a059a75a512f736edd} + resolution: {gitHosted: true, tarball: https://codeload.github.com/universal-ember/kolay/tar.gz/2485d3f44037017d35c4c4a059a75a512f736edd} version: 5.4.0 engines: {node: '>= 18'} @@ -17085,7 +17082,7 @@ snapshots: - '@glint/template' - supports-color - ember-repl@8.2.2(b124149995ac6fe1e0820550d6115f50): + ember-repl@8.2.2(97a2ca31a8e4cf9a385bac208eb0c5f1): dependencies: '@ember/test-helpers': 5.4.1(c8332dc3f90c0e60eb130a3a85f980b9) '@ember/test-waiters': 4.1.2 @@ -17095,7 +17092,7 @@ snapshots: ember-primitives: 0.61.1(a64917a33376b7be2998bd4d1c9f349f) ember-resolver: 13.1.1 ember-resources: 7.1.0(5417512ae382abcafe5679ee7b1ac481) - repl-sdk: 1.6.2(6f4bb085e066dcd1e8cba157a733f3a5) + repl-sdk: 1.6.2(93d4969b5698d0d4464d33ccc0660007) optionalDependencies: '@glint/template': 1.7.7 transitivePeerDependencies: @@ -18799,7 +18796,7 @@ snapshots: kind-of@6.0.3: {} - kolay@https://codeload.github.com/universal-ember/kolay/tar.gz/2485d3f44037017d35c4c4a059a75a512f736edd(4303bcd233b82c5146db71a8b074d6a0): + kolay@https://codeload.github.com/universal-ember/kolay/tar.gz/2485d3f44037017d35c4c4a059a75a512f736edd(57fc87d098bbfe8820010d56435c776e): dependencies: '@ember/test-waiters': 4.1.2 '@embroider/addon-shim': 1.10.3 @@ -18811,7 +18808,7 @@ snapshots: decorator-transforms: 2.4.0(@babel/core@7.29.0) ember-modifier: 4.3.0(@babel/core@7.29.0) ember-primitives: 0.61.1(a64917a33376b7be2998bd4d1c9f349f) - ember-repl: 8.2.2(b124149995ac6fe1e0820550d6115f50) + ember-repl: 8.2.2(97a2ca31a8e4cf9a385bac208eb0c5f1) ember-resources: 7.1.0(5417512ae382abcafe5679ee7b1ac481) globby: 16.2.3 json5: 2.2.3 @@ -18823,7 +18820,7 @@ snapshots: remark-gfm: 4.0.1 remark-parse: 11.0.0 remark-rehype: 11.1.2 - repl-sdk: 1.6.2(6f4bb085e066dcd1e8cba157a733f3a5) + repl-sdk: 1.6.2(93d4969b5698d0d4464d33ccc0660007) resolve.exports: 2.0.3 send: 1.2.1 tracked-built-ins: 4.1.2(@babel/core@7.29.0) @@ -20558,7 +20555,7 @@ snapshots: transitivePeerDependencies: - supports-color - repl-sdk@1.6.2(6f4bb085e066dcd1e8cba157a733f3a5): + repl-sdk@1.6.2(93d4969b5698d0d4464d33ccc0660007): dependencies: '@codemirror/autocomplete': 6.20.0 '@codemirror/commands': 6.10.1 From 431a10f33f360f4e990d2c7dd45ec9d7bab21bc4 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Fri, 14 Aug 2026 17:56:45 -0700 Subject: [PATCH 4/9] fix: keep the lockfile change to just this PR's dependency My earlier `pnpm install --no-frozen-lockfile` swept in unrelated upgrades. This repo's .npmrc sets `resolution-mode=highest`, so a full install re-resolves every caret range to the newest match -- including `kolay: ^4.1.0`, which moved from 757d6a0e to 2485d3f4 and broke `tutorial#build:test` with `"isCollection" is not exported by kolay`. markdown-it, globby, vite and vitest drifted too. Restored the lockfile to main and hand-applied only the repl-sdk importer delta: change-case out, github-slugger in. 3 lines instead of 323. `github-slugger@2.0.0` was already in the lockfile via rehype-slug, and change-case is still used by ember-repl, so no package entries change. Verified with `pnpm install --frozen-lockfile`. --- pnpm-lock.yaml | 316 +++++++++++++++++++++++++++---------------------- 1 file changed, 174 insertions(+), 142 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69dd31370..94384184d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -235,7 +235,7 @@ importers: version: 0.1.3 '@embroider/vite': specifier: ^1.7.2 - version: 1.7.2(994f87776d275bddf9bc421f787b0ba3) + version: 1.7.2(97aaab996a93266663833be1dd991207) '@fortawesome/ember-fontawesome': specifier: ^3.0.0 version: 3.1.0(b8564c087c2abe66190fe4148dad7724) @@ -283,7 +283,7 @@ importers: version: 6.1.0(2feedaebee37a40334972cdc05053654) '@tailwindcss/typography': specifier: ^0.5.19 - version: 0.5.19(tailwindcss@3.4.19(yaml@2.9.0)) + version: 0.5.19(tailwindcss@3.4.19(yaml@2.8.3)) '@tsconfig/ember': specifier: ^3.0.9 version: 3.0.12 @@ -400,7 +400,7 @@ importers: version: 3.5.1 tailwindcss: specifier: ^3.0.0 - version: 3.4.19(yaml@2.9.0) + version: 3.4.19(yaml@2.8.3) terser: specifier: ^5.44.1 version: 5.46.0 @@ -424,19 +424,19 @@ importers: version: 6.0.3 vite: specifier: 8.0.8 - version: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) + version: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) vite-bundle-analyzer: specifier: ^1.3.7 version: 1.3.7 vite-ember-ssr: specifier: 0.1.0-alpha.13 - version: 0.1.0-alpha.13(3a4ac714ec0686fc376fb28470713e65) + version: 0.1.0-alpha.13(cc3e84ea8bf61933c19981acdb6d64f0) vite-plugin-circular-dependency: specifier: ^0.5.0 version: 0.5.0(rollup@4.57.1) vite-plugin-mkcert: specifier: ^1.17.9 - version: 1.17.9(3a4ac714ec0686fc376fb28470713e65) + version: 1.17.9(cc3e84ea8bf61933c19981acdb6d64f0) zimmerframe: specifier: ^1.1.4 version: 1.1.4 @@ -466,13 +466,13 @@ importers: version: 0.61.1(a64917a33376b7be2998bd4d1c9f349f) ember-repl: specifier: latest - version: 8.2.2(97a2ca31a8e4cf9a385bac208eb0c5f1) + version: 8.1.0(97a2ca31a8e4cf9a385bac208eb0c5f1) ember-resources: specifier: ^7.1.0 version: 7.1.0(5417512ae382abcafe5679ee7b1ac481) kolay: specifier: github:universal-ember/kolay#dist - version: https://codeload.github.com/universal-ember/kolay/tar.gz/2485d3f44037017d35c4c4a059a75a512f736edd(57fc87d098bbfe8820010d56435c776e) + version: https://codeload.github.com/universal-ember/kolay/tar.gz/757d6a0ec85ebe6244c1e8c53a15f3840392536f(0d7798950681c8d6f11c89df9fde55ef) limber-ui: specifier: workspace:* version: link:../../packages/limber-ui @@ -530,7 +530,7 @@ importers: version: 1.20.2(c8332dc3f90c0e60eb130a3a85f980b9) '@embroider/vite': specifier: ^1.7.2 - version: 1.7.2(994f87776d275bddf9bc421f787b0ba3) + version: 1.7.2(97aaab996a93266663833be1dd991207) '@fortawesome/ember-fontawesome': specifier: ^3.0.0 version: 3.1.0(b8564c087c2abe66190fe4148dad7724) @@ -632,7 +632,7 @@ importers: version: 3.5.1 tailwindcss: specifier: ^3.0.0 - version: 3.4.19(yaml@2.9.0) + version: 3.4.19(yaml@2.8.3) terser: specifier: ^5.44.1 version: 5.46.0 @@ -647,7 +647,7 @@ importers: version: 2.3.11 vite: specifier: ^8.0.8 - version: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) + version: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) dev: dependencies: @@ -832,7 +832,7 @@ importers: dependencies: '@tailwindcss/typography': specifier: ^0.5.16 - version: 0.5.19(tailwindcss@3.4.19(yaml@2.9.0)) + version: 0.5.19(tailwindcss@3.4.19(yaml@2.8.3)) broccoli-funnel: specifier: ^3.0.8 version: 3.0.8 @@ -863,7 +863,7 @@ importers: version: 2.1.2(prettier@3.8.1) tailwindcss: specifier: ^3.0.0 - version: 3.4.19(yaml@2.9.0) + version: 3.4.19(yaml@2.8.3) packages/consts: devDependencies: @@ -957,7 +957,7 @@ importers: version: 1.20.2(c8332dc3f90c0e60eb130a3a85f980b9) '@embroider/vite': specifier: ^1.7.2 - version: 1.7.2(994f87776d275bddf9bc421f787b0ba3) + version: 1.7.2(97aaab996a93266663833be1dd991207) '@eslint/js': specifier: ^9.39.1 version: 9.39.2 @@ -1095,10 +1095,10 @@ importers: version: 13.0.0 vite: specifier: ^8.0.8 - version: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) + version: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) vite-plugin-mkcert: specifier: ^1.17.9 - version: 1.17.9(3a4ac714ec0686fc376fb28470713e65) + version: 1.17.9(cc3e84ea8bf61933c19981acdb6d64f0) packages/horizon-theme: devDependencies: @@ -1234,7 +1234,7 @@ importers: version: 5.9.3 vite: specifier: ^8.0.8 - version: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) + version: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) packages/repl-sdk: dependencies: @@ -1406,13 +1406,13 @@ importers: version: 5.9.3 vite: specifier: ^8.0.8 - version: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) + version: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) vite-plugin-dts: specifier: 4.5.4 - version: 4.5.4(40d780d8e1564ef6a9c3e74a5a7db6b7) + version: 4.5.4(8ff8d5b5f4292de1ffd303963911e09b) vitest: specifier: ^4.0.18 - version: 4.0.18(3f5b38b2ba6f5a8623730cd6c6aa5611) + version: 4.0.18(f35b124e478be927e5c31cf57dd7ef77) packages/repl-sdk/tests-browser: dependencies: @@ -1446,13 +1446,13 @@ importers: version: 4.0.4 '@vitest/browser': specifier: ^3.2.4 - version: 3.2.4(bb9d26120721aeee32f0e6e10380de87) + version: 3.2.4(d10d1473c0c51b1ece3e87404f191532) '@vitest/browser-playwright': specifier: ^4.0.18 - version: 4.0.18(a822696f60557b9c5a75ed597ed66851) + version: 4.0.18(beb02c5d0877ed4a855a5dbf53682eda) '@vitest/browser-webdriverio': specifier: ^4.0.18 - version: 4.0.18(08833fb9beb09b339566be3b8fab4608) + version: 4.0.18(82de5a8281df24df283ca5b9c3bb8729) '@vitest/ui': specifier: 3.2.4 version: 3.2.4(vitest@4.0.18) @@ -1500,13 +1500,13 @@ importers: version: 5.1.0 vite: specifier: ^8.0.8 - version: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) + version: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) vite-plugin-wasm: specifier: ^3.5.0 - version: 3.5.0(3a4ac714ec0686fc376fb28470713e65) + version: 3.5.0(cc3e84ea8bf61933c19981acdb6d64f0) vitest: specifier: ^4.0.18 - version: 4.0.18(3f5b38b2ba6f5a8623730cd6c6aa5611) + version: 4.0.18(f35b124e478be927e5c31cf57dd7ef77) vue: specifier: ^3.5.17 version: 3.5.27(typescript@5.9.3) @@ -1713,7 +1713,7 @@ importers: version: 5.9.3 vite: specifier: ^8.0.8 - version: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) + version: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) packages/syntax/glimdown/codemirror/tests: devDependencies: @@ -1880,7 +1880,7 @@ importers: version: 5.9.3 vite: specifier: ^8.0.8 - version: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) + version: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) packages/syntax/glimmer-s-expression/lezer: dependencies: @@ -2026,7 +2026,7 @@ importers: version: 5.9.3 vite: specifier: ^8.0.8 - version: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) + version: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) packages/syntax/glimmer/codemirror: dependencies: @@ -2147,7 +2147,7 @@ importers: version: 5.9.3 vite: specifier: ^8.0.8 - version: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) + version: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) packages/syntax/glimmer/lezer: dependencies: @@ -2269,7 +2269,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.2.4 - version: 3.2.4(58efc7f29c65c378bec41e1d061beee1) + version: 3.2.4(c3ff36e9605173de7c7f8d7b5e3812c6) packages: @@ -3455,8 +3455,8 @@ packages: '@gar/promisify@1.1.3': resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} - '@gerrit0/mini-shiki@3.23.0': - resolution: {integrity: sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==} + '@gerrit0/mini-shiki@3.21.0': + resolution: {integrity: sha512-9PrsT5DjZA+w3lur/aOIx3FlDeHdyCEFlv9U+fmsVyjPZh61G5SYURQ/1ebe2U63KbDmI2V8IhIUegWb8hjOyg==} '@glimdown/lezer-glimmer-expression@2.0.3': resolution: {integrity: sha512-E7yiSfaL4qkbW+zfbVMGZ9G81MCzst0R02wPK5u2Vt1wWxrpo+mZkCA2fFJuKwvjdcPFt4X04KSRmBQdnL/qVw==} @@ -6862,6 +6862,22 @@ packages: '@glint/template': optional: true + ember-primitives@0.60.1: + resolution: {integrity: sha512-OLUSUM4eqGVaxWGnq3oV1FIBE8JyYUTDTXYWDHd8OHdiFbU10CUJ4A7k6FbpCfBXO7k0Ce+aualiyuIpY28mmg==} + hasBin: true + peerDependencies: + '@ember/test-helpers': '>= 3.2.0' + '@ember/test-waiters': '>= 3.0.2' + '@glimmer/component': ^2.0.0 + '@glint/template': 1.7.7 + ember-modifier: '>= 4.1.0' + ember-resources: '>= 6.1.0' + peerDependenciesMeta: + '@ember/test-helpers': + optional: true + '@glint/template': + optional: true + ember-primitives@0.61.1: resolution: {integrity: sha512-bnr9XEQCqYb86a0yBn9hP5xDuWZwmyT12oPzZrNR8GZJmRjVanlvk64NjVbYVKs56/xxs/zYk4/oct5S9MmsIw==} hasBin: true @@ -6884,8 +6900,8 @@ packages: '@ember/test-helpers': '>=3.0.3' qunit: ^2.13.0 - ember-repl@8.2.2: - resolution: {integrity: sha512-Zh2OosBRdctmyq7mzKdoWAU0hIRIv5B7GNNSOY3AiiK8XVj4jQ2VelZvW6xyV3EWwUpgd8VKPpZAdaKTx7gQoQ==} + ember-repl@8.1.0: + resolution: {integrity: sha512-XO3ryrvnik72AZrhRcGjlAJe6IjI9Vi7EaZghW8y/a1oSdiP2SL/j1DP/e5zOfDvTOxvAcPwHJVWOKH2KTyrCw==} peerDependencies: '@glint/template': 1.7.7 peerDependenciesMeta: @@ -7713,8 +7729,8 @@ packages: resolution: {integrity: sha512-+A4Hq7m7Ze592k9gZRy4gJ27DrXRNnC1vPjxTt1qQxEY8RxagBkBxivkCwg7FxSTG0iLLEMaUx13oOr0R2/qcQ==} engines: {node: '>=20'} - globby@16.2.3: - resolution: {integrity: sha512-VZX7TV7jmd/pn71vdnLKtgwy1IWqc3KjI9x1/UtPkwoKk5fKrNLY30ltDe3cAM5xruIN7YuuaulFt133jRrKZg==} + globby@16.2.2: + resolution: {integrity: sha512-NLvV9ubZ6NDsJaOpKPy3cQeJpKi9DcWiyCiFUpJPA0YihRqiE6RWaLUmgNNPr8MgPpLZjnBjSmou7uZBRJv9wA==} engines: {node: '>=20'} globrex@0.1.2: @@ -8258,8 +8274,8 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} - kolay@https://codeload.github.com/universal-ember/kolay/tar.gz/2485d3f44037017d35c4c4a059a75a512f736edd: - resolution: {gitHosted: true, tarball: https://codeload.github.com/universal-ember/kolay/tar.gz/2485d3f44037017d35c4c4a059a75a512f736edd} + kolay@https://codeload.github.com/universal-ember/kolay/tar.gz/757d6a0ec85ebe6244c1e8c53a15f3840392536f: + resolution: {gitHosted: true, integrity: sha512-jMfOl5d8NP0Crjdd7EU/Mn5NCSre99nh87SX56Ze3N9xC2Pj5UTHbDBLH+fRJGdRcUGo5U/kDp6vDJ59eaRnuw==, tarball: https://codeload.github.com/universal-ember/kolay/tar.gz/757d6a0ec85ebe6244c1e8c53a15f3840392536f} version: 5.4.0 engines: {node: '>= 18'} @@ -8382,8 +8398,8 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - linkify-it@5.0.2: - resolution: {integrity: sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} loader.js@4.7.0: resolution: {integrity: sha512-9M2KvGT6duzGMgkOcTkWb+PR/Q2Oe54df/tLgHGVmFpAmtqJ553xJh6N63iFYI2yjo2PeJXbS5skHi/QpJq4vA==} @@ -8517,8 +8533,8 @@ packages: resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} engines: {node: '>=6'} - markdown-it@14.3.0: - resolution: {integrity: sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==} + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} hasBin: true markdown-table@3.0.4: @@ -9724,8 +9740,8 @@ packages: remove-types@1.0.0: resolution: {integrity: sha512-G7Hk1Q+UJ5DvlNAoJZObxANkBZGiGdp589rVcTW/tYqJWJ5rwfraSnKSQaETN8Epaytw8J40nS/zC7bcHGv36w==} - repl-sdk@1.6.2: - resolution: {integrity: sha512-jGQv4s2IYZmW102DCWWwd14QpkmQdx2QpwiW7axPUsHDN8CPZXVbQVUguAIWpfHhoIJCn+QU+AKq0I8nv3CMMQ==} + repl-sdk@1.6.0: + resolution: {integrity: sha512-zgf2dJy/Vnei15WZ9keI/YR/g1LRwk6AKD6F96oEQiHQdnqpQEWH1CS6Ua0iA89Iyo1SGM/Qmufp67rNvNGKnQ==} request-light@0.7.0: resolution: {integrity: sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==} @@ -10649,12 +10665,12 @@ packages: peerDependencies: typedoc: '>=0.22.x <0.29.x' - typedoc@0.28.20: - resolution: {integrity: sha512-uSKqkh8Cr48vllnEy+jdaAgOeR6Y+QCBW7usgUsKj7gJEfR7stw9U/fE49LBnj2tPRKPY0c0EBJSWe9Appmplg==} + typedoc@0.28.16: + resolution: {integrity: sha512-x4xW77QC3i5DUFMBp0qjukOTnr/sSg+oEs86nB3LjDslvAmwe/PUGDWbe3GrIqt59oTqoXK5GRK9tAa0sYMiog==} engines: {node: '>= 18', pnpm: '>= 10'} hasBin: true peerDependencies: - typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x + typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x typesafe-path@0.2.2: resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} @@ -11380,11 +11396,6 @@ packages: engines: {node: '>= 14.6'} hasBin: true - yaml@2.9.0: - resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} - engines: {node: '>= 14.6'} - hasBin: true - yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -12937,7 +12948,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@embroider/vite@1.7.2(994f87776d275bddf9bc421f787b0ba3)': + '@embroider/vite@1.7.2(97aaab996a93266663833be1dd991207)': dependencies: '@babel/core': 7.29.0 '@embroider/core': 4.4.7(@glint/template@1.7.7) @@ -12955,7 +12966,7 @@ snapshots: send: 0.18.0 source-map-url: 0.4.1 terser: 5.46.0 - vite: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) + vite: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) transitivePeerDependencies: - '@glint/template' - bufferutil @@ -13145,7 +13156,7 @@ snapshots: '@gar/promisify@1.1.3': {} - '@gerrit0/mini-shiki@3.23.0': + '@gerrit0/mini-shiki@3.21.0': dependencies: '@shikijs/engine-oniguruma': 3.23.0 '@shikijs/langs': 3.23.0 @@ -14233,10 +14244,10 @@ snapshots: dependencies: acorn: 8.16.0 - '@tailwindcss/typography@0.5.19(tailwindcss@3.4.19(yaml@2.9.0))': + '@tailwindcss/typography@0.5.19(tailwindcss@3.4.19(yaml@2.8.3))': dependencies: postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.19(yaml@2.9.0) + tailwindcss: 3.4.19(yaml@2.8.3) '@testing-library/dom@10.4.1': dependencies: @@ -14708,23 +14719,23 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitest/browser-playwright@4.0.18(a822696f60557b9c5a75ed597ed66851)': + '@vitest/browser-playwright@4.0.18(beb02c5d0877ed4a855a5dbf53682eda)': dependencies: - '@vitest/browser': 4.0.18(2614c19a9db521911c99ed2a491c14d3) - '@vitest/mocker': 4.0.18(3a4ac714ec0686fc376fb28470713e65) + '@vitest/browser': 4.0.18(a756b5983c87fcbd13c59e78394f6ad4) + '@vitest/mocker': 4.0.18(cc3e84ea8bf61933c19981acdb6d64f0) playwright: 1.58.1 tinyrainbow: 3.0.3 - vitest: 4.0.18(3f5b38b2ba6f5a8623730cd6c6aa5611) + vitest: 4.0.18(f35b124e478be927e5c31cf57dd7ef77) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser-webdriverio@4.0.18(08833fb9beb09b339566be3b8fab4608)': + '@vitest/browser-webdriverio@4.0.18(82de5a8281df24df283ca5b9c3bb8729)': dependencies: - '@vitest/browser': 4.0.18(2614c19a9db521911c99ed2a491c14d3) - vitest: 4.0.18(3f5b38b2ba6f5a8623730cd6c6aa5611) + '@vitest/browser': 4.0.18(a756b5983c87fcbd13c59e78394f6ad4) + vitest: 4.0.18(f35b124e478be927e5c31cf57dd7ef77) webdriverio: 9.23.3 transitivePeerDependencies: - bufferutil @@ -14732,16 +14743,16 @@ snapshots: - utf-8-validate - vite - '@vitest/browser@3.2.4(5e31ff89691a6aa19bc5de8e97aa636e)': + '@vitest/browser@3.2.4(4fd95ed8106a190e5d70d04cbde3b222)': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/mocker': 3.2.4(2e088714ecb038fa5c6f5f197b01307a) + '@vitest/mocker': 3.2.4(449e7d7b1644d13515d67bb1a50e94e2) '@vitest/utils': 3.2.4 magic-string: 0.30.21 sirv: 3.0.2 tinyrainbow: 2.0.0 - vitest: 3.2.4(58efc7f29c65c378bec41e1d061beee1) + vitest: 3.2.4(c3ff36e9605173de7c7f8d7b5e3812c6) ws: 8.19.0 optionalDependencies: playwright: 1.58.1 @@ -14753,16 +14764,16 @@ snapshots: - vite optional: true - '@vitest/browser@3.2.4(bb9d26120721aeee32f0e6e10380de87)': + '@vitest/browser@3.2.4(d10d1473c0c51b1ece3e87404f191532)': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/mocker': 3.2.4(3a4ac714ec0686fc376fb28470713e65) + '@vitest/mocker': 3.2.4(cc3e84ea8bf61933c19981acdb6d64f0) '@vitest/utils': 3.2.4 magic-string: 0.30.21 sirv: 3.0.2 tinyrainbow: 2.0.0 - vitest: 4.0.18(3f5b38b2ba6f5a8623730cd6c6aa5611) + vitest: 4.0.18(f35b124e478be927e5c31cf57dd7ef77) ws: 8.19.0 optionalDependencies: playwright: 1.58.1 @@ -14773,16 +14784,16 @@ snapshots: - utf-8-validate - vite - '@vitest/browser@4.0.18(2614c19a9db521911c99ed2a491c14d3)': + '@vitest/browser@4.0.18(a756b5983c87fcbd13c59e78394f6ad4)': dependencies: - '@vitest/mocker': 4.0.18(3a4ac714ec0686fc376fb28470713e65) + '@vitest/mocker': 4.0.18(cc3e84ea8bf61933c19981acdb6d64f0) '@vitest/utils': 4.0.18 magic-string: 0.30.21 pixelmatch: 7.1.0 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.0.3 - vitest: 4.0.18(3f5b38b2ba6f5a8623730cd6c6aa5611) + vitest: 4.0.18(f35b124e478be927e5c31cf57dd7ef77) ws: 8.19.0 transitivePeerDependencies: - bufferutil @@ -14807,37 +14818,37 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@3.2.4(2e088714ecb038fa5c6f5f197b01307a)': + '@vitest/mocker@3.2.4(449e7d7b1644d13515d67bb1a50e94e2)': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(bb1b215ab7298e4ef4d4b8cdd426c297) + vite: 7.3.1(c15aa76a89bd46a8955d1cc0ec2e331d) - '@vitest/mocker@3.2.4(3a4ac714ec0686fc376fb28470713e65)': + '@vitest/mocker@3.2.4(cc3e84ea8bf61933c19981acdb6d64f0)': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) + vite: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) - '@vitest/mocker@4.0.18(2e088714ecb038fa5c6f5f197b01307a)': + '@vitest/mocker@4.0.18(449e7d7b1644d13515d67bb1a50e94e2)': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(bb1b215ab7298e4ef4d4b8cdd426c297) + vite: 7.3.1(c15aa76a89bd46a8955d1cc0ec2e331d) - '@vitest/mocker@4.0.18(3a4ac714ec0686fc376fb28470713e65)': + '@vitest/mocker@4.0.18(cc3e84ea8bf61933c19981acdb6d64f0)': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) + vite: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) '@vitest/pretty-format@3.2.4': dependencies: @@ -14885,7 +14896,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.4(58efc7f29c65c378bec41e1d061beee1) + vitest: 3.2.4(c3ff36e9605173de7c7f8d7b5e3812c6) optional: true '@vitest/ui@3.2.4(vitest@4.0.18)': @@ -14897,7 +14908,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 4.0.18(3f5b38b2ba6f5a8623730cd6c6aa5611) + vitest: 4.0.18(f35b124e478be927e5c31cf57dd7ef77) '@vitest/utils@3.2.4': dependencies: @@ -17045,6 +17056,31 @@ snapshots: - '@babel/core' - supports-color + ember-primitives@0.60.1(a64917a33376b7be2998bd4d1c9f349f): + dependencies: + '@babel/runtime': 7.29.2 + '@ember/test-waiters': 4.1.2 + '@embroider/addon-shim': 1.10.2 + '@floating-ui/dom': 1.7.5 + '@glimmer/component': 2.1.1 + decorator-transforms: 2.3.2(@babel/core@7.29.0) + ember-element-helper: 0.8.8 + ember-modifier: 4.3.0(@babel/core@7.29.0) + ember-resources: 7.1.0(5417512ae382abcafe5679ee7b1ac481) + form-data-utils: 0.6.0 + reactiveweb: 1.9.1(9f91e21669edde5a0835948ba5900801) + should-handle-link: 1.3.0 + tabster: 8.7.0 + tracked-built-ins: 4.1.2(@babel/core@7.29.0) + tracked-toolbox: 3.0.0(@babel/core@7.29.0) + which-heading-do-i-need: 0.4.1 + optionalDependencies: + '@ember/test-helpers': 5.4.1(c8332dc3f90c0e60eb130a3a85f980b9) + '@glint/template': 1.7.7 + transitivePeerDependencies: + - '@babel/core' + - supports-color + ember-primitives@0.61.1(a64917a33376b7be2998bd4d1c9f349f): dependencies: '@babel/runtime': 7.29.2 @@ -17082,17 +17118,17 @@ snapshots: - '@glint/template' - supports-color - ember-repl@8.2.2(97a2ca31a8e4cf9a385bac208eb0c5f1): + ember-repl@8.1.0(97a2ca31a8e4cf9a385bac208eb0c5f1): dependencies: '@ember/test-helpers': 5.4.1(c8332dc3f90c0e60eb130a3a85f980b9) '@ember/test-waiters': 4.1.2 codemirror: 6.0.2 content-tag: 4.2.0 decorator-transforms: 2.3.1(@babel/core@7.29.0) - ember-primitives: 0.61.1(a64917a33376b7be2998bd4d1c9f349f) + ember-primitives: 0.51.0(a64917a33376b7be2998bd4d1c9f349f) ember-resolver: 13.1.1 ember-resources: 7.1.0(5417512ae382abcafe5679ee7b1ac481) - repl-sdk: 1.6.2(93d4969b5698d0d4464d33ccc0660007) + repl-sdk: 1.6.0(93d4969b5698d0d4464d33ccc0660007) optionalDependencies: '@glint/template': 1.7.7 transitivePeerDependencies: @@ -18182,7 +18218,7 @@ snapshots: slash: 5.1.0 unicorn-magic: 0.4.0 - globby@16.2.3: + globby@16.2.2: dependencies: '@sindresorhus/merge-streams': 4.0.0 fast-glob: 3.3.3 @@ -18796,10 +18832,10 @@ snapshots: kind-of@6.0.3: {} - kolay@https://codeload.github.com/universal-ember/kolay/tar.gz/2485d3f44037017d35c4c4a059a75a512f736edd(57fc87d098bbfe8820010d56435c776e): + kolay@https://codeload.github.com/universal-ember/kolay/tar.gz/757d6a0ec85ebe6244c1e8c53a15f3840392536f(0d7798950681c8d6f11c89df9fde55ef): dependencies: '@ember/test-waiters': 4.1.2 - '@embroider/addon-shim': 1.10.3 + '@embroider/addon-shim': 1.10.2 '@glimmer/component': 2.1.1 '@glint/template': 1.7.7 change-case: 5.4.4 @@ -18807,12 +18843,11 @@ snapshots: content-tag: 4.2.0 decorator-transforms: 2.4.0(@babel/core@7.29.0) ember-modifier: 4.3.0(@babel/core@7.29.0) - ember-primitives: 0.61.1(a64917a33376b7be2998bd4d1c9f349f) - ember-repl: 8.2.2(97a2ca31a8e4cf9a385bac208eb0c5f1) + ember-primitives: 0.60.1(a64917a33376b7be2998bd4d1c9f349f) + ember-repl: 8.1.0(97a2ca31a8e4cf9a385bac208eb0c5f1) ember-resources: 7.1.0(5417512ae382abcafe5679ee7b1ac481) - globby: 16.2.3 + globby: 16.2.2 json5: 2.2.3 - lilconfig: 3.1.3 package-up: 5.0.0 reactiveweb: 1.9.1(9f91e21669edde5a0835948ba5900801) rehype-raw: 7.0.0 @@ -18820,14 +18855,13 @@ snapshots: remark-gfm: 4.0.1 remark-parse: 11.0.0 remark-rehype: 11.1.2 - repl-sdk: 1.6.2(93d4969b5698d0d4464d33ccc0660007) - resolve.exports: 2.0.3 + repl-sdk: 1.6.0(93d4969b5698d0d4464d33ccc0660007) send: 1.2.1 tracked-built-ins: 4.1.2(@babel/core@7.29.0) - typedoc: 0.28.20(typescript@5.9.3) - typedoc-plugin-rename-defaults: 0.7.3(typedoc@0.28.20(typescript@5.9.3)) + typedoc: 0.28.16(typescript@5.9.3) + typedoc-plugin-rename-defaults: 0.7.3(typedoc@0.28.16(typescript@5.9.3)) unist-util-visit: 5.1.0 - unplugin: 3.3.0(c09f9af6a20ab73299230aff0ce4a374) + unplugin: 3.3.0(90af3858bcb41b7217b92ee30358691a) transitivePeerDependencies: - '@babel/core' - '@codemirror/lang-css' @@ -18949,7 +18983,7 @@ snapshots: lines-and-columns@1.2.4: {} - linkify-it@5.0.2: + linkify-it@5.0.0: dependencies: uc.micro: 2.1.0 @@ -19089,11 +19123,11 @@ snapshots: dependencies: p-defer: 1.0.0 - markdown-it@14.3.0: + markdown-it@14.1.0: dependencies: argparse: 2.0.1 entities: 4.5.0 - linkify-it: 5.0.2 + linkify-it: 5.0.0 mdurl: 2.0.0 punycode.js: 2.3.1 uc.micro: 2.1.0 @@ -20141,13 +20175,13 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.5.9 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.9)(yaml@2.9.0): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.9)(yaml@2.8.3): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 postcss: 8.5.9 - yaml: 2.9.0 + yaml: 2.8.3 postcss-nested@6.2.0(postcss@8.5.9): dependencies: @@ -20555,7 +20589,7 @@ snapshots: transitivePeerDependencies: - supports-color - repl-sdk@1.6.2(93d4969b5698d0d4464d33ccc0660007): + repl-sdk@1.6.0(93d4969b5698d0d4464d33ccc0660007): dependencies: '@codemirror/autocomplete': 6.20.0 '@codemirror/commands': 6.10.1 @@ -21372,7 +21406,7 @@ snapshots: tagged-tag@1.0.0: {} - tailwindcss@3.4.19(yaml@2.9.0): + tailwindcss@3.4.19(yaml@2.8.3): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -21391,7 +21425,7 @@ snapshots: postcss: 8.5.9 postcss-import: 15.1.0(postcss@8.5.9) postcss-js: 4.1.0(postcss@8.5.9) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.9)(yaml@2.9.0) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.9)(yaml@2.8.3) postcss-nested: 6.2.0(postcss@8.5.9) postcss-selector-parser: 6.1.2 resolve: 1.22.12 @@ -21804,19 +21838,19 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 - typedoc-plugin-rename-defaults@0.7.3(typedoc@0.28.20(typescript@5.9.3)): + typedoc-plugin-rename-defaults@0.7.3(typedoc@0.28.16(typescript@5.9.3)): dependencies: camelcase: 8.0.0 - typedoc: 0.28.20(typescript@5.9.3) + typedoc: 0.28.16(typescript@5.9.3) - typedoc@0.28.20(typescript@5.9.3): + typedoc@0.28.16(typescript@5.9.3): dependencies: - '@gerrit0/mini-shiki': 3.23.0 + '@gerrit0/mini-shiki': 3.21.0 lunr: 2.3.9 - markdown-it: 14.3.0 - minimatch: 10.2.5 + markdown-it: 14.1.0 + minimatch: 9.0.5 typescript: 5.9.3 - yaml: 2.9.0 + yaml: 2.8.3 typesafe-path@0.2.2: {} @@ -21955,7 +21989,7 @@ snapshots: picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 - unplugin@3.3.0(c09f9af6a20ab73299230aff0ce4a374): + unplugin@3.3.0(90af3858bcb41b7217b92ee30358691a): dependencies: '@jridgewell/remapping': 2.3.5 picomatch: 4.0.4 @@ -21963,7 +21997,7 @@ snapshots: optionalDependencies: esbuild: 0.27.2 rollup: 4.57.1 - vite: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) + vite: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) unrs-resolver@1.11.1: dependencies: @@ -22045,22 +22079,22 @@ snapshots: vite-bundle-analyzer@1.3.7: {} - vite-ember-ssr@0.1.0-alpha.13(3a4ac714ec0686fc376fb28470713e65): + vite-ember-ssr@0.1.0-alpha.13(cc3e84ea8bf61933c19981acdb6d64f0): dependencies: happy-dom: 20.8.9 tinypool: 2.1.0 - vite: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) + vite: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) transitivePeerDependencies: - bufferutil - utf-8-validate - vite-node@3.2.4(bb1b215ab7298e4ef4d4b8cdd426c297): + vite-node@3.2.4(c15aa76a89bd46a8955d1cc0ec2e331d): dependencies: cac: 6.7.14 debug: 4.4.3(supports-color@8.1.1) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.1(bb1b215ab7298e4ef4d4b8cdd426c297) + vite: 7.3.1(c15aa76a89bd46a8955d1cc0ec2e331d) transitivePeerDependencies: - '@types/node' - jiti @@ -22082,7 +22116,7 @@ snapshots: transitivePeerDependencies: - rollup - vite-plugin-dts@4.5.4(40d780d8e1564ef6a9c3e74a5a7db6b7): + vite-plugin-dts@4.5.4(8ff8d5b5f4292de1ffd303963911e09b): dependencies: '@microsoft/api-extractor': 7.56.0(@types/node@24.10.9) '@rollup/pluginutils': 5.3.0(rollup@4.57.1) @@ -22095,26 +22129,26 @@ snapshots: magic-string: 0.30.21 typescript: 5.9.3 optionalDependencies: - vite: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) + vite: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite-plugin-mkcert@1.17.9(3a4ac714ec0686fc376fb28470713e65): + vite-plugin-mkcert@1.17.9(cc3e84ea8bf61933c19981acdb6d64f0): dependencies: axios: 1.13.4(debug@4.4.3) debug: 4.4.3(supports-color@8.1.1) picocolors: 1.1.1 - vite: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) + vite: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) transitivePeerDependencies: - supports-color - vite-plugin-wasm@3.5.0(3a4ac714ec0686fc376fb28470713e65): + vite-plugin-wasm@3.5.0(cc3e84ea8bf61933c19981acdb6d64f0): dependencies: - vite: 8.0.8(0958e47a27b9dbd3a2ad515effef1b96) + vite: 8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7) - vite@7.3.1(bb1b215ab7298e4ef4d4b8cdd426c297): + vite@7.3.1(c15aa76a89bd46a8955d1cc0ec2e331d): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.4) @@ -22128,9 +22162,9 @@ snapshots: jiti: 2.6.1 lightningcss: 1.32.0 terser: 5.46.0 - yaml: 2.9.0 + yaml: 2.8.3 - vite@8.0.8(0958e47a27b9dbd3a2ad515effef1b96): + vite@8.0.8(a73d90236c2f1639dd2b0f0c483e3bf7): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -22143,13 +22177,13 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 terser: 5.46.0 - yaml: 2.9.0 + yaml: 2.8.3 - vitest@3.2.4(58efc7f29c65c378bec41e1d061beee1): + vitest@3.2.4(c3ff36e9605173de7c7f8d7b5e3812c6): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(2e088714ecb038fa5c6f5f197b01307a) + '@vitest/mocker': 3.2.4(449e7d7b1644d13515d67bb1a50e94e2) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -22167,13 +22201,13 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.1(bb1b215ab7298e4ef4d4b8cdd426c297) - vite-node: 3.2.4(bb1b215ab7298e4ef4d4b8cdd426c297) + vite: 7.3.1(c15aa76a89bd46a8955d1cc0ec2e331d) + vite-node: 3.2.4(c15aa76a89bd46a8955d1cc0ec2e331d) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 '@types/node': 24.10.9 - '@vitest/browser': 3.2.4(5e31ff89691a6aa19bc5de8e97aa636e) + '@vitest/browser': 3.2.4(4fd95ed8106a190e5d70d04cbde3b222) '@vitest/ui': 3.2.4(vitest@3.2.4) happy-dom: 20.8.9 jsdom: 26.1.0 @@ -22191,10 +22225,10 @@ snapshots: - tsx - yaml - vitest@4.0.18(3f5b38b2ba6f5a8623730cd6c6aa5611): + vitest@4.0.18(f35b124e478be927e5c31cf57dd7ef77): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(2e088714ecb038fa5c6f5f197b01307a) + '@vitest/mocker': 4.0.18(449e7d7b1644d13515d67bb1a50e94e2) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -22211,12 +22245,12 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(bb1b215ab7298e4ef4d4b8cdd426c297) + vite: 7.3.1(c15aa76a89bd46a8955d1cc0ec2e331d) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.10.9 - '@vitest/browser-playwright': 4.0.18(a822696f60557b9c5a75ed597ed66851) - '@vitest/browser-webdriverio': 4.0.18(08833fb9beb09b339566be3b8fab4608) + '@vitest/browser-playwright': 4.0.18(beb02c5d0877ed4a855a5dbf53682eda) + '@vitest/browser-webdriverio': 4.0.18(82de5a8281df24df283ca5b9c3bb8729) '@vitest/ui': 3.2.4(vitest@4.0.18) happy-dom: 20.8.9 jsdom: 26.1.0 @@ -22530,8 +22564,6 @@ snapshots: yaml@2.8.3: {} - yaml@2.9.0: {} - yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} From 11beba2b925302c087ae2e2cbb48328e9f9931f4 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Fri, 14 Aug 2026 18:00:21 -0700 Subject: [PATCH 5/9] fix(repl-sdk): concatenate heading text instead of normalizing it @gitKrystan asked whether the whitespace normalization was needed at all with github-slugger. It wasn't -- and it was making us diverge from GitHub. Checked against rehype-slug, which is the github-slugger reference: ## Hello World -> id="hello----world" (does NOT collapse) ## Hello *there* -> id="hello-there" (no separator inserted) ## `setupMirage` and more -> id="setupmirage-and-more" So the actual defect was `extractText` joining children with a space, adding one the markdown never had: `## Hello *there*` extracted as `'Hello there'` and slugged as `hello--there`. Joining with '' fixes that at the source. Collapsing author-written runs on top of it was wrong -- GitHub keeps those. Drops normalizeText and the test asserting collapsing (which came from #2215, back when kebabCase was the target). Adds two tests pinning parity with the rehype-slug results above. --- packages/repl-sdk/README.md | 2 ++ .../src/compilers/markdown/heading-id.js | 24 ++++++++----------- .../src/compilers/markdown/parse.test.ts | 22 +++++++++-------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/repl-sdk/README.md b/packages/repl-sdk/README.md index 83c3b9535..781336cd0 100644 --- a/packages/repl-sdk/README.md +++ b/packages/repl-sdk/README.md @@ -31,6 +31,8 @@ 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. diff --git a/packages/repl-sdk/src/compilers/markdown/heading-id.js b/packages/repl-sdk/src/compilers/markdown/heading-id.js index 1bed0855d..1f7cbda79 100644 --- a/packages/repl-sdk/src/compilers/markdown/heading-id.js +++ b/packages/repl-sdk/src/compilers/markdown/heading-id.js @@ -2,6 +2,14 @@ import GithubSlugger from 'github-slugger'; import { visit } from 'unist-util-visit'; /** + * A heading's text, as `github-slugger` should see it: the children's text + * concatenated, with nothing inserted between them. Joining with a space would + * add one that the markdown never had -- `## Hello *there*` becomes + * `'Hello there'` and slugs as `hello--there` rather than `hello-there`. + * + * Whitespace the author actually wrote is left alone, since GitHub keeps it too: + * `## Hello World` slugs as `hello----world` there and here. + * * @param {import('mdast').PhrasingContent[]} children * @return {string} */ @@ -23,19 +31,7 @@ function extractText(children) { } } ) - .join(' '); -} - -/** - * `extractText` joins a heading's children with a space, on top of whatever - * spacing the text nodes already carry, so `## Hello *there*` extracts as - * `'Hello there'`. Collapse that before slugging, or the gap survives into the - * id: `hello--there` rather than `hello-there`. - * - * @param {string} value - */ -function normalizeText(value) { - return value.replaceAll(/\s+/g, ' ').trim(); + .join(''); } /** @@ -85,7 +81,7 @@ export function headingId() { } } - setNodeId(node, slugger.slug(normalizeText(extractText(node.children)))); + setNodeId(node, slugger.slug(extractText(node.children))); }); }; } diff --git a/packages/repl-sdk/src/compilers/markdown/parse.test.ts b/packages/repl-sdk/src/compilers/markdown/parse.test.ts index 94bcb9798..31127b7d5 100644 --- a/packages/repl-sdk/src/compilers/markdown/parse.test.ts +++ b/packages/repl-sdk/src/compilers/markdown/parse.test.ts @@ -238,13 +238,6 @@ describe('default features', () => { }); describe('heading ids', () => { - it('collapses runs of whitespace in the heading text', async () => { - const result = await parseMarkdown(`## Hello World `, { ...defaults }); - - // Only the id is normalized; the rendered text keeps the author's spacing. - expect(result.text).toContain('id="hello-world"'); - }); - it('treats a literal \\s in a heading as text, not as whitespace', async () => { // The regex in formatDefaultId used to be /\\s+/g -- a literal backslash // followed by `s`, rather than whitespace -- so `\s` here was replaced with @@ -285,14 +278,23 @@ describe('options', () => { expect(second.text).toBe('

Usage

'); }); - it('collapses the whitespace that joining children introduces', async () => { - // `## Hello *there*` extracts as 'Hello there'; without normalizing, the - // gap survives into the id as `hello--there`. + it("inserts nothing between a heading's children", async () => { + // Verified against rehype-slug, which is the github-slugger reference: + // `## Hello *there*` -> id="hello-there". Joining the children with a + // space instead would add one the markdown never had -> `hello--there`. const result = await parseMarkdown(`## Hello *there*`, { ...defaults }); expect(result.text).toContain('id="hello-there"'); }); + it('keeps whitespace the author actually wrote, like GitHub does', async () => { + // github-slugger does not collapse runs; rehype-slug gives + // id="hello----world" for this input, so matching it means not collapsing. + const result = await parseMarkdown(`## Hello World `, { ...defaults }); + + expect(result.text).toContain('id="hello----world"'); + }); + it('leaves an explicit {#custom-id} heading alone', async () => { const result = await parseMarkdown(`## Title {#custom}`, { ...defaults }); From 171ed64118d2408bb06371b66a6bf4bda9fddd32 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Fri, 14 Aug 2026 18:14:26 -0700 Subject: [PATCH 6/9] fix(repl-sdk): restore build-compiler formatting Reverting the headingId option left the call chain split across lines; with the argument gone it fits on one again, which is what main had. Caught by repl-sdk#lint:prettier -- I had checked the other files but not this one. --- packages/repl-sdk/src/compilers/markdown/build-compiler.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/repl-sdk/src/compilers/markdown/build-compiler.js b/packages/repl-sdk/src/compilers/markdown/build-compiler.js index d6408b58f..44072dc60 100644 --- a/packages/repl-sdk/src/compilers/markdown/build-compiler.js +++ b/packages/repl-sdk/src/compilers/markdown/build-compiler.js @@ -20,10 +20,7 @@ import { sanitizeForGlimmer } from './sanitize-for-glimmer.js'; * @returns {import('unified').Processor} */ export function buildCompiler(options) { - let compiler = unified() - .use(remarkParse) - .use(remarkGfm, { singleTilde: true }) - .use(headingId); + let compiler = unified().use(remarkParse).use(remarkGfm, { singleTilde: true }).use(headingId); /** * If this were "use"d after `remarkRehype`, From 6eeeca37798d92c7b2edde93933aa6acea2caa12 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli-ai-agent <268630448+NullVoxPopuli-ai-agent@users.noreply.github.com> Date: Sat, 15 Aug 2026 18:16:59 -0400 Subject: [PATCH 7/9] refactor(repl-sdk): extract heading text with mdast-util-to-string Replaces the hand-rolled extractText. The hand-rolled walk dropped whitespace-only text nodes, so a heading like '## *a* *b*' slugged as 'ab' where GitHub gives 'a-b'. mdast-util-to-string is the maintained extractor with GitHub's semantics, and is already in the dependency tree via remark-gfm, so declaring it installs nothing new. Co-Authored-By: Claude Fable 5 --- packages/repl-sdk/package.json | 1 + .../src/compilers/markdown/heading-id.js | 38 ++----------------- .../src/compilers/markdown/parse.test.ts | 8 ++++ pnpm-lock.yaml | 3 ++ 4 files changed, 16 insertions(+), 34 deletions(-) diff --git a/packages/repl-sdk/package.json b/packages/repl-sdk/package.json index e4d90340d..909c1d003 100644 --- a/packages/repl-sdk/package.json +++ b/packages/repl-sdk/package.json @@ -96,6 +96,7 @@ "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 1f7cbda79..3b3d99d1c 100644 --- a/packages/repl-sdk/src/compilers/markdown/heading-id.js +++ b/packages/repl-sdk/src/compilers/markdown/heading-id.js @@ -1,39 +1,7 @@ import GithubSlugger from 'github-slugger'; +import { toString } from 'mdast-util-to-string'; import { visit } from 'unist-util-visit'; -/** - * A heading's text, as `github-slugger` should see it: the children's text - * concatenated, with nothing inserted between them. Joining with a space would - * add one that the markdown never had -- `## Hello *there*` becomes - * `'Hello there'` and slugs as `hello--there` rather than `hello-there`. - * - * Whitespace the author actually wrote is left alone, since GitHub keeps it too: - * `## Hello World` slugs as `hello----world` there and here. - * - * @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 {import('mdast').Heading} node * @param {string} id @@ -81,7 +49,9 @@ export function headingId() { } } - setNodeId(node, slugger.slug(extractText(node.children))); + // github-slugger must see the text exactly as GitHub renders it, + // including whitespace-only nodes: `## *a* *b*` is `a b`, not `ab`. + 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 31127b7d5..601e7f54e 100644 --- a/packages/repl-sdk/src/compilers/markdown/parse.test.ts +++ b/packages/repl-sdk/src/compilers/markdown/parse.test.ts @@ -287,6 +287,14 @@ describe('options', () => { expect(result.text).toContain('id="hello-there"'); }); + it('keeps whitespace-only text nodes between formatted children', async () => { + // The space between the emphases is its own text node. Dropping it + // would slug `ab`; GitHub slugs `a-b`. + const result = await parseMarkdown(`## *a* *b*`, { ...defaults }); + + expect(result.text).toContain('id="a-b"'); + }); + it('keeps whitespace the author actually wrote, like GitHub does', async () => { // github-slugger does not collapse runs; rehype-slug gives // id="hello----world" for this input, so matching it means not collapsing. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94384184d..684ae113c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1325,6 +1325,9 @@ importers: mdast: specifier: ^3.0.0 version: 3.0.0 + mdast-util-to-string: + specifier: ^4.0.0 + version: 4.0.0 mime: specifier: ^4.0.7 version: 4.1.0 From f3f43048f65b1f0b73fd3ed958ed6c0347ee6ba3 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli-ai-agent <268630448+NullVoxPopuli-ai-agent@users.noreply.github.com> Date: Sat, 15 Aug 2026 18:27:19 -0400 Subject: [PATCH 8/9] test(repl-sdk): move headingId tests out of the options block headingId no longer takes options; all heading-id tests now live in one top-level describe. Co-Authored-By: Claude Fable 5 --- .../src/compilers/markdown/parse.test.ts | 90 +++++++++---------- 1 file changed, 44 insertions(+), 46 deletions(-) diff --git a/packages/repl-sdk/src/compilers/markdown/parse.test.ts b/packages/repl-sdk/src/compilers/markdown/parse.test.ts index 601e7f54e..12c327215 100644 --- a/packages/repl-sdk/src/compilers/markdown/parse.test.ts +++ b/packages/repl-sdk/src/compilers/markdown/parse.test.ts @@ -237,7 +237,7 @@ describe('default features', () => { }); }); -describe('heading ids', () => { +describe('headingId', () => { it('treats a literal \\s in a heading as text, not as whitespace', async () => { // The regex in formatDefaultId used to be /\\s+/g -- a literal backslash // followed by `s`, rather than whitespace -- so `\s` here was replaced with @@ -246,70 +246,68 @@ describe('heading ids', () => { expect(result.text).toContain('id="a-s-b"'); }); -}); -describe('options', () => { - describe('headingId', () => { - it('matches the anchor GitHub generates', async () => { - const result = await parseMarkdown(`## setupMirage`, { ...defaults }); + it('matches the anchor GitHub generates', async () => { + const result = await parseMarkdown(`## setupMirage`, { ...defaults }); - expect(result.text).toBe('

setupMirage

'); - }); + expect(result.text).toBe('

setupMirage

'); + }); - it('keeps a colon out of the id, the way GitHub does', async () => { - const result = await parseMarkdown(`## V2 JSON:API`, { ...defaults }); + it('keeps a colon out of the id, the way GitHub does', async () => { + const result = await parseMarkdown(`## V2 JSON:API`, { ...defaults }); - expect(result.text).toBe('

V2 JSON:API

'); - }); + expect(result.text).toBe('

V2 JSON:API

'); + }); - it('de-duplicates repeated headings within a document', async () => { - const result = await parseMarkdown(`## Usage\n\n## Usage\n\n## Usage`, { ...defaults }); + it('de-duplicates repeated headings within a document', async () => { + const result = await parseMarkdown(`## Usage\n\n## Usage\n\n## Usage`, { ...defaults }); - expect(result.text).toContain('id="usage"'); - expect(result.text).toContain('id="usage-1"'); - expect(result.text).toContain('id="usage-2"'); - }); + expect(result.text).toContain('id="usage"'); + expect(result.text).toContain('id="usage-1"'); + expect(result.text).toContain('id="usage-2"'); + }); - it('restarts numbering for each document', async () => { - await parseMarkdown(`## Usage`, { ...defaults }); + it('restarts numbering for each document', async () => { + await parseMarkdown(`## Usage`, { ...defaults }); - const second = await parseMarkdown(`## Usage`, { ...defaults }); + const second = await parseMarkdown(`## Usage`, { ...defaults }); - expect(second.text).toBe('

Usage

'); - }); + expect(second.text).toBe('

Usage

'); + }); - it("inserts nothing between a heading's children", async () => { - // Verified against rehype-slug, which is the github-slugger reference: - // `## Hello *there*` -> id="hello-there". Joining the children with a - // space instead would add one the markdown never had -> `hello--there`. - const result = await parseMarkdown(`## Hello *there*`, { ...defaults }); + it("inserts nothing between a heading's children", async () => { + // Verified against rehype-slug, which is the github-slugger reference: + // `## Hello *there*` -> id="hello-there". Joining the children with a + // space instead would add one the markdown never had -> `hello--there`. + const result = await parseMarkdown(`## Hello *there*`, { ...defaults }); - expect(result.text).toContain('id="hello-there"'); - }); + expect(result.text).toContain('id="hello-there"'); + }); - it('keeps whitespace-only text nodes between formatted children', async () => { - // The space between the emphases is its own text node. Dropping it - // would slug `ab`; GitHub slugs `a-b`. - const result = await parseMarkdown(`## *a* *b*`, { ...defaults }); + it('keeps whitespace-only text nodes between formatted children', async () => { + // The space between the emphases is its own text node. Dropping it + // would slug `ab`; GitHub slugs `a-b`. + const result = await parseMarkdown(`## *a* *b*`, { ...defaults }); - expect(result.text).toContain('id="a-b"'); - }); + expect(result.text).toContain('id="a-b"'); + }); - it('keeps whitespace the author actually wrote, like GitHub does', async () => { - // github-slugger does not collapse runs; rehype-slug gives - // id="hello----world" for this input, so matching it means not collapsing. - const result = await parseMarkdown(`## Hello World `, { ...defaults }); + it('keeps whitespace the author actually wrote, like GitHub does', async () => { + // github-slugger does not collapse runs; rehype-slug gives + // id="hello----world" for this input, so matching it means not collapsing. + const result = await parseMarkdown(`## Hello World `, { ...defaults }); - expect(result.text).toContain('id="hello----world"'); - }); + expect(result.text).toContain('id="hello----world"'); + }); - it('leaves an explicit {#custom-id} heading alone', async () => { - const result = await parseMarkdown(`## Title {#custom}`, { ...defaults }); + it('leaves an explicit {#custom-id} heading alone', async () => { + const result = await parseMarkdown(`## Title {#custom}`, { ...defaults }); - expect(result.text).not.toContain('id="title"'); - }); + expect(result.text).not.toContain('id="title"'); }); +}); +describe('options', () => { describe('remarkPlugins', () => { it('works', async () => { const result = await parseMarkdown(`# Title`, { From 5d8266ed1d83c7503cae0a500a0d73e7e784c056 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli-ai-agent <268630448+NullVoxPopuli-ai-agent@users.noreply.github.com> Date: Sat, 15 Aug 2026 18:31:18 -0400 Subject: [PATCH 9/9] style(repl-sdk): drop narrative comments, keep signature JSDoc Co-Authored-By: Claude Fable 5 --- .../src/compilers/markdown/heading-id.js | 19 ------------------- .../src/compilers/markdown/parse.test.ts | 10 ---------- 2 files changed, 29 deletions(-) diff --git a/packages/repl-sdk/src/compilers/markdown/heading-id.js b/packages/repl-sdk/src/compilers/markdown/heading-id.js index 3b3d99d1c..cd0e6137d 100644 --- a/packages/repl-sdk/src/compilers/markdown/heading-id.js +++ b/packages/repl-sdk/src/compilers/markdown/heading-id.js @@ -13,28 +13,11 @@ function setNodeId(node, id) { /** @type {any} */ (node.data).id = node.data.hProperties.id = id; } -/** - * Assign an `id` to every heading, so anchors can link to sections. - * - * Ids match what GitHub generates for the same markdown, via `github-slugger`. - * That matters because 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: - * - * ### `setupMirage` -> #setupmirage - * ### V2 JSON:API -> #v2-jsonapi - * - * A heading with an explicit `{#custom-id}` suffix is left alone. - */ export function headingId() { /** * @param {import('mdast').Root} node */ return function (node) { - // Per document, not per plugin: the slugger carries the duplicate-heading - // counter (`usage`, `usage-1`), and a compiler can be reused across - // documents. Building it here restarts numbering per document, which is - // also what GitHub does. const slugger = new GithubSlugger(); visit(node, 'heading', (node) => { @@ -49,8 +32,6 @@ export function headingId() { } } - // github-slugger must see the text exactly as GitHub renders it, - // including whitespace-only nodes: `## *a* *b*` is `a b`, not `ab`. 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 12c327215..621cfaf1b 100644 --- a/packages/repl-sdk/src/compilers/markdown/parse.test.ts +++ b/packages/repl-sdk/src/compilers/markdown/parse.test.ts @@ -239,9 +239,6 @@ describe('default features', () => { describe('headingId', () => { it('treats a literal \\s in a heading as text, not as whitespace', async () => { - // The regex in formatDefaultId used to be /\\s+/g -- a literal backslash - // followed by `s`, rather than whitespace -- so `\s` here was replaced with - // a space and the `s` vanished from the id. const result = await parseMarkdown(`## a \\s b`, { ...defaults }); expect(result.text).toContain('id="a-s-b"'); @@ -276,25 +273,18 @@ describe('headingId', () => { }); it("inserts nothing between a heading's children", async () => { - // Verified against rehype-slug, which is the github-slugger reference: - // `## Hello *there*` -> id="hello-there". Joining the children with a - // space instead would add one the markdown never had -> `hello--there`. const result = await parseMarkdown(`## Hello *there*`, { ...defaults }); expect(result.text).toContain('id="hello-there"'); }); it('keeps whitespace-only text nodes between formatted children', async () => { - // The space between the emphases is its own text node. Dropping it - // would slug `ab`; GitHub slugs `a-b`. const result = await parseMarkdown(`## *a* *b*`, { ...defaults }); expect(result.text).toContain('id="a-b"'); }); it('keeps whitespace the author actually wrote, like GitHub does', async () => { - // github-slugger does not collapse runs; rehype-slug gives - // id="hello----world" for this input, so matching it means not collapsing. const result = await parseMarkdown(`## Hello World `, { ...defaults }); expect(result.text).toContain('id="hello----world"');