Skip to content
Open
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
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
2 changes: 1 addition & 1 deletion tests/expected/canonical/01-chrome-strip.xhtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div xmlns="http://www.w3.org/1999/xhtml" about=""
<div xmlns="http://www.w3.org/1999/xhtml" id="content" about=""
typeof="https://www.w3.org/ns/ldt/document-hierarchy#Container">
<p>First paragraph.</p>
<h2>A heading</h2>
Expand Down
2 changes: 1 addition & 1 deletion tests/expected/canonical/02-rendering-strip.xhtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div xmlns="http://www.w3.org/1999/xhtml" about="">
<div xmlns="http://www.w3.org/1999/xhtml" id="content" about="">
<figure about="#top-selling" typeof="https://w3id.org/atomgraph/linkeddatahub#ResultSetChart">
<figcaption property="http://purl.org/dc/terms/title">Top selling</figcaption>
<span property="http://spinrdf.org/spin#query" resource="#products-query"></span>
Expand Down
2 changes: 1 addition & 1 deletion tests/expected/canonical/03-inline-mess.xhtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div xmlns="http://www.w3.org/1999/xhtml" about="">
<div xmlns="http://www.w3.org/1999/xhtml" id="content" about="">
<p>Some <strong>bold</strong> and <em>italic</em> and colored
and underlined and styled text,
plus <strong><em>nested</em></strong> mess.</p>
Expand Down
4 changes: 2 additions & 2 deletions tests/expected/canonical/04-div-normalization.xhtml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div xmlns="http://www.w3.org/1999/xhtml" about="">
<div xmlns="http://www.w3.org/1999/xhtml" id="content" about="">
<p>Browser-generated paragraph.</p>
<p>Classed browser div.</p>
<div class="junk">Classed browser div.</div>
<div about="#view" typeof="https://w3id.org/atomgraph/linkeddatahub#View">
<span property="http://spinrdf.org/spin#query" resource="#q"></span>
</div>
Expand Down
3 changes: 2 additions & 1 deletion tests/expected/canonical/05-empty-inline-pruning.xhtml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<div xmlns="http://www.w3.org/1999/xhtml" about="">
<div xmlns="http://www.w3.org/1999/xhtml" id="content" about="">
<p>Text with empty inlines.
<a id="note1"></a>
<span property="http://schema.org/name" resource="http://example.org/thing"></span>
<span property="http://schema.org/identifier" content="42"></span>
<span property="http://schema.org/name">Kept annotation</span></p>
Expand Down
2 changes: 1 addition & 1 deletion tests/expected/canonical/06-br-policy.xhtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div xmlns="http://www.w3.org/1999/xhtml" about="">
<div xmlns="http://www.w3.org/1999/xhtml" id="content" about="">
<p>line one<br/>line two</p>
<pre>first
second</pre>
Expand Down
8 changes: 4 additions & 4 deletions tests/expected/canonical/07-attr-strip.xhtml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div xmlns="http://www.w3.org/1999/xhtml" about="">
<p lang="en" property="http://purl.org/dc/terms/description">Attributed paragraph.</p>
<p><a href="http://example.org/page">a link</a>
<img src="pic.png" alt="A picture"/></p>
<div xmlns="http://www.w3.org/1999/xhtml" id="content" about="" class="ldh-main" data-mode="edit">
<p class="x" id="y" aria-hidden="true" data-x="1" lang="en" property="http://purl.org/dc/terms/description">Attributed paragraph.</p>
<p><a href="http://example.org/page" class="resource-link">a link</a>
<img src="pic.png" alt="A picture" class="thumb" id="img1"/></p>
<p xml:lang="da" property="http://purl.org/dc/terms/title">Dansk titel</p>
</div>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div xmlns="http://www.w3.org/1999/xhtml" about="">
<div xmlns="http://www.w3.org/1999/xhtml" id="content" about="">
<p>Author
<span property="http://xmlns.com/foaf/0.1/name">John Doe</span>
wrote this <span property="http://purl.org/dc/terms/created" content="2024-06-01">last month</span>.
Expand Down
2 changes: 1 addition & 1 deletion tests/expected/canonical/09-block-set-passthrough.xhtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div xmlns="http://www.w3.org/1999/xhtml" about=""
<div xmlns="http://www.w3.org/1999/xhtml" id="content" about=""
typeof="https://www.w3.org/ns/ldt/document-hierarchy#Container">
<h1 property="http://purl.org/dc/terms/title">Title</h1>
<h2>Section</h2>
Expand Down
2 changes: 1 addition & 1 deletion tests/expected/canonical/10-pre-whitespace-lang.xhtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div xmlns="http://www.w3.org/1999/xhtml" about="" lang="en">
<div xmlns="http://www.w3.org/1999/xhtml" id="content" about="" lang="en">
<pre>PREFIX schema: &lt;https://schema.org/&gt;
SELECT ?name WHERE {
?s schema:name ?name .
Expand Down
3 changes: 2 additions & 1 deletion tests/expected/canonical/11-sanitization.xhtml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<div xmlns="http://www.w3.org/1999/xhtml" about="">
<div xmlns="http://www.w3.org/1999/xhtml" id="content" about="">
<p>Text with <strong>handlers</strong>
and a <a>bad link</a> and a <a href="http://ok.example/">good link</a>.</p>
<p>Embedded gone.</p>
<p>A standalone <button type="button" class="tab">button</button> is inert content and stays, handler-stripped.</p>
<figure>
<img src="data:image/png;base64,iVBORw0KGgo=" alt="inline image kept"/>
<img alt="data html dropped"/>
Expand Down
6 changes: 3 additions & 3 deletions tests/expected/canonical/12-table-editing-strip.xhtml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<div xmlns="http://www.w3.org/1999/xhtml" about=""
<div xmlns="http://www.w3.org/1999/xhtml" id="content" about=""
typeof="https://www.w3.org/ns/ldt/document-hierarchy#Container">
<table>
<caption>Sales</caption>
<table class="ldh-table" data-mode="edit">
<caption class="cap">Sales</caption>
<thead>
<tr>
<th scope="col">Region</th>
Expand Down
2 changes: 1 addition & 1 deletion tests/expected/canonical/23-ldh-block-placeholder.xhtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div xmlns="http://www.w3.org/1999/xhtml" about="">
<div xmlns="http://www.w3.org/1999/xhtml" id="content" about="">
<div about="#chart1"
typeof="https://w3id.org/atomgraph/linkeddatahub#ResultSetChart">
<span property="http://spinrdf.org/spin#query" resource="queries/population/#this"></span>
Expand Down
2 changes: 1 addition & 1 deletion tests/expected/canonical/23-nested-chrome-strip.xhtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div xmlns="http://www.w3.org/1999/xhtml" about=""
<div xmlns="http://www.w3.org/1999/xhtml" id="content" about=""
typeof="https://www.w3.org/ns/ldt/document-hierarchy#Container">
<p>Intro paragraph.</p>
<ul>
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/canonical/05-empty-inline-pruning.xhtml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<div xmlns="http://www.w3.org/1999/xhtml" id="content" about="">
<p contenteditable="true">Text<strong></strong> with<em> </em> empty<a href="http://example.org/"></a> inlines.
<a id="note1"></a>
<span property="http://schema.org/name" resource="http://example.org/thing"></span>
<span property="http://schema.org/identifier" content="42"></span>
<span property="http://schema.org/name">Kept annotation</span></p>
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/canonical/11-sanitization.xhtml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<!-- Word paste junk comment -->
<p><iframe src="http://evil.example/"></iframe><object data="x"></object>Embedded gone.</p>
<form action="/steal"><input type="text" name="x"/><button>go</button></form>
<p>A standalone <button type="button" class="tab" onclick="evil()">button</button> is inert content and stays, handler-stripped.</p>
<figure>
<img src="data:image/png;base64,iVBORw0KGgo=" alt="inline image kept"/>
<img src="data:text/html;base64,PHNjcmlwdD4=" alt="data html dropped"/>
Expand Down
Loading