frontend: Renderer + Modell headless auslagern (src/model.js, src/render.js)
TASKS Phase 1. Baum-/Kostenlogik -> model.js (gateOf, needsBreakdown, visibleChildren, computeCheapSet, cheapCls), HTML-Erzeugung -> render.js (renderTreeHtml). Beide sind headless: UI-State (verworfene einblenden, günstigster Pfad) kommt als Parameter herein statt aus Globals; nur cheapPathOn bleibt UI-State in app.js. render() in app.js ist jetzt die dünne DOM-Anbindung. Snapshot-Tests (tests/render.test.js): Grundzustand, günstigster Pfad, verworfene einblenden, Geister-Knoten („Untergliederung fehlt"), plus die Zusicherung, dass der Renderer nie eine Modus-Klasse erzeugt (Modus = CSS-Container-Klasse in app.js -> ein Snapshot deckt alle drei Modi). 24 Tests grün. Byte-identisches Rendering im Browser verifiziert (18 Knoten, 6 Blätter, 3 Geister, Toggles ok, keine Konsolenfehler). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7c8f216589
commit
5905712aa5
@@ -0,0 +1,77 @@
|
||||
/* Werkbaum-Renderer — erzeugt den HTML-String des Diagrammbaums (die <li>-Liste
|
||||
für #out). Headless: keine DOM-Zugriffe, kein globaler UI-State. Alles kommt
|
||||
über `opts` herein. Der Darstellungsmodus (horizontal/vertikal/kompakt) ist
|
||||
rein CSS (Klasse am Container, von app.js gesetzt) und ändert diesen String
|
||||
NICHT. Vgl. docs/SPEC.md §4–§9, D18.
|
||||
|
||||
opts = {
|
||||
t, // i18n-Funktion (key, vars?) -> String
|
||||
showDiscarded, // verworfene einblenden?
|
||||
cheapPath, // günstigster Pfad aktiv? (steuert das implizite M-Badge)
|
||||
cheapSet, // Set der nötigen Knoten (leer, wenn Pfad aus)
|
||||
} */
|
||||
|
||||
import { gateOf, needsBreakdown, visibleChildren, cheapCls } from './model.js';
|
||||
|
||||
export function esc(s){
|
||||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
function nodeHtml(n, extra, opts){
|
||||
const { t, cheapPath } = opts;
|
||||
const need = needsBreakdown(n);
|
||||
const cls = ['node', extra || '', n.status ? 'st-' + n.status.key : '']
|
||||
.filter(Boolean).join(' ');
|
||||
const title = n.status ? ` title="${esc(t('st_' + n.status.key)).replace(/"/g,'"')}"` : '';
|
||||
const tagsHtml = n.tags && n.tags.length
|
||||
? `<span class="tags">${n.tags.map(tag => `<span class="tag">${esc(tag)}</span>`).join('')}</span>`
|
||||
: '';
|
||||
const implicitTip = esc(t('implicitSizeTooltip')).replace(/"/g,'"');
|
||||
const sizeBadge = n.size
|
||||
? `<span class="size">${n.size}</span>`
|
||||
: (cheapPath ? `<span class="size implicit" title="${implicitTip}">M</span>` : '');
|
||||
const inner = esc(n.label) +
|
||||
(n.url ? '<span class="ext">↗</span>' : '') +
|
||||
sizeBadge +
|
||||
tagsHtml;
|
||||
const html = n.url
|
||||
? `<a class="${cls}" href="${esc(n.url).replace(/"/g,'"')}" target="_blank" rel="noopener"${title}>${inner}</a>`
|
||||
: `<div class="${cls}"${title}>${inner}</div>`;
|
||||
const ghostTip = esc(t('ghostTooltip')).replace(/"/g,'"');
|
||||
const ghost = `<div class="ghost-node" title="${ghostTip}">${esc(t('ghost'))}</div>`;
|
||||
return html + (need ? ghost : '');
|
||||
}
|
||||
|
||||
function renderChildren(node, warnings, opts){
|
||||
const kids = visibleChildren(node, opts.showDiscarded);
|
||||
if(!kids.length) return '';
|
||||
const types = new Set(kids.map(k => k.type));
|
||||
if(types.size > 1){
|
||||
warnings.push(opts.t('mixedWarn', {line: kids[0].line, label: esc(node.label)}));
|
||||
}
|
||||
const gate = gateOf(kids);
|
||||
const items = kids.map(k => {
|
||||
const vk = visibleChildren(k, opts.showDiscarded);
|
||||
const liCls = vk.length ? (gateOf(vk) === 'or' ? ' class="has-or"' : ' class="has-and"') : '';
|
||||
return `<li${liCls}>` +
|
||||
nodeHtml(k, cheapCls(k, opts.cheapSet), opts) +
|
||||
renderChildren(k, warnings, opts) +
|
||||
`</li>`;
|
||||
}).join('');
|
||||
return `<ul class="${gate}">${items}</ul>`;
|
||||
}
|
||||
|
||||
/* Baut den inneren HTML-String für #out aus (bereits gefilterten) Wurzeln und
|
||||
sammelt Warnungen (gemischte Gates). Leere Wurzelliste ⇒ leerer String. */
|
||||
export function renderTreeHtml(roots, opts){
|
||||
const warnings = [];
|
||||
const html = roots.map(root => {
|
||||
const vk = visibleChildren(root, opts.showDiscarded);
|
||||
const liCls = vk.length ? (gateOf(vk) === 'or' ? ' class="has-or"' : ' class="has-and"') : '';
|
||||
return `<li${liCls}>` +
|
||||
nodeHtml(root, ('root-node ' + cheapCls(root, opts.cheapSet)).trim(), opts) +
|
||||
renderChildren(root, warnings, opts) +
|
||||
`</li>`;
|
||||
}).join('');
|
||||
return { html, warnings };
|
||||
}
|
||||
Reference in New Issue
Block a user