Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions scripts/foldKeys.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

// Fold state is keyed by `h.id || textContent`. comrak emits the
// deduplicated heading id on an empty inner <a class="anchor">, not on the
// heading element, so without promotion every fold consumer falls back to
// heading text: duplicate titles share one fold state, and the ToC's
// id-based fold keys never match the preview's text-based ones.

test('processMarkdownHtml promotes the anchor id onto the heading element', () => {
const source = readFileSync('src/lib/utils/markdown.ts', 'utf8');

const headingLoop = source.slice(source.indexOf('querySelectorAll("h1, h2, h3, h4, h5, h6")'));
assert.match(headingLoop, /querySelector\("a\.anchor"\)/, 'heading loop looks up the comrak anchor');
assert.match(headingLoop, /h\.id = \w+\.id/, 'anchor id is promoted onto the heading');
assert.match(headingLoop, /removeAttribute\("id"\)/, 'anchor id is removed so document ids stay unique');
});

test('fold restore keys by heading id before falling back to text', () => {
const source = readFileSync('src/lib/utils/markdown.ts', 'utf8');
assert.match(source, /const key = h\.id \|\| h\.textContent/, 'restore key prefers the (now populated) heading id');
});

test('viewer fold handlers key by heading id before falling back to text', () => {
const viewer = readFileSync('src/lib/MarkdownViewer.svelte', 'utf8');
assert.match(viewer, /foldableHeader\.id \|\| foldableHeader\.textContent/, 'preview chevron keys by heading id first');
assert.match(viewer, /\[id="\$\{CSS\.escape\(key\)\}"\]\.foldable-header/, 'toggleFold resolves the heading by id');
});
12 changes: 12 additions & 0 deletions src/lib/utils/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,18 @@ export function processMarkdownHtml(

const headings = Array.from(doc.querySelectorAll("h1, h2, h3, h4, h5, h6"));
for (const h of headings) {
// comrak puts the deduplicated heading id ("title", "title-1", ...)
// on an empty inner <a class="anchor">, so h.id is empty and every
// consumer that keys folds by `h.id || textContent` falls back to
// the heading text — which collides for duplicate titles and never
// matches the ToC's id-based keys. Promote the id onto the heading
// itself (and off the anchor, so the document keeps unique ids).
const headingAnchor = h.querySelector("a.anchor");
if (headingAnchor && headingAnchor.id && !h.id) {
h.id = headingAnchor.id;
headingAnchor.removeAttribute("id");
}

const chevron = doc.createElement("span");
chevron.className = "header-fold-icon";
chevron.innerHTML = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>`;
Expand Down
Loading