import { createPlugin, windowTransport, type OutlineEntry } from '@dorfteich/plugin-sdk'; import de from '../i18n/de.json'; import en from '../i18n/en.json'; /** * Table-of-contents reference plugin (issue #77, plugin-architecture.md * §Reference plugins): renders the current page's heading outline via the * `readCurrentPage` capability; clicking an entry scrolls the host page to * the heading (`ui.scrollToHeading`). The outline is re-fetched on a slow * poll while the surface is mounted, so live heading edits appear once the * server has re-derived the content cache (persistence debounce). * * Strings are bundled (the sandbox CSP blocks runtime fetches); the files in * `i18n/` are the single source and are inlined at build time. */ const STRINGS: Record> = { de, en }; const REFRESH_MS = 5000; function labelFor(locale: string, key: string): string { const base = locale.split('-')[0] ?? locale; return STRINGS[base]?.[key] ?? STRINGS.en?.[key] ?? key; } const { host } = createPlugin({ transport: windowTransport({ // Window.postMessage's overloads don't structurally match the transport's // minimal shape; this adapter pins the sandbox-safe wildcard origin. target: { postMessage: (message) => window.parent.postMessage(message, '*') }, source: window, }), onRender: (context) => start(context.locale), onDestroy: () => stop(), }); let timer: number | null = null; let lastOutlineJson = ''; function start(locale: string): void { stop(); void refresh(locale, true); timer = window.setInterval(() => void refresh(locale, false), REFRESH_MS); } function stop(): void { if (timer !== null) window.clearInterval(timer); timer = null; } async function refresh(locale: string, force: boolean): Promise { let outline: OutlineEntry[]; try { outline = await host.readCurrentPage.getOutline(); } catch { document.body.textContent = labelFor(locale, 'error'); return; } const json = JSON.stringify(outline); if (!force && json === lastOutlineJson) return; lastOutlineJson = json; render(outline, locale); } function render(outline: OutlineEntry[], locale: string): void { document.body.textContent = ''; document.body.className = 'dt-toc'; if (outline.length === 0) { const empty = document.createElement('p'); empty.textContent = labelFor(locale, 'empty'); document.body.appendChild(empty); } else { const list = document.createElement('ul'); for (const entry of outline) { const item = document.createElement('li'); item.style.marginLeft = `${(entry.level - 1) * 1}rem`; const link = document.createElement('a'); link.href = '#'; link.textContent = entry.text; link.dataset.headingId = entry.id; link.addEventListener('click', (event) => { event.preventDefault(); void host.ui.scrollToHeading(entry.id); }); item.appendChild(link); list.appendChild(item); } document.body.appendChild(list); } void host.ui.resize(document.body.scrollHeight + 16); }