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
7 changes: 4 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ Modules under `src/`:
- **index.xsl** — entry point: `$vocab-hrefs` param, `main` template (async vocabulary prefetch via `ixsl:schedule-action/@document`, then overlay init), `local:document-uri()`.
- **RDFa2RDFXML-v3.xsl** — RDFa 1.1 → RDF/XML extraction. **Pure XSLT, no `ixsl:`** — it runs headless via `xslt3` for the test suite. Strictly W3C-conformant (RDFa 1.1 processing rules, section 7.5, including chaining via typed resources and `rdfa:usesVocabulary`). Covers `@about`/`@typeof`/`@property`/`@content`/`@datatype`/`@resource`/`@href`/`@src`/`@prefix` (and `xmlns:*`)/`@vocab` with bare terms, `@lang`+`@xml:lang` inheritance, base-URI resolution (`about=""` = the document, `<base href>` honored), and exclusion of `head`/`script`/`style`/`[data-role='rendering']` subtrees. Out of scope (documented future work): `@rel`/`@rev`, `@inlist`, safe CURIEs, `@datetime`/`<time>`, `xml:base`.
- **content-model.xsl** — the XHTML content model as data. **Pure XSLT, include-free** (no dependencies; `index.xsl` includes it once, before its consumers): a 1:1 transcription of the XHTML 1.0 Strict DTD (Second Edition, Appendix A.1 entities + element declarations) and the Appendix B prohibitions, extended with HTML5 `figure`/`figcaption`. `$cm:model` (element → allowed children + `#PCDATA` flag), `$cm:prohibitions`, predicates `cm:known`/`cm:block`/`cm:inline`/`cm:allows-child`/`cm:allows-text`/`cm:inline-only`/`cm:flow`/`cm:structural`/`cm:valid-nesting`. The **single source of truth** for nesting: editability init and keyboard machines (edit.xsl), boundary normalization (canonical-xhtml.xsl) and nesting lint (lint-xhtml.xsl) all consult it, so verdicts can't drift. `$cm:model` is built with `map:merge`+`map:entry` — the SaxonJS compiler fails an internal assertion on large nested map literals. The editor contract "region children are blocks" is deliberately NOT in this module.
- **vocab.xsl** — generic ontology RDF/XML → dropdown options. Handles typed elements (`<owl:Class rdf:about>`) and `rdf:Description`+`rdf:type` shapes (including striped one-triple-per-description files like DCMI terms); labels as attributes or child elements; terms outside the ontology namespace filtered out.
- **overlay.xsl** — the annotation form (single copy), form reading (`local:form-values` map), pre-filling (`local:populate-form`), show/hide with viewport clamping (`local:show-at`, shared with the edit dialogs).
- **vocab.xsl** — generic ontology RDF/XML → term maps (`local:vocab-terms` → `{uri, label}`, consumed by the typeahead). Handles typed elements (`<owl:Class rdf:about>`) and `rdf:Description`+`rdf:type` shapes (including striped one-triple-per-description files like DCMI terms); labels as attributes or child elements; terms outside the ontology namespace filtered out.
- **overlay.xsl** — the annotation form (single copy), form reading (`local:form-values` map), pre-filling (`local:populate-form`), show/hide with viewport clamping (`local:show-at`, shared with the edit dialogs). The property/type fields are typeaheads (typeahead.xsl); the datatype field stays a `<select>` (short XSD list) with a `urn:rdfa-editor:custom` sentinel + free-text `custom-datatype` input and the datatype↔lang mutual-exclusion handler.
- **typeahead.xsl** — autocomplete lookups for the **property** and **type** fields (replacing the long dropdowns; LDH's typeahead interaction ported with local synchronous filtering — no SPARQL/debounce, since vocabularies are already pooled). A stable wrapper `span[@data-field]` holds one of two states, swapped via `result-document` literal result elements: typing (`input.typeahead-input` + `ul.typeahead-menu`) or committed (`button.typeahead-value` + hidden input carrying the IRI, LDH-style). Filtering re-renders per keystroke from `local:vocab-terms` (prefix-ranked, capped, bold-highlighted match); keyboard nav mirrors the slash menu (Arrow/Enter/Escape, first option pre-selected); options commit on `ixsl:onmousedown` (before the input's `focusout`, children `pointer-events:none` so the `<li>` is the target). `local:typeahead-value` reads the committed hidden IRI, else a free-typed absolute IRI (`local:is-absolute-iri`), else the untouched button→edit value parked on `@data-editing-iri` (cleared on the first keystroke — the stale-selection guard). Escape closes the menu else the overlay (the innermost template fully owns the input's keys).
- **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.
Expand Down Expand Up @@ -67,6 +68,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), `select.mjs` (two-stage Ctrl+A, cross-host sweep delete, type-to-replace, canonical copy/cut, composite/clamp semantics, plus the real gestures: drag takeover with real mouse events, Shift+Click, Shift+Up/Down, gutter/backward/cross-region drags, drag-handle non-hijack), `notion.mjs` (slash menu, markdown input rules, context filtering) 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, type-to-replace, canonical copy/cut, composite/clamp semantics, plus the real gestures: drag takeover with real mouse events, Shift+Click, Shift+Up/Down, gutter/backward/cross-region drags, drag-handle non-hijack), `notion.mjs` (slash menu, markdown input rules, context filtering), `typeahead.mjs` (property/type autocomplete: filter, keyboard/mouse select, free-IRI entry, non-IRI rejection, edit-prefill button, stale-selection invalidation — via `typeahead-helper.mjs`) 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.
101 changes: 99 additions & 2 deletions rdfa-editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -722,11 +722,108 @@
box-shadow: 0 2px 8px rgba(244, 67, 54, 0.3);
}

