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