1<?php
2/**
3 * Renderer for XHTML output
4 *
5 * @author Emmanuel Klinger <emmanuel.klinger@gmail.com>
6 * @author Ottmar Gobrecht <ottmar.gobrecht@gmail.com>
7 */
8// must be run within Dokuwiki
9if(!defined('DOKU_INC')) die();
10
11// we inherit from the XHTML renderer instead directly of the base renderer
12require_once DOKU_INC.'inc/parser/xhtml.php';
13
14/**
15 * The Renderer
16 */
17class renderer_plugin_revealjs extends Doku_Renderer_xhtml {
18    var $base = '';
19    var $tpl = '';
20    var $slide_indicator_headers = true;
21    var $slide_number = 0;
22    var $slide_open = false;
23    var $column_open = false;
24    var $notes_open = false;
25    var $quote_open = false;
26    var $fragment_list_open = false;
27    var $no_fragment_list_open = false;
28    var $fragment_style = '';
29    var $next_slide_background_color = '';
30    var $next_slide_background_image = '';
31    var $next_slide_background_size = '';
32    var $next_slide_background_position = '';
33    var $next_slide_background_repeat = '';
34    var $next_slide_background_transition = '';
35    var $next_slide_transition = '';
36    var $next_slide_transition_speed  = '';
37    var $next_slide_no_footer = false;
38
39    /**
40     * the format we produce
41     */
42    function getFormat(){
43        // this should be 'revealjs' usally, but we inherit from the xhtml renderer
44        // and produce XHTML as well, so we can gain magically compatibility
45        // by saying we're the 'xhtml' renderer here.
46        return 'xhtml';
47    }
48
49
50    /**
51     * Initialize the rendering
52     */
53    function document_start() {
54        global $ID;
55        global $conf;
56        global $lang;
57
58        // merge URL params into plugin conf - changing params direct in the URL is only working, when page is not cached (~~NOCACHE~~)
59        if (count($_GET)){
60            if (!array_key_exists('plugin', $conf)) {
61                $conf['plugin'] = array('revealjs' => $_GET);
62            }
63            elseif (!array_key_exists('revealjs', $conf['plugin'])) {
64                $conf['plugin']['revealjs'] = $_GET;
65            }
66            else {
67                $conf['plugin']['revealjs'] = array_merge($conf['plugin']['revealjs'], $_GET);
68            }
69        }
70
71        // call the parent
72        parent::document_start();
73
74        // store the content type headers in metadata
75        $headers = array(
76            'Content-Type' => 'text/html; charset=utf-8'
77        );
78
79        p_set_metadata($ID,array('format' => array('revealjs' => $headers) ));
80        $this->base = DOKU_BASE.'lib/plugins/revealjs/';
81        $this->doc = '<!DOCTYPE html>
82<html lang="'.$conf['lang'].'" dir="'.$lang['direction'].'">
83<head>
84    <meta charset="utf-8">
85    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
86
87    <title>'.tpl_pagetitle($ID, true).'</title>
88
89    <meta name="apple-mobile-web-app-capable" content="yes" />
90    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
91    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, minimal-ui">
92
93    <link rel="stylesheet" href="'.$this->base.'css/reveal.css">
94    <link rel="stylesheet" href="'.$this->base.'css/theme/'.$this->getConf('theme').'.css" id="theme">
95    <link rel="stylesheet" href="'.$this->base.'doku-substitutes.css">
96
97    <!-- Code syntax highlighting -->
98    <link rel="stylesheet" href="'.$this->base.'lib/css/zenburn.css">
99
100    ' . ($this->getConf('show_image_borders') ?
101    '<!-- Image borders are switched on -->' :
102    '<!-- Image borders are switched off -->
103    <style>.reveal img { border: none !important; box-shadow: none !important; background: none !important; } .level1, .level2, .level3, .level4, .level5 {min-height:300px;}</style>') . '
104
105    <!-- Printing and PDF exports -->
106    <script>
107        var link = document.createElement( \'link\' );
108        link.rel = \'stylesheet\';
109        link.type = \'text/css\';
110        link.href = window.location.search.match( /print-pdf/gi ) ? \''.$this->base.'css/print/pdf.css\' : \''.$this->base.'css/print/paper.css\';
111        document.getElementsByTagName( \'head\' )[0].appendChild( link );
112    </script>
113
114    <!--[if lt IE 9]>
115    <script src='.$this->base.'"lib/js/html5shiv.js"></script>
116    <![endif]-->
117</head>
118<body>
119    <div class="reveal">
120
121        <!-- Any section element inside of this container is displayed as a slide -->
122        <div class="slides">
123
124<!-- page content start ------------------------------------------------------->';
125    }
126
127
128    /**
129     * Closes the document
130     */
131    function document_end(){
132        // we don't care for footnotes and toc
133        // but cleanup is nice
134        $this->doc = preg_replace('#<p>\s*</p>#','',$this->doc);
135
136        // cleanup quotes - too much whitspace from declaration: ">    valid quote"
137        $this->doc = preg_replace('/<blockquote>&ldquo;\s*/u','<blockquote>&ldquo;',$this->doc);
138
139        // close maybe open slide and column
140        $this->close_slide_container();
141
142        $show_controls = $this->getConf('controls') ? 'true' : 'false';
143        $show_progress_bar = $this->getConf('show_progress_bar') ? 'true' : 'false';
144        $size = explode("x", $this->getConf('size'));
145        $auto_slide = $this->getConf('auto_slide');
146        $loop = $this->getConf('loop') ? 'true' : 'false';
147        $this->doc .= '
148<!-- page content stop -------------------------------------------------------->
149
150        </div><!-- slides -->
151    </div><!-- reveal -->
152
153    <script src="'.$this->base.'lib/js/head.min.js"></script>
154    <script src="'.$this->base.'js/reveal.js"></script>
155    <script>
156        Reveal.initialize({
157            width: '. ($size[0] ? $size[0] : 960) .',
158            height: '. ($size[1] ? $size[1] : 700) .',
159            controls: '. $show_controls .',
160            progress: '. $show_progress_bar .',
161            history: true,
162            center: true,
163            autoSlide: '. $auto_slide .',
164            loop: '. $loop .',
165            transition: \''.$this->getConf('transition').'\', // none/fade/slide/convex/concave/zoom
166            math: {
167                mathjax: \'//cdn.mathjax.org/mathjax/latest/MathJax.js\',
168                config: \'TeX-AMS_HTML-full\'  // See http://docs.mathjax.org/en/latest/config-files.html
169            },
170            dependencies: [
171                { src: \''.$this->base.'lib/js/classList.js\', condition: function() { return !document.body.classList; } },
172                { src: \''.$this->base.'plugin/markdown/marked.js\', condition: function() { return !!document.querySelector( \'[data-markdown]\' ); } },
173                { src: \''.$this->base.'plugin/markdown/markdown.js\', condition: function() { return !!document.querySelector( \'[data-markdown]\' ); } },
174                { src: \''.$this->base.'plugin/highlight/highlight.js\', async: true, condition: function() { return !!document.querySelector( \'pre code\' ); }, callback: function() { hljs.initHighlightingOnLoad(); } },
175                { src: \''.$this->base.'plugin/zoom-js/zoom.js\', async: true, condition: function() { return !!document.body.classList; } },
176                { src: \''.$this->base.'plugin/notes/notes.js\', async: true, condition: function() { return !!document.body.classList; } },
177                // MathJax
178                { src: \''.$this->base.'plugin/math/math.js\', async: true }
179            ]
180        });
181    </script>
182</body>
183</html>';
184    }
185
186    /**
187     * Creates a slide container (section), possibly containing nested slides (sections)
188     */
189    function open_slide_container () {
190        $this->close_slide_container();
191        $this->doc .= '<section>'.DOKU_LF;
192        $this->column_open = true;
193    }
194
195    /**
196     * Creates a slide (section)
197     */
198    function open_slide () {
199        $this->close_slide();
200        $this->doc .= '  <section';
201        if ($this->next_slide_background_color) {
202            $this->doc .= ' data-background-color="'.$this->next_slide_background_color.'"';
203            $this->next_slide_background_color = '';
204        }
205        if ($this->next_slide_background_image) {
206            $this->doc .= ' data-background-image="'.$this->next_slide_background_image.'"';
207            $this->next_slide_background_image = '';
208        }
209        if ($this->next_slide_background_size) {
210            $this->doc .= ' data-background-size="'.$this->next_slide_background_size.'"';
211            $this->next_slide_background_size = '';
212        }
213        if ($this->next_slide_background_position) {
214            $this->doc .= ' data-background-position="'.$this->next_slide_background_position.'"';
215            $this->next_slide_background_position = '';
216        }
217         $data['background_position'];
218        if ($this->next_slide_background_repeat) {
219            $this->doc .= ' data-background-repeat="'.$this->next_slide_background_repeat.'"';
220            $this->next_slide_background_repeat = '';
221        }
222        if ($this->next_slide_background_transition) {
223            $this->doc .= ' data-background-transition="'.$this->next_slide_background_transition.'"';
224            $this->next_slide_background_transition = '';
225        }
226        if ($this->next_slide_transition) {
227            $this->doc .= ' data-transition="'.$this->next_slide_transition.'"';
228            $this->next_slide_transition = '';
229        }
230        if ($this->next_slide_transition_speed) {
231            $this->doc .= ' data-transition-speed="'.$this->next_slide_transition_speed.'"';
232            $this->next_slide_transition_speed  = '';
233        }
234        if ($this->next_slide_no_footer) {
235            $this->doc .= ' data-state="no-footer"';
236            $this->next_slide_no_footer = false;
237        }
238        $this->doc .= '>'.DOKU_LF;
239
240        // mark slide as open
241        $this->slide_open = true;
242    }
243
244    /**
245     * Closes a slide container (section)
246     */
247    function close_slide_container () {
248        $this->close_slide();
249        if ($this->column_open) {
250            $this->doc .= '</section>'.DOKU_LF;
251            $this->column_open = false;
252        }
253    }
254
255    /**
256     * Closes a slide (section)
257     */
258    function close_slide () {
259        if ($this->slide_open) {
260            $this->doc .= '  </section>'.DOKU_LF;
261            $this->slide_open = false;
262        }
263    }
264
265    /**
266     * DokuWiki sections are not used on a slideshow - so we redeclare it here only
267     */
268    function section_open($level) {}
269    function section_close() {}
270
271    /**
272     * Throw away footnote
273     */
274    function footnote_close() {
275        // recover footnote into the stack and restore old content
276        $footnote = $this->doc;
277        $this->doc = $this->store;
278        $this->store = '';
279    }
280
281    /**
282     * No acronyms in a presentation
283     */
284    function acronym($acronym){
285        $this->doc .= $this->_xmlEntities($acronym);
286    }
287
288    /**
289     * Start a table
290     *
291     * @param int $maxcols maximum number of columns
292     * @param int $numrows NOT IMPLEMENTED
293     * @param int $pos     byte position in the original source
294     */
295    function table_open($maxcols = null, $numrows = null, $pos = null, $classes = NULL) {
296        // initialize the row counter used for classes
297        $this->_counter['row_counter'] = 0;
298        $class                         = 'table';
299        if($pos !== null) {
300
301	    $sectionEditStartData = ['target' => 'table'];
302	    if (!defined('SEC_EDIT_PATTERN')) {
303		// backwards-compatibility for Frusterick Manners (2017-02-19)
304		$sectionEditStartData = 'table';
305	    }
306
307            $class .= ' '.$this->startSectionEdit($pos, $sectionEditStartData);
308        }
309        $this->doc .= '<table class="doku_revealjs_table">'.
310            DOKU_LF;
311    }
312
313    /**
314     * Close a table
315     *
316     * @param int $pos byte position in the original source
317     */
318    function table_close($pos = null) {
319        $this->doc .= '</table>'.DOKU_LF;
320    }
321
322    /**
323     * Open a table header
324     */
325    function tablethead_open() {
326        $this->doc .= DOKU_TAB.'<thead>'.DOKU_LF;
327    }
328
329    /**
330     * Close a table header
331     */
332    function tablethead_close() {
333        $this->doc .= DOKU_TAB.'</thead>'.DOKU_LF;
334    }
335
336    /**
337     * Open a table row
338     */
339    function tablerow_open($classes = NULL) {
340        $this->doc .= DOKU_TAB.'<tr>'.DOKU_LF.DOKU_TAB.DOKU_TAB;
341    }
342
343    /**
344     * Close a table row
345     */
346    function tablerow_close() {
347        $this->doc .= DOKU_LF.DOKU_TAB.'</tr>'.DOKU_LF;
348    }
349
350    /**
351     * Open a table header cell
352     *
353     * @param int    $colspan
354     * @param string $align left|center|right
355     * @param int    $rowspan
356     */
357    function tableheader_open($colspan = 1, $align = null, $rowspan = 1, $classes = NULL) {
358        $class = 'class="col'.$this->_counter['cell_counter']++;
359        if(!is_null($align)) {
360            $class .= ' '.$align.'align';
361        }
362        $class .= '"';
363        $this->doc .= '<th ';
364        if($colspan > 1) {
365            $this->_counter['cell_counter'] += $colspan - 1;
366            $this->doc .= ' colspan="'.$colspan.'"';
367        }
368        if($rowspan > 1) {
369            $this->doc .= ' rowspan="'.$rowspan.'"';
370        }
371        $this->doc .= '>';
372    }
373
374    /**
375     * Close a table header cell
376     */
377    function tableheader_close() {
378        $this->doc .= '</th>';
379    }
380
381    /**
382     * Open a table cell
383     *
384     * @param int    $colspan
385     * @param string $align left|center|right
386     * @param int    $rowspan
387     */
388    function tablecell_open($colspan = 1, $align = null, $rowspan = 1, $classes = NULL) {
389        $class = 'class="col'.$this->_counter['cell_counter']++;
390        if(!is_null($align)) {
391            $class .= ' '.$align.'align';
392        }
393        $class .= '"';
394        $this->doc .= '<td ';
395        if($colspan > 1) {
396            $this->_counter['cell_counter'] += $colspan - 1;
397            $this->doc .= ' colspan="'.$colspan.'"';
398        }
399        if($rowspan > 1) {
400            $this->doc .= ' rowspan="'.$rowspan.'"';
401        }
402        $this->doc .= '>';
403    }
404
405    /**
406     * Close a table cell
407     */
408    function tablecell_close() {
409        $this->doc .= '</td>';
410    }
411
412
413    /**
414    * Open a list item
415    *
416    * @param int $level the nesting level
417    *
418    * Default: build list item per item.
419    * This is called "fragment" in reveal.js
420    */
421    function listitem_open($level, $node=false) {
422        if( !$this->notes_open
423            && !$this->no_fragment_list_open
424            && ($this->getConf('build_all_lists') || $this->fragment_list_open) ) {
425        $this->doc .= '<li class="fragment' . ($this->fragment_style ? ' '.$this->fragment_style : '') . '">';
426        }
427        else {
428            $this->doc .= '<li>';
429        }
430    }
431
432
433    /**
434     * Start a block quote
435     */
436    function quote_open() {
437        if (!$this->quote_open) {
438            $this->doc .= '<blockquote>&ldquo;';
439            $this->quote_open = true;
440        }
441    }
442
443    /**
444     * Stop a block quote
445     */
446    function quote_close() {
447        if ($this->quote_open) {
448            $this->doc .= '&rdquo;</blockquote>'.DOKU_LF;
449            $this->quote_open = false;
450        }
451    }
452
453
454    /**
455     * Don't use Geshi. Overwrite the Geshi function.
456     * @author Emmanuel Klinger
457     * @author Andreas Gohr <andi@splitbrain.org>
458     * @param string $type     code|file
459     * @param string $text     text to show
460     * @param string $language programming language to use for syntax highlighting
461     * @param string $filename file path label
462     * @param string $options highlight options - not used
463     */
464    function _highlight($type, $text, $language = null, $filename = null, $options = null) {
465        global $ID;
466        global $lang;
467
468        if($filename) {
469            // add icon
470            list($ext) = mimetype($filename, false);
471            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
472            $class = 'mediafile mf_'.$class;
473
474            $this->doc .= '<dl class="'.$type.'">'.DOKU_LF;
475            $this->doc .= '<dt><a href="'.exportlink($ID, 'code', array('codeblock' => $this->_codeblock)).'" title="'.$lang['download'].'" class="'.$class.'">';
476            $this->doc .= hsc($filename);
477            $this->doc .= '</a></dt>'.DOKU_LF.'<dd>';
478        }
479
480        if($text{0} == "\n") {
481            $text = substr($text, 1);
482        }
483        if(substr($text, -1) == "\n") {
484            $text = substr($text, 0, -1);
485        }
486
487        if(is_null($language)) {
488            //@author Emmanuel: This line is changed from the original
489            $this->doc .= '<pre><code>'.$this->_xmlEntities($text).'</code></pre>'.DOKU_LF;
490        } else {
491            //@author Emmanuel: This line is changed from the original
492            $this->doc .= '<pre><code class="'.$language.'">'.$this->_xmlEntities($text).'</code></pre>'.DOKU_LF;
493        }
494
495        if($filename) {
496            $this->doc .= '</dd></dl>'.DOKU_LF;
497        }
498
499        $this->_codeblock++;
500    }
501
502}
503