Stylesheets

Built-in XSLT, CSS and JavaScript assets, and how to override them

XSLT

XSLT is a functional, Turing-complete XML transformation language.

LinkedDataHub's XSLT 3.0 stylesheets work by transforming the RDF/XML response body from the underlying HTTP API. Additional metadata from RDF vocabularies is used to improve the user experience.

Plain RDF/XML

RDF/XML is an important RDF syntax which functions as a bridge to the XML technology stack. The stylesheets use Jena's "plain" RDF/XML output which groups statements by subject and does not nest resource descriptions. This allows for predictable XPath patterns:

XPath expressions and what they match in RDF/XML
XPath Represents
/rdf:RDF The RDF graph
/rdf:RDF/rdf:Description or /rdf:RDF/*[*][@rdf:about] | /rdf:RDF/*[*][@rdf:nodeID] Resource description which contains properties
/rdf:RDF/rdf:Description/@rdf:about Subject resource URI
/rdf:RDF/rdf:Description/@rdf:nodeID Subject blank node ID
/rdf:RDF/rdf:Description/* Predicate (e.g. rdf:type) whose URI is concat(namespace-uri(), local-name())
/rdf:RDF/rdf:Description/*/@rdf:resource Object resource
/rdf:RDF/rdf:Description/*/@rdf:nodeID Object blank node ID
/rdf:RDF/rdf:Description/*/text() Literal value

The Northwind Chai product in that shape — the input that the templates match:

<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:schema="https://schema.org/">
    <rdf:Description rdf:about="https://localhost:4443/products/1/#this">
        <rdf:type rdf:resource="https://schema.org/Product"/>
        <schema:name>Chai</schema:name>
        <schema:category rdf:resource="https://localhost:4443/categories/1/#this"/>
        <schema:provider rdf:resource="https://localhost:4443/suppliers/1/#this"/>
    </rdf:Description>
</rdf:RDF>

Stylesheet structure

XSLT stylesheet components used by LinkedDataHub:

Includes
<xsl:include> is used to include one stylesheet into another. The inclusion mechanism is specified in 3.10.2 Stylesheet Inclusion of the XSLT 3.0 specification. The templates from the included stylesheets have the same priority as those of the including stylesheet.
Imports
<xsl:import> is used to import one stylesheet into another. The import mechanism is specified in 3.10.3 Stylesheet Import of the XSLT 3.0 specification. The templates from the imported stylesheets have lower priority than those of the importing stylesheet.
Parameters
XSD-typed global parameters passed to the stylesheet
Keys
Lookup keys
Templates
Template rules for XML node processing

One XSLT stylesheet can be specified per dataspace. In order to reuse LinkedDataHub's built-in templates, it should import the system stylesheet layout.xsl and only override the necessary templates. That is not a requirement, however; a stylesheet can also use its own independent transformation logic.

If there is no stylesheet specified for the dataspace, the system stylesheet is used. It defines the overall layout and imports resource-level and container-specific stylesheets, as well as per-vocabulary stylesheets.

Note that LinkedDataHub itself imports stylesheets from Web-Client, which uses the same template modes but produces a much simpler layout.

The same XSLT 3.0 stylesheets run in two environments. On the server they are executed by Saxon-HE to produce the initial HTML response. In the browser they are executed by Saxon-JS 3, which provides IXSL (Interactive XSLT extensions) for reading and manipulating the browser DOM.

There is a dedicated client-side stylesheet client.xsl that drives the browser. After the server-rendered page loads, its main template renders the layout client-side — the left sidebar, navigation, document and tab panes, content blocks, forms and modal dialogs are injected into the DOM using IXSL (e.g. ixsl:append-content). It also handles all subsequent navigation, so following links and switching documents re-renders in place without a full page reload. It imports and reuses the same document-, resource- and property-level templates as the server-side system stylesheet, but avoids loading per-vocabulary stylesheets in order to improve page load time. Templates of the client-side stylesheet can also be overridden.

