Search code examples
xmlxsltxsl-fo

XSL-FO - how to generate table of contents?


I have the following document structure and I want to generate a table of contents which would fit to my XSLT transformation. I was trying many things but none of them worked for me. Could anybody help me with this?

With the following transformation I get this WARNING and empty page number in the place.

 WARNING: Page 2: Unresolved id reference "N65898" found.

Document structure

<article>
    <chapter>
        <title>Chapter 1</title>
        <para>chapter 1 text</para>
        <sect1>
            <title>Section1 1.1</title>
            <para>text 1.1</para>
            <sect2>
                <title>Section2 1.1.1</title>
                <para>text 1.1.1</para>
            </sect2>
            <sect2>
                <title>Section2 1.1.2</title>
                <para>text 1.1.2</para>
            </sect2>
            <sect2>
                <title>Section2 1.1.3</title>
                <para>text 1.1.3</para>
            </sect2>
            <sect2>
                <title>Section2 1.1.4</title>
                <para>text 1.1.4</para>
            </sect2>
        </sect1>
        <sect1>
            <title>Section1 1.2</title>
            <sect2>
                <title>Section2 1.2.1</title>
            </sect2>
            <sect2>
                <title>Section2 1.2.2</title>
                <sect3>
                    <title>Section3 1.2.2.1</title>
                </sect3>
                <sect3>
                    <title>Section3 1.2.2.2</title>
                </sect3>
            </sect2>
        </sect1>
    </chapter>
    <chapter>
        <title>Chapter 2</title>
        <sect1>
            <title>Section1 2.1</title>
            <sect2>
                <title>Section2 2.1.1</title>
            </sect2>
            <sect2>
                <title>Section2 2.1.2</title>
                <sect3>
                    <title>Section3 2.1.2.1</title>
                </sect3>
                <sect3>
                    <title>Section3 2.1.2.2</title>
                </sect3>
            </sect2>
        </sect1>
        <sect1>
            <title>Section1 2.2</title>
            <sect2>
                <title>Section2 2.2.1</title>
            </sect2>
            <sect2>
                <title>Section2 2.2.2</title>
                <sect3>
                    <title>Section3 2.2.2.1</title>
                </sect3>
                <sect3>
                    <title>Section3 2.2.2.2</title>
                </sect3>
            </sect2>
        </sect1>
    </chapter>
    <chapter>
        <title>Chapter 3</title>
        <sect1>
            <title>Section1 3.1</title>
            <sect2>
                <title>Section2 3.1.1</title>
            </sect2>
            <sect2>
                <title>Section2 3.1.2</title>
                <sect3>
                    <title>Section3 3.1.2.1</title>
                </sect3>
                <sect3>
                    <title>Section3 3.1.2.2</title>
                </sect3>
            </sect2>
        </sect1>
        <sect1>
            <title>Section1 3.2</title>
            <sect2>
                <title>Section2 3.2.1</title>
            </sect2>
            <sect2>
                <title>Section2 3.2.2</title>
                <sect3>
                    <title>Section3 3.2.2.1</title>
                </sect3>
                <sect3>
                    <title>Section3 3.2.2.2</title>
                </sect3>
            </sect2>
        </sect1>
    </chapter>
</article>

Transformation

<!-- table of contents -->
<fo:block break-before='page'>
    <fo:block font-size="16pt" font-weight="bold">TABLE OF CONTENTS</fo:block>
    <xsl:for-each select="//chapter">
        <fo:block text-align-last="justify">
            <fo:basic-link internal-destination="{generate-id(.)}">
                <xsl:value-of select="count(preceding::chapter) + 1" />
                <xsl:text> </xsl:text>
                <xsl:value-of select="title" />
                <fo:leader leader-pattern="dots" />
                <fo:page-number-citation ref-id="{generate-id(.)}" />
            </fo:basic-link>
        </fo:block>
    </xsl:for-each>
</fo:block>

