Schematron sample code
This article contains some general Schematron code samples to enable a quick start when writing Schematron validation code in PageSeeder.
For more specific code samples, see:
Basic Schematron
The following is a boilerplate Schematron schema. It has placeholders for different types of validation rules.
<?xml version="1.0"?>
<!-- A sample schematron -->
<sch:schema
xmlns:sch="http://purl.oclc.org/dsdl/schematron"
title="Rules for [type] documents">
<!-- You can creates as many patterns
as you like -->
<sch:pattern name="[your name]">
<!-- Rules matchings an element in
your document -->
<sch:rule context="[your XSLT pattern]">
<!-- Reported as an info if
test is true -->
<sch:report test="[your xpath test]">
[your message]
</sch:report>
<!-- Failed assertions are reported
as errors -->
<sch:assert test="[your xpath]">
[your message]
</sch:assert>
<!-- Add more <sch:report>
or <sch:assert>... -->
</sch:rule>
<!-- Add more <sch:rule> ... -->
</sch:pattern>
<!-- Add more <sch:pattern> ... -->
</sch:schema>
Example Schema
The following schema is designed to validate some generic best practice rules for PSML.
<?xml version="1.0"?>
<!-- This schematron validates a PSML document.
The schematron rules can be used to enforce
additional constraints required by the
application.
see https://dev.pageseeder.com/api/data/psml.html
-->
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron"
title="Rules for tutorial documents">
<!-- Set of rules applying to the entire document -->
<sch:pattern name="Document">
<sch:rule context="/document">
<!-- Too many sections -->
<sch:assert test="count(section) le 20">This
document contains over 20 sections. Consider
whether to split it into multiple documents.
</sch:assert>
<!-- No Doc ID -->
<sch:report test="not(documentinfo/uri/@docid)">
This document does not have a Doc ID.
</sch:report>
<!-- No Label -->
<sch:report test="not(documentinfo/uri/@labels)">
This document has no label.</sch:report>
</sch:rule>
</sch:pattern>
<!-- Validation rules for document fragments -->
<sch:pattern name="Fragments">
<sch:rule context="section/fragment">
<!-- Fragment has no heading -->
<sch:assert test="name(*[1]) = 'heading'">
Fragment '<sch:value-of select="@id"/>'
has no title.</sch:assert>
<!-- Fragment is too long (over 2000 chars)-->
<sch:assert test="string-length(string-join(.//text(), ''))
le 2000">
Fragment '<sch:value-of select="@id"/>'
has over 2000 characters.
</sch:assert>
</sch:rule>
</sch:pattern>
</sch:schema>
Check cross-referenced document
A rule to check the content of an xref target document.
<!-- Match cross references -->
<sch:rule context="xref|blockxref">
<!-- Check referenced document contains a heading2 -->
<sch:assert test="document(@href)//heading[@level='2']">
Document <sch:value-of select="@href"/> has no heading2.
</sch:assert>
</sch:rule>
Check terms used in a publication
A schematron which when run on a publication root document will check which defined terms are used in the publication. Requires PageSeeder 5.9800 or higher.
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron">
<sch:let name="usage"
value="document(concat(
'ps:search?filters=pstype:document&pspublicationid:',
/document/documentinfo/publication/@id,
'&facets=psinline-definition',
'&facetsize=1000&pagesize=1'))" />
<sch:pattern>
<sch:title>Usage</sch:title>
<sch:rule context="/">
<sch:let name="terms"
value="$usage//facets/facet/term" />
<sch:report test="$terms">
The following <sch:value-of select="count($terms)" />
defintions are used in this publication:
<sch:value-of select="string-join(sort($terms/@text,
'http://www.w3.org/2005/xpath-functions/collation/html-ascii-case-insensitive')
,', ')"/>
</sch:report>
</sch:rule>
</sch:pattern>
</sch:schema>
Check documents assigned to me
A schematron to check which documents are assigned to me and have status Complete or In Progress. Requires PageSeeder 5.9811 or higher.
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron">
<sch:let name="self" value="document('ps:self')" />
<sch:let name="memberid" value="$self/member/@id" />
<sch:let name="workflow" value="document('ps:workflow')" />
<sch:pattern>
<sch:title>Document</sch:title>
<sch:rule context="/document">
<!-- Complete status -->
<sch:assert id="COMPLETE_STATUS" flag="info"
test="not(@status='Complete' and
$workflow/workflow/assignedto[@id=$memberid])">
Document is assigned to you and COMPLETE.</sch:assert>
<!-- In Progress status -->
<sch:assert id="IN_PROGRESS_STATUS" flag="info"
test="not(@status='In Progress' and
$workflow/workflow/assignedto[@id=$memberid])">
Document is assigned to you and IN PROGRESS.</sch:assert>
</sch:rule>
</sch:pattern>
</sch:schema>
Debug Schematron code
If you need to debug Schematron and you're not sure what your getting in a variable, you can used code similar to this:
<!-- A separate pattern helps you avoid any side-effect -->
<sch:pattern name="DEBUG">
<!-- Have a rule that matches what you want, the document node always matches -->
<sch:rule context="/">
<!-- A true() test on a report always matches -->
<sch:report test="true()">
<!-- The serialize function serializes the nodes as a string -->
<sch:value-of select="serialize($variable-to-check)"/>
</sch:report>
</sch:rule>
</sch:pattern>Quickfix
A schematron to identify and fix headings that contain markup such as formatting. Requires PageSeeder 6.0 or higher.
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron"> <!-- Patterns --> <sch:include href="../../schema/pattern/heading_no_markup.sch" /> <!-- Properties --> <sch:include href="../../schema/properties.sch" /> </sch:schema>
schema/pattern/heading_no_markup.sch
<sch:pattern id="heading_no_markup"
xmlns:sch="http://purl.oclc.org/dsdl/schematron">
<sch:title>No markup in heading</sch:title>
<sch:rule context="fragment">
<sch:assert test="not(heading) or heading[not(*)]" subject="heading"
properties="fragment remove-markup-from-heading">
Heading should not contain markup
</sch:assert>
</sch:rule>
</sch:pattern>schema/properties.sch
<sch:properties xmlns:sch="http://purl.oclc.org/dsdl/schematron">
<!-- Allows PageSeeder to identify the fragment and show in interface -->
<sch:property id="fragment" ><sch:value-of
select="ancestor-or-self::fragment/@id"/></sch:property>
<!-- Quick fix -->
<sch:property id="remove-markup-from-heading" role="quickfix">
<description>Remove the markup from the heading</description>
</sch:property>
</sch:properties>quickfix/remove-markup-from-heading.xsl
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
exclude-result-prefixes="#all">
<xsl:template match="/fragment">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:apply-templates mode="copy"/>
</xsl:copy>
</xsl:template>
<xsl:template match="*" mode="copy">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:apply-templates mode="#current"/>
</xsl:copy>
</xsl:template>
<xsl:template match="heading/*" mode="copy">
<xsl:apply-templates mode="#current"/>
</xsl:template>
</xsl:stylesheet>