Claude 06574c5cfd
feat(ai-act-kompass): anti-AI print design and company personalization
- 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
2026-07-04 09:18:07 +00:00

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('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;');
}
/** 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');
}