feat: Größe ist das letzte alleinstehende (L)-Token der Zeile (D68)
Erkannt nur alleinstehend angesetzt (wie #id und :#…) — damit greifen die Zitier-Konventionen "(L)" und ((L)) von selbst, und Backend(L) bleibt Label. Das letzte Token gewinnt statt des ersten: Die Größe steht hinter dem Titel, frühere Vorkommen sind Text. SPEC §1/§5 und llms.md nachgezogen; mitgelieferte Beispiele unverändert (Parse-Vergleich alt/neu). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
9999c7973e
commit
59da485af6
@@ -105,7 +105,10 @@ One node per line. Everything except the label is optional.
|
||||
|
||||
### Size (effort)
|
||||
|
||||
- T-shirt sizes in parentheses: `(XS) (S) (M) (L) (XL) (XXL)`.
|
||||
- T-shirt sizes in parentheses: `(XS) (S) (M) (L) (XL) (XXL)`. Recognized
|
||||
only **free-standing** (preceded by start-of-line or whitespace), and the
|
||||
**last** such token of the line is the size — earlier ones stay in the
|
||||
label. `"(L)"` and `((L))` mention a size literally (quoting convention).
|
||||
- From `(M)` upward a node **should be decomposed further**; a node ≥ M
|
||||
without children gets a placeholder hint in the diagram.
|
||||
- For cost estimation a missing size is **estimated from the sub-packages**:
|
||||
@@ -134,7 +137,8 @@ One node per line. Everything except the label is optional.
|
||||
(whole line or trailing part). This also applies inside descriptions.
|
||||
2. Bare `https?://…` URL — makes the node clickable; extracted early so `@`,
|
||||
`#` and `!!!` inside URLs never trigger.
|
||||
3. `(SIZE)` — first match, case-insensitive.
|
||||
3. `(SIZE)` — the **last free-standing** match, case-insensitive; earlier
|
||||
size-like tokens stay in the label.
|
||||
4. `@name` — people tags; several per line, any position. Characters:
|
||||
Unicode letters, digits, `.`, `_`, `-`.
|
||||
5. `#id` — **node ID**: the *first* free-standing `#token` of the line
|
||||
@@ -214,7 +218,7 @@ One node per line. Everything except the label is optional.
|
||||
- Keep rejected alternatives as `[-]` instead of deleting them: the decision
|
||||
stays visible (add the reason as a `%%` comment).
|
||||
- To mention syntax literally in a label without triggering it, wrap it in
|
||||
parentheses or quotes: `(#id)`, `(:#a,#b)`, `"#123"`.
|
||||
parentheses or quotes: `(#id)`, `(:#a,#b)`, `"#123"`, `"(L)"`, `((L))`.
|
||||
|
||||
## Complete example
|
||||
|
||||
|
||||
+15
-1
@@ -286,7 +286,21 @@ export function parse(text){
|
||||
let rest = m[6], 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 ''; });
|
||||
/* Größe (SPEC §1 Schritt 4, D68): das LETZTE alleinstehend angesetzte
|
||||
`(L)`-Token der Zeile — die Größe steht nach der üblichen Schreibweise
|
||||
hinter dem Titel, frühere Vorkommen sind Text und bleiben im Label.
|
||||
Alleinstehend wie bei `#id` und `:#…`: `Backend(L)` bleibt Label, und
|
||||
die Zitier-Konventionen `"(L)"` und `((L))` gelten damit von selbst. */
|
||||
{
|
||||
const reSize = /(^|\s)\((XXL|XS|XL|S|M|L)\)/gi;
|
||||
let sm, lastSize = null;
|
||||
while((sm = reSize.exec(rest))) lastSize = sm;
|
||||
if(lastSize){
|
||||
size = lastSize[2].toUpperCase();
|
||||
rest = rest.slice(0, lastSize.index) + lastSize[1]
|
||||
+ rest.slice(lastSize.index + lastSize[0].length);
|
||||
}
|
||||
}
|
||||
rest = rest.replace(/@([\p{L}\p{N}._-]+)/gu, (s, g) => { tags.push(g); return ''; });
|
||||
/* Knoten-ID `#name` (SPEC §1, D36): nur ALLEINSTEHEND ANGESETZT — „C#"
|
||||
bleibt Label, und das reservierte `:#a,#b` (§11) wird nicht gefressen.
|
||||
|
||||
@@ -128,6 +128,35 @@ describe('parse — Randfälle', () => {
|
||||
expect(roots[1].size).toBe('M');
|
||||
});
|
||||
|
||||
/* Größen-Token (SPEC §1 Schritt 4, D68): das LETZTE alleinstehend
|
||||
angesetzte `(L)`-Token ist die Größe; frühere bleiben im Label, und die
|
||||
Zitier-Konventionen `"(L)"`/`((L))` halten ein Literal heraus. */
|
||||
it('das letzte alleinstehende Größen-Token gewinnt, frühere bleiben im Label', () => {
|
||||
const { roots } = parse('- Variante (L) bauen (M)');
|
||||
expect(roots[0].size).toBe('M');
|
||||
expect(roots[0].label).toBe('Variante (L) bauen');
|
||||
});
|
||||
|
||||
it('Größe nur alleinstehend angesetzt: Backend(L) bleibt Label', () => {
|
||||
const { roots } = parse('- Backend(L)');
|
||||
expect(roots[0].size).toBe(null);
|
||||
expect(roots[0].label).toBe('Backend(L)');
|
||||
});
|
||||
|
||||
it('Zitier-Konventionen: "(L)" und ((L)) bleiben Label', () => {
|
||||
const { roots } = parse('- Merch "(L)" drucken\n- Auswahl ((L)) treffen (S)');
|
||||
expect(roots[0].size).toBe(null);
|
||||
expect(roots[0].label).toBe('Merch "(L)" drucken');
|
||||
expect(roots[1].size).toBe('S');
|
||||
expect(roots[1].label).toBe('Auswahl ((L)) treffen');
|
||||
});
|
||||
|
||||
it('Größe am Zeilenanfang des Rests zählt weiterhin', () => {
|
||||
const { roots } = parse('- (L) Backend');
|
||||
expect(roots[0].size).toBe('L');
|
||||
expect(roots[0].label).toBe('Backend');
|
||||
});
|
||||
|
||||
it('ohne Angaben: status/url/size null, tags leer', () => {
|
||||
const { roots } = parse('- Nackt');
|
||||
expect(roots[0]).toMatchObject({ status: null, url: null, size: null, tags: [] });
|
||||
|
||||
Reference in New Issue
Block a user