import { createPlugin, windowTransport, type RenderContext } from '@dorfteich/plugin-sdk'; import de from '../i18n/de.json'; import en from '../i18n/en.json'; import { chordProToSvg } from './chordpro'; /** * ChordPro leadsheet plugin (issue #155), following the mermaid reference * plugin's shape: the source text is the document of record, `svg` is the * last successfully rendered snapshot — persisted together so the read view, * public view, and office/PDF exports show the sheet without executing * anything (#79). Edit mode is a source textarea with a live, debounced * preview; render mode shows the rendered sheet. */ const STRINGS: Record> = { de, en }; const PREVIEW_DEBOUNCE_MS = 300; function labelFor(locale: string, key: string): string { const base = locale.split('-')[0] ?? locale; return STRINGS[base]?.[key] ?? STRINGS.en?.[key] ?? key; } interface SheetData { source?: string; svg?: string; } function dataOf(context: RenderContext): SheetData { return context.data && typeof context.data === 'object' ? (context.data as SheetData) : {}; } const { host } = createPlugin({ transport: windowTransport({ target: { postMessage: (message) => window.parent.postMessage(message, '*') }, source: window, }), onRender: (context) => renderMode(context), onEdit: (context) => editMode(context), }); function resize(): void { void host.ui.resize(Math.max(64, document.body.scrollHeight + 16)); } function renderMode(context: RenderContext): void { const data = dataOf(context); document.body.textContent = ''; document.body.className = 'dt-chordpro dt-chordpro--render'; const source = (data.source ?? '').trim(); if (source === '') { const hint = document.createElement('p'); hint.textContent = labelFor(context.locale, 'empty'); document.body.appendChild(hint); resize(); return; } try { document.body.innerHTML = chordProToSvg(source); } catch { if (data.svg) { document.body.innerHTML = data.svg; const note = document.createElement('p'); note.textContent = labelFor(context.locale, 'stale'); document.body.appendChild(note); } else { document.body.textContent = labelFor(context.locale, 'error'); } } resize(); } function editMode(context: RenderContext): void { const data = dataOf(context); document.body.textContent = ''; document.body.className = 'dt-chordpro dt-chordpro--edit'; const textarea = document.createElement('textarea'); textarea.placeholder = labelFor(context.locale, 'placeholder'); textarea.value = data.source ?? ''; textarea.rows = 10; textarea.style.width = '100%'; textarea.style.boxSizing = 'border-box'; textarea.style.fontFamily = 'monospace'; const error = document.createElement('p'); error.className = 'dt-chordpro__error'; error.style.color = '#b91c1c'; error.hidden = true; const preview = document.createElement('div'); preview.className = 'dt-chordpro__preview'; if (data.svg) preview.innerHTML = data.svg; document.body.append(textarea, error, preview); resize(); let debounce: number | undefined; let lastGoodSvg = data.svg ?? ''; const refresh = (): void => { const source = textarea.value; try { const svg = chordProToSvg(source); lastGoodSvg = svg; preview.innerHTML = svg; error.hidden = true; // Persist source + snapshot together — the snapshot carries the read // view, public view, and exports (#79). void host.blockData.setData({ source, svg }); } catch { error.textContent = labelFor(context.locale, 'error'); error.hidden = false; void host.blockData.setData({ source, svg: lastGoodSvg }); } resize(); }; textarea.addEventListener('input', () => { window.clearTimeout(debounce); debounce = window.setTimeout(refresh, PREVIEW_DEBOUNCE_MS); }); if (textarea.value.trim() !== '' && !data.svg) refresh(); }