diff --git a/CLAUDE.md b/CLAUDE.md index 05937cc..1f04d0f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 `` 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 `` 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). diff --git a/src/canonical-xhtml.xsl b/src/canonical-xhtml.xsl index 46453ed..db6e36b 100644 --- a/src/canonical-xhtml.xsl +++ b/src/canonical-xhtml.xsl @@ -120,9 +120,11 @@ version="3.0"> + 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 --> + | form | input | select | textarea | link | meta | base" mode="cm:canonical" priority="3"/> @@ -137,10 +139,41 @@ version="3.0"> | @src[matches(normalize-space(.), '^data:', 'i')][not(matches(normalize-space(.), '^data:image/', 'i'))]" mode="cm:canonical"/> - - + + + + + + + + + + + + + + + + + + + + + + @@ -160,15 +193,15 @@ version="3.0"> - - + + -

@@ -178,7 +211,7 @@ version="3.0"> - @@ -202,8 +235,9 @@ version="3.0"> - diff --git a/tests/expected/canonical/01-chrome-strip.xhtml b/tests/expected/canonical/01-chrome-strip.xhtml index db4ffb3..08abcf4 100644 --- a/tests/expected/canonical/01-chrome-strip.xhtml +++ b/tests/expected/canonical/01-chrome-strip.xhtml @@ -1,4 +1,4 @@ -

First paragraph.

A heading

diff --git a/tests/expected/canonical/02-rendering-strip.xhtml b/tests/expected/canonical/02-rendering-strip.xhtml index 088d700..9c0ba84 100644 --- a/tests/expected/canonical/02-rendering-strip.xhtml +++ b/tests/expected/canonical/02-rendering-strip.xhtml @@ -1,4 +1,4 @@ -
+
Top selling
diff --git a/tests/expected/canonical/03-inline-mess.xhtml b/tests/expected/canonical/03-inline-mess.xhtml index 19942f4..aa94fcb 100644 --- a/tests/expected/canonical/03-inline-mess.xhtml +++ b/tests/expected/canonical/03-inline-mess.xhtml @@ -1,4 +1,4 @@ -
+

Some bold and italic and colored and underlined and styled text, plus nested mess.

diff --git a/tests/expected/canonical/04-div-normalization.xhtml b/tests/expected/canonical/04-div-normalization.xhtml index 6c5b505..6d188fd 100644 --- a/tests/expected/canonical/04-div-normalization.xhtml +++ b/tests/expected/canonical/04-div-normalization.xhtml @@ -1,6 +1,6 @@ -
+

Browser-generated paragraph.

-

Classed browser div.

+
Classed browser div.
diff --git a/tests/expected/canonical/05-empty-inline-pruning.xhtml b/tests/expected/canonical/05-empty-inline-pruning.xhtml index 3cee384..90deaac 100644 --- a/tests/expected/canonical/05-empty-inline-pruning.xhtml +++ b/tests/expected/canonical/05-empty-inline-pruning.xhtml @@ -1,5 +1,6 @@ -
+

Text with empty inlines. + Kept annotation

diff --git a/tests/expected/canonical/06-br-policy.xhtml b/tests/expected/canonical/06-br-policy.xhtml index 7f815a5..e630d61 100644 --- a/tests/expected/canonical/06-br-policy.xhtml +++ b/tests/expected/canonical/06-br-policy.xhtml @@ -1,4 +1,4 @@ -
+

line one
line two

first
 second
diff --git a/tests/expected/canonical/07-attr-strip.xhtml b/tests/expected/canonical/07-attr-strip.xhtml index d02b190..cfcccb5 100644 --- a/tests/expected/canonical/07-attr-strip.xhtml +++ b/tests/expected/canonical/07-attr-strip.xhtml @@ -1,6 +1,6 @@ -
-

Attributed paragraph.

-

a link - A picture

+
+ +

a link + A picture

Dansk titel

