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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Modules under `src/`:
- **navigate.xsl** — FontoXML-inspired navigation: ToC drawer (recursive `for-each-group group-starting-with` outline, click-to-jump, section drag-reorder via `rdfae:section-of`), breadcrumb bar (element path + `rdfax: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.
- **lint-xhtml.xsl** — nesting lint (`lint:nesting-issues`, concatenated with `lint:element-issues` at every surfacing site). **Pure XSLT**, consults content-model.xsl only, so verdicts can't drift from normalization. Checks: invalid-nesting (known child not allowed by known parent), stray-text (non-ws text in an element-only container), prohibited-nesting (Appendix B), unknown-element (outside the model — preserved, unvalidated).
- **canonical-xhtml.xsl** — canonical serialization form. **Pure XSLT**, tested headless via `tests/canonical-driver.xsl` (it consults `cm:*`, so it is no longer standalone-compilable). **Two passes in fixed order**: `mode="canonical"` (drops `*[@data-role]` subtrees — nesting analysis must never see chrome; strips `contenteditable`/`draggable`/`class`/`id`/`style`/`tabindex`/`aria-*`/`data-*`; normalizes browser mess: `b`→`strong`, `i`→`em`, `font`/`u`/meaningless `span` unwrap, empty non-RDFa inline pruning, trailing-`br` drop, `br`-in-`pre`→newline; content-aware `div`: inline-content attributeless div → `p` (C7a), block-holding attributeless div unwraps (C7b); run-wrapper `p.rdfa-editor-run` unwraps (C11) unless RDFa-annotated; non-RDFa HTML5 sectioning wrappers unwrap (C12)), then `mode="cm-normalize"` (N-rules: N1 matches every inline-only element and decides on the PROCESSED children, so inner splits reach the fixed point in one bottom-up pass — an RDFa-bearing parent keeps its blocks as recursively-demoted inline via `mode="cm-demote"` (a list becomes nested spans, never a bare `li` in a `span`), a plain parent splits around them with shells keeping ALL attributes (an `<a href>` split by a block keeps its target on both halves — safe, the branch is non-RDFa by construction); N2 blockquote stray runs → `p`; N3/N4/N5 `ul`/`dl`/`tr` strays wrapped in `li`/`dd`/`td`; N5b section strays → rows; N6 invalid `table` children hoisted before it; N7/N8 Appendix B fixes: `pre` exclusions unwrap/alt-text, `a`-in-`a` → `span` with attributes kept). `cm:wrap-inline-runs` pulls only text and known-inline elements into its inline-only wrapper — blocks, ephemera, unknowns AND known non-inline strays (an `li` outside any list) pass through bare for lint to report, never wrapped into fresh invalid nesting. The entry template finally coerces region children to blocks (the editor contract, not the DTD's). Shared primitives `cm:normalize`, `cm:wrap-inline-runs`, `cm:coerce-children` are reused by edit.xsl's load-init and paste. RDFa attributes and `pre` whitespace are never touched.
- **canonical-xhtml.xsl** — canonical serialization form. **Pure XSLT**, tested headless via `tests/canonical-driver.xsl` (it consults `cm:*`, so it is no longer standalone-compilable). **Two passes in fixed order**: `mode="canonical"` (drops `*[@data-role]` subtrees — nesting analysis must never see chrome; strips only what the editor itself injects — `contenteditable`/`draggable`/`tabindex="-1"`, its own class *tokens* (`rdfa-editor-*`, `dragging`, `drop-*`, `rdfa-invalid` — subtracted, the attribute survives with its authored tokens) — plus `@style`, which no editing gesture can author (browser/paste mess). Authored `class`/`id`/`aria-*`/`data-*`/`role`/`hidden` round-trip untouched: the canonical form owes the author fidelity, and a host with a stricter storage policy overrides these templates at higher import precedence. `cm:authored-attributes()` backs the meaninglessness tests below. Normalizes browser mess: `b`→`strong`, `i`→`em`, `font`/`u` unwrap, `span` with no authored attribute unwraps, empty non-RDFa inline pruning (an empty `a[@id]` anchor is kept), trailing-`br` drop, `br`-in-`pre`→newline; content-aware `div` (no authored attribute): inline-content div → `p` (C7a), block-holding div unwraps (C7b); run-wrapper `p.rdfa-editor-run` unwraps (C11) unless RDFa-annotated; non-RDFa HTML5 sectioning wrappers unwrap (C12)), then `mode="cm-normalize"` (N-rules: N1 matches every inline-only element and decides on the PROCESSED children, so inner splits reach the fixed point in one bottom-up pass — an RDFa-bearing parent keeps its blocks as recursively-demoted inline via `mode="cm-demote"` (a list becomes nested spans, never a bare `li` in a `span`), a plain parent splits around them with shells keeping ALL attributes (an `<a href>` split by a block keeps its target on both halves — safe, the branch is non-RDFa by construction); N2 blockquote stray runs → `p`; N3/N4/N5 `ul`/`dl`/`tr` strays wrapped in `li`/`dd`/`td`; N5b section strays → rows; N6 invalid `table` children hoisted before it; N7/N8 Appendix B fixes: `pre` exclusions unwrap/alt-text, `a`-in-`a` → `span` with attributes kept). `cm:wrap-inline-runs` pulls only text and known-inline elements into its inline-only wrapper — blocks, ephemera, unknowns AND known non-inline strays (an `li` outside any list) pass through bare for lint to report, never wrapped into fresh invalid nesting. The entry template finally coerces region children to blocks (the editor contract, not the DTD's). Shared primitives `cm:normalize`, `cm:wrap-inline-runs`, `cm:coerce-children` are reused by edit.xsl's load-init and paste. RDFa attributes and `pre` whitespace are never touched. Sanitization (S1) drops active/embedding elements (`script`/`iframe`/`form`/`input`/…) but exempts `button` — inert once S2 strips its handlers, and content components (e.g. tab strips) legitimately store it.

The editor **emits absolute IRIs** in RDFa attributes (LinkedDataHub v6 convention — no CURIEs/`@vocab`/`@prefix` in output).

Expand Down
33 changes: 21 additions & 12 deletions src/blocks.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -99,18 +99,27 @@ version="3.0">
<xsl:template name="rdfae:replace-rendering">
<xsl:param name="island" as="element()"/>
<xsl:param name="content" as="node()*"/>
<xsl:for-each select="$island/*[@data-role = 'rendering']">
<xsl:sequence select="ixsl:call(., 'remove', [])[current-date() lt xs:date('2000-01-01')]"/>
</xsl:for-each>
<xsl:variable name="rendering" as="element()" select="rdfae:element('div')"/>
<xsl:for-each select="$rendering">
<ixsl:set-attribute name="data-role" select="'rendering'"/>
</xsl:for-each>
<!-- html method: XML's self-closing tags read as OPEN tags to the HTML
fragment parser and swallow following siblings -->
<ixsl:set-property name="innerHTML"
select="serialize($content, map{ 'method': 'html' })" object="$rendering"/>
<xsl:sequence select="ixsl:call($island, 'appendChild', [ $rendering ])[current-date() lt xs:date('2000-01-01')]"/>
<xsl:variable name="rendering" as="element()">
<div data-role="rendering">
<xsl:copy-of select="$content"/>
</div>
</xsl:variable>
<xsl:choose>
<xsl:when test="$island/*[@data-role = 'rendering']">
<xsl:for-each select="($island/*[@data-role = 'rendering'])[1]">
<xsl:result-document href="?." method="ixsl:replace-element">
<xsl:copy-of select="$rendering"/>
</xsl:result-document>
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<xsl:for-each select="$island">
<xsl:result-document href="?." method="ixsl:append-content">
<xsl:copy-of select="$rendering"/>
</xsl:result-document>
</xsl:for-each>
</xsl:otherwise>
</xsl:choose>
</xsl:template>

<!-- ............................ extension hooks ............................ -->
Expand Down
62 changes: 48 additions & 14 deletions src/canonical-xhtml.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,11 @@ version="3.0">
<xsl:template match="*[@data-role]" mode="cm:canonical" priority="2"/>

<!-- S1: active/embedding elements never survive into stored content (the
canonical form is the sanitization boundary for multi-user content) -->
canonical form is the sanitization boundary for multi-user content).
button is exempt: inert markup once S2 strips its handlers, and content
components (e.g. tab strips) legitimately store it -->
<xsl:template match="script | style | iframe | object | embed | applet
| form | input | button | select | textarea | link | meta | base" mode="cm:canonical" priority="3"/>
| form | input | select | textarea | link | meta | base" mode="cm:canonical" priority="3"/>

<!-- S1b: comments and processing instructions are noise (Word/HTML paste junk) -->
<xsl:template match="comment() | processing-instruction()" mode="cm:canonical"/>
Expand All @@ -137,10 +139,41 @@ version="3.0">
| @src[matches(normalize-space(.), '^data:', 'i')][not(matches(normalize-space(.), '^data:image/', 'i'))]"
mode="cm:canonical"/>

<!-- C2: editing-state and styling-hook attributes never serialize (tabindex is
injected to make block images focusable navigation islands) -->
<xsl:template match="@contenteditable | @draggable | @class | @id | @style | @tabindex
| @*[starts-with(name(), 'aria-')] | @*[starts-with(name(), 'data-')]" mode="cm:canonical"/>
<!-- the class tokens the editor itself puts on content elements: the region and
run/island markers plus transient gesture and lint state. Everything else in
@class is authored content -->
<xsl:function name="cm:authored-class-tokens" as="xs:string*">
<xsl:param name="class" as="xs:string?"/>
<xsl:sequence select="tokenize($class, '\s+')[.][not(starts-with(., 'rdfa-editor-') or . = ('dragging', 'drop-before', 'drop-after', 'drop-into', 'rdfa-invalid'))]"/>
</xsl:function>

<!-- attributes that survive canonicalization and so count as authored meaning:
everything except the editor's own injections (C2) and stripped handlers (S2).
C6/C7's meaninglessness tests consult this, so an element keeping an authored
class, id, aria-* etc. is never unwrapped as browser junk -->
<xsl:function name="cm:authored-attributes" as="attribute()*">
<xsl:param name="element" as="element()"/>
<xsl:sequence select="$element/@*[not(name() = ('contenteditable', 'draggable', 'style') or (name() = 'tabindex' and . = '-1') or matches(local-name(), '^on', 'i') or (name() = 'class' and empty(cm:authored-class-tokens(.))))]"/>
</xsl:function>

<!-- C2: only the editing-state attributes the editor itself injects are stripped
(tabindex="-1" makes block images/islands focusable navigation islands), plus
@style, which no editing gesture can author - browsers mint styled spans during
editing and paste, so inline style is browser mess like font/u, not content.
Authored attributes - class, id, aria-*, data-*, role, hidden - are content
and round-trip untouched: the canonical form owes the author fidelity. A host
with a stricter storage policy layers its own stripping by overriding these
templates at higher import precedence -->
<xsl:template match="@contenteditable | @draggable | @style | @tabindex[. = '-1']" mode="cm:canonical"/>

<!-- C2b: the editor's own class tokens are subtracted from @class; authored tokens
survive, and an attribute left empty drops -->
<xsl:template match="@class" mode="cm:canonical">
<xsl:variable name="tokens" select="cm:authored-class-tokens(.)" as="xs:string*"/>
<xsl:if test="exists($tokens)">
<xsl:attribute name="class" select="string-join($tokens, ' ')"/>
</xsl:if>
</xsl:template>

<!-- C3/C4: presentational aliases to their semantic elements -->
<xsl:template match="b" mode="cm:canonical">
Expand All @@ -160,15 +193,15 @@ version="3.0">
<xsl:apply-templates select="node()" mode="#current"/>
</xsl:template>

<!-- C6: a span left without RDFa or language attributes carries no meaning -->
<xsl:template match="span[not(@property or @about or @typeof or @resource or @content
or @datatype or @lang or @xml:lang)]" mode="cm:canonical">
<!-- C6: a span left without any authored attribute carries no meaning; RDFa,
language, class, id etc. all count as meaning -->
<xsl:template match="span[empty(cm:authored-attributes(.))]" mode="cm:canonical">
<xsl:apply-templates select="node()" mode="#current"/>
</xsl:template>

<!-- C7a: browser-generated attributeless div with inline content becomes a
paragraph; RDFa-bearing divs pass -->
<xsl:template match="div[not(@property or @about or @typeof or @resource)]
paragraph; divs bearing any authored attribute pass -->
<xsl:template match="div[empty(cm:authored-attributes(.))]
[empty(*[cm:block(local-name(.))])]" mode="cm:canonical">
<p>
<xsl:apply-templates select="@* | node()" mode="#current"/>
Expand All @@ -178,7 +211,7 @@ version="3.0">
<!-- C7b: an attributeless div holding blocks is a semantics-free grouping
wrapper (p may not contain blocks) - unwrap to its children; stray inline
residue is re-coerced by pass 2 in the parent's context -->
<xsl:template match="div[not(@property or @about or @typeof or @resource)]
<xsl:template match="div[empty(cm:authored-attributes(.))]
[exists(*[cm:block(local-name(.))])]" mode="cm:canonical">
<xsl:apply-templates select="node()" mode="#current"/>
</xsl:template>
Expand All @@ -202,8 +235,9 @@ version="3.0">
</xsl:template>

<!-- C8: empty non-RDFa inline elements are junk; RDFa-bearing empties
(hidden <span property resource/> definitions) are kept by C6's predicates -->
<xsl:template match="(strong | em | a | code)[not(normalize-space(.))][not(.//img)]
(hidden <span property resource/> definitions) are kept by C6's predicates,
and an empty a[@id] is an anchor target, not junk -->
<xsl:template match="(strong | em | a | code)[not(normalize-space(.))][not(.//img)][not(@id)]
[not(@property or @about or @typeof or @resource or @content)]" mode="cm:canonical" priority="1"/>

<!-- C10: line structure inside pre is text, not markup -->
Expand Down
36 changes: 21 additions & 15 deletions src/edit.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,16 @@ version="3.0">
<xsl:template name="rdfae:init-region">
<xsl:param name="region" as="element()"/>

<!-- the region is the canvas' focusable floor. Only leaf text hosts are
contenteditable, so the surface between them - sibling margins, the
handle gutter, a structural container's own box, chrome on a structural
block - has no focusable ancestor at all, and a press there drops focus
out of the editor entirely (which hosts read as leaving it, and a press
on a drag handle cannot preventDefault without killing dragstart). With
tabindex the region absorbs that focus instead: same idiom as the block
images and object-block islands in rdfae:init-block, out of the tab
order, and stripped by the canonical form -->
<ixsl:set-attribute name="tabindex" select="'-1'" object="$region"/>
<!-- boundary-normalize invalid host markup (bare text in blockquote,
blocks inside p, stray inline at region level, ...) before
editability init; the probe keeps the valid case zero-churn -->
Expand All @@ -390,8 +400,11 @@ version="3.0">
<xsl:if test="$invalid">
<xsl:variable name="fixed" as="node()*"
select="cm:wrap-inline-runs(cm:normalize($region/node()), 'p')"/>
<ixsl:set-property name="innerHTML"
select="serialize($fixed, map{ 'method': 'html' })" object="$region"/>
<xsl:for-each select="$region">
<xsl:result-document href="?." method="ixsl:replace-content">
<xsl:copy-of select="$fixed"/>
</xsl:result-document>
</xsl:for-each>
</xsl:if>
<!-- an empty region cannot hold a caret: seed a paragraph (the
empty-blockquote idiom in rdfae:init-block) -->
Expand Down Expand Up @@ -1651,19 +1664,12 @@ version="3.0">
<xsl:with-param name="host" select="$host"/>
<xsl:with-param name="range" select="$range"/>
</xsl:call-template>
<!-- method html: XML's self-closing <p/> reads as an OPEN tag to the
HTML fragment parser and swallows following siblings -->
<xsl:variable name="stage" as="element()" select="rdfae:element('div')"/>
<ixsl:set-property name="innerHTML" select="serialize($blocks, map{ 'method': 'html' })" object="$stage"/>
<xsl:variable name="count" as="xs:integer" select="xs:integer(ixsl:get($stage, 'childNodes.length'))"/>
<xsl:iterate select="1 to $count">
<xsl:param name="anchor" select="$host"/>
<xsl:variable name="node" select="ixsl:get($stage, 'firstChild')"/>
<xsl:sequence select="ixsl:call($anchor, 'after', [ $node ])[current-date() lt xs:date('2000-01-01')]"/>
<xsl:next-iteration>
<xsl:with-param name="anchor" select="$node"/>
</xsl:next-iteration>
</xsl:iterate>
<xsl:variable name="count" as="xs:integer" select="count($blocks)"/>
<xsl:for-each select="$host">
<xsl:result-document href="?." method="ixsl:insert-after">
<xsl:copy-of select="$blocks"/>
</xsl:result-document>
</xsl:for-each>
<xsl:for-each select="$host/following-sibling::*[position() le $count]">
<xsl:call-template name="rdfae:init-block">
<xsl:with-param name="block" select="."/>
Expand Down
Loading
Loading