Add Dynamic Tables of Contents to DOC Templates

Introduction

You can input dynamic tables of contents (TOCs) into your DOC and DOC-NEW templates. To do so, you must create 2 new Apex classes and add special syntax to your template.

Create Apex Classes

In the setup menu, type "Apex classes" into the Quick Find bar, then click Apex classes from the options that drop down, and click New.

Paste in the class below, then click Save.

global class TOCCallableClass implements Callable {
    public Object call(String action, Map<String,Object> args) {
        switch on action {
            when 'getTOC' {
                return this.getTOC();
            }
            when 'getTOCCustom' {
                return this.getTOC((String) args.get('firstCol'), (String) args.get('secondCol'), (String) args.get('headerLevels'));
            }
            when else {
                throw new ExtensionMalformedCallException('Method not implemented');
            }
        }
    }
    public class ExtensionMalformedCallException extends Exception {}
    public class UnexpectedBehaviorException extends Exception {}
    
    public String TOC = '<p class="MsoNormal" align="center" style="text-align:center"><b style="mso-bidi-font-weight:normal"><span style="font-family:\'Times New Roman\',serif">TABLE OF CONTENTS<p></p></span></b></p><p class="MsoNormal"><b><u><span style="font-family:\'Times New Roman\',serif"><p><span style="text-decoration:none">&nbsp;</span></p></span></u></b></p><table class="MsoTable15Plain4" border="0" cellspacing="0" cellpadding="0" width="100%" style="border-collapse:collapse;mso-yfti-tbllook:1184;mso-padding-alt:0in 5.4pt 0in 5.4pt"> <tr style="mso-yfti-irow:-1;mso-yfti-firstrow:yes;mso-yfti-lastfirstrow:yes;  mso-yfti-lastrow:yes">  <td width="328" valign="top" style="width:246.1pt;padding:0in 5.4pt 0in 5.4pt">  <p class="MsoNormal" style="tab-stops:center 117.65pt;mso-yfti-cnfc:5"><b><u><span  style="font-family:\'Times New Roman\',serif">Contents<p></p></span></u></b></p>  </td>  <td width="295" valign="top" style="width:221.4pt;padding:0in 5.4pt 0in 5.4pt">  <p class="MsoNormal" align="right" style="text-align:right;mso-yfti-cnfc:1"><b><u><span  style="font-family:\'Times New Roman\',serif">Page<p></p></span></u></b></p>  </td> </tr></table><Sdt SdtDocPart="t" DocPartType="Table of Contents" DocPartUnique="t" ID="861095738"> <p class="MsoTocHeading"><span style="font-family:\'Times New Roman\',serif; color:black;mso-themecolor:text1"><p>&nbsp;</p><sdtPr></sdtPr></span></p> <p class="MsoNormal"><!--[if supportFields]><span style="font-family:\'Times New Roman\',serif; color:black;mso-themecolor:text1"><span style="mso-element:field-begin"></span><span style="mso-spacerun:yes"> </span>TOC escaped_o &quot;1-1&quot; escaped_h escaped_z escaped_u <span style="mso-element:field-separator"></span></span><![endif]--><span style="font-family:\'Times New Roman\',serif;color:black;mso-themecolor:text1"><span style="mso-no-proof:yes">No table of contents entries found.</span></span><!--[if supportFields]><span style="font-family:\'Times New Roman\',serif;color:black;mso-themecolor:text1; mso-no-proof:yes"><span style="mso-element:field-end"></span></span><![endif]--><span style="font-family:\'Times New Roman\',serif;color:black;mso-themecolor:text1"><p></p></span></p></Sdt>';
    
    public String customTOC = '<table class="MsoTable15Plain4" border="0" cellspacing="0" cellpadding="0" width="100%"style="border-collapse:collapse;mso-yfti-tbllook:1184;mso-padding-alt:0in 5.4pt 0in 5.4pt"> <tr style="mso-yfti-irow:-1;mso-yfti-firstrow:yes;mso-yfti-lastfirstrow:yes;  mso-yfti-lastrow:yes">  <td width="20%" valign="top" style="width:20%;padding:0in 5.4pt 0in 5.4pt">  <p class="MsoNormal" style="tab-stops:center 117.65pt;mso-yfti-cnfc:5"><b><u><span  style="font-family:\'Times New Roman\',serif">{0}<p></p></span></u></b></p>  </td>  <td width="80%" valign="top" style="width:80%;padding:0in 5.4pt 0in 5.4pt">  <p class="MsoNormal" align="right" style="text-align:right;mso-yfti-cnfc:1"><b><u><span  style="font-family:\'Times New Roman\',serif">{1}<p></p></span></u></b></p>  </td> </tr></table><Sdt SdtDocPart="t" DocPartType="Table of Contents" DocPartUnique="t" ID="861095738"> <p class="MsoTocHeading"><span style="font-family:\'Times New Roman\',serif; color:black;mso-themecolor:text1"><p>&nbsp;</p><sdtPr></sdtPr></span></p> <p class="MsoNormal"><!--[if supportFields]><span style="font-family:\'Times New Roman\',serif; color:black;mso-themecolor:text1"><span style="mso-element:field-begin"></span><span style="mso-spacerun:yes"> </span>TOC escaped_o &quot;1-{2}&quot; escaped_h escaped_z escaped_u <span style="mso-element:field-separator"></span></span><![endif]--><span style="font-family:\'Times New Roman\',serif;color:black;mso-themecolor:text1"><span style="mso-no-proof:yes">No table of contents entries found.</span></span><!--[if supportFields]><span style="font-family:\'Times New Roman\',serif;color:black;mso-themecolor:text1; mso-no-proof:yes"><span style="mso-element:field-end"></span></span><![endif]--><span style="font-family:\'Times New Roman\',serif;color:black;mso-themecolor:text1"><p></p></span></p></Sdt>';
    
    public String TOF = '<p class="MsoNormal"><!--[if supportFields]><span style="mso-element:field-begin"></span><span style="mso-spacerun:yes"> </span>TOC escaped_h escaped_z escaped_t &quot;Heading 2&quot; escaped_c <span style="mso-element:field-separator"></span><![endif]--><b><span style="mso-no-proof:yes">No table of figures entries found.</span></b><!--[if supportFields]><span style="mso-element:field-end"></span><![endif]--></p>';
     
    public Map<String, String> getTOC() {
        return new Map<String, String>{
            'TOC' => TOC,
            'TOF' => TOF
        };
    }
    
    public static String swapFields(String toSwap, List<String> replacements) {
        for (Integer i = 0; i < replacements.size(); i++) {
            toSwap = toSwap.replace('{' + i + '}', replacements[i]);
        }
        return toSwap;
    }
    public Map<String, String> getTOC(String firstCol, String secondCol, String headerLevels) {
        if (headerLevels == null || headerLevels == 'null') {
            headerLevels = '1';
        }
        if (firstCol == null && secondCol == null) {
            return new Map<String, String>{
            'TOC' => TOC.replace('1-1', '1-' + headerLevels),
            'TOF' => TOF
            };
        }
        return new Map<String, String>{
            'TOC' => swapFields(customTOC, new List<String>{firstCol, secondCol, headerLevels}),
            'TOF' => TOF
        };
    }
 
}

