1<?php
2
3if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
4if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
5require_once(DOKU_PLUGIN.'syntax.php');
6require_once(DOKU_INC.'inc/search.php');
7
8/*
9* {{compose> namespace#depth | print numbers how_deep absolute }}
10*
11* print:
12* 		print optimization
13* numbers + how_deep:
14* 		numbering for headers(1.1.3 hallo...)
15* absolute:
16* 		absolute indentation of headers, corresponding to file-depth in namespace
17* 		or (default) relative indentation
18*/
19class syntax_plugin_composer extends DokuWiki_Syntax_Plugin {
20
21    /**
22     * how to handle <p>
23     */
24    function getPType(){
25        return 'block';
26    }
27
28    /**
29     * what other types are allowed iside
30     */
31    function getAllowedTypes() {
32        return array('substition');
33    }
34
35    /**
36     * What kind of syntax are we?
37     */
38    function getType(){
39        return 'substition';
40    }
41
42    /**
43     * Where to sort in?
44     */
45    function getSort(){
46        return 311;
47    }
48
49    /**
50     * Connect pattern to lexer
51     */
52    function connectTo($mode) {
53        $this->Lexer->addSpecialPattern("{{compose>.*?}}",$mode,'plugin_composer');
54    }
55
56    /**
57     * Handle the match
58     */
59    function handle($match, $state, $pos, &$handler){
60
61        global $conf;
62
63        $opt = array(
64            "level" => 0,
65            "nons" => false,
66            "relative" => false,
67            "print" => false,
68            "numbers" => false,
69            "numbers_depth" => 2,
70            "resort" => false
71        );
72
73        $givenOptString = "";
74        $givenOpt = array();
75        $content = "";
76
77        $match = preg_replace("/\{\{compose\>(.*)\}\}/", "$1", $match);
78
79        $nsOpt = preg_split("/\|/", $match);
80
81        if (count($nsOpt) == 1) {
82
83            $ns = $nsOpt[0];
84
85        } else {
86
87            $ns = $nsOpt[0];
88            $givenOptString = $nsOpt[1];
89            $givenOpt = preg_split("/ /", $givenOptString);
90
91        }
92
93        $nsLevel = preg_split("/#/", $ns);
94
95        if (count($nsLevel) > 1) {
96
97            $ns = $nsLevel[0];
98            $opt["level"] = $nsLevel[1];
99
100        }
101
102        if (in_array("absolute", $givenOpt)) {
103
104            if ($conf["useslashes"]) {
105
106                $opt["relative"] = substr_count($ns, "/") + 1;
107
108            } else {
109
110                $opt["relative"] = substr_count($ns, ":") + 1;
111
112            }
113
114        }
115
116        if (in_array("nons", $givenOpt)) {
117
118            $opt["nons"] = true;
119
120        }
121
122        if (in_array("print", $givenOpt)) {
123
124            $opt["split_too_long"] = true;
125
126        }
127
128        // Do we have the numbers option without a level specification?
129
130        if (in_array("numbers", $givenOpt)) {
131
132            $opt["numbers"] = true;
133
134        }
135
136        // Do we have the numbers option with the level specification?
137
138        if (preg_match("/numbers:(\d)*/", $givenOptString, $matches)) {
139
140            $opt["numbers"] = true;
141            $opt["numbers_depth"] = $matches[1];
142
143        }
144
145        if (in_array("resort", $givenOpt)) {
146
147            $opt["resort"] = true;
148
149        }
150
151        return array(
152            'namespace'=> $ns,
153            'options' => $opt
154        );
155    }
156
157    /**
158     * Create output
159     */
160    function render($mode, &$renderer, $data) {
161        if($mode == 'xhtml'){
162            //for header numbering
163            $header_count = array(	1=>0,
164                2=>0,
165                3=>0,
166                4=>0,
167                5=>0,
168            );
169
170            $data['header_count'] = &$header_count;
171
172            // get all files in the supplied namespace, with their level
173            $file_data = $this->_get_file_data($data);
174
175            /*
176             * Resort results to optimize output
177             * Sort by levels, then first sort the directories,
178             * than the start page and then the files
179             */
180
181            if ($data["options"]["resort"]) {
182
183                usort($file_data, "composer_sort_filedata");
184
185            }
186
187            foreach($file_data as $file) {
188                // build the output
189                $this->_render_output($renderer, $file, $data);
190            }
191
192            return true;
193        }
194
195        return false;
196    }
197
198    /*
199     * Get all files within the namespace
200     */
201    function _get_file_data($data) {
202        global $conf;
203
204        $options = $data['options'];
205        $ns = $data['namespace'];
206
207        $resolvedPage = "";
208        $pageExists = false;
209
210        // Reformat namespace to filename
211
212        // namespace options . or ..
213
214        $pageFile = wikiFN($ns);
215
216        if (!file_exists($pageFile)) {
217
218            // Try adding the start page
219
220            if ($conf["useslash"]) {
221
222                $ns .= "/";
223
224            } else {
225
226                $ns .= ":";
227
228            }
229
230            $ns .= $conf['start'];
231
232            $pageFile = wikiFN($ns);
233
234            if (!file_exists($pageFile)) {
235
236                return false;
237
238            }
239
240        }
241
242        $dirname = dirname($pageFile);
243        $dirname = str_replace($conf['datadir'], "", $dirname);
244
245        $file_data = array(
246            'start_lvl' => substr_count($dirname, DIRECTORY_SEPARATOR),
247            'blocked' => array(),					    //blocked files
248        );
249
250        // find all matching files
251
252        search(
253            $file_data,
254            $conf['datadir'],
255            'composer_search_index',
256            $options,
257            $dirname
258        );
259
260        // clean the file_data from non-files (for safety)
261        unset($file_data['start_lvl']);
262        unset($file_data['blocked']);
263
264        return $file_data;
265    }
266
267
268    /*
269     * Render the output
270     */
271
272    function _render_output(&$renderer, $file_data, $o){
273
274        // file attributes
275        $id = $file_data['id'];
276        $open =	$file_data['open'];
277        $content = "";
278
279        // build with relative indentation
280
281        $clevel = $file_data['level'];
282
283        if($o['options']['relative']){
284
285            // Relative identation
286
287            $clevel -= $o['options']['relative'];
288
289        }
290
291        // Don't open the file (in case of directories and nons on)
292
293        if(!$open) {
294
295            return false;
296
297        }
298
299        // Convert a:b -> home/data/pages/a/b
300
301        $file = wikiFN($id);
302
303        // Get data(in instructions format) from $file (dont use cache: false)
304
305        $instr = p_cached_instructions($file, false);
306
307        // Page was not empty
308
309        if (!empty($instr)) {
310
311            // Fix relative links and lower headers of included pages
312
313            //we dont need this option for conversion
314
315            unset($o['options']['relative']);
316
317            $instr   = $this->_convertInstructions(
318                $instr,
319                $id,
320                $renderer,
321                $clevel,
322                $o
323            );
324
325            $info = array();
326
327            // Render page
328
329            $content = p_render('xhtml', $instr, $info);
330
331            // Remove TOC`s, section edit buttons and tags
332
333            $content = $this->_cleanXHTML($content);
334        }
335
336        // Embed the included page
337
338        $renderer->doc .= '<div class="include">';
339
340        // Add an anchor to find start of a inserted page
341
342        $id = str_replace(":", "_", $file_data['id']);
343        $renderer->doc .= "<a name='$id' id='$id'>";
344        $renderer->doc .= $content;
345        $renderer->doc .= '</div>';
346
347        return true;
348
349    }
350
351    /*
352     * Corrects relative internal links and media and
353     * converts headers of included pages to subheaders of the current page
354     */
355    function _convertInstructions($instr, $incl, &$renderer, $clevel, $o){
356
357        global $ID;
358
359        // check if included page is already in output namespace
360        $iNS =	getNS($incl);
361        $iID =	getNS($ID);
362
363        //the content belongs to the original page
364        if ($iID == $iNS) {
365            $convert = false;	//just leave as it is
366        } else {
367            // The content was newly included, and is not original page content
368            $convert = true;	//convert content
369        }
370
371        for ($i = 0; $i < count($instr); $i++){
372
373            if ($convert) {
374
375                if((substr($instr[$i][0], 0, 8) == 'internal')){
376
377                    // Internal links(links inside this wiki) an relative links
378
379                    $this->_convert_link($renderer,$instr[$i],$iNS,$iID,$o);
380
381                } elseif ($instr[$i][0] == 'header'){
382
383                    // Set header level to current section level + header level
384
385                    $this->_convert_header($renderer,$instr[$i],$clevel,$o);
386
387                } elseif ($instr[$i][0] == 'section_open'){
388
389                    // The same for sections
390
391                    $level = $instr[$i][1][0] + $clevel;
392
393                    if ($level > 5) {
394
395                        $level = 5;
396
397                    }
398
399                    $instr[$i][1][0] = $level;
400
401                }
402            }
403
404            // Split long lines?
405
406            if($o['split_too_long'] && $instr[$i][0] == 'code') {
407
408                $instr[$i][1][0] = wordwrap($instr[$i][1][0],70,"\n");
409
410            }
411
412        }
413
414        // If its the document start, cut off the document information
415
416        if ($instr[0][0] == 'document_start') {
417
418            return array_slice($instr, 1, -1);
419
420        } else {
421
422            return $instr;
423
424        }
425
426    }
427
428    /*
429     * convert header of given instruction
430     */
431    function _convert_header(&$renderer,&$instr,$clevel,$o) {
432
433        global $conf;
434
435        $level = $instr[1][1] + $clevel;
436
437        // If a header level gets "lower" than 5
438        if ($level > 5) {
439
440            $level = 5;
441
442        }
443
444        $instr[1][1] = $level;
445
446        // Number headers
447
448        if($o['numbers']) {
449
450            // Number in front of header
451            $number = "";
452
453            // Reset lower levels
454            for($x = $level+1; $x <= 5; $x++) {
455                $o['header_count'][$x] = 0;
456            }
457
458            // Raise this level by 1
459            $o['header_count'][$level]++;
460
461            // If the level is high (1=high)
462
463            if($level <= $o['numbering_depth']) {
464                //build the number
465                for($x = 1; $x <= $level; $x++) {
466                    $number .= $o['header_count'][$x] . ".";
467                }
468            }
469
470            //save
471            $instr[1][0] = $number . " " . $instr[1][0];
472        }
473
474        // add TOC items
475        if ($level >= $conf['toptoclevel'] && $level <= $conf['maxtoclevel']){
476
477            $text = $instr[1][0];
478            $header_id  = $renderer->_headerToLink($text, 'true');
479
480            //add item to TOC
481            $renderer->toc[] = array(
482                'hid'   => $header_id,
483                'title' => $text,
484                'type'  => 'ul',
485                'level' => $level-$conf['toptoclevel']+1,
486            );
487
488        }
489    }
490
491    /*
492     * Convert link of given instruction
493     */
494    function _convert_link(&$renderer, &$instr, $iNS, $id, $o) {
495
496        // Relative subnamespace
497        if ($instr[1][0]{0} == '.'){
498
499            // Build complete address
500            $instr[1][0] = $iNS.':'.substr($instr[1][0], 1);
501
502        } else if (strpos($instr[1][0],':') === false) {
503            //relative link
504
505            $instr[1][0] = $iNS.':'.$instr[1][0];
506        }
507
508        // link to another page, but within our included namespace
509        // if the link starts with our namespace
510
511        if(strpos($instr[1][0], $o['start_ns']) === 0) {
512
513            //if the link is inside the included depth of pages
514            if($o['depth'] === 0 ||
515                substr_count($o['start_ns'],":") + $o['depth'] >=
516                    substr_count($instr[1][0],":")
517            ) {
518
519                // Convert to internal link
520                if(strpos($instr[1][0],"#") !== false) {
521                    // Get last part a:b#c -> c
522                    $levels = preg_split("/#/",$instr[1][0]);
523
524                    // $id we are in atm + # + id of anchor
525                    $instr[1][0] = $id . "#" . $levels[sizeof($levels)-1];
526                } else {
527
528                    // a:b:c
529
530                    /**
531                     * $id we are in atm + # + full id of page to find(->
532                     * unique id) not possible to use a:b:c for id --> : -> _
533                     */
534
535                    $instr[1][0] = $id . "#" . str_replace(
536                        ":",
537                        "_",
538                        $instr[1][0]
539                    );
540
541                    // If page-link has no name
542
543                    if(empty($instr[1][1])) {
544
545                        // TODO: get the real name of the page :(
546
547                        $name = preg_split("/_/",$instr[1][0]);
548
549                        // Last part of id -> name
550
551                        $instr[1][1] = $name[sizeof($name)-1];
552                    }
553                }
554            }
555        }
556    }
557
558    /**
559     * Remove TOC, section edit buttons and tags
560     */
561    function _cleanXHTML($xhtml){
562
563        $replace  = array(
564            '!<div class="toc">.*?(</div>\n</div>)!s' => '', // remove TOCs
565            '#<!-- SECTION \[(\d*-\d*)\] -->#e'       => '', // remove section edit buttons
566            '!<div id="tags">.*?(</div>)!s'           => ''  // remove category tags
567        );
568
569        $xhtml  = preg_replace(
570            array_keys($replace),
571            array_values($replace),
572            $xhtml
573        );
574
575        return $xhtml;
576    }
577}
578
579/*
580* evaluate the files presented by search
581*
582* modify $data according to our wishes
583*/
584function composer_search_index(&$data, $base, $file, $type, $lvl, $opts) {
585
586    // include in result ?
587    $return = true;
588
589    // Directories
590    if ($type == 'd'){
591        //if max-level is reached, don't return them
592        if ($opts['level'] == $lvl) {
593
594            $return = false;
595
596        }
597
598        //add the directory name to list of blocked files
599        //so that file with same name will not be included
600        $data['blocked'][] = $file . ".txt";
601
602        //if we don't want namespace-nodes in the resultset
603        if ($opts['nons']) return $return;
604
605    } elseif($type == 'f' && !preg_match('#\.txt$#',$file)){
606        // Don't add files, that end in txt
607        return false;
608    }
609
610    $id = pathID($file);
611
612    // Check hiddens
613    if($type == 'f' && isHiddenPage($id)){
614        return false;
615    }
616
617    // Check ACL (for namespaces too)
618    if(auth_quickaclcheck($id) < AUTH_READ){
619        //we are not allowed to read
620        return false;
621    }
622
623    //check if this files was blocked by us
624    if($type == 'f' && in_array($file,$data['blocked'])){
625        return false;
626    }
627
628    // add the start level(ns we build) to the current level
629    $lvl += $data['start_lvl'];
630
631    //pack it up
632    $data[]=array( 'id'    => $id,
633        'type'  => $type,	//which type is it ?
634        'level' => $lvl,	//show on which level ?
635        'open'  => $return,	//open it ?
636    );
637
638    return $return;
639
640}
641
642/**
643 * User defined sort function to sort by level, than by directory,
644 * than by start.txt and then by filename
645 *
646 * @param $a Array left side sort argument
647 * @param $b Array right side sort argument
648 */
649
650function composer_sort_filedata($a, $b) {
651
652    global $conf;
653
654    // Sort by level
655
656    if ($a["level"] < $b["level"]) {
657
658        return -1;
659
660    } else if ($a["level"] > $b["level"]) {
661
662        return 1;
663
664    } else {
665
666        // Level is the same. Sort by directories
667
668        if (($a["type"] == "d") and ($b["type"] == "f")) {
669
670            return -1;
671
672        } else if (($a["type"] == "f") and ($b["type"] == "d")) {
673
674            return 1;
675
676        } else if (($a["type"] == "d") and ($b["type"] == "d")) {
677
678            // Both are directories. Sort by id
679
680            return strcmp($a["id"], $b["id"]);
681
682        } else {
683
684            // Both are files. Sort by start.txt
685
686            if (stristr($a["id"], $conf["start"]) &&
687                !stristr($b["id"], $conf["start"])
688            ) {
689
690                return -1;
691
692            } else if (!stristr($a["id"], $conf["start"]) &&
693                stristr($b["id"], $conf["start"])
694            ) {
695
696                return 1;
697
698            } else {
699
700                // No page is a start page or both, sort by id
701
702                return strcmp($a["id"], $b["id"]);
703
704            }
705
706        }
707    }
708
709}