xref: /plugin/templater/syntax.php (revision dbaa014835af3c169bde45a7bc53735fe7b358af)
11b9d62daSnerun<?php
21b9d62daSnerun/**
31b9d62daSnerun * Templater Plugin: Based from the include plugin, like MediaWiki's template
41b9d62daSnerun * Usage:
51b9d62daSnerun *    {{template>page}} for "page" in same namespace
61b9d62daSnerun *    {{template>:page}} for "page" in top namespace
71b9d62daSnerun *    {{template>namespace:page}} for "page" in namespace "namespace"
81b9d62daSnerun *    {{template>.namespace:page}} for "page" in subnamespace "namespace"
91b9d62daSnerun *    {{template>page#section}} for a section of "page"
101b9d62daSnerun *
111b9d62daSnerun * Replacers are handled in a simple key/value pair method:
121b9d62daSnerun *    {{template>page|key=val|key2=val|key3=val}}
131b9d62daSnerun *
141b9d62daSnerun * Templates are wiki pages, with replacers being delimited like:
151b9d62daSnerun *    @key1@ @key2@ @key3@
161b9d62daSnerun *
171b9d62daSnerun * @license        GPL 2 (http://www.gnu.org/licenses/gpl.html)
181b9d62daSnerun * @author         Jonathan Arkell <jonnay@jonnay.net>
191b9d62daSnerun *                    based on code by Esther Brunner <esther@kaffeehaus.ch>
201b9d62daSnerun * @maintainer     Daniel Dias Rodrigues (aka Nerun) <danieldiasr@gmail.com>
211b9d62daSnerun * @contributors   Vincent de Lau <vincent@delau.nl>
221b9d62daSnerun *                 Ximin Luo <xl269@cam.ac.uk>
231b9d62daSnerun *                 jack126guy <halfgray7e@gmail.com>
241b9d62daSnerun *                 Turq Whiteside <turq@mage.city>
251b9d62daSnerun */
261b9d62daSnerun
2769c933b0Snerunuse dokuwiki\File\PageResolver;
2869c933b0Snerun
291b9d62daSnerundefine('BEGIN_REPLACE_DELIMITER', '@');
301b9d62daSnerundefine('END_REPLACE_DELIMITER', '@');
311b9d62daSnerun
321b9d62daSnerunrequire_once('debug.php');
331b9d62daSnerun
341b9d62daSnerun/**
351b9d62daSnerun * All DokuWiki plugins to extend the parser/rendering mechanism
361b9d62daSnerun * need to inherit from this class
371b9d62daSnerun */
381b9d62daSnerunclass syntax_plugin_templater extends DokuWiki_Syntax_Plugin {
391b9d62daSnerun    /**
401b9d62daSnerun     * What kind of syntax are we?
411b9d62daSnerun     */
421b9d62daSnerun    function getType() {
431b9d62daSnerun        return 'container';
441b9d62daSnerun    }
451b9d62daSnerun
461b9d62daSnerun    function getAllowedTypes() {
471b9d62daSnerun        return array('container', 'substition', 'protected', 'disabled', 'formatting');
481b9d62daSnerun    }
491b9d62daSnerun
501b9d62daSnerun    /**
511b9d62daSnerun     * Where to sort in?
521b9d62daSnerun     */
531b9d62daSnerun    function getSort() {
541b9d62daSnerun        return 302;
551b9d62daSnerun    }
561b9d62daSnerun
571b9d62daSnerun    /**
581b9d62daSnerun     * Paragraph Type
591b9d62daSnerun     */
601b9d62daSnerun    function getPType() {
611b9d62daSnerun        return 'block';
621b9d62daSnerun    }
631b9d62daSnerun
641b9d62daSnerun    /**
651b9d62daSnerun     * Connect pattern to lexer
661b9d62daSnerun     */
671b9d62daSnerun    function connectTo($mode) {
681b9d62daSnerun        $this->Lexer->addSpecialPattern("{{template>.+?}}", $mode, 'plugin_templater');
691b9d62daSnerun    }
701b9d62daSnerun
711b9d62daSnerun    /**
721b9d62daSnerun     * Handle the match
731b9d62daSnerun     */
741b9d62daSnerun    function handle($match, $state, $pos, Doku_Handler $handler) {
751b9d62daSnerun        global $ID;
761b9d62daSnerun
771b9d62daSnerun        $match = substr($match, 11, -2);                        // strip markup
781b9d62daSnerun        $replacers = preg_split('/(?<!\\\\)\|/', $match);        // Get the replacers
791b9d62daSnerun        $wikipage = array_shift($replacers);
801b9d62daSnerun
811b9d62daSnerun        $replacers = $this->_massageReplacers($replacers);
821b9d62daSnerun
831b9d62daSnerun        $wikipage = preg_split('/\#/u', $wikipage, 2);                        // split hash from filename
841b9d62daSnerun        $parentpage = empty(self::$pagestack)? $ID : end(self::$pagestack); // get correct namespace
8569c933b0Snerun        // resolve shortcuts:
8669c933b0Snerun        $resolver = new PageResolver(getNS($parentpage));
8769c933b0Snerun        $wikipage[0] = $resolver->resolveId($wikipage[0]);
8869c933b0Snerun        $exists = page_exists($wikipage[0]);
891b9d62daSnerun
901b9d62daSnerun        // check for perrmission
911b9d62daSnerun        if (auth_quickaclcheck($wikipage[0]) < 1)
921b9d62daSnerun            return false;
931b9d62daSnerun
941b9d62daSnerun        // $wikipage[1] is the header of a template enclosed within a section {{template>page#section}}
951b9d62daSnerun        // Not all template calls will be {{template>page#section}}, some will be {{template>page}}
961b9d62daSnerun        // It fix "Undefined array key 1" warning
971b9d62daSnerun        if (array_key_exists(1, $wikipage)) {
981b9d62daSnerun            $section = cleanID($wikipage[1]);
991b9d62daSnerun        } else {
1001b9d62daSnerun            $section = null;
1011b9d62daSnerun        }
1021b9d62daSnerun
1031b9d62daSnerun        return array($wikipage[0], $replacers, $section);
1041b9d62daSnerun    }
1051b9d62daSnerun
1061b9d62daSnerun    private static $pagestack = array(); // keep track of recursing template renderings
1071b9d62daSnerun
1081b9d62daSnerun    /**
1091b9d62daSnerun     * Create output
1101b9d62daSnerun     * This is a refactoring candidate. Needs to be a little clearer.
1111b9d62daSnerun     */
1121b9d62daSnerun    function render($mode, Doku_Renderer $renderer, $data) {
1131b9d62daSnerun        if ($mode != 'xhtml')
1141b9d62daSnerun            return false;
1151b9d62daSnerun
1161b9d62daSnerun        if ($data[0] === false) {
1171b9d62daSnerun            // False means no permissions
118bfb1a0d0Snerun            $renderer->doc .= '<div class="templater"> ';
1191b9d62daSnerun            $renderer->doc .= $this->getLang('no_permissions_view');
120bfb1a0d0Snerun            $renderer->doc .= ' </div>';
1211b9d62daSnerun            $renderer->info['cache'] = FALSE;
1221b9d62daSnerun            return true;
1231b9d62daSnerun        }
1241b9d62daSnerun
1251b9d62daSnerun        $file = wikiFN($data[0]);
1261b9d62daSnerun        if (!@file_exists($file)) {
127bfb1a0d0Snerun            $renderer->doc .= '<div class="templater">— ';
1281b9d62daSnerun            $renderer->doc .= $this->getLang('template');
129bfb1a0d0Snerun            $renderer->doc .= ' ';
1301b9d62daSnerun            $renderer->internalLink($data[0]);
131bfb1a0d0Snerun            $renderer->doc .= ' ';
1321b9d62daSnerun            $renderer->doc .= $this->getLang('not_found');
1331b9d62daSnerun            $renderer->doc .= '<br/><br/></div>';
1341b9d62daSnerun            $renderer->info['cache'] = FALSE;
1351b9d62daSnerun            return true;
1361b9d62daSnerun        } else if (array_search($data[0], self::$pagestack) !== false) {
137bfb1a0d0Snerun            $renderer->doc .= '<div class="templater">— ';
1381b9d62daSnerun            $renderer->doc .= $this->getLang('processing_template');
139bfb1a0d0Snerun            $renderer->doc .= ' ';
1401b9d62daSnerun            $renderer->internalLink($data[0]);
141bfb1a0d0Snerun            $renderer->doc .= ' ';
1421b9d62daSnerun            $renderer->doc .= $this->getLang('stopped_recursion');
1431b9d62daSnerun            $renderer->doc .= '<br/><br/></div>';
1441b9d62daSnerun            return true;
1451b9d62daSnerun        }
1461b9d62daSnerun        self::$pagestack[] = $data[0]; // push this onto the stack
1471b9d62daSnerun
1481b9d62daSnerun        // Get the raw file, and parse it into its instructions. This could be cached... maybe.
1491b9d62daSnerun        $rawFile = io_readfile($file);
15069c933b0Snerun
15169c933b0Snerun        // fill in all known values
1521b9d62daSnerun        if(!empty($data[1]['keys']) && !empty($data[1]['vals'])) {
153*bb587b91Snerun            $rawFile = str_replace($data[1]['keys'], $data[1]['vals'], $rawFile);
1541b9d62daSnerun        }
1551b9d62daSnerun
1561b9d62daSnerun        // replace unmatched substitutions with "" or use DEFAULT_STR from data arguments if exists.
1571b9d62daSnerun        $left_overs = '/'.BEGIN_REPLACE_DELIMITER.'.*'.END_REPLACE_DELIMITER.'/';
1581b9d62daSnerun
1591b9d62daSnerun        if(!empty($data[1]['keys']) && !empty($data[1]['vals'])) {
1601b9d62daSnerun            $def_key = array_search(BEGIN_REPLACE_DELIMITER."DEFAULT_STR".END_REPLACE_DELIMITER, $data[1]['keys']);
1611b9d62daSnerun            $DEFAULT_STR = $def_key ? $data[1]['vals'][$def_key] : "";
162*bb587b91Snerun            $rawFile = preg_replace($left_overs, $DEFAULT_STR, $rawFile);
1631b9d62daSnerun        }
1641b9d62daSnerun
165*bb587b91Snerun        $instr = p_get_instructions($rawFile);
1661b9d62daSnerun
1671b9d62daSnerun        // filter section if given
1681b9d62daSnerun        if ($data[2]) {
1691b9d62daSnerun            $getSection = $this->_getSection($data[2], $instr);
1701b9d62daSnerun
1711b9d62daSnerun            $instr = $getSection[0];
1721b9d62daSnerun
1731b9d62daSnerun            if(!is_null($getSection[1])) {
1741b9d62daSnerun                $renderer->doc .= sprintf($getSection[1], $data[2]);
1751b9d62daSnerun                $renderer->internalLink($data[0]);
1761b9d62daSnerun                $renderer->doc .= '.<br/><br/></div>';
1771b9d62daSnerun            }
1781b9d62daSnerun        }
1791b9d62daSnerun
1801b9d62daSnerun        // correct relative internal links and media
1811b9d62daSnerun        $instr = $this->_correctRelNS($instr, $data[0]);
1821b9d62daSnerun
1831b9d62daSnerun        // doesn't show the heading for each template if {{template>page#section}}
1841b9d62daSnerun        if (sizeof($instr) > 0 && !isset($getSection[1])) {
1851b9d62daSnerun            if (array_key_exists(0, $instr[0][1]) && $instr[0][1][0] == $data[2]) {
1861b9d62daSnerun                $instr[0][1][0] = null;
1871b9d62daSnerun            }
1881b9d62daSnerun        }
1891b9d62daSnerun
1901b9d62daSnerun        // render the instructructions on the fly
1911b9d62daSnerun        $text = p_render('xhtml', $instr, $info);
1921b9d62daSnerun
1931b9d62daSnerun        // remove toc, section edit buttons and category tags
1941b9d62daSnerun        $patterns = array('!<div class="toc">.*?(</div>\n</div>)!s',
1951b9d62daSnerun                          '#<!-- SECTION \[(\d*-\d*)\] -->#',
1961b9d62daSnerun                          '!<div class="category">.*?</div>!s');
1971b9d62daSnerun        $replace  = array('', '', '');
1981b9d62daSnerun        $text = preg_replace($patterns, $replace, $text);
1991b9d62daSnerun
2001b9d62daSnerun        // prevent caching to ensure the included page is always fresh
2011b9d62daSnerun        $renderer->info['cache'] = FALSE;
2021b9d62daSnerun
2031b9d62daSnerun        // embed the included page
2041b9d62daSnerun        $renderer->doc .= '<div class="templater">';
2051b9d62daSnerun        $renderer->doc .= $text;
2061b9d62daSnerun        $renderer->doc .= '</div>';
2071b9d62daSnerun
2081b9d62daSnerun        array_pop(self::$pagestack); // pop off the stack when done
2091b9d62daSnerun        return true;
2101b9d62daSnerun    }
2111b9d62daSnerun
2121b9d62daSnerun    /**
2131b9d62daSnerun     * Get a section including its subsections
2141b9d62daSnerun     */
2151b9d62daSnerun    function _getSection($title, $instructions) {
2161b9d62daSnerun        $i = (array) null;
2171b9d62daSnerun        $level = null;
2181b9d62daSnerun        $no_section = null;
2191b9d62daSnerun
2201b9d62daSnerun        foreach ($instructions as $instruction) {
2211b9d62daSnerun            if ($instruction[0] == 'header') {
2221b9d62daSnerun
2231b9d62daSnerun                // found the right header
2241b9d62daSnerun                if (cleanID($instruction[1][0]) == $title) {
2251b9d62daSnerun                    $level = $instruction[1][1];
2261b9d62daSnerun                    $i[] = $instruction;
2271b9d62daSnerun                } else {
2281b9d62daSnerun                    if (isset($level) && isset($i)) {
2291b9d62daSnerun                        if ($instruction[1][1] > $level) {
2301b9d62daSnerun                            $i[] = $instruction;
2311b9d62daSnerun                // next header of the same level or higher -> exit
2321b9d62daSnerun                        } else {
2331b9d62daSnerun                            return array($i,null);
2341b9d62daSnerun                        }
2351b9d62daSnerun                    }
2361b9d62daSnerun                }
2371b9d62daSnerun            } else { // content between headers
2381b9d62daSnerun                if (isset($level) && isset($i)) {
2391b9d62daSnerun                    $i[] = $instruction;
2401b9d62daSnerun                }
2411b9d62daSnerun            }
2421b9d62daSnerun        }
2431b9d62daSnerun
2441b9d62daSnerun        // Fix for when page#section doesn't exist
2451b9d62daSnerun        if(sizeof($i) == 0) {
246bfb1a0d0Snerun            $no_section_begin = '<div class="templater">— ';
247bfb1a0d0Snerun            $no_section_end = $this->getLang('no_such_section');
248bfb1a0d0Snerun            $no_section = $no_section_begin . $no_section_end . ' ';
2491b9d62daSnerun        }
2501b9d62daSnerun
2511b9d62daSnerun        return array($i,$no_section);
2521b9d62daSnerun    }
2531b9d62daSnerun
2541b9d62daSnerun    /**
2551b9d62daSnerun     * Corrects relative internal links and media
2561b9d62daSnerun     */
2571b9d62daSnerun    function _correctRelNS($instr, $incl) {
2581b9d62daSnerun        global $ID;
2591b9d62daSnerun
2601b9d62daSnerun        // check if included page is in same namespace
2611b9d62daSnerun        $iNS = getNS($incl);
2621b9d62daSnerun        if (getNS($ID) == $iNS)
2631b9d62daSnerun            return $instr;
2641b9d62daSnerun
2651b9d62daSnerun        // convert internal links and media from relative to absolute
2661b9d62daSnerun        $n = count($instr);
2671b9d62daSnerun        for($i = 0; $i < $n; $i++) {
2681b9d62daSnerun            if (substr($instr[$i][0], 0, 8) != 'internal')
2691b9d62daSnerun                continue;
2701b9d62daSnerun
2711b9d62daSnerun            // relative subnamespace
2721b9d62daSnerun            if ($instr[$i][1][0][0] == '.') {
2731b9d62daSnerun                $instr[$i][1][0] = $iNS.':'.substr($instr[$i][1][0], 1);
2741b9d62daSnerun
2751b9d62daSnerun            // relative link
2761b9d62daSnerun            } else if (strpos($instr[$i][1][0], ':') === false) {
2771b9d62daSnerun                $instr[$i][1][0] = $iNS.':'.$instr[$i][1][0];
2781b9d62daSnerun            }
2791b9d62daSnerun        }
2801b9d62daSnerun
2811b9d62daSnerun        return $instr;
2821b9d62daSnerun    }
2831b9d62daSnerun
2841b9d62daSnerun    /**
2851b9d62daSnerun     * Handles the replacement array
2861b9d62daSnerun     */
2871b9d62daSnerun    function _massageReplacers($replacers) {
2881b9d62daSnerun        $r = array();
2891b9d62daSnerun        if (is_null($replacers)) {
2901b9d62daSnerun            $r['keys'] = null;
2911b9d62daSnerun            $r['vals'] = null;
2921b9d62daSnerun        } else if (is_string($replacers)) {
293a93c15b6Snerun            if ( str_contains($replacers, '=') && (substr(trim($replacers), -1) != '=') ){
2941b9d62daSnerun                list($k, $v) = explode('=', $replacers, 2);
2951b9d62daSnerun                $r['keys'] = BEGIN_REPLACE_DELIMITER.trim($k).END_REPLACE_DELIMITER;
2961b9d62daSnerun                $r['vals'] = trim(str_replace('\|', '|', $v));
29769c933b0Snerun            }
2981b9d62daSnerun        } else if ( is_array($replacers) ) {
2991b9d62daSnerun            foreach($replacers as $rep) {
300a93c15b6Snerun                if ( str_contains($rep, '=') && (substr(trim($rep), -1) != '=') ){
3011b9d62daSnerun                    list($k, $v) = explode('=', $rep, 2);
3021b9d62daSnerun                    $r['keys'][] = BEGIN_REPLACE_DELIMITER.trim($k).END_REPLACE_DELIMITER;
3031b9d62daSnerun                    if (trim($v)[0] == '"' and trim($v)[-1] == '"') {
3041b9d62daSnerun                        $r['vals'][] = substr(trim(str_replace('\|','|',$v)), 1, -1);
3051b9d62daSnerun                    } else {
3061b9d62daSnerun                        $r['vals'][] = trim(str_replace('\|','|',$v));
3071b9d62daSnerun                    }
3081b9d62daSnerun                }
30969c933b0Snerun            }
3101b9d62daSnerun        } else {
3111b9d62daSnerun            // This is an assertion failure. We should NEVER get here.
31269c933b0Snerun            //die("FATAL ERROR!  Unknown type passed to syntax_plugin_templater::massageReplaceMentArray() can't message syntax_plugin_templater::\$replacers!  Type is:".gettype($r)." Value is:".$r);
3131b9d62daSnerun            $r['keys'] = null;
3141b9d62daSnerun            $r['vals'] = null;
3151b9d62daSnerun        }
3161b9d62daSnerun        return $r;
3171b9d62daSnerun    }
3181b9d62daSnerun}
319