Namespaces

Namespace prefixes and their vocabularies
Prefix Namespace Vocabulary Description
rdf: http://www.w3.org/1999/02/22-rdf-syntax-ns# The RDF Concepts Vocabulary Namespace for the RDF/XML elements, mostly used for matching input data
srx: http://www.w3.org/2005/sparql-results# SPARQL Query Results XML Format Namespace for the SPARQL Query Results XML elements, mostly used for matching input data
xsl: http://www.w3.org/1999/XSL/Transform XSL Transformations (XSLT) 3.0 Namespace for the XSLT stylesheet elements
ixsl: http://saxonica.com/ns/interactiveXSLT Saxon Interactive XSLT extensions Namespace for the Interactive XSL extensions
ldh: https://w3id.org/atomgraph/linkeddatahub# LinkedDataHub vocabulary LinkedDataHub concepts, also used for LinkedDataHub's own component template modes (named after the design system's components)
xhtml: http://www.w3.org/1999/xhtml XHTML 1.0 XHTML namespace, also used for generic (X)HTML templates
ac: https://w3id.org/atomgraph/client# Web-Client vocabulary Client-side concepts
lds: https://w3id.org/atomgraph/linkeddatahub/dataspaces# LinkedDataHub dataspace ontology LinkedDataHub dataspace concepts
lacl: https://w3id.org/atomgraph/linkeddatahub/admin/acl# LinkedDataHub ACL ontology ACL concepts

Templates

XSLT template components:

Match
XPath-based match pattern which either does or does not match an XML node
Mode
Groups templates and distinguishes them from other groups that share the same match patterns (e.g. different layout modes)
Parameters
XSD-typed parameters passed to the template invocation
Body
Contains the XML output nodes as well as XSLT processing instructions

XSLT processing starts at the root of the RDF/XML document and produces HTML elements by applying templates to all of the RDF/XML nodes while moving down the XML tree. In other words, it starts at the graph level, moves down to resource description elements, then to property elements, and ends with identifier attributes and literal text nodes.

Templates are applied (invoked) using <xsl:apply-templates>. Mode can be specified, e.g. <xsl:apply-templates mode="ac:BlockHeader">. To stay in the current mode without explicitly specifying it, use <xsl:apply-templates mode="#current">. <xsl:with-param> is used to supply parameters.

LinkedDataHub provides the following default template modes, which are used to render the layout modes:

  • Graph-level modes that apply to rdf:RDF
    • default mode renders full resource descriptions
    • ac:List renders a list of resources
    • ac:ResultsTable renders a table with resources as rows and properties as columns
    • ac:Grid renders a gallery of thumbnails
    • ac:Map renders resources with coordinates on a map
    • ac:Graph renders the resources as a node-and-edge graph
    • ac:ResourceForm renders an RDF/POST form for creating new resources or editing an existing one
  • Resource-level template modes that apply to rdf:Description
    • default mode renders one full resource description (by default header and property list)
    • ac:BlockHeader renders resource header (by default with type information)
    • ac:PropertyEditor renders definition list with property names and values (by default grouped by resource types)

When adding new user-defined modes, it is recommended to choose a new namespace for them as well as a user-defined prefix.

An example of a template that matches rdf:Description:

<xsl:template match="*[*][@rdf:about] | *[*][@rdf:nodeID]">
    <xsl:param name="id" as="xs:string?"/>
    <xsl:param name="class" as="xs:string?"/>

    <div>
        <xsl:if test="$id">
            <xsl:attribute name="id" select="$id"/>
        </xsl:if>
        <xsl:if test="$class">
            <xsl:attribute name="class" select="$class"/>
        </xsl:if>

        <xsl:apply-templates select="." mode="ac:BlockHeader"/>

        <xsl:apply-templates select="." mode="ac:PropertyEditor"/>
    </div>
