- Full redesign: paper tone, serif typography, hairlines, rectangular stamp badges, mono labels — technical print look instead of SaaS style, no gradients/shadows/emoji - Company profile settings (name, address, contact person, email, phone, website, logo upload as local data URL) persisted in localStorage and included in JSON backup (schema v2, v1 imports still supported) - Print/PDF output via letterhead print view (window.print): logo, company name and contact data in header, contact line in footer; applies to risk report, action plan, Annex IV skeleton and inventory - Company block also embedded in generated Markdown headers - Minimal auditable Markdown renderer (no external parser), HTML-escaped - 8 new tests (32 total) Claude-Session: https://claude.ai/code/session_017YjohVNx1ZGNM4MX7tHpfv
126 lines
3.5 KiB
TypeScript
126 lines
3.5 KiB
TypeScript
/**
|
|
* Minimaler Markdown-Renderer für die intern erzeugten Dokumente.
|
|
*
|
|
* Unterstützt genau die Konstrukte, die der Dokumenten-Generator ausgibt:
|
|
* Überschriften (#/##/###), Tabellen, Listen, Blockquotes, Absätze sowie
|
|
* **fett** und _kursiv_. Kein externer Parser — bewusst klein und auditierbar.
|
|
*/
|
|
|
|
/** Maskiert HTML-Sonderzeichen. */
|
|
function escapeHtml(text: string): string {
|
|
return text
|
|
.replaceAll('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"');
|
|
}
|
|
|
|
/** Wandelt Inline-Markdown (**fett**, _kursiv_) in HTML um. */
|
|
function inline(text: string): string {
|
|
return escapeHtml(text)
|
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/_(.+?)_/g, '<em>$1</em>');
|
|
}
|
|
|
|
function isTableRow(line: string): boolean {
|
|
return line.trimStart().startsWith('|');
|
|
}
|
|
|
|
function isSeparatorRow(line: string): boolean {
|
|
return /^\s*\|[\s\-|:]+\|\s*$/.test(line);
|
|
}
|
|
|
|
function splitCells(line: string): readonly string[] {
|
|
const trimmed = line.trim().replace(/^\|/, '').replace(/\|$/, '');
|
|
return trimmed.split('|').map((c) => c.trim());
|
|
}
|
|
|
|
function renderTable(rows: readonly string[]): string {
|
|
const [first, second] = rows;
|
|
const hasHeader = first !== undefined && second !== undefined && isSeparatorRow(second);
|
|
const bodyRows = hasHeader ? rows.slice(2) : rows.filter((r) => !isSeparatorRow(r));
|
|
const parts: string[] = ['<table>'];
|
|
if (hasHeader) {
|
|
const headerCells = splitCells(first)
|
|
.map((c) => `<th>${inline(c)}</th>`)
|
|
.join('');
|
|
parts.push(`<thead><tr>${headerCells}</tr></thead>`);
|
|
}
|
|
parts.push('<tbody>');
|
|
for (const row of bodyRows) {
|
|
const cells = splitCells(row)
|
|
.map((c) => `<td>${inline(c)}</td>`)
|
|
.join('');
|
|
parts.push(`<tr>${cells}</tr>`);
|
|
}
|
|
parts.push('</tbody></table>');
|
|
return parts.join('');
|
|
}
|
|
|
|
/**
|
|
* Rendert die intern erzeugten Markdown-Dokumente als HTML.
|
|
*
|
|
* @param markdown - Vom Dokumenten-Generator erzeugtes Markdown
|
|
* @returns HTML-Fragment (ohne umgebendes Dokument)
|
|
*/
|
|
export function renderMarkdown(markdown: string): string {
|
|
const lines = markdown.split('\n');
|
|
const out: string[] = [];
|
|
let i = 0;
|
|
|
|
while (i < lines.length) {
|
|
const line = lines[i] ?? '';
|
|
|
|
if (line.trim() === '') {
|
|
i += 1;
|
|
continue;
|
|
}
|
|
if (line.startsWith('### ')) {
|
|
out.push(`<h3>${inline(line.slice(4))}</h3>`);
|
|
i += 1;
|
|
continue;
|
|
}
|
|
if (line.startsWith('## ')) {
|
|
out.push(`<h2>${inline(line.slice(3))}</h2>`);
|
|
i += 1;
|
|
continue;
|
|
}
|
|
if (line.startsWith('# ')) {
|
|
out.push(`<h1>${inline(line.slice(2))}</h1>`);
|
|
i += 1;
|
|
continue;
|
|
}
|
|
if (line.startsWith('> ')) {
|
|
const quote: string[] = [];
|
|
while (i < lines.length && (lines[i] ?? '').startsWith('> ')) {
|
|
quote.push(inline((lines[i] ?? '').slice(2)));
|
|
i += 1;
|
|
}
|
|
out.push(`<blockquote>${quote.join('<br>')}</blockquote>`);
|
|
continue;
|
|
}
|
|
if (line.startsWith('- ')) {
|
|
const items: string[] = [];
|
|
while (i < lines.length && (lines[i] ?? '').startsWith('- ')) {
|
|
items.push(`<li>${inline((lines[i] ?? '').slice(2))}</li>`);
|
|
i += 1;
|
|
}
|
|
out.push(`<ul>${items.join('')}</ul>`);
|
|
continue;
|
|
}
|
|
if (isTableRow(line)) {
|
|
const rows: string[] = [];
|
|
while (i < lines.length && isTableRow(lines[i] ?? '')) {
|
|
rows.push(lines[i] ?? '');
|
|
i += 1;
|
|
}
|
|
out.push(renderTable(rows));
|
|
continue;
|
|
}
|
|
out.push(`<p>${inline(line)}</p>`);
|
|
i += 1;
|
|
}
|
|
|
|
return out.join('\n');
|
|
}
|