import './style.css'; import { parse, setFoldMark, expandShortIds, shortIdClosed } from './parser.js'; import { computeCheapPlan, overloadedAssignee, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, presetFoldSet, lineTargets } from './model.js'; import { esc, renderTreeHtml, TIP_RULE } from './render.js'; import { formatWarning, warningText } from './warnings.js'; import * as live from './live.js'; import { depFragment, collectIds, matchIds, depIdAt, idLine } from './autocomplete.js'; import { LS_SNAPS, SNAP_EVERY, parseSnaps, addSnapshot, persistSnaps, snapLabel } from './snapshots.js'; import { FILE_ACCEPT, FILE_TYPES, saveFileName } from './localfile.js'; import { LIVE_PARAM, SOURCE_PARAM, ETHERPAD_PARAM, docSearch, docKind } from './docurl.js'; import { readDocs, storeDocs, storeDocText, isDocKey, LS_DOCS, LS_ACTIVE, LS_SRC } from './docstore.js'; /* Neuigkeiten (D58): die git-Historie, zur BAUZEIT eingelesen (Vite-Plugin in vite.config.js). Zur Laufzeit gibt es kein git — und keinen Server, der nachliefern könnte (D11/D19). Leer, wo git nicht erreichbar war. */ import NEWS from 'virtual:werkbaum-news'; /* Werkbaum, mit Werkbaum geplant — als mitgeliefertes Dokument „Werkbank" (D27). Dieselbe Datei, die auch per ?sourceUrl= geladen werden kann; `?raw` bettet sie beim Build in die eine Ausgabedatei ein (D19), es wird nichts nachgeladen (D20). Quelle bleibt docs/examples/ — keine Kopie, die auseinanderläuft. */ import WERKBAUM_DOC from '../../docs/examples/werkbaum.werkbaum?raw'; const INITIAL = `%% Project structure – Sprint 14 [~] Website relaunch (XL) https://wiki.example.com/relaunch " Folded chapters are done — click the ▸ to peek inside. - [x] > Concept (M) - [x] Audience analysis (S) - [x] Sitemap (XS) - [~] Implementation (XL) - [~] Frontend (M) https://git.example.com/frontend @anna | [ ] PWA (S) | [ ] Web+Native - [/] Web (S) - [ ] Android (M) - [ ] iOS (M) - [!] Backend (L) @ben @carla - [ ] #cms: CMS integration (M) | [ ] WordPress | [?] Headless CMS | [-] Custom build %% too much effort - [x] Landing page (S) :#cms %% done, but effectively waiting for the CMS + [?] Dark mode (S) %% nice to have, never on the cheapest path - [?] Hosting (M) %% exactly one of these, hence = = [ ] Cooperative Community Cloud https://hostsharing.net = [?] On-premise --- #cms The articles live in the CMS, so everything that shows content depends on it — the dotted arrows point at what is needed.`; const src = document.getElementById('src'); const out = document.getElementById('out'); const warnBox = document.getElementById('warn'); /* Zeilennummern-Streifen (D33) — hier oben geholt, weil render() ihn füllt. */ const srcWrap = document.getElementById('srcWrap'); const lineNoBox = document.getElementById('lineNos'); const lineNoInner = lineNoBox.firstElementChild; /* Baum-/Kostenlogik (gateOf, needsBreakdown, visibleChildren, günstigster Pfad) lebt headless in model.js, das HTML-Erzeugen in render.js. Hier bleibt nur der UI-State des Günstigster-Pfad-Toggles (persistiert). */ let cheapPathOn = true; let depLinksOn = true; /* Querverbindungen der Abhängigkeiten anzeigen (D75) */ let showIds = false; /* Knoten-IDs im Diagramm einblenden (D56) */ /* Falt-Durchschalter (SPEC §9/D75): vier Voreinstellungen, reihum. `Next` ist der Schritt, den der nächste Druck ausführt (Icon + Tooltip zeigen IHN), `Applied` der zuletzt hergestellte — render() prüft ihn gegen den Baum und setzt die Position zurück, sobald er nicht mehr stimmt. Nicht persistiert: Der Faltzustand steht im Text (D38), die Position ist nur der Zeiger darin. */ const FOLD_CYCLE = ['small', 'path', 'closed', 'open']; let foldCycleNext = 0, foldCycleApplied = -1; /* Sichtbarer Bereich auf kleinem Bildschirm (D17-Nachtrag): dort ist immer genau einer zu sehen. Hier oben deklariert, weil saveUI() ihn liest und schon aus applySplit() heraus laufen kann — weiter unten stünde er dann noch in der temporalen Todeszone. */ let mobilePane = 'diagram'; /* Cursor-Zeile (D25) — bleibt `null`, bis der Cursor das erste Mal bewegt wurde, sonst wäre nach dem Laden ungefragt die Wurzel markiert. Aus demselben Grund hier oben: der Zeilennummern-Streifen (D33) liest sie, und der hängt an render(). */ let caretLine = null, currentNodeEl = null; /* Warnung des ?sourceUrl-Ladens (D23) — zeilenlos und persistent, siehe render(). */ let sourceWarning = null; /* Scheitert das Speichern im Browser (Quota voll), wird das GEMELDET statt geschluckt (D82) — vorher lief der Editor scheinbar normal weiter, und der Verlust fiel erst beim Neuladen auf. Persistent, bis ein Schreiben wieder gelingt; zeilenlos und zuoberst. */ let storeWarning = null; /* Ein ANDERER Tab schreibt in dieselbe Dokument-Ablage (D84): Das storage-Ereignis feuert nur in fremden Tabs desselben Ursprungs — genau das richtige Signal. Kein Sync (der letzte Flush gewinnt weiterhin, wie immer), aber kein STILLER Verlust mehr: Die Warnung bleibt stehen, solange dieser Tab lebt — die Lage ändert sich ja nicht dadurch, dass der andere Tab gerade nichts schreibt. */ let tabWarning = null; window.addEventListener('storage', e => { if(tabWarning || !e.key || !isDocKey(e.key)) return; tabWarning = {type: 'tabConflict'}; render(); }); function noteStore(ok){ const neu = ok ? null : {type: 'storeFailed'}; const wechsel = (!!storeWarning) !== (!!neu); storeWarning = neu; if(wechsel) render(); /* nur an der Flanke — nicht bei jedem Tastendruck */ } /* Steht die App? Erst danach folgt die Adresszeile dem aktiven Dokument (D80) und wird eine Live-Sitzung beim Umschalten übernommen. Während des Starts dürfen beide nicht laufen: `loadRemoteSource()`/`loadLive()` lesen ihre Parameter erst nach dem Wiederherstellen des zuletzt aktiven Dokuments — ein vorschnelles Aufräumen der Adresszeile nähme ihnen die Vorlage. Hier oben deklariert, weil `loadActiveIntoEditor()` schon beim Start läuft. */ let bootDone = false; /* „Was ist neu?" (D28): Knoten, die gegenüber der zuletzt GESEHENEN Fassung neu in Produktion sind. Gilt immer für genau ein Dokument (`freshDocId`) — nur Dokumente von außen (mitgeliefert oder ?sourceUrl=) haben eine Vergleichsfassung. `freshBaseline` ist der Text, gegen den verglichen wurde; er wird erst beim Bestätigen fortgeschrieben. */ let freshSet = new Set(), freshDocId = null, freshBaseline = null, freshPrevRoots = null; /* Führt das Neuigkeiten-Popup gerade einen Tag vor (D58)? Dann tritt dessen Knotenmenge an die Stelle des Besuchsvergleichs — `newsDay` ist das Datum (auch die Marke im Popup), `newsKeySet` die Label-Pfade. Sitzungssache: Der Faltzustand steht im Text (D38), der Besuchsstand im localStorage — eine vorgeführte Chronik ist weder das eine noch das andere. */ let newsDay = null, newsKeySet = null; /* Faltung (SPEC §9, D38): `foldOverrides` sind die interaktiven Eingriffe des Nutzers (Schlüssel = Label-Pfad wie bei D28, damit sie das Neu-Parsen bei jedem Tastendruck überleben); sie überlagern den Anfangszustand aus den Textmarken, gelten nur für die Sitzung und fallen beim Dokumentwechsel weg. `foldByLine` ist der Zustand des letzten Renders für Klick/Tastatur. */ let foldOverrides = new Map(), foldByLine = new Map(); /* Zeile -> Zeile des sichtbaren Vertreters (D38-Nachtrag 4): Für Zeilen in eingeklappten Teilbäumen zeigt sie auf den nächsten sichtbaren Vorfahren; `nodeOfLine()` greift darauf zurück, wenn die Zeile keinen DOM-Knoten hat. */ let lineTargetMap = new Map(); /* Der zuletzt geparste Baum, UNGEFILTERT — die ID-Vorschläge (D63) lesen daraus die vergebenen IDs, und eine Abhängigkeit darf auch auf Verworfenes zeigen. */ let acRoots = []; /* ---------- Renderer (Anbindung an den DOM) ---------- parse -> Wurzeln filtern (verworfene) -> günstigen Pfad markieren -> render.js baut den HTML-String -> in #out schreiben -> Pfadlinie zeichnen. */ function render(){ /* Das Knoten-Fenster (D52) hängt an einem Element, das der Neubau ersetzt — und seine Position ist ohnehin gemessen, also gleich hinfällig. */ closeNodeTip(); const parsed = parse(src.value); acRoots = parsed.roots; let roots = parsed.roots; const showDiscarded = discardedShown(); if(!showDiscarded){ roots = roots.filter(r => !r.status || r.status.key !== 'verworfen'); } /* Warnungen aus Parser (unbekannte Statuszeichen) + Renderer (gemischte Gates) zusammenführen, nach Zeile sortiert anzeigen. `sourceWarning` (?sourceUrl nicht ladbar, D23) gehört keiner Zeile und bleibt über Neu-Renderings bestehen, bis das Laden gelingt. */ let warnings = sourceWarning ? [sourceWarning].concat(parsed.warnings) : parsed.warnings; if(tabWarning) warnings = [tabWarning].concat(warnings); /* fremder Tab schreibt mit (D84) */ if(storeWarning) warnings = [storeWarning].concat(warnings); /* Datenverlust droht — zuoberst (D82) */ if(!roots.length){ out.innerHTML = `