1<?php
2
3/**
4 * Plugin Columns: Syntax & rendering
5 *
6 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
7 * @author     Mykola Ostrovskyy <dwpforge@gmail.com>
8 *             Based on plugin by Michael Arlt <michael.arlt [at] sk-schwanstetten [dot] de>
9 */
10
11/* Must be run within Dokuwiki */
12if(!defined('DOKU_INC')) die();
13
14if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
15require_once(DOKU_PLUGIN . 'syntax.php');
16
17class syntax_plugin_columns extends DokuWiki_Syntax_Plugin {
18
19    private $mode;
20    private $lexerSyntax;
21    private $syntax;
22    private $xhtmlRenderer;
23    private $odtRenderer;
24
25    /**
26     * Constructor
27     */
28    public function __construct() {
29        $this->mode = substr(get_class($this), 7);
30
31        $columns = $this->getColumnsTagName();
32        $newColumn = $this->getNewColumnTagName();
33        if ($this->getConf('wrapnewcol') == 1) {
34            $newColumnLexer = '<' . $newColumn . '(?:>|\s.*?>)';
35            $newColumnHandler = '<' . $newColumn . '(.*?)>';
36        }
37        else {
38            $newColumnLexer = $newColumn;
39            $newColumnHandler = $newColumn;
40        }
41        $enterLexer = '<' . $columns . '(?:>|\s.*?>)';
42        $enterHandler = '<' . $columns . '(.*?)>';
43        $exit = '<\/' . $columns . '>';
44
45        $this->lexerSyntax['enter'] = $enterLexer;
46        $this->lexerSyntax['newcol'] = $newColumnLexer;
47        $this->lexerSyntax['exit'] = $exit;
48
49        $this->syntax[DOKU_LEXER_ENTER] = '/' . $enterHandler . '/';
50        $this->syntax[DOKU_LEXER_MATCHED] = '/' . $newColumnHandler . '/';
51        $this->syntax[DOKU_LEXER_EXIT] = '/' . $exit . '/';
52    }
53
54    /**
55     * What kind of syntax are we?
56     */
57    public function getType() {
58        return 'substition';
59    }
60
61    public function getPType() {
62        return 'block';
63    }
64
65    /**
66     * Where to sort in?
67     */
68    public function getSort() {
69        return 65;
70    }
71
72    public function connectTo($mode) {
73        $this->Lexer->addSpecialPattern($this->lexerSyntax['enter'], $mode, $this->mode);
74        $this->Lexer->addSpecialPattern($this->lexerSyntax['newcol'], $mode, $this->mode);
75        $this->Lexer->addSpecialPattern($this->lexerSyntax['exit'], $mode, $this->mode);
76    }
77
78    /**
79     * Handle the match
80     */
81    public function handle($match, $state, $pos, Doku_Handler $handler) {
82        foreach ($this->syntax as $state => $pattern) {
83            if (preg_match($pattern, $match, $data) == 1) {
84                break;
85            }
86        }
87        switch ($state) {
88            case DOKU_LEXER_ENTER:
89            case DOKU_LEXER_MATCHED:
90                return array($state, preg_split('/\s+/', $data[1], -1, PREG_SPLIT_NO_EMPTY));
91
92            case DOKU_LEXER_EXIT:
93                return array($state, array());
94        }
95        return false;
96    }
97
98    /**
99     * Create output
100     */
101    public function render($mode, Doku_Renderer $renderer, $data) {
102        $columnsRenderer = $this->getRenderer($mode, $renderer);
103
104        if ($columnsRenderer != NULL) {
105            $columnsRenderer->render($data[0], $renderer, $data[1]);
106            return true;
107        }
108        return false;
109    }
110
111    /**
112     *
113     */
114    private function getRenderer($mode, Doku_Renderer $renderer) {
115        switch ($mode) {
116            case 'xhtml':
117                if ($this->xhtmlRenderer == NULL) {
118                    $this->xhtmlRenderer = new columns_renderer_xhtml();
119                }
120                return $this->xhtmlRenderer;
121
122            case 'odt':
123                if ($this->odtRenderer == NULL) {
124                    if (method_exists($renderer, 'getODTPropertiesFromElement')) {
125                        $this->odtRenderer = new columns_renderer_odt_v2();
126                    }
127                    else {
128                        $this->odtRenderer = new columns_renderer_odt_v1();
129                    }
130                }
131                return $this->odtRenderer;
132        }
133
134        return NULL;
135    }
136
137    /**
138     * Returns columns tag
139     */
140    private function getColumnsTagName() {
141        $tag = $this->getConf('kwcolumns');
142        if ($tag == '') {
143            $tag = $this->getLang('kwcolumns');
144        }
145        return $tag;
146    }
147
148    /**
149     * Returns new column tag
150     */
151    private function getNewColumnTagName() {
152        $tag = $this->getConf('kwnewcol');
153        if ($tag == '') {
154            $tag = $this->getLang('kwnewcol');
155        }
156        return $tag;
157    }
158}
159
160/**
161 * Base class for columns rendering.
162 */
163abstract class columns_renderer {
164    /**
165     *
166     */
167    public function render($state, Doku_Renderer $renderer, $attribute) {
168        switch ($state) {
169            case DOKU_LEXER_ENTER:
170                $this->render_enter($renderer, $attribute);
171                break;
172
173            case DOKU_LEXER_MATCHED:
174                $this->render_matched($renderer, $attribute);
175                break;
176
177            case DOKU_LEXER_EXIT:
178                $this->render_exit($renderer, $attribute);
179                break;
180        }
181    }
182
183    abstract protected function render_enter(Doku_Renderer $renderer, $attribute);
184    abstract protected function render_matched(Doku_Renderer $renderer, $attribute);
185    abstract protected function render_exit(Doku_Renderer $renderer, $attribute);
186
187    /**
188     *
189     */
190    protected function getAttribute($attribute, $name) {
191        $result = '';
192        if (array_key_exists($name, $attribute)) {
193            $result = $attribute[$name];
194        }
195        return $result;
196    }
197
198    /**
199     *
200     */
201    protected function getStyle($attribute, $attributeName, $styleName = '') {
202        $result = $this->getAttribute($attribute, $attributeName);
203        if ($result != '') {
204            if ($styleName == '') {
205                $styleName = $attributeName;
206            }
207            $result = $styleName . ':' . $result . ';';
208        }
209        return $result;
210    }
211}
212
213/**
214 * Class columns_renderer_xhtml
215 * @author LarsDW223
216 */
217class columns_renderer_xhtml extends columns_renderer {
218    /**
219     *
220     */
221    public function render($state, Doku_Renderer $renderer, $attribute) {
222        parent::render($state, $renderer, $attribute);
223
224        if ($state == 987 && method_exists($renderer, 'finishSectionEdit')) {
225            $renderer->finishSectionEdit($attribute);
226        }
227    }
228
229    /**
230     *
231     */
232    protected function render_enter(Doku_Renderer $renderer, $attribute) {
233        $renderer->doc .= $this->renderTable($attribute) . DOKU_LF;
234        $renderer->doc .= '<tr>' . $this->renderTd($attribute) . DOKU_LF;
235    }
236
237    /**
238     *
239     */
240    protected function render_matched(Doku_Renderer $renderer, $attribute) {
241        $renderer->doc .= '</td>' . $this->renderTd($attribute) . DOKU_LF;
242    }
243
244    /**
245     *
246     */
247    protected function render_exit(Doku_Renderer $renderer, $attribute) {
248        $renderer->doc .= '</td></tr></table>' . DOKU_LF;
249    }
250
251    /**
252     *
253     */
254    private function renderTable($attribute) {
255        $width = $this->getAttribute($attribute, 'table-width');
256        if ($width != '') {
257            return '<table class="columns-plugin" style="width:' . $width . '">';
258        }
259        else {
260            return '<table class="columns-plugin">';
261        }
262    }
263
264    /**
265     *
266     */
267    private function renderTd($attribute) {
268        $class[] = 'columns-plugin';
269        $class[] = $this->getAttribute($attribute, 'class');
270        $class[] = $this->getAttribute($attribute, 'text-align');
271        $html = '<td class="' . implode(' ', array_filter($class)) . '"';
272        $style = $this->getStyle($attribute, 'column-width', 'width');
273        $style .= $this->getStyle($attribute, 'vertical-align');
274        if ($style != '') {
275            $html .= ' style="' . $style . '"';
276        }
277        return $html . '>';
278    }
279}
280
281/**
282 * Class columns_renderer_odt_v1
283 */
284class columns_renderer_odt_v1 extends columns_renderer {
285    /**
286     *
287     */
288    protected function render_enter(Doku_Renderer $renderer, $attribute) {
289        $this->addOdtTableStyle($renderer, $attribute);
290        $this->addOdtColumnStyles($renderer, $attribute);
291        $this->renderOdtTableEnter($renderer, $attribute);
292        $this->renderOdtColumnEnter($renderer, $attribute);
293    }
294
295    /**
296     *
297     */
298    protected function render_matched(Doku_Renderer $renderer, $attribute) {
299        $this->addOdtColumnStyles($renderer, $attribute);
300        $this->renderOdtColumnExit($renderer);
301        $this->renderOdtColumnEnter($renderer, $attribute);
302    }
303
304    /**
305     *
306     */
307    protected function render_exit(Doku_Renderer $renderer, $attribute) {
308        $this->renderOdtColumnExit($renderer);
309        $this->renderOdtTableExit($renderer);
310    }
311
312    /**
313     *
314     */
315    private function addOdtTableStyle(Doku_Renderer $renderer, $attribute) {
316        $styleName = $this->getOdtTableStyleName($this->getAttribute($attribute, 'block-id'));
317        $style = '<style:style style:name="' . $styleName . '" style:family="table">';
318        $style .= '<style:table-properties';
319        $width = $this->getAttribute($attribute, 'table-width');
320
321        if (($width != '') && ($width != '100%')) {
322            $metrics = $this->getOdtMetrics($renderer->autostyles);
323            $style .= ' style:width="' . $this->getOdtAbsoluteWidth($metrics, $width) . '"';
324        }
325        $align = ($width == '100%') ? 'margins' : 'left';
326        $style .= ' table:align="' . $align . '"/>';
327        $style .= '</style:style>';
328
329        $renderer->autostyles[$styleName] = $style;
330    }
331
332    /**
333     *
334     */
335    private function addOdtColumnStyles(Doku_Renderer $renderer, $attribute) {
336        $blockId = $this->getAttribute($attribute, 'block-id');
337        $columnId = $this->getAttribute($attribute, 'column-id');
338        $styleName = $this->getOdtTableStyleName($blockId, $columnId);
339
340        $style = '<style:style style:name="' . $styleName . '" style:family="table-column">';
341        $style .= '<style:table-column-properties';
342        $width = $this->getAttribute($attribute, 'column-width');
343
344        if ($width != '') {
345            $metrics = $this->getOdtMetrics($renderer->autostyles);
346            $style .= ' style:column-width="' . $this->getOdtAbsoluteWidth($metrics, $width) . '"';
347        }
348        $style .= '/>';
349        $style .= '</style:style>';
350
351        $renderer->autostyles[$styleName] = $style;
352
353        $styleName = $this->getOdtTableStyleName($blockId, $columnId, 1);
354
355        $style = '<style:style style:name="' . $styleName . '" style:family="table-cell">';
356        $style .= '<style:table-cell-properties';
357        $style .= ' fo:border="none"';
358        $style .= ' fo:padding-top="0cm"';
359        $style .= ' fo:padding-bottom="0cm"';
360
361        switch ($this->getAttribute($attribute, 'class')) {
362            case 'first':
363                $style .= ' fo:padding-left="0cm"';
364                $style .= ' fo:padding-right="0.4cm"';
365                break;
366
367            case 'last':
368                $style .= ' fo:padding-left="0.4cm"';
369                $style .= ' fo:padding-right="0cm"';
370                break;
371        }
372
373        /* There seems to be no easy way to control horizontal alignment of text within
374           the column as fo:text-align aplies to individual paragraphs. */
375        //TODO: $this->getAttribute($attribute, 'text-align');
376
377        $align = $this->getAttribute($attribute, 'vertical-align');
378        if ($align != '') {
379            $style .= ' style:vertical-align="' . $align . '"';
380        }
381        else {
382            $style .= ' style:vertical-align="top"';
383        }
384
385        $style .= '/>';
386        $style .= '</style:style>';
387
388        $renderer->autostyles[$styleName] = $style;
389    }
390
391    /**
392     *
393     */
394    private function renderOdtTableEnter(Doku_Renderer $renderer, $attribute) {
395        $columns = $this->getAttribute($attribute, 'columns');
396        $blockId = $this->getAttribute($attribute, 'block-id');
397        $styleName = $this->getOdtTableStyleName($blockId);
398
399        $renderer->doc .= '<table:table table:style-name="' . $styleName . '">';
400        for ($c = 0; $c < $columns; $c++) {
401            $styleName = $this->getOdtTableStyleName($blockId, $c + 1);
402            $renderer->doc .= '<table:table-column table:style-name="' . $styleName . '" />';
403        }
404        $renderer->doc .= '<table:table-row>';
405    }
406
407    /**
408     *
409     */
410    private function renderOdtColumnEnter(Doku_Renderer $renderer, $attribute) {
411        $blockId = $this->getAttribute($attribute, 'block-id');
412        $columnId = $this->getAttribute($attribute, 'column-id');
413        $styleName = $this->getOdtTableStyleName($blockId, $columnId, 1);
414        $renderer->doc .= '<table:table-cell table:style-name="' . $styleName . '" office:value-type="string">';
415    }
416
417    /**
418     *
419     */
420    private function renderOdtColumnExit(Doku_Renderer $renderer) {
421        $renderer->doc .= '</table:table-cell>';
422    }
423
424    /**
425     *
426     */
427    private function renderOdtTableExit(Doku_Renderer $renderer) {
428        $renderer->doc .= '</table:table-row>';
429        $renderer->doc .= '</table:table>';
430    }
431
432    /**
433     * Convert relative units to absolute
434     */
435    private function getOdtAbsoluteWidth($metrics, $width) {
436        if (preg_match('/([\d\.]+)(.+)/', $width, $match) == 1) {
437            switch ($match[2]) {
438                case '%':
439                    /* Won't work for nested column blocks */
440                    $width = ($match[1] / 100 * $metrics['page-width']) . $metrics['page-width-units'];
441                    break;
442                case 'em':
443                    /* Rough estimate */
444                    $width = ($match[1] * 0.8 * $metrics['font-size']) . $metrics['font-size-units'];
445                    break;
446            }
447        }
448        return $width;
449    }
450
451    /**
452     *
453     */
454    private function getOdtTableStyleName($blockId, $columnId = 0, $cell = 0) {
455        $result = 'ColumnsBlock' . $blockId;
456        if ($columnId != 0) {
457            if ($columnId <= 26) {
458                $result .= '.' . chr(ord('A') + $columnId - 1);
459            }
460            else {
461                /* To unlikey to handle it properly */
462                $result .= '.a';
463            }
464            if ($cell != 0) {
465                $result .= $cell;
466            }
467        }
468        return $result;
469    }
470
471    /**
472     *
473     */
474    private function getOdtMetrics($autoStyle) {
475        $result = array();
476        if (array_key_exists('pm1', $autoStyle)) {
477            $style = $autoStyle['pm1'];
478            if (preg_match('/fo:page-width="([\d\.]+)(.+?)"/', $style, $match) == 1) {
479                $result['page-width'] = floatval($match[1]);
480                $result['page-width-units'] = $match[2];
481                $units = $match[2];
482
483                if (preg_match('/fo:margin-left="([\d\.]+)(.+?)"/', $style, $match) == 1) {
484                    // TODO: Unit conversion
485                    if ($match[2] == $units) {
486                        $result['page-width'] -= floatval($match[1]);
487                    }
488                }
489                if (preg_match('/fo:margin-right="([\d\.]+)(.+?)"/', $style, $match) == 1) {
490                    if ($match[2] == $units) {
491                        $result['page-width'] -= floatval($match[1]);
492                    }
493                }
494            }
495        }
496        if (!array_key_exists('page-width', $result)) {
497            $result['page-width'] = 17;
498            $result['page-width-units'] = 'cm';
499        }
500
501        /* There seems to be no easy way to get default font size apart from loading styles.xml. */
502        $styles = io_readFile(DOKU_PLUGIN . 'odt/styles.xml');
503        if (preg_match('/<style:default-style style:family="paragraph">(.+?)<\/style:default-style>/s', $styles, $match) == 1) {
504            if (preg_match('/<style:text-properties(.+?)>/', $match[1], $match) == 1) {
505                if (preg_match('/fo:font-size="([\d\.]+)(.+?)"/', $match[1], $match) == 1) {
506                    $result['font-size'] = floatval($match[1]);
507                    $result['font-size-units'] = $match[2];
508                }
509            }
510        }
511        if (!array_key_exists('font-size', $result)) {
512            $result['font-size'] = 12;
513            $result['font-size-units'] = 'pt';
514        }
515        return $result;
516    }
517}
518
519/**
520 * Class columns_renderer_odt_v2
521 * @author LarsDW223
522 */
523class columns_renderer_odt_v2 extends columns_renderer {
524    /**
525     *
526     */
527    protected function render_enter(Doku_Renderer $renderer, $attribute) {
528        $this->renderOdtTableEnter($renderer, $attribute);
529        $this->renderOdtColumnEnter($renderer, $attribute);
530    }
531
532    /**
533     *
534     */
535    protected function render_matched(Doku_Renderer $renderer, $attribute) {
536        $this->renderOdtColumnExit($renderer);
537        $this->renderOdtColumnEnter($renderer, $attribute);
538    }
539
540    /**
541     *
542     */
543    protected function render_exit(Doku_Renderer $renderer, $attribute) {
544        $this->renderOdtColumnExit($renderer);
545        $this->renderOdtTableExit($renderer);
546    }
547
548    /**
549     *
550     */
551    private function renderOdtTableEnter(Doku_Renderer $renderer, $attribute) {
552        $properties = array();
553        $properties ['width'] = $this->getAttribute($attribute, 'table-width');
554        $properties ['align'] = 'left';
555        $renderer->_odtTableOpenUseProperties ($properties);
556        $renderer->tablerow_open();
557    }
558
559    /**
560     *
561     */
562    private function renderOdtColumnEnter(Doku_Renderer $renderer, $attribute) {
563        $properties = array();
564        $properties ['width'] = $this->getAttribute($attribute, 'column-width');
565        $properties ['border'] = 'none';
566        $properties ['padding-top'] = '0cm';
567        $properties ['padding-bottom'] = '0cm';
568        switch ($this->getAttribute($attribute, 'class')) {
569            case 'first':
570                $properties ['padding-left'] = '0cm';
571                $properties ['padding-right'] = '0.4cm';
572                break;
573
574            case 'last':
575                $properties ['padding-left'] = '0.4cm';
576                $properties ['padding-right'] = '0cm';
577                break;
578        }
579        $align = $this->getAttribute($attribute, 'vertical-align');
580        if ($align != '') {
581            $properties ['vertical-align'] = $align;
582        }
583        else {
584            $properties ['vertical-align'] = 'top';
585        }
586        $align = $this->getAttribute($attribute, 'text-align');
587        if ($align != '') {
588            $properties ['text-align'] = $align;
589        }
590        else {
591            $properties ['text-align'] = 'left';
592        }
593
594        $renderer->_odtTableCellOpenUseProperties($properties);
595    }
596
597    /**
598     *
599     */
600    private function renderOdtColumnExit(Doku_Renderer $renderer) {
601        $renderer->tablecell_close();
602    }
603
604    /**
605     *
606     */
607    private function renderOdtTableExit(Doku_Renderer $renderer) {
608        $renderer->tablerow_close();
609        $renderer->table_close();
610    }
611}
612