diff --git a/tests/expected/canonical/08-editor-emission-roundtrip.xhtml b/tests/expected/canonical/08-editor-emission-roundtrip.xhtml index 768f82a..2b5190e 100644 --- a/tests/expected/canonical/08-editor-emission-roundtrip.xhtml +++ b/tests/expected/canonical/08-editor-emission-roundtrip.xhtml @@ -1,4 +1,4 @@ -
+

Author John Doe wrote this last month. diff --git a/tests/expected/canonical/09-block-set-passthrough.xhtml b/tests/expected/canonical/09-block-set-passthrough.xhtml index fec1733..0bc47c1 100644 --- a/tests/expected/canonical/09-block-set-passthrough.xhtml +++ b/tests/expected/canonical/09-block-set-passthrough.xhtml @@ -1,4 +1,4 @@ -

Title

Section

diff --git a/tests/expected/canonical/10-pre-whitespace-lang.xhtml b/tests/expected/canonical/10-pre-whitespace-lang.xhtml index ee23836..e2d0671 100644 --- a/tests/expected/canonical/10-pre-whitespace-lang.xhtml +++ b/tests/expected/canonical/10-pre-whitespace-lang.xhtml @@ -1,4 +1,4 @@ -
+
PREFIX schema: <https://schema.org/>
 SELECT ?name WHERE {
     ?s  schema:name  ?name .
diff --git a/tests/expected/canonical/11-sanitization.xhtml b/tests/expected/canonical/11-sanitization.xhtml
index 8463cde..3ad05ad 100644
--- a/tests/expected/canonical/11-sanitization.xhtml
+++ b/tests/expected/canonical/11-sanitization.xhtml
@@ -1,7 +1,8 @@
-
+

Text with handlers and a bad link and a good link.

Embedded gone.

+

A standalone is inert content and stays, handler-stripped.

inline image kept data html dropped diff --git a/tests/expected/canonical/12-table-editing-strip.xhtml b/tests/expected/canonical/12-table-editing-strip.xhtml index 379f71d..c902aa4 100644 --- a/tests/expected/canonical/12-table-editing-strip.xhtml +++ b/tests/expected/canonical/12-table-editing-strip.xhtml @@ -1,7 +1,7 @@ -
- - +
Sales
+ diff --git a/tests/expected/canonical/23-ldh-block-placeholder.xhtml b/tests/expected/canonical/23-ldh-block-placeholder.xhtml index 7b3fba9..c27c520 100644 --- a/tests/expected/canonical/23-ldh-block-placeholder.xhtml +++ b/tests/expected/canonical/23-ldh-block-placeholder.xhtml @@ -1,4 +1,4 @@ -
+
diff --git a/tests/expected/canonical/23-nested-chrome-strip.xhtml b/tests/expected/canonical/23-nested-chrome-strip.xhtml index 0814158..5cf613a 100644 --- a/tests/expected/canonical/23-nested-chrome-strip.xhtml +++ b/tests/expected/canonical/23-nested-chrome-strip.xhtml @@ -1,4 +1,4 @@ -

Intro paragraph.

    diff --git a/tests/fixtures/canonical/05-empty-inline-pruning.xhtml b/tests/fixtures/canonical/05-empty-inline-pruning.xhtml index 0cf644f..14ebdb8 100644 --- a/tests/fixtures/canonical/05-empty-inline-pruning.xhtml +++ b/tests/fixtures/canonical/05-empty-inline-pruning.xhtml @@ -1,5 +1,6 @@

    Text with empty inlines. + Kept annotation

    diff --git a/tests/fixtures/canonical/11-sanitization.xhtml b/tests/fixtures/canonical/11-sanitization.xhtml index bc3d3d8..4c1cc99 100644 --- a/tests/fixtures/canonical/11-sanitization.xhtml +++ b/tests/fixtures/canonical/11-sanitization.xhtml @@ -5,6 +5,7 @@

    Embedded gone.

    +

    A standalone is inert content and stays, handler-stripped.

    inline image kept data html dropped
Sales
Region