684f063a8e
- Hilfsmodul note-task-list.tsx mit isTaskLine/toggleTaskLine (GFM-konforme Regex, Code-Zaeune werden uebersprungen) und NoteCheckbox (kein disabled) - previewOptions des Notiz-Widgets reicht components.input = NoteCheckbox durch react-markdown nach rehypeSanitize durch (bleibt aktiv, T-IEX-01) - Klick im Vorschau-Container kippt genau die N-te Aufgabenzeile und speichert sofort (verwirft einen laufenden Tipp-Entprell-Timer) - Hinweis: note-task-list als .tsx statt .ts angelegt (Komponente braucht JSX) — Abweichung dokumentiert in SUMMARY.md - 16 neue/erweiterte Tests gruen, tsc Exit 0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
101 lines
3.6 KiB
TypeScript
101 lines
3.6 KiB
TypeScript
/**
|
|
* note-task-list — reine Hilfsfunktionen + Kaestchen-Komponente fuer das
|
|
* Notiz-Widget (quick-260916-iex).
|
|
*
|
|
* `rehypeSanitize` (hast-util-sanitize) erzwingt bei `input`-Elementen
|
|
* grundsaetzlich `disabled=true`. Die Loesung ersetzt NICHT das
|
|
* Sanitize-Verhalten, sondern das gerenderte Element selbst: react-markdown
|
|
* (ueber `@uiw/react-md-editor`s `previewOptions.components`) laesst uns das
|
|
* `input`-Tag durch eine eigene Komponente ersetzen, die NACH dem Sanitize
|
|
* ausgefuehrt wird und deshalb selbst entscheidet, ob sie `disabled` setzt.
|
|
* `rehypeSanitize` bleibt aktiv (XSS-Schranke fuer den Rest des Markdowns,
|
|
* T-IEX-01).
|
|
*
|
|
* Die Zuordnung Klick -> Zeile laeuft ueber einen Index (n-tes Kaestchen im
|
|
* DOM == n-te Aufgabenzeile im Markdown). Damit das stimmt, MUSS
|
|
* `isTaskLine` exakt dieselben Zeilen als Aufgabe erkennen wie die
|
|
* GFM-Erkennung von react-markdown (remark-gfm /
|
|
* micromark-extension-gfm-task-list-item): nach der schliessenden Klammer
|
|
* muss Leerraum UND mindestens ein Nicht-Leerraum-Zeichen folgen. Eine
|
|
* laxere Regex wuerde bei einer noch leeren Aufgabenzeile (`- [ ]`, typisch
|
|
* beim Tippen einer neuen Aufgabe) den Index verschieben und beim Klick die
|
|
* falsche Zeile kippen.
|
|
*
|
|
* Vorbild: `toggleMarkdownCheckbox` im alten persoenlichen Dashboard des
|
|
* Anwenders (user-files/personal-dashboard/src/app/page.tsx, ~Z. 1399).
|
|
*/
|
|
|
|
/**
|
|
* Erkennt eine Markdown-Aufgabenzeile (`- [ ] Text`, `* [x] Text`,
|
|
* `+ [X] Text`, `1. [ ] Text`, `1) [ ] Text`, auch eingerueckt). Bewusst
|
|
* streng: nach `]` muss Leerraum und danach mindestens ein
|
|
* Nicht-Leerraum-Zeichen folgen (siehe Modulkommentar).
|
|
*/
|
|
export const TASK_LINE_RE =
|
|
/^(\s*(?:[-*+]|\d+[.)])\s+\[)([ \txX])(\]\s+\S.*)$/;
|
|
|
|
const FENCE_RE = /^\s*(```|~~~)/;
|
|
|
|
export function isTaskLine(line: string): boolean {
|
|
return TASK_LINE_RE.test(line);
|
|
}
|
|
|
|
/**
|
|
* Kippt die `index`-te Aufgabenzeile (0-basiert, Zeilen in Code-Zaeunen
|
|
* zaehlen nicht mit) zwischen `[ ]` und `[x]`. Gibt bei ungueltigem Index
|
|
* (< 0 oder >= Anzahl Aufgabenzeilen) dieselbe Referenz wie `content`
|
|
* zurueck, damit Aufrufer per `===` erkennen, dass nichts zu speichern ist.
|
|
*/
|
|
export function toggleTaskLine(content: string, index: number): string {
|
|
if (index < 0) return content;
|
|
|
|
const lines = content.split('\n');
|
|
let inFence = false;
|
|
let taskCounter = 0;
|
|
let changed = false;
|
|
|
|
const nextLines = lines.map((line) => {
|
|
if (FENCE_RE.test(line)) {
|
|
inFence = !inFence;
|
|
return line;
|
|
}
|
|
if (inFence) return line;
|
|
if (!isTaskLine(line)) return line;
|
|
|
|
const isTarget = taskCounter === index;
|
|
taskCounter += 1;
|
|
if (!isTarget) return line;
|
|
|
|
const match = line.match(TASK_LINE_RE);
|
|
if (!match) return line;
|
|
|
|
const [, prefix, mark, suffix] = match;
|
|
const newMark = mark === ' ' || mark === '\t' ? 'x' : ' ';
|
|
changed = true;
|
|
return `${prefix}${newMark}${suffix}`;
|
|
});
|
|
|
|
if (!changed) return content;
|
|
return nextLines.join('\n');
|
|
}
|
|
|
|
/**
|
|
* Ersetzt das von react-markdown gerenderte `<input>` einer Aufgabenzeile.
|
|
* Zieht bewusst NUR `checked` aus den Props (react-markdown reicht
|
|
* zusaetzlich `node`, `disabled`, `type` durch) — sonst landen fremde
|
|
* Attribute (insbesondere `node`) im DOM. Kein `disabled`: das Kaestchen
|
|
* bleibt anklickbar. `readOnly` unterdrueckt Reacts Warnung
|
|
* "checked ohne onChange" — der Klick wird nicht am Kaestchen selbst,
|
|
* sondern delegiert am Vorschau-Container verarbeitet.
|
|
*/
|
|
export function NoteCheckbox({ checked }: { checked?: boolean }) {
|
|
return (
|
|
<input
|
|
type="checkbox"
|
|
checked={!!checked}
|
|
readOnly
|
|
className="cursor-pointer"
|
|
/>
|
|
);
|
|
}
|