Search code examples
xsltxslt-1.0xsl-fo

Check if xsl:attribute name is valid for XSL-FO


In my data it's possible that there are one or more processing-instructions which are used to give that specific block of content new attributes or overwrite the values of existing ones.

The way I do this requires it that the name of the PIs are valid xsl attribute names.

The Question: Is it possible to check within the xsl-stylesheet if the name of the PI is an actual valid (=allowed as <xsl:attribute name="*thisname*"> in XSL-FO) attribute name?

<xsl:if test="./processing-instruction()"> <!-- add condition to test for valid name? -->
    <xsl:for-each select="./processing-instruction()">
        <xsl:variable name="pi_name"><xsl:value-of select="local-name()" /></xsl:variable>
        <xsl:attribute name="{$pi_name}"><xsl:value-of select="." /></xsl:attribute>
    </xsl:for-each>
</xsl:if>

EDIT:

Regarding this problem: Check if xsl:attribute name is valid for XSL-FO

That's the code I use derived from Tony's solution:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <xsl:template name="handle_block_attribute">
        <xsl:variable name="attributes" select="document('PATH\attributelist.xml')//attributelist/attribute"/>

        <xsl:if test=".//processing-instruction()">
            <xsl:for-each select=".//processing-instruction()">
                <xsl:variable name="pi_name"><xsl:value-of select="local-name()" /></xsl:variable>
                <xsl:choose>
                    <xsl:when test="$pi_name = $attributes">
                        <xsl:attribute name="{$pi_name}">
                            <xsl:value-of select="." />
                        </xsl:attribute>
                    </xsl:when>
                    <xsl:otherwise>
                        <fo:inline color="red">Invalid attribute-name in PI: <xsl:value-of select="$pi_name" /></fo:inline><fo:block />
                    </xsl:otherwise>
                </xsl:choose>
            </xsl:for-each>
        </xsl:if>

    </xsl:template>
</xsl:stylesheet>

The template is called in this way:

<xsl:template match="para">
    <fo:block xsl:use-attribute-sets="para.standard">
        <xsl:call-template name="handle_block_attribute" />
        <xsl:apply-templates/>
    </fo:block>
</xsl:template>

And that's the data:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<book>
    <sect class="hierarchic" type="chapter">
        <para class="heading" style="Chapter">
            <inline style="HeadingText">HD</inline>
        </para>
        <para style="Standard">Lorem ipsum dolor sit amet consectetuer eleifend consequat pede Aenean est. <?font-size 13pt?><?fotn-family Arial?><?color green?><?fline-height 3pt?></para>
        <para style="Standard">Consequat semper tortor id convallis leo Phasellus eget non sagittis neque.</para>
    </sect>
</book>

EDIT2:

Well, maybe there is a more elegant way, but it works: I'm going with two for-each-loops, one for the correct PIs and after that another one for the flawed ones including error-output.

<xsl:if test=".//processing-instruction()">
    <xsl:for-each select=".//processing-instruction()">
        <xsl:variable name="pi_name"><xsl:value-of select="local-name()" /></xsl:variable>
        <xsl:if test="$pi_name = $attributes">
            <xsl:attribute name="{$pi_name}">
                <xsl:value-of select="." />
            </xsl:attribute>
        </xsl:if>
    </xsl:for-each>
    <xsl:for-each select=".//processing-instruction()">
        <xsl:variable name="pi_name"><xsl:value-of select="local-name()" /></xsl:variable>
        <xsl:if test="not($pi_name = $attributes)">
            <fo:inline color="red">Invalid attribute name in PI: <xsl:value-of select="$pi_name" /></fo:inline><fo:block />
        </xsl:if>
    </xsl:for-each>
</xsl:if>

Solution

  • You've made it harder than necessary for yourself by using XSLT 1.0 instead of either XSLT 2.0 or XSLT 3.0 simply because it's harder to construct a list of property names to compare against. With the later versions, you could just make a sequence of strings. With XSLT 1.0, the simplest way (that I can remember) is to put each property name as the value of a separate element and then compare the prospective property name against the selected set of elements. The magic of XPath's = is that it is true if any value of one side matches any value on the other side.

    You can extract the definitive list of XSL-FO property names by processing the XML source for the XSL specification: http://www.w3.org/TR/2006/REC-xsl11-20061205/xslspec.xml

    With this stylesheet:

    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        version="1.0"
        xmlns:fo="http://www.w3.org/1999/XSL/Format"
        xmlns:local="uuid:503f6e96-fda1-4464-98b6-a60dbecc5946"
        exclude-result-prefixes="local">
    
        <properties xmlns="uuid:503f6e96-fda1-4464-98b6-a60dbecc5946">
            <property>font-size</property>
            <property>font-weight</property>
        </properties>
    
        <xsl:variable name="properties" select="document('')/*/local:properties/local:property"/>
        <xsl:template match="fo:*">
            <xsl:copy>
                <xsl:copy-of select="@*" />
                <xsl:for-each select="processing-instruction()">
                    <xsl:variable name="pi_name" select="local-name()" />
                    <xsl:choose>
                        <xsl:when test="$pi_name = $properties">
                            <xsl:attribute name="{$pi_name}">
                                <xsl:value-of select="." />
                            </xsl:attribute>
                        </xsl:when>
                        <xsl:otherwise>
                            <xsl:message>Unrecognised property: <xsl:value-of select="$pi_name"
                                 /></xsl:message>
                        </xsl:otherwise>
                    </xsl:choose>
                </xsl:for-each>
                <xsl:apply-templates />
            </xsl:copy>
        </xsl:template>
    </xsl:stylesheet>
    

    this document:

    <fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
        <fo:block><?font-size 16pt?><?font-weight bold?></fo:block>
        <fo:block><?fotn-size 16pt?><?bogus bold?></fo:block>
    </fo:root>
    

    generates this:

    <fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
      <fo:block font-size="16pt" font-weight="bold"/>
      <fo:block/>
    </fo:root>
    

    plus messages about 'fotn-size' and 'bogus'.

    I put the property names in the stylesheet to make it easier to test. You could put the XML in an external document and not have to fuss with the local namespace. The namespace was necessary because the only non-XSLT elements allowed at the top level of an XSLT stylesheet have to be in a non-XSLT namespace.