#overlay input[name="custom-property"],
#overlay input[name="custom-type"] {
#overlay input[name="custom-datatype"] {
margin-top: 8px;
}

/* Typeahead lookups (property / type) */
.rdfa-editor-ui .typeahead-field {
position: relative;
display: block;
}

.rdfa-editor-ui .typeahead-menu {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 10002;
margin: 2px 0 0 0;
padding: 4px;
list-style: none;
background: white;
border: 1px solid #e0e0e0;
border-radius: 6px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
max-height: 260px;
overflow-y: auto;
}

.rdfa-editor-ui .typeahead-option {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
padding: 7px 10px;
border-radius: 4px;
font-size: 14px;
color: #212121;
cursor: pointer;
}

/* children never intercept the mousedown, so the <li> is always the event target */
.rdfa-editor-ui .typeahead-option * {
pointer-events: none;
}

.rdfa-editor-ui .typeahead-option:hover,
.rdfa-editor-ui .typeahead-option[aria-selected="true"] {
background: #e3f2fd;
color: #0d47a1;
}

.rdfa-editor-ui .typeahead-option-label strong {
font-weight: 700;
}

.rdfa-editor-ui .typeahead-option-uri {
flex: 0 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px;
color: #9e9e9e;
}

.rdfa-editor-ui .typeahead-value {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 10px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
font-size: 14px;
font-family: "Roboto", sans-serif;
text-align: left;
cursor: pointer;
}

.rdfa-editor-ui .typeahead-value:hover {
border-color: #2196f3;
}

.rdfa-editor-ui .typeahead-label {
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.rdfa-editor-ui .typeahead-clear {
flex: 0 0 auto;
color: #9e9e9e;
font-size: 16px;
line-height: 1;
padding: 0 2px;
}

.rdfa-editor-ui .typeahead-clear:hover {
color: #f44336;
}

/* Modal Dialog */
#output-modal {
position: fixed;
Expand Down
4 changes: 3 additions & 1 deletion src/index.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ version="3.0">
- RDFa2RDFXML-v3.xsl RDFa 1.1 to RDF/XML extraction (pure XSLT, tested headless)
- content-model.xsl XHTML Strict content model as data (pure XSLT, include-free)
- canonical-xhtml.xsl canonical serialization form (pure XSLT, tested headless)
- vocab.xsl generic ontology RDF/XML to dropdown options
- vocab.xsl generic ontology RDF/XML to term maps
- overlay.xsl annotation form: rendering, reading, populating, visibility
- typeahead.xsl autocomplete lookups for the property and type fields
- annotate.xsl right-click dispatch, apply/remove annotation, extraction modal
- edit.xsl XHTML editing: blocks, keyboard, toolbar, dialogs, drag-and-drop
- tables.xsl table blocks: insert dialog, row/column operations, cell traversal
Expand All @@ -35,6 +36,7 @@ version="3.0">
<xsl:include href="lint-xhtml.xsl"/>
<xsl:include href="vocab.xsl"/>
<xsl:include href="overlay.xsl"/>
<xsl:include href="typeahead.xsl"/>
<xsl:include href="annotate.xsl"/>
<xsl:include href="edit.xsl"/>
<xsl:include href="tables.xsl"/>
Expand Down
Loading
Loading