notation: \ am Zeilenende setzt die Zeile fort (SPEC §1, D59)
Eine Zeile trägt alles auf einmal — im mitgelieferten Plan ist die längste 122 Zeichen lang, und seit das Textfeld nicht mehr umbricht (D49) muss man dafür schieben. Ein `\` am Zeilenende verteilt sie jetzt auf mehrere Textzeilen, ohne dass ein neuer Knoten entsteht. Leerraum davor ist Pflicht (`… \`, nicht `…\`) — bewusst gegen die Shell-Gewohnheit: Ohne die Regel verschluckt ein Label wie `C:\temp\` stumm den folgenden Knoten, mit ihr bekommt man nur keine Fortsetzung, was man sofort sieht. Der laute Fehler ist der bessere. Umgesetzt als Vor-Durchlauf `logicalLines()`: Die Folgezeile ist Teil der Zeile, bevor irgendetwas aus ihr gelesen wird — die Extraktionsreihenfolge aus §1 bleibt unangetastet. Alles gehört zur ersten Zeile: Einrückung, Warnungs-Zeilennummer, Rückschreiber; der Cursor in der Fortsetzung wählt ihren Knoten aus, wie bei Beschreibungszeilen. SPEC §1/§9, llms.md, Legende in neun Sprachen, D59; 20 neue Tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
257aab08dd
commit
410dacdc6c
@@ -19,6 +19,7 @@ reverse.
|
|||||||
|
|
||||||
## 2026-08-24
|
## 2026-08-24
|
||||||
|
|
||||||
|
- A line ending in a space and `\` continues on the next line, so a long node no longer has to fit into one
|
||||||
- A star in the header opens What's new: the changes of the last few days, and a link per day that shows the nodes it touched in the diagram
|
- A star in the header opens What's new: the changes of the last few days, and a link per day that shows the nodes it touched in the diagram
|
||||||
- A warning triangle replaces the question mark as the pointer over faulty line numbers
|
- A warning triangle replaces the question mark as the pointer over faulty line numbers
|
||||||
- The node window replaces the browser tooltip everywhere — at the pointer, on keyboard focus and on touch
|
- The node window replaces the browser tooltip everywhere — at the pointer, on keyboard focus and on touch
|
||||||
|
|||||||
@@ -4202,3 +4202,76 @@ Besuchsvergleich darunter bleibt damit das erste, worauf der Blick fällt.
|
|||||||
nichts. Der Schlüssel ist trotzdem in allen neun Sprachen angelegt, auch auf
|
nichts. Der Schlüssel ist trotzdem in allen neun Sprachen angelegt, auch auf
|
||||||
Englisch: Ein Loch in der Tabelle lädt dazu ein, beim nächsten Durchsehen für
|
Englisch: Ein Loch in der Tabelle lädt dazu ein, beim nächsten Durchsehen für
|
||||||
einen Fehler gehalten und „repariert" zu werden.
|
einen Fehler gehalten und „repariert" zu werden.
|
||||||
|
|
||||||
|
## D59 — Fortsetzungszeilen: `\` am Zeilenende, Leerraum davor Pflicht
|
||||||
|
Eine Zeile trägt in dieser Notation alles auf einmal — Einrückung, Zeichen,
|
||||||
|
Statusbox, Label, Größe, URL, Tags, ID, Abhängigkeiten, Fokusmarke, Kommentar.
|
||||||
|
Im mitgelieferten Plan ist die längste Zeile 122 Zeichen lang (D49), und seit
|
||||||
|
das Textfeld nicht mehr umbricht, muss man dafür waagerecht schieben. Ein `\`
|
||||||
|
am Zeilenende verteilt sie jetzt auf mehrere Textzeilen, ohne dass ein neuer
|
||||||
|
Knoten entsteht.
|
||||||
|
|
||||||
|
**Leerraum vor dem `\` ist Pflicht** — `… \` setzt fort, `…\` nicht. Das ist
|
||||||
|
die entscheidende Festlegung, und sie geht bewusst gegen die Gewohnheit aus
|
||||||
|
Shell, C und Makefile, wo `foo\` fortsetzt. Der Grund ist die **Asymmetrie der
|
||||||
|
Fehlerfälle**:
|
||||||
|
|
||||||
|
- Ohne die Regel verschluckt ein Label, das selbst auf einen Backslash endet
|
||||||
|
(`C:\temp\`), **stumm den folgenden Knoten**. Ein Knoten verschwindet aus dem
|
||||||
|
Diagramm, und im Text sieht die Zeile richtig aus.
|
||||||
|
- Mit der Regel bekommt, wer aus der Shell `foo\` schreibt, **keine
|
||||||
|
Fortsetzung**. Die Zeile bleibt stehen, der `\` ist sichtbar, der Fehler
|
||||||
|
erklärt sich beim Hinsehen.
|
||||||
|
|
||||||
|
Der zweite Fehler ist häufiger, der erste ist schlimmer — und dieses Projekt
|
||||||
|
zieht durchweg den lauten dem stillen vor (SPEC §4, D40). Die Regel ist zudem
|
||||||
|
keine neue Sorte: `=`, `>`/`<` und `"` verlangen alle Leerraum, nur auf der
|
||||||
|
**anderen** Seite des Zeichens. Ein `\\`-Escape als Alternative wäre die dritte
|
||||||
|
Möglichkeit gewesen — verworfen, weil die Notation sonst nirgends escapt und
|
||||||
|
eine einzige Escape-Regel für einen Randfall mehr Erklärung kostet, als sie
|
||||||
|
wert ist.
|
||||||
|
|
||||||
|
**Verbunden wird mit genau einem Leerzeichen.** Damit ist ein Token nicht über
|
||||||
|
den Umbruch trennbar — eine zerschnittene URL bleibt zerschnitten. Das ist eine
|
||||||
|
echte Einschränkung und trotzdem richtig herum: Ohne das Leerzeichen führen
|
||||||
|
`Backend \` + `Frontend` zu `BackendFrontend`, und *das* wäre die stille
|
||||||
|
Variante. Wer eine lange URL hat, lässt sie in ihrer Zeile.
|
||||||
|
|
||||||
|
**Alles gehört zur ersten Zeile.** Ihre Einrückung bestimmt die Ebene, ihre
|
||||||
|
Nummer nennen die Warnungen, und alles, was zurückschreibt, fasst nur sie an —
|
||||||
|
`setFoldMark()` (D38) und `expandShortIds()` (D55) finden Gate und Statusbox
|
||||||
|
dort. `expandShortIds()` musste dafür lernen, Fortsetzungszeilen zu
|
||||||
|
**überspringen**: Sonst hätte es eine solche Zeile für eine Wurzelzeile
|
||||||
|
gehalten (sie hat kein Gate) und den Vorfahren-Stapel verdorben.
|
||||||
|
|
||||||
|
**Der Cursor in einer Fortsetzungszeile wählt ihren Knoten aus** — dieselbe
|
||||||
|
Regel wie bei Beschreibungszeilen (D40-Nachtrag 2), und aus demselben Grund:
|
||||||
|
Die Zeile trägt keinen eigenen Knoten, gehört aber zu einem, und wer darin
|
||||||
|
schreibt, arbeitet an genau diesem. Getragen wird das von `node.descLines`, das
|
||||||
|
damit nicht mehr nur Beschreibungen führt; der Name bleibt, weil eine
|
||||||
|
Umbenennung durch Renderer, Tests und Snapshots nichts hinzufügte, was der
|
||||||
|
Kommentar nicht sagt.
|
||||||
|
|
||||||
|
**Gilt nur im Baumteil.** Hinter dem `---`-Trenner ist der Zeilenumbruch
|
||||||
|
Absatzstruktur (§1), und der Inhalt ist ausdrücklich Freitext — ein `\` bleibt
|
||||||
|
dort gewöhnlicher Text.
|
||||||
|
|
||||||
|
**Reihenfolge gegenüber dem Kommentar:** Kommentare fallen zuerst weg (§1,
|
||||||
|
Schritt 1), das Verbinden ist Schritt 1b. Also setzt `- A \ %% Notiz` fort und
|
||||||
|
`- A %% Notiz \` nicht — beides ist die natürliche Lesart, und sie fällt ohne
|
||||||
|
Sonderregel richtig aus.
|
||||||
|
|
||||||
|
**Umgesetzt als Vor-Durchlauf** (`logicalLines()` in parser.js): Er liefert
|
||||||
|
statt roher Zeilen `{raw, line, cont}`, und `parse()` arbeitet unverändert
|
||||||
|
darauf weiter. Dadurch gibt es genau **eine** Stelle, die die Regel kennt, und
|
||||||
|
die Extraktionsreihenfolge aus §1 bleibt unangetastet — die Folgezeile ist
|
||||||
|
schon Teil der Zeile, bevor irgendetwas aus ihr gelesen wird. Ein Sonderfall
|
||||||
|
steckt darin: Bleibt von der ersten Zeile nur die **Einrückung** übrig (` \`),
|
||||||
|
darf sie nicht mit weggeputzt werden — sie trägt die Ebene.
|
||||||
|
|
||||||
|
**Nachgemessen** im laufenden Editor: `- Ein Knoten mit einem \` + `sehr langen
|
||||||
|
Titel (L) @anna` ergibt einen Knoten mit `data-line="2"` und
|
||||||
|
`data-desc-lines="3"`, Größe `L` und Tag `anna` von der zweiten Zeile gelesen,
|
||||||
|
0 Warnungen; der Cursor auf Zeile 3 hebt denselben Knoten hervor wie auf Zeile
|
||||||
|
2. 20 neue Tests; die Gegenprobe (Leerraum-Pflicht aus dem Regex entfernt)
|
||||||
|
lässt genau die zwei danach benannten Zusicherungen fallen.
|
||||||
|
|||||||
+38
-2
@@ -14,6 +14,8 @@ Alle Bestandteile außer dem Label sind optional. Die Extraktion erfolgt in
|
|||||||
dieser Reihenfolge (wichtig für Kollisionsfreiheit):
|
dieser Reihenfolge (wichtig für Kollisionsfreiheit):
|
||||||
|
|
||||||
1. Kommentar entfernen: alles ab `%%` bis Zeilenende.
|
1. Kommentar entfernen: alles ab `%%` bis Zeilenende.
|
||||||
|
1b. Fortsetzung: Endet die Zeile jetzt auf Leerraum + `\`, gehört die
|
||||||
|
Folgezeile noch dazu (siehe unten). Erst danach ist die Zeile vollständig.
|
||||||
2. Einrückung, Zeichen (`-` / `+` / `|` / `=`), Statusbox `[…]` und Faltmarke
|
2. Einrückung, Zeichen (`-` / `+` / `|` / `=`), Statusbox `[…]` und Faltmarke
|
||||||
(`>` / `<`) per Zeilen-Regex; `=` nur mit folgendem Leerraum (§3),
|
(`>` / `<`) per Zeilen-Regex; `=` nur mit folgendem Leerraum (§3),
|
||||||
die Faltmarke ebenso (siehe unten).
|
die Faltmarke ebenso (siehe unten).
|
||||||
@@ -25,6 +27,38 @@ dieser Reihenfolge (wichtig für Kollisionsfreiheit):
|
|||||||
8. Fokusmarke: `!!!` als **alleinstehendes** Token (siehe unten).
|
8. Fokusmarke: `!!!` als **alleinstehendes** Token (siehe unten).
|
||||||
9. Rest, whitespace-normalisiert = Label. Leeres Label ⇒ Zeile ignorieren.
|
9. Rest, whitespace-normalisiert = Label. Leeres Label ⇒ Zeile ignorieren.
|
||||||
|
|
||||||
|
**Fortsetzungszeile `\`** — eine lange Zeile darf auf mehrere Textzeilen
|
||||||
|
verteilt werden, ohne dass ein neuer Knoten entsteht:
|
||||||
|
|
||||||
|
- Endet eine Zeile — **nach** dem Entfernen des Kommentars — auf **Leerraum und
|
||||||
|
dann `\`** als letztem Zeichen, wird die **Folgezeile angehängt**: Der `\`
|
||||||
|
entfällt, die Einrückung der Folgezeile entfällt, verbunden wird mit **genau
|
||||||
|
einem Leerzeichen**. Mehrere `\` hintereinander setzen die Zeile über
|
||||||
|
entsprechend viele Textzeilen fort.
|
||||||
|
- **Leerraum davor ist Pflicht** (`… \`, nicht `…\`) — dieselbe Sorte Regel wie
|
||||||
|
bei `=`, `>`/`<` und `"`, nur an der anderen Seite des Zeichens. Sie hält
|
||||||
|
Labels heraus, die selbst auf einen Backslash enden (`C:\temp\`): Ohne sie
|
||||||
|
verschluckte so eine Zeile stumm den folgenden Knoten. Der Preis ist die
|
||||||
|
umgekehrte Verwechslung — wer aus der Shell `…\` gewohnt ist, bekommt keine
|
||||||
|
Fortsetzung —, und die ist die harmlosere: Die Zeile bleibt stehen, was man
|
||||||
|
sofort sieht.
|
||||||
|
- **Alles gehört zur ersten Zeile:** Ihre Einrückung bestimmt die Ebene (§2),
|
||||||
|
ihre Nummer nennen die Warnungen, und Werkzeuge, die zurückschreiben
|
||||||
|
(Faltmarke §9, ID-Kurzform §9), fassen nur sie an. Der Cursor in einer
|
||||||
|
Fortsetzungszeile wählt den Knoten der ersten aus (§9, Sprung).
|
||||||
|
- Ein Token darf **nicht** über den Umbruch hinweg getrennt werden — verbunden
|
||||||
|
wird mit einem Leerzeichen, eine zerschnittene URL bleibt zerschnitten.
|
||||||
|
- Gilt nur im **Baumteil**. Im Beschreibungsteil hinter `---` (siehe unten) sind
|
||||||
|
Zeilenumbrüche Absatzstruktur; ein `\` bleibt dort gewöhnlicher Text.
|
||||||
|
- Steht der `\` in der **letzten** Zeile der Datei, gibt es nichts anzuhängen:
|
||||||
|
Er entfällt, die Zeile bleibt für sich.
|
||||||
|
|
||||||
|
Referenz-Regex (Schritt 1b, geprüft auf der kommentarfreien Zeile):
|
||||||
|
|
||||||
|
```
|
||||||
|
(^|[ \t])\\[ \t]*$
|
||||||
|
```
|
||||||
|
|
||||||
**Faltmarke `>` / `<`** — bestimmt, wie das Dokument **eröffnet** wird:
|
**Faltmarke `>` / `<`** — bestimmt, wie das Dokument **eröffnet** wird:
|
||||||
|
|
||||||
- Steht **unmittelbar vor dem Label**, also hinter der Statusbox
|
- Steht **unmittelbar vor dem Label**, also hinter der Statusbox
|
||||||
@@ -709,8 +743,10 @@ verknüpft (siehe D25):
|
|||||||
„hinschauen": Er holt **keine Bildschirmtastatur** herauf — die erscheint erst,
|
„hinschauen": Er holt **keine Bildschirmtastatur** herauf — die erscheint erst,
|
||||||
wenn das Textfeld selbst angetippt wird.
|
wenn das Textfeld selbst angetippt wird.
|
||||||
- **Text → Diagramm:** Der Knoten der **Cursor-Zeile** wird im Diagramm
|
- **Text → Diagramm:** Der Knoten der **Cursor-Zeile** wird im Diagramm
|
||||||
hervorgehoben und beim Zeilenwechsel ins Bild gescrollt. **Beschreibungszeilen
|
hervorgehoben und beim Zeilenwechsel ins Bild gescrollt. **Beschreibungs- und
|
||||||
zählen zu ihrem Knoten**: Steht der Cursor in einer `"`-Zeile (§1) oder in
|
Fortsetzungszeilen (§1)
|
||||||
|
zählen zu ihrem Knoten**: Steht der Cursor in einer `"`-Zeile (§1), in einer
|
||||||
|
Fortsetzung hinter `\` oder in
|
||||||
einem ID-Block des `---`-Beschreibungsteils (Kopfzeile eingeschlossen), gilt
|
einem ID-Block des `---`-Beschreibungsteils (Kopfzeile eingeschlossen), gilt
|
||||||
der beschriebene Knoten als ausgewählt — solche Zeilen tragen keinen eigenen
|
der beschriebene Knoten als ausgewählt — solche Zeilen tragen keinen eigenen
|
||||||
Knoten, gehören aber zu einem, und wer darin schreibt, arbeitet an genau
|
Knoten, gehören aber zu einem, und wer darin schreibt, arbeitet an genau
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
- [^] #not.status: Status box, eight states (S)
|
- [^] #not.status: Status box, eight states (S)
|
||||||
- [^] #not.size: T-shirt size and the "decompose from M" rule (S)
|
- [^] #not.size: T-shirt size and the "decompose from M" rule (S)
|
||||||
- [^] #not.people: People tags, bare URLs, %% comments (XS)
|
- [^] #not.people: People tags, bare URLs, %% comments (XS)
|
||||||
|
- [x] #not.cont: A trailing backslash continues the line (XS) %% space before it is required
|
||||||
- [^] #not.gates: And/or decomposition (S)
|
- [^] #not.gates: And/or decomposition (S)
|
||||||
- [^] #not.opt: Optional nodes — neither required nor an alternative (S)
|
- [^] #not.opt: Optional nodes — neither required nor an alternative (S)
|
||||||
- [^] #not.id: Node IDs, (#auth:) before the title (S) %% often the ticket number, see SPEC §11
|
- [^] #not.id: Node IDs, (#auth:) before the title (S) %% often the ticket number, see SPEC §11
|
||||||
@@ -206,6 +207,12 @@
|
|||||||
label, size, URL, tags, comment. The order in which those are extracted is
|
label, size, URL, tags, comment. The order in which those are extracted is
|
||||||
fixed, which is what keeps them from colliding with each other.
|
fixed, which is what keeps them from colliding with each other.
|
||||||
|
|
||||||
|
#not.cont
|
||||||
|
A long line may be spread over several text lines: ending one with a space
|
||||||
|
and a backslash makes the next line part of it, so no new node begins. The
|
||||||
|
space is required, or a label like C:\temp\ would silently swallow the node
|
||||||
|
below it.
|
||||||
|
|
||||||
#not.status
|
#not.status
|
||||||
Eight states from vague idea to live in production, written as an extended
|
Eight states from vague idea to live in production, written as an extended
|
||||||
Markdown task box. The colours follow risk rather than progress: money spent
|
Markdown task box. The colours follow risk rather than progress: money spent
|
||||||
|
|||||||
@@ -18,6 +18,25 @@ https://werkbaum.javagil.de renders the notation as a diagram;
|
|||||||
|
|
||||||
One node per line. Everything except the label is optional.
|
One node per line. Everything except the label is optional.
|
||||||
|
|
||||||
|
### Line continuation (`\` at the end)
|
||||||
|
|
||||||
|
- A line ending in **whitespace followed by `\`** continues on the next line:
|
||||||
|
no new node begins. The `\` and the next line's indentation are dropped, the
|
||||||
|
two parts are joined with **exactly one space**. Several `\` in a row extend
|
||||||
|
the line further.
|
||||||
|
- The whitespace before the `\` is **required** — `C:\temp\` stays a label and
|
||||||
|
does not swallow the following node. Note this differs from shell habit,
|
||||||
|
where `foo\` continues.
|
||||||
|
- Everything belongs to the **first** line: its indentation sets the level, its
|
||||||
|
number is the one warnings report, and tools that write back (fold mark, id
|
||||||
|
shorthand) only touch it.
|
||||||
|
- A token must not be split across the break — the join inserts a space, so a
|
||||||
|
cut URL stays cut.
|
||||||
|
- Only in the tree part. Behind the `---` divider line breaks are paragraph
|
||||||
|
structure, and `\` is ordinary text.
|
||||||
|
- Comments are removed **first**: `- A \ %% note` continues, `- A %% note \`
|
||||||
|
does not.
|
||||||
|
|
||||||
### Hierarchy (indentation)
|
### Hierarchy (indentation)
|
||||||
|
|
||||||
- Indentation defines the tree: a node's parent is the nearest line **above**
|
- Indentation defines the tree: a node's parent is the nearest line **above**
|
||||||
|
|||||||
@@ -1943,6 +1943,7 @@ const I18N = {
|
|||||||
hint_size:"Aufwand als T-Shirt-Größe in Klammern, Link einfach als URL anhängen:",
|
hint_size:"Aufwand als T-Shirt-Größe in Klammern, Link einfach als URL anhängen:",
|
||||||
hint_break:"Ab (M) gilt: weiter untergliedern — fehlt die Untergliederung, erscheint ein Platzhalter im Diagramm.",
|
hint_break:"Ab (M) gilt: weiter untergliedern — fehlt die Untergliederung, erscheint ein Platzhalter im Diagramm.",
|
||||||
hint_comment:"Kommentare mit %% — als ganze Zeile oder am Zeilenende.",
|
hint_comment:"Kommentare mit %% — als ganze Zeile oder am Zeilenende.",
|
||||||
|
hint_cont:"Leerzeichen und \\ am Zeilenende — die nächste Zeile gehört noch dazu.",
|
||||||
hint_people:"Personen mit @name — erscheinen unten rechts am Knoten.",
|
hint_people:"Personen mit @name — erscheinen unten rechts am Knoten.",
|
||||||
hint_id:"Knoten-ID mit #name: vor dem Titel — erscheint im Tooltip des Knotens.",
|
hint_id:"Knoten-ID mit #name: vor dem Titel — erscheint im Tooltip des Knotens.",
|
||||||
hint_deps:"Abhängigkeiten mit :#name,#name — erscheinen im Tooltip.",
|
hint_deps:"Abhängigkeiten mit :#name,#name — erscheinen im Tooltip.",
|
||||||
@@ -2043,6 +2044,7 @@ const I18N = {
|
|||||||
hint_size:"Effort as a T-shirt size in parentheses; add a link simply as a URL:",
|
hint_size:"Effort as a T-shirt size in parentheses; add a link simply as a URL:",
|
||||||
hint_break:"From (M) on: break it down further — if the breakdown is missing, a placeholder appears in the diagram.",
|
hint_break:"From (M) on: break it down further — if the breakdown is missing, a placeholder appears in the diagram.",
|
||||||
hint_comment:"Comments with %% — whole line or at the end of a line.",
|
hint_comment:"Comments with %% — whole line or at the end of a line.",
|
||||||
|
hint_cont:"A space and \\ at the end of a line — the next line still belongs to it.",
|
||||||
hint_people:"People with @name — shown at the bottom-right of the node.",
|
hint_people:"People with @name — shown at the bottom-right of the node.",
|
||||||
hint_id:"Node ID with #name: before the title — shown in the node's tooltip.",
|
hint_id:"Node ID with #name: before the title — shown in the node's tooltip.",
|
||||||
hint_deps:"Dependencies with :#name,#name — shown in the tooltip.",
|
hint_deps:"Dependencies with :#name,#name — shown in the tooltip.",
|
||||||
@@ -2143,6 +2145,7 @@ const I18N = {
|
|||||||
hint_size:"Esfuerzo como talla de camiseta entre paréntesis; añade un enlace simplemente como URL:",
|
hint_size:"Esfuerzo como talla de camiseta entre paréntesis; añade un enlace simplemente como URL:",
|
||||||
hint_break:"A partir de (M): sigue desglosando — si falta el desglose, aparece un marcador de posición en el diagrama.",
|
hint_break:"A partir de (M): sigue desglosando — si falta el desglose, aparece un marcador de posición en el diagrama.",
|
||||||
hint_comment:"Comentarios con %% — línea completa o al final de la línea.",
|
hint_comment:"Comentarios con %% — línea completa o al final de la línea.",
|
||||||
|
hint_cont:"Un espacio y \\ al final de una línea — la línea siguiente sigue perteneciendo a ella.",
|
||||||
hint_people:"Personas con @nombre — aparecen abajo a la derecha del nodo.",
|
hint_people:"Personas con @nombre — aparecen abajo a la derecha del nodo.",
|
||||||
hint_id:"ID de nodo con #nombre: delante del título — visible en el tooltip del nodo.",
|
hint_id:"ID de nodo con #nombre: delante del título — visible en el tooltip del nodo.",
|
||||||
hint_deps:"Dependencias con :#nombre,#nombre — visibles en el tooltip.",
|
hint_deps:"Dependencias con :#nombre,#nombre — visibles en el tooltip.",
|
||||||
@@ -2243,6 +2246,7 @@ const I18N = {
|
|||||||
hint_size:"Effort en taille de T-shirt entre parenthèses ; ajoutez un lien simplement comme URL :",
|
hint_size:"Effort en taille de T-shirt entre parenthèses ; ajoutez un lien simplement comme URL :",
|
||||||
hint_break:"À partir de (M) : décomposer davantage — si la décomposition manque, un espace réservé apparaît dans le diagramme.",
|
hint_break:"À partir de (M) : décomposer davantage — si la décomposition manque, un espace réservé apparaît dans le diagramme.",
|
||||||
hint_comment:"Commentaires avec %% — ligne entière ou en fin de ligne.",
|
hint_comment:"Commentaires avec %% — ligne entière ou en fin de ligne.",
|
||||||
|
hint_cont:"Une espace et \\ en fin de ligne — la ligne suivante en fait encore partie.",
|
||||||
hint_people:"Personnes avec @nom — affichées en bas à droite du nœud.",
|
hint_people:"Personnes avec @nom — affichées en bas à droite du nœud.",
|
||||||
hint_id:"ID de nœud avec #nom : devant le titre — visible dans l’infobulle du nœud.",
|
hint_id:"ID de nœud avec #nom : devant le titre — visible dans l’infobulle du nœud.",
|
||||||
hint_deps:"Dépendances avec :#nom,#nom — visibles dans l’infobulle.",
|
hint_deps:"Dépendances avec :#nom,#nom — visibles dans l’infobulle.",
|
||||||
@@ -2343,6 +2347,7 @@ const I18N = {
|
|||||||
hint_size:"Nakład jako rozmiar koszulki w nawiasach; link dodaj po prostu jako URL:",
|
hint_size:"Nakład jako rozmiar koszulki w nawiasach; link dodaj po prostu jako URL:",
|
||||||
hint_break:"Od (M): dziel dalej — gdy brakuje podziału, w diagramie pojawia się symbol zastępczy.",
|
hint_break:"Od (M): dziel dalej — gdy brakuje podziału, w diagramie pojawia się symbol zastępczy.",
|
||||||
hint_comment:"Komentarze z %% — cały wiersz lub na końcu wiersza.",
|
hint_comment:"Komentarze z %% — cały wiersz lub na końcu wiersza.",
|
||||||
|
hint_cont:"Spacja i \\ na końcu wiersza — następny wiersz nadal do niego należy.",
|
||||||
hint_people:"Osoby z @nazwa — pokazywane w prawym dolnym rogu węzła.",
|
hint_people:"Osoby z @nazwa — pokazywane w prawym dolnym rogu węzła.",
|
||||||
hint_id:"ID węzła przez #nazwa: przed tytułem — widoczne w podpowiedzi węzła.",
|
hint_id:"ID węzła przez #nazwa: przed tytułem — widoczne w podpowiedzi węzła.",
|
||||||
hint_deps:"Zależności przez :#nazwa,#nazwa — widoczne w podpowiedzi.",
|
hint_deps:"Zależności przez :#nazwa,#nazwa — widoczne w podpowiedzi.",
|
||||||
@@ -2443,6 +2448,7 @@ const I18N = {
|
|||||||
hint_size:"Трудоёмкость как размер футболки в скобках; ссылку добавьте просто как URL:",
|
hint_size:"Трудоёмкость как размер футболки в скобках; ссылку добавьте просто как URL:",
|
||||||
hint_break:"С (M): дробите дальше — если декомпозиции нет, в диаграмме появляется заполнитель.",
|
hint_break:"С (M): дробите дальше — если декомпозиции нет, в диаграмме появляется заполнитель.",
|
||||||
hint_comment:"Комментарии через %% — вся строка или в конце строки.",
|
hint_comment:"Комментарии через %% — вся строка или в конце строки.",
|
||||||
|
hint_cont:"Пробел и \\ в конце строки — следующая строка всё ещё относится к ней.",
|
||||||
hint_people:"Люди через @имя — показываются справа внизу узла.",
|
hint_people:"Люди через @имя — показываются справа внизу узла.",
|
||||||
hint_id:"ID узла через #имя: перед заголовком — виден во всплывающей подсказке узла.",
|
hint_id:"ID узла через #имя: перед заголовком — виден во всплывающей подсказке узла.",
|
||||||
hint_deps:"Зависимости через :#имя,#имя — видны в подсказке.",
|
hint_deps:"Зависимости через :#имя,#имя — видны в подсказке.",
|
||||||
@@ -2543,6 +2549,7 @@ const I18N = {
|
|||||||
hint_size:"प्रयास कोष्ठक में टी-शर्ट आकार के रूप में; लिंक बस URL के रूप में जोड़ें:",
|
hint_size:"प्रयास कोष्ठक में टी-शर्ट आकार के रूप में; लिंक बस URL के रूप में जोड़ें:",
|
||||||
hint_break:"(M) से आगे: और विभाजित करें — विभाजन न होने पर आरेख में प्लेसहोल्डर दिखता है।",
|
hint_break:"(M) से आगे: और विभाजित करें — विभाजन न होने पर आरेख में प्लेसहोल्डर दिखता है।",
|
||||||
hint_comment:"%% से टिप्पणियाँ — पूरी पंक्ति या पंक्ति के अंत में।",
|
hint_comment:"%% से टिप्पणियाँ — पूरी पंक्ति या पंक्ति के अंत में।",
|
||||||
|
hint_cont:"पंक्ति के अंत में स्पेस और \\ — अगली पंक्ति उसी की बनी रहती है।",
|
||||||
hint_people:"@नाम से व्यक्ति — नोड के नीचे-दाएँ दिखते हैं।",
|
hint_people:"@नाम से व्यक्ति — नोड के नीचे-दाएँ दिखते हैं।",
|
||||||
hint_id:"शीर्षक से पहले #नाम: के रूप में नोड आईडी — नोड के टूलटिप में दिखती है।",
|
hint_id:"शीर्षक से पहले #नाम: के रूप में नोड आईडी — नोड के टूलटिप में दिखती है।",
|
||||||
hint_deps:":#नाम,#नाम से निर्भरताएँ — टूलटिप में दिखती हैं।",
|
hint_deps:":#नाम,#नाम से निर्भरताएँ — टूलटिप में दिखती हैं।",
|
||||||
@@ -2643,6 +2650,7 @@ const I18N = {
|
|||||||
hint_size:"用括号中的 T 恤尺码表示工作量;链接直接作为 URL 附加:",
|
hint_size:"用括号中的 T 恤尺码表示工作量;链接直接作为 URL 附加:",
|
||||||
hint_break:"从 (M) 起:继续细分——若缺少细分,图表中会出现占位符。",
|
hint_break:"从 (M) 起:继续细分——若缺少细分,图表中会出现占位符。",
|
||||||
hint_comment:"用 %% 注释——整行或行尾。",
|
hint_comment:"用 %% 注释——整行或行尾。",
|
||||||
|
hint_cont:"行尾的空格加 \\ —— 下一行仍属于这一行。",
|
||||||
hint_people:"用 @姓名 表示人员——显示在节点右下角。",
|
hint_people:"用 @姓名 表示人员——显示在节点右下角。",
|
||||||
hint_id:"用 #名称: 写在标题前指定节点 ID——显示在节点提示中。",
|
hint_id:"用 #名称: 写在标题前指定节点 ID——显示在节点提示中。",
|
||||||
hint_deps:"用 :#名称,#名称 表示依赖——显示在提示中。",
|
hint_deps:"用 :#名称,#名称 表示依赖——显示在提示中。",
|
||||||
@@ -2743,6 +2751,7 @@ const I18N = {
|
|||||||
hint_size:"工数は括弧内の T シャツサイズで;リンクは URL としてそのまま追加:",
|
hint_size:"工数は括弧内の T シャツサイズで;リンクは URL としてそのまま追加:",
|
||||||
hint_break:"(M) 以上:さらに分解 — 分解がないと図にプレースホルダーが表示されます。",
|
hint_break:"(M) 以上:さらに分解 — 分解がないと図にプレースホルダーが表示されます。",
|
||||||
hint_comment:"%% でコメント — 行全体または行末。",
|
hint_comment:"%% でコメント — 行全体または行末。",
|
||||||
|
hint_cont:"行末の空白と \\ — 次の行も同じ行に属します。",
|
||||||
hint_people:"@名前 で担当者 — ノードの右下に表示されます。",
|
hint_people:"@名前 で担当者 — ノードの右下に表示されます。",
|
||||||
hint_id:"タイトルの前に #名前: でノード ID — ノードのツールチップに表示されます。",
|
hint_id:"タイトルの前に #名前: でノード ID — ノードのツールチップに表示されます。",
|
||||||
hint_deps:":#名前,#名前 で依存関係 — ツールチップに表示されます。",
|
hint_deps:":#名前,#名前 で依存関係 — ツールチップに表示されます。",
|
||||||
@@ -2780,6 +2789,7 @@ function buildHint(){
|
|||||||
<code>- [ ] Backend (L) https://…</code><br>
|
<code>- [ ] Backend (L) https://…</code><br>
|
||||||
${esc(t('hint_break'))}<br>
|
${esc(t('hint_break'))}<br>
|
||||||
${esc(t('hint_comment'))}
|
${esc(t('hint_comment'))}
|
||||||
|
${esc(t('hint_cont'))}
|
||||||
${esc(t('hint_people'))}
|
${esc(t('hint_people'))}
|
||||||
${esc(t('hint_id'))}
|
${esc(t('hint_id'))}
|
||||||
${esc(t('hint_deps'))}
|
${esc(t('hint_deps'))}
|
||||||
|
|||||||
+55
-4
@@ -38,6 +38,41 @@ export function setFoldMark(line, mark){
|
|||||||
|
|
||||||
const RE_LINE = /^([ \t]*)([-|+]|=(?=[ \t]))?\s*(?:([><])(?=[ \t])\s*)?(?:\[([^\]])\]\s*)?(?:([><])(?=[ \t])\s*)?(.*)$/;
|
const RE_LINE = /^([ \t]*)([-|+]|=(?=[ \t]))?\s*(?:([><])(?=[ \t])\s*)?(?:\[([^\]])\]\s*)?(?:([><])(?=[ \t])\s*)?(.*)$/;
|
||||||
const RE_ID_TOKEN = /(^|\s)#([\p{L}\p{N}._-]+)/u;
|
const RE_ID_TOKEN = /(^|\s)#([\p{L}\p{N}._-]+)/u;
|
||||||
|
/* Fortsetzungszeile (SPEC §1): Leerraum, dann `\` als letztes Zeichen. Der
|
||||||
|
Leerraum davor ist Pflicht — ohne ihn verschluckte ein Label wie `C:\temp\`
|
||||||
|
stumm den folgenden Knoten. */
|
||||||
|
const RE_CONT = /(^|[ \t])\\[ \t]*$/;
|
||||||
|
const RE_SEP = /^[ \t]*-{3,}[ \t]*$/;
|
||||||
|
|
||||||
|
/* Text → logische Zeilen: `%%`-Kommentare weg (SPEC §1, Schritt 1), dann
|
||||||
|
Fortsetzungen anhängen (Schritt 1b). Ergebnis je Eintrag:
|
||||||
|
`{raw, line, cont}` — `line` ist die Nummer der ERSTEN Textzeile (sie trägt
|
||||||
|
Einrückung, Gate und Statusbox und wird von allen Rückschreibern angefasst),
|
||||||
|
`cont` sind die Nummern der angehängten.
|
||||||
|
|
||||||
|
Verbunden wird mit genau einem Leerzeichen, die Einrückung der Folgezeile
|
||||||
|
entfällt — bis auf den Fall, dass von der ersten Zeile nur die Einrückung
|
||||||
|
übrig bleibt (` \`): Dann trägt sie die Ebene und muss stehen bleiben.
|
||||||
|
Hinter dem `---`-Trenner wird nicht mehr verbunden; dort ist der
|
||||||
|
Zeilenumbruch Absatzstruktur (SPEC §1). */
|
||||||
|
export function logicalLines(text){
|
||||||
|
const lines = text.split('\n');
|
||||||
|
const out = [];
|
||||||
|
let inDesc = false;
|
||||||
|
for(let i = 0; i < lines.length; i++){
|
||||||
|
const rec = {raw: lines[i].replace(/%%.*$/, ''), line: i + 1, cont: []};
|
||||||
|
if(!inDesc && RE_SEP.test(rec.raw)) inDesc = true;
|
||||||
|
while(!inDesc && RE_CONT.test(rec.raw)){
|
||||||
|
rec.raw = rec.raw.replace(RE_CONT, '$1');
|
||||||
|
if(i + 1 >= lines.length) break; /* letzte Zeile: der `\` entfällt einfach */
|
||||||
|
if(rec.raw && !/[ \t]$/.test(rec.raw)) rec.raw += ' ';
|
||||||
|
rec.raw += lines[++i].replace(/%%.*$/, '').replace(/^[ \t]*/, '');
|
||||||
|
rec.cont.push(i + 1);
|
||||||
|
}
|
||||||
|
out.push(rec);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
/* Kurzschreibweise der Knoten-ID auflösen: `#.kc` unter `#prod-stage` wird zu
|
/* Kurzschreibweise der Knoten-ID auflösen: `#.kc` unter `#prod-stage` wird zu
|
||||||
`#prod-stage.kc` (D55). Das ist eine **Eingabehilfe**, keine Notation — die
|
`#prod-stage.kc` (D55). Das ist eine **Eingabehilfe**, keine Notation — die
|
||||||
@@ -55,12 +90,19 @@ export function expandShortIds(text){
|
|||||||
const lines = text.split('\n');
|
const lines = text.split('\n');
|
||||||
const stack = []; /* {width, id} — auch Knoten OHNE ID stehen drin */
|
const stack = []; /* {width, id} — auch Knoten OHNE ID stehen drin */
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
let fortsetzung = false; /* die vorige Zeile endete auf `\` (SPEC §1) */
|
||||||
for(let i = 0; i < lines.length; i++){
|
for(let i = 0; i < lines.length; i++){
|
||||||
const raw = lines[i];
|
const raw = lines[i];
|
||||||
if(/^\s*-{3,}\s*$/.test(raw)) break;
|
if(/^\s*-{3,}\s*$/.test(raw)) break;
|
||||||
const k = raw.indexOf('%%');
|
const k = raw.indexOf('%%');
|
||||||
const head = k === -1 ? raw : raw.slice(0, k);
|
const head = k === -1 ? raw : raw.slice(0, k);
|
||||||
const tail = k === -1 ? '' : raw.slice(k);
|
const tail = k === -1 ? '' : raw.slice(k);
|
||||||
|
/* Eine Fortsetzungszeile trägt keinen eigenen Knoten: Sie darf weder den
|
||||||
|
Vorfahren-Stapel verändern noch als Ort einer Kurzform gelten — die ID
|
||||||
|
steht an der ersten Zeile. */
|
||||||
|
const warFortsetzung = fortsetzung;
|
||||||
|
fortsetzung = RE_CONT.test(head);
|
||||||
|
if(warFortsetzung) continue;
|
||||||
const m = head.match(RE_LINE);
|
const m = head.match(RE_LINE);
|
||||||
if(!m) continue;
|
if(!m) continue;
|
||||||
const body = m[6];
|
const body = m[6];
|
||||||
@@ -100,8 +142,10 @@ const REALIZED = new Set(['arbeit', 'durchstich', 'fertig', 'prod']);
|
|||||||
`desc` (SPEC §11/D40) ist der Beschreibungstext: `"`-Zeilen unter dem
|
`desc` (SPEC §11/D40) ist der Beschreibungstext: `"`-Zeilen unter dem
|
||||||
Knoten (Kurzform) und ID-Blöcke aus dem `---`-Beschreibungsteil (Langform),
|
Knoten (Kurzform) und ID-Blöcke aus dem `---`-Beschreibungsteil (Langform),
|
||||||
in Dokumentreihenfolge mit Zeilenumbrüchen zusammengefügt; null ohne.
|
in Dokumentreihenfolge mit Zeilenumbrüchen zusammengefügt; null ohne.
|
||||||
`descLines` sind die ZEILENNUMMERN dieser Beschreibung (SPEC §9): Steht der
|
`descLines` sind die ZEILENNUMMERN der Zeilen, die zu diesem Knoten gehören,
|
||||||
Cursor dort, gilt dieser Knoten als ausgewählt.
|
ohne einen eigenen zu tragen (SPEC §9): die der Beschreibung und die der
|
||||||
|
Fortsetzungen hinter `\`. Steht der Cursor dort, gilt dieser Knoten als
|
||||||
|
ausgewählt.
|
||||||
`fold` ('>'|'<'|null, SPEC §1/D38) ist nur der ANFANGSZUSTAND der Faltung —
|
`fold` ('>'|'<'|null, SPEC §1/D38) ist nur der ANFANGSZUSTAND der Faltung —
|
||||||
den wirksamen Zustand rechnet `initialCollapsed()` in model.js.
|
den wirksamen Zustand rechnet `initialCollapsed()` in model.js.
|
||||||
`deps` sind ID-Strings, keine Knoten-Referenzen — aufgelöst wird erst beim
|
`deps` sind ID-Strings, keine Knoten-Referenzen — aufgelöst wird erst beim
|
||||||
@@ -147,8 +191,11 @@ export function parse(text){
|
|||||||
arr.push(text);
|
arr.push(text);
|
||||||
};
|
};
|
||||||
|
|
||||||
text.split('\n').forEach((raw, i) => {
|
/* Kommentare sind bereits entfernt und Fortsetzungszeilen angehängt
|
||||||
raw = raw.replace(/%%.*$/, ''); /* %%-Kommentare entfernen (Mermaid-Konvention) */
|
(`logicalLines`, SPEC §1 Schritt 1 und 1b). `i` ist ab hier die Nummer der
|
||||||
|
ERSTEN Textzeile einer logischen Zeile, minus eins. */
|
||||||
|
logicalLines(text).forEach(rec => {
|
||||||
|
const raw = rec.raw, i = rec.line - 1;
|
||||||
/* Trenner `---` (SPEC §11, D40): drei oder mehr Bindestriche, umgebender
|
/* Trenner `---` (SPEC §11, D40): drei oder mehr Bindestriche, umgebender
|
||||||
Leerraum erlaubt — ab hier gilt der Beschreibungsteil. Es gibt keinen
|
Leerraum erlaubt — ab hier gilt der Beschreibungsteil. Es gibt keinen
|
||||||
Schlusszaun; weitere Trennzeilen darin haben keine Bedeutung. */
|
Schlusszaun; weitere Trennzeilen darin haben keine Bedeutung. */
|
||||||
@@ -265,6 +312,10 @@ export function parse(text){
|
|||||||
parent.children.push(node);
|
parent.children.push(node);
|
||||||
stack.push({node, width});
|
stack.push({node, width});
|
||||||
lastNode = node;
|
lastNode = node;
|
||||||
|
/* Die Fortsetzungszeilen gehören diesem Knoten (SPEC §1/§9) — genau wie
|
||||||
|
Beschreibungszeilen tragen sie keinen eigenen und wählen ihn deshalb
|
||||||
|
aus, wenn der Cursor darin steht. */
|
||||||
|
rec.cont.forEach(n => ownLine(node, n - 1));
|
||||||
if(id != null && !idNodes.has(id)) idNodes.set(id, node);
|
if(id != null && !idNodes.has(id)) idNodes.set(id, node);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { parse, logicalLines, expandShortIds } from '../src/parser.js';
|
||||||
|
|
||||||
|
/* Fortsetzungszeilen (SPEC §1): Endet eine Zeile auf Leerraum + `\`, gehört
|
||||||
|
die Folgezeile noch dazu — es beginnt kein neuer Knoten. */
|
||||||
|
|
||||||
|
const baum = t => parse(t).roots;
|
||||||
|
const labels = nodes => nodes.map(n => n.label);
|
||||||
|
|
||||||
|
describe('logicalLines — Zeilen zusammenfügen', () => {
|
||||||
|
it('hängt die Folgezeile mit genau einem Leerzeichen an', () => {
|
||||||
|
expect(logicalLines('- Ein sehr langes \\\n Label').map(r => r.raw))
|
||||||
|
.toEqual(['- Ein sehr langes Label']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merkt sich die erste Zeilennummer und die angehängten', () => {
|
||||||
|
const r = logicalLines('a\n- b \\\n c \\\n d\ne');
|
||||||
|
expect(r.map(x => ({line: x.line, cont: x.cont})))
|
||||||
|
.toEqual([{line: 1, cont: []}, {line: 2, cont: [3, 4]}, {line: 5, cont: []}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Von der ersten Zeile bleibt nur die Einrückung übrig — sie trägt die
|
||||||
|
Ebene (§2) und muss stehen bleiben. */
|
||||||
|
it('erhält die Einrückung, wenn die erste Zeile nur aus ihr besteht', () => {
|
||||||
|
expect(logicalLines(' \\\n- Kind').map(r => r.raw)).toEqual([' - Kind']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lässt eine Zeile ohne Leerraum vor dem Backslash in Ruhe', () => {
|
||||||
|
expect(logicalLines('- C:\\temp\\\n- Zweiter').map(r => r.raw))
|
||||||
|
.toEqual(['- C:\\temp\\', '- Zweiter']);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Der Kommentar fällt zuerst weg (Schritt 1) — ein `\` DAHINTER wirkt also
|
||||||
|
nicht, einer davor schon. */
|
||||||
|
it('achtet auf die Reihenfolge mit dem Kommentar', () => {
|
||||||
|
expect(logicalLines('- A %% Notiz \\\n- B').map(r => r.raw)).toEqual(['- A ', '- B']);
|
||||||
|
expect(logicalLines('- A \\ %% Notiz\n- B').map(r => r.raw)).toEqual(['- A - B']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lässt am Dateiende einfach den Backslash weg', () => {
|
||||||
|
expect(logicalLines('- A \\').map(r => r.raw)).toEqual(['- A ']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hängt auch eine leere Folgezeile an, ohne etwas zu verlieren', () => {
|
||||||
|
expect(logicalLines('- A \\\n\n- B').map(r => r.raw)).toEqual(['- A ', '- B']);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Hinter dem Trenner ist der Zeilenumbruch Absatzstruktur (SPEC §1). */
|
||||||
|
it('verbindet im Beschreibungsteil nicht', () => {
|
||||||
|
const r = logicalLines('- A #x\n---\n#x\n erste \\\n zweite');
|
||||||
|
expect(r.map(x => x.raw)).toEqual(['- A #x', '---', '#x', ' erste \\', ' zweite']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parse — die fortgesetzte Zeile ergibt EINEN Knoten', () => {
|
||||||
|
it('macht aus zwei Textzeilen einen Knoten', () => {
|
||||||
|
const r = baum('- Backend mit einem \\\n sehr langen Titel');
|
||||||
|
expect(labels(r)).toEqual(['Backend mit einem sehr langen Titel']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('liest Größe, Tag, ID und URL auch von der Folgezeile', () => {
|
||||||
|
const r = parse('- #api: Backend \\\n (L) @anna https://example.org/x');
|
||||||
|
const n = r.roots[0];
|
||||||
|
expect(n.label).toBe('Backend');
|
||||||
|
expect(n.size).toBe('L');
|
||||||
|
expect(n.tags).toEqual(['anna']);
|
||||||
|
expect(n.id).toBe('api');
|
||||||
|
expect(n.url).toBe('https://example.org/x');
|
||||||
|
expect(r.warnings).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Die Einrückung der ERSTEN Zeile bestimmt die Ebene — die der Folgezeile
|
||||||
|
ist bedeutungslos, auch wenn sie tiefer steht. */
|
||||||
|
it('nimmt die Ebene von der ersten Zeile', () => {
|
||||||
|
const r = baum('Wurzel\n - Kind \\\n Fortsetzung\n - Zweites');
|
||||||
|
expect(labels(r)).toEqual(['Wurzel']);
|
||||||
|
expect(labels(r[0].children)).toEqual(['Kind Fortsetzung', 'Zweites']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('zählt die Zeile der ersten Textzeile', () => {
|
||||||
|
const r = baum('\n\n- Knoten \\\n weiter');
|
||||||
|
expect(r[0].line).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Warnungen nennen die Zeile, an der man eingreift. */
|
||||||
|
it('meldet eine Warnung an der ersten Zeile', () => {
|
||||||
|
const w = parse('- [z] Knoten \\\n weiter').warnings;
|
||||||
|
expect(w).toEqual([{type: 'unknownStatus', line: 1, code: 'z'}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Fortsetzungszeilen tragen keinen eigenen Knoten und wählen deshalb den
|
||||||
|
ihren aus, wenn der Cursor darin steht (SPEC §9, wie Beschreibungen). */
|
||||||
|
it('ordnet die Fortsetzungszeilen ihrem Knoten zu', () => {
|
||||||
|
const n = baum('- Knoten \\\n weiter \\\n und weiter')[0];
|
||||||
|
expect(n.descLines).toEqual([2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setzt eine Zeile über mehrere Umbrüche fort', () => {
|
||||||
|
expect(labels(baum('- a \\\n b \\\n c \\\n d'))).toEqual(['a b c d']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lässt einen Backslash ohne Leerraum davor im Label stehen', () => {
|
||||||
|
expect(labels(baum('- C:\\temp\\\n- Zweiter'))).toEqual(['C:\\temp\\', 'Zweiter']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fügt kein Token über den Umbruch zusammen', () => {
|
||||||
|
/* Verbunden wird mit einem Leerzeichen — eine zerschnittene URL bleibt
|
||||||
|
zerschnitten, und das ist die ehrliche Auskunft. */
|
||||||
|
expect(parse('- Titel https://exa \\\n mple.org').roots[0].url).toBe('https://exa');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('verbindet eine Beschreibungszeile ebenso', () => {
|
||||||
|
const n = parse('- Knoten\n " erster Teil \\\n zweiter Teil').roots[0];
|
||||||
|
expect(n.desc).toBe('erster Teil zweiter Teil');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('expandShortIds — Fortsetzungszeilen sind keine Knotenzeilen', () => {
|
||||||
|
it('nimmt eine Fortsetzung nicht in den Vorfahren-Stapel auf', () => {
|
||||||
|
const text = ['#a: A', ' - Zwischen \\', ' weiter', ' - #.c: C'].join('\n');
|
||||||
|
expect(expandShortIds(text)).toContain('#a.c: C');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fasst eine Kurzform in einer Fortsetzungszeile nicht an', () => {
|
||||||
|
const text = ['#a: A', ' - Kind \\', ' #.x bleibt'].join('\n');
|
||||||
|
expect(expandShortIds(text)).toBe(text);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user