Edit online

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.

Edit online

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).

Sample Code:
<?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).

Sample Code:
<?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>
where the XML document looks something like this:
<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 (#).

Sample Code:
<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>
For links that contain an anchor, the Schematron rule must look something like this:
<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.

Sample Code:
<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.

Sample Code:
<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).

Sample Code:
<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.

Sample Code:
<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

Description: The following sample rule checks for spaces at the beginning and end of elements.
Tip: You could specify a list of elements to check to make the rule context-sensitive.
Sample Code:
<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.

Sample Code:
<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 &lt;$element&gt; 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.

Sample Code:
<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.

Sample Code:
<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>
where the XML file would look something like this:
<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.

Edit online

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.

Sample Code:
<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.

Sample Code:
<<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.

Sample Code:
<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.

Sample Code:
<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 @rowsepand @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 .

Sample Code:
<?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>