An upload while writing is now a file node of the document, through
POST /nodes/{id}/files, instead of a loose row of media that only the media
browser listed. It moves, seals, goes to the trash and comes back with the
document. Migration 0029 gives every embedded upload that had no node a
place under the document that embeds it (a living one before a trashed
one), keeping its media id, so no body changes; the sealed-folder triggers
now ask a document's folder for the files under it.
- nodes: `holds` and `place_for` say what may go where; a document holds
files and nothing else, in moves, uploads and restores from the trash.
A loaded document carries its files and whether its folder seals them.
- seal: a document moving into a sealed folder seals its files first.
- media: POST /media, GET /media and DELETE /media/{id} are gone, and with
them MediaBrowser and :media.
- import: `plan/` beside `plan.md` comes back as the document's files, the
shape export writes; an older export's media/ goes to the first document
that embeds each file.
- sftp: a document's files are `<slug>/` beside its `.md`, listed once it
has any, holding files only, moving with the `.md`.
- web: paste, drop and :upload while reading go to the document; the info
sheet has a files section (Enter opens, i embeds, y copies how); the list
shows +N on a document, za folds its files open, p drops marked files in.
Enter on a card floats a sheet over the board: its labels, a markdown
description written in the vim editor (:w writes it to the card), and a
checklist walked with j/k, ticked with Space, grown with o. Labels take
their colour from their name. A repeating card comes back with its
checklist reopened.
Code in the editor and in rendered fences is coloured from nvim's
yorha.lua, one class per highlight group, for every language
@codemirror/language-data knows; the languages load on first use.
A ```typst (or ```typ) block is compiled by the typst crate into SVG and
shown as an image where it stands, so math, tables and figures can be
written in typst beside the markdown around them. It shows its source while
it is set, and the error typst gives, with the block's line, when it cannot
be.
Each block is a document of its own: its source is the only file in its
world and the fonts are typst-assets', inside the binary, so nothing is read
from disk or fetched — packages and images are refused. The page fits what
is on it, is transparent, and carries the theme's text colour. Answers are
cached by source, two blocks are laid out at a time off the async threads,
and comemo's memo is evicted after each compile.
A share page asks POST /api/share/{token}/typst, which takes the reader's
cookie and typesets only a block the shared document holds.
typst brings a palette crate whose AsRef impls on arrays made four
`kek.as_ref().into()` in the vault ambiguous; they deref instead.
`breaks` is on because the diary is written a line at a time and a line typed
in the browser is meant. Markdown written elsewhere is usually wrapped at a
width instead — an import, a save over the SFTP mount from vim with
`textwidth`, a pasted README — and kept its breaks, one in the middle of every
sentence, worst on a phone.
`hardWrapped` tells the two apart by what a wrapping editor leaves behind:
every line but the last is long, each break comes where the next word would
not have fitted on the line before, and the lines seldom end where a sentence
does. A paragraph that looks like that has its soft breaks read as spaces; a
break asked for outright, two spaces or a backslash at the end of the line,
stays. Lines broken by hand — a short sentence and then another, a list of
things that happened, a verse — keep their breaks as before.
A document links to another by its path, `[the plan](/n/work/plan)`, or
now by its title, `[[Plan]]` or `[[Plan|the plan]]`. What each document
links to is kept in a new `links` table, rebuilt whenever its text is
written: from the browser, over the mount, or by an import. A title is
looked for in the linking document's space first, then in every other.
Renaming or moving a document, or a folder above it, rewrites the links
to it in every document that has one, in the same transaction: the path
to the new one, a title to the new name, with the label left as it was.
Each rewritten document keeps a revision of what it said before. A link
to something in the trash keeps its row, so it follows the thing when it
is restored somewhere else. A document created, or renamed, under a title
that a link was waiting for is found by that link from then on.
The reader draws `[[titles]]` as links, and a title no document has yet
as a dimmed one that offers to write that document. `gf` follows either
from the editor, and `[[` there completes titles from the search. `gb`
and `:backlinks` list the documents that link to the open one, with the
line each does it on; the line under the title counts them. A share page
shows these links as plain text, as it does every /n/ link.
The documents written before this are indexed once, when the server
starts and finds the table empty.
The share page used to put the key in the API path, and every embedded
picture or video carried it in its src, so a reverse proxy's access log
held whole links. The page now sends the key in the body of
POST /api/share/{token}. The answer sets a diary_share cookie
(__Secure- over HTTPS) scoped to /api/share/{token}/, HttpOnly,
SameSite=Strict, good for twelve hours, and signed over the key's hash.
Media is served from /api/share/{token}/media/{id} only to a browser
holding that cookie, so minting a new key or unsharing voids it.
The old /api/share/{token}/{key} routes are gone; links themselves are
unchanged, since the key was always in their fragment.
Every link with a target now gets rel="noopener noreferrer", and only
_blank is kept as a target. A named target such as target="foo" left
the page it opened a handle on this one — browsers imply noopener for
_blank alone — which it could use to send the tab to a lookalike login.
The share page turned links into the private workspace into plain text
with a regex over the sanitised HTML. A pattern over markup can end
inside an attribute, and on an engine that does not escape < and > in
attribute values a crafted link title became markup. The rewrite now
happens in renderMarkdown, on the parsed page.
$x^2$ inline, $$…$$ for display, and a ```math block, the way GitHub
writes them, in documents, shared pages and .md files alike. KaTeX loads
on first use; until then the math shows as its source. Only the woff2
fonts are bundled.
Single-user diary served as one Rust binary with the Svelte frontend
embedded.
- axum 0.8 + SQLite (sqlx), migrations run at startup
- stateless HMAC session cookie; credentials from .env, compared in
constant time
- FTS5 full-text search, user input rewritten into quoted prefix terms
- media uploads linked to entries by scanning bodies for /api/media/<id>,
which scopes share-token media access and entry deletion
- unguessable share links (192-bit token) serving a public read-only page
- vim keybindings via CodeMirror plus a modal browse layer and : command
line; disabled on touch devices