1<?php
2
3/**
4 * Plugin Columns: Layout parser
5 *
6 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
7 * @author     Mykola Ostrovskyy <dwpforge@gmail.com>
8 */
9
10/* Must be run within Dokuwiki */
11if(!defined('DOKU_INC')) die();
12
13if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
14require_once(DOKU_PLUGIN . 'action.php');
15require_once(DOKU_PLUGIN . 'columns/rewriter.php');
16
17class action_plugin_columns extends DokuWiki_Action_Plugin {
18
19    private $block;
20    private $currentBlock;
21    private $currentSectionLevel;
22    private $sectionEdit;
23
24    /**
25     * Register callbacks
26     */
27    public function register(Doku_Event_Handler $controller) {
28        $controller->register_hook('PARSER_HANDLER_DONE', 'AFTER', $this, 'handle');
29    }
30
31    /**
32     *
33     */
34    public function handle(&$event, $param) {
35        $this->reset();
36        $this->buildLayout($event);
37        $rewriter = new instruction_rewriter();
38        foreach ($this->block as $block) {
39            $block->processAttributes($event);
40            $rewriter->addCorrections($block->getCorrections());
41        }
42        $rewriter->process($event->data->calls);
43    }
44
45    /**
46     * Find all columns instructions and construct columns layout based on them
47     */
48    private function buildLayout(&$event) {
49        $calls = count($event->data->calls);
50        for ($c = 0; $c < $calls; $c++) {
51            $call =& $event->data->calls[$c];
52            switch ($call[0]) {
53                case 'section_open':
54                    $this->currentSectionLevel = $call[1][0];
55                    $this->currentBlock->openSection();
56                    break;
57
58                case 'section_close':
59                    $this->currentBlock->closeSection($c);
60                    break;
61
62                case 'plugin':
63                    if ($call[1][0] == 'columns') {
64                        $this->handleColumns($c, $call[1][1][0], $this->detectSectionEdit($event->data->calls, $c));
65                    }
66                    break;
67            }
68        }
69    }
70
71    /**
72     * Reset internal state
73     */
74    private function reset() {
75        $this->block = array();
76        $this->block[0] = new columns_root_block();
77        $this->currentBlock = $this->block[0];
78        $this->currentSectionLevel = 0;
79        $this->sectionEdit = array();
80    }
81
82    /**
83     *
84     */
85    private function detectSectionEdit($call, $start) {
86        $result = null;
87        $calls = count($call);
88        for ($c = $start + 1; $c < $calls; $c++) {
89            switch ($call[$c][0]) {
90                case 'section_close':
91                case 'p_open':
92                case 'p_close':
93                    /* Skip these instructions */
94                    break;
95
96                case 'header':
97                    if (end($this->sectionEdit) != $c) {
98                        $this->sectionEdit[] = $c;
99                        $result = $call[$c][2];
100                    }
101                    break 2;
102
103                case 'plugin':
104                    if ($call[$c][1][0] == 'columns') {
105                        break;
106                    } else {
107                        break 2;
108                    }
109
110                default:
111                    break 2;
112            }
113        }
114        return $result;
115    }
116
117    /**
118     *
119     */
120    private function handleColumns($callIndex, $state, $sectionEdit) {
121        switch ($state) {
122            case DOKU_LEXER_ENTER:
123                $this->currentBlock = new columns_block(count($this->block), $this->currentBlock);
124                $this->currentBlock->addColumn($callIndex, $this->currentSectionLevel);
125                $this->currentBlock->startSection($sectionEdit);
126                $this->block[] = $this->currentBlock;
127                break;
128
129            case DOKU_LEXER_MATCHED:
130                $this->currentBlock->addColumn($callIndex, $this->currentSectionLevel);
131                $this->currentBlock->startSection($sectionEdit);
132                break;
133
134            case DOKU_LEXER_EXIT:
135                $this->currentBlock->endSection($sectionEdit);
136                $this->currentBlock->close($callIndex);
137                $this->currentBlock = $this->currentBlock->getParent();
138                break;
139        }
140    }
141}
142
143class columns_root_block {
144
145    private $sectionLevel;
146    private $call;
147
148    /**
149     * Constructor
150     */
151    public function __construct() {
152        $this->sectionLevel = 0;
153        $this->call = array();
154    }
155
156    /**
157     *
158     */
159    public function getParent() {
160        return $this;
161    }
162
163    /**
164     * Collect stray <newcolumn> tags
165     */
166    public function addColumn($callIndex, $sectionLevel) {
167        $this->call[] = $callIndex;
168    }
169
170    /**
171     *
172     */
173    public function openSection() {
174        $this->sectionLevel++;
175    }
176
177    /**
178     *
179     */
180    public function closeSection($callIndex) {
181        if ($this->sectionLevel > 0) {
182            $this->sectionLevel--;
183        }
184        else {
185            $this->call[] = $callIndex;
186        }
187    }
188
189    /**
190     *
191     */
192    public function startSection($callInfo) {
193    }
194
195    /**
196     *
197     */
198    public function endSection($callInfo) {
199    }
200
201    /**
202     * Collect stray </colums> tags
203     */
204    public function close($callIndex) {
205        $this->call[] = $callIndex;
206    }
207
208    /**
209     *
210     */
211    public function processAttributes(&$event) {
212    }
213
214    /**
215     * Delete all captured tags
216     */
217    public function getCorrections() {
218        $correction = array();
219        foreach ($this->call as $call) {
220            $correction[] = new instruction_rewriter_delete($call);
221        }
222        return $correction;
223    }
224}
225
226class columns_block {
227
228    private $id;
229    private $parent;
230    private $column;
231    private $currentColumn;
232    private $closed;
233
234    /**
235     * Constructor
236     */
237    public function __construct($id, $parent) {
238        $this->id = $id;
239        $this->parent = $parent;
240        $this->column = array();
241        $this->currentColumn = null;
242        $this->closed = false;
243    }
244
245    /**
246     *
247     */
248    public function getParent() {
249        return $this->parent;
250    }
251
252    /**
253     *
254     */
255    public function addColumn($callIndex, $sectionLevel) {
256        if ($this->currentColumn != null) {
257            $this->currentColumn->close($callIndex);
258        }
259        $this->currentColumn = new columns_column($callIndex, $sectionLevel);
260        $this->column[] = $this->currentColumn;
261    }
262
263    /**
264     *
265     */
266    public function openSection() {
267        $this->currentColumn->openSection();
268    }
269
270    /**
271     *
272     */
273    public function closeSection($callIndex) {
274        $this->currentColumn->closeSection($callIndex);
275    }
276
277    /**
278     *
279     */
280    public function startSection($callInfo) {
281        $this->currentColumn->startSection($callInfo);
282    }
283
284    /**
285     *
286     */
287    public function endSection($callInfo) {
288        $this->currentColumn->endSection($callInfo);
289    }
290
291    /**
292     *
293     */
294    public function close($callIndex) {
295        $this->currentColumn->close($callIndex);
296        $this->closed = true;
297    }
298
299    /**
300     * Convert raw attributes and layout information into column attributes
301     */
302    public function processAttributes(&$event) {
303        $columns = count($this->column);
304        for ($c = 0; $c < $columns; $c++) {
305            $call =& $event->data->calls[$this->column[$c]->getOpenCall()];
306            if ($c == 0) {
307                $this->loadBlockAttributes($call[1][1][1]);
308                $this->column[0]->addAttribute('columns', $columns);
309                $this->column[0]->addAttribute('class', 'first');
310            }
311            else {
312                $this->loadColumnAttributes($c, $call[1][1][1]);
313                if ($c == ($columns - 1)) {
314                    $this->column[$c]->addAttribute('class', 'last');
315                }
316            }
317            $this->column[$c]->addAttribute('block-id', $this->id);
318            $this->column[$c]->addAttribute('column-id', $c + 1);
319            $call[1][1][1] = $this->column[$c]->getAttributes();
320        }
321    }
322
323    /**
324     * Convert raw attributes into column attributes
325     */
326    private function loadBlockAttributes($attribute) {
327        $column = -1;
328        $nextColumn = -1;
329        foreach ($attribute as $a) {
330            list($name, $temp) = $this->parseAttribute($a);
331            if ($name == 'width') {
332                if (($column == -1) && array_key_exists('column-width', $temp)) {
333                    $this->column[0]->addAttribute('table-width', $temp['column-width']);
334                }
335                $nextColumn = $column + 1;
336            }
337            if (($column >= 0) && ($column < count($this->column))) {
338                $this->column[$column]->addAttributes($temp);
339            }
340            $column = $nextColumn;
341        }
342    }
343
344    /**
345     * Convert raw attributes into column attributes
346     */
347    private function loadColumnAttributes($column, $attribute) {
348        foreach ($attribute as $a) {
349            list($name, $temp) = $this->parseAttribute($a);
350            $this->column[$column]->addAttributes($temp);
351        }
352    }
353
354    /**
355     *
356     */
357    private function parseAttribute($attribute) {
358        static $syntax = array(
359            '/^left|right|center|justify$/' => 'text-align',
360            '/^top|middle|bottom$/' => 'vertical-align',
361            '/^[lrcjtmb]{1,2}$/' => 'align',
362            '/^continue|\.{3}$/' => 'continue',
363            '/^(\*?)((?:-|(?:\d+\.?|\d*\.\d+)(?:%|em|px|cm|mm|in|pt)))(\*?)$/' => 'width'
364        );
365        $result = array();
366        $attributeName = '';
367        foreach ($syntax as $pattern => $name) {
368            if (preg_match($pattern, $attribute, $match) == 1) {
369                $attributeName = $name;
370                break;
371            }
372        }
373        switch ($attributeName) {
374            case 'text-align':
375            case 'vertical-align':
376                $result[$attributeName] = $match[0];
377                break;
378
379            case 'align':
380                $result = $this->parseAlignAttribute($match[0]);
381                break;
382
383            case 'continue':
384                $result[$attributeName] = 'on';
385                break;
386
387            case 'width':
388                $result = $this->parseWidthAttribute($match);
389                break;
390        }
391        return array($attributeName, $result);
392    }
393
394    /**
395     *
396     */
397    private function parseAlignAttribute($syntax) {
398        $result = array();
399        $align1 = $this->getAlignStyle($syntax[0]);
400        if (strlen($syntax) == 2) {
401            $align2 = $this->getAlignStyle($syntax[1]);
402            if ($align1 != $align2) {
403                $result[$align1] = $this->getAlignment($syntax[0]);
404                $result[$align2] = $this->getAlignment($syntax[1]);
405            }
406        }
407        else {
408            $result[$align1] = $this->getAlignment($syntax[0]);
409        }
410        return $result;
411    }
412
413    /**
414     *
415     */
416    private function getAlignStyle($align) {
417        return preg_match('/[lrcj]/', $align) ? 'text-align' : 'vertical-align';
418    }
419
420    /**
421     *
422     */
423    private function parseWidthAttribute($syntax) {
424        $result = array();
425        if ($syntax[2] != '-') {
426            $result['column-width'] = $syntax[2];
427        }
428        $align = $syntax[1] . '-' . $syntax[3];
429        if ($align != '-') {
430            $result['text-align'] = $this->getAlignment($align);
431        }
432        return $result;
433    }
434
435    /**
436     * Returns column text alignment
437     */
438    private function getAlignment($syntax) {
439        static $align = array(
440            'l' => 'left', '-*' => 'left',
441            'r' => 'right', '*-' => 'right',
442            'c' => 'center', '*-*' => 'center',
443            'j' => 'justify',
444            't' => 'top',
445            'm' => 'middle',
446            'b' => 'bottom'
447        );
448        if (array_key_exists($syntax, $align)) {
449            return $align[$syntax];
450        }
451        else {
452            return '';
453        }
454    }
455
456    /**
457     * Returns a list of corrections that have to be applied to the instruction array
458     */
459    public function getCorrections() {
460        if ($this->closed) {
461            $correction = $this->fixSections();
462        }
463        else {
464            $correction = $this->deleteColumns();
465        }
466        return $correction;
467    }
468
469    /**
470     * Re-write section open/close instructions to produce valid HTML
471     * See columns:design#section_fixing for details
472     */
473    private function fixSections() {
474        $correction = array();
475        foreach ($this->column as $column) {
476            $correction = array_merge($correction, $column->getCorrections());
477        }
478        return $correction;
479    }
480
481    /**
482     *
483     */
484    private function deleteColumns() {
485        $correction = array();
486        foreach ($this->column as $column) {
487            $correction[] = $column->delete();
488        }
489        return $correction;
490    }
491}
492
493class columns_attributes_bag {
494
495    private $attribute;
496
497    /**
498     * Constructor
499     */
500    public function __construct() {
501        $this->attribute = array();
502    }
503
504    /**
505     *
506     */
507    public function addAttribute($name, $value) {
508        $this->attribute[$name] = $value;
509    }
510
511    /**
512     *
513     */
514    public function addAttributes($attribute) {
515        if (is_array($attribute) && (count($attribute) > 0)) {
516            $this->attribute = array_merge($this->attribute, $attribute);
517        }
518    }
519
520    /**
521     *
522     */
523    public function getAttribute($name) {
524        $result = '';
525        if (array_key_exists($name, $this->attribute)) {
526            $result = $this->attribute[$name];
527        }
528        return $result;
529    }
530
531    /**
532     *
533     */
534    public function getAttributes() {
535        return $this->attribute;
536    }
537}
538
539class columns_column extends columns_attributes_bag {
540
541    private $open;
542    private $close;
543    private $sectionLevel;
544    private $sectionOpen;
545    private $sectionClose;
546    private $sectionStart;
547    private $sectionEnd;
548
549    /**
550     * Constructor
551     */
552    public function __construct($open, $sectionLevel) {
553        parent::__construct();
554
555        $this->open = $open;
556        $this->close = -1;
557        $this->sectionLevel = $sectionLevel;
558        $this->sectionOpen = false;
559        $this->sectionClose = -1;
560        $this->sectionStart = null;
561        $this->sectionEnd = null;
562    }
563
564    /**
565     *
566     */
567    public function getOpenCall() {
568        return $this->open;
569    }
570
571    /**
572     *
573     */
574    public function openSection() {
575        $this->sectionOpen = true;
576    }
577
578    /**
579     *
580     */
581    public function closeSection($callIndex) {
582        if ($this->sectionClose == -1) {
583            $this->sectionClose = $callIndex;
584        }
585    }
586
587    /**
588     *
589     */
590    public function startSection($callInfo) {
591        $this->sectionStart = $callInfo;
592    }
593
594    /**
595     *
596     */
597    public function endSection($callInfo) {
598        $this->sectionEnd = $callInfo;
599    }
600
601    /**
602     *
603     */
604    public function close($callIndex) {
605        $this->close = $callIndex;
606    }
607
608    /**
609     *
610     */
611    public function delete() {
612        return new instruction_rewriter_delete($this->open);
613    }
614
615    /**
616     * Re-write section open/close instructions to produce valid HTML
617     * See columns:design#section_fixing for details
618     */
619    public function getCorrections() {
620        $result = array();
621        $deleteSectionClose = ($this->sectionClose != -1);
622        $closeSection = $this->sectionOpen;
623        if ($this->sectionStart != null) {
624            $result = array_merge($result, $this->moveStartSectionEdit());
625        }
626        if (($this->getAttribute('continue') == 'on') && ($this->sectionLevel > 0)) {
627            $result[] = $this->openStartSection();
628            /* Ensure that this section will be properly closed */
629            $deleteSectionClose = false;
630            $closeSection = true;
631        }
632        if ($deleteSectionClose) {
633            /* Remove first section_close from the column to prevent </div> in the middle of the column */
634            $result[] = new instruction_rewriter_delete($this->sectionClose);
635        }
636        if ($closeSection || ($this->sectionEnd != null)) {
637            $result = array_merge($result, $this->closeLastSection($closeSection));
638        }
639        return $result;
640    }
641
642    /**
643     * Moves section_edit at the start of the column out of the column
644     */
645    private function moveStartSectionEdit() {
646        $result = array();
647        $result[0] = new instruction_rewriter_insert($this->open);
648        $result[0]->addPluginCall('columns', array(987, $this->sectionStart - 1), DOKU_LEXER_MATCHED);
649        return $result;
650    }
651
652    /**
653     * Insert section_open at the start of the column
654     */
655    private function openStartSection() {
656        $insert = new instruction_rewriter_insert($this->open + 1);
657        $insert->addCall('section_open', array($this->sectionLevel));
658        return $insert;
659    }
660
661    /**
662     * Close last open section in the column
663     */
664    private function closeLastSection($closeSection) {
665        $result = array();
666        $result[0] = new instruction_rewriter_insert($this->close);
667        if ($closeSection) {
668            $result[0]->addCall('section_close', array());
669        }
670        if ($this->sectionEnd != null) {
671            $result[0]->addPluginCall('columns', array(987, $this->sectionEnd - 1), DOKU_LEXER_MATCHED);
672        }
673        return $result;
674    }
675}
676