xmlschemaschematron

Schematron to check the location of given class attribute values


I’d like to create a Schematron schema that validates:

Here’s a valid example:

<html xmlns="http://www.w3.org/1999/xhtml">
    <head></head>
    <body>
        <main>
            <div class="name">
                <div class="cat1 cat3"> <!— Other values are acceptable as long as cat1 or cat2 is present. —>
                    <div></div>
                </div>
                <div class="cat2">
                    <div></div>
                </div>
                <div> <!— No class attribute is OK —>
                    <div></div>
                </div>
            </div>
        </main>
    </body>
</html>

And an invalid example:

<html xmlns="http://www.w3.org/1999/xhtml">
    <head></head>
    <body>
        <main>
            <div class="name cat1"> <!— cat1 not allowed here —>
                <div class="cat3"> <!— cat1 or cat2 missing —>
                    <div></div>
                </div>
                <div class="cat2">
                    <div class="cat2"></div> <!— cat2 not allowed here —>
                </div>
                <div class=""> <!— Empty class attribute is not OK —>
                </div>
            </div>
        </main>
    </body>
</html>

Here’s what I’ve got so far. The XPath query //*[not(self::html/body/main/div/div)] doesn’t work. It also returns nodes at /html/body/main/div/div.

<schema xmlns="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt2">
    <pattern id="test">
        <rule context="//*[not(self::html/body/main/div/div)]">
            <assert test="
                    not(contains(@class, ’cat1’)) and
                    not(contains(@class, ’cat2’))
                    " role="error"> 
                        The class cannot contain 
                        "<value-of select="@class"/>". 
            </assert>
        </rule>
    </pattern>
</schema>

Solution

  • Martins Answer works (except of a small mistake) but it is unnecessarily complex in my opinion. If you want it simpler or needs to use XSLT2, you can do it in this way:

    <schema xmlns="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt2">
        <ns prefix="xhtml" uri="http://www.w3.org/1999/xhtml"/>
        <let name="cats" value="('cat1', 'cat2')"/>
        <pattern>
            <rule context="/xhtml:html/xhtml:body/xhtml:main/xhtml:div/xhtml:div[@class]">
                <assert test="tokenize(@class, '\s') = $cats">Missing class <value-of select="string-join($cats, ' or ')"/>.</assert>
            </rule>
            <!--this rule will only checks elements which are not checked by the rule above-->
            <rule context="*[@class]">
                <let name="class" value="@class"/>
                <!-- Just use report instead of assert to get the opposite check result. -->
                <report test="tokenize($class, '\s') = $cats"
                    >Used forbidden class: <value-of select="$cats[. = tokenize($class, '\s')]"/>.</report>
            </rule>
        </pattern>
    </schema>