Search code examples
xmlxsltindentation

How can I fix the alignment of the closing tag when using XSLT?


I'm still learning XSLT, but I have no idea how to fix this:

The closing tag </result> created by xsltproc is still indented as if it were part of the element; for example:

  <result>
      <channel>netgroup_home_hosts</channel>
      <value>0.006</value>
      <!--thresholds start--><LimitMaxWarning>1</LimitMaxWarning><LimitMaxError>2</LimitMaxError><!--thresholds end-->
      <!--range: min="0"-->
    </result>

However I'd like the closing tag to be aligned with the opening tag. Either I forgot something, or I'm not using XSLT correctly (the extra comments are some debugging left-over).

Here is a (simple) example input:

<?xml version="1.0" encoding="utf-8"?>
<MonitoringOutput id="id-13951" version="0.1">
  <description>input</description>
  <exit_code>0</exit_code>
  <status_string>OK</status_string>
  <info_string>response_time(netgroup:home_hosts) is 0.006 (0)</info_string>
  <perf_string> netgroup_home_hosts=0.006;1;2;0</perf_string>
  <perf_data count="1">
    <sample label="netgroup_home_hosts">
      <label>netgroup_home_hosts</label>
      <value>0.006</value>
      <thresholds>
        <warn end="1"/>
        <crit end="2"/>
      </thresholds>
      <range min="0"/>
    </sample>
  </perf_data>
</MonitoringOutput>

And this is the output created (the comments are for helping me debug XSLT):

<?xml version="1.0" encoding="UTF-8"?>
<!--MonitoringOutput v0.1 id=id-13951 (PRTG.xsl v0.0.0)-->
<prtg>
  <!--status_string=OK-->
  <!--success status!-->
  <!--perf_data:-->
  <!--Description: input-->
  <!--Exit Code: 0-->
  <text>OK</text>
  <!--Info String: response_time(netgroup:home_hosts) is 0.006 (0)-->
  <!--Perf String:  netgroup_home_hosts=0.006;1;2;0-->
  <result>
      <channel>netgroup_home_hosts</channel>
      <value>0.006</value>
      <!--thresholds start--><LimitMaxWarning>1</LimitMaxWarning><LimitMaxError\
>2</LimitMaxError><!--thresholds end-->
      <!--range: min="0"-->
    </result>
</prtg>

And this is the XSLT (probably overly complicated, but I wanted to get it right first, then optimize):

<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

<xsl:template match="/MonitoringOutput[@version]">
  <xsl:comment><xsl:value-of select="name(.)"
  /> v<xsl:value-of select="@version" /> id=<xsl:value-of select="@id"
  /> (PRTG.xsl v0.0.0)</xsl:comment>
  <xsl:variable name="main.node" select="." />
  <prtg>
    <xsl:comment>status_string=<xsl:value-of
    select="$main.node/status_string" /> </xsl:comment>
    <xsl:choose>
      <xsl:when test="perf_data[@count &gt; 0]">
        <xsl:choose>
          <xsl:when test="$main.node/perf_data/crit_labels[@count &gt; 0]">
            <xsl:comment>critical status!</xsl:comment>
          </xsl:when>
          <xsl:when test="$main.node/perf_data/warn_labels[@count &gt; 0]">
            <xsl:comment>warnings status!</xsl:comment>
          </xsl:when>
          <xsl:otherwise>
            <xsl:comment>success status!</xsl:comment>
          </xsl:otherwise>
        </xsl:choose>
        <xsl:apply-templates select="$main.node/perf_data"/>
      </xsl:when>
      <xsl:otherwise>
        <error>1</error>
        <xsl:comment>(<xsl:value-of select="$main.node/exit_code"
        />)</xsl:comment>
        <text><xsl:value-of select="$main.node/info_string" /></text>
      </xsl:otherwise>
    </xsl:choose>
  </prtg>
</xsl:template>

<xsl:template match="perf_data[@count &gt; 0]">
  <!-- no parsing error -->
  <xsl:variable name="main.node" select=".." />
  <xsl:variable name="perf.node" select="." />
  <xsl:comment><xsl:value-of select="name(.)" />:</xsl:comment>
  <!-- HOW TO? -->
  <xsl:comment>Description: <xsl:value-of select="$main.node/description"
  /></xsl:comment>
  <xsl:comment>Exit Code: <xsl:value-of select="$main.node/exit_code"
  /></xsl:comment>
  <text><xsl:value-of select="$main.node/status_string" /></text>
  <xsl:comment>Info String: <xsl:value-of select="$main.node/info_string"
  /></xsl:comment>
  <xsl:comment>Perf String: <xsl:value-of select="$main.node/perf_string"
  /></xsl:comment>
  <xsl:apply-templates select="$perf.node/sample"/>
</xsl:template>

<xsl:template match="perf_data/sample[@label]">
  <result>
    <xsl:apply-templates />
    <xsl:variable name="is_crit" select="//crit_labels/name/text() = @label" />
    <xsl:variable name="is_warn" select="//warn_labels/name/text() = @label" />
    <xsl:choose>
      <xsl:when test="$is_crit">
        <warning>1</warning><xsl:comment>critical!</xsl:comment>
      </xsl:when>
      <xsl:when test="$is_warn">
        <warning>1</warning>
      </xsl:when>
    </xsl:choose>
  </result>