Solution

  • This will only work if you output an ID when you are outputting <chapter>. The @ref-id in <fo:page-number-citation> needs something to point to.

    See my answer here.


    EDIT - Example of generated ID

    Here's an example stylesheet. It will generate a PDF with a working TOC from your input XML. I tested with Saxon 6.5.5 and FOP.

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">
      <xsl:output indent="yes"/>
      <xsl:strip-space elements="*"/>
    
      <xsl:template match="node()|@*">
        <xsl:apply-templates select="node()|@*"/>
      </xsl:template>
      
      <xsl:template match="/article">
        <fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
           <fo:layout-master-set>
              <fo:simple-page-master master-name="my-page" page-width="8.5in" page-height="11in">
                 <fo:region-body margin="1in" margin-top="1.5in"/>
              </fo:simple-page-master>
           </fo:layout-master-set>
           <fo:page-sequence master-reference="my-page">
             <fo:flow flow-name="xsl-region-body">
               <xsl:call-template name="genTOC"/>
             </fo:flow>
            </fo:page-sequence>
            <xsl:apply-templates/>
        </fo:root>
      </xsl:template>
    
      <xsl:template name="genTOC">
        <fo:block break-before='page'>
          <fo:block font-size="16pt" font-weight="bold">TABLE OF CONTENTS</fo:block>
          <xsl:for-each select="//chapter">
            <fo:block text-align-last="justify">
              <fo:basic-link internal-destination="{generate-id(.)}">
                <xsl:value-of select="count(preceding::chapter) + 1" />
                <xsl:text> </xsl:text>
                <xsl:value-of select="title" />
                <fo:leader leader-pattern="dots" />
                <fo:page-number-citation ref-id="{generate-id(.)}" />
              </fo:basic-link>
            </fo:block>
          </xsl:for-each>
        </fo:block>
      </xsl:template>
      
      <xsl:template match="title|para">
        <fo:block><xsl:value-of select="."/></fo:block>
      </xsl:template>
      
      <xsl:template match="chapter">
        <fo:page-sequence master-reference="my-page" id="{generate-id(.)}">
           <fo:flow flow-name="xsl-region-body">
             <xsl:apply-templates/>
           </fo:flow>
        </fo:page-sequence>
      </xsl:template>
    
    </xsl:stylesheet>
    

    EDIT 2023-07-27 - Noticed a deleted answer that was actually a question from 2014...

    This is the best example I have found on the inet to create a toc. HOwever, this works for me, but can't figure out how to generate the sect1, sect2, etc headings in in toc with page numbers. tried repeating the fo:basic link code for section, but doesn't work. Please help. - Lori Boyters

    Here's an updated XSLT 1.0 stylesheet that will also create TOC entries for sect1, sect2, and sect3.

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">
        <xsl:output indent="yes"/>
        <xsl:strip-space elements="*"/>
        
        <xsl:template match="@*|node()">
            <xsl:copy>
                <xsl:apply-templates select="@*|node()"/>
            </xsl:copy>
        </xsl:template>
        
        <xsl:template match="/article">
            <fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
                <fo:layout-master-set>
                    <fo:simple-page-master master-name="my-page" page-width="8.5in" page-height="11in">
                        <fo:region-body margin="1in" margin-top="1.5in"/>
                    </fo:simple-page-master>
                </fo:layout-master-set>
                <fo:page-sequence master-reference="my-page">
                    <fo:flow flow-name="xsl-region-body">
                        <xsl:call-template name="genTOC"/>
                    </fo:flow>
                </fo:page-sequence>
                <xsl:apply-templates/>
            </fo:root>
        </xsl:template>
        
        <xsl:template name="genTOC">
            <fo:block break-before='page'>
                <fo:block font-size="16pt" font-weight="bold" text-align="center">TABLE OF CONTENTS</fo:block>
                <xsl:for-each select="//chapter|//sect1|//sect2|//sect3">
                    <fo:block text-align-last="justify" text-indent="{count(ancestor::chapter|ancestor::sect1|ancestor::sect2|ancestor::sect3) * 18}pt">
                        <fo:basic-link internal-destination="{generate-id(.)}">
                            <xsl:number count="chapter|sect1|sect2|sect3" 
                                level="multiple" format="1.1.1.1."/>
                            <xsl:value-of select="concat(' ',title,' ')"/>
                            <fo:leader leader-pattern="dots" />
                            <fo:page-number-citation ref-id="{generate-id(.)}" />
                        </fo:basic-link>
                    </fo:block>
                </xsl:for-each>
            </fo:block>
        </xsl:template>
        
        <xsl:template match="title|para">
            <fo:block><xsl:value-of select="."/></fo:block>
        </xsl:template>
        
        <xsl:template match="chapter">
            <fo:page-sequence master-reference="my-page" id="{generate-id(.)}">
                <fo:flow flow-name="xsl-region-body">
                    <xsl:apply-templates/>
                </fo:flow>
            </fo:page-sequence>
        </xsl:template>
        
        <xsl:template match="sect1|sect2|sect3">
            <fo:block-container id="{generate-id()}">
                <xsl:apply-templates/>
            </fo:block-container>
        </xsl:template>
        
    </xsl:stylesheet>
    

    Here's a working fiddle: http://xsltfiddle.liberty-development.net/bFD9uum