For test coverage, create another apex class and paste in the class below, then click Save.

@isTest
private class TOCCallableTest {
    public static String hardcodedTOCExpected = '<p class="MsoNormal" align="center" style="text-align:center"><b style="mso-bidi-font-weight:normal"><span style="font-family:\'Times New Roman\',serif">TABLE OF CONTENTS<p></p></span></b></p><p class="MsoNormal"><b><u><span style="font-family:\'Times New Roman\',serif"><p><span style="text-decoration:none">&nbsp;</span></p></span></u></b></p><table class="MsoTable15Plain4" border="0" cellspacing="0" cellpadding="0" width="100%" style="border-collapse:collapse;mso-yfti-tbllook:1184;mso-padding-alt:0in 5.4pt 0in 5.4pt"> <tr style="mso-yfti-irow:-1;mso-yfti-firstrow:yes;mso-yfti-lastfirstrow:yes;  mso-yfti-lastrow:yes">  <td width="328" valign="top" style="width:246.1pt;padding:0in 5.4pt 0in 5.4pt">  <p class="MsoNormal" style="tab-stops:center 117.65pt;mso-yfti-cnfc:5"><b><u><span  style="font-family:\'Times New Roman\',serif">Contents<p></p></span></u></b></p>  </td>  <td width="295" valign="top" style="width:221.4pt;padding:0in 5.4pt 0in 5.4pt">  <p class="MsoNormal" align="right" style="text-align:right;mso-yfti-cnfc:1"><b><u><span  style="font-family:\'Times New Roman\',serif">Page<p></p></span></u></b></p>  </td> </tr></table><Sdt SdtDocPart="t" DocPartType="Table of Contents" DocPartUnique="t" ID="861095738"> <p class="MsoTocHeading"><span style="font-family:\'Times New Roman\',serif; color:black;mso-themecolor:text1"><p>&nbsp;</p><sdtPr></sdtPr></span></p> <p class="MsoNormal"><!--[if supportFields]><span style="font-family:\'Times New Roman\',serif; color:black;mso-themecolor:text1"><span style="mso-element:field-begin"></span><span style="mso-spacerun:yes"> </span>TOC escaped_o &quot;1-1&quot; escaped_h escaped_z escaped_u <span style="mso-element:field-separator"></span></span><![endif]--><span style="font-family:\'Times New Roman\',serif;color:black;mso-themecolor:text1"><span style="mso-no-proof:yes">No table of contents entries found.</span></span><!--[if supportFields]><span style="font-family:\'Times New Roman\',serif;color:black;mso-themecolor:text1; mso-no-proof:yes"><span style="mso-element:field-end"></span></span><![endif]--><span style="font-family:\'Times New Roman\',serif;color:black;mso-themecolor:text1"><p></p></span></p></Sdt>';
    public static String hardcodedTOFExpected = '<p class="MsoNormal"><!--[if supportFields]><span style="mso-element:field-begin"></span><span style="mso-spacerun:yes"> </span>TOC escaped_h escaped_z escaped_t &quot;Heading 2&quot; escaped_c <span style="mso-element:field-separator"></span><![endif]--><b><span style="mso-no-proof:yes">No table of figures entries found.</span></b><!--[if supportFields]><span style="mso-element:field-end"></span><![endif]--></p>';
    public static String customTOCExpected = '<table class="MsoTable15Plain4" border="0" cellspacing="0" cellpadding="0" width="100%"style="border-collapse:collapse;mso-yfti-tbllook:1184;mso-padding-alt:0in 5.4pt 0in 5.4pt"> <tr style="mso-yfti-irow:-1;mso-yfti-firstrow:yes;mso-yfti-lastfirstrow:yes;  mso-yfti-lastrow:yes">  <td width="20%" valign="top" style="width:20%;padding:0in 5.4pt 0in 5.4pt">  <p class="MsoNormal" style="tab-stops:center 117.65pt;mso-yfti-cnfc:5"><b><u><span  style="font-family:\'Times New Roman\',serif">test1<p></p></span></u></b></p>  </td>  <td width="80%" valign="top" style="width:80%;padding:0in 5.4pt 0in 5.4pt">  <p class="MsoNormal" align="right" style="text-align:right;mso-yfti-cnfc:1"><b><u><span  style="font-family:\'Times New Roman\',serif">test2<p></p></span></u></b></p>  </td> </tr></table><Sdt SdtDocPart="t" DocPartType="Table of Contents" DocPartUnique="t" ID="861095738"> <p class="MsoTocHeading"><span style="font-family:\'Times New Roman\',serif; color:black;mso-themecolor:text1"><p>&nbsp;</p><sdtPr></sdtPr></span></p> <p class="MsoNormal"><!--[if supportFields]><span style="font-family:\'Times New Roman\',serif; color:black;mso-themecolor:text1"><span style="mso-element:field-begin"></span><span style="mso-spacerun:yes"> </span>TOC escaped_o &quot;1-1&quot; escaped_h escaped_z escaped_u <span style="mso-element:field-separator"></span></span><![endif]--><span style="font-family:\'Times New Roman\',serif;color:black;mso-themecolor:text1"><span style="mso-no-proof:yes">No table of contents entries found.</span></span><!--[if supportFields]><span style="font-family:\'Times New Roman\',serif;color:black;mso-themecolor:text1; mso-no-proof:yes"><span style="mso-element:field-end"></span></span><![endif]--><span style="font-family:\'Times New Roman\',serif;color:black;mso-themecolor:text1"><p></p></span></p></Sdt>';    
    @isTest
    public static void testGetTOCCall() {
        TOCCallableClass tocClass = new TOCCallableClass(); 
        Map<String, String> getTOCReturn = (Map<String, String>) tocClass.call('getTOC', null);
        System.assertEquals(hardcodedTOCExpected, getTOCReturn.get('TOC'));
        System.assertEquals(hardcodedTOFExpected, getTOCReturn.get('TOF'));
    }
    @isTest
    public static void testGetTOCCustomCall() {
        Map<String, String> args = new Map<String, String>{
            'firstCol' => 'test1',
            'secondCol' => 'test2'
        };
        TOCCallableClass tocClass = new TOCCallableClass(); 
        Map<String, String> getTOCReturn = (Map<String, String>) tocClass.call('getTOCCustom', args);
        System.assertEquals(customTOCExpected, getTOCReturn.get('TOC'));
        System.assertEquals(hardcodedTOFExpected, getTOCReturn.get('TOF'));
    }
    @isTest
    public static void testIncorrectCall() {
        Map<String, String> args = new Map<String, String>{
            'firstCol' => 'test1',
            'secondCol' => 'test2'
        };
        TOCCallableClass tocClass = new TOCCallableClass(); 
        try {
            Map<String, String> getTOCReturn = (Map<String, String>) tocClass.call('getCustomTOC', args);
        } catch (Exception e) {
            System.assertEquals('Method not implemented', e.getMessage());
            return;
        }
        System.assert(false, 'Incorrect call to TOCCallableClass failed to throw exception');
    }
}

Add TOC Syntax To Your Template

Place the following syntax in your Template Source where you would like your table of contents to appear.

{{!TOC}} <!--{{!
<callable>
<class>TOCCallableClass</class>
<action>getTOCCustom</action>
<args>{“headerLevels” : “2"}</args>
</callable>
}}-->

Then, wrap any document headers that should be included in your table of contents in <h1> and <h2> tags.

Update TOC in Generated Document

Open your document after it generates. You will see the following line where your TOC will appear:

"No table of contents entries found"

Right click this line and click Update Field.

The line of text will be replaced with your table of contents.

If you make further edits to your document that require an updated TOC, simply right click anywhere within the table of contents and click Update Field again.

Tags: , ,

Was this helpful?