PDF-UA In a table not organized with Headers attributes and IDs, a TH cell does not contain a Scope attribute

113 Views Asked by At

We use xsl-fo to generate pdf.

We want the pdf to be pdf-ua-compliant.

We use veraPDF for validation this standard.

When there is a table-header that has horizontal spanned cells, it complains about rule 7.5-1:

"In a table not organized with Headers attributes and IDs, a TH cell does not contain a Scope attribute"

Normally each header cell has an id, that we can refer to in body-cells by using rx:header-idref (a renderx-xep specific solution, see comments in this question for more context)

But with spanned header cells this fails because there are body-cells that have no corresponding header-cell.

See i.e this screenshot: TableIssuePdfUa.png

the second cell of the first body-row with "R ≤ 10 (hb +hw)" is missing it's header-cell to reference it's id.

How can this be resolved?

EDIT: for better understanding this is the source xml:

<table>
  <tgroup cols="4" align="left">
    <colspec colnum="1" colwidth="28.1*" colname="col1"/>
    <colspec colnum="2" colwidth="34.3*" colname="col2"/>
    <colspec colnum="3" colwidth="20*" colname="col3"/>
    <colspec colnum="4" colwidth="17.7*" colname="col4"/>
    <thead valign="bottom">
      <row rowsep="1">
        <entry namest="col1" nameend="col2" valign="top" align="center" colname="col1" rotate="0" morerows="1" colsep="1">
          <Al>afstand</Al>
        </entry>
        <entry namest="col3" nameend="col4" valign="top" align="left" colname="col3" rotate="0" colsep="1">
          <Al>minimum aantal metingen <i>N</i></Al>
        </entry>
      </row>
      <row rowsep="1">
        <entry valign="top" align="left" colname="col3" rotate="0" colsep="1">
          <Al>zonder afscherming</Al>
        </entry>
        <entry valign="top" align="left" colname="col4" rotate="0" colsep="1">
          <Al>met afscherming</Al>
        </entry>
      </row>
    </thead>
    <tbody valign="top">
      <row rowsep="1">
        <entry valign="top" align="left" colname="col1" rotate="0" colsep="1"/>
        <entry valign="top" align="left" colname="col2" rotate="0" colsep="1">
          <Al><i>R</i> ≤ 10 (<i>h<sub>b</sub></i> + <i>h<sub>w</sub></i>)</Al>
        </entry>
        <entry valign="top" align="left" colname="col3" rotate="0" colsep="1">
          <Al>1</Al>
        </entry>
        <entry valign="top" align="left" colname="col4" rotate="0" colsep="1">
          <Al>1</Al>
        </entry>
      </row>
      <row rowsep="1">
        <entry valign="top" align="left" colname="col1" rotate="0" colsep="1">
          <Al>10 (<i>h<sub>b</sub></i> + <i>h<sub>w</sub></i>) &lt;</Al>
        </entry>
        <entry valign="top" align="left" colname="col2" rotate="0" colsep="1">
          <Al><i>R</i> ≤ 20 (<i>h<sub>b</sub></i> + <i>h<sub>w</sub></i>)</Al>
        </entry>
        <entry valign="top" align="left" colname="col3" rotate="0" colsep="1">
          <Al>1</Al>
        </entry>
        <entry valign="top" align="left" colname="col4" rotate="0" colsep="1">
          <Al>2</Al>
        </entry>
      </row>
      <row rowsep="1">
        <entry valign="top" align="left" colname="col1" rotate="0" colsep="1">
          <Al>20 (<i>h<sub>b</sub></i> + <i>h<sub>w</sub></i>) &lt;</Al>
        </entry>
        <entry valign="top" align="left" colname="col2" rotate="0" colsep="1">
          <Al>
            <i>R</i>
          </Al>
        </entry>
        <entry valign="top" align="left" colname="col3" rotate="0" colsep="1">
          <Al>2</Al>
        </entry>
        <entry valign="top" align="left" colname="col4" rotate="0" colsep="1">
          <Al>3</Al>
        </entry>
      </row>
    </tbody>
  </tgroup>
</table>

And this is a snippet of the current xslt:

<xsl:template match="row/entry" mode="content">
  <xsl:param name="inThead"  as="xs:boolean"        tunnel="yes"/>
  <fo:table-cell>
    <xsl:choose>
      <xsl:when test="$inThead">
        <!-- In de thead delen we aleeen id's uit aan die entries die als eerste voorkomen met hun betreffende colname, zodat id uniek is. -->
        <xsl:variable name="colname" select="@colname"/>
        <xsl:if test="not(parent::row/preceding-sibling::row/entry[@colname=$colname])">
          <xsl:attribute name="id" select="@colname"/>
        </xsl:if>
      </xsl:when>
      <xsl:otherwise>
        <xsl:attribute name="rx:header-idref" select="@colname"/>
      </xsl:otherwise>
    </xsl:choose>
    <!-- here some other logic -->
  </fo:table-cell>
