Write notes as formatted text, store them as Markdown

The editor was a textarea holding raw Markdown, which is the wrong thing
to hand someone taking notes during a lesson: nobody types `##` and `**`
while a teacher is talking. It now shows the note formatted and puts a
toolbar above it — headings, bold, lists, tick boxes, quotes, links,
tables — while the file on disk stays exactly what it was, because that
is what the indexer reads and what outlives this app.

`markdown.js` is the whole translation: `markdownToHtml` on the way in,
`markdownFromDom` on the way out. The property that matters is that the
round trip settles — one pass may tidy a note, a second must change
nothing — because these notes are the only record of what was said in
the room and there is nothing to restore a lossy save from. `editor.js`
checks exactly that before opening a note formatted, and a note it
cannot hold unchanged opens in the Markdown view and says so instead of
being quietly reduced.

No editor library: the content security policy allows no outside script
and the app has no bundler, so this is `contenteditable` and
`execCommand` with a tolerant serializer behind it — an element it does
not model keeps its words and loses its tag. Pasted HTML is converted to
Markdown before it reaches the document, which is the one place where
sanitising and formatting are the same operation.

Tested against `test/mini-dom.ts`, sixty lines of read-only DOM, rather
than a headless browser or a DOM dependency; the toolbar itself was
driven by hand in Firefox. WebKit has still never run it.
This commit is contained in:
MechaCat02
2026-09-19 21:24:42 +02:00
parent 5c0b658855
commit 534b1b0f58
13 changed files with 1690 additions and 33 deletions

View File

@@ -67,6 +67,11 @@ are stale — they were last taken before the notes and class-register work, and
could not be retaken because the live session had lapsed. Every Schulcloud check fails with 401 when the live
session has lapsed — check the container's keepalive log before suspecting code.
The editor's Markdown round trip is unit-tested; the **browser** side of
`editor.js` is not, because nothing here runs one. It was checked by hand in
Firefox against a page that drives the toolbar — WebKit, which is the engine on
the phone this is written on, has still never run it.
Store tests need a database and skip without one:
`TEST_DATABASE_URL=postgresql://… npm test`. They use a real Postgres on
purpose — the generation/diff semantics are entirely SQL, so a mock would test
@@ -145,7 +150,20 @@ bin/cli.ts ──HTTP──────────┘ cli/{config,client,sync
**files** under `src/http/app/`, copied to `dist/` by `scripts/copy-assets.mjs`
and read relative to `import.meta.dirname` — real HTML, CSS and JS that an
editor and a linter understand, which is also what the CSP requires, since it
forbids inline script.
forbids inline script. `app.js` is an ES **module**; a new asset must be added
to `ASSETS` *and* to the route's regex in `app-page.ts`, or it 404s.
- **`http/app/markdown.js` + `editor.js`** — the note is edited as formatted
text and stored as Markdown, and these two are that translation.
`markdown.js` is the pair `markdownToHtml` / `markdownFromDom`; `editor.js`
drives a `contenteditable` element with `execCommand` (no library: the CSP
allows no outside script and the app has no bundler). **The round trip must
settle**: one pass may tidy a note, a second must change nothing, and
`editor.js` checks exactly that before opening a note formatted — a note that
fails opens in the Markdown view instead. `test/app-markdown.test.ts` covers
it against `test/mini-dom.ts`, ~60 lines of read-only DOM, because losing a
lesson's notes to a lossy serializer is not a bug anyone can recover from.
Pasted HTML goes through Markdown before it reaches the document, which is
where sanitising and formatting are the same operation.
- **`http/web-auth.ts`** — the app's login, which is a different kind of
credential from everything else here: a password a person types, not a token a
program was configured with. scrypt at startup, a signed `HttpOnly` /