spaces: footnotes — text-token anchor, numbers derived at render time - #438
Open
nyblnet wants to merge 1 commit into
Open
spaces: footnotes — text-token anchor, numbers derived at render time#438nyblnet wants to merge 1 commit into
nyblnet wants to merge 1 commit into
Conversation
A footnote reference is the literal text `[^label]` in a block's html and the notes live in a document-level `doc.footnotes` map (bento/type's shape, so a note outlives the paragraph pointing at it). The number is derived from order of appearance when the page is drawn and is never stored. The anchor is the part that could not be copied from type. type anchors by character offset into a block's `text` runs; a spaces block carries `html`, and an offset into html is moved by canonicalize() reordering mark nesting, by sanitizeInline() unwrapping and stripping on every read, and by the CRDT merging html as one register. A text token moves with the prose through all three because it IS the prose. It also needs no new tag, attribute or href scheme — and a new allowlist entry would be a one-way data hazard, stripping this build's references in every build already shipped. Numbering is per page and derived, so inserting a reference above two others renumbers them with nothing in the file changing. Verified in the built shell: `[^1]` renders as "2" while block.html still says `[^1]`. Markdown round trips both halves — references pass through untouched, `[^1]:` definitions are lifted out before the block parser can downgrade them to paragraphs, and the exporter writes them back per page. Imports of several files deconflict colliding labels and rewrite that file's references; the subtree graft does the same. validate() reports a dangling reference (warning, with its block), an orphaned note (info, kept never deleted) and a label outside the token grammar. Neither can throw on a hand-edited `footnotes` field; every lookup uses Object.hasOwn. Print: the section is endnotes at the foot of the document page, kept whole across a sheet break. `float: footnote` is the typographic answer and no browser implements it, so it would silently not print at all. Costs 3,176 bytes on the shell.
Build size
Updated: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Footnotes in
bento/spaces. New logic is in a new file,spaces/src/footnotes.ts; the diffs inrender.ts,editor.ts,markdown.ts,model.tsandportable.tsare localized, because nine sibling branches are in those files.The anchor, and why it is this one
doc.footnotesis a document-level, label-keyed map of inline html — bento/type's shape (type/src/model.ts), for its reason: a note has to outlive the paragraph pointing at it.The anchor is where spaces cannot follow type. type anchors a reference by character offset into a block's
textruns, and can: it owns the run list and rewrites every offset in one place. A spaces block carrieshtml, and an offset into html is a position in one particular serialization of a position. Three things move it without changing a word of the prose:canonicalize()runs at every typing-run close and reorders mark nesting, coalesces runs, drops empty ones;sanitizeInline()runs on every read of untrusted html, unwrapping<p>, stripping a stale href, filtering class tokens;htmlas one register, which an offset in a second register cannot merge with.So the reference is stored in the html, as text:
[^label].sanitize.ts— no tag, no attribute, no href scheme. That is not tidiness.sanitize.ts's own comment onHREF_OKspells out why a new allowlist entry is a one-way data hazard: a reference written by this build would be silently stripped by every build already on disk, on the first edit that touched the block. The alternative designs (<sup class=…>,<a href="#fn/…">) all require one.[^1]is the markdown/pandoc source form. Verified by loading a footnoted document into a shell built fromorigin/main: the prose renders readably anddoc.footnotesround-trips byte-for-byte.textOf()see it because it is text.The cost, stated: while a block is being edited you see
[^1], not a superscript. That is deliberate —host.innerHTMLis written toBlock.htmlon everyinput, so marker markup injected into a contenteditable host is one keystroke from being committed, at which point the note is a literal "1" and the reference is gone. The derived form is drawn in reading view, print and the file-manager still, the waycalc.tsdraws an answer where it cannot be typed into.Numbering is derived, per page
Order of appearance, computed in
renderBlocks, never stored —calc.ts's rule and slides' dynamic-field rule. The label is an identifier, not a number (pandoc and Obsidian agree), which is also what makes the markdown round trip lossless.Authoring
Type
[^1]in a sentence; a numbered, empty slot appears at the foot of the page to write the note into. No new menu, no new gesture — the token is the gesture, the same opt-incalc.tsargues for. Emptying a note deletes the key rather than storing"".Verification
Shell cost: 3,176 bytes — 280,945 against 277,769 for
origin/mainbuilt in a temporary worktree with the same toolchain.Behavioural assertions, imported and run (not source greps — this rig has twice watched a source grep pass through a live regression). Each of the three load-bearing ones was sabotage-verified: broken, watched fail, restored.
[^1]:definitions out of markdownBrowser-verified on a uniquely-named copy over
http://127.0.0.1:5321, with a build-marker check first — which earned its keep immediately: the tab pool is shared with sibling agents and one navigated my tab away mid-check, so the first measurement was of a different app entirely.Measured with
getBoundingClientRect:[^1]renders as 2 and[^tea]as 3, whileblock.htmlstill says[^1].line-height: 0on the raised box). Marker box 7.7 × 14.5px, raised above the paragraph's vertical middle.href="#spfn-…"resolves to a real element; no raw[^is left anywhere in reading view; the section sits below the prose..sp-fnrefexists inside any editable host. Typing into a note writesdoc.footnotes; emptying it deletes the key; the other notes are untouched.#bento-doccarries the raw tokens and thefootnoteskey, and no derived markup is ever serialized.What is NOT verified
renderPage(…, {editable:false, printing:true})— the same call reading view makes, which is measured — and the@media printrules are confirmed present in the built shell's CSSOM. Nobody printed a page.input— repainting per keystroke would take the caret with it, since half of[^1is a signature change too. So a reference typed and not yet blurred has no slot for a second or two.rowsis.Flagged
scripts/test-spaces-model.tsandscripts/test-spaces-agent.tsare ops surface, edited under the file's-zone rule. No new rig file and noci.ymlstep — both are already registered, and the contract says not to add a CI step while the queue is contended.docs/spaces-agents.mdgains a Footnotes section and three validate codes;docs/DECISIONS.mdgains the anchor decision;spaces/CHANGELOG.mdunder[Unreleased], appended at the tail.