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