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
4 changes: 4 additions & 0 deletions .github/workflows/deploy-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ jobs:
mkdir -p _site
cp index.html rdfa-editor.css _site/
cp -r lib dist vocabs _site/
# core editor only: the LinkedDataHub-extended flavor (demo/ +
# its SEF) needs the content-negotiating dev server (serve.mjs)
# and is never deployed to Pages
rm -f _site/dist/ldh-editor.xsl.sef.json
- name: Publish to gh-pages branch
uses: peaceiris/actions-gh-pages@v4
with:
Expand Down
26 changes: 16 additions & 10 deletions CLAUDE.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
PORT ?= 8000

# Bring up the editor locally: build the SEF, then serve on http://localhost:$(PORT)
# (serve.mjs content-negotiates document URIs — required for the object-block demo)
up: sef
@echo "Serving http://localhost:$(PORT)/index.html — Ctrl-C to stop"
python3 -m http.server $(PORT)
node serve.mjs $(PORT)

# Compile src/*.xsl to the SaxonJS SEF (dist/index.xsl.sef.json) and copy vocabs
sef:
Expand Down
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,27 @@ RDFa extraction and the document canonicalization are all XSLT.
wavy underlines + a breadcrumb badge.
- **Navigation**: ToC drawer (outline with section drag-reorder), breadcrumb bar with
the RDFa subject in scope at the caret, find & replace.
- **Object blocks** (optional LinkedDataHub extension): blocks defined purely in RDF —
queries, charts, referenced resources — embedded as self-describing RDFa placeholders
wherever the content model admits a block, edited as atomic islands, and hydrated
client-side from clean, content-negotiated Linked Data URIs. Only the RDFa
description is stored; the rendering is ephemeral.

## Run

```bash
make up # build the SEF, then serve on http://localhost:8000
# open http://localhost:8000/index.html — override the port with: make up PORT=9000
make up # build the SEFs, then serve on http://localhost:8000
# open http://localhost:8000/ — override the port with: make up PORT=9000
# open http://localhost:8000/demo/ for the LinkedDataHub object-blocks demo
```

`make up` runs `make sef` first (compile `src/*.xsl` → `dist/index.xsl.sef.json`, needs
xmlstarlet + npx), then serves the repo with `python3 -m http.server`. Run `make sef`
alone to rebuild the SEF without serving.
`make up` runs `make sef` first (compile the core `src/index.xsl` →
`dist/index.xsl.sef.json` and the LDH-extended `src/ldh-editor.xsl` →
`dist/ldh-editor.xsl.sef.json`; needs xmlstarlet + npx), then serves the repo with
`node serve.mjs` — a static server that **content-negotiates** trailing-slash document
URIs (the object-block demo fetches its RDF and canned SPARQL results that way). Run
`make sef` alone to rebuild the SEFs without serving. GitHub Pages deploys the core
editor only.

## Test

