1<?php
2
3/**
4 * DokuWiki Plugin stepbystep (Syntax Component)
5 *
6 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
7 * @author  saggi <saggi@gmx.de>
8 */
9class syntax_plugin_stepbystep_step extends \dokuwiki\Extension\SyntaxPlugin
10{
11    protected $tagcount = 1;
12
13    // default name and style definitions
14    protected $options = [
15        'collapsible_class'        => ' class="stepbystep_collapsible"',
16        'collapsible_class_active' => ' class="stepbystep_collapsible active"',
17        'content_height'           => '',
18        'content_height_max'       => ' style="max-height: fit-content;"',
19        'height_preview'           => ' style="--preview: %s;"',
20        'container'                => 'stepbystep',
21        'container-noback'         => 'nobackground-stepbystep'
22    ];
23
24    /**
25     * Get plugin and component name
26     * @return string
27     */
28    public function getMode(): string
29    {
30        return sprintf("plugin_%s_%s", $this->getPluginName(), $this->getPluginComponent());
31    }
32
33    /** @inheritDoc */
34    public function getType()
35    {
36        return 'substition';
37    }
38
39    /** @inheritDoc */
40    public function getPType()
41    {
42        return 'normal';
43    }
44
45    /** @inheritDoc */
46    public function getSort()
47    {
48        return 410;
49    }
50
51    function getAllowedTypes()
52    {
53        return array('container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs');
54    }
55
56    /**
57     * accept nesting
58     * @param $mode
59     * @return bool
60     */
61    function accepts($mode)
62    {
63        if ($mode == $this->getMode()) {
64            return true;
65        }
66        return parent::accepts($mode);
67    }
68
69    /**
70     * Set the EntryPattern
71     * @param string $mode
72     */
73    public function connectTo($mode)
74    {
75        $this->Lexer->addEntryPattern(
76            sprintf('^#{%1$d}:.*?(?=\n.*?^:#{%1$d}$)', $this->tagcount),
77            $mode,
78            $this->getmode()
79        );
80    }
81
82    /**
83     * Set the ExitPattern
84     */
85    public function postConnect()
86    {
87        $this->Lexer->addExitPattern("^:#{{$this->tagcount}}$", $this->getmode());
88    }
89
90    /**
91     * Handle the match
92     * @param string       $match   The match of the syntax
93     * @param int          $state   The state of the handler
94     * @param int          $pos     The position in the document
95     * @param Doku_Handler $handler The handler
96     * @return array Data for the renderer
97     */
98    public function handle($match, $state, $pos, Doku_Handler $handler)
99    {
100        switch ($state) {
101            case DOKU_LEXER_ENTER :
102                $match = trim(substr($match, $this->tagcount + 1));// returns match after '#{$tagcount}:'
103                $check = sexplode('||', $match, 2);
104                // set default values
105                $data = [
106                    'title'             => '',
107                    'anchor'            => '',
108                    'options'           => [],
109                    'collapsible_class' => $this->options['collapsible_class'],
110                    'content_height'    => $this->options['content_height'],
111                    'preview'           => '',
112                    'container'         => $this->options['container']
113                ];
114                if ($check[0]) {
115                    $data['title']  = hsc($check[0]);
116                    $data['anchor'] = str_replace([':', '.'], '_', cleanID($data['title']));
117                    $data['anchor'] = substr($data['anchor'], 0, 40);
118                }
119                $data = $this->checkOptions($check[1], $data);
120                return [$state, $data];
121            case DOKU_LEXER_UNMATCHED :
122                return [$state, $match];
123            case DOKU_LEXER_EXIT :
124                return [$state, ''];
125        }
126        return [];
127    }
128
129    /**
130     * Create output
131     *
132     * @param string        $mode     string     output format being rendered
133     * @param Doku_Renderer $renderer the current renderer object
134     * @param array         $data     data created by handler()
135     * @return  bool                 rendered correctly?
136     */
137    public function render($mode, Doku_Renderer $renderer, $data)
138    {
139        if ($mode !== 'xhtml') {
140            return false;
141        }
142        list($state, $indata) = $data;
143        switch ($state) {
144            case DOKU_LEXER_ENTER :
145                $type          = 'button';
146                $renderer->doc .= '<div class="' . $indata['container'] . '">' . DOKU_LF;
147                if (is_a($renderer, 'renderer_plugin_dw2pdf')) {
148                    $type = 'div';
149                }
150                // Create the Button/Div e.g.: <button id="anchor"  class="stepbystep_collapsible">title</button>
151                $renderer->doc .= sprintf(
152                    '<%s id="%s"%s>%s</%s>%s',
153                    $type,
154                    $indata['anchor'],
155                    $indata['collapsible_class'],
156                    $indata['title'],
157                    $type,
158                    DOKU_LF
159                );
160                $renderer->doc .= '<div class="stepbystep_content"' . $indata['content_height'] . $indata['preview'] . '>' . DOKU_LF;
161                $renderer->doc .= '<p>' . DOKU_LF;
162                break;
163            case DOKU_LEXER_UNMATCHED :
164                $renderer->cdata($indata);
165                break;
166            case DOKU_LEXER_EXIT :
167                $renderer->doc .= DOKU_LF . '</p>' . DOKU_LF;
168                $renderer->doc .= '</div>' . DOKU_LF;
169                $renderer->doc .= '</div>' . DOKU_LF;
170                break;
171        }
172        return true;
173    }
174
175    /**
176     * check if value of size is a valid length
177     * @param $size
178     * @return string|null
179     */
180    public function checkSize($size): ?string
181    {
182        $size = hsc($size);
183        // check if value of size is a valid length
184        $result = preg_match(
185            '/^(\d*\.?\d)+(|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|px|pt|pc)$/i',
186            $size,
187            $matches
188        );
189        if (!$result) {
190            return null;
191        }
192        $return_size = $matches[0];
193        // check if a unit is given, add px if not
194        if (preg_match('/^(\d*\.?\d)+$/', $return_size)) {
195            $return_size .= 'px';
196        }
197        return $return_size;
198    }
199
200    /**
201     * check and pass options to the data array
202     * @param $options
203     * @param $data
204     * @return array
205     */
206    public function checkOptions($options, $data): array
207    {
208        if (!$options) {
209            return $data;
210        }
211        // pass all options to the renderer
212        $data['options'] = explode(' ', $options);
213        // update default values by option
214        foreach ($data['options'] as $option) {
215            switch ($option) {
216                case 'open':
217                    $data['collapsible_class'] = $this->options['collapsible_class_active'];
218                    $data['content_height']    = $this->options['content_height_max'];
219                    $data['preview']           = '';
220                    break;
221                case (preg_match('/preview:.*/', $option) ? true : false) :
222                    $preview = sexplode(':', $option, 2);
223                    $size    = $this->checkSize($preview[1]);
224                    if (($preview[1]) && ($size)) {
225                        $data['collapsible_class'] = $this->options['collapsible_class'];
226                        $data['content_height']    = $this->options['content_height'];
227                        $data['preview']           = sprintf($this->options['height_preview'], $size);
228                    }
229                    break;
230                case 'noframe' :
231                    $data['container'] = $this->options['container-noback'];
232                    break;
233            }
234        }
235        return $data;
236    }
237}
238
239