1<?php
2/**
3 * Bootstrap Wrapper Plugin
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
7 * @copyright  (C) 2015-2019, Giuseppe Di Terlizzi
8 */
9// must be run within Dokuwiki
10if (!defined('DOKU_INC')) {
11    die();
12}
13
14if (!defined('DOKU_PLUGIN')) {
15    define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
16}
17
18class syntax_plugin_bootswrapper_bootstrap extends DokuWiki_Syntax_Plugin
19{
20
21    public $p_type           = 'stack';
22    public $pattern_start    = '<BOOTSTRAP.+?>';
23    public $pattern_end      = '</BOOTSTRAP>';
24    public $template_start   = '<div class="%s">';
25    public $template_content = '%s';
26    public $template_end     = '</div>';
27    public $header_pattern   = '[ \t]*={2,}[^\n]+={2,}[ \t]*(?=\n)';
28    public $tag_attributes   = array();
29    public $tag_name         = null;
30
31    // HTML core/global attribute
32    public $core_attributes = array(
33        'id'    => array(
34            'type'     => 'string',
35            'values'   => null,
36            'required' => false,
37            'default'  => null),
38        'class' => array(
39            'type'     => 'string',
40            'values'   => null,
41            'required' => false,
42            'default'  => null),
43        'style' => array(
44            'type'     => 'string',
45            'values'   => null,
46            'required' => false,
47            'default'  => null),
48        'title' => array(
49            'type'     => 'string',
50            'values'   => null,
51            'required' => false,
52            'default'  => null),
53        'lang'  => array(
54            'type'     => 'string',
55            'values'   => null,
56            'required' => false,
57            'default'  => null),
58        'dir'   => array(
59            'type'     => 'string',
60            'values'   => array('ltr', 'rtl'),
61            'required' => false,
62            'default'  => null),
63    );
64
65    /**
66     * Check default and user attributes
67     *
68     * @param   array  $attributes
69     */
70    protected function checkAttributes($attributes = array())
71    {
72
73        global $ACT;
74
75        $default_attributes = array();
76        $merged_attributes  = array();
77        $checked_attributes = array();
78
79        if ($ACT == 'preview') {
80            $msg_title = '<strong>Bootstrap Wrapper Plugin - ' . (ucfirst(str_replace('syntax_plugin_bootswrapper_',
81                '', get_class($this)))) . '</strong>';
82        }
83
84        $tag_attributes = array_merge($this->core_attributes, $this->tag_attributes);
85
86        // Save the default values of attributes
87        foreach ($tag_attributes as $attribute => $item) {
88            $default_attributes[$attribute] = $item['default'];
89        }
90
91        foreach ($attributes as $name => $value) {
92
93            if (!isset($tag_attributes[$name])) {
94                if ($ACT == 'preview') {
95                    msg("$msg_title Unknown attribute <code>$name</code>", -1);
96                }
97                continue;
98            }
99
100            $item = $tag_attributes[$name];
101
102            $required = isset($item['required']) ? $item['required'] : false;
103            $values   = isset($item['values']) ? $item['values'] : null;
104            $default  = isset($item['default']) ? $item['default'] : null;
105
106            // Normalize boolean value
107            if ($item['type'] == 'boolean') {
108                switch ($value) {
109                    case 'false':
110                    case 'FALSE':
111                        $value = false;
112                        break;
113                    case 'true':
114                    case 'TRUE':
115                        $value = true;
116                        break;
117                }
118            }
119
120            if ($name == 'class') {
121                $value = explode(' ', $value);
122            }
123
124            $checked_attributes[$name] = $value;
125
126            // Set the default value when the user-value is empty
127            if ($required && empty($value)) {
128                $checked_attributes[$name] = $default;
129
130                // Check if the user attribute have a valid range values (single value)
131            } elseif ($item['type'] !== 'multiple' && is_array($values) && !in_array($value, $values)) {
132                if ($ACT == 'preview') {
133                    msg("$msg_title Invalid value (<code>$value</code>) for <code>$name</code> attribute. It will apply the default value <code>$default</code>", 2);
134                }
135
136                $checked_attributes[$name] = $default;
137
138                // Check if the user attribute have a valid range values (multiple values)
139            } elseif ($item['type'] == 'multiple') {
140
141                $multitple_values = explode(' ', $value);
142                $check            = 0;
143
144                foreach ($multitple_values as $single_value) {
145                    if (!in_array($single_value, $values)) {
146                        $check = 1;
147                    }
148                }
149
150                if ($check) {
151                    if ($ACT == 'preview') {
152                        msg("$msg_title Invalid value (<code>$value</code>) for <code>$name</code> attribute. It will apply the default value <code>$default</code>", 2);
153                    }
154                    $checked_attributes[$name] = $default;
155                }
156            }
157        }
158
159        // Merge attributes (default + user)
160        $merged_attributes = array_merge($default_attributes, $checked_attributes);
161
162        // Remove empty attributes
163        foreach ($merged_attributes as $attribute => $value) {
164            if (empty($value)) {
165                unset($merged_attributes[$attribute]);
166            }
167        }
168
169        // Uncomment for debug
170        // msg("$msg_title " . print_r($merged_attributes, 1));
171
172        return $merged_attributes;
173
174    }
175
176    public function getType()
177    {
178        return 'formatting';
179    }
180
181    public function getAllowedTypes()
182    {
183        return array('container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs');
184    }
185
186    public function getPType()
187    {
188        return $this->p_type;
189    }
190
191    public function getSort()
192    {
193        return 195;
194    }
195
196    public function connectTo($mode)
197    {
198        $this->Lexer->addEntryPattern($this->pattern_start, $mode, 'plugin_bootswrapper_' . $this->getPluginComponent());
199    }
200
201    public function postConnect()
202    {
203        $this->Lexer->addExitPattern($this->pattern_end, 'plugin_bootswrapper_' . $this->getPluginComponent());
204        $this->Lexer->addPattern($this->header_pattern, 'plugin_bootswrapper_' . $this->getPluginComponent());
205    }
206
207    public function handle($match, $state, $pos, Doku_Handler $handler)
208    {
209
210        switch ($state) {
211            case DOKU_LEXER_MATCHED:
212                $title = trim($match);
213                $level = 7 - strspn($title, '=');
214                if ($level < 1) {
215                    $level = 1;
216                }
217
218                $title = trim($title, '=');
219                $title = trim($title);
220
221                $handler->_addCall('header', array($title, $level, $pos), $pos);
222
223                break;
224
225            case DOKU_LEXER_ENTER:
226                $attributes = array();
227                $xml        = simplexml_load_string(str_replace('>', '/>', $match));
228
229                if (!is_object($xml)) {
230                    $xml = simplexml_load_string('<foo />');
231
232                    global $ACT;
233
234                    if ($ACT == 'preview') {
235                        msg('<strong>Bootstrap Wrapper</strong> - Malformed tag (<code>' . hsc($match) . '</code>). Please check your code!', -1);
236                    }
237                }
238
239                $tag = $xml->getName();
240
241                foreach ($xml->attributes() as $key => $value) {
242                    $attributes[$key] = (string) $value;
243                }
244
245                if ($tag == strtolower($tag)) {
246                    $is_block = false;
247                }
248
249                if ($tag == strtoupper($tag)) {
250                    $is_block = true;
251                }
252
253                $checked_attributes = $this->checkAttributes($attributes);
254
255                return array($state, $match, $pos, $checked_attributes, $is_block);
256
257            case DOKU_LEXER_UNMATCHED:
258                $handler->_addCall('cdata', array($match), $pos, null);
259                break;
260
261            case DOKU_LEXER_EXIT:
262                return array($state, $match, $pos, null);
263        }
264
265        return array();
266    }
267
268    public function render($mode, Doku_Renderer $renderer, $data)
269    {
270
271        if (empty($data)) {
272            return false;
273        }
274
275        if ($mode !== 'xhtml') {
276            return false;
277        }
278
279        /** @var Doku_Renderer_xhtml $renderer */
280        list($state, $match) = $data;
281
282        if ($state == DOKU_LEXER_ENTER) {
283            $markup = $this->template_start;
284            $renderer->doc .= $markup;
285            return true;
286        }
287
288        if ($state == DOKU_LEXER_EXIT) {
289            $renderer->doc .= $this->template_end;
290            return true;
291        }
292
293        return true;
294    }
295
296    protected function mergeCoreAttributes($attributes)
297    {
298
299        $core_attributes = array();
300
301        foreach (array_keys($this->core_attributes) as $attribute) {
302            if (isset($attributes[$attribute])) {
303                $core_attributes[$attribute] = $attributes[$attribute];
304            }
305        }
306
307        return $core_attributes;
308    }
309
310    protected function buildAttributes($attributes, $override_attributes = array())
311    {
312
313        $attributes      = array_merge_recursive($attributes, $override_attributes);
314        $html_attributes = array();
315
316        foreach ($attributes as $attribute => $value) {
317            if ($attribute == 'class') {
318                $value = trim(implode(' ', array_unique($value)));
319            }
320
321            if ($attribute == 'style') {
322                $tmp = '';
323                foreach ($value as $property => $val) {
324                    $tmp .= "$property:$val";
325                }
326                $value = $tmp;
327            }
328
329            if ($value) {
330                $html_attributes[] = "$attribute=\"$value\"";
331            }
332        }
333
334        return implode(' ', $html_attributes);
335
336    }
337
338}
339