1<?php
2
3require_once DOKU_PLUGIN . 'odt/ODT/ODTDocument.php';
4
5/**
6 * ODTIndex:
7 * Class containing static code for handling indexes.
8 * Actually these are the table of contents and the chapter index.
9 *
10 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
11 */
12class ODTIndex
13{
14    /**
15     * This function does not really render/insert an index but inserts a placeholder.
16     * See also replaceIndexesPlaceholders().
17     *
18     * @return string
19     */
20    public static function insertIndex(ODTInternalParams $params, array &$indexesData, $type='toc', array $settings=NULL) {
21        // Insert placeholder
22        $index_count = count ($indexesData);
23
24        $params->document->paragraphClose();
25        $params->content .= '<index-placeholder no="'.($index_count+1).'"/>';
26
27        // Prepare index data
28        $new = array();
29        foreach ($settings as $key => $value) {
30            $new [$key] = $value;
31        }
32        $new ['type'] = $type;
33        $new ['width'] = $params->document->getAbsWidthMindMargins();
34
35        if ($type == 'chapter') {
36            $new ['start_ref'] = $params->document->getPreviousToCItem(1);
37        } else {
38            $new ['start_ref'] = NULL;
39        }
40
41        // Add new index data
42        $indexesData [] = $new;
43
44        return '';
45    }
46
47    /**
48     * This function builds the actual TOC and replaces the placeholder with it.
49     * It is called in document_end() after all headings have been added to the TOC, see toc_additem().
50     * The page numbers are just a counter. Update the TOC e.g. in LibreOffice to get the real page numbers!
51     *
52     * The TOC is inserted by the syntax tag '{{odt>toc:setting=value;}};'.
53     * The following settings are supported:
54     * - Title e.g. '{{odt>toc:title=Example;}}'.
55     *   Default is 'Table of Contents' (for english, see language files for other languages default value).
56     * - Leader sign, e.g. '{{odt>toc:leader-sign=.;}}'.
57     *   Default is '.'.
58     * - Indents (in cm), e.g. '{{odt>toc:indents=indents=0,0.5,1,1.5,2,2.5,3;}};'.
59     *   Default is 0.5 cm indent more per level.
60     * - Maximum outline/TOC level, e.g. '{{odt>toc:maxtoclevel=5;}}'.
61     *   Default is taken from DokuWiki config setting 'maxtoclevel'.
62     * - Insert pagebreak after TOC, e.g. '{{odt>toc:pagebreak=1;}}'.
63     *   Default is '1', means insert pagebreak after TOC.
64     * - Set style per outline/TOC level, e.g. '{{odt>toc:styleL2="color:red;font-weight:900;";}}'.
65     *   Default is 'color:black'.
66     *
67     * It is allowed to use defaults for all settings by using '{{odt>toc}}'.
68     * Multiple settings can be combined, e.g. '{{odt>toc:leader-sign=.;indents=0,0.5,1,1.5,2,2.5,3;}}'.
69     */
70    public static function replaceIndexesPlaceholders(ODTInternalParams $params, array $indexesData, array $toc) {
71        $index_count = count($indexesData);
72        for ($index_no = 0 ; $index_no < $index_count ; $index_no++) {
73            $data = $indexesData [$index_no];
74
75            // At the moment it does not make sense to disable links for the TOC
76            // because LibreOffice will insert links on updating the TOC.
77            $data ['create_links'] = true;
78            $indexContent = self::buildIndex($params->document, $toc, $data, $index_no+1);
79
80            // Replace placeholder with TOC content.
81            $params->content = str_replace ('<index-placeholder no="'.($index_no+1).'"/>', $indexContent, $params->content);
82        }
83    }
84
85    /**
86     * This function builds a TOC or chapter index.
87     * The page numbers are just a counter. Update the TOC e.g. in LibreOffice to get the real page numbers!
88     *
89     * The layout settings are taken from the configuration and $settings.
90     * $settings can include the following options syntax:
91     * - Title e.g. 'title=Example;'.
92     *   Default is 'Table of Contents' (for english, see language files for other languages default value).
93     * - Leader sign, e.g. 'leader-sign=.;'.
94     *   Default is '.'.
95     * - Indents (in cm), e.g. 'indents=indents=0,0.5,1,1.5,2,2.5,3;'.
96     *   Default is 0.5 cm indent more per level.
97     * - Maximum outline/TOC level, e.g. 'maxtoclevel=5;'.
98     *   Default is taken from DokuWiki config setting 'maxtoclevel'.
99     * - Insert pagebreak after TOC, e.g. 'pagebreak=1;'.
100     *   Default is '1', means insert pagebreak after TOC.
101     * - Set style per outline/TOC level, e.g. 'styleL2="color:red;font-weight:900;";'.
102     *   Default is 'color:black'.
103     *
104     * It is allowed to use defaults for all settings by omitting $settings.
105     * Multiple settings can be combined, e.g. 'leader-sign=.;indents=0,0.5,1,1.5,2,2.5,3;'.
106     */
107    protected static function buildIndex(ODTDocument $doc, array $toc, array $settings, $indexNo) {
108        $stylesL = array();
109        $stylesLNames = array();
110
111        // Get index type
112        $type = $settings ['type'];
113
114        // It seems to be not supported in ODT to have a different start
115        // outline level than 1.
116        $max_outline_level = 10;
117        if (!empty($settings ['maxlevel'])) {
118            $max_outline_level = $settings ['maxlevel'];
119        }
120
121        // Determine title, default for table of contents is 'Table of Contents'.
122        // Default for chapter index is empty.
123        // Syntax for 'Test' as title would be "title=test;".
124        $title = '';
125        if (!empty($settings ['title'])) {
126            $title = $settings ['title'];
127        }
128
129        // Determine leader-sign, default is '.'.
130        // Syntax for '.' as leader-sign would be "leader_sign=.;".
131        $leader_sign = '.';
132        if (!empty($settings ['leader_sign'])) {
133            $leader_sign = $settings ['leader_sign'];
134        }
135
136        // Determine indents, default is '0.5' (cm) per level.
137        // Syntax for a indent of '0.5' for 5 levels would be "indents=0,0.5,1,1.5,2;".
138        // The values are absolute for each level, not relative to the higher level.
139        $indents = '0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5';
140        if (!empty($settings ['indents'])) {
141            $indents = $settings ['indents'];
142        }
143
144        // Determine pagebreak, default is on '1'.
145        // Syntax for pagebreak off would be "pagebreak=0;".
146        $pagebreak = true;
147        if (!empty($settings ['pagebreak'])) {
148            $temp = $settings ['pagebreak'];
149            $pagebreak = false;
150            if ( $temp == '1' ) {
151                $pagebreak = true;
152            } else if ( strcasecmp($temp, 'true') == 0 ) {
153                $pagebreak = true;
154            }
155        }
156
157        // Determine text style for the index heading.
158        $styleH = '';
159        if (!empty($settings ['style_heading'])) {
160            $styleH = $settings ['style_heading'];
161        }
162
163        // Determine text styles per level.
164        // Syntax for a style level 1 is "styleL1="color:black;"".
165        // The default style is just 'color:black;'.
166        for ( $count = 0 ; $count < $max_outline_level ; $count++ ) {
167            $stylesL [$count + 1] = 'color:black;';
168            if (!empty($settings ['styleL'.($count + 1)])) {
169                $stylesL [$count + 1] = $settings ['styleL'.($count + 1)];
170            }
171        }
172
173        // Create Heading style if not empty.
174        // Default index heading style is taken from styles.xml
175        $title_style = $doc->getStyleName('contents heading');
176        if (!empty($styleH)) {
177            $properties = array();
178            $doc->getCSSStylePropertiesForODT ($properties, $styleH);
179            $properties ['style-parent'] = 'Heading';
180            $properties ['style-class'] = 'index';
181            $properties ['style-name'] = 'Contents_20_Heading_'.$indexNo;
182            $properties ['style-display-name'] = 'Contents Heading '.$indexNo;
183            $style_obj = ODTParagraphStyle::createParagraphStyle($properties);
184            $doc->addStyle($style_obj);
185            $title_style = $style_obj->getProperty('style-name');
186        }
187
188        // Create paragraph styles
189        $p_styles = array();
190        $p_styles_auto = array();
191        $indent = 0;
192        for ( $count = 0 ; $count < $max_outline_level ; $count++ )
193        {
194            $indent = $indents [$count];
195            $properties = array();
196            $doc->getCSSStylePropertiesForODT ($properties, $stylesL [$count+1]);
197            $properties ['style-parent'] = 'Index';
198            $properties ['style-class'] = 'index';
199            $properties ['style-position'] = $settings ['width'] - $indent .'cm';
200            $properties ['style-type'] = 'right';
201            $properties ['style-leader-style'] = 'dotted';
202            $properties ['style-leader-text'] = $leader_sign;
203            $properties ['margin-left'] = $indent.'cm';
204            $properties ['margin-right'] = '0cm';
205            $properties ['text-indent'] = '0cm';
206            $properties ['style-name'] = 'ToC '.$indexNo.'- Level '.($count+1);
207            $properties ['style-display-name'] = 'ToC '.$indexNo.', Level '.($count+1);
208            $style_obj = ODTParagraphStyle::createParagraphStyle($properties);
209
210            // Add paragraph style to common styles.
211            // (It MUST be added to styles NOT to automatic styles. Otherwise LibreOffice will
212            //  overwrite/change the style on updating the TOC!!!)
213            $doc->addStyle($style_obj);
214            $p_styles [$count+1] = $style_obj->getProperty('style-name');
215
216            // Create a copy of that but with parent set to the copied style
217            // and no class
218            $properties ['style-parent'] = $style_obj->getProperty('style-name');
219            $properties ['style-class'] = NULL;
220            $properties ['style-name'] = 'ToC Auto '.$indexNo.'- Level '.($count+1);
221            $properties ['style-display-name'] = NULL;
222            $style_obj_auto = ODTParagraphStyle::createParagraphStyle($properties);
223
224            // Add paragraph style to automatic styles.
225            // (It MUST be added to automatic styles NOT to styles. Otherwise LibreOffice will
226            //  overwrite/change the style on updating the TOC!!!)
227            $doc->addAutomaticStyle($style_obj_auto);
228            $p_styles_auto [$count+1] = $style_obj_auto->getProperty('style-name');
229        }
230
231        // Create text style for TOC text.
232        // (this MUST be a text style (not paragraph!) and MUST be placed in styles (not automatic styles) to work!)
233        for ( $count = 0 ; $count < $max_outline_level ; $count++ ) {
234            $properties = array();
235            $doc->getCSSStylePropertiesForODT ($properties, $stylesL [$count+1]);
236            $properties ['style-name'] = 'ToC '.$indexNo.'- Text Level '.($count+1);
237            $properties ['style-display-name'] = 'ToC '.$indexNo.', Level '.($count+1);
238            $style_obj = ODTTextStyle::createTextStyle($properties);
239            $stylesLNames [$count+1] = $style_obj->getProperty('style-name');
240            $doc->addStyle($style_obj);
241        }
242
243        // Generate ODT toc tag and content
244        switch ($type) {
245            case 'toc':
246                $tag = 'table-of-content';
247                $name = 'Table of Contents';
248                $index_name = 'Table of Contents';
249                $source_attrs = 'text:outline-level="'.$max_outline_level.'" text:use-index-marks="false"';
250            break;
251            case 'chapter':
252                $tag = 'table-of-content';
253                $name = 'Table of Contents';
254                $index_name = 'Table of Contents';
255                $source_attrs = 'text:outline-level="'.$max_outline_level.'" text:use-index-marks="false" text:index-scope="chapter"';
256            break;
257        }
258
259        $content  = '<text:'.$tag.' text:style-name="Standard" text:protected="true" text:name="'.$name.'">';
260        $content .= '<text:'.$tag.'-source '.$source_attrs.'>';
261        if (!empty($title)) {
262            $content .= '<text:index-title-template text:style-name="'.$title_style.'">'.$title.'</text:index-title-template>';
263        } else {
264            $content .= '<text:index-title-template text:style-name="'.$title_style.'"/>';
265        }
266
267        // Create TOC templates per outline level.
268        // The styles listed here need to be the same as later used for the headers.
269        // Otherwise the style of the TOC entries/headers will change after an update.
270        for ( $count = 0 ; $count < $max_outline_level ; $count++ )
271        {
272            $level = $count + 1;
273            $content .= '<text:'.$tag.'-entry-template text:outline-level="'.$level.'" text:style-name="'.$p_styles [$level].'">';
274            $content .= '<text:index-entry-link-start text:style-name="'.$stylesLNames [$level].'"/>';
275            $content .= '<text:index-entry-chapter/>';
276            if ($settings ['numbered_headings'] == true) {
277                $content .= '<text:index-entry-span> </text:index-entry-span>';
278            }
279            $content .= '<text:index-entry-text/>';
280            $content .= '<text:index-entry-tab-stop style:type="right" style:leader-char="'.$leader_sign.'"/>';
281            $content .= '<text:index-entry-page-number/>';
282            $content .= '<text:index-entry-link-end/>';
283            $content .= '</text:'.$tag.'-entry-template>';
284        }
285
286        $content .= '</text:'.$tag.'-source>';
287        $content .= '<text:index-body>';
288        if (!empty($title)) {
289            $content .= '<text:index-title text:style-name="Standard" text:name="'.$index_name.'_Head">';
290            $content .= '<text:p text:style-name="'.$title_style.'">'.$title.'</text:p>';
291            $content .= '</text:index-title>';
292        }
293
294        // Add headers to TOC.
295        $page = 0;
296        $links = $settings ['create_links'];
297        if ($type == 'toc') {
298            $content .= self::getTOCBody ($toc, $p_styles_auto, $stylesLNames, $max_outline_level, $links);
299        } else {
300            $startRef = $settings ['start_ref'];
301            $content .= self::getChapterIndexBody ($toc, $p_styles_auto, $stylesLNames, $max_outline_level, $links, $startRef);
302        }
303
304        $content .= '</text:index-body>';
305        $content .= '</text:'.$tag.'>';
306
307        // Add a pagebreak if required.
308        if ( $pagebreak ) {
309            $style_name = $doc->createPagebreakStyle(NULL, false);
310            $content .= '<text:p text:style-name="'.$style_name.'"/>';
311        }
312
313        // Return index content.
314        return $content;
315    }
316
317    /**
318     * This function creates the entries for a table of contents.
319     * All heading are included up to level $max_outline_level.
320     *
321     * @param array   $p_styles            Array of style names for the paragraphs.
322     * @param array   $stylesLNames        Array of style names for the links.
323     * @param array   $max_outline_level   Depth of the table of contents.
324     * @param boolean $links               Shall links be created.
325     * @return string TOC body entries
326     */
327    protected static function getTOCBody(array $toc, $p_styles, $stylesLNames, $max_outline_level, $links) {
328        $page = 0;
329        $content = '';
330        foreach ($toc as $item) {
331            $params = explode (',', $item);
332
333            // Only add the heading to the TOC if its <= $max_outline_level
334            if ( $params [3] <= $max_outline_level ) {
335                $level = $params [3];
336                $content .= '<text:p text:style-name="'.$p_styles [$level].'">';
337                if ( $links == true ) {
338                    $content .= '<text:a xlink:type="simple" xlink:href="#'.$params [0].'" text:style-name="'.$stylesLNames [$level].'" text:visited-style-name="'.$stylesLNames [$level].'">';
339                }
340                $content .= $params [2];
341                $content .= '<text:tab/>';
342                $page++;
343                $content .= $page;
344                if ( $links == true ) {
345                    $content .= '</text:a>';
346                }
347                $content .= '</text:p>';
348            }
349        }
350        return $content;
351    }
352
353    /**
354     * This function creates the entries for a chapter index.
355     * All headings of the chapter are included uo to level $max_outline_level.
356     *
357     * @param array   $p_styles            Array of style names for the paragraphs.
358     * @param array   $stylesLNames        Array of style names for the links.
359     * @param array   $max_outline_level   Depth of the table of contents.
360     * @param boolean $links               Shall links be created.
361     * @param string  $startRef            Reference-ID of chapter main heading.
362     * @return string TOC body entries
363     */
364    protected static function getChapterIndexBody(array $toc, $p_styles, $stylesLNames, $max_outline_level, $links, $startRef) {
365        $start_outline = 1;
366        $in_chapter = false;
367        $first = true;
368        $content = '';
369        foreach ($toc as $item) {
370            $params = explode (',', $item);
371
372            if ($in_chapter == true || $params [0] == $startRef ) {
373                $in_chapter = true;
374
375                // Is this the start of a new chapter?
376                if ( $first == false && $params [3] <= $start_outline ) {
377                    break;
378                }
379
380                // Only add the heading to the TOC if its <= $max_outline_level
381                if ( $params [3] <= $max_outline_level ) {
382                    $level = $params [3];
383                    $content .= '<text:p text:style-name="'.$p_styles [$level].'">';
384                    if ( $links == true ) {
385                        $content .= '<text:a xlink:type="simple" xlink:href="#'.$params [0].'" text:style-name="'.$stylesLNames [$level].'" text:visited-style-name="'.$stylesLNames [$level].'">';
386                    }
387                    $content .= $params [2];
388                    $content .= '<text:tab/>';
389                    $page++;
390                    $content .= $page;
391                    if ( $links == true ) {
392                        $content .= '</text:a>';
393                    }
394                    $content .= '</text:p>';
395                }
396                $first = false;
397            }
398        }
399        return $content;
400    }
401}
402