Graph to page with XSLT
The card in the last chapter was filled from named parts. Most of what the inspector shows is not like that: the kernel describes its cache, its threads, its arrangement and its endpoints as graphs, and a page about a graph has to walk it. There is a standard for turning a document into another document by walking it, and ikigai uses it: XSLT. When the input is a graph the page is an XSLT transform, and when the input is named arguments the page is a compose template (UI conventions D2).
The inspector uses RDF and XSLT because of where its data comes from, not because a view needs them. The kernel already publishes its introspection as Turtle, so the shortest path from a readout to a page is a stylesheet over that graph. A view composes whatever form its data already has. D2 says what to do with data that is ALREADY a graph; it is not a rule that data must become one before it can be shown. Your data may be a JSON document from an API or a CSV export from a spreadsheet, and the two chapters after this one build views over exactly those, with no RDF in them: Views from JSON and Views from CSV: one model, two sources.
This chapter builds the threads view, then reads the four others built the same way.
The input: a readout, as a graph
Golden threads were introduced in Golden threads in practice:
a cached answer hangs from the threads of what it read, and a write cuts a thread. The kernel
lists the threads that have been cut at urn:kernel:threads. So that there is one to see, read
the card and write the note:
<div id="note-card" class="inspect-card" hx-get="/inspect/card" hx-trigger="ikigai:cut from:body" hx-swap="outerHTML" data-ui-quiet=""> <p>Write me, then watch the cache.</p> </div> [computed]
written [uncacheable]
The readout’s default face is text, for a person at a terminal. Its graph face is Turtle:
@prefix ik: <https://ikigai-rs.dev/ns#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
<urn:ikigai:thread:urn:iki:tutorial:inspect:demo:note> a ik:GoldenThread ;
ik:threadName "urn:iki:tutorial:inspect:demo:note" ;
ik:cutCount "1"^^xsd:nonNegativeInteger .
[uncacheable]
That graph is what the view is made from. Two steps stand between it and a page.
Step one: the graph a stylesheet can read
XSLT reads XML, and RDF has an XML syntax, so the first step is a transreption: the same graph,
in RDF/XML. ikigai-rdf serves that as a resource, urn:rdf:transrept; the inspector does not
write a serializer. It does add one thing first. A thread’s page has a URL, and building a URL
means percent-encoding the thread’s name, which XSLT 1.0 cannot do. So the link arrives as data
on the node it belongs to, computed once, by the shared encoder. That is graph:{view}, the one
piece of the views that is code:
<?xml version="1.0" encoding="UTF-8"?> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:its="http://www.w3.org/2005/11/its"> <GoldenThread xmlns="https://ikigai-rs.dev/ns#" rdf:about="urn:ikigai:thread:urn:iki:tutorial:inspect:demo:note"> <cutCount xmlns="https://ikigai-rs.dev/ns#" rdf:datatype="http://www.w3.org/2001/XMLSchema#nonNegativeInteger">1</cutCount> <threadName xmlns="https://ikigai-rs.dev/ns#">urn:iki:tutorial:inspect:demo:note</threadName> <fragment xmlns="urn:iki:tutorial:inspect:term#">/inspect/fragment/thread/urn%3Aiki%3Atutorial%3Ainspect%3Ademo%3Anote</fragment> <page xmlns="urn:iki:tutorial:inspect:term#">/inspect/thread/urn%3Aiki%3Atutorial%3Ainspect%3Ademo%3Anote</page> </GoldenThread> </rdf:RDF> [uncacheable]
It reads the kernel’s readout under the caller’s capability. A viewer who may not inspect the kernel is refused by the kernel, for the kernel’s reason; the inspector shows only what its viewer may see, and it never decides that itself.
Step two: the stylesheet
The stylesheet is an artifact, a resource the inspector serves at
urn:iki:tutorial:inspect:xsl:threads. It is short enough to read whole:
<?xml version="1.0" encoding="UTF-8"?>
<!-- The threads view: every golden thread that has been cut, and how often (urn:kernel:threads). -->
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:ik="https://ikigai-rs.dev/ns#"
xmlns:insp="urn:iki:tutorial:inspect:term#"
exclude-result-prefixes="rdf ik insp">
<xsl:output method="html"/>
<xsl:template match="/">
<section id="view" class="inspect">
<span hidden="" data-ui-quiet="" hx-get="/inspect/fragment/threads" hx-trigger="ikigai:cut from:body" hx-target="closest section" hx-swap="outerHTML"></span>
<h1>Golden threads</h1>
<p>Every thread that has been cut, and how many times. A cached answer is valid only while every thread it hangs from still stands at the count it was stored under.</p>
<table class="inspect-table">
<caption>Cut threads, by name</caption>
<thead><tr><th scope="col">Thread</th><th scope="col">Cuts</th></tr></thead>
<tbody>
<xsl:apply-templates select="rdf:RDF/ik:GoldenThread">
<xsl:sort select="ik:threadName"/>
</xsl:apply-templates>
</tbody>
</table>
</section>
</xsl:template>
<xsl:template match="ik:GoldenThread">
<tr>
<td><a href="{insp:page}" hx-get="{insp:fragment}" hx-target="#main" hx-push-url="{insp:page}"><xsl:value-of select="ik:threadName"/></a></td>
<td><xsl:value-of select="ik:cutCount"/></td>
</tr>
</xsl:template>
</xsl:stylesheet>
Values leave it only through xsl:value-of and attribute value templates, which escape what
they write. Nothing disables output escaping, and a test fails if any stylesheet ever does.
Every link is a real href that works without a script, plus hx-get for the fragment and
hx-push-url for the page, so htmx can swap the view in place and the address bar still names
what you see.
The view is the two, composed
The fragment is a compose template whose one marker asks urn:xslt:transform for the transform
of the graph by the stylesheet:
$r{urn:xslt:transform?src=urn:iki:tutorial:inspect:graph:{view}&stylesheet=urn:iki:tutorial:inspect:xsl:{view}}
and the inspector binds it at fragment:{view}, so every graph view is the same three lines of
binding over a different stylesheet:
space
.bind(template(&format!("{BASE}graph:{{view}}")), graph_endpoint())
.bind(
template(&format!("{BASE}title:{{name}}")),
titles_endpoint(),
)
.bind(
Exact::new(format!("{BASE}fragment:scheduler")),
composed(
"urn:iki:tutorial:inspect:template:scheduler",
"inspect-fragment-scheduler",
),
)
.bind(
Exact::new(format!("{BASE}fragment:note")),
composed(
"urn:iki:tutorial:inspect:template:note",
"inspect-fragment-note",
),
)
.bind(
template(&format!("{BASE}fragment:{{view}}")),
composed("urn:iki:tutorial:inspect:template:view", "inspect-fragment"),
)
Here is the threads view, in this page’s kernel:
<section class='inspect' id='view'><span data-ui-quiet='' hidden='' hx-get='/inspect/fragment/threads' hx-swap… [uncacheable]
The stylesheet, the template and the transform of an unchanged input are cached. The readout is not: what the kernel holds changes from one read to the next, so the view over it is recomputed every time, and it says so.
The other views
The cache, the topology and the catalog are built the same way, each with its own stylesheet
over its own readout. The topology’s graph states order as RDF lists (a chain’s layers, a
space’s doors), which a stylesheet cannot walk without features xrust does not run reliably, so
graph:topology gives each node its place in resolution order as data, the same way it gives a
thread its link.
<section class='inspect' id='view'><span data-ui-quiet='' hidden='' hx-get='/inspect/fragment/cache' hx-swap='… [uncacheable]
A detail is a view too. fragment:thread:{name} is the thread and the entries hanging from it,
and fragment:entry:{id} is one cache entry and the threads it hangs from. Their graphs keep
only the focus and what is linked to it, and a detail the cache does not hold is refused as not
found, with the reason, rather than shown as an empty page:
error: not found: the cache holds no entry 0123456789abcdef for you (it may have been evicted)
The scheduler, the stopgap
urn:kernel:scheduler has a text face and no Turtle face in the kernel this book runs on, so
there is no graph for a stylesheet to read. Its view shows the text, escaped, and says so
rather than pretending to be a graph view:
<section id="view" class="inspect"> <h1>Scheduler</h1> <p class="inspect-stopgap">Shown as the kernel's text: <code>urn:kernel:scheduler</code> has no Turtle face in… <pre>scheduler backend single threads 1 </pre> </section> [uncacheable]
When a Turtle face arrives, the scheduler becomes one more stylesheet.
In Rust
#![allow(unused)]
fn main() {
extern crate ikigai_core;
extern crate inspector;
extern crate futures;
use futures::executor::block_on;
use ikigai_core::{ArgRef, Capability, Error, Iri, Request, Verb};
let kernel = inspector::kernel(Vec::new());
let viewer = Capability::root().attenuate(inspector::VIEWER_SCOPES);
let read = |name: &str, cap: &Capability| {
block_on(kernel.issue(Request::new(Verb::Source, Iri::parse(name).unwrap()), cap))
.map(|rep| String::from_utf8(rep.bytes).unwrap())
};
read("urn:iki:tutorial:inspect:demo:card", &viewer).unwrap();
block_on(kernel.issue(
Request::new(Verb::Sink, Iri::parse("urn:iki:tutorial:inspect:demo:note").unwrap())
.with_arg("content", ArgRef::Inline(b"Threads, please".to_vec())),
&viewer,
))
.unwrap();
let threads = read("urn:iki:tutorial:inspect:fragment:threads", &viewer).unwrap();
assert!(threads.contains("<h1>Golden threads</h1>"));
assert!(threads.contains(">urn:iki:tutorial:inspect:demo:note</a>"));
// A viewer who may write the note but not inspect the kernel: the kernel says no.
let writer = Capability::root().attenuate([inspector::CAP_NOTE]);
let refused = read("urn:iki:tutorial:inspect:fragment:threads", &writer).unwrap_err();
assert!(matches!(refused, Error::Denied(_)), "{refused}");
}
What was built, and what it rules out
Five views, four of them graph views, and one small piece of code that turns a readout into the
graph a stylesheet reads. Ruled out: walking a graph in Rust to emit HTML when a stylesheet
would do, and disable-output-escaping anywhere.
Next, the same kind of view over data that is not a graph: Views from JSON.