</xsl:template>

<xsl:template match="sample/label">
  <channel><xsl:value-of select="."/></channel>
</xsl:template>

<xsl:template match="sample/range[@*]">
  <xsl:for-each select="@*">
    <xsl:comment>range: <xsl:value-of select="name(.)" />="<xsl:value-of
    select="."/>"</xsl:comment>
  </xsl:for-each>
</xsl:template>

<xsl:template match="sample/unit">
  <CustomUnit><xsl:value-of select="."/></CustomUnit>
</xsl:template>

<xsl:template match="sample/value">
  <value><xsl:value-of select="."/></value>
</xsl:template>

<xsl:template match="sample/thresholds">
  <xsl:variable name="thresholds.node" select="." />
  <xsl:comment><xsl:value-of select="name(.)" /> start</xsl:comment>
  <xsl:apply-templates select="$thresholds.node/*"/>
  <xsl:comment><xsl:value-of select="name(.)" /> end</xsl:comment>
</xsl:template>

<xsl:template match="thresholds/crit[@*]">
  <xsl:for-each select="@*">
    <xsl:variable name="attr.name" select="name(.)" />
    <xsl:choose>
      <xsl:when test="$attr.name = 'start'">
        <LimitMinError><xsl:value-of select="." /></LimitMinError>
      </xsl:when>
      <xsl:when test="$attr.name = 'end'">
        <LimitMaxError><xsl:value-of select="." /></LimitMaxError>
      </xsl:when>
      <xsl:otherwise>
        <xsl:comment><xsl:value-of select="$attr.name" />="<xsl:value-of
        select="." />"</xsl:comment>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:for-each>
</xsl:template>

<xsl:template match="thresholds/warn[@*]">
  <xsl:for-each select="@*">
    <xsl:variable name="attr.name" select="name(.)" />
    <xsl:choose>
      <xsl:when test="$attr.name = 'start'">
        <LimitMinWarning><xsl:value-of select="." /></LimitMinWarning>
      </xsl:when>
      <xsl:when test="$attr.name = 'end'">
        <LimitMaxWarning><xsl:value-of select="." /></LimitMaxWarning>
      </xsl:when>
      <xsl:otherwise>
        <xsl:comment><xsl:value-of select="$attr.name" />="<xsl:value-of
        select="." />"</xsl:comment>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:for-each>
</xsl:template>

</xsl:stylesheet>

Update 2023-08-28

As https://stackoverflow.com/a/76976759/6607497 suggested the problem might be related to processing text nodes, I added the -v flag when calling xsltproc. I think the interesting part is this (note that I've added a $ to show where the line consisting of spaces only ends):

xsltApplyTemplates: node: 'sample'
xsltApplyTemplates: list of 9 nodes
xsltProcessOneNode: no template found for text
xsltDefaultProcessOneNode: copy text
      $
xsltCopyText: copy text
      $
xsltProcessOneNode: applying template 'sample/label' for label

The corresponding part of the input should be this:

  <perf_data count="1">
    <sample label="netgroup_home_hosts">
      <label>netgroup_home_hosts</label>

So is it that I have to override some default "copy text"? Probably my beginner's mistake was assuming "If I specify nothing, nothing will be done."


Solution

  • The problem with mismatching indent of </result> (and actually the contents of the element was that some whitespace had been being copied to output, even though the content model of the element (e.g: sample) had no direct CDATA or PCDTA content.

    Realizing that the solution was rather simple: Either one of the following did work:

    <xsl:template match="sample/text()">
      <xsl:comment>ignored: "<xsl:value-of select="."/>"</xsl:comment>
    </xsl:template>
    
    <xsl:template match="sample/text()">
      <xsl:value-of select="normalize-space()"/>
    </xsl:template>
    

    The first variant was for debugging, putting the ignored text as comment into the output, while the second variant did normalize the space to an empty string like this (xsltproc verbose output):

    xsltProcessOneNode: applying template 'sample/text()' for text
    xsltValueOf: select normalize-space()
    xsltValueOf: result ''
    

    The final output for the original input now looks like this:

    <?xml version="1.0" encoding="UTF-8"?>
    <!--MonitoringOutput v0.1 id=id-13951 (PRTG.xsl v0.0.1)-->
    <prtg>
      <!--success status!-->
      <text>OK</text>
      <!--Info String: response_time(netgroup:home_hosts) is 0.006 (0)-->
      <result>
        <channel>netgroup_home_hosts</channel>
        <value>0.006</value>
        <!--thresholds start-->
        <LimitMaxWarning>1</LimitMaxWarning>
        <LimitMaxError>2</LimitMaxError>
        <!--thresholds end-->
      </result>
    </prtg>
    

    Note: The amount of comments in the output may be different from the example shown in https://stackoverflow.com/a/76976759/6607497 as I had added many more while debugging the solution, and eventually had removed them all before creating the final output for verification.