</xsl:template>

Applied to the Chai description above, it produces markup of this shape (abridged):

<div about="https://localhost:4443/products/1/#this" typeof="https://schema.org/Product">
    <h2>Chai</h2>
    <dl>
        <dt>Name</dt>
        <dd>Chai</dd>
        <dt>Category</dt>
        <dd><a href="https://localhost:4443/categories/1/#this">Beverages</a></dd>
        <dt>Provider</dt>
        <dd><a href="https://localhost:4443/suppliers/1/#this">Exotic Liquids</a></dd>
    </dl>
</div>

ldh:ContentList mode renders the content specified by the rdf:_1, rdf:_2, … values of the current document.

There are a few special template modes such as ac:label and ac:description, together with the related ac:label() and ac:description() functions, which are used not to render layout but to extract metadata from resource descriptions. They can be used to retrieve a resource label and description no matter which RDF vocabularies are used in the data. They do so by invoking templates of the respective mode from vocabulary-specific stylesheets.

Overriding templates

Templates are overridden by redefining them in the importing stylesheet and providing the same or more specific match pattern and the same mode. The XSLT specification defines exactly how template priorities are determined in 6.4 Conflict Resolution for Template Rules.

The overriding template can then get the output of the overridden template by invoking either <xsl:apply-imports> or <xsl:next-match>. Read more in 6.7 Overriding Template Rules.

Always override the most specific template, i.e. if you want to change how a property is rendered, do not override the template for the resource description — override only the one for the property.

Keys

Keys are a lookup mechanism. They are defined on the stylesheet level using <xsl:key> and invoked using the key() function. For example:

<xsl:key name="resources" match="*[*][@rdf:about] | *[*][@rdf:nodeID]" use="@rdf:about | @rdf:nodeID"/>

<xsl:template match="*">
    <xsl:for-each select="key('resources', ldh:base-uri(.))">
        <xsl:value-of select="ac:label(.)"/>
    </xsl:for-each>
</xsl:template>

The key definition matches rdf:Description elements and uses their identifiers (URI or blank node ID). The template then looks up the RDF description of the current resource, i.e. the resource with URI that equals ldh:base-uri(.) which is the absolute URI of the current document, and outputs its label.

Loading data

The stylesheet processes one main RDF/XML document at a time, supplied by LinkedDataHub's HTML writer. However, it is possible to load additional XML documents over HTTP using the document() XSLT function. To avoid XSLT errors on any possible error responses, it is advisable to do a conditional check using the doc-available() function before doing the actual document() call.

For example, instead of hardcoding the title of this document as Stylesheets, you can use the following code to load it and output it on the fly:

<xsl:value-of select="key('resources', 'https://docs.linkeddatahub.com/reference/stylesheets/', document('https://docs.linkeddatahub.com/reference/stylesheets/'))"/>

In case this document changes its title, all such references would automatically render the updated title. On the other hand, it incurs the overhead of making an HTTP request.

LinkedDataHub's default stylesheets use this feature extensively. In fact, one HTML page is rendered from a dozen RDF/XML documents.

Built-in ontologies, as well as some other system and well-known ontologies, have a local copy in each LinkedDataHub instance. As a result, retrieving their descriptions by dereferencing their URIs using document() does not incur an HTTP request and is much faster. The URI-to-file mapping is defined as Jena's location mapping and can be found in location-mapping.ttl and prefix-mapping.ttl.

Client-side stylesheets use <ixsl:schedule-action> (deprecated) and <ixsl:promise> to load XML documents asynchronously. The metadata required to render forms — constructors, SHACL shapes, and property and object descriptions — is loaded client-side through chains of <ixsl:promise>, so the browser fetches it on demand rather than the server fetching everything up front.

Parameters

Both global (i.e. stylesheet-level) and template parameters are declared using <xsl:param>. LinkedDataHub sets the following global parameters:

