Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Modules under `src/`:
- **annotate.xsl** — the unified right-click dispatcher (edit vs create), `local:apply-annotation` (single write path for RDFa attributes), shared `local:wrap-range`/`local:unwrap-element` (used by annotations AND inline formatting), invalid-selection flash, the shared output modal (`local:show-output`).
- **edit.xsl** — the XHTML editor. **Recursive editability init** (`local:init-block`): an element is a *text host* (contenteditable) while its content model allows text and it holds no block children; mixed flow containers (`li`/`td`/`dd`/… with blocks) and structural containers (blockquote, lists, figure, table) lock their own markup and recurse into their parts (`figure` is always composite: image island + caption host); `img` never editable but given `tabindex="-1"` so a block image is a focusable **navigation island**. **Run wrappers**: the stray inline runs of a mixed flow container (`<li>text<ul>…</ul></li>`) get an ephemeral `p.rdfa-editor-run` host (`local:wrap-stray-runs`) so they stay editable — unwrapped again at canonicalization (C11), so mixed content round-trips byte-identical; structural gestures promote a wrapper to a real paragraph. **Load-init normalization**: each region is probed for invalid nesting (`cm:valid-nesting`, stray text in structural containers, stray region-level inline) and rebuilt through `mode="cm-normalize"` only when invalid (the valid case is zero-churn; host-page node references into a rewritten region go stale, same as undo restore). First-child chrome injection is a **top-level-only** affordance (`span[@data-role='chrome']`, hover-revealed via CSS) — nested hosts never carry drag handles. The Enter/Backspace state machine (split/merge primitives with explicit caret placement): splitting a run wrapper *promotes* it (marker class off); E4a — Enter on the empty last item of a *nested* list outdents one level (progressive), E4b — top-level exits to a paragraph; B2 merges text blocks, B2b descends into a preceding non-composite container's (blockquote/list) last host — bare text in the container itself would be invalid — while tables/figures stay hard boundaries (B3); B4 merges an `li` into the *visually preceding line* (`local:merge-host-in` — the last host of the previous item, unless it sits inside a nested table/figure: composites are hard boundaries at any depth, the gesture stays inert), B4b outdents the first item of a nested list, B4c merges the first item of a container-nested list (cell, quote, dd) into the preceding line inside the container — dissolving the emptied list and collapsing the container back to a text host — B7 exits the first block of a blockquote upward (emptied quote removed); E4b anchors the list-exit paragraph *before* removal, so it lands where the list was (inside the cell/quote for nested lists); E6 anchors the caption-exit paragraph after the figure/table itself (which may be nested). **Ancestor walks clamp at the region root** (`[exists(local:block-of(.))]` on `local:item-of`, the Tab cell lookup and the quote-toggle's blockquote lookup) — a host-page li/td/blockquote wrapping an embedded region is never touched. A focused image island deletes its *figure* when it has one, else just the image — never the whole containing list/table. **Tab/Shift+Tab** indent/outdent list items (`local:list-indent`/`local:list-outdent`: indent moves the item into a trailing-or-fresh nested list *inside the previous item* per `ul → (li)+`; outdent moves it after the container item, demoting followers into a nested list inside it; `local:collapse-container` reverts an emptied container to a text host; extremes flash) — the innermost context wins over table-cell traversal (li-in-td indents, a table nested in an item traverses). The **quote toggle** (`format-quote`) wraps the current host block in a `blockquote` where the parent admits one / unwraps all children out (refused with a flash when the quote carries RDFa — triples would drop); the block-type select is **host-based** (`local:current-host`): a paragraph inside a quote converts alone, non-convertible hosts disable it. The **insert buttons** (+¶, lists) place the new block per the content model (`local:insert-block-at-caret`): after the caret's host when its parent admits the kind, *inside* a leaf flow host otherwise (a list inserted in a cell nests in the cell; in a list item it becomes a sublist), after the top-level block as a last resort. **Paste** is cm-driven: clipboard HTML runs `mode="canonical"` + `mode="cm-normalize"`, then blocks land *inside* a mixed flow host (`local:paste-into-flow-host` — head/tail runs wrapped, host re-inited as container), or as siblings of an inline-only host via split-and-thread (when `cm:allows-child` permits), or flatten to text (caption, dt). Drag-and-drop ported from LinkedDataHub's `client/block.xsl` (handle-gated `draggable`, midpoint before/after marks). Arrow-key block crossing walks `local:nav-targets` (editable hosts **plus** block images, in document order): a host gets a caret, an image is *selected* (`local:select-image` focuses it, clearing the caret) — so images are never skipped; on a selected image, arrows step to the adjacent target and Backspace/Delete removes the whole figure (no confirm, undo-covered). Table cell keys (Tab/Enter) and the table dialog live in **tables.xsl**.
- **tables.xsl** — table blocks as a composite kind: the insert dialog (rows×cols + header-row + caption, modeled on the figure dialog), positional row/column operations (`local:op-insert-row`/`-column`, `-delete-row`/`-column`) gated behind `local:has-spans` (disabled on pasted `colspan`/`rowspan` grids — positional edits would corrupt a spanned table) and toolbar-synced via `local:sync-table-toolbar` riding `local:update-breadcrumb`, plus `local:table-tab`/`local:table-enter` cell traversal that appends a body row at the bottom edge. Cells are `%Flow;` containers: every caret landing resolves through `local:first-host-in`/`local:last-host-in`, so a cell holding blocks lands the caret in its first/last editable host; the Tab dispatcher (edit.xsl) passes the *cell* as the traversal host even when the caret sits in a nested block. Deleting the last body row/column is a no-op — the confirm-guarded delete-block button removes the whole table. **Undo hazard:** a chrome `span` serialized as a direct child of `<table>` is foster-parented out by the HTML fragment parser on `innerHTML` restore, so `local:restore-snapshot` drops stray region-child chrome and re-injects (idempotent) before caret resolution.
- **select.xsl** — region-scoped select-all and cross-host selection delete (dispatch lives in edit.xsl's keydown/body/paste templates, mirroring the tables.xsl split). **Two-stage Ctrl/Cmd+A** (Docs-style): stage 1 stays native — the browser scopes select-all to the focused host; stage 2 (host already fully selected per the chrome-aware `local:at-*` probes, or empty, or the selection already spans hosts) selects all blocks of the region as a **document-level range** (`local:select-region`) — it paints natively across host boundaries and never reaches the host page. Ctrl+A away from a caret also selects editor content, never the page: from the body it targets the swept region, else `local:active-root()` (selection anchor → last focused host → first region), and a focused image island is its own fully-selected unit (stage 2 directly, with Backspace/Delete then running the delete machine instead of the island's figure-delete); native page select-all happens only when no region exists. One **delete machine** (`local:delete-cross-host-selection`) serves stage-2 selections and mouse sweeps alike (`local:selection-crosses-hosts()` gates it), fired by Backspace/Delete from host *or* body focus — a sweep from the page background leaves focus on body. Deletion is **block-granular**, never one raw `deleteContents` across the range: fully covered blocks are removed whole; partial edge hosts get sub-range deletes scoped *inside* the host; composites holding a range boundary never lose structure — their covered cells are cleared and flow cells collapse back to text hosts (`local:clear-host`; B3/B4 doctrine at partial coverage) while a *fully* covered composite is removed whole; non-composite edge remnants merge Docs-style via `local:merge-into-previous` with the caret at the seam (never with `pre` — B6); emptied structural containers are pruned (`local:prune-husks`), chrome is re-injected (idempotent), and an emptied region is reseeded with a fresh `p` host (`local:seed-region`). The range is **clamped to a single region** (the start's, else the first swept) — one gesture, one region-keyed undo entry; other regions stay byte-identical. Typing/Enter/Tab/paste over a cross-host selection are suppressed (type-to-replace is future work); Ctrl+C and plain arrows stay native.
- **undo.xsl** — unified snapshot undo/redo over `#content` innerHTML: every mutating handler calls `local:push-undo` first and `local:after-mutation` last; plain typing coalesces into ~1s bursts via `ixsl:onbeforeinput`. Ctrl/Cmd+Z / Shift+Z / Ctrl+Y are intercepted (native undo is replaced). Stacks live in a hidden DOM stash (`#undo-storage`, `data-role="storage"`) — JS arrays don't survive the IXSL boundary and sequence-valued window properties keep only their first item. Caret restoration after undo is approximate (first host) by design.
- **navigate.xsl** — FontoXML-inspired navigation: ToC drawer (recursive `for-each-group group-starting-with` outline, click-to-jump, section drag-reorder via `local:section-of`), breadcrumb bar (element path + `rdfa:in-scope-subject` at the caret), lint surfacing (markers + badge + issues modal), find & replace (single-text-node matches, `nodeValue`-rewrite replace-all — annotation-safe by construction).
- **lint-rdfa.xsl** — RDFa lint logic. **Pure XSLT**, tested headless via `tests/lint-driver.xsl` (`xsl:import` resolves the output-method conflict). Reuses the extractor's resolution functions so lint verdicts can't drift from extraction semantics. Checks: term-unresolvable, empty-href, content-resource-conflict, empty-literal, about-relative.
Expand Down Expand Up @@ -65,6 +66,6 @@ Run `make sef` after any XSLT change; run the tests after any extractor change.
- **Undo contract**: every mutating handler pushes a snapshot first (`local:push-undo`, optionally with a pre-captured `$snapshot` when the operation can fail) and calls `local:after-mutation` last — never push from shared primitives. Undo/redo restore invalidates all node-valued window properties (cleared in `local:restore-snapshot`) and closes overlay/dialogs. The caret rides along on stash entries as (block, text-node, offset) data attributes and is restored on undo/redo.
- **Content markers**: anything written into `#content` for UI purposes must be `@class` (or `aria-*`) only — both are canonicalization-stripped. `@title` is NOT stripped and is forbidden as a marker.
- **SaxonJS gotchas**: computed numeric predicates (`[xs:integer(...)]`) are evaluated as booleans in some contexts — bind to a variable and use the bare `[$index]` form. JS arrays returned by `ixsl:call` marshal to XDM sequences (empty array = empty sequence). Large nested map literals fail the compiler with an internal assertion — build them with `map:merge` over `map:entry` (see `$cm:model`).
- Browser tests live in `tests/browser/` (`npm run test:browser` self-serves the repo): `editor.mjs`, `features.mjs`, `fixes.mjs`, `hardening.mjs`, `multiinstance.mjs`, `tables.mjs`, `datatype.mjs`, `inspector.mjs`, `nesting.mjs` (content-model foundation), `authoring.mjs` (nesting gestures) and `invariants.mjs` (the cross-product net: a uniform gesture battery in every caret context asserting properties that must always hold — no orphan text outside an editable host, no nested hosts, chrome top-level only, run wrappers editable, zero lint issues, undo restores the exact baseline), all against `tests/fixture-nesting.html`. Scenario tests assert *semantics* (where things land); the invariant suite catches *validity/editability/undo* regressions in combinations nobody enumerated. CI runs both headless loops and the browser suites (`.github/workflows/ci.yml`).
- Browser tests live in `tests/browser/` (`npm run test:browser` self-serves the repo): `editor.mjs`, `features.mjs`, `fixes.mjs`, `hardening.mjs`, `multiinstance.mjs`, `tables.mjs`, `datatype.mjs`, `inspector.mjs`, `nesting.mjs` (content-model foundation), `authoring.mjs` (nesting gestures), `select.mjs` (two-stage Ctrl+A, cross-host sweep delete, composite/clamp semantics) and `invariants.mjs` (the cross-product net: a uniform gesture battery in every caret context asserting properties that must always hold — no orphan text outside an editable host, no nested hosts, chrome top-level only, run wrappers editable, zero lint issues, undo restores the exact baseline), all against `tests/fixture-nesting.html`. Scenario tests assert *semantics* (where things land); the invariant suite catches *validity/editability/undo* regressions in combinations nobody enumerated. CI runs both headless loops and the browser suites (`.github/workflows/ci.yml`).
- **Sanitization**: `canonical-xhtml.xsl` is the storage boundary — it drops script/style/iframe/object/embed/applet/form controls/link/meta/base subtrees, comments/PIs, all `on*` attributes, and `javascript:`/`vbscript:`/`data:` URLs (`data:image/*` allowed in `@src`). HTML paste goes through this same mode. Lint mirrors these as `unsafe-attribute`/`unsafe-url`.
- Editor-contract CSS lives in `rdfa-editor.css` (host pages include it; the container needs ~2.5em left padding for the gutter handles); `index.html` keeps only demo styles.
8 changes: 8 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,14 @@ <h4>Editing</h4>
block, and <strong>Backspace</strong> at the start of a block to merge it
with the one above.</p>

<h4>Selecting and deleting</h4>
<p>Press <strong>Ctrl/Cmd+A</strong> to select the current block's text;
press it again to select the whole document (never the surrounding
page). You can also drag a selection across several blocks with the
mouse. <strong>Backspace</strong> or <strong>Delete</strong> removes
whatever is selected, and <strong>Ctrl/Cmd+Z</strong> undoes any
change.</p>

<h4>The toolbar</h4>
<p>Use the toolbar (top-left) to change a block's type, make text
<strong>bold</strong> or <em>italic</em>, add links, lists, images and
Expand Down
Loading
Loading