1<?php
2/**
3 * Renderer for adding Purple Numbers
4 *
5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author Anika Henke <anika@selfthinker.org>
7 * @author Andreas Gohr <andi@splitbrain.org>
8 */
9// must be run within Dokuwiki
10if(!defined('DOKU_INC')) die();
11
12// we inherit from the XHTML renderer instead directly of the base renderer
13require_once DOKU_INC.'inc/parser/xhtml.php';
14
15class renderer_plugin_purplenumbers extends Doku_Renderer_xhtml {
16
17    var $PNitemCount = 0;
18
19    function getFormat(){
20        return 'xhtml';
21    }
22
23    function canRender($format) {
24        return ($format=='xhtml');
25    }
26
27
28    function document_end() {
29        parent::document_end();
30
31        // make sure there are no empty paragraphs
32        $this->doc = preg_replace('#<p[^>]*>\s*<!--PN-->.*?(?:</p>)#','',$this->doc);
33        // remove PN comment again (see _getLink())
34        $this->doc = preg_replace('/<!--PN-->/','',$this->doc);
35    }
36
37    function header($text, $level, $pos, $returnonly = false) {
38        parent::header($text, $level, $pos);
39
40        if ($this->_displayPN()) {
41            $pnid = $this->_getID($this->getConf('numbering')?2:1);
42            $linkText = $this->getConf('linkText') ? $pnid : '§';
43
44            $link = '&nbsp;<a href="#'.$pnid.'" id="'.$pnid;
45            $link .= '" class="pn" title="'.$this->getLang('sectionlink').'">'.$linkText.'</a>';
46            $link .= $this->_getAnnotationLink();
47
48            $this->doc = preg_replace('/(<\/h[1-5]>)$/', $link.'\\1', $this->doc);
49        }
50    }
51
52    function p_open() {
53        $eventdata = array(
54            'doc' => &$this->doc,
55            'pid' => $this->_getID()
56        );
57        // note: this will also be triggered by empty paragraphs
58        trigger_event('PLUGIN_PURPLENUMBERS_P_OPENED', $eventdata);
59        $this->doc .= DOKU_LF.'<p'.$this->_getID(1,1).'>'.DOKU_LF;
60    }
61
62    function p_close() {
63        $this->doc .= $this->_getLink().'</p>'.DOKU_LF;
64        if (preg_match('/<p[^>]*>\s*<!--PN-->.*?(?:<\/p>)$/',$this->doc)) {
65            $this->PNitemCount--;
66        } else {
67            $eventdata = array(
68                'doc' => &$this->doc,
69                'pid' => $this->_getID()
70            );
71            trigger_event('PLUGIN_PURPLENUMBERS_P_CLOSED', $eventdata);
72        }
73    }
74
75    function listitem_open($level, $node = false) {
76        $this->doc .= '<li class="level'.$level.'"'.$this->_getID(1,1).'>';
77    }
78
79    function listcontent_close() {
80        $this->doc .= $this->_getLink().'</div>'.DOKU_LF;
81    }
82
83    function preformatted($text, $type='code') {
84        $this->doc .= '<pre class="'.$type.'"'.$this->_getID(1,1).'>'.
85                      trim($this->_xmlEntities($text),"\n\r").
86                      $this->_getLink().'</pre>'.DOKU_LF;
87    }
88
89    function table_open($maxcols = null, $numrows = null, $pos = null, $classes = null) {
90        $this->_counter['row_counter'] = 0;
91        $this->doc .= '<div class="table"><table class="inline"'.$this->_getID(1,1).'>'.DOKU_LF;
92    }
93
94    function table_close($pos = null){
95        $this->doc .= '</table></div>'.$this->_getLink(1).DOKU_LF;
96    }
97
98    function php($text, $wrapper='code') {
99        global $conf;
100
101        if($conf['phpok']) {
102            ob_start();
103            eval($text);
104            $this->doc .= ob_get_contents();
105            ob_end_clean();
106        } elseif($wrapper != 'code') {
107            $code  = '<'.$wrapper.$this->_getID(1,1).' class="code php">';
108            $code .= trim(p_xhtml_cached_geshi($text, 'php', false),"\n\r");
109            $code .= $this->_getLink();
110            $code .= '</'.$wrapper.'>';
111            $this->doc .= $code;
112        } else {
113            $this->doc .= p_xhtml_cached_geshi($text, 'php', $wrapper);
114        }
115    }
116
117    function html($text, $wrapper='code') {
118        global $conf;
119
120        if($conf['htmlok']){
121            $this->doc .= $text;
122        } elseif($wrapper != 'code') {
123            $code  = '<'.$wrapper.$this->_getID(1,1).' class="code html4strict">';
124            $code .= trim(p_xhtml_cached_geshi($text, 'html4strict', false),"\n\r");
125            $code .= $this->_getLink();
126            $code .= '</'.$wrapper.'>';
127            $this->doc .= $code;
128        } else {
129            $this->doc .= p_xhtml_cached_geshi($text, 'html4strict', $wrapper);
130        }
131    }
132
133    function _highlight($type, $text, $language=null, $filename=null, $options = null) {
134        global $conf;
135        global $ID;
136        global $lang;
137
138        if($filename){
139            list($ext) = mimetype($filename,false);
140            $class = preg_replace('/[^_\-a-z0-9]+/i','_',$ext);
141            $class = 'mediafile mf_'.$class;
142
143            $this->doc .= '<dl class="'.$type.'">'.DOKU_LF;
144            $this->doc .= '<dt><a href="'.exportlink($ID,'code',array('codeblock'=>$this->_codeblock)).'" title="'.$lang['download'].'" class="'.$class.'">';
145            $this->doc .= hsc($filename);
146            $this->doc .= '</a></dt>'.DOKU_LF.'<dd>';
147        }
148
149        if ($text[0] == "\n") {
150            $text = substr($text, 1);
151        }
152        if (substr($text, -1) == "\n") {
153            $text = substr($text, 0, -1);
154        }
155
156        if ( is_null($language) ) {
157            $this->preformatted($text, $type);
158        } else {
159            $class = 'code';
160            if($type != 'code') $class .= ' '.$type;
161
162            $this->doc .= "<pre class=\"$class $language\" ".$this->_getID(1,1).">".
163                          p_xhtml_cached_geshi($text, $language, '').
164                          $this->_getLink().'</pre>'.DOKU_LF;
165        }
166
167        if($filename){
168            $this->doc .= '</dd></dl>'.DOKU_LF;
169        }
170
171        $this->_codeblock++;
172    }
173
174
175    /**
176     * Builds Purple Number ID.
177     *
178     * $setCount: increases (1) or resets (2) $PNitemCount
179     * $wrap: wrap output in 'id=""'
180     * $noprefix: lets you get the current ID without its prefix
181     * $internalID: clean ID, if it needs to be used as an internal ID
182     */
183    function _getID($setCount=0, $wrap=0, $noprefix=0, $internalID=0) {
184        if ($this->_displayPN()) {
185
186            if (!$internalID) {
187                $internalID = $this->getConf('internalID');
188            }
189
190            if ($setCount == 1) {
191                //increase for each new paragraph, etc
192                $this->PNitemCount++;
193            } else if ($setCount == 2) {
194                //reset for each new section (headline)
195                $this->PNitemCount = 0;
196            }
197
198            // build prefix
199            if ($noprefix) {
200                $prefix = '';
201            } else if ($this->getConf('uniqueness')) {
202                //site-wide
203                global $ID;
204                $prefix = $ID.'.';
205            } else {
206                //page-wide
207                $prefix = 'ID';
208            }
209
210            if ($this->getConf('numbering')) {
211                //hierarchical
212                $nodeID = preg_replace('/(\.0)*$/','',join('.',$this->node));
213                $itemNo = str_replace(':0','',':'.$this->PNitemCount);
214                $out = $prefix.$nodeID.$itemNo;
215            } else {
216                //consecutive
217                $out = $prefix.$this->PNitemCount;
218            }
219
220            // if the ID should be re-usable as an anchor in an internal link
221            if ($internalID) {
222                // sectionID() will strip out ':' and '.'
223                $out = str_replace(array(':','.'), array('-','_'), $out);
224                $out = cleanID($out);
225            }
226
227            if ($wrap) return ' id="'.$out.'"';
228            return $out;
229        }
230        return '';
231    }
232
233    /**
234     * Creates a link to the current Purple Number ID.
235     *
236     * $outside: puts a p.pnlink around the link, useful if
237     *     the link cannot be inside its corresponding element (e.g. tables)
238     */
239    function _getLink($outside=0) {
240        if ($this->_displayPN()) {
241            $linkText = $this->getConf('linkText') ? $this->_getID() : '¶';
242            $sep = $outside ? '' : '&nbsp;';
243
244            $pnlink = $sep.'<a href="#'.$this->_getID().'" class="pn" title="'.$this->getLang('sectionlink').'">'.$linkText.'</a>';
245            $pnlink .= $this->_getAnnotationLink();
246
247            if ($outside) {
248                return '<p class="pnlink">'.$pnlink.'</p>';
249            }
250            return '<!--PN-->'.$pnlink;
251            // that <!--PN--> comment is added to be able to delete empty paragraphs in document_end()
252        }
253        return '';
254    }
255
256    /**
257     * Creates a link to an annotation page per Purple Number.
258     */
259    function _getAnnotationLink() {
260        $annotationPage = $this->getConf('annotationPage');
261        if ($annotationPage) {
262            global $ID;
263            // resolve placeholders
264            $aID = str_replace(
265                       array(
266                          '@PN@',
267                          '@PNID@',
268                          '@ID@',
269                          '@PAGE@'
270                       ),
271                       array(
272                           $this->_getID(0,0,1,1),
273                           $this->_getID(0,0,0,1),
274                           $ID,
275                           noNSorNS($ID)
276                       ),
277                       $annotationPage
278                   );
279            // in case linkText is only a pilcrow, only show the icon
280            $onlyIcon = $this->getConf('linkText') ? '' : 'onlyIcon';
281            $sep = $onlyIcon ? '&nbsp;' : ' ';
282
283            return $sep.'<span class="pn '.$onlyIcon.'">'.
284                   html_wikilink($aID,$this->getLang('comment')).
285                   '</span>';
286        }
287        return '';
288    }
289
290    /**
291     * Checks if the adding of the Purple Number should be restricted
292     *   (by configuration settings 'restrictionNS' and 'restrictionType').
293     */
294    function _displayPN() {
295        global $ID, $INFO, $ACT, $conf;
296
297        if (!page_exists($ID)) return false;
298        // only show PNs in the main content, not in included pages (like sidebars)
299        if ($ID != $INFO['id']) return false;
300        if ($ACT != 'show') return false;
301        if (!$this->getConf('includeStartpage') && noNS($ID)==$conf['start']) return false;
302
303        if ($this->getConf('restrictionNS')) {
304            $curRootNS = substr($ID, 0, strpos($ID,':'));
305            $restrictionNS = explode(',', $this->getConf('restrictionNS'));
306            $restrictionType = $this->getConf('restrictionType');
307
308            foreach ($restrictionNS as $r) {
309                if (trim($r) == $curRootNS) {
310                    if ($restrictionType)
311                        return true;
312                    return false;
313                }
314            }
315            if ($restrictionType)
316                return false;
317            return true;
318        }
319        return true;
320    }
321
322}
323
324