xref: /plugin/siteexport/syntax/toc.php (revision 0571ece201b9e3bc14846f6c88d943a4f1512014)
1<?php
2/**
3 * Search with Scopes
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     i-net software <tools@inetsoftware.de>
7 * @author     Gerry Weissbach <gweissbach@inetsoftware.de>
8 */
9
10// must be run within Dokuwiki
11if (!defined('DOKU_INC')) die();
12if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
13
14require_once(DOKU_PLUGIN . 'syntax.php');
15
16class syntax_plugin_siteexport_toc extends DokuWiki_Syntax_Plugin {
17
18	var $insideToc = false;
19	var $savedToc = array();
20	var $options = array();
21
22	var $mergedPages = array();
23	var $includedPages = array();
24
25	function getType() { return 'protected'; }
26	function getPType() { return 'block'; }
27	function getAllowedTypes() { return array('container'); }
28	function getSort() { return 100; }
29
30	/**
31	 * Connect pattern to lexer
32	 */
33	function connectTo($mode) {
34		$this->Lexer->addEntryPattern('<toc>(?=.*?</toc>)', $mode, 'plugin_siteexport_toc');
35		$this->Lexer->addEntryPattern('<toc .+?>(?=.*?</toc>)', $mode, 'plugin_siteexport_toc');
36		$this->Lexer->addSpecialPattern("\[\[.+?\]\]", $mode, 'plugin_siteexport_toc');
37	}
38
39	function postConnect() {
40		$this->Lexer->addExitPattern('</toc.*?>', 'plugin_siteexport_toc');
41	}
42
43	function handle($match, $state, $pos, Doku_Handler $handler) {
44		global $ID, $INFO;
45
46		switch ($state) {
47			case DOKU_LEXER_ENTER:
48
49				$this->insideToc = true;
50
51				$this->options = explode(' ', substr($match, 5, -1));
52
53				return array('start' => true, 'pos' => $pos, 'options' => $this->options);
54				break;
55
56			case DOKU_LEXER_SPECIAL:
57
58				if ($this->insideToc) {
59
60					$link = preg_replace(array('/^\[\[/', '/\]\]$/u'), '', $match);
61					// Split title from URL
62					$link = explode('|', $link, 2);
63					if (!isset($link[1])) {
64						$link[1] = NULL;
65					} else if (preg_match('/^\{\{[^\}]+\}\}$/', $link[1])) {
66						// If the title is an image, convert it to an array containing the image details
67						$link[1] = Doku_Handler_Parse_Media($link[1]);
68					}
69					$link[0] = trim($link[0]);
70
71					if (!(preg_match('/^[a-zA-Z0-9\.]+>{1}.*$/u', $link[0]) ||
72					preg_match('/^\\\\\\\\[\w.:?\-;,]+?\\\\/u', $link[0]) ||
73					preg_match('#^([a-z0-9\-\.+]+?)://#i', $link[0]) ||
74					preg_match('<' . PREG_PATTERN_VALID_EMAIL . '>', $link[0]) ||
75					preg_match('!^#.+!', $link[0]))
76					) {
77
78						// Get current depth from call stack
79						$depth = 1;
80						if ($handler->CallWriter instanceof Doku_Handler_List) {
81
82							$calls = array_reverse($handler->CallWriter->calls);
83							$call = $calls[0];
84							foreach ($calls as $item) {
85								if (in_array($item[0], array('list_item', 'list_open'))) { $call = $item; break; }
86							}
87
88							$depth = $handler->CallWriter->interpretSyntax($call[1][0], $listType)-1; // Minus one because of plus one inside the interpret function
89						}
90
91						if (empty($link[0])) { break; } // No empty elements. This would lead to problems
92						return array($link[0], $link[1], $depth);
93						break;
94					} else {
95						// use parser! - but with another p
96						$handler->internallink($match, $state, $pos);
97					}
98				} else {
99					// use parser!
100					$handler->internallink($match, $state, $pos);
101				}
102
103				return false;
104			case DOKU_LEXER_UNMATCHED:
105
106    			$handler->_addCall('cdata', array($match), $pos);
107
108				return false;
109				break;
110			case DOKU_LEXER_EXIT:
111
112				$this->insideToc = false;
113				return 'save__meta';
114				break;
115		}
116		return false;
117	}
118
119	function render($mode, Doku_Renderer $renderer, $data) {
120		global $ID, $lang, $INFO;
121
122		list($SID, $NAME, $DEPTH) = $data;
123
124		resolve_pageid(getNS($ID), $SID, $exists);
125//		$SID = cleanID($SID); // hier kein cleanID, da sonst moeglicherweise der anker verloren geht
126
127        //    Render XHTML and ODT
128		if ($mode == 'xhtml' || $mode == 'odt') {
129
130		    // TOC Title
131			if (is_array($data) && $data['start'] == true) {
132
133			    if (is_Array($data['options'])) {
134                    foreach ($data['options'] as $opt) {
135    					switch ($opt) {
136    						case 'description' : $renderer->meta['sitetoc']['showDescription'] = true; break;
137    						case 'notoc' : $renderer->meta['sitetoc']['noTOC'] = true; break;
138    						case 'merge' : $renderer->meta['sitetoc']['mergeDoc'] = true; break;
139    						case 'nohead' : $renderer->meta['sitetoc']['noTocHeader'] = true; break;
140    						case 'mergeheader' : $renderer->meta['sitetoc']['mergeHeader'] = true; break;
141    						case 'pagebreak' : $renderer->meta['sitetoc']['pagebreak'] = true; break;
142    					}
143    				}
144			    }
145
146				$renderer->section_open("1 sitetoc");
147				if ($renderer->meta['sitetoc']['noTocHeader'] === false) {
148					$renderer->header($lang['toc'], 1, $data['pos']);
149				}
150
151				return true;
152			} else
153
154			// All Output has been done
155			if (!is_array($data) && $data == 'save__meta') {
156
157				// Close TOC
158				$renderer->section_close();
159
160				if ($renderer->meta['sitetoc']['noTOC'] === true) {
161					$renderer->doc = preg_replace("/<div.*?sitetoc.*?$/si", "", $renderer->doc);
162				}
163
164				// If this is not set, we may have it as Metadata
165				if (!$this->mergedPages && $renderer->meta['sitetoc']['mergeDoc']) {
166					$toc = $renderer->meta['sitetoc']['siteexportTOC'];
167
168					if (is_array($toc)) {
169						foreach ($toc as $tocItem) {
170							$this->mergedPages[] = array($tocItem['id'], $tocItem['depth']);
171						}
172					}
173
174				}
175
176				// If there is some data to be merged
177				if (count($this->mergedPages) > 0) {
178
179					$renderer->doc = ''; // Start fresh!
180
181					$renderer->section_open("1 mergedsite");
182
183					// Prepare lookup Array
184					foreach ($this->mergedPages as $tocItem) {
185						$this->includedPages[] = array_shift(explode('#', $tocItem[0]));
186					}
187
188					// Load the instructions
189					$instr = array();
190					foreach ($this->mergedPages as $tocElement) {
191
192					    list($tocItem, $depth) = $tocElement;
193						$file = wikiFN($tocItem);
194
195						if (@file_exists($file)) {
196							$instructions = p_cached_instructions($file, false, $tocItem);
197						} else {
198							$instructions = p_get_instructions(io_readWikiPage($file, $tocItem));
199						}
200
201						// Convert Link and header instructions
202						$instructions = $this->_convertInstructions($instructions, $addID, $renderer, $depth);
203
204						if ($renderer->meta['sitetoc']['mergeHeader'] && !empty($instr)) {
205							// Merge
206							$instr = $this->_mergeWithHeaders($instr, $instructions, 1);
207						} else
208						if ($renderer->meta['sitetoc']['pagebreak']) {
209    						$instr = array_merge($instr, $instructions, $this->_convertInstructions(p_get_instructions('<sitepagebreak>'), $addID, $renderer, $depth));
210						} else {
211							// Concat
212							$instr = array_merge($instr, $instructions);
213						}
214					}
215
216					if (!empty($instr)) {
217    					$this->_cleanInstructions($instr, '/section_(close|open)/');
218    					$this->_cleanInstructions($instr, '/listu_(close|open)/');
219    					$this->_cleanInstructions($instr, '/listo_(close|open)/');
220
221						//if its the document start, cut off the first element(document information)
222                        if ($instr[count($instr)-1][1][0] == 'siteexport_pagebreak') {
223                            $instr = array_slice($instr, 0, -1);
224                        }
225
226    					$this->_render_output($renderer, $mode, $instr);
227					}
228
229					$renderer->section_close();
230				}
231				return true;
232			}
233
234			// Save the current ID
235			$LNID = $SID;
236
237			// Add ID to flags['mergeDoc']
238			if ($renderer->meta['sitetoc']['mergeDoc'] === true) { // || (count($renderer->meta['sitetoc']['siteexportTOC']) > 0 && $renderer->meta['sitetoc']['siteexportMergeDoc'] === true) ) {
239				$this->mergedPages[] = array($SID, $DEPTH);
240				$default = $renderer->_simpleTitle($SID); $isImage = false;
241				resolve_pageid(getNS($ID), $SID, $exists);
242
243				$NAME = empty($NAME) ? p_get_first_heading($SID, true) : $NAME;
244				$LNID = "$ID#" . sectionID($SID, $check);
245
246			} else {
247				// // print normal internal link (XHTML odt)
248				$renderer->internallink($LNID, $NAME, null);
249
250				// Display Description underneath
251				if ($renderer->meta['sitetoc']['showDescription'] === true) {
252					// $renderer->p_open();
253					$renderer->cdata(p_get_metadata($SID, 'description abstract', true));
254					// $renderer->p_close();
255				}
256			}
257
258			// Render Metadata
259		} else if ($mode == 'metadata') {
260			if (!is_array($data) && $data == 'save__meta') {
261				$renderer->meta['sitetoc']['siteexportTOC'] = $this->savedToc;
262
263                foreach ($this->savedToc as $page) {
264                    $renderer->meta['relation']['references'][$page['id']] = $page['exists'];
265                }
266
267				$this->savedToc = array();
268			} else if (!isset($data['start']) && !isset($data['pos'])) {
269				$this->savedToc[] = $this->__addTocItem($SID, $NAME, $DEPTH, $renderer);
270			}
271		} else {
272			return false;
273		}
274
275		return true;
276	}
277
278	/*
279	 * pull apart the ID and create an Entry for the TOC
280	 */
281	function __addTocItem($id, $name, $depth, $renderer) {
282		global $conf;
283		global $ID;
284
285		// Render Title
286		$default = $renderer->_simpleTitle($id);
287		$exists = false; $isImage = false; $linktype = null;
288		resolve_pageid(getNS($ID), $id, $exists);
289		$name = $renderer->_getLinkTitle($name, $default, $isImage, $id, $linktype);
290
291		//keep hash anchor
292		list($id, $hash) = explode('#', $id, 2);
293		if (!empty($hash)) $hash = $renderer->_headerToLink($hash);
294
295		// Build Sitetoc Item
296		$item = array();
297		$item['id'] = $id;
298		$item['name'] = $name;
299		$item['anchor'] = $hash;
300		$item['depth'] = $depth;
301		$item['exists'] = $exists;
302		if (!$conf['skipacl'] && auth_quickaclcheck($item['id']) < AUTH_READ) {
303			return false;
304		}
305
306		return $item;
307	}
308
309	/*
310	 * Render the output of one page
311	 */
312	function _render_output($renderer, $mode, $instr) {
313		global $ID;
314
315		// Section IDs
316		// $addID = sectionID($addID, $check);	//not possible to use a:b:c for id
317
318		if ($mode == 'xhtml') {
319
320			//--------RENDER
321			//renderer information(TOC build / Cache used)
322			$info = array();
323			$content = p_render($mode, $instr, $info);
324
325			//Remove TOC`s, section edit buttons and tags
326			$content = $this->_cleanXHTML($content);
327
328			// embed the included page
329			// $renderer->doc .= '<div class="include">';
330			//add an anchor to find start of a inserted page
331			// $renderer->doc .= "<a name='$addID' id='$addID'>";
332			$renderer->doc .= $content;
333			// $renderer->doc .= '</div>';
334		} else if ($mode == 'odt') {
335
336			// Loop through the instructions
337			foreach ($instr as $instruction) {
338				// Execute the callback against the Renderer
339				call_user_func_array(array($renderer, $instruction[0]), $instruction[1]);
340			}
341		}
342	}
343
344	/*
345	 * Corrects relative internal links and media and
346	 * converts headers of included pages to subheaders of the current page
347	 */
348	function _convertInstructions($instr, $id, &$renderer, $depth = 1) {
349		global $ID;
350		global $conf;
351
352		$n = count($instr);
353
354		for ($i = 0; $i < $n; $i++) {
355			//internal links(links inside this wiki) an relative links
356			if ((substr($instr[$i][0], 0, 12) == 'internallink')) {
357				$this->_convert_link($renderer, $instr[$i], $id);
358			}
359			else if ((substr($instr[$i][0], 0, 13) == 'internalmedia')) {
360				$this->_convert_media($renderer, $instr[$i], $id);
361			}
362			else if ((substr($instr[$i][0], 0, 6) == 'header')) {
363				$this->_convert_header($renderer, $instr[$i], $depth-1); // -1 because the depth starts at 1
364			}
365			else if ((substr($instr[$i][0], 0, 12) == 'section_open')) {
366				$this->_convert_section($renderer, $instr[$i], $depth-1); // -1 because the depth starts at 1
367			}
368		}
369
370		//if its the document start, cut off the first element(document information)
371		if ($instr[0][0] == 'document_start')
372		return array_slice($instr, 1, -1);
373		else
374		return $instr;
375	}
376
377	/*
378	 * Convert link of given instruction
379	 */
380	function _convert_link(&$renderer, &$instr, $id) {
381		global $ID;
382
383		$exists = false;
384
385		resolve_pageid(getNS($id), $instr[1][0], $exists);
386		list($pageID, $pageReference) = explode("#", $instr[1][0], 2);
387
388		if (in_array($pageID, $this->includedPages)) {
389			// Crate new internal Links
390			$check = null;
391
392			// Either get existing reference or create from first heading. If still not there take the alternate ID
393			$pageNameLink = empty($pageReference) ? sectionID($pageID, $check) : $pageReference;
394
395			$instr[1][0] = $ID . "#" . $pageNameLink;
396
397		} else {
398			// Convert external Links to plain Text
399
400			$instr = array(
401						"cdata",
402			array($instr[1][1]),
403			$instr[2]
404			);
405		}
406	}
407
408	/*
409	 * Convert internalmedia of given instruction
410	 */
411	function _convert_media(&$renderer, &$instr, $id) {
412		global $ID;
413
414		// Resolvemedia returns the absolute path to media by reference
415		$exists = false;
416		resolve_mediaid(getNS($id), $instr[1][0], $exists);
417	}
418
419	/**
420	 * @param integer $depth
421	 */
422	function _convert_header(&$renderer, &$instr, $depth) {
423    	// More Depth!
424    	$instr[1][1] += $depth;
425	}
426
427	/**
428	 * @param integer $depth
429	 */
430	function _convert_section(&$renderer, &$instr, $depth) {
431    	// More Depth!
432    	$instr[1][0] += $depth;
433	}
434
435	function _mergeWithHeaders($existing, $newInstructions, $level = 1) {
436
437		$returnInstructions = array();
438		$preparedInstructions = array();
439		$existingStart = $existingEnd = 0;
440		$firstRun = true;
441
442		while ($this->_findNextHeaderSection($existing, $level, $existingStart, $existingEnd)) {
443
444			if ($firstRun) {
445				$returnInstructions = array_merge($returnInstructions, array_slice($existing, 0, $existingStart));
446				$firstRun = false;
447			}
448
449			$currentSlice = array_slice($existing, $existingStart, $existingEnd-$existingStart);
450
451			// Find matching part with headername
452			$newStart = $newEnd = 0;
453			if ($this->_findNextHeaderSection($newInstructions, $level, $newStart, $newEnd, $currentSlice[0][1][0])) {
454
455				$newSlice = array_slice($newInstructions, $newStart, $newEnd-$newStart);
456				if ($newSlice[0][0] == 'header')
457					array_shift($newSlice); // Remove Heading
458
459				// merge found parts on next level.
460				$returnedInstructions = $this->_mergeWithHeaders($currentSlice, $newSlice, $level+1);
461
462				// Put them at the end!
463				$preparedInstructions = array_merge($preparedInstructions, $returnedInstructions);
464
465				// Remove from input
466				array_splice($newInstructions, $newStart, $newEnd-$newStart);
467			} else {
468				$preparedInstructions = array_merge($preparedInstructions, $currentSlice);
469			}
470
471			$existingStart = $existingEnd;
472		}
473
474		// Append the rest
475		$returnInstructions = array_merge($returnInstructions, array_slice($existing, $existingStart));
476
477		// Check for section close inconsistencies and put one at the very end ...
478		$section_postpend = array();
479		if (
480		    (
481			($tmp = array_slice($newInstructions, -1))
482			&& ($tmp[0][0] == 'section_close')
483		    )
484		    &&
485		    (
486			($tmp = array_slice($newInstructions, -2))
487			&& ($tmp[0][0] == 'section_close')
488		    )
489		) {
490			$section_postpend = array_splice($newInstructions, -1);
491		}
492		if (
493		    (
494			($tmp = array_slice($returnInstructions, -1))
495			&& ($tmp[0][0] == 'section_close')
496		    )
497		    &&
498		    (
499			($tmp = array_slice($returnInstructions, -2))
500			&& ($tmp[0][0] == 'section_close')
501		    )
502		) {
503			$section_postpend = array_merge($section_postpend, array_splice($returnInstructions, -1));
504		}
505
506		// What if there are headings left inside the $newInstructions?????
507		// Find matching part with headername
508		$newStart = $newEnd = 0;
509		$section_prepend = array();
510		if ($this->_findNextHeaderSection($newInstructions, $level, $newStart, $newEnd)) {
511			// If there are header in here, build a prepend and have the rest at the end
512			$section_prepend = array_splice($newInstructions, 0, $newStart);
513		} else {
514			// If not, prepend all of it.
515			$section_prepend = $newInstructions;
516			$newInstructions = array();
517		}
518
519		$returnInstructions = array_merge($returnInstructions, $section_prepend, $preparedInstructions, $newInstructions, $section_postpend);
520
521		return $returnInstructions;
522	}
523
524	/**
525	 * @param integer $level
526	 */
527	function _findNextHeaderSection($section, $level, &$start, &$end, $headerName = null) {
528
529		$inCount = count($section);
530		$currentSlice = -1;
531
532		// Find Level 1 Header that matches.
533		for ($i = $start; $i < $inCount; $i++) {
534
535			$instruction = $section[$i];
536			$end = $i; // Or it will be lost and a section close will be missing.
537
538			// First Level Header
539			if ($instruction[0] == 'header' && $instruction[1][1] == $level) {
540
541				if ($currentSlice > 0) {
542					return true;
543				}
544
545				if ($headerName == null || ($headerName == $instruction[1][0])) {
546					// Begin of new slice ...
547					$start = $currentSlice = $i;
548				}
549			}
550		}
551
552		// Nothing found
553		$end = $i; // Or it will be lost and a section close will be missing.
554		return $currentSlice > 0;
555	}
556
557	/**
558	 * @param string $tag
559	 */
560	function _cleanInstructions(&$instructions, $tag) {
561
562		$inCount = count($instructions);
563		for ($i = 0; $i < $inCount; $i++) {
564
565			// Last instruction
566			if ($i == $inCount-1) {
567				break;
568			}
569
570			if (preg_match($tag, $instructions[$i][0]) && preg_match($tag, $instructions[$i+1][0]) && $instructions[$i][0] != $instructions[$i+1][0]) {
571
572				// found different tags, but both match the expression and follow each other - so they can be elliminated
573				array_splice($instructions, $i, 2);
574				$inCount -= 2;
575				$i--;
576			}
577
578		}
579	}
580
581	/**
582	 * Remove TOC, section edit buttons and tags
583	 */
584	function _cleanXHTML($xhtml) {
585		$replace = array(
586			'!<div class="toc">.*?(</div>\n</div>)!s' => '', // remove TOCs
587			'#<!-- SECTION \[(\d*-\d*)\] -->#e'       => '', // remove section edit buttons
588			'!<div id="tags">.*?(</div>)!s'           => ''  // remove category tags
589		);
590		$xhtml = preg_replace(array_keys($replace), array_values($replace), $xhtml);
591		return $xhtml;
592	}
593
594
595	/**
596	 * Allow the plugin to prevent DokuWiki creating a second instance of itself
597	 *
598	 * @return bool   true if the plugin can not be instantiated more than once
599	 */
600	function isSingleton() {
601		return true;
602	}
603}
604// vim:ts=4:sw=4:et:enc=utf-8:
605