All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 5m30s
CI / Build container images (pull_request) Successful in 10s
CI / Auth e2e pack (pull_request) Successful in 7m39s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CD / Build and push images (push) Successful in 17s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 4m46s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m1s
CI / Import/export fidelity gate (push) Successful in 54s
Release / Build release images and notes (push) Successful in 1m11s
Release / Release-candidate operations QA (push) Successful in 44s
Prod deploy / Deploy the released images to Prod (push) Successful in 15s
Neues Referenz-Plugin packages/plugins/chordpro nach dem mermaid-Muster: bewusst ohne Fremdbibliothek (Supply-Chain-Lehre aus #136) — eigener minimaler ChordPro-Parser (Direktiven title/subtitle/ artist/key/capo/tempo/comment, Chorus-Fences, [Akkord]-Marker, #-Kommentare) plus SVG-Formatter mit Monospace-Raster: Akkorde über dem Text, Titelkopf, Chorus-Einrückung, XML-escaped. Persistenz {source, svg} — der Snapshot bedient Lesemodus, Public-Ansicht und Exporte über den PluginFallbackRenderer. Edit-Modus: Textarea + debounced Live-Preview. ZIP 20 KB (Limits 64/256 MiB), 4 Parser-/Formatter-Tests, i18n de+en, Doku-Listen (site-admin en+de, plugin-architecture) ergänzt. Prod-Installation wie üblich per Site-Admin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
125 lines
3.9 KiB
TypeScript
125 lines
3.9 KiB
TypeScript
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<string, Record<string, string>> = { 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();
|
|
}
|