Expand Down
121 changes: 121 additions & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" type="text/css">
<title>RDFa Editor — LinkedDataHub blocks demo</title>
<script type="text/javascript" src="../lib/SaxonJS3.js"></script>
<script type="text/javascript">
window.onload = function() {
// preload the vocabulary documents into the SaxonJS document pool,
// keyed by the page-relative URI the stylesheet resolves - the pool
// satisfies doc() before any fetch (fixture-nesting.html precedent)
const vocabs = ["vocabs/foaf.rdf", "vocabs/dcterms.rdf"];
Promise.all(vocabs.map(href =>
SaxonJS.getResource({ location: "../" + href, type: "xml" })))
.then(resources => {
const pool = {};
vocabs.forEach((href, i) =>
pool[new URL(href, document.baseURI).href] = resources[i]);
return SaxonJS.transform({
documentPool: pool,
stylesheetLocation: "../dist/ldh-editor.xsl.sef.json",
initialTemplate: "main",
logLevel: 10
}, "async");
})
.catch(err => console.log("Transformation failed: " + err));
}
</script>
<link rel="stylesheet" href="../rdfa-editor.css" type="text/css"/>
<style type="text/css">
html, body { margin: 0; }
body {
font-family: "Roboto", sans-serif;
padding-top: 70px; /* account for the fixed navbar */
padding-bottom: 56px; /* account for the fixed breadcrumb bar */
}
nav {
position: fixed;
top: 0;
left: 0;
right: 0;
background: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 12px 20px;
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
z-index: 1000;
border-bottom: 1px solid #e0e0e0;
}
.nav-btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
font-family: "Roboto", sans-serif;
background-color: #2196f3;
color: white;
}
.nav-btn:hover { background-color: #1976d2; }
.rdfa-editor-content {
margin: 0;
min-height: calc(100vh - 70px - 56px);
padding: 32px 48px 48px 48px; /* left >= 2.5em keeps the drag-handle gutter */
border: none;
background: white;
box-sizing: border-box;
}
</style>
</head>
<body>
<nav>
<button id="parse-rdf" class="nav-btn">Extract RDF</button>
</nav>

<div id="content" class="rdfa-editor-content" about="" typeof="http://schema.org/Article">
<h1 property="http://purl.org/dc/terms/title">LinkedDataHub blocks in the editor</h1>

<p>This page runs the editor with the <strong>LinkedDataHub blocks
extension</strong>: blocks defined purely in RDF — queries, charts, referenced
resources — embedded wherever an ordinary XHTML block can go. Each one is
stored as a few RDFa attributes; the rendering below is fetched and drawn on
the fly (content-negotiated from this server) and is never saved with the
page. Check <strong>Source</strong> in the toolbar to see the storage form,
or <strong>Extract RDF</strong> for the triples.</p>

<h2>A chart block</h2>
<p>A <code>ldh:ResultSetChart</code> pointing at a SPARQL query document:</p>

<div about="#population-chart"
typeof="https://w3id.org/atomgraph/linkeddatahub#ResultSetChart">
<span property="http://spinrdf.org/spin#query" resource="queries/population/#this"></span>
<span property="https://w3id.org/atomgraph/linkeddatahub#chartType" resource="https://w3id.org/atomgraph/client#BarChart"></span>
<span property="https://w3id.org/atomgraph/linkeddatahub#categoryVarName">country</span>
<span property="https://w3id.org/atomgraph/linkeddatahub#seriesVarName">population</span>
</div>

<h2>Blocks nest per the content model</h2>
<p>Anywhere a block element is allowed, an object block is allowed — here an
<code>ldh:Object</code> embedding a resource inside a list item:</p>
<ul>
<li>Author of the analytical engine notes:
<div about="#ada-object"
typeof="https://w3id.org/atomgraph/linkeddatahub#Object">
<span property="http://www.w3.org/1999/02/22-rdf-syntax-ns#value" resource="resources/ada/#this"></span>
</div>
</li>
<li>Ordinary list items edit as usual around it.</li>
</ul>

<p>Try it: press <strong>/</strong> in an empty paragraph and pick "Block…", or
use the ▦ toolbar button. Select a block with the arrow keys, delete it with
Backspace, undo with Ctrl/Cmd+Z — it behaves as one atomic unit.</p>
</div>
</body>
</html>
19 changes: 19 additions & 0 deletions demo/queries/population/index.rdf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
xmlns:sp="http://spinrdf.org/sp#">
<!-- served by content negotiation at the document URI .../queries/population/
so "#this" resolves to .../queries/population/#this -->
<sp:Select rdf:about="#this">
<rdfs:label>Population by country</rdfs:label>
<sp:text>PREFIX dbo: &lt;http://dbpedia.org/ontology/&gt;

SELECT ?country ?population
WHERE
{ ?country a dbo:Country ;
dbo:populationTotal ?population
}
ORDER BY DESC(?population)
LIMIT 5</sp:text>
</sp:Select>
</rdf:RDF>
29 changes: 29 additions & 0 deletions demo/queries/population/results.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<sparql xmlns="http://www.w3.org/2005/sparql-results#">
<head>
<variable name="country"/>
<variable name="population"/>
</head>
<results>
<result>
<binding name="country"><literal>India</literal></binding>
<binding name="population"><literal datatype="http://www.w3.org/2001/XMLSchema#integer">1428627663</literal></binding>
</result>
<result>
<binding name="country"><literal>China</literal></binding>
<binding name="population"><literal datatype="http://www.w3.org/2001/XMLSchema#integer">1425671352</literal></binding>
</result>
<result>
<binding name="country"><literal>United States</literal></binding>
<binding name="population"><literal datatype="http://www.w3.org/2001/XMLSchema#integer">339996563</literal></binding>
</result>
<result>
<binding name="country"><literal>Indonesia</literal></binding>
<binding name="population"><literal datatype="http://www.w3.org/2001/XMLSchema#integer">277534122</literal></binding>
</result>
<result>
<binding name="country"><literal>Pakistan</literal></binding>
<binding name="population"><literal datatype="http://www.w3.org/2001/XMLSchema#integer">240485658</literal></binding>
</result>
</results>
</sparql>
14 changes: 14 additions & 0 deletions demo/resources/ada/index.rdf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
xmlns:foaf="http://xmlns.com/foaf/0.1/">
<!-- served by content negotiation at the document URI .../resources/ada/
so "#this" resolves to .../resources/ada/#this -->
<foaf:Person rdf:about="#this">
<rdfs:label>Ada Lovelace</rdfs:label>
<foaf:name>Ada Lovelace</foaf:name>
<foaf:birthday>1815-12-10</foaf:birthday>
<foaf:interest rdf:resource="http://dbpedia.org/resource/Analytical_Engine"/>
<foaf:knows rdf:resource="http://dbpedia.org/resource/Charles_Babbage"/>
</foaf:Person>
</rdf:RDF>
78 changes: 78 additions & 0 deletions docs/ldh/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,81 @@ wrappers): view markup is edited in place and the canonical document is PUT, wit
extracted server-side. This form-control integration is the stepping stone: the same
canonicalization/sanitization boundary and the same editing surface carry over; the
XMLLiteral round-trip (steps 1/3) is what falls away.

