xref: /plugin/siteexport/syntax/toc.php (revision a99d6dee585a54b79ac48307f651073082842aab)
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 m�glicherweise 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    					}
142    				}
143			    }
144
145				$renderer->section_open("1 sitetoc");
146				if ( $renderer->meta['sitetoc']['noTocHeader'] === false ) {
147					$renderer->header($lang['toc'], 1, $data['pos']);
148				}
149
150				return true;
151			} else
152
153			// All Output has been done
154			if ( !is_array($data) && $data == 'save__meta' ) {
155
156				// Close TOC
157				$renderer->section_close();
158
159				if ( $renderer->meta['sitetoc']['noTOC'] === true ) {
160					$renderer->doc = preg_replace("/<div.*?sitetoc.*?$/si", "", $renderer->doc);
161				}
162
163				// If this is not set, we may have it as Metadata
164				if ( !$this->mergedPages && $renderer->meta['sitetoc']['mergeDoc'] ) {
165					$toc = $renderer->meta['sitetoc']['siteexportTOC'];
166
167					if ( is_array($toc)) {
168						foreach ($toc as $tocItem ) {
169							$this->mergedPages[] = array($tocItem['id'], $tocItem['depth']);
170						}
171					}
172
173				}
174
175				// If there is some data to be merged
176				if ( count($this->mergedPages) > 0) {
177
178					$renderer->doc = ''; // Start fresh!
179
180					$renderer->section_open("1 mergedsite");
181
182					// Prepare lookup Array
183					foreach ( $this->mergedPages as $tocItem ) {
184						$this->includedPages[] = array_shift(explode('#', $tocItem[0]));
185					}
186
187					// Load the instructions
188					$instr = array();
189					foreach ( $this->mergedPages as $tocElement ) {
190
191					    list($tocItem, $depth) = $tocElement;
192						$file    = wikiFN($tocItem);
193
194						if(@file_exists($file)) {
195							$instructions = p_cached_instructions($file, false, $tocItem);
196						} else {
197							$instructions = p_get_instructions(io_readWikiPage($file,$tocItem));
198						}
199
200						// Convert Link and header instructions
201						$instructions = $this->_convertInstructions($instructions, $addID, $renderer, $depth);
202
203						if ( $renderer->meta['sitetoc']['mergeHeader'] && !empty($instr) ) {
204							// Merge
205							$instr = $this->_mergeWithHeaders($instr, $instructions, 1);
206
207						} else {
208							// Concat
209							$instr = array_merge($instr, $instructions);
210						}
211					}
212
213					if (!empty($instr)) {
214    					$this->_cleanInstructions($instr, '/section_(close|open)/');
215    					$this->_cleanInstructions($instr, '/listu_(close|open)/');
216    					$this->_cleanInstructions($instr, '/listo_(close|open)/');
217
218    					$this->_render_output($renderer, $mode, $instr);
219					}
220
221					$renderer->section_close();
222				}
223				return true;
224			}
225
226			// Save the current ID
227			$LNID = $SID;
228
229			// Add ID to flags['mergeDoc']
230			if ( $renderer->meta['sitetoc']['mergeDoc'] === true ) { // || (count($renderer->meta['sitetoc']['siteexportTOC']) > 0 && $renderer->meta['sitetoc']['siteexportMergeDoc'] === true) ) {
231				$this->mergedPages[] = array($SID, $DEPTH);
232				$default = $renderer->_simpleTitle($SID); $isImage = false;
233				resolve_pageid(getNS($ID),$SID,$exists);
234
235				$NAME = empty($NAME) ? p_get_first_heading($SID,true) : $NAME;
236				$LNID = "$ID#" . sectionID($SID, $check);
237
238			} else {
239				// // print normal internal link (XHTML odt)
240				$renderer->internallink($LNID, $NAME, null);
241
242				// Display Description underneath
243				if ( $renderer->meta['sitetoc']['showDescription'] === true ) {
244					// $renderer->p_open();
245					$renderer->cdata(p_get_metadata($SID, 'description abstract', true));
246					// $renderer->p_close();
247				}
248			}
249
250			// Render Metadata
251		} else if ($mode == 'metadata') {
252			if ( !is_array($data) && $data == 'save__meta' ) {
253				$renderer->meta['sitetoc']['siteexportTOC'] = $this->savedToc;
254
255                foreach ($this->savedToc as $page) {
256                    $renderer->meta['relation']['references'][$page['id']] = $page['exists'];
257                }
258
259				$this->savedToc = array();
260			} else if ( !isset($data['start']) && !isset($data['pos']) ) {
261				$this->savedToc[] = $this->__addTocItem($SID, $NAME, $DEPTH, $renderer);
262			}
263		} else {
264			return false;
265		}
266
267		return true;
268	}
269
270	/*
271	 * pull apart the ID and create an Entry for the TOC
272	 */
273	function __addTocItem($id, $name, $depth, $renderer) {
274		global $conf;
275		global $ID;
276
277		// Render Title
278		$default = $renderer->_simpleTitle($id);
279		$exists = false; $isImage = false; $linktype = null;
280		resolve_pageid(getNS($ID),$id,$exists);
281		$name = $renderer->_getLinkTitle($name, $default, $isImage, $id, $linktype);
282
283		//keep hash anchor
284		list($id,$hash) = explode('#',$id,2);
285		if(!empty($hash)) $hash = $renderer->_headerToLink($hash);
286
287		// Build Sitetoc Item
288		$item = array();
289		$item['id'] = $id;
290		$item['name'] = $name;
291		$item['anchor'] = $hash;
292		$item['depth'] = $depth;
293		$item['exists'] = $exists;
294		if(!$conf['skipacl'] && auth_quickaclcheck($item['id']) < AUTH_READ){
295			return false;
296		}
297
298		return $item;
299	}
300
301	/*
302	 * Render the output of one page
303	 */
304	function _render_output($renderer, $mode, $instr) {
305		global $ID;
306
307		// Section IDs
308		// $addID = sectionID($addID, $check);	//not possible to use a:b:c for id
309
310		if ( $mode == 'xhtml' ) {
311
312			//--------RENDER
313			//renderer information(TOC build / Cache used)
314			$info = array();
315			$content = p_render($mode, $instr, $info);
316
317			//Remove TOC`s, section edit buttons and tags
318			$content = $this->_cleanXHTML($content);
319
320			// embed the included page
321			// $renderer->doc .= '<div class="include">';
322			//add an anchor to find start of a inserted page
323			// $renderer->doc .= "<a name='$addID' id='$addID'>";
324			$renderer->doc .= $content;
325			// $renderer->doc .= '</div>';
326		} else if ( $mode == 'odt') {
327
328			// Loop through the instructions
329			foreach ( $instr as $instruction ) {
330				// Execute the callback against the Renderer
331				call_user_func_array(array($renderer, $instruction[0]),$instruction[1]);
332			}
333		}
334	}
335
336	/*
337	 * Corrects relative internal links and media and
338	 * converts headers of included pages to subheaders of the current page
339	 */
340	function _convertInstructions($instr, $id, &$renderer, $depth=1) {
341		global $ID;
342		global $conf;
343
344		$n = count($instr);
345
346		for ($i = 0; $i < $n; $i++){
347			//internal links(links inside this wiki) an relative links
348			if((substr($instr[$i][0], 0, 12) == 'internallink')){
349				$this->_convert_link($renderer,$instr[$i],$id);
350			}
351			else if((substr($instr[$i][0], 0, 13) == 'internalmedia')){
352				$this->_convert_media($renderer,$instr[$i],$id);
353			}
354			else if((substr($instr[$i][0], 0, 6) == 'header')){
355				$this->_convert_header($renderer,$instr[$i],$depth-1); // -1 because the depth starts at 1
356			}
357			else if((substr($instr[$i][0], 0, 12) == 'section_open')){
358				$this->_convert_section($renderer,$instr[$i],$depth-1); // -1 because the depth starts at 1
359			}
360		}
361
362		//if its the document start, cut off the first element(document information)
363		if ($instr[0][0] == 'document_start')
364		return array_slice($instr, 1, -1);
365		else
366		return $instr;
367	}
368
369	/*
370	 * Convert link of given instruction
371	 */
372	function _convert_link(&$renderer,&$instr,$id) {
373		global $ID;
374
375		$exists = false;
376
377		resolve_pageid(getNS($id),$instr[1][0],$exists);
378		list( $pageID, $pageReference ) = explode("#", $instr[1][0], 2);
379
380		if ( in_array($pageID, $this->includedPages) ) {
381			// Crate new internal Links
382			$check = null;
383
384			// Either get existing reference or create from first heading. If still not there take the alternate ID
385			$pageNameLink = empty( $pageReference ) ? sectionID($pageID,$check) : $pageReference;
386
387			$instr[1][0] = $ID . "#" . $pageNameLink;
388
389		} else {
390			// Convert external Links to plain Text
391
392			$instr = array(
393						"cdata",
394			array($instr[1][1]),
395			$instr[2]
396			);
397		}
398	}
399
400	/*
401	 * Convert internalmedia of given instruction
402	 */
403	function _convert_media(&$renderer,&$instr,$id) {
404		global $ID;
405
406		// Resolvemedia returns the absolute path to media by reference
407		$exists = false;
408		resolve_mediaid(getNS($id),$instr[1][0],$exists);
409	}
410
411	function _convert_header(&$renderer, &$instr, $depth) {
412    	// More Depth!
413    	$instr[1][1] += $depth;
414	}
415
416	function _convert_section(&$renderer, &$instr, $depth) {
417    	// More Depth!
418    	$instr[1][0] += $depth;
419	}
420
421	function _mergeWithHeaders($existing, $newInstructions, $level = 1) {
422
423		$returnInstructions = array();
424		$preparedInstructions = array();
425		$existingStart = $existingEnd = 0;
426		$firstRun = true;
427
428		while ( $this->_findNextHeaderSection($existing, $level, $existingStart, $existingEnd) ) {
429
430			if ( $firstRun ) {
431				$returnInstructions = array_merge($returnInstructions, array_slice($existing, 0, $existingStart));
432				$firstRun = false;
433			}
434
435			$currentSlice = array_slice($existing, $existingStart, $existingEnd - $existingStart);
436
437			// Find matching part with headername
438			$newStart = $newEnd = 0;
439			if ( $this->_findNextHeaderSection($newInstructions, $level, $newStart, $newEnd, $currentSlice[0][1][0]) ) {
440
441				$newSlice = array_slice($newInstructions, $newStart, $newEnd - $newStart);
442				if ( $newSlice[0][0] == 'header' )
443					array_shift($newSlice); // Remove Heading
444
445				// merge found parts on next level.
446				$returnedInstructions = $this->_mergeWithHeaders($currentSlice, $newSlice, $level+1);
447
448				// Put them at the end!
449				$preparedInstructions = array_merge($preparedInstructions, $returnedInstructions);
450
451				// Remove from input
452				array_splice($newInstructions, $newStart, $newEnd - $newStart);
453			} else {
454				$preparedInstructions = array_merge($preparedInstructions, $currentSlice);
455			}
456
457			$existingStart = $existingEnd;
458		}
459
460		// Append the rest
461		$returnInstructions = array_merge($returnInstructions, array_slice($existing, $existingStart));
462
463		// Check for section close inconsistencies and put one at the very end ...
464		$section_postpend = array();
465		if (
466		    (
467			($tmp = array_slice($newInstructions, -1))
468			&& ($tmp[0][0] == 'section_close')
469		    )
470		    &&
471		    (
472			($tmp = array_slice($newInstructions, -2))
473			&& ($tmp[0][0] == 'section_close' )
474		    )
475		) {
476			$section_postpend = array_splice($newInstructions, -1);
477		}
478		if (
479		    (
480			($tmp = array_slice($returnInstructions, -1))
481			&& ($tmp[0][0] == 'section_close')
482		    )
483		    &&
484		    (
485			($tmp = array_slice($returnInstructions, -2))
486			&& ($tmp[0][0] == 'section_close' )
487		    )
488		) {
489			$section_postpend = array_merge($section_postpend, array_splice($returnInstructions, -1));
490		}
491
492		// What if there are headings left inside the $newInstructions?????
493		// Find matching part with headername
494		$newStart = $newEnd = 0;
495		$section_prepend = array();
496		if ( $this->_findNextHeaderSection($newInstructions, $level, $newStart, $newEnd) ) {
497			// If there are header in here, build a prepend and have the rest at the end
498			$section_prepend = array_splice($newInstructions, 0, $newStart);
499		} else {
500			// If not, prepend all of it.
501			$section_prepend = $newInstructions;
502			$newInstructions = array();
503		}
504
505		$returnInstructions = array_merge($returnInstructions, $section_prepend, $preparedInstructions, $newInstructions, $section_postpend);
506
507		return $returnInstructions;
508	}
509
510	function _findNextHeaderSection($section, $level, &$start, &$end, $headerName = null) {
511
512		$inCount = count($section);
513		$currentSlice = -1;
514
515		// Find Level 1 Header that matches.
516		for( $i=$start ; $i < $inCount ; $i++ ) {
517
518			$instruction = $section[$i];
519			$end = $i; // Or it will be lost and a section close will be missing.
520
521			// First Level Header
522			if ( $instruction[0] == 'header' && $instruction[1][1] == $level ) {
523
524				if ( $currentSlice > 0 ) {
525					return true;
526				}
527
528				if ( $headerName == null || ( $headerName == $instruction[1][0] ) ) {
529					// Begin of new slice ...
530					$start = $currentSlice = $i;
531				}
532			}
533		}
534
535		// Nothing found
536		$end = $i; // Or it will be lost and a section close will be missing.
537		return $currentSlice > 0;
538	}
539
540	function _cleanInstructions(&$instructions, $tag) {
541
542		$inCount = count($instructions);
543		for( $i=0 ; $i < $inCount ; $i++ ) {
544
545			// Last instruction
546			if ( $i == $inCount-1 ) {
547				break;
548			}
549
550			if ( preg_match($tag, $instructions[$i][0]) && preg_match($tag, $instructions[$i+1][0]) && $instructions[$i][0] != $instructions[$i+1][0] ) {
551
552				// found different tags, but both match the expression and follow each other - so they can be elliminated
553				array_splice($instructions, $i, 2);
554				$inCount -= 2;
555				$i--;
556			}
557
558		}
559	}
560
561	/**
562	 * Remove TOC, section edit buttons and tags
563	 */
564	function _cleanXHTML($xhtml){
565		$replace  = array(
566			'!<div class="toc">.*?(</div>\n</div>)!s' => '', // remove TOCs
567			'#<!-- SECTION \[(\d*-\d*)\] -->#e'       => '', // remove section edit buttons
568			'!<div id="tags">.*?(</div>)!s'           => ''  // remove category tags
569		);
570		$xhtml  = preg_replace(array_keys($replace), array_values($replace), $xhtml);
571		return $xhtml;
572	}
573
574
575	/**
576	 * Allow the plugin to prevent DokuWiki creating a second instance of itself
577	 *
578	 * @return bool   true if the plugin can not be instantiated more than once
579	 */
580	function isSingleton() {
581		return true;
582	}
583}
584// vim:ts=4:sw=4:et:enc=utf-8:
585