From 6b3e9e4fbabcb265266a55a3593f02decdd18e98 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Fri, 14 Aug 2026 16:03:57 -0700 Subject: [PATCH 1/2] fix(repl-sdk): correct the whitespace escape in formatDefaultId MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `/\\s+/g` matches a literal backslash followed by one or more `s`, not whitespace. The intent reads as collapsing runs of whitespace before kebab-casing the heading text. In practice this is a no-op: `kebabCase` already splits on whitespace, so every realistic heading slugs identically either way. The one input that differs is a heading containing a literal `\s`: "a \s b" before: a-b after: a-s-b Happy to close this in favour of dropping the `replaceAll` entirely, since `kebabCase` makes it redundant — I went with the minimal change. --- packages/repl-sdk/src/compilers/markdown/heading-id.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/repl-sdk/src/compilers/markdown/heading-id.js b/packages/repl-sdk/src/compilers/markdown/heading-id.js index 4f2647d9e..f679ba029 100644 --- a/packages/repl-sdk/src/compilers/markdown/heading-id.js +++ b/packages/repl-sdk/src/compilers/markdown/heading-id.js @@ -38,7 +38,7 @@ function extractText(children) { * @param {string} value */ function formatDefaultId(value) { - return kebabCase(value.replaceAll(/\\s+/g, ' ').trim()); + return kebabCase(value.replaceAll(/\s+/g, ' ').trim()); } /** From 4e57935810e2f9aa3ca89d77915aa89304d57c83 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Fri, 14 Aug 2026 16:44:11 -0700 Subject: [PATCH 2/2] test(repl-sdk): cover the heading-id whitespace handling Two cases for the escape fix: - runs of whitespace in a heading collapse to a single `-` in the id (passes either way, since kebabCase also splits on whitespace -- pins the intent so a future refactor of formatDefaultId cannot quietly change it) - a literal `\s` in a heading is text, not whitespace The second is the actual regression test. With the old `/\\s+/g`, `## a \s b` produced `id="a-b"` -- the `s` was consumed as part of the match. --- .../src/compilers/markdown/parse.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/repl-sdk/src/compilers/markdown/parse.test.ts b/packages/repl-sdk/src/compilers/markdown/parse.test.ts index 213679e87..ae952b93a 100644 --- a/packages/repl-sdk/src/compilers/markdown/parse.test.ts +++ b/packages/repl-sdk/src/compilers/markdown/parse.test.ts @@ -237,6 +237,24 @@ 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 + // 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"'); + }); +}); + describe('options', () => { describe('remarkPlugins', () => { it('works', async () => {