### 12. Object blocks — the v6 block idiom and the render-hook bridge

The editor's object blocks (src/blocks.xsl) implement the **v6 document format**
([XHTML+RDFa as LDH v6 Document Format](https://github.com/AtomGraph/LinkedDataHub/wiki/XHTML-RDFa-as-LDH-v6-Document-Format)):
blocks are XHTML elements carrying `@about` (a fragment URI — the block is a document
part) and `@typeof` directly, with definition triples as `span[@property]` children
(objects via `@resource`, literals as span *text content*), arbitrarily nestable per
the content model. There are **no wrappers, no `rdf:_N` sequences, no
document→block containment edges** — the document owns its blocks implicitly through
its content, and `ldh:content` remains the document→body XMLLiteral property, never a
per-block edge. The v5 block model (`ldh:XHTML`, `ldh:Object` indirection, flat
sequences, row scaffolding, per-block CRUD SPARQL) is obsolete by the format itself;
what the editor adds on top is the *editing* semantics (atomic islands) and the
*rendering* seam.

That stored shape is **the same shape LDH's renderers already match**: e.g. the v5
chart entry (`client/block/chart.xsl:259`) matches
`*[@typeof = ('&ldh;ResultSetChart', …)][descendant::*[@property = '&spin;query'][@resource]]…`
and reads `spin:query`/`ldh:chartType`/`ldh:categoryVarName` off descendant
`@property` elements — and the v6 rendering mode `ldh:Block` is defined exactly as
"match `@typeof`, read properties from `@property` descendants". So the bridge is
thin: client.xsl plays the extension role that src/ldh-blocks.xsl plays in the
standalone demo — it re-declares the island class list and, per type, hands the
island to its block-rendering machinery (`ldh:Block` in v6; the v5 `ldh:RenderRow`
example below works the same way) with the ephemeral rendering div as the container:

```xml
<!-- client.xsl (imports the editor modules, higher import precedence) -->
<xsl:param name="object-block-types" as="xs:string*"
select="('&ldh;Object', '&ldh;View', '&ldh;ResultSetChart', '&ldh;GraphChart')"/>

<xsl:template match="div[tokenize(@typeof) = ('&ldh;ResultSetChart', '&ldh;GraphChart')]"
mode="local:render-island">
<xsl:variable name="island" as="element()" select="."/>
<!-- the container ldh:RenderRow renders into: the ephemeral rendering div
(canonicalization-stripped, extractor-skipped). Seed it with the LDH
progress-bar markup; the thunk chain replaces it with the chart -->
<xsl:call-template name="local:replace-rendering">
<xsl:with-param name="island" select="$island"/>
<xsl:with-param name="content">
<div class="progress progress-striped active">
<div class="bar" style="width: 0%;"></div>
</div>
</xsl:with-param>
</xsl:call-template>
<xsl:variable name="container" as="element()" select="$island/*[@data-role = 'rendering']"/>
<xsl:for-each select="$container">
<!-- RenderRow addresses its container by id (canonicalization-stripped) -->
<ixsl:set-attribute name="id" select="generate-id($island) || '-rendering'"/>
</xsl:for-each>
<xsl:apply-templates select="$island" mode="ldh:RenderRow">
<!-- the island IS the block: no .row-fluid.block scaffolding, the subject
is the island's own @about (a fragment URI per the v6 format) -->
<xsl:with-param name="block" select="$island"/>
<xsl:with-param name="about"
select="xs:anyURI(resolve-uri($island/@about, ldh:base-uri(ixsl:page())))"/>
<xsl:with-param name="container" select="$container"/>
</xsl:apply-templates>
</xsl:template>
```

The existing async pipelines (`ldh:chart-self-thunk` → `ldh:chart-query-thunk` →
`ldh:chart-results-thunk` → Google Charts `draw()`) run unchanged — they are already
container-plus-context-map based, and literal definition values are span text
content per the v6 format, exactly what their selectors read. One caveat: chart
canvases don't survive an `innerHTML` undo restore; the editor re-fires
`local:render-island` only for islands *without* a rendering div, so a
canvas-bearing bridge should also re-draw from the `LinkedDataHub.contents` cache
when its canvas is dead (LDH already does this on mode switches).

What the seam exposes to client.xsl is exactly what v6's `ldh:Block` mode needs: the
per-type match shapes (`@typeof` + `@property` descendants, already RDFa-driven) and
the thunk pipelines behind them; everything the v6 format removed (`rdf:_N`
ordering/renumbering SPARQL, `ldh:XHTML`/`ldh:Object` wrappers, row scaffolding,
per-block CRUD chrome) is likewise absent from the editor's side of the contract —
ordering is document order, reorder/delete/undo are editor gestures, persistence is
the one document PATCH.
17 changes: 16 additions & 1 deletion generate-sef.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
mkdir -p build
find src -maxdepth 1 -type f -name "*.xsl" -exec sh -c 'xmlstarlet c14n "$1" > "build/$(basename "$1")"' x {} \;

# compile the client stylesheet to SEF. -relocate:on means relative document() hrefs
# compile the client stylesheets to SEF. -relocate:on means relative document() hrefs
# resolve against the SEF load location, i.e. dist/ — hence the vocabs/ copy below.
# Two flavors: the CORE editor (index.xsl → dist/index.xsl.sef.json, deployed to
# GitHub Pages) and the LinkedDataHub-extended editor (ldh-editor.xsl →
# dist/ldh-editor.xsl.sef.json, used by demo/ and the browser tests — local only,
# its object blocks need the content-negotiating dev server).

echo "Generating SEF file from src/index.xsl..."

Expand All @@ -18,6 +22,17 @@ else
exit 1
fi

echo "Generating SEF file from src/ldh-editor.xsl..."

npx xslt3-he -t -xsl:./build/ldh-editor.xsl -export:./dist/ldh-editor.xsl.sef.json -nogo -ns:##html5 -relocate:on

if [ $? -eq 0 ]; then
echo "✓ SEF file generated successfully: dist/ldh-editor.xsl.sef.json"
else
echo "✗ Error generating SEF file"
exit 1
fi

mkdir -p dist/vocabs
cp vocabs/*.rdf dist/vocabs/
echo "✓ Vocabularies copied to dist/vocabs/"
28 changes: 28 additions & 0 deletions rdfa-editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,34 @@
outline-offset: 2px;
}

/* Object blocks (RDF-defined islands): the placeholder div renders as a card;
its RDFa definition spans are invisible - only the injected rendering shows.
Focused = selected navigation target, mirroring the image island */
.rdfa-editor-content .rdfa-editor-island {
border: 1px solid #e0e0e0;
border-radius: 4px;
background: #fafafa;
padding: 8px 12px;
margin: 16px 0;
}

.rdfa-editor-content .rdfa-editor-island > span[property] {
display: none;
}

.rdfa-editor-content .rdfa-editor-island:focus {
outline: 2px solid rgba(33, 150, 243, 0.7);
outline-offset: 2px;
}

.rdfa-editor-content .rdfa-editor-island.rdfa-editor-loading {
opacity: 0.5;
}

.rdfa-editor-content .rdfa-editor-island [data-role="rendering"] table {
margin: 8px 0;
}

/* Block chrome (drag handle) - ephemeral, data-role stripped on serialization */
[data-role="chrome"] {
visibility: hidden;
Expand Down
Loading
Loading