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
4 changes: 3 additions & 1 deletion docs/ldh/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ kept the textarea in sync continuously; the replacement syncs once, declarativel
Key points: serialize the container's **children only** (no wrapper element —
parse-rdf-post adds the div); `mode="canonical"` guarantees the stored literal is
sanitized and free of editing ephemera (chrome, contenteditable, classes, on*
handlers, unsafe URLs).
handlers, unsafe URLs). SaxonJS 3 applies `ixsl:set-property` immediately
(verified in-browser), so the value is readable by `ldh:parse-rdf-post` within
the same submit event.

### 4. Asset loading — `layout.xsl`

Expand Down
4 changes: 2 additions & 2 deletions rdfa-editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@
}

/* Breadcrumb bar (Fonto-style element path + RDFa subject) */
#breadcrumb {
#rdfa-editor-breadcrumb {
position: fixed;
left: 0;
right: 0;
Expand Down Expand Up @@ -565,7 +565,7 @@
margin: 0 4px;
}

#breadcrumb-subject {
#rdfa-editor-breadcrumb-subject {
font-family: 'Courier New', monospace;
font-size: 12px;
color: #1565c0;
Expand Down
56 changes: 39 additions & 17 deletions src/edit.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ version="3.0">
<xsl:sequence select="ixsl:page()//*[contains-token(@class, 'rdfa-editor-content')]"/>
</xsl:function>

<!-- where local:init-editing appends the toolbar; hosts without a nav element
override this to point at their own chrome -->
<xsl:function name="local:toolbar-host" as="element()*">
<xsl:sequence select="ixsl:page()//nav"/>
</xsl:function>

<xsl:function name="local:root-of" as="element()?">
<xsl:param name="node"/>
<xsl:sequence select="$node/ancestor-or-self::*[contains-token(@class, 'rdfa-editor-content')][1]"/>
Expand Down Expand Up @@ -346,7 +352,7 @@ version="3.0">
<!-- ................................ init ................................ -->

<xsl:template name="local:init-editing">
<xsl:for-each select="ixsl:page()//nav">
<xsl:for-each select="local:toolbar-host()">
<xsl:result-document href="?." method="ixsl:append-content">
<xsl:call-template name="local:render-toolbar"/>
</xsl:result-document>
Expand All @@ -362,23 +368,39 @@ version="3.0">
</xsl:result-document>
</xsl:for-each>
<xsl:for-each select="local:roots()">
<!-- 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 -->
<xsl:variable name="region" as="element()" select="."/>
<xsl:variable name="invalid" as="xs:boolean" select="
exists($region//*[not(ancestor-or-self::*[@data-role])][not(cm:valid-nesting(.))])
or exists($region//*[not(ancestor-or-self::*[@data-role])]
[cm:structural(local-name(.))][text()[normalize-space()]])
or exists($region/(text()[normalize-space()] | *[cm:inline(local-name(.))]))"/>
<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:if>
<xsl:call-template name="local:init-region">
<xsl:with-param name="region" select="."/>
</xsl:call-template>
</xsl:for-each>
<xsl:for-each select="local:roots()/*">
</xsl:template>

<!-- per-region bring-up, callable for regions rendered after the initial page
(hosts that open editable regions lazily init each one through this) -->
<xsl:template name="local:init-region">
<xsl:param name="region" as="element()"/>

<!-- 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 -->
<xsl:variable name="invalid" as="xs:boolean" select="
exists($region//*[not(ancestor-or-self::*[@data-role])][not(cm:valid-nesting(.))])
or exists($region//*[not(ancestor-or-self::*[@data-role])]
[cm:structural(local-name(.))][text()[normalize-space()]])
or exists($region/(text()[normalize-space()] | *[cm:inline(local-name(.))]))"/>
<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:if>
<!-- an empty region cannot hold a caret: seed a paragraph (the
empty-blockquote idiom in local:init-block) -->
<xsl:if test="empty($region/*[not(@data-role)])">
<xsl:variable name="p" as="element()" select="local:element('p')"/>
<xsl:sequence select="ixsl:call($p, 'appendChild', [ local:element('br') ])[current-date() lt xs:date('2000-01-01')]"/>
<xsl:sequence select="ixsl:call($region, 'appendChild', [ $p ])[current-date() lt xs:date('2000-01-01')]"/>
</xsl:if>
<xsl:for-each select="$region/*">
<xsl:call-template name="local:init-block">
<xsl:with-param name="block" select="."/>
</xsl:call-template>
Expand Down
26 changes: 19 additions & 7 deletions src/index.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,31 @@ version="3.0">
documents into the SaxonJS document pool (SaxonJS.getResource +
documentPool), keyed by page-relative URI -->
<xsl:template name="main">
<!-- all editor state lives on a single window.rdfaEditor container (mirrors
LinkedDataHub's window.LinkedDataHub); reached everywhere via local:editor-state() -->
<ixsl:set-property name="rdfaEditor" select="ixsl:call(ixsl:window(), 'Object', [])" object="ixsl:window()"/>
<xsl:call-template name="local:init-state"/>
<xsl:call-template name="local:init-editor"/>
</xsl:template>

<!-- all editor state lives on a single window.rdfaEditor container (mirrors
LinkedDataHub's window.LinkedDataHub); reached everywhere via local:editor-state().
Hosts with their own initial template call this from there instead of main -->
<xsl:template name="local:init-state">
<xsl:variable name="state" select="ixsl:call(ixsl:window(), 'Object', [])"/>
<ixsl:set-property name="rdfaEditor" select="$state" object="ixsl:window()"/>
<xsl:for-each select="('editingSpan', 'range', 'activeBlock', 'draggedBlock',
'editRange', 'editingLink', 'insertHost', 'lastUndoHost',
'breadcrumbLeaf', 'draggedSectionHeading', 'findNode', 'tocRoot',
'slashHost', 'sweepAnchorNode', 'sweepAnchorHost', 'sweepRegion')">
<ixsl:set-property name="{.}" select="()" object="local:editor-state()"/>
<ixsl:set-property name="{.}" select="()" object="$state"/>
</xsl:for-each>
<ixsl:set-property name="lastUndoTime" select="0" object="local:editor-state()"/>
<ixsl:set-property name="findOffset" select="1" object="local:editor-state()"/>
<ixsl:set-property name="sweepAnchorOffset" select="0" object="local:editor-state()"/>
<ixsl:set-property name="lastUndoTime" select="0" object="$state"/>
<ixsl:set-property name="findOffset" select="1" object="$state"/>
<ixsl:set-property name="sweepAnchorOffset" select="0" object="$state"/>
</xsl:template>

<!-- the full editor bring-up: page chrome (undo stash, overlay, output modal,
toolbar, dialogs, drawers) plus block init of every editable region. Hosts
that render regions lazily call this once, when the first region appears -->
<xsl:template name="local:init-editor">
<xsl:call-template name="local:init-undo"/>
<xsl:call-template name="local:init-overlay"/>
<xsl:call-template name="local:init-annotate"/>
Expand Down
16 changes: 8 additions & 8 deletions src/navigate.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ version="3.0">
<div id="inspector-subject"/>
<div id="inspector-body"/>
</aside>
<footer id="breadcrumb" class="rdfa-editor-ui" role="navigation" aria-label="Document position">
<div id="breadcrumb-path"/>
<div id="breadcrumb-meta">
<span id="breadcrumb-subject"/>
<footer id="rdfa-editor-breadcrumb" class="rdfa-editor-ui" role="navigation" aria-label="Document position">
<div id="rdfa-editor-breadcrumb-path"/>
<div id="rdfa-editor-breadcrumb-meta">
<span id="rdfa-editor-breadcrumb-subject"/>
<button type="button" id="lint-badge" class="lint-badge"
aria-label="RDFa validation issues" style="display: none;"/>
</div>
Expand Down Expand Up @@ -275,7 +275,7 @@ version="3.0">
<ixsl:set-property name="breadcrumbLeaf" select="$leaf" object="local:editor-state()"/>
<xsl:variable name="ancestors" as="element()*"
select="$leaf/ancestor-or-self::* intersect local:root-of($leaf)/descendant-or-self::*"/>
<xsl:for-each select="id('breadcrumb-path', ixsl:page())">
<xsl:for-each select="id('rdfa-editor-breadcrumb-path', ixsl:page())">
<xsl:result-document href="?." method="ixsl:replace-content">
<xsl:for-each select="$ancestors">
<xsl:if test="position() gt 1">
Expand All @@ -287,17 +287,17 @@ version="3.0">
</xsl:for-each>
</xsl:result-document>
</xsl:for-each>
<xsl:for-each select="id('breadcrumb-subject', ixsl:page())">
<xsl:for-each select="id('rdfa-editor-breadcrumb-subject', ixsl:page())">
<ixsl:set-property name="textContent"
select="rdfa:in-scope-subject($leaf, local:document-uri())" object="."/>
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<ixsl:set-property name="breadcrumbLeaf" select="()" object="local:editor-state()"/>
<xsl:for-each select="id('breadcrumb-path', ixsl:page())">
<xsl:for-each select="id('rdfa-editor-breadcrumb-path', ixsl:page())">
<ixsl:set-property name="textContent" select="''" object="."/>
</xsl:for-each>
<xsl:for-each select="id('breadcrumb-subject', ixsl:page())">
<xsl:for-each select="id('rdfa-editor-breadcrumb-subject', ixsl:page())">
<ixsl:set-property name="textContent" select="local:document-uri()" object="."/>
</xsl:for-each>
</xsl:otherwise>
Expand Down
10 changes: 5 additions & 5 deletions tests/browser/features.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ page.on('pageerror', err => errors.push(String(err)));
page.on('dialog', d => d.accept());

await page.goto(BASE + '/tests/fixture.html');
await page.waitForSelector('#breadcrumb', { state: 'attached', timeout: 15000 })
await page.waitForSelector('#rdfa-editor-breadcrumb', { state: 'attached', timeout: 15000 })
.catch(() => errors.push('breadcrumb never rendered'));
await page.waitForSelector('#content > * > [data-role=chrome]', { state: 'attached', timeout: 5000 })
.catch(() => errors.push('chrome never injected'));
Expand Down Expand Up @@ -215,8 +215,8 @@ await page.evaluate(() => {
await page.keyboard.press('ArrowRight'); // trigger keyup -> breadcrumb refresh
await page.waitForTimeout(150);
results.breadcrumb = await page.evaluate(() => ({
path: document.getElementById('breadcrumb-path').textContent,
subject: document.getElementById('breadcrumb-subject').textContent,
path: document.getElementById('rdfa-editor-breadcrumb-path').textContent,
subject: document.getElementById('rdfa-editor-breadcrumb-subject').textContent,
}));
results.breadcrumb.pathOk = /content›p›strong/.test(results.breadcrumb.path.replace(/\s/g, ''));
results.breadcrumb.subjectOk = results.breadcrumb.subject === BASE + '/tests/fixture.html';
Expand All @@ -229,10 +229,10 @@ await page.evaluate(() => {
await page.keyboard.press('ArrowRight');
await page.waitForTimeout(150);
results.breadcrumb.annotatedLabel = await page.evaluate(() =>
document.getElementById('breadcrumb-path').textContent.includes('span[name]'));
document.getElementById('rdfa-editor-breadcrumb-path').textContent.includes('span[name]'));
// crumb click selects the element
await page.evaluate(() => {
const crumbs = [...document.querySelectorAll('#breadcrumb-path .crumb')];
const crumbs = [...document.querySelectorAll('#rdfa-editor-breadcrumb-path .crumb')];
crumbs[crumbs.length - 1].dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
results.breadcrumb.clickSelects = await page.evaluate(() =>
Expand Down
2 changes: 1 addition & 1 deletion tests/browser/hardening.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ results.aria = await page.evaluate(() => ({
dialogsModal: ['link-dialog', 'figure-dialog', 'find-dialog'].every(id =>
document.getElementById(id).getAttribute('aria-modal') === 'true'),
tocNav: document.getElementById('toc-drawer').getAttribute('role') === 'navigation',
breadcrumbNav: document.getElementById('breadcrumb').getAttribute('role') === 'navigation',
breadcrumbNav: document.getElementById('rdfa-editor-breadcrumb').getAttribute('role') === 'navigation',
badgeIsButton: document.getElementById('lint-badge').tagName === 'BUTTON',
}));

Expand Down
Loading