</xsl:template>
2

There are 2 best solutions below

6
On BEST ANSWER

To me the question without markup is a bit unclear however note that there is nothing that stops multiple body cells from referencing the same header cell. So the second cell should reference the spanned header cell. As should all the body cells in columns 1 and 2.

Note that the rx:header-idref can be a comma separated list of id's. This is the order in which they should be read. So in the third cell in that first row, you would reference the id's of the spanned top header and the one below that does not span. This same technique would apply to row headers. As in you could have:

@rx:header-idref="col3-4head1spanned, col3head2"

and

 @rx:header-idref="col3-4head1spanned, col4head2"

as an image of what I mean:

enter image description here

0
On

Using the answer of @Kevin Brown I came up with the following code:

  <!--
    functie: f:getColspecsMetTbodyNaarTheadLinks
    param: colspecs
    param: thead
    Adds  attribute @bodyNaarTheadLinks to every colspec with the @id's of thead-entries where every entry with the same @colname in the tbody belongs to
  -->
  <xsl:function name="f:getColspecsMetTbodyNaarTheadLinks">
    <xsl:param name="colspecs" as="element(colspec)*"/>
    <xsl:param name="thead" as="element(thead)?"/>
    <xsl:for-each select="$colspecs">
      <xsl:variable name="colnumColspec" as="xs:integer" select="xs:integer(@colnum)"/>
      <xsl:copy copy-namespaces="false">
        <xsl:copy-of select="@*"/>
        <xsl:variable name="linksToHeaderCells" as="xs:string*">
          <xsl:for-each select="$thead/row">
            <!-- de links should go from bottom to top -->
          <xsl:sort select="position()" order="descending" />
          <xsl:for-each select="entry">
            <xsl:variable name="theadEntryId" as="xs:string" select="xs:string(@id)"/>
            <xsl:variable name="colnumEntry" as="xs:integer" select="index-of($colspecs/@colname,@colname)"/>
              <xsl:choose>
                <xsl:when test="@namest and @nameend">
                  <xsl:variable name="colnumStart" as="xs:integer" select="index-of($colspecs/@colname,@namest)"/>
                  <xsl:variable name="colnumEnd" as="xs:integer"   select="index-of($colspecs/@colname,@nameend)"/>
                  <xsl:for-each select="$colnumStart to $colnumEnd">
                    <xsl:if test="$colnumColspec = .">
                      <xsl:value-of select="$theadEntryId"/>
                    </xsl:if>
                  </xsl:for-each>
                </xsl:when>
                <xsl:when test="$colnumColspec eq $colnumEntry">
                  <xsl:value-of select="$theadEntryId"/>       
                </xsl:when>
              </xsl:choose>
            </xsl:for-each>
          </xsl:for-each>
        </xsl:variable>
        <!-- 
          veraPdf cannot find relations (issue?) when there are more than one links to thead-entries. Therefore we use just only the first for now: $linksToHeaderCells[1]
        -->
        <xsl:attribute name="bodyNaarTheadLinks" select="$linksToHeaderCells[1]" separator=", "/>
      </xsl:copy>
    </xsl:for-each>
  </xsl:function>

In below tgroup match I call the function

  <xsl:template match="tgroup" mode="content">
    <xsl:apply-templates mode="#current">
      <xsl:with-param name="colspecs" as="element(colspec)*" tunnel="yes" select="if(thead) then f:getColspecsMetTbodyNaarTheadLinks(colspec,thead) else colspec" />
    </xsl:apply-templates>
  </xsl:template>

Btw: the tunnel-param colspecs is used in other places as well

And in below entry-match

  <xsl:template match="row/entry" mode="content">
    <xsl:param name="inThead"       as="xs:boolean"         tunnel="yes"/>
    <xsl:param name="colspecs"      as="element(colspec)*"  tunnel="yes"/>
    <fo:table-cell>
        <xsl:variable name="colspec" as="element()" select="$colspecs[@colname= current()/@colname]"/>
        <xsl:if test="not($inThead) and $colspec/@bodyNaarTheadLinks">
          <xsl:attribute name="rx:header-idref" select="$colspec/@bodyNaarTheadLinks"/>
        </xsl:if>
      <!-- some more logic -->
    </fo:table-cell>
  </xsl:template>