Stylesheet parameters and their types
Parameter Type Description
$lds:Context document-node() RDF/XML description of the configured dataspaces; the current dataspace is looked up in it by origin
$lds:origin xs:anyURI Origin of the application shell that serves the static assets (the pane-scoped counterpart is the lds:origin() function)
$foaf:Agent document-node()? RDF/XML metadata of the authenticated agent (if any)
$acl:agent xs:anyURI? URI of the authenticated agent
$ldh:requestUri xs:anyURI Full request URI including query string
$ldh:httpHeaders map(xs:string, xs:string*) HTTP response headers by name — Content-Language, Link, Memento-Datetime etc. are read from it
$ldh:ajaxRendering xs:boolean Whether client-side (Saxon-JS) rendering is enabled
$ldh:renderSystemResources xs:boolean Whether system resources are included in the rendering
$ac:contextUri xs:anyURI Root deployment URI, against which system endpoints such as oauth2/* are resolved
$ac:uri xs:anyURI? URI of the external resource when the request goes through the Linked Data proxy; absent for local documents (prefer the ac:uri() function)

Former parameters $ac:mode, $acl:mode, $sd:endpoint, $ac:langs and $lds:Dataspace have been replaced by the functions ac:mode(), acl:mode(), sd:endpoint(), ac:langs() and lds:dataspace() — their values can change per pane or per response, which a global parameter cannot express.

Functions

LinkedDataHub and Web-Client provide XSLT functions available to all stylesheets.

URI utility functions

URI utility functions and what they return
Function Returns Description
ac:absolute-path($href as xs:anyURI) xs:anyURI Strips query string and fragment from a URI, returning the path-only form
ac:build-uri(
  $path as xs:anyURI?,
  $params as map(xs:string, xs:string*)
)
xs:anyURI? Appends a percent-encoded query string built from the parameter map to $path
ac:document-uri($uri as item()) xs:anyURI Strips the fragment identifier from a URI, making it suitable for use with document()
ac:fragment-id($uri as xs:anyURI) xs:string? Extracts the fragment identifier (the part after #)
ac:uri() xs:anyURI? URI of the external resource being viewed through the Linked Data proxy; absent for local documents. Works in both environments, unlike the server-side $ac:uri parameter it wraps
lds:origin() xs:anyURI Returns the origin (scheme, host and port) of the current dataspace, e.g. https://localhost:4443/. Used to build absolute URIs for static resources and same-site requests
lds:base() xs:anyURI Returns the base URI of the current dataspace
ldh:request-uri() xs:anyURI Returns the full request URI including query string (server-side only)
ldh:base-uri($node as node()) xs:anyURI Returns the base URI of an XML node; wraps the built-in base-uri()
ldh:href($uri as xs:anyURI?) xs:anyURI Resolves a URI to a local href, proxying external URIs through the LinkedDataHub proxy. Overloads accept $query-params as map(xs:string, xs:string*) and $fragment as xs:string?
ldh:parse-href($href as xs:anyURI) map(xs:string, item()?) Inverse of ldh:href() — parses a local href back into its target URI, query parameters and fragment. Shared by the client-side navigation handlers
ldh:query-params() map(xs:string, xs:string*) Parses the current request URI's query string into a map of parameter names to values
ldh:build-query($mode as xs:anyURI*) map(xs:string, xs:string*) Builds the mode query parameter map that selects the given layout mode(s)
ldh:parse-query-params($query-string as xs:string) map(xs:string, xs:string*) Parses a URL query string into a map of key to value(s)
ldh:url-decode($encoded-string as xs:string) xs:string Percent-decodes a URL-encoded string

RDF metadata functions

RDF metadata functions and what they return
Function Returns Description
ac:label($resource as element()) xs:string? Extracts a human-readable label from an RDF resource description, trying multiple vocabulary properties
ac:description($resource as element()) xs:string? Extracts a human-readable description from an RDF resource description
ac:image($resource as element()) attribute()* Extracts image references (e.g. foaf:depiction, foaf:img) from a resource description
ac:property-label($property as element()) xs:string? Label of a predicate element, capitalized — resolved by the ac:property-label mode templates. An overload accepts $property-metadata as document-node(), the property descriptions loaded for the current document
ac:object-label($object as node()) xs:string? Label of a property's object: a resource-valued object is looked up in the current graph, the loaded metadata or its dereferenced document, falling back to the URI's fragment or last path segment; a literal yields its lexical value. An overload accepts $object-metadata as document-node()
ac:mode($doc as document-node()) xs:anyURI The active layout mode of the given document. Replaces the former $ac:mode parameter
ac:langs() xs:string* The reader's accepted languages, deduplicated to primary subtags and always ending with en as a fallback — negotiated server-side, read from navigator.languages in the browser
ac:lang-rank($value as element()) xs:integer Ranks a property value's language against ac:langs(), so values sort by the reader's preference
ldh:date-time($value as xs:string?) xs:dateTime? Parses a lexical value into a xs:dateTime where possible
ldh:datatype-family($datatype as xs:anyURI?) xs:string Groups an XSD datatype into a family (numeric, dateTime, string etc.) — drives table column sorting
ldh:sort-key($key as xs:string?, $datatype as xs:anyURI?) xs:anyAtomicType? Converts a lexical value into a typed sort key for its datatype family
ldh:sort-key-lexical($resource as element(), $predicates as xs:anyURI*) xs:string? The lexical sort key of a resource: coalesces across $predicates in order, preferring values whose language matches the reader's first accepted language. Feeds ldh:sort-key()
ldh:sort-datatype($resources as element()*, $predicates as xs:anyURI*) xs:anyURI? The datatype shared by a sort column's literals; absent when the column mixes datatypes or carries plain literals or resources. Keys the typed re-sort that keeps a view block's client-side order agreeing with its SPARQL ORDER BY

HTTP and SPARQL functions

HTTP and SPARQL functions and what they return
Function Returns Description
ldh:query-result($endpoint as xs:anyURI, $query as xs:string) document-node() Executes a SPARQL query against the given endpoint and returns the result document (cached)
ldh:send-request(
  $href as xs:anyURI,
  $method as xs:string,
  $media-type as xs:string?,
  $body as item()?,
  $headers as map(xs:string, xs:string)
)
document-node()? Makes an HTTP request and returns the response as a document (server-side extension function)
ldh:link-targets($link-header as xs:string?, $marker as xs:string) xs:anyURI* Extracts the target URIs of the Link response header entries whose parameters contain $marker (e.g. a rel value) — the parser behind acl:mode(), sd:endpoint() and ldh:timemap() (client-side)
ldh:parse-html($string as xs:string, $mime-type as xs:string) document-node() Parses a markup string into a document
ldh:reserialize($doc as document-node()) document-node() Round-trips a document through serialization, normalizing it

Dataspace and access functions

Dataspace and access functions and what they return
Function Returns Description
sd:endpoint() xs:anyURI SPARQL endpoint of the current data pane — the local endpoint, or the remote dataspace's when browsing another dataspace through the proxy. Replaces the former $sd:endpoint parameter
lds:dataspace() xs:anyURI? URI of the dataspace resource describing the current dataspace. Replaces the former $lds:Dataspace parameter (its description is looked up in $lds:Context)
acl:mode() xs:anyURI* Access modes granted on the current document, read from the response's Link headers. Replaces the former $acl:mode parameter

Versioning functions

Versioning functions and what they return
Function Returns Description
ldh:memento-datetime() xs:string? The Memento-Datetime of the currently viewed historical version, absent on the live document
ldh:timemap() xs:anyURI? The document's TimeMap URI from the Link response header; absent when the document is not versioned
ldh:snapshot-params($query-params as map(xs:string, xs:string*)) map(xs:string, xs:string*) Filters a query parameter map down to the snapshot parameters (version, timemap), so they survive URL rebuilds

Utility functions

Utility functions and what they return
Function Returns Description
ac:uuid() xs:string Generates a random UUID
ldh:hash-code($string as xs:string) xs:integer Deterministic 32-bit djb2 hash of a string's codepoints — the counterpart to ac:uuid() for identifiers that must survive a reload because they are re-derived rather than stored (client-side)
ac:value-intersect($arg1 as xs:anyAtomicType*, $arg2 as xs:anyAtomicType*) xs:anyAtomicType* Distinct values present in both sequences
ac:value-except($arg1 as xs:anyAtomicType*, $arg2 as xs:anyAtomicType*) xs:anyAtomicType* Distinct values of $arg1 that are not present in $arg2

The many remaining ac: and ldh: functions are internal machinery, not a stable API, and are deliberately not documented here: the client-side render-*, load-*, set-*, *-response and *-thunk families together with the promise, progress, cursor and DOM helpers they call; the constructor, form and SPARQL-manipulation helpers behind the editing UI; and Web-Client functions that LinkedDataHub has superseded, such as ac:construct() and the SVG rendering helpers.

Other assets

CSS

The stylesheet links are emitted by the ac:Stylesheets XSLT template mode, in cascade order: the vendored design-system stylesheets — fonts.css, colors_and_type.css (tokens), app.css (components; it imports core.css, which imports controls.css and overlays.css) and retro.css (the skin) — then ldh.css, LinkedDataHub's one app layer over the kits, loaded last so it wins the ties it is written to win. Feature-specific stylesheets are linked ahead of them and only when needed: rdfa-editor.css for rich-text editing when an agent is authenticated, yasqe.css for the SPARQL editor. Dataspace stylesheets that import layout.xsl can override the ac:Stylesheets template mode to add or replace stylesheets.

Components are named by class rather than by element: ac-* for the design system's own components (ac-btn, ac-alert, ac-card, ac-badge) and ldh-* for the ones LinkedDataHub adds over them (ldh-block, ldh-actionbar, ldh-address). A component is varied by stacking one class from each axis onto it, not by a compound name — the action bar's Actions button is ac-btn in-neutral ap-outline sz-md, a form's Save button ac-btn in-primary ap-solid sz-md.

Class axes that vary a component
Axis Classes
Intent in-primary, in-neutral, in-accent, in-destructive, in-negative, in-inverse
Appearance ap-solid, ap-outline, ap-ghost, ap-fixed
Size sz-xs, sz-sm, sz-md, sz-lg, sz-xl
Variant va-informative, va-positive, va-negative, va-neutral, va-emphasis, va-enclosed, va-line, va-hero, va-inverse, va-fixed — carried by components that vary by kind rather than by intent, such as ac-alert

That vocabulary is the contract the templates emit, so restyling is a matter of writing rules against those classes. Changes belong in a stylesheet of the dataspace's own: ldh.css is the platform's app layer and is replaced wholesale on upgrade. Override the ac:Stylesheets template mode and call xsl:next-match first, which emits the platform's links and leaves yours last in the cascade, where they win the ties they need to win. Omitting xsl:next-match drops the vendored kits along with ldh.css and leaves the components unstyled.

JavaScript

The script links are emitted by the xhtml:Script XSLT template mode. It loads functions.js, the Saxon-JS 3 runtime together with the compiled client stylesheet (when client-side rendering is enabled), and feature-specific libraries: the YASQE SPARQL editor, the SPARQL builder, OpenLayers for maps, Google Charts for charts, and three.js with 3d-force-graph for the 3D graph mode.

LinkedDataHub only uses JavaScript for the functionality that cannot be achieved using client-side XSLT.

To customize the built-in stylesheets, follow the Change layout and Build apps guides