notation: !!! als Fokusmarke — ein geteilter Zeigefinger im Text
Beim gemeinsamen Arbeiten an einem Pad (D31) fehlte das Naheliegendste: „schau mal hier". Etherpad zeigt die Cursor der anderen im Pad, aber Werkbaum kommt nicht an sie heran — ein fremdstämmiger Rahmen gibt keinen DOM-Zugriff, der Klartext-Export überträgt nur Text. Eine Marke IM Text ist damit nicht der Notbehelf, sondern der einzige Weg. Und sie hat etwas, das ein Cursor nicht hat: alle sehen dieselbe Stelle. Erkannt wird `!!!` nur ALLEINSTEHEND (`(^|\s)!!!(?=\s|$)`), sonst verlöre jedes „Achtung!!!" seine Ausrufezeichen und niemand fände den Grund. Bewusst ohne Lookbehind — Safari kennt es erst ab 16.4, und die Notation soll nicht an einer Browserversion hängen. Position in der Extraktion (SPEC §1): nach den Tags; der Kommentar fällt vorher weg, eine Marke hinter `%%` wirkt also nicht. Darstellung ist DIESELBE wie die der Cursor-Zeile (D25), nicht eine vierte: neben Fokusrahmen, Strahlenkranz (D28) und Cursor-Ring wäre ein weiterer Ring ein Zeichen zu viel — es gibt nur einen Begriff „hier schauen". Preis: Im Bild ist nicht zu unterscheiden, ob die eigene Cursor-Zeile oder eine fremde Marke leuchtet; für Screenreader benennt `a11yFocusMark` es ausdrücklich. Ins Bild geholt wird nur bei ÄNDERUNG der Marke, Schlüssel ist der Label-Text statt der Zeilennummer (Umsortieren im Pad soll nicht neu scrollen). Ohne diese Bedingung zöge jeder Neubau den Blick zurück und man käme nicht weg. Beides geprüft: gleiche Marke -> Scrollstand bleibt 0, Marke gewandert -> springt auf 242 und markiert den neuen Knoten. Beim Hinsehen gefunden: Die Pfad-Inversion verschluckte den Zeigefinger. Der erste markierte Knoten im Test war eine nicht gewählte any-of-Alternative („Headless CMS") — `opacity:.32; saturate(.4)`, der Ring praktisch unsichtbar. Der Zeiger zeigte auf nichts. Dieselbe Ausnahme wie für D28 behebt es, und hier ist sie zwingender: „Sollten wir diese verworfene Alternative noch einmal ansehen?" ist einer der wahrscheinlichsten Gründe, überhaupt zu zeigen. Anders als beim optionalen Knoten (D29), wo das Zurücktreten die Aussage IST. Nicht im Druck und nicht im Grafikexport (wie die Cursor-Zeile): Die Marke sagt „schau jetzt hierhin", nicht „so ist der Plan". Nicht ins kanonische Beispiel (SPEC §10) aufgenommen — das ist zugleich Test-Fixture, ein dauerhafter Zeigefinger darin wäre eine Aussage, die niemand gemacht hat. Verworfen war `%%!` (Kommentar-Variante, bliebe harmlos liegen und könnte die Begründung tragen) — der Nutzer wählte `!!!`, weil es im Moment des Zeigens schnell zu tippen ist, und genau dann will man nicht über Syntax nachdenken. Der Preis steht in D32: die Marke bleibt sichtbar stehen, bis jemand sie löscht; Werkbaum kann sie nicht entfernen (kein Schreibzugriff auf das Pad). SPEC §1/§9 zuerst, dann D32, dann Code. 15 neue Tests (86 -> 101), Legende und i18n in allen 9 Sprachen.
This commit is contained in:
+18
-1
@@ -121,8 +121,25 @@ Getestet gegen Etherpad: Der Klartext-Export sendet
|
|||||||
zurück — führende Leerzeichen, `-`/`+`/`|`, Statusboxen und `%%` überleben
|
zurück — führende Leerzeichen, `-`/`+`/`|`, Statusboxen und `%%` überleben
|
||||||
Etherpads Speichermodell, das `-` wird nicht zur Aufzählung umgedeutet.
|
Etherpads Speichermodell, das `-` wird nicht zur Aufzählung umgedeutet.
|
||||||
|
|
||||||
|
Das Pad lässt sich auch **im Editor-Panel einbetten**: Ein Wähler in der
|
||||||
|
Titelzeile schaltet reihum zwischen *Pad und Text* (durch einen Splitter frei
|
||||||
|
geteilt), *nur Pad* und *nur Text*. Der schmal gezogene Textspiegel behält seinen
|
||||||
|
Zweck — der Sprung zwischen Diagramm und Text arbeitet auf ihm, und in *nur Pad*
|
||||||
|
holt ein Sprung ihn selbst zurück. Der Rahmen wird nur geladen, solange er
|
||||||
|
sichtbar ist, denn ein geladenes Pad verbindet sich und zeigt dich in dessen
|
||||||
|
Anwesenden-Liste.
|
||||||
|
|
||||||
|
**Der gemeinsame Zeigefinger:** Schreib `!!!` in eine Zeile, und dieser Knoten
|
||||||
|
wird hervorgehoben und ins Bild geholt — bei **allen**, die auf das Pad schauen.
|
||||||
|
Das kann ein Cursor nicht. Erkannt nur als alleinstehendes Token, `Achtung!!!`
|
||||||
|
bleibt also ein gewöhnliches Label. Die Marke bleibt stehen, bis jemand sie
|
||||||
|
löscht.
|
||||||
|
|
||||||
**Bedenke:** Der Plantext liegt damit auf fremder Infrastruktur, und ein Pad ist
|
**Bedenke:** Der Plantext liegt damit auf fremder Infrastruktur, und ein Pad ist
|
||||||
für jeden lesbar, der die Adresse kennt. Siehe `docs/DECISIONS.md` D31.
|
für jeden lesbar, der die Adresse kennt. Im eingebetteten Rahmen wird Etherpads
|
||||||
|
Autoren-Cookie (`SameSite=Lax`) nicht mitgesendet — man gilt bei jedem Laden als
|
||||||
|
neuer Autor, was nur serverseitig zu beheben ist (`cookie.sameSite: "None"`).
|
||||||
|
Siehe `docs/DECISIONS.md` D31 und D32.
|
||||||
|
|
||||||
### Lokal ausführen
|
### Lokal ausführen
|
||||||
|
|
||||||
|
|||||||
@@ -116,8 +116,23 @@ Verified against Etherpad: the plain-text export sends
|
|||||||
leading spaces, `-`/`+`/`|`, status boxes and `%%` survive Etherpad's storage
|
leading spaces, `-`/`+`/`|`, status boxes and `%%` survive Etherpad's storage
|
||||||
model, and `-` is not turned into a bullet list.
|
model, and `-` is not turned into a bullet list.
|
||||||
|
|
||||||
|
The pad can also be **embedded** in the editor panel: a selector in the title bar
|
||||||
|
cycles through *pad and text* (split by a draggable divider), *pad only* and *text
|
||||||
|
only*. The narrow text mirror keeps its purpose — the jump between diagram and
|
||||||
|
text works on it, and in *pad only* a jump brings it back by itself. The frame is
|
||||||
|
only loaded while it is visible, because a loaded pad connects and shows you in
|
||||||
|
the pad's list of people.
|
||||||
|
|
||||||
|
**A shared pointer:** write `!!!` on a line and that node is highlighted and
|
||||||
|
scrolled into view — for **everyone** looking at the pad, which is something a
|
||||||
|
cursor cannot do. Recognised only as a standalone token, so `Careful!!!` stays an
|
||||||
|
ordinary label. It stays in the text until someone deletes it.
|
||||||
|
|
||||||
**Be aware:** your plan text then lives on third-party infrastructure, and a pad
|
**Be aware:** your plan text then lives on third-party infrastructure, and a pad
|
||||||
is readable by anyone who knows its address. See `docs/DECISIONS.md` D31.
|
is readable by anyone who knows its address. In an embedded frame Etherpad's
|
||||||
|
author cookie (`SameSite=Lax`) is not sent, so you count as a new author on every
|
||||||
|
load — fixable only on the server (`cookie.sameSite: "None"`). See
|
||||||
|
`docs/DECISIONS.md` D31 and D32.
|
||||||
|
|
||||||
### Running it locally
|
### Running it locally
|
||||||
|
|
||||||
|
|||||||
@@ -1164,3 +1164,77 @@ Name und Farbe halten nicht. Reparieren lässt sich das nur **serverseitig**
|
|||||||
Auf kleinem Bildschirm wurde „beide" nachgemessen statt geschätzt: bei 375 × 812
|
Auf kleinem Bildschirm wurde „beide" nachgemessen statt geschätzt: bei 375 × 812
|
||||||
bleiben Pad 317 px und Spiegel 180 px, per Splitter verschiebbar — knapp, aber
|
bleiben Pad 317 px und Spiegel 180 px, per Splitter verschiebbar — knapp, aber
|
||||||
brauchbar. Eine Sonderregel für Mobil braucht es deshalb nicht.
|
brauchbar. Eine Sonderregel für Mobil braucht es deshalb nicht.
|
||||||
|
|
||||||
|
## D32 — `!!!` als Fokusmarke: ein geteilter Zeigefinger im Text
|
||||||
|
Beim gemeinsamen Arbeiten an einem Pad (D31) fehlt das Naheliegendste: „schau
|
||||||
|
mal hier". Etherpad zeigt die Cursor der anderen im **Pad**, aber Werkbaum
|
||||||
|
kommt nicht an sie heran — ein fremdstämmiger Rahmen gibt keinen DOM-Zugriff,
|
||||||
|
und der Klartext-Export überträgt ohnehin nur Text. Eine Marke **im Text** ist
|
||||||
|
damit nicht der Notbehelf, sondern der einzige Weg.
|
||||||
|
|
||||||
|
Sie hat sogar etwas, das ein Cursor nicht hat: **alle sehen dieselbe Stelle.**
|
||||||
|
Ein Cursor ist privat, `!!!` ist eine Aussage im gemeinsamen Dokument.
|
||||||
|
|
||||||
|
**Syntax `!!!`.** Vorgeschlagen und **verworfen** war `%%!` — eine
|
||||||
|
Kommentar-Variante (`- [ ] Backend %%! schau mal hier`). Deren Vorzug: Bleibt die
|
||||||
|
Marke liegen, ist sie harmlos, denn `%%` ist längst ein Kommentar (SPEC §8);
|
||||||
|
jedes andere Werkzeug und jeder Leser überliest sie, und der Resttext hätte die
|
||||||
|
Begründung tragen können. Der Nutzer hat `!!!` gewählt, „am einfachsten schnell
|
||||||
|
mal zu tippen" — und das ist das entscheidende Kriterium, weil der Moment des
|
||||||
|
Zeigens genau der Moment ist, in dem man **nicht** über Syntax nachdenken will.
|
||||||
|
Bewusst in Kauf genommen: Die Marke bleibt sichtbar im Plan stehen, bis jemand
|
||||||
|
sie löscht (Werkbaum kann sie nicht entfernen — auf das Pad gibt es keinen
|
||||||
|
Schreibzugriff), und in einer committeten Datei sieht sie nach einer Aussage über
|
||||||
|
den Knoten aus. Ebenfalls verworfen: `@@` als Pseudo-Tag — `@` bedeutet in dieser
|
||||||
|
Notation „zuständig" (SPEC §7), ein `@@` daneben lädt zur Verwechslung ein.
|
||||||
|
|
||||||
|
**Nur alleinstehend.** Erkannt wird `!!!` am Zeilenanfang/-ende oder von Leerraum
|
||||||
|
umgeben. Ohne diese Einschränkung verlöre jedes `Achtung!!!` seine
|
||||||
|
Ausrufezeichen — und niemand fände den Grund, weil die Zeile im Diagramm nur
|
||||||
|
stumm anders aussieht. `!!` und `!!!!` zählen nicht, `!!!` innerhalb einer URL
|
||||||
|
bleibt Teil der URL. Regex bewusst **ohne Lookbehind** (`(^|\s)!!!(?=\s|$)`,
|
||||||
|
führender Leerraum wird mitgefangen und wieder eingesetzt): Safari kennt
|
||||||
|
Lookbehind erst ab 16.4, und die Notation soll nicht an einer Browserversion
|
||||||
|
hängen.
|
||||||
|
|
||||||
|
Position in der Extraktionsreihenfolge (SPEC §1): **nach** den Tags, vor dem
|
||||||
|
Label. Kollisionsfrei, weil `!!!` weder `@` noch Klammern noch `:` enthält. Der
|
||||||
|
Kommentar fällt als Erstes weg — eine Marke **im** Kommentar wirkt deshalb nicht,
|
||||||
|
was richtig ist: Was hinter `%%` steht, ist Beiwerk.
|
||||||
|
|
||||||
|
**Darstellung: dieselbe wie die Cursor-Zeile** (weißer Halo + Ring in Tinte,
|
||||||
|
D25), plus Scrollen ins Bild. Erwogen war ein eigenes Aussehen (Petrol-Ring mit
|
||||||
|
Sprechblasen-Spitze); der Nutzer entschied für die vorhandene Hervorhebung. Das
|
||||||
|
ist die sparsamere Wahl: Es gibt nur **einen** Begriff „hier schauen", und neben
|
||||||
|
Fokusrahmen (Petrol), Strahlenkranz (gelb, D28) und Cursor-Ring wäre ein vierter
|
||||||
|
Ring ein Zeichen zu viel. **Preis:** Im Bild ist nicht zu unterscheiden, ob ein
|
||||||
|
Knoten wegen der eigenen Cursor-Zeile oder wegen einer fremden Marke leuchtet.
|
||||||
|
Für Screenreader wird der Unterschied benannt (`a11yFocusMark`, „hierhin
|
||||||
|
schauen"), weil ein `box-shadow` dort ohnehin nicht ankommt.
|
||||||
|
|
||||||
|
**Ins Bild geholt wird nur bei Änderung.** Schlüssel ist der **Label-Text** des
|
||||||
|
markierten Knotens, nicht die Zeilennummer: Umsortieren im Pad soll nicht als
|
||||||
|
neue Marke gelten. Ohne diese Bedingung zöge jeder Neubau des Baums den Blick
|
||||||
|
zurück — man könnte nicht wegscrollen, und bei einem Pad-Dokument wird oft neu
|
||||||
|
gebaut.
|
||||||
|
|
||||||
|
**Beim Hinsehen gefunden: die Pfad-Inversion verschluckte den Zeigefinger.**
|
||||||
|
`.cheap-on .node:not(.cheap)` setzt `opacity:.32; filter:saturate(.4)` — und die
|
||||||
|
erste markierte Alternative im Test war genau ein nicht gewählter any-of-Zweig
|
||||||
|
(„Headless CMS"). Gemessen: Deckkraft 0,32, der Ring praktisch unsichtbar. Der
|
||||||
|
Zeiger zeigte auf nichts. Dieselbe Ausnahme wie für „neu in Produktion" (D28)
|
||||||
|
behebt es, und hier ist sie noch zwingender: „Sollten wir diese verworfene
|
||||||
|
Alternative noch einmal ansehen?" ist einer der wahrscheinlichsten Gründe,
|
||||||
|
überhaupt zu zeigen. Anders als beim optionalen Knoten (D29), wo das
|
||||||
|
Zurücktreten die Aussage **ist** — dort bleibt die Schwäche bewusst stehen.
|
||||||
|
|
||||||
|
**Nicht im Druck, nicht im Grafikexport** — wie die Cursor-Zeile. Die Marke sagt
|
||||||
|
„schau jetzt hierhin", nicht „so ist der Plan"; ein Export wäre für jeden
|
||||||
|
Betrachter etwas anderes. Der Export erbt das ohnehin, weil `diagramToSvg()` nie
|
||||||
|
`box-shadow` ausliest (D25).
|
||||||
|
|
||||||
|
**Dritte, unabhängige Achse.** Die Marke sagt nichts über Fortschritt (§4) oder
|
||||||
|
Notwendigkeit (§3) — test-abgedeckt, damit niemand später Status oder `optional`
|
||||||
|
daran koppelt. **Nicht** in das kanonische Beispiel (SPEC §10) aufgenommen: Das
|
||||||
|
ist zugleich Test-Fixture, und ein dauerhafter Zeigefinger darin wäre eine
|
||||||
|
Aussage, die niemand gemacht hat.
|
||||||
|
|||||||
+41
-2
@@ -7,7 +7,7 @@ Syntaxänderungen werden zuerst hier dokumentiert, dann implementiert.
|
|||||||
## 1. Zeilenformat
|
## 1. Zeilenformat
|
||||||
|
|
||||||
```
|
```
|
||||||
[Einrückung][Zeichen] [Statusbox] Label (Größe) URL @tag … %% Kommentar
|
[Einrückung][Zeichen] [Statusbox] Label (Größe) URL @tag … !!! %% Kommentar
|
||||||
```
|
```
|
||||||
|
|
||||||
Alle Bestandteile außer dem Label sind optional. Die Extraktion erfolgt in
|
Alle Bestandteile außer dem Label sind optional. Die Extraktion erfolgt in
|
||||||
@@ -18,7 +18,25 @@ dieser Reihenfolge (wichtig für Kollisionsfreiheit):
|
|||||||
3. URL: erstes Token, das auf `https?://\S+` passt (dadurch stören `@` in URLs nicht).
|
3. URL: erstes Token, das auf `https?://\S+` passt (dadurch stören `@` in URLs nicht).
|
||||||
4. Größe: erstes `(XS|S|M|L|XL|XXL)`, Groß-/Kleinschreibung egal.
|
4. Größe: erstes `(XS|S|M|L|XL|XXL)`, Groß-/Kleinschreibung egal.
|
||||||
5. Tags: alle `@name`-Vorkommen.
|
5. Tags: alle `@name`-Vorkommen.
|
||||||
6. Rest, whitespace-normalisiert = Label. Leeres Label ⇒ Zeile ignorieren.
|
6. Fokusmarke: `!!!` als **alleinstehendes** Token (siehe unten).
|
||||||
|
7. Rest, whitespace-normalisiert = Label. Leeres Label ⇒ Zeile ignorieren.
|
||||||
|
|
||||||
|
**Fokusmarke `!!!`** — „schau hier hin": Der Knoten wird im Diagramm
|
||||||
|
hervorgehoben und ins Bild geholt (§9). Gedacht für das gemeinsame Arbeiten an
|
||||||
|
einem Pad (§9, `?etherpad=`): Weil dort niemand den Cursor der anderen sieht,
|
||||||
|
ist eine Marke **im Text** der einzige Weg, auf eine Stelle zu zeigen — und sie
|
||||||
|
hat etwas, das ein Cursor nicht hat: **alle** sehen dieselbe Stelle.
|
||||||
|
|
||||||
|
- Erkannt nur **alleinstehend**, also am Zeilenanfang/-ende oder von Leerraum
|
||||||
|
umgeben. `Achtung!!!` bleibt damit ein gewöhnliches Label; auch `!!!`
|
||||||
|
innerhalb einer URL bleibt Teil der URL.
|
||||||
|
- Die Marke gehört **nicht** zum Label und ist an jeder Position der Zeile
|
||||||
|
zulässig. Mehrere Marken sind erlaubt: alle markierten Knoten werden
|
||||||
|
hervorgehoben, ins Bild geholt wird der **erste**.
|
||||||
|
- Sie sagt nichts über Fortschritt (§4) oder Notwendigkeit (§3) — eine dritte,
|
||||||
|
unabhängige Achse.
|
||||||
|
- Sie bleibt im Text stehen, bis jemand sie löscht; ein Werkzeug entfernt sie
|
||||||
|
nicht von selbst.
|
||||||
|
|
||||||
Referenz-Regex der Implementierung:
|
Referenz-Regex der Implementierung:
|
||||||
|
|
||||||
@@ -26,6 +44,12 @@ Referenz-Regex der Implementierung:
|
|||||||
^([ \t]*)([-|+])?\s*(?:\[([ ?~xX^/-])\]\s*)?(.*)$
|
^([ \t]*)([-|+])?\s*(?:\[([ ?~xX^/-])\]\s*)?(.*)$
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Für die Fokusmarke (Schritt 6):
|
||||||
|
|
||||||
|
```
|
||||||
|
(^|\s)!!!(?=\s|$)
|
||||||
|
```
|
||||||
|
|
||||||
## 2. Hierarchie
|
## 2. Hierarchie
|
||||||
|
|
||||||
- Die Einrückung bestimmt die Ebene. Es gibt keine feste Schrittweite:
|
- Die Einrückung bestimmt die Ebene. Es gibt keine feste Schrittweite:
|
||||||
@@ -372,6 +396,21 @@ verknüpft (siehe D25):
|
|||||||
- Die Hervorhebung ist eine reine Editierhilfe: nicht im Grafikexport, nicht im
|
- Die Hervorhebung ist eine reine Editierhilfe: nicht im Grafikexport, nicht im
|
||||||
Druck.
|
Druck.
|
||||||
|
|
||||||
|
### Fokusmarke im Diagramm (`!!!`, §1)
|
||||||
|
Ein mit `!!!` markierter Knoten trägt **dieselbe Hervorhebung wie die
|
||||||
|
Cursor-Zeile** (weißer Halo + Ring in Tinte) und wird ins Bild geholt — bewusst
|
||||||
|
kein eigenes Aussehen: Es gibt nur einen Begriff „hier schauen", und ein
|
||||||
|
dritter Ring neben Fokusrahmen, Strahlenkranz (§9) und Cursor-Ring wäre ein
|
||||||
|
Zeichen zu viel.
|
||||||
|
|
||||||
|
- Ins Bild geholt wird nur, wenn sich die Marke **ändert** — sonst zöge das
|
||||||
|
Diagramm bei jedem Neubau den Blick zurück und man könnte nicht wegscrollen.
|
||||||
|
- Weil `!!!` im Text steht, sehen **alle** dieselbe Stelle. Der Unterschied zur
|
||||||
|
eigenen Cursor-Zeile ist im Bild nicht zu erkennen; für Screenreader benennt
|
||||||
|
das `aria-label` die Marke ausdrücklich.
|
||||||
|
- Wie die Cursor-Zeile erscheint sie **nicht** im Druck und **nicht** im
|
||||||
|
Grafikexport: Sie sagt „schau jetzt hierhin", nicht „so ist der Plan".
|
||||||
|
|
||||||
### Grafikexport des Diagramms
|
### Grafikexport des Diagramms
|
||||||
Das Diagramm wird aus der Live-Geometrie in ein eigenständiges SVG (nur Formen
|
Das Diagramm wird aus der Live-Geometrie in ein eigenständiges SVG (nur Formen
|
||||||
+ Text, keine externen Ressourcen) nachgezeichnet. Zwei Icon-Schaltflächen:
|
+ Text, keine externen Ressourcen) nachgezeichnet. Zwei Icon-Schaltflächen:
|
||||||
|
|||||||
@@ -68,6 +68,7 @@
|
|||||||
- [x] Embed the pad, keep the text mirror beside it (M)
|
- [x] Embed the pad, keep the text mirror beside it (M)
|
||||||
- [x] Three views: pad and text, pad only, text only (S)
|
- [x] Three views: pad and text, pad only, text only (S)
|
||||||
- [x] Splitter between pad and mirror (XS)
|
- [x] Splitter between pad and mirror (XS)
|
||||||
|
- [x] A shared pointer: !!! focuses a node for everyone (XS)
|
||||||
- [ ] Git as the shared store (L)
|
- [ ] Git as the shared store (L)
|
||||||
| [ ] A file in a repository, changed by pull request (S) %% works today, no code
|
| [ ] A file in a repository, changed by pull request (S) %% works today, no code
|
||||||
| [?] The backend commits every change (L)
|
| [?] The backend commits every change (L)
|
||||||
|
|||||||
@@ -170,6 +170,19 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
|
|||||||
Nichts. Das Markup: `#srcArea` umschließt Rahmen + Splitter + `#src`, damit die
|
Nichts. Das Markup: `#srcArea` umschließt Rahmen + Splitter + `#src`, damit die
|
||||||
Legenden-Aufteilung unberührt bleibt — `.editor-body textarea` ist ein
|
Legenden-Aufteilung unberührt bleibt — `.editor-body textarea` ist ein
|
||||||
Nachfahren-Selektor und greift weiter.
|
Nachfahren-Selektor und greift weiter.
|
||||||
|
- Fokusmarke `!!!` (D32, SPEC §1): Parser setzt `focus`, Renderer die Klasse
|
||||||
|
`focusmark`, CSS teilt die Regel mit `.node.current` (ein Begriff „hier
|
||||||
|
schauen"). Erkannt **nur alleinstehend** — `(^|\s)!!!(?=\s|$)`, bewusst ohne
|
||||||
|
Lookbehind (Safari erst ab 16.4), sonst verlöre `Achtung!!!` seine
|
||||||
|
Ausrufezeichen. Extraktion **nach** den Tags; der Kommentar fällt vorher weg,
|
||||||
|
eine Marke hinter `%%` wirkt also nicht. `revealFocusMark()` scrollt **nur bei
|
||||||
|
Änderung** (Schlüssel = Label-Text, nicht Zeilennummer — Umsortieren im Pad soll
|
||||||
|
nicht neu scrollen), sonst zöge jeder Neubau den Blick zurück. Die Regel braucht
|
||||||
|
den `#out`-Präfix wie `.current` (`ul.or .node{box-shadow:none}` ist
|
||||||
|
spezifischer) **und** die Ausnahme
|
||||||
|
`.cheap-on .node:not(.cheap).focusmark{opacity:1;filter:none}` — sonst blasst die
|
||||||
|
Pfad-Inversion genau den Knoten aus, auf den gezeigt wird (bei einer nicht
|
||||||
|
gewählten Alternative gemessen: Deckkraft 0,32).
|
||||||
- Sprung Diagramm ↔ Text (D25): `render.js` schreibt die Parser-Zeilennummer als
|
- Sprung Diagramm ↔ Text (D25): `render.js` schreibt die Parser-Zeilennummer als
|
||||||
`data-line` an jeden Knoten (Geister-Knoten bekommen keine). `jumpToLine()` in
|
`data-line` an jeden Knoten (Geister-Knoten bekommen keine). `jumpToLine()` in
|
||||||
`app.js` klappt bei Bedarf das Editor-Panel auf (`revealEditor()`), markiert die
|
`app.js` klappt bei Bedarf das Editor-Panel auf (`revealEditor()`), markiert die
|
||||||
|
|||||||
@@ -96,6 +96,7 @@
|
|||||||
Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.<br>
|
Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.<br>
|
||||||
<code>-</code> Teilpaket, alle erforderlich <em>(all of)</em><br>
|
<code>-</code> Teilpaket, alle erforderlich <em>(all of)</em><br>
|
||||||
<code>+</code> Zugabe, nicht erforderlich <em>(optional)</em><br>
|
<code>+</code> Zugabe, nicht erforderlich <em>(optional)</em><br>
|
||||||
|
<code>!!!</code> hierhin schauen (gemeinsamer Zeigefinger)<br>
|
||||||
<code class="or-code">|</code> Alternative, eine wählen <em>(any of)</em><br>
|
<code class="or-code">|</code> Alternative, eine wählen <em>(any of)</em><br>
|
||||||
Zeile ohne Zeichen = Wurzelknoten. <code class="or-code">|</code> nicht mit <code>-</code>/<code>+</code> mischen.<br>
|
Zeile ohne Zeichen = Wurzelknoten. <code class="or-code">|</code> nicht mit <code>-</code>/<code>+</code> mischen.<br>
|
||||||
Status als Kästchen nach dem Zeichen, z. B. <code>- [~] Frontend</code>:
|
Status als Kästchen nach dem Zeichen, z. B. <code>- [~] Frontend</code>:
|
||||||
|
|||||||
+36
-9
@@ -96,9 +96,26 @@ function render(){
|
|||||||
/* Der Baum ist neu gebaut — die Markierung der Cursor-Zeile neu setzen (D25).
|
/* Der Baum ist neu gebaut — die Markierung der Cursor-Zeile neu setzen (D25).
|
||||||
Ohne Scrollen: beim Tippen soll das Diagramm stehen bleiben. */
|
Ohne Scrollen: beim Tippen soll das Diagramm stehen bleiben. */
|
||||||
highlightCurrentNode(false);
|
highlightCurrentNode(false);
|
||||||
|
revealFocusMark(); /* `!!!` ins Bild holen, wenn die Marke neu ist (SPEC §1) */
|
||||||
updateFreshBtn(); /* Zähler folgt der gerade gerenderten Menge (D28) */
|
updateFreshBtn(); /* Zähler folgt der gerade gerenderten Menge (D28) */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fokusmarke `!!!` (SPEC §1): Der erste markierte Knoten wird ins Bild geholt —
|
||||||
|
aber nur, wenn sich die Marke **geändert** hat. Sonst zöge jeder Neubau des
|
||||||
|
Baums den Blick zurück und man käme nicht weg. Verglichen wird das Label des
|
||||||
|
Knotens, nicht die Zeilennummer: Umsortieren im Pad soll nicht als neue Marke
|
||||||
|
gelten. Die Hervorhebung selbst macht die Klasse `focusmark` aus dem Renderer,
|
||||||
|
hier geht es nur ums Scrollen. */
|
||||||
|
let lastFocusMark = null;
|
||||||
|
function revealFocusMark(){
|
||||||
|
const el = out.querySelector('.node.focusmark');
|
||||||
|
const key = el ? el.textContent : null;
|
||||||
|
if(key && key !== lastFocusMark){
|
||||||
|
el.scrollIntoView({block:'center', inline:'center', behavior:'smooth'});
|
||||||
|
}
|
||||||
|
lastFocusMark = key;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Günstigster-Pfad-Linie ----------
|
/* ---------- Günstigster-Pfad-Linie ----------
|
||||||
Eine gestrichelte, geschwungene Petrol-Linie fädelt durch die Endknoten
|
Eine gestrichelte, geschwungene Petrol-Linie fädelt durch die Endknoten
|
||||||
(Blätter) des günstigen Pfads. Das Overlay-SVG liegt in #out und erbt damit
|
(Blätter) des günstigen Pfads. Das Overlay-SVG liegt in #out und erbt damit
|
||||||
@@ -948,10 +965,11 @@ const I18N = {
|
|||||||
unknownStatusWarn:"Zeile {line}: unbekanntes Statuszeichen „{code}“ — als neutral dargestellt.",
|
unknownStatusWarn:"Zeile {line}: unbekanntes Statuszeichen „{code}“ — als neutral dargestellt.",
|
||||||
sourceLoadWarn:"„{url}“ konnte nicht geladen werden ({error}). Die Datei muss per http(s) erreichbar sein und CORS erlauben (Access-Control-Allow-Origin).",
|
sourceLoadWarn:"„{url}“ konnte nicht geladen werden ({error}). Die Datei muss per http(s) erreichbar sein und CORS erlauben (Access-Control-Allow-Origin).",
|
||||||
sourceTimeoutWarn:"„{url}“ hat innerhalb von {seconds} s nicht geantwortet — der Abruf wurde abgebrochen. Etherpad begrenzt, wie oft der Export geholt werden darf (serienmäßig 10-mal pro 90 s); warte einen Moment und lade dann erneut.",
|
sourceTimeoutWarn:"„{url}“ hat innerhalb von {seconds} s nicht geantwortet — der Abruf wurde abgebrochen. Etherpad begrenzt, wie oft der Export geholt werden darf (serienmäßig 10-mal pro 90 s); warte einen Moment und lade dann erneut.",
|
||||||
a11yStatus:"Status: {status}", a11ySize:"Aufwand: {size}", a11ySizeImplicit:"Aufwand: M (angenommen)", a11yTags:"Zuständig: {names}", a11yOptional:"optional", a11yLink:"verlinkt",
|
a11yStatus:"Status: {status}", a11ySize:"Aufwand: {size}", a11ySizeImplicit:"Aufwand: M (angenommen)", a11yTags:"Zuständig: {names}", a11yOptional:"optional", a11yFocusMark:"hierhin schauen", a11yLink:"verlinkt",
|
||||||
hint_indent:"Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.",
|
hint_indent:"Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.",
|
||||||
hint_all:"Teilpaket, alle erforderlich", hint_any:"Alternative, eine wählen",
|
hint_all:"Teilpaket, alle erforderlich", hint_any:"Alternative, eine wählen",
|
||||||
hint_opt:"Zugabe, nicht erforderlich",
|
hint_opt:"Zugabe, nicht erforderlich",
|
||||||
|
hint_focus:"hierhin schauen (gemeinsamer Zeigefinger)",
|
||||||
hint_root:"Zeile ohne Zeichen = Wurzelknoten. | nicht mit - / + mischen.",
|
hint_root:"Zeile ohne Zeichen = Wurzelknoten. | nicht mit - / + mischen.",
|
||||||
hint_status:"Status als Kästchen nach dem Zeichen, z. B.",
|
hint_status:"Status als Kästchen nach dem Zeichen, z. B.",
|
||||||
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:",
|
||||||
@@ -1011,10 +1029,11 @@ const I18N = {
|
|||||||
unknownStatusWarn:"Line {line}: unknown status code “{code}” — shown as neutral.",
|
unknownStatusWarn:"Line {line}: unknown status code “{code}” — shown as neutral.",
|
||||||
sourceLoadWarn:"Could not load “{url}” ({error}). The file must be reachable via http(s) and allow CORS (Access-Control-Allow-Origin).",
|
sourceLoadWarn:"Could not load “{url}” ({error}). The file must be reachable via http(s) and allow CORS (Access-Control-Allow-Origin).",
|
||||||
sourceTimeoutWarn:"“{url}” did not answer within {seconds} s — the request was aborted. Etherpad limits how often the export may be fetched (10 times per 90 s by default); wait a moment, then reload.",
|
sourceTimeoutWarn:"“{url}” did not answer within {seconds} s — the request was aborted. Etherpad limits how often the export may be fetched (10 times per 90 s by default); wait a moment, then reload.",
|
||||||
a11yStatus:"Status: {status}", a11ySize:"Effort: {size}", a11ySizeImplicit:"Effort: M (assumed)", a11yTags:"Assigned: {names}", a11yOptional:"optional", a11yLink:"has link",
|
a11yStatus:"Status: {status}", a11ySize:"Effort: {size}", a11ySizeImplicit:"Effort: M (assumed)", a11yTags:"Assigned: {names}", a11yOptional:"optional", a11yFocusMark:"look here", a11yLink:"has link",
|
||||||
hint_indent:"Indentation (2 spaces or a tab) defines the hierarchy.",
|
hint_indent:"Indentation (2 spaces or a tab) defines the hierarchy.",
|
||||||
hint_all:"sub-task, all required", hint_any:"alternative, choose one",
|
hint_all:"sub-task, all required", hint_any:"alternative, choose one",
|
||||||
hint_opt:"extra, not required",
|
hint_opt:"extra, not required",
|
||||||
|
hint_focus:"look here (a shared pointer)",
|
||||||
hint_root:"Line without a marker = root node. Do not mix | with - / +.",
|
hint_root:"Line without a marker = root node. Do not mix | with - / +.",
|
||||||
hint_status:"Status as a checkbox after the marker, e.g.",
|
hint_status:"Status as a checkbox after the marker, e.g.",
|
||||||
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:",
|
||||||
@@ -1074,10 +1093,11 @@ const I18N = {
|
|||||||
unknownStatusWarn:"Línea {line}: código de estado desconocido «{code}» — mostrado como neutral.",
|
unknownStatusWarn:"Línea {line}: código de estado desconocido «{code}» — mostrado como neutral.",
|
||||||
sourceLoadWarn:"No se pudo cargar «{url}» ({error}). El archivo debe ser accesible por http(s) y permitir CORS (Access-Control-Allow-Origin).",
|
sourceLoadWarn:"No se pudo cargar «{url}» ({error}). El archivo debe ser accesible por http(s) y permitir CORS (Access-Control-Allow-Origin).",
|
||||||
sourceTimeoutWarn:"«{url}» no respondió en {seconds} s — se canceló la petición. Etherpad limita la frecuencia de descarga del export (10 veces por 90 s de forma predeterminada); espera un momento y vuelve a cargar.",
|
sourceTimeoutWarn:"«{url}» no respondió en {seconds} s — se canceló la petición. Etherpad limita la frecuencia de descarga del export (10 veces por 90 s de forma predeterminada); espera un momento y vuelve a cargar.",
|
||||||
a11yStatus:"Estado: {status}", a11ySize:"Esfuerzo: {size}", a11ySizeImplicit:"Esfuerzo: M (asumido)", a11yTags:"Responsable: {names}", a11yOptional:"opcional", a11yLink:"con enlace",
|
a11yStatus:"Estado: {status}", a11ySize:"Esfuerzo: {size}", a11ySizeImplicit:"Esfuerzo: M (asumido)", a11yTags:"Responsable: {names}", a11yOptional:"opcional", a11yFocusMark:"mirar aquí", a11yLink:"con enlace",
|
||||||
hint_indent:"La sangría (2 espacios o un tabulador) define la jerarquía.",
|
hint_indent:"La sangría (2 espacios o un tabulador) define la jerarquía.",
|
||||||
hint_all:"subtarea, todas obligatorias", hint_any:"alternativa, elige una",
|
hint_all:"subtarea, todas obligatorias", hint_any:"alternativa, elige una",
|
||||||
hint_opt:"extra, no obligatorio",
|
hint_opt:"extra, no obligatorio",
|
||||||
|
hint_focus:"mirar aquí (un puntero compartido)",
|
||||||
hint_root:"Línea sin marcador = nodo raíz. No mezcles | con - / +.",
|
hint_root:"Línea sin marcador = nodo raíz. No mezcles | con - / +.",
|
||||||
hint_status:"Estado como casilla tras el marcador, p. ej.",
|
hint_status:"Estado como casilla tras el marcador, p. ej.",
|
||||||
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:",
|
||||||
@@ -1137,10 +1157,11 @@ const I18N = {
|
|||||||
unknownStatusWarn:"Ligne {line} : code de statut inconnu « {code} » — affiché comme neutre.",
|
unknownStatusWarn:"Ligne {line} : code de statut inconnu « {code} » — affiché comme neutre.",
|
||||||
sourceLoadWarn:"Impossible de charger « {url} » ({error}). Le fichier doit être accessible en http(s) et autoriser CORS (Access-Control-Allow-Origin).",
|
sourceLoadWarn:"Impossible de charger « {url} » ({error}). Le fichier doit être accessible en http(s) et autoriser CORS (Access-Control-Allow-Origin).",
|
||||||
sourceTimeoutWarn:"« {url} » n’a pas répondu en {seconds} s — la requête a été interrompue. Etherpad limite la fréquence de récupération de l’export (10 fois par 90 s par défaut) ; attends un instant, puis recharge.",
|
sourceTimeoutWarn:"« {url} » n’a pas répondu en {seconds} s — la requête a été interrompue. Etherpad limite la fréquence de récupération de l’export (10 fois par 90 s par défaut) ; attends un instant, puis recharge.",
|
||||||
a11yStatus:"Statut : {status}", a11ySize:"Effort : {size}", a11ySizeImplicit:"Effort : M (supposé)", a11yTags:"Responsable : {names}", a11yOptional:"facultatif", a11yLink:"avec lien",
|
a11yStatus:"Statut : {status}", a11ySize:"Effort : {size}", a11ySizeImplicit:"Effort : M (supposé)", a11yTags:"Responsable : {names}", a11yOptional:"facultatif", a11yFocusMark:"regarder ici", a11yLink:"avec lien",
|
||||||
hint_indent:"L'indentation (2 espaces ou une tabulation) définit la hiérarchie.",
|
hint_indent:"L'indentation (2 espaces ou une tabulation) définit la hiérarchie.",
|
||||||
hint_all:"sous-tâche, toutes requises", hint_any:"alternative, en choisir une",
|
hint_all:"sous-tâche, toutes requises", hint_any:"alternative, en choisir une",
|
||||||
hint_opt:"supplément, non requis",
|
hint_opt:"supplément, non requis",
|
||||||
|
hint_focus:"regarder ici (un pointeur partagé)",
|
||||||
hint_root:"Ligne sans marqueur = nœud racine. Ne mélangez pas | avec - / +.",
|
hint_root:"Ligne sans marqueur = nœud racine. Ne mélangez pas | avec - / +.",
|
||||||
hint_status:"Statut sous forme de case après le marqueur, p. ex.",
|
hint_status:"Statut sous forme de case après le marqueur, p. ex.",
|
||||||
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 :",
|
||||||
@@ -1200,10 +1221,11 @@ const I18N = {
|
|||||||
unknownStatusWarn:"Wiersz {line}: nieznany znak statusu „{code}” — pokazany jako neutralny.",
|
unknownStatusWarn:"Wiersz {line}: nieznany znak statusu „{code}” — pokazany jako neutralny.",
|
||||||
sourceLoadWarn:"Nie udało się wczytać „{url}” ({error}). Plik musi być dostępny przez http(s) i zezwalać na CORS (Access-Control-Allow-Origin).",
|
sourceLoadWarn:"Nie udało się wczytać „{url}” ({error}). Plik musi być dostępny przez http(s) i zezwalać na CORS (Access-Control-Allow-Origin).",
|
||||||
sourceTimeoutWarn:"„{url}” nie odpowiedział w ciągu {seconds} s — żądanie przerwano. Etherpad ogranicza częstość pobierania eksportu (domyślnie 10 razy na 90 s); odczekaj chwilę i wczytaj ponownie.",
|
sourceTimeoutWarn:"„{url}” nie odpowiedział w ciągu {seconds} s — żądanie przerwano. Etherpad ogranicza częstość pobierania eksportu (domyślnie 10 razy na 90 s); odczekaj chwilę i wczytaj ponownie.",
|
||||||
a11yStatus:"Status: {status}", a11ySize:"Nakład: {size}", a11ySizeImplicit:"Nakład: M (założony)", a11yTags:"Przypisano: {names}", a11yOptional:"opcjonalny", a11yLink:"z linkiem",
|
a11yStatus:"Status: {status}", a11ySize:"Nakład: {size}", a11ySizeImplicit:"Nakład: M (założony)", a11yTags:"Przypisano: {names}", a11yOptional:"opcjonalny", a11yFocusMark:"spójrz tutaj", a11yLink:"z linkiem",
|
||||||
hint_indent:"Wcięcie (2 spacje lub tabulator) definiuje hierarchię.",
|
hint_indent:"Wcięcie (2 spacje lub tabulator) definiuje hierarchię.",
|
||||||
hint_all:"podzadanie, wszystkie wymagane", hint_any:"alternatywa, wybierz jedną",
|
hint_all:"podzadanie, wszystkie wymagane", hint_any:"alternatywa, wybierz jedną",
|
||||||
hint_opt:"dodatek, niewymagany",
|
hint_opt:"dodatek, niewymagany",
|
||||||
|
hint_focus:"spójrz tutaj (wspólny wskaźnik)",
|
||||||
hint_root:"Wiersz bez znacznika = węzeł główny. Nie mieszaj | z - / +.",
|
hint_root:"Wiersz bez znacznika = węzeł główny. Nie mieszaj | z - / +.",
|
||||||
hint_status:"Status jako pole wyboru po znaczniku, np.",
|
hint_status:"Status jako pole wyboru po znaczniku, np.",
|
||||||
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:",
|
||||||
@@ -1263,10 +1285,11 @@ const I18N = {
|
|||||||
unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.",
|
unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.",
|
||||||
sourceLoadWarn:"Не удалось загрузить «{url}» ({error}). Файл должен быть доступен по http(s) и разрешать CORS (Access-Control-Allow-Origin).",
|
sourceLoadWarn:"Не удалось загрузить «{url}» ({error}). Файл должен быть доступен по http(s) и разрешать CORS (Access-Control-Allow-Origin).",
|
||||||
sourceTimeoutWarn:"«{url}» не ответил за {seconds} с — запрос прерван. Etherpad ограничивает частоту загрузки экспорта (по умолчанию 10 раз за 90 с); подождите немного и обновите снова.",
|
sourceTimeoutWarn:"«{url}» не ответил за {seconds} с — запрос прерван. Etherpad ограничивает частоту загрузки экспорта (по умолчанию 10 раз за 90 с); подождите немного и обновите снова.",
|
||||||
a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: M (предполагается)", a11yTags:"Ответственные: {names}", a11yOptional:"необязательно", a11yLink:"со ссылкой",
|
a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: M (предполагается)", a11yTags:"Ответственные: {names}", a11yOptional:"необязательно", a11yFocusMark:"смотрите здесь", a11yLink:"со ссылкой",
|
||||||
hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.",
|
hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.",
|
||||||
hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну",
|
hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну",
|
||||||
hint_opt:"дополнение, не обязательно",
|
hint_opt:"дополнение, не обязательно",
|
||||||
|
hint_focus:"смотрите здесь (общая указка)",
|
||||||
hint_root:"Строка без маркера = корневой узел. Не смешивайте | с - / +.",
|
hint_root:"Строка без маркера = корневой узел. Не смешивайте | с - / +.",
|
||||||
hint_status:"Статус в виде флажка после маркера, напр.",
|
hint_status:"Статус в виде флажка после маркера, напр.",
|
||||||
hint_size:"Трудоёмкость как размер футболки в скобках; ссылку добавьте просто как URL:",
|
hint_size:"Трудоёмкость как размер футболки в скобках; ссылку добавьте просто как URL:",
|
||||||
@@ -1326,10 +1349,11 @@ const I18N = {
|
|||||||
unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।",
|
unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।",
|
||||||
sourceLoadWarn:"„{url}“ लोड नहीं हो सका ({error})। फ़ाइल http(s) से उपलब्ध होनी चाहिए और CORS की अनुमति देनी चाहिए (Access-Control-Allow-Origin)।",
|
sourceLoadWarn:"„{url}“ लोड नहीं हो सका ({error})। फ़ाइल http(s) से उपलब्ध होनी चाहिए और CORS की अनुमति देनी चाहिए (Access-Control-Allow-Origin)।",
|
||||||
sourceTimeoutWarn:"„{url}“ ने {seconds} स॰ में उत्तर नहीं दिया — अनुरोध रद्द कर दिया गया। Etherpad सीमित करता है कि एक्सपोर्ट कितनी बार लिया जा सके (डिफ़ॉल्ट रूप से 90 स॰ में 10 बार); कुछ क्षण रुकें, फिर दोबारा लोड करें।",
|
sourceTimeoutWarn:"„{url}“ ने {seconds} स॰ में उत्तर नहीं दिया — अनुरोध रद्द कर दिया गया। Etherpad सीमित करता है कि एक्सपोर्ट कितनी बार लिया जा सके (डिफ़ॉल्ट रूप से 90 स॰ में 10 बार); कुछ क्षण रुकें, फिर दोबारा लोड करें।",
|
||||||
a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: M (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yOptional:"वैकल्पिक", a11yLink:"लिंक सहित",
|
a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: M (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yOptional:"वैकल्पिक", a11yFocusMark:"यहाँ देखें", a11yLink:"लिंक सहित",
|
||||||
hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।",
|
hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।",
|
||||||
hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें",
|
hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें",
|
||||||
hint_opt:"अतिरिक्त, आवश्यक नहीं",
|
hint_opt:"अतिरिक्त, आवश्यक नहीं",
|
||||||
|
hint_focus:"यहाँ देखें (साझा संकेतक)",
|
||||||
hint_root:"बिना मार्कर वाली पंक्ति = मूल नोड। | को - / + के साथ न मिलाएँ।",
|
hint_root:"बिना मार्कर वाली पंक्ति = मूल नोड। | को - / + के साथ न मिलाएँ।",
|
||||||
hint_status:"मार्कर के बाद चेकबॉक्स के रूप में स्थिति, जैसे",
|
hint_status:"मार्कर के बाद चेकबॉक्स के रूप में स्थिति, जैसे",
|
||||||
hint_size:"प्रयास कोष्ठक में टी-शर्ट आकार के रूप में; लिंक बस URL के रूप में जोड़ें:",
|
hint_size:"प्रयास कोष्ठक में टी-शर्ट आकार के रूप में; लिंक बस URL के रूप में जोड़ें:",
|
||||||
@@ -1389,10 +1413,11 @@ const I18N = {
|
|||||||
unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。",
|
unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。",
|
||||||
sourceLoadWarn:"无法加载“{url}”({error})。该文件必须可通过 http(s) 访问并允许 CORS(Access-Control-Allow-Origin)。",
|
sourceLoadWarn:"无法加载“{url}”({error})。该文件必须可通过 http(s) 访问并允许 CORS(Access-Control-Allow-Origin)。",
|
||||||
sourceTimeoutWarn:"“{url}” 在 {seconds} 秒内没有响应 — 请求已中止。Etherpad 会限制导出的获取频率(默认每 90 秒 10 次);请稍候再重新加载。",
|
sourceTimeoutWarn:"“{url}” 在 {seconds} 秒内没有响应 — 请求已中止。Etherpad 会限制导出的获取频率(默认每 90 秒 10 次);请稍候再重新加载。",
|
||||||
a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:M(假定)", a11yTags:"负责人:{names}", a11yOptional:"可选", a11yLink:"含链接",
|
a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:M(假定)", a11yTags:"负责人:{names}", a11yOptional:"可选", a11yFocusMark:"看这里", a11yLink:"含链接",
|
||||||
hint_indent:"缩进(2 个空格或制表符)定义层级。",
|
hint_indent:"缩进(2 个空格或制表符)定义层级。",
|
||||||
hint_all:"子任务,全部必需", hint_any:"备选项,择其一",
|
hint_all:"子任务,全部必需", hint_any:"备选项,择其一",
|
||||||
hint_opt:"附加项,非必需",
|
hint_opt:"附加项,非必需",
|
||||||
|
hint_focus:"看这里(共享的指针)",
|
||||||
hint_root:"无标记的行 = 根节点。请勿将 | 与 - / + 混用。",
|
hint_root:"无标记的行 = 根节点。请勿将 | 与 - / + 混用。",
|
||||||
hint_status:"在标记后用方框表示状态,例如",
|
hint_status:"在标记后用方框表示状态,例如",
|
||||||
hint_size:"用括号中的 T 恤尺码表示工作量;链接直接作为 URL 附加:",
|
hint_size:"用括号中的 T 恤尺码表示工作量;链接直接作为 URL 附加:",
|
||||||
@@ -1452,10 +1477,11 @@ const I18N = {
|
|||||||
unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。",
|
unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。",
|
||||||
sourceLoadWarn:"「{url}」を読み込めませんでした({error})。ファイルは http(s) でアクセス可能で、CORS(Access-Control-Allow-Origin)を許可する必要があります。",
|
sourceLoadWarn:"「{url}」を読み込めませんでした({error})。ファイルは http(s) でアクセス可能で、CORS(Access-Control-Allow-Origin)を許可する必要があります。",
|
||||||
sourceTimeoutWarn:"「{url}」が {seconds} 秒以内に応答しませんでした — 要求を中止しました。Etherpad はエクスポートの取得回数を制限します(既定で 90 秒あたり 10 回)。少し待ってから再読み込みしてください。",
|
sourceTimeoutWarn:"「{url}」が {seconds} 秒以内に応答しませんでした — 要求を中止しました。Etherpad はエクスポートの取得回数を制限します(既定で 90 秒あたり 10 回)。少し待ってから再読み込みしてください。",
|
||||||
a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: M(想定)", a11yTags:"担当: {names}", a11yOptional:"任意", a11yLink:"リンクあり",
|
a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: M(想定)", a11yTags:"担当: {names}", a11yOptional:"任意", a11yFocusMark:"ここを見る", a11yLink:"リンクあり",
|
||||||
hint_indent:"インデント(スペース2つまたはタブ)で階層を定義します。",
|
hint_indent:"インデント(スペース2つまたはタブ)で階層を定義します。",
|
||||||
hint_all:"サブタスク、すべて必須", hint_any:"選択肢、1つを選ぶ",
|
hint_all:"サブタスク、すべて必須", hint_any:"選択肢、1つを選ぶ",
|
||||||
hint_opt:"追加、必須ではない",
|
hint_opt:"追加、必須ではない",
|
||||||
|
hint_focus:"ここを見る(共有の指さし)",
|
||||||
hint_root:"マーカーのない行 = ルートノード。| を - / + と混在させないでください。",
|
hint_root:"マーカーのない行 = ルートノード。| を - / + と混在させないでください。",
|
||||||
hint_status:"マーカーの後にチェックボックスで状態、例:",
|
hint_status:"マーカーの後にチェックボックスで状態、例:",
|
||||||
hint_size:"工数は括弧内の T シャツサイズで;リンクは URL としてそのまま追加:",
|
hint_size:"工数は括弧内の T シャツサイズで;リンクは URL としてそのまま追加:",
|
||||||
@@ -1493,6 +1519,7 @@ function buildHint(){
|
|||||||
${esc(t('hint_break'))}<br>
|
${esc(t('hint_break'))}<br>
|
||||||
${esc(t('hint_comment'))}
|
${esc(t('hint_comment'))}
|
||||||
${esc(t('hint_people'))}
|
${esc(t('hint_people'))}
|
||||||
|
<code>!!!</code> ${esc(t('hint_focus'))}
|
||||||
<div class="hint-op">${esc(t('hint_jump'))}</div>`;
|
<div class="hint-op">${esc(t('hint_jump'))}</div>`;
|
||||||
}
|
}
|
||||||
function applyLang(l){
|
function applyLang(l){
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ export const STATUS_BY_CODE = {
|
|||||||
|
|
||||||
/* Parst den Notationstext zu { roots, warnings }.
|
/* Parst den Notationstext zu { roots, warnings }.
|
||||||
Jeder Knoten: {label, type:'and'|'or', optional, status, url, size, tags,
|
Jeder Knoten: {label, type:'and'|'or', optional, status, url, size, tags,
|
||||||
children, line}.
|
focus, children, line}.
|
||||||
`type` ist das Gate der Geschwistergruppe, `optional` (Zeichen `+`, SPEC §3)
|
`type` ist das Gate der Geschwistergruppe, `optional` (Zeichen `+`, SPEC §3)
|
||||||
eine Eigenschaft des einzelnen Knotens: er hängt an derselben Und-Zerlegung
|
eine Eigenschaft des einzelnen Knotens: er hängt an derselben Und-Zerlegung
|
||||||
(`type:'and'`), ist darin aber entbehrlich. Dadurch bleibt die
|
(`type:'and'`), ist darin aber entbehrlich. Dadurch bleibt die
|
||||||
Gemischt-Warnung unverändert richtig — sie schlägt nur an, wenn `|` mit
|
Gemischt-Warnung unverändert richtig — sie schlägt nur an, wenn `|` mit
|
||||||
`-`/`+` gemischt wird.
|
`-`/`+` gemischt wird.
|
||||||
Extraktionsreihenfolge (SPEC §1): Kommentar -> Zeichen/Status -> URL -> Größe
|
Extraktionsreihenfolge (SPEC §1): Kommentar -> Zeichen/Status -> URL -> Größe
|
||||||
-> Tags -> Label. Hierarchie über Einrückungsbreite (Tab = 2 Leerzeichen);
|
-> Tags -> Fokusmarke -> Label. Hierarchie über Einrückungsbreite (Tab = 2 Leerzeichen);
|
||||||
Elternknoten ist die nächste vorangehende Zeile mit kleinerer Breite. */
|
Elternknoten ist die nächste vorangehende Zeile mit kleinerer Breite. */
|
||||||
export function parse(text){
|
export function parse(text){
|
||||||
const virtualRoot = {label:'', type:'and', children:[]};
|
const virtualRoot = {label:'', type:'and', children:[]};
|
||||||
@@ -52,6 +52,11 @@ export function parse(text){
|
|||||||
rest = rest.replace(/https?:\/\/\S+/i, s => { url = s; return ''; });
|
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(/\((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 ''; });
|
rest = rest.replace(/@([\p{L}\p{N}._-]+)/gu, (s, g) => { tags.push(g); return ''; });
|
||||||
|
/* Fokusmarke `!!!` (SPEC §1) — nur ALLEINSTEHEND, damit „Achtung!!!" ein
|
||||||
|
gewöhnliches Label bleibt. Kein Lookbehind (Safari kennt es erst ab 16.4):
|
||||||
|
der führende Leerraum wird mitgefangen und wieder eingesetzt. */
|
||||||
|
let focus = false;
|
||||||
|
rest = rest.replace(/(^|\s)!!!(?=\s|$)/g, (s, pre) => { focus = true; return pre; });
|
||||||
const label = rest.replace(/\s+/g, ' ').trim();
|
const label = rest.replace(/\s+/g, ' ').trim();
|
||||||
if(!label) return;
|
if(!label) return;
|
||||||
|
|
||||||
@@ -64,7 +69,7 @@ export function parse(text){
|
|||||||
while(stack.length > 1 && stack[stack.length-1].width >= width) stack.pop();
|
while(stack.length > 1 && stack[stack.length-1].width >= width) stack.pop();
|
||||||
const parent = stack[stack.length-1].node;
|
const parent = stack[stack.length-1].node;
|
||||||
|
|
||||||
const node = {label, type, optional, status, url, size, tags, children:[], line:i+1};
|
const node = {label, type, optional, status, url, size, tags, focus, children:[], line:i+1};
|
||||||
parent.children.push(node);
|
parent.children.push(node);
|
||||||
stack.push({node, width});
|
stack.push({node, width});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { gateOf, needsBreakdown, visibleChildren, cheapCls } from './model.js';
|
|||||||
function extraCls(n, opts){
|
function extraCls(n, opts){
|
||||||
const cheap = cheapCls(n, opts.cheapSet);
|
const cheap = cheapCls(n, opts.cheapSet);
|
||||||
const fresh = opts.freshSet && opts.freshSet.has(n) ? 'fresh' : '';
|
const fresh = opts.freshSet && opts.freshSet.has(n) ? 'fresh' : '';
|
||||||
return [cheap, fresh, n.optional ? 'opt' : ''].filter(Boolean).join(' ');
|
return [cheap, fresh, n.optional ? 'opt' : '', n.focus ? 'focusmark' : ''].filter(Boolean).join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Klassen des <li>: Gate der eigenen Kinder (steuert die Anordnung) plus
|
/* Klassen des <li>: Gate der eigenen Kinder (steuert die Anordnung) plus
|
||||||
@@ -52,6 +52,10 @@ function nodeAria(n, opts){
|
|||||||
else if(cheapPath) parts.push(t('a11ySizeImplicit'));
|
else if(cheapPath) parts.push(t('a11ySizeImplicit'));
|
||||||
if(n.tags && n.tags.length) parts.push(t('a11yTags', {names: n.tags.join(', ')}));
|
if(n.tags && n.tags.length) parts.push(t('a11yTags', {names: n.tags.join(', ')}));
|
||||||
if(n.optional) parts.push(t('a11yOptional'));
|
if(n.optional) parts.push(t('a11yOptional'));
|
||||||
|
/* Die Fokusmarke ist rein als box-shadow sichtbar — ohne diese Ansage wüsste
|
||||||
|
ein Screenreader nichts davon. Zugleich der einzige Ort, an dem sie sich von
|
||||||
|
der eigenen Cursor-Zeile unterscheidet (SPEC §9). */
|
||||||
|
if(n.focus) parts.push(t('a11yFocusMark'));
|
||||||
if(n.url) parts.push(t('a11yLink'));
|
if(n.url) parts.push(t('a11yLink'));
|
||||||
return parts.join(', ');
|
return parts.join(', ');
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-2
@@ -532,7 +532,12 @@
|
|||||||
im Druck. Der `#out`-Präfix ist nötig: `ul.or .node{box-shadow:none}`
|
im Druck. Der `#out`-Präfix ist nötig: `ul.or .node{box-shadow:none}`
|
||||||
(weiter unten) ist spezifischer als `.node.current` und schluckte den Ring
|
(weiter unten) ist spezifischer als `.node.current` und schluckte den Ring
|
||||||
sonst überall unterhalb einer any-of-Gruppe. */
|
sonst überall unterhalb einer any-of-Gruppe. */
|
||||||
#out .node.current{box-shadow:0 0 0 2px var(--card),0 0 0 4px var(--ink)}
|
/* Die Fokusmarke `!!!` (SPEC §1) trägt bewusst DIESELBE Hervorhebung: Es gibt
|
||||||
|
nur einen Begriff „hier schauen", und ein dritter Ring neben Fokusring,
|
||||||
|
Strahlenkranz und Cursor-Ring wäre ein Zeichen zu viel. Der Unterschied
|
||||||
|
steht im `aria-label`. */
|
||||||
|
#out .node.current,
|
||||||
|
#out .node.focusmark{box-shadow:0 0 0 2px var(--card),0 0 0 4px var(--ink)}
|
||||||
/* Neu in Produktion seit dem letzten Besuch (D28): gelber Strahlenkranz um den
|
/* Neu in Produktion seit dem letzten Besuch (D28): gelber Strahlenkranz um den
|
||||||
Knoten. Bewusst ein Schein nach AUSSEN statt Farbe im Knoten — die Füllung
|
Knoten. Bewusst ein Schein nach AUSSEN statt Farbe im Knoten — die Füllung
|
||||||
gehört dem Status (SPEC §4) und bleibt lesbar. Kein Blinken: WCAG 2.2.2/2.3.1
|
gehört dem Status (SPEC §4) und bleibt lesbar. Kein Blinken: WCAG 2.2.2/2.3.1
|
||||||
@@ -609,6 +614,12 @@
|
|||||||
unsichtbar), und gerade bei einer nicht gewählten Alternative ist „das ist
|
unsichtbar), und gerade bei einer nicht gewählten Alternative ist „das ist
|
||||||
jetzt live" die interessantere Nachricht (D28). */
|
jetzt live" die interessantere Nachricht (D28). */
|
||||||
.cheap-on .node:not(.cheap).fresh{opacity:1;filter:none}
|
.cheap-on .node:not(.cheap).fresh{opacity:1;filter:none}
|
||||||
|
/* Dasselbe für die Fokusmarke `!!!` (SPEC §1), und hier noch zwingender: Ein
|
||||||
|
Zeigefinger, der auf einen ausgeblassten Knoten zeigt, zeigt auf nichts —
|
||||||
|
und „sollten wir diese verworfene Alternative noch einmal ansehen?" ist einer
|
||||||
|
der wahrscheinlichsten Gründe, überhaupt zu zeigen. Anders als beim
|
||||||
|
optionalen Knoten (D29), wo das Zurücktreten die Aussage IST. */
|
||||||
|
.cheap-on .node:not(.cheap).focusmark{opacity:1;filter:none}
|
||||||
/* implizit als M angenommene Größe: invertiertes Badge (weiß statt petrol) */
|
/* implizit als M angenommene Größe: invertiertes Badge (weiß statt petrol) */
|
||||||
.size.implicit{background:var(--card);color:var(--or);border-color:var(--or)}
|
.size.implicit{background:var(--card);color:var(--or);border-color:var(--or)}
|
||||||
/* Günstigster-Pfad-Linie: gestrichelt, geschwungen, petrol — fädelt durch
|
/* Günstigster-Pfad-Linie: gestrichelt, geschwungen, petrol — fädelt durch
|
||||||
@@ -964,7 +975,8 @@
|
|||||||
*{-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}
|
*{-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}
|
||||||
/* Knoten nicht über den Seitenrand zerschneiden */
|
/* Knoten nicht über den Seitenrand zerschneiden */
|
||||||
#out li{break-inside:avoid}
|
#out li{break-inside:avoid}
|
||||||
.node.current{box-shadow:none!important} /* Editierhilfe, nicht drucken (D25) */
|
/* Editierhilfe bzw. Zuruf, nicht drucken (D25, Fokusmarke SPEC §1) */
|
||||||
|
.node.current,.node.focusmark{box-shadow:none!important}
|
||||||
.node.fresh{box-shadow:none!important} /* „neu seit dem letzten Besuch" ist persönlich (D28) */
|
.node.fresh{box-shadow:none!important} /* „neu seit dem letzten Besuch" ist persönlich (D28) */
|
||||||
@page{margin:12mm}
|
@page{margin:12mm}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { parse } from '../src/parser.js';
|
||||||
|
import { renderTreeHtml } from '../src/render.js';
|
||||||
|
|
||||||
|
const t = (key, vars) => {
|
||||||
|
const dict = {
|
||||||
|
a11yStatus: 'Status: ' + (vars && vars.status),
|
||||||
|
a11yFocusMark: 'hierhin schauen',
|
||||||
|
a11yOptional: 'optional',
|
||||||
|
st_geplant: 'geplant'
|
||||||
|
};
|
||||||
|
return dict[key] !== undefined ? dict[key] : key;
|
||||||
|
};
|
||||||
|
/* renderTreeHtml liefert {html, warnings} — hier interessiert nur der HTML-String. */
|
||||||
|
const html = text => renderTreeHtml(parse(text).roots,
|
||||||
|
{t, showDiscarded: false, cheapPath: false, cheapSet: new Set()}).html;
|
||||||
|
|
||||||
|
describe('Fokusmarke `!!!` — Parser (SPEC §1)', () => {
|
||||||
|
it('erkennt die Marke am Zeilenende und hält sie aus dem Label', () => {
|
||||||
|
const [n] = parse('Backend !!!').roots;
|
||||||
|
expect(n.focus).toBe(true);
|
||||||
|
expect(n.label).toBe('Backend');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('erkennt sie am Zeilenanfang und in der Mitte', () => {
|
||||||
|
expect(parse('!!! Backend').roots[0]).toMatchObject({focus: true, label: 'Backend'});
|
||||||
|
expect(parse('Backend !!! fertig').roots[0]).toMatchObject({focus: true, label: 'Backend fertig'});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Der Kern der Regel: nur ALLEINSTEHEND. Sonst verlöre jedes „Achtung!!!"
|
||||||
|
seine Ausrufezeichen und niemand fände den Grund. */
|
||||||
|
it('lässt angehängte Ausrufezeichen in Ruhe', () => {
|
||||||
|
expect(parse('Achtung!!!').roots[0]).toMatchObject({focus: false, label: 'Achtung!!!'});
|
||||||
|
expect(parse('!!!wichtig').roots[0]).toMatchObject({focus: false, label: '!!!wichtig'});
|
||||||
|
expect(parse('a!!!b').roots[0]).toMatchObject({focus: false, label: 'a!!!b'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('zählt zwei oder vier Ausrufezeichen nicht als Marke', () => {
|
||||||
|
expect(parse('Backend !!').roots[0]).toMatchObject({focus: false, label: 'Backend !!'});
|
||||||
|
expect(parse('Backend !!!!').roots[0]).toMatchObject({focus: false, label: 'Backend !!!!'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lässt `!!!` innerhalb einer URL unberührt', () => {
|
||||||
|
const [n] = parse('Doku https://example.org/a!!!b').roots;
|
||||||
|
expect(n.focus).toBe(false);
|
||||||
|
expect(n.url).toBe('https://example.org/a!!!b');
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Reihenfolge der Extraktion (SPEC §1): Kommentar, Zeichen/Status, URL, Größe,
|
||||||
|
Tags, Marke, Label — die Marke darf keinen der anderen Teile verschlucken. */
|
||||||
|
it('verträgt sich mit Status, Größe, URL, Tags und Kommentar', () => {
|
||||||
|
const [n] = parse(' - [~] Backend (L) https://git.example/x @ben !!! %% Kommentar').roots;
|
||||||
|
expect(n).toMatchObject({focus: true, label: 'Backend', size: 'L', tags: ['ben']});
|
||||||
|
expect(n.status.key).toBe('arbeit');
|
||||||
|
expect(n.url).toBe('https://git.example/x');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('eine Marke im Kommentar wirkt nicht — der Kommentar fällt zuerst weg', () => {
|
||||||
|
expect(parse('Backend %% später !!!').roots[0].focus).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mehrere Marken sind erlaubt und markieren jeden betroffenen Knoten', () => {
|
||||||
|
const roots = parse('A !!!\nB\nC !!!').roots;
|
||||||
|
expect(roots.map(n => n.focus)).toEqual([true, false, true]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('eine Zeile aus nur einer Marke hat kein Label und fällt weg', () => {
|
||||||
|
expect(parse('!!!').roots).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ohne Marke ist focus false, nicht undefined', () => {
|
||||||
|
expect(parse('Backend').roots[0].focus).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Die Marke ist eine dritte, unabhängige Achse (SPEC §1): weder Fortschritt
|
||||||
|
(§4) noch Notwendigkeit (§3) dürfen sich daran ändern. */
|
||||||
|
it('lässt Status und Optionalität unberührt', () => {
|
||||||
|
const [root] = parse('Wurzel\n + [^] Zugabe !!!').roots;
|
||||||
|
const kid = root.children[0];
|
||||||
|
expect(kid).toMatchObject({focus: true, optional: true, type: 'and'});
|
||||||
|
expect(kid.status.key).toBe('prod');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Fokusmarke — Renderer', () => {
|
||||||
|
it('setzt die Klasse `focusmark` am Knoten', () => {
|
||||||
|
expect(html('Backend !!!')).toMatch(/class="node root-node focusmark"/);
|
||||||
|
/* auch an einem Kind, nicht nur an der Wurzel */
|
||||||
|
expect(html('Wurzel\n - Kind !!!')).toMatch(/class="node focusmark"[^>]*>Kind/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setzt sie nicht ohne Marke', () => {
|
||||||
|
expect(html('Backend')).not.toMatch(/focusmark/);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Sichtbar ist die Marke nur als box-shadow — ohne diese Ansage wüsste ein
|
||||||
|
Screenreader nichts davon (SPEC §9). */
|
||||||
|
it('nennt die Marke im aria-label', () => {
|
||||||
|
expect(html('[ ] Backend !!!')).toContain('hierhin schauen');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('das Ausrufezeichen steht nicht im sichtbaren Label', () => {
|
||||||
|
const out = html('Backend !!!');
|
||||||
|
expect(out).toContain('>Backend<');
|
||||||
|
expect(out).not.toContain('!!!');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user