The prototype (through commit b8466cf) is functionally rich: structured-block XHTML editing, strict RDFa 1.1 annotation + extraction, canonical serialization, unified undo/redo, ToC/breadcrumb/lint/find, DnD — all XSLT 3.0 + SaxonJS with 31 headless tests and ~115 Playwright assertions. The user asks: what is missing for (a) production readiness and (b) embedding into LinkedDataHub as the WYMEditor replacement (the jQuery/iframe WYSIWYG editing ldh:XHTML blocks stored as rdf:XMLLiteral).
canonical-xhtml.xsldoes NOT stripon*event-handler attributes (onclick,onerror, …) norjavascript:/data:values in@href/@src— the canonical transform is the sanitization boundary for stored content; XSS surface once content is multi-user. Add strip rules + lint checks + fixtures.- Element allowlist question: canonicalization currently passes unknown elements through (shallow-copy) — decide pass-through vs allowlist for embedded content (iframe/object/embed/form should not survive).
- HTML paste with sanitization: currently plain-text only. We already own the cleanup machinery — parse
clipboardDatatext/htmlviaparse-xml/fragment parsing and run it throughmode="cm:canonical"+ the new sanitization rules; insert clean fragment. (Word/Google-Docs paste is the single biggest prod-usability item.) - Nested lists (indent/outdent via Tab/Shift+Tab in
li),ul↔olconversion of an existing list. - h4–h6 in the block-type select (cheap);
code/sub/supinline toggles (cheap — theformat-inlinemachinery is generic). - Image upload (LDH has upload flows) — URL-only first; wire LDH upload later.
- Caret restoration after undo (currently approximate: first host) — snapshot the caret as (block index, text offset) alongside innerHTML.
- Cross-text-node find matches (currently single-text-node only).
- Multiple editable regions per page: everything assumes a single
id('content')(rdfae:content(), block-of, undo snapshots, lint, ToC). LDH pages have MANY XHTML blocks. Refactor to instance-scoped editing: editable roots resolved by class/typeof convention or param; undo stack per root (or keyed snapshots); ToC/breadcrumb scoped to the active root. - Keyboard/a11y: Escape closes dialogs/overlay; focus trap in dialogs; ARIA roles/labels on toolbar, drawer, dialogs, breadcrumb; keyboard block move (Alt+Arrow) as DnD alternative;
prefers-reduced-motion. - i18n: UI strings hardcoded English — externalize following LDH's translations.rdf pattern.
- Browser matrix: only Chromium is CI-verified; Firefox + Safari manual/automated passes (Playwright Firefox/WebKit installable).
- IME: composition guard exists for Enter/Backspace; audit beforeinput coalescer under composition.
- Touch: drag handles unusable on touch; fallback (long-press or keyboard move).
@rel/@rev,@inlist, safe CURIEs,@datetime/<time>,xml:base(documented out of scope; @rel/@rev first — real-world pages hit it).- Vocabulary breadth: schema.org vocab file ships nowhere; 118-option selects won't scale — typeahead (LDH has the pattern) over vocab terms replaces the selects; domain/range-aware ranking as follow-up.
- CI: GitHub Actions running
tests/run-tests.sh+ the Playwright suites (currently scratchpad-only — move smoke scripts intotests/browser/in-repo). - Packaging: CSS lives in the demo
index.html— extractrdfa-editor.css; demo page vs library separation; README/integration docs; versioning. - Namespace: done —
rdfae:=https://w3id.org/atomgraph/rdfa-editor#(withcontent-model#andlint#sub-namespaces); register therdfa-editorredirect at perma-id/w3id.org.
B. LinkedDataHub embedding contract (WYMEditor replacement) — ships as docs/ldh/MIGRATION.md in this repo
Mapped integration surface (all in LinkedDataHub/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/):
- Form control swap —
imports/default.xsl:1236-1258renders<textarea name="ol" class="wymeditor">withserialize()d XMLLiteral children + hiddenlt=rdf:XMLLiteral input. Replacement: render an editable container holding thexhtml:divcontent (our structured-block root) + a hiddenolinput + the sameltinput. - Instantiation —
client/form.xsl:144-160(ldh:RenderRowFormontextarea.wymeditor, jQuery plugin + iframe). Replacement: a RenderRowForm template on our container class running per-instance init (editability + chrome). No JS assets at all (SEF-compiled) — onlyrdfa-editor.css. - Submit sync — LDH reads
textarea.valueat submit with no update() hook;client/functions.xsl:221-226wraps theolvalue in<div xmlns=…>for the XMLLiteral. Replacement hook:ldh:FormPreSubmit(form.xsl:178-198) template serializing the container's canonicalized CHILDREN (no wrapper div) into the hiddenolinput. - Assets gate —
layout.xsl:335/342-344/355/384-386($load-wymeditor): swap for a CSS-only include; jQuery+WYMEditor JS/skins removable after cutover. - Multiple blocks — one inline edit form per block (
client/block.xsl:314-354), re-rendered after save; requires the multi-instance refactor (noid('content')singletons; per-instance undo). - Vocabularies —
/ns?uri=<vocab>&accept=application/rdf+xmlendpoint (constructor.xsl:203-209 pattern) viaixsl:promise; typeahead control (LDH typeahead.xsl precedent) replaces the selects. - i18n — strings into
translations.rdf(key('resources', id, document(...))+ac:label). - Feature flag — decision DEFERRED (user); document options:
lapp:Applicationproperty / XSL param via web.xml / hard cutover. - Build — modules pulled into the LDH webapp tree at build time and
xsl:imported fromclient.xsl; existing pomxslt3-he … -relocate:onstep compiles everything (pom.xml:39, 383-391). Conflict audit checklist: the extractor entry (named-onlyrdfax:extract-rdfasince the unnamed-modematch="/"was dropped) vs LDH root templates,bodykeydown fallback, host-level event templates,xsl:output,id('content')assumptions. - v6 note — view mode renders XMLLiteral via identity transform (
imports/default.xsl:1489-1503); the eventual v6 in-place model (edit the view markup, PUT canonical doc) is the successor to this form-control integration.
- M1 — Hardening (THIS ROUND, implemented): sanitization + HTML paste + a11y/keys + undo caret restoration + in-repo tests/CI + CSS extraction + README; migration plan document in
ldh/. - M2 — Multi-instance component (DONE):
.rdfa-editor-contentregions, region-keyed undo, scoped ToC/source,rdfaEditor*state prefix,.rdfa-editor-uiCSS scoping; LDH integration compile-proven (docs/ldh/MIGRATION.md §10). - Tables (DONE): composite table blocks — rows×cols insert dialog (optional header row + caption), positional row/column operations gated on
rdfae:has-spans, Tab/Shift+Tab + Enter cell traversal that grows the grid at its bottom edge (src/tables.xsl). - M3 — LDH swap: the contract above (LDH-side patches + build wiring + e2e in an LDH dev instance).
- M4 — Vocabulary UX: typeahead over ontology terms from
/ns, schema.org vocab, domain/range-aware ranking. - M5 — Editing completeness: nested lists, h4–h6, code/sub/sup, image upload via LDH, cross-node find, i18n strings, Firefox/Safari passes, touch fallback.
- Beyond:
@rel/@revextraction, v6 in-place document editing (PUT canonical XHTML), review/comments.
User decisions: LDH materials in a separate folder of this repo (docs/ldh/); feature flag deferred (documented); this round = migration plan doc + M1 implementation.
- Drop subtrees (documented blocklist, priority above shallow-copy):
script | style | iframe | object | embed | applet | form | input | button | select | textarea | link | meta | base. - Strip attributes:
@*[matches(local-name(), '^on', 'i')](empty template). - Neutralize URL schemes: drop
@href/@srcwhoselower-case(normalize-space(.))starts withjavascript:/vbscript:/data:— exceptdata:image/allowed in@src. - Lint additions (shared semantics):
unsafe-attribute(on*),unsafe-urlchecks insrc/lint-rdfa.xsl. - Fixtures:
canonical/11-sanitization(script/iframe dropped; onclick stripped;javascript:href attr dropped, element kept;data:imagesrc kept),lint/10-unsafe.
text/htmlnon-empty → HTML path; else existing plain-text path.- Parse: detached
$carrier := createElement('div')+innerHTML :=clipboard HTML (scripts inert when detached; XPath over detached nodes already proven). - Sanitize:
$clean :=apply-templates$carrier/node()inmode="cm:canonical"(now incl. D1 rules). - Wrap stray top-level inline runs:
for-each-group group-adjacent="boolean(self::p|self::h1|…block…)"→ non-block groups wrapped in<p>. - Re-materialize XDM → live DOM:
serialize()the fragment →$stage := createElement('div')+innerHTML(safe post-sanitization). - Insert:
- Inline-only fragment:
range.deleteContents(); move$stagechildNodes into acreateDocumentFragment(countedfirstChildloop, order preserved); capture$lastref before the move;range.insertNode($frag); caret after$lastviardfae:place-caret(parent, count(preceding-sibling)+1). - Blocks into p/h/blockquote host: push-undo;
rdfae:split-blockat caret; insert pasted blocks after the first half (xsl:iterateanchor pattern from section drop);rdfae:init-blockeach (editable + chrome);rdfae:ensure-placeholderon halves; caret at end of last inserted block. - Into li/figcaption: flatten to
string($stage)through the plain-text path (documented).
- Inline-only fragment:
- Single push-undo before, after-mutation after; one Ctrl+Z reverts the paste.
- Escape closes: keydown templates on
#overlayand the three dialogs (input events bubble to the containers) → preventDefault +rdfae:hide-overlay/rdfae:hide-dialogs. - Alt+ArrowUp/Down moves the current block (keyboard DnD alternative): new dispatcher branch BEFORE the plain-arrow branch (plain arrows must also gain a
not(altKey)guard);before(prev)/after(next)sibling move; push-undo + after-mutation; caret survives (node refs move with the block); no sibling → no-op. - ARIA in render templates:
role="dialog" aria-modal="true" aria-labelon overlay + dialogs;aria-labelmirroring@titleon toolbar buttons +role="toolbar"on#edit-toolbar;role="navigation" aria-labelon#toc-drawerand#breadcrumb; lint badge becomes a real<button class="lint-badge">. - Focus return: hide-overlay/hide-dialogs focus the
activeBlockhost when present. Full focus trap out of scope (documented).
rdfae:push-undo(and both apply-undo/redo current-state pushes) capture the caret when the selection anchor is a TEXT node inside content, as data attrs on the stash entry (stash lives outside #content):data-block(index among content children),data-node(index among the block's chrome-free descendant text nodes),data-offset.rdfae:restore-snapshotresolvescontent/*[$bi]→ chrome-freetext()[$ni]→collapse(min(offset, length)); bare-[$index]predicates; fallback = current first-host behavior. Caret stored with a snapshot = caret when that state existed → symmetric for undo and redo.
tests/browser/{editor,features,fixes}.mjs(ported suites;PORT/BASE_URLenv);package.json(private; devDepplaywright; scriptstest→ run-tests.sh,test:browser)..github/workflows/ci.yml: setup-node,npm ci,npx playwright install chromium --with-deps,python3 -m http.serverbackground, run both.rdfa-editor.css: all editor-contract styles move out of index.html (overlay/statement/dialogs/buttons, chrome + gutter, DnD marks, ToC drawer, breadcrumb/crumbs, lint badge +.rdfa-invalid, toolbar,#content *[property]-family RDFa highlighting, scroll-margin, focus outline, modal); demo-only styles stay (body/nav/test-section/demo content box).README.md: overview, quickstart, test commands, architecture pointer,docs/ldh/MIGRATION.mdpointer.
The section-B contract expanded into a step-by-step LDH patch plan: default.xsl form-control swap; form.xsl RenderRowForm init + FormPreSubmit serialization templates; functions.xsl ol+lt expectations (children only, no wrapper div); layout.xsl asset gate (CSS-only, WYMEditor/jQuery removable at cutover); translations.rdf strings; deferred feature-flag options; build wiring (modules copied into the LDH webapp tree, xsl:imported from client.xsl, compiled by the existing pom xslt3-he step); conflict-audit checklist (unnamed-mode match="/", body keydown, host event templates, xsl:output, id('content') singletons); prerequisites (M2 multi-instance, M4 typeahead).
- P1 Sanitization: D1 + fixtures. Gate: all headless loops green (12+11+10 after additions).
- P2 HTML paste: D2. Gate: new Playwright paste assertions (inline junk canonicalized; multi-block Word-ish paste; onclick/javascript: stripped; li flatten; single-undo revert).
- P3 A11y/keys: D3. Gate: Escape/Alt-move/ARIA assertions.
- P4 Caret restoration: D4. Gate: undo returns caret to pre-mutation block/offset (typing + structural cases).
- P5 Packaging: D5 + D6 + README + full regression (all suites, all loops) + commits.
bash tests/run-tests.sh (3 loops, grown fixture set); npm run test:browser (ported suites + new M1 assertions); manual Firefox pass note. Canonical-source purity re-asserted after sanitization changes (no on*/javascript:/script in output).