diff --git a/docs/TASKS.md b/docs/TASKS.md index 2438c84..8f1e548 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -10,7 +10,9 @@ Abhaken beim Erledigen; neue Aufgaben unten anfügen. einer self-contained `dist/index.html` (file://-tauglich). Gerüst steht (aktuell `src/app.js` + `src/style.css`); Parser/Renderer werden in den folgenden Checkboxen herausgelöst. -- [ ] Parser extrahieren; Verhalten exakt wie in `docs/SPEC.md` §1–§8. +- [x] Parser extrahieren; Verhalten exakt wie in `docs/SPEC.md` §1–§8. + → `src/parser.js` exportiert `parse`, `STATUS_BY_CODE`, `SIZE_RANK` + (headless, kein DOM); `app.js` importiert sie. - [ ] Unit-Tests für den Parser (Vitest): kanonisches Beispiel aus SPEC §10 als Fixture; Randfälle: gemischte Gates, Tabs/ungleichmäßige Einrückung, URL mit `@`, mehrere Wurzeln, leere Labels, `%%` am Zeilenanfang/-ende. diff --git a/frontend/src/app.js b/frontend/src/app.js index c8b5982..4f1b7bf 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -1,4 +1,5 @@ import './style.css'; +import { SIZE_RANK, STATUS_BY_CODE, parse } from './parser.js'; const INITIAL = `%% Project structure – Sprint 14 [~] Website relaunch (XL) https://wiki.example.com/relaunch @@ -21,18 +22,6 @@ const INITIAL = `%% Project structure – Sprint 14 | Cooperative Community Cloud https://hostsharing.net | On-premise`; -const SIZE_RANK = {XS:0, S:1, M:2, L:3, XL:4, XXL:5}; - -const STATUS_BY_CODE = { - '?': {key:'idee', name:'Idee'}, - ' ': {key:'geplant', name:'geplant'}, - '~': {key:'arbeit', name:'in Arbeit'}, - '/': {key:'durchstich', name:'Durchstich – funktionsbereit, Feinarbeiten offen'}, - 'x': {key:'fertig', name:'fertig'}, - '^': {key:'prod', name:'in Produktion'}, - '-': {key:'verworfen', name:'verworfen'} -}; - const src = document.getElementById('src'); const out = document.getElementById('out'); const warnBox = document.getElementById('warn'); @@ -41,39 +30,6 @@ function esc(s){ return s.replace(/&/g,'&').replace(//g,'>'); } -/* ---------- Parser ---------- */ -function parse(text){ - const virtualRoot = {label:'', type:'and', children:[]}; - const stack = [{node:virtualRoot, width:-1}]; - const warnings = []; - - text.split('\n').forEach((raw, i) => { - raw = raw.replace(/%%.*$/, ''); /* %%-Kommentare entfernen (Mermaid-Konvention) */ - if(!raw.trim()) return; - const m = raw.match(/^([ \t]*)([-|])?\s*(?:\[([ ?~xX^\/-])]\s*)?(.*)$/); - const width = m[1].replace(/\t/g,' ').length; - const type = m[2] === '|' ? 'or' : 'and'; - const status = m[3] ? STATUS_BY_CODE[m[3].toLowerCase()] : null; - - let rest = m[4], url = null, size = null; - const tags = []; - rest = rest.replace(/https?:\/\/\S+/i, s => { url = s; return ''; }); - rest = rest.replace(/\((XXL|XS|XL|S|M|L)\)/i, (s, g) => { size = g.toUpperCase(); return ''; }); - rest = rest.replace(/@([\p{L}\p{N}._-]+)/gu, (s, g) => { tags.push(g); return ''; }); - const label = rest.replace(/\s+/g, ' ').trim(); - if(!label) return; - - while(stack.length > 1 && stack[stack.length-1].width >= width) stack.pop(); - const parent = stack[stack.length-1].node; - - const node = {label, type, status, url, size, tags, children:[], line:i+1}; - parent.children.push(node); - stack.push({node, width}); - }); - - return {roots: virtualRoot.children, warnings}; -} - /* ---------- Renderer ---------- */ function gateOf(children){ return children.length && children[0].type === 'or' ? 'or' : 'and'; diff --git a/frontend/src/parser.js b/frontend/src/parser.js new file mode 100644 index 0000000..4f3b8c9 --- /dev/null +++ b/frontend/src/parser.js @@ -0,0 +1,56 @@ +/* Werkbaum-Parser — headless, ohne DOM/UI nutzbar. + Setzt docs/SPEC.md §1–§8 um: Zeilenformat, Hierarchie (Einrückung), + Zerlegungsart (Gate), Status, Größe, Links, Personen-Tags, Kommentare. + Verhalten ist normativ gegen SPEC — Änderungen zuerst dort dokumentieren. */ + +/* T-Shirt-Größen (SPEC §5), aufsteigend geordnet. */ +export const SIZE_RANK = { XS: 0, S: 1, M: 2, L: 3, XL: 4, XXL: 5 }; + +/* Status-Vokabular (SPEC §4): Checkbox-Code -> {key, name}. + `name` ist der deutsche Anzeigename (Quellsprache). */ +export const STATUS_BY_CODE = { + '?': {key:'idee', name:'Idee'}, + ' ': {key:'geplant', name:'geplant'}, + '~': {key:'arbeit', name:'in Arbeit'}, + '/': {key:'durchstich', name:'Durchstich – funktionsbereit, Feinarbeiten offen'}, + 'x': {key:'fertig', name:'fertig'}, + '^': {key:'prod', name:'in Produktion'}, + '-': {key:'verworfen', name:'verworfen'} +}; + +/* Parst den Notationstext zu { roots, warnings }. + Jeder Knoten: {label, type:'and'|'or', status, url, size, tags, children, line}. + Extraktionsreihenfolge (SPEC §1): Kommentar -> Zeichen/Status -> URL -> Größe + -> Tags -> Label. Hierarchie über Einrückungsbreite (Tab = 2 Leerzeichen); + Elternknoten ist die nächste vorangehende Zeile mit kleinerer Breite. */ +export function parse(text){ + const virtualRoot = {label:'', type:'and', children:[]}; + const stack = [{node:virtualRoot, width:-1}]; + const warnings = []; + + text.split('\n').forEach((raw, i) => { + raw = raw.replace(/%%.*$/, ''); /* %%-Kommentare entfernen (Mermaid-Konvention) */ + if(!raw.trim()) return; + const m = raw.match(/^([ \t]*)([-|])?\s*(?:\[([ ?~xX^\/-])]\s*)?(.*)$/); + const width = m[1].replace(/\t/g,' ').length; + const type = m[2] === '|' ? 'or' : 'and'; + const status = m[3] ? STATUS_BY_CODE[m[3].toLowerCase()] : null; + + let rest = m[4], url = null, size = null; + const tags = []; + rest = rest.replace(/https?:\/\/\S+/i, s => { url = s; return ''; }); + rest = rest.replace(/\((XXL|XS|XL|S|M|L)\)/i, (s, g) => { size = g.toUpperCase(); return ''; }); + rest = rest.replace(/@([\p{L}\p{N}._-]+)/gu, (s, g) => { tags.push(g); return ''; }); + const label = rest.replace(/\s+/g, ' ').trim(); + if(!label) return; + + while(stack.length > 1 && stack[stack.length-1].width >= width) stack.pop(); + const parent = stack[stack.length-1].node; + + const node = {label, type, status, url, size, tags, children:[], line:i+1}; + parent.children.push(node); + stack.push({node, width}); + }); + + return {roots: virtualRoot.children, warnings}; +}