frontend: Parser nach src/parser.js auslagern (headless)
TASKS Phase 1. `parse`, `STATUS_BY_CODE` und `SIZE_RANK` wandern unverändert in ein eigenes, DOM-freies Modul (SPEC §1–§8 normativ); app.js importiert sie. Vorbereitung für die Parser-Unit-Tests (nächste Checkbox) und den SVG-/Mermaid- Renderer, die den Parser headless nutzen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
78d1000f61
commit
80d05a33a2
+3
-1
@@ -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.
|
||||
|
||||
+1
-45
@@ -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,'<').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';
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
Reference in New Issue
Block a user