Examples of Schematron Rules and Quick Fixes
This topic is meant to provide some basic examples of Schematron Rules and Schematron Quick Fixes (SQF) to help you create and impose your own rules and quick fixes.
Schematron Examples
Schematron Use Case 1: Impose a Relax NG Schema Declaration
Description: The following sample rule is useful if, for example, you need to enforce the use of Relax NG schema declarations in all of your documents (i.e. instead of using DTD schemas).
<?xml version="1.0" encoding="UTF-8"?>
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron"
queryBinding="xslt2" xmlns:saxon="http://saxon.sf.net/">
<sch:let name="rngDeclaration"
value="processing-instruction('xml-model')
[saxon:get-pseudo-attribute('schematypens')='http://relaxng.org/ns/structure/1.0']"/>
<sch:pattern>
<sch:rule context="/element()">
<sch:assert test="exists($rngDeclaration)">You must define a Relax NG schema
declaration in the document (DTD schemas are not supported).</sch:assert>
</sch:rule>
</sch:pattern>
</sch:schema>
Result: The engine checks for a Relax NG schema declaration in the document and
displays an error if it is missing. The error is reported on the document's root element
(/element()
).
Schematron Use Case 2: Check for Missing IDs
Description: The following sample rule checks for missing or undefined IDs in a
TEI document. Specifically, it looks for IDs from the tei:rs/@ref
attribute defined in the document named persons.xml (as
xml:id
of a TEI person
element).
<?xml version="1.0" encoding="UTF-8"?>
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt2">
<sch:ns uri="http://www.tei-c.org/ns/1.0" prefix="tei"/>
<sch:let name="personIds"
value="document('../persons.xml')/tei:TEI//tei:person/@xml:id"/>
<sch:pattern>
<sch:rule context="tei:rs">
<sch:let name="refIds"
value="for $id in tokenize(@ref, ' ') return substring-after($id, '#')"/>
<sch:let name="missingIds"
value="for $id in $refIds return (if($id = $personIds) then '' else $id)"/>
<sch:report test="$missingIds != ''">
The following ids "<sch:value-of select="$missingIds"/>"
are not defined in "<sch:value-of select="$personIds"/>"
</sch:report>
</sch:rule>
</sch:pattern>
</sch:schema>
<tei xmlns="http://www.tei-c.org/ns/1.0">
<rs ref="../SomePerson/persons.xml#EDP ../personography/HAMpersons.xml#SD">text</rs>
<rs ref="../SomePerson/persons.xml#EDP">text</rs>
</tei>
Result: The engine displays an error message listing the missing/undefined IDs.
Schematron Use Case 3: Check for Broken Links
Description: The following sample rule detects broken links in DITA
<xref>
or <link>
elements. The first
example only checks links that do not contain an anchor (#).
<rule
context="*[contains(@class, ' topic/xref ') or contains(@class, ' topic/link ')]
[@href][not(contains(@href, '#'))][not(@scope = 'external')]
[not(@type) or @type='dita']">
<assert test="doc-available(resolve-uri(@href, base-uri(.)))">
The document linked by <value-of select="local-name()"/>
"<value-of select="@href"/>" does not exist!</assert>
</rule>
<rule
context="*[contains(@class, ' topic/xref ') or contains(@class, ' topic/link ')]
[@href][contains(@href, '#')][not(@scope = 'external')]
[not(@type) or @type='dita']">
<let name="file" value="substring-before(@href, '#')"/>
<let name="idPart" value="substring-after(@href, '#')"/>
<let name="topicId"
value="if (contains($idPart, '/')) then substring-before($idPart, '/') else $idPart"/>
<let name="id" value="substring-after($idPart, '/')"/>
<assert test="document($file, .)//*[@id=$topicId]">
Invalid topic id "<value-of select="$topicId"/>" </assert>
<assert test="$id ='' or document($file, .)//*[@id=$id]">
No such id "<value-of select="$id"/>" is defined! </assert>
<assert test="$id='' or document($file, .)//*[@id=$id]
[ancestor::*[contains(@class, ' topic/topic ')][1][@id=$topicId]]">
The id "<value-of select="$id"/>" is not in the scope of the referenced topic id
"<value-of select="$topicId"/>". </assert>
</rule>
Result: The engine displays an error message when a broken link or cross reference is detected.
Schematron Use Case 4: Check for Duplicate IDs
Description: The following sample rule detects if there are two sibling
<step>
elements with the same @id
value in a DITA
Task document.
<sch:rule context="*[contains(@class, ' task/step ')]">
<sch:let name="id" value="@id"/>
<sch:report
test="preceding-sibling::element()[contains(@class, ' task/step ')][@id = $id]">
Element with duplicate ID "<sch:value-of select="$id"/>" detected.
</sch:report>
</sch:rule>
Result: The engine displays an error message when a duplicate ID is detected in
sibling <step>
elements within a DITA Task document.
Schematron Use Case 5: Check for Duplicate DITA Topic References
Description: The following sample rule checks a DITA map for duplicate
<topicref>
elements with the same @href
value.
<sch:rule context="*[contains(@class, ' map/topicref ')]">
<sch:let name="href" value="@href"/>
<sch:report
test="preceding::element()[contains(@class, ' map/topicref ')][@href = $href]">
Duplicate topicref "<sch:value-of select="$href"/>" detected in map.
</sch:report>
</sch:rule>
Result: The engine displays an error message when multiple
<topicref>
elements with the same @href
value are
detected in a DITA map.
Schematron Use Case 6: Restrict Certain Words from the Title
Description: The following sample rule checks for instances of specified words to
be restricted from a <title>
element (in this example, the words
test and hello are restricted).
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron"
queryBinding="xslt2">
<sch:let name="words" value="'test,hello'"/>
<sch:let name="wordsToMatch" value="replace($words, ',', '|')"/>
<sch:pattern>
<sch:rule context="title">
<sch:report test="matches(text(), $wordsToMatch)" role="warn">
The following words should not be added in the title:
<sch:value-of select="$words"/>
</sch:report>
</sch:rule>
</sch:pattern>
</sch:schema>
Result: The engine displays an error message if one of the specified restricted words appear in a title.
Schematron Use Case 7: Check the Location of a Resource
Description: The following sample rule checks if the path to a resource (in this
case, an image) is specified correctly. Specifically, this sample rule reports that the
image must be located in the current project (the images location must be relative to the
parent folder and no more than one "../"
in the path.
<sch:rule context="image">
<sch:report test="count(tokenize(@href, '\.\./')) > 2">
The image must be located in the current project. It is currently located
in: <sch:value-of select="@href"/>
</sch:report>
</sch:rule>
Result: The engine displays an error message if an image is detected in a location other than the current project, relative to the parent folder.
Schematron Use Case 8: Check for Extra Spaces at Beginning/End of Elements
<rule context="p|ph|codeph|filename|indexterm|xref|user-defined|user-input">
<let name="firstNodeIsElement" value="node()[1] instance of element()"/>
<let name="lastNodeIsElement" value="node()[last()] instance of element()"/>
<report test="(not($firstNodeIsElement) and matches(.,'^\s',';j'))
or (not($lastNodeIsElement) and matches(.,'\s$',';j'))"
role="warning">
Textual elements should not begin or end with whitespace.</report>
</rule>
Result: The engine displays an error message if a whitespace is detected at the beginning or end of a textual element.
Schematron Use Case 9: Impose Capitalizing the First Letter
Description: The following sample rule detects if elements start with a capital
letter or a number. The rule is implemented using abstract patterns. The abstract pattern
starts-with-capital
has one argument representing the element to be
checked. There are two implementations of the abstract pattern, one that specifies the
<tittle>
element as the element to verify, and one that specifies
the <li>
element.
<sch:pattern abstract="true" id="starts-with-capital">
<sch:rule context="$element" role="information">
<sch:let name="firstNodeIsElement" value="node()[1] instance of element()"/>
<sch:report test="(not($firstNodeIsElement) and (not(matches(., '^[A-Z|0-9]'))))">
Start the element <$element> with a capital letter.</sch:report>
</sch:rule>
</sch:pattern>
<sch:pattern is-a="starts-with-capital">
<sch:param name="element" value="title"/>
</sch:pattern>
<sch:pattern is-a="starts-with-capital">
<sch:param name="element" value="li"/>
</sch:pattern>
Result: The engine displays an error message if a title begins with a word that does not contain a capital letter or number as its first character.
Schematron Use Case 10: Check for Specified Terms in a Paragraph
Description: The following sample rule checks if any DITA
<p>
elements contain certain keywords defined in an external
document.
<sch:pattern>
<sch:let name="keys" value="document('keys-common.ditamap')//keyword"/>
<sch:rule context="p">
<sch:let name="text" value="."/>
<sch:let name="matchedKeys" value="$keys[contains($text, normalize-space(.))]"/>
<sch:report id="now001" test="count($matchedKeys) > 0" role="error">
The paragraph text contains the keywords: <sch:value-of select="$matchedKeys"/>
</sch:report>
</sch:rule>
</sch:pattern>
Result: The engine displays an error message if any of the keywords listed in an
external document are detected within a DITA <p>
element.
Schematron Use Case 11: Impose a Minimum Value
Description: The following sample rule determines the
<type>
element value with the minimum version specified by the
@version
attribute and then verifies that they are all equal to the
determined value.
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt2">
<sch:let name="typeValue" value="//Node1[not(@version >
../Node1/@version)][1]/Type/text()"/>
<sch:pattern>
<sch:rule context="Type">
<sch:assert test="text() = $typeValue">
The Type value must be "<sch:value-of select="$typeValue"/>"
</sch:assert>
</sch:rule>
</sch:pattern>
</sch:schema>
<root>
<Node1 version="1">
<Element1>Value1</Element1>
<Type>123456</Type>
</Node1>
<Node1 version="2">
<Element1>Value1</Element1>
<Type>123456</Type>
</Node1>
<Node1 version="3">
<Element1>Value1</Element1>
<Type>1234567</Type>
</Node1>
</root>
Result: The engine displays an error message if a <type>
element value does not equal the minimum version specified by the @version
attribute.
SQF (Schematron Quick Fix) Examples
SQF Use Case 1: Impose a DITA Prolog
Description: The following sample Schematron rule checks a DITA topic to make sure
it contains <prolog>
, <critdates>
,
<revised>
elements and the sample Quick Fix proposes options for
inserting the missing elements.
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt2"
xmlns:sqf="http://www.schematron-quickfix.com/validator/process">
<sch:pattern>
<sch:rule context="*[contains(@class, ' topic/topic ')]">
<sch:assert sqf:fix="add_prolog" test="prolog" role="warn">Every topic must contain
prolog/critdates/revised elements where the revised modified date is in
YYYY-MM-DD format.</sch:assert>
<sqf:fix id="add_prolog">
<sqf:description>
<sqf:title>Add prolog/critdates/revised elements, where the revised element's
@modified attribute value is the current date in YYYY-MM-DD
format.</sqf:title>
</sqf:description>
<sqf:add match="*[contains(@class, ' topic/body ')]" node-type="element"
position="before" target="prolog">
<critdates>
<revised modified=""> </revised>
</critdates>
</sqf:add>
</sqf:fix>
</sch:rule>
<sch:rule context="*[contains(@class, ' topic/prolog ')]">
<sch:report role="warn" test="not(critdates)" sqf:fix="add_critdates">The prolog
element must have critdates/revised elements with the @modified attribute value
in YYYY-MM-DD format.</sch:report>
<sqf:fix id="add_critdates">
<sqf:description>
<sqf:title>Add the critdates element.</sqf:title>
</sqf:description>
<sqf:add node-type="element" target="critdates">
<revised modified=""> </revised>
</sqf:add>
</sqf:fix>
</sch:rule>
<sch:rule context="*[contains(@class, ' topic/critdates ')]">
<sch:report role="warn" test="not(revised)" sqf:fix="add_revised">The critdates
element must have revised @modified in YYYY-MM-DD format. </sch:report>
<sqf:fix id="add_revised">
<sqf:description>
<sqf:title>Add the revised element.</sqf:title>
</sqf:description>
<sqf:add node-type="element" target="revised"/>
</sqf:fix>
</sch:rule>
</sch:pattern>
</sch:schema>
Result: The engine displays an error message if the
<prolog>
, <critdates>
, or
<revised>
elements are missing from a DITA topic and the Quick
Fix mechanism proposes options for inserting the missing elements.
SQF Use Case 2: Impose an ID for all DITA Section Elements
Description: The following sample Schematron rule checks if each DITA
<section>
element has a specified ID and the sample Quick Fix
proposes options for inserting the missing IDs.
<<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron"
queryBinding="xslt2"
xmlns:sqf="http://www.schematron-quickfix.com/validator/process">
<sch:pattern>
<!-- Add IDs to all sections to impose link targets -->
<sch:rule context="section">
<sch:assert test="@id" sqf:fix="addId addIds"> [Bug] All sections should
have an @id attribute </sch:assert>
<sqf:fix id="addId">
<sqf:description>
<sqf:title>Add @id to the current section</sqf:title>
<sqf:p>Add an @id attribute to the current section. The ID is
generated from the section title.</sqf:p>
</sqf:description>
<!-- Generate an id based on the section title. If there is no title then
generate a random id. -->
<sqf:add target="id" node-type="attribute"
select="
concat('section_',
if (exists(title) and string-length(title) > 0)
then
substring(lower-case(replace(replace(
normalize-space(string(title)), '\s', '_'),
'[^a-zA-Z0-9_]', '')), 0, 50)
else
generate-id())"/>
</sqf:fix>
<sqf:fix id="addIds">
<sqf:description>
<sqf:title>Add @id to all sections</sqf:title>
<sqf:p>Add an @id attribute to each section from the document. The ID
is generated from the section title.</sqf:p>
</sqf:description>
<!-- Generate an id based on the section title. If there is no title then
generate a random id. -->
<sqf:add match="//section[not(@id)]" target="id" node-type="attribute"
select="
concat('section_',
if (exists(title) and string-length(title) > 0)
then substring(lower-case(replace(replace(
normalize-space(string(title)), '\s', '_'),
'[^a-zA-Z0-9_]', '')), 0, 50)
else generate-id())"/>
</sqf:fix>
</sch:rule>
</sch:pattern>
</sch:schema>
Result: The engine displays an error message if an @id
attribute
is missing for any <section>
element in a DITA topic and the Quick
Fix mechanism proposes options for inserting the missing ID.
SQF Use Case 3: Impose a Short Description in an Abstract Element
Description: The following sample Schematron rule checks a DITA topic to make sure
it contains a <shortdesc>
element inside an
<abstract>
element and the sample Quick Fix proposes options for
correcting the missing structure.
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt2"
xmlns:sqf="http://www.schematron-quickfix.com/validator/process"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<sch:pattern>
<sch:rule context="shortdesc">
<sch:assert test="parent::abstract" sqf:fix="moveToAbstract moveToExistingAbstract">
The short description must be added in an abstract element
</sch:assert>
<!-- Check if there is an abstarct element -->
<sch:let name="abstractElem" value="preceding-sibling::abstract |
following-sibling::abstract"/>
<!-- Create an abstract element and add the short description -->
<sqf:fix id="moveToAbstract" use-when="not($abstractElem)">
<sqf:description>
<sqf:title>Move short description in an abstract element</sqf:title>
</sqf:description>
sqf:replace>
<abstract>
<xsl:apply-templates mode="copyExceptClass" select="."/>
</abstract>
</sqf:replace>
</sqf:fix>
<!-- Move the short description in the abstract element-->
sqf:fix id="moveToExistingAbstract" use-when="$abstractElem">
<sqf:description>
<sqf:title>Move short description in the abstract element</sqf:title>
</sqf:description>
<sch:let name="shortDesc">
<xsl:apply-templates mode="copyExceptClass" select="."/>
</sch:let>
<sqf:add match="$abstractElem" select="$shortDesc"/>
<sqf:delete/>
</sqf:fix>
</sch:rule>
</sch:pattern>
<!-- Template used to copy the current node -->
<xsl:template match="node() | @*" mode="copyExceptClass">
<xsl:copy copy-namespaces="no">
<xsl:apply-templates select="node() | @*" mode="copyExceptClass"/>
</xsl:copy>
</xsl:template>
<!-- Template used to skip the @class attribute from being copied -->
<xsl:template match="@class" mode="copyExceptClass"/>
</sch:schema>
Result: The engine displays an error message if an
<abstract>
element does not contain a
<shortdesc>
element and the Quick Fix mechanism proposes options
for inserting the missing structure or to move the <shortdesc>
element inside the <abstract>
element.
SQF Use Case 4: Impose a Certain Article Type
Description: The following sample Schematron rule checks the
@article-type
attribute to make sure its value is one of the specified
allowed values (abstract, addendum, announcement,
article-commentary) and the sample Quick Fix proposes options for replacing any
other detected value with one of the allowed values.
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt2"
xmlns:sqf="http://www.schematron-quickfix.com/validator/process">
<sch:let name="articleTypes" value="('abstract', 'addendum', 'announcement',
'article-commentary')"/>
<sch:pattern>
<sch:rule context="article/@article-type">
<sch:assert test=". = $articleTypes" sqf:fix="setArticleType">
Should be one of the article types:
<sch:value-of select="$articleTypes"/></sch:assert>
<sqf:fix id="setArticleType" use-for-each="$articleTypes">
<sqf:description>
<sqf:title>Set article type to '<sch:value-of select="$sqf:current"/>'
</sqf:title>
</sqf:description>
<sqf:replace node-type="attribute" target="article-type" select="$sqf:current"/>
</sqf:fix>
</sch:rule>
</sch:pattern>
</sch:schema>
Result: The engine displays an error message if an @article-type
attribute has any other value other than abstract, addendum,
announcement, or article-commentary and the Quick Fix mechanism proposes
options for replacing the disallowed value with one of those four allowed values (using
the use-for-each
construct).
SQF Use Case 5: Impose Certain Attributes and Values
Description: The following sample Schematron rule checks the
@rowsep
and @colsep
attributes are added on the
<colspec>
element and their value is set to 1. The Quick Fix
proposes options for adding the attributes in case they are missing or set the correct
value .
<?xml version="1.0" encoding="UTF-8"?>
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt2"
xmlns:sqf="http://www.schematron-quickfix.com/validator/process">
<sch:pattern>
<sch:rule context="colspec">
<sch:assert test="@rowsep = 1" sqf:fix="addRowsep">The @rowsep should be
set to 1</sch:assert>
<sch:assert test="@colsep = 1" sqf:fix="addColsep">The @colsep should be
set to 1</sch:assert>
<sqf:fix id="addRowsep">
<sqf:description>
<sqf:title>Add @rowsep attribute</sqf:title>
</sqf:description>
<sqf:add node-type="attribute" target="rowsep" select="'1'"/>
</sqf:fix>
<sqf:fix id="addColsep">
<sqf:description>
<sqf:title>Add @colsep attribute</sqf:title>
</sqf:description>
<sqf:add node-type="attribute" target="colsep" select="'1'"/>
</sqf:fix>
</sch:rule>
</sch:pattern>
</sch:schema>