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