Completed
Push — master ( be8c5d...b94655 )
by Adrian
02:54
created

Tag::cleanAttributeName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 6
Bugs 0 Features 1
Metric Value
c 6
b 0
f 1
dl 0
loc 9
ccs 3
cts 3
cp 1
rs 9.6666
cc 2
eloc 4
nc 2
nop 1
crap 2
1
<?php
2
namespace Sirius\Html;
3
4
/**
5
 * Extended class for building HTML elements.
6
 *
7
 * It offers an interface similar to jQuery's DOM handling functionality
8
 * (besides the Input's class functionality)
9
 * - before(): add something before the element
10
 * - after(): add something after the element;
11
 */
12
class Tag
13
{
14
15
    /**
16
     * Items (strings) to be added before the element
17
     *
18
     * @var array
19
     */
20
    protected $before = array();
21
22
    /**
23
     * Items (strings) to be added after the element
24
     *
25
     * @var array
26
     */
27
    protected $after = array();
28
29
    /**
30
     * The HTML tag
31
     *
32
     * @var string
33
     */
34
    protected $tag = 'div';
35
36
    /**
37
     * Is the element self enclosing
38
     *
39
     * @var bool
40
     */
41
    protected $isSelfClosing = false;
42
43
    /**
44
     * Properties collection
45
     *
46
     * @var array
47
     */
48
    protected $props = array();
49
50
    /**
51
     * Content of the element.
52
     * Can be a string, array, object that has __toString()
53
     *
54
     * @var mixed
55
     */
56
    protected $content = array();
57
58
    /**
59
     * Parent element
60
     *
61
     * @var Tag
62
     */
63
    protected $parent;
64
65
    /**
66
     * The tag builder.
67
     * This is so we can attach children without having to construct them
68
     *
69
     * @var Builder
70
     */
71
    protected $builder;
72
73
    /**
74
     * Factory method.
75
     * If $tag ends in '/' the tag will be considered 'self-closing'
76
     *
77
     * @example ExtendedTag::factory('hr/', null, ['class' => 'separator']);
78
     *          ExtendedTag::factory('div', 'This is my content', ['class' => 'container']);
79
     *
80
     * @param string $tag
81
     * @param array $props
82
     * @param mixed $content
83
     * @param Builder $builder
84
     *
85
     * @return Tag
86
     */
87 5
    static public function create($tag, $props = null, $content = null, Builder $builder = null)
88
    {
89 5
        $widget = new static($props, $content, $builder);
90 5
        if (substr($tag, - 1) === '/') {
91 2
            $widget->tag           = substr($tag, 0, - 1);
92 2
            $widget->isSelfClosing = true;
93 2
        } else {
94 4
            $widget->tag           = $tag;
95 4
            $widget->isSelfClosing = false;
96
        }
97 5
        $widget->setContent($content);
98
99 5
        return $widget;
100
    }
101
102
    /**
103
     *
104
     * @param array $props
105
     *            Additional data for the HTML element (attributes, private data)
106
     * @param mixed $content
107
     *            Content of the HTML element (a string, an array)
108
     * @param Builder $builder
109 36
     */
110
    public function __construct($props = null, $content = null, Builder $builder = null)
111 36
    {
112 36
        $this->builder = $builder;
113 36
        if ($props !== null) {
114 36
            $this->setProps($props);
115 36
        }
116 33
        if ($content !== null) {
117 33
            $this->setContent($content);
118 36
        }
119
    }
120
121
    /**
122
     * Set multipe properties to the HTML element
123
     *
124
     * @param array $props
125
     *
126
     * @return self
127 36
     */
128
    public function setProps($props)
129 36
    {
130
        if ( ! is_array($props) && ! ($props instanceof \Traversable)) {
131
            return $this;
132 36
        }
133 18
        foreach ($props as $name => $value) {
134 36
            $this->set($name, $value);
135
        }
136 36
137
        return $this;
138
    }
139
140
    /**
141
     * Set a single property to the HTML element
142
     *
143
     * @param string $name
144
     * @param mixed $value
145
     *
146
     * @return Tag
147 29
     */
148
    public function set($name, $value = null)
149 29
    {
150 29
        if (is_string($name)) {
151 29
            $name = $this->cleanAttributeName($name);
152 4
            if ($value === null && isset($this->props[$name])) {
153 29
                unset($this->props[$name]);
154 29
            } elseif ($value !== null) {
155 29
                $this->props[$name] = $value;
156 29
            }
157
        }
158 29
159
        return $this;
160
    }
161
162
    /**
163
     * @param $name
164
     *
165
     * @return mixed
166 29
     */
167
    protected function cleanAttributeName($name)
168
    {
169 29
        // private attributes are allowed to have any form
170 10
        if (substr($name, 0, 1) === '_') {
171
            return $name;
172
        }
173 29
174
        return preg_replace('/[^a-zA-Z0-9-]+/', '', $name);
175
    }
176
177
    /**
178
     * Returns some or all of the HTML element's properties
179
     *
180
     * @param array|null $list
181
     *
182
     * @return array
183 27
     */
184
    public function getProps($list = null)
185 27
    {
186 1
        if ($list && is_array($list)) {
187 1
            $result = array();
188 1
            foreach ($list as $key) {
189 1
                $result[$key] = $this->get($key);
190
            }
191 1
192
            return $result;
193
        }
194 26
195
        return $this->props;
196
    }
197
198
    /**
199
     * Returns one of HTML element's properties
200
     *
201
     * @param string $name
202
     *
203
     * @return mixed
204 18
     */
205
    public function get($name)
206 18
    {
207
        $name = $this->cleanAttributeName($name);
208 18
209
        return isset($this->props[$name]) ? $this->props[$name] : null;
210
    }
211
212
    /**
213
     * Add a class to the element's class list
214
     *
215
     * @param string $class
216
     *
217
     * @return self
218 4
     */
219
    public function addClass($class)
220 4
    {
221 4
        if ( ! $this->hasClass($class)) {
222 4
            $this->set('class', trim((string) $this->get('class') . ' ' . $class));
223
        }
224 4
225
        return $this;
226
    }
227
228
    /**
229
     * Remove a class from the element's class list
230
     *
231
     * @param string $class
232
     *
233
     * @return self
234 2
     */
235
    public function removeClass($class)
236 2
    {
237 2
        $classes = $this->get('class');
238 2
        if ($classes) {
239 2
            $classes = trim(preg_replace('/(^| ){1}' . $class . '( |$){1}/i', ' ', $classes));
240 2
            $this->set('class', $classes);
241
        }
242 2
243
        return $this;
244
    }
245
246
    /**
247
     * Toggles a class on the element
248
     *
249
     * @param string $class
250
     *
251
     * @return self
252 1
     */
253
    public function toggleClass($class)
254 1
    {
255 1
        if ($this->hasClass($class)) {
256
            return $this->removeClass($class);
257
        }
258 1
259
        return $this->addClass($class);
260
    }
261
262
    /**
263
     * Checks if the element has a specific class
264
     *
265
     * @param string $class
266
     *
267
     * @return boolean
268 7
     */
269
    public function hasClass($class)
270 7
    {
271
        $classes = $this->get('class');
272 7
273
        return $classes && ((bool) preg_match('/(^| ){1}' . $class . '( |$){1}/i', $classes));
274
    }
275
276
    /**
277
     * Set the content
278
     *
279
     * @param mixed $content
280
     *
281
     * @return $this
282 24
     */
283
    public function setContent($content)
284 24
    {
285 2
        if ( ! $content) {
286
            return $this;
287 23
        }
288 23
        if ( ! is_array($content)) {
289 23
            $content = array( $content );
290 23
        }
291 23
        $this->clearContent();
292 23
        foreach ($content as $child) {
293 23
            $this->addChild($child);
294
        }
295 23
296
        return $this;
297
    }
298
299
    /**
300
     * Get the content/children
301
     *
302
     * @return mixed
303
     */
304
    public function getContent()
305
    {
306
        return $this->content;
307
    }
308
309
    /**
310
     * Empties the content of the tag
311
     *
312
     * @return \Sirius\Html\Tag
313 23
     */
314
    public function clearContent()
315 23
    {
316
        $this->content = array();
317 23
318
        return $this;
319
    }
320
321
    /**
322
     * @param $tagTextOrArray
323
     *
324
     * @return self
325 23
     */
326
    protected function addChild($tagTextOrArray)
327
    {
328 23
        // a list of arguments to be passed to builder->make()
329
        if (is_array($tagTextOrArray) && ! empty($tagTextOrArray)) {
330 1
331
            if ( ! isset($this->builder)) {
332
                throw new \InvalidArgumentException(sprintf('Builder not attached to tag `%s`', $this->tag));
333
            }
334 1
335 1
            $tagName        = $tagTextOrArray[0];
336 1
            $props          = isset($tagTextOrArray[1]) ? $tagTextOrArray[1] : [ ];
337 1
            $content        = isset($tagTextOrArray[2]) ? $tagTextOrArray[2] : [ ];
338 1
            $tagTextOrArray = $this->builder->make($tagName, $props, $content);
339
        }
340 23
341
        array_push($this->content, $tagTextOrArray);
342 23
343
        return $this;
344
    }
345
346
    /**
347
     * Return the attributes as a string for HTML output
348
     * example: title="Click here to delete" class="remove"
349
     *
350
     * @return string
351 23
     */
352
    protected function getAttributesString()
353 23
    {
354 23
        $result = '';
355 23
        ksort($this->props);
356 23
        foreach ($this->props as $k => $v) {
357 19
            if (substr($k, 0, 1) !== '_') {
358 19
                if ($v === true) {
359 19
                    $result .= $k . ' ';
360 1
                } else {
361
                    $result .= $k . '="' . $this->escapeAttr($v) . '" ';
362 23
                }
363 23
            }
364 23
        }
365 19
        if ($result) {
366 19
            $result = ' ' . trim($result);
367
        }
368 23
369
        return $result;
370
    }
371
372
    /**
373
     * @param $attr
374
     *
375
     * @return string
376
     */
377 23
    protected function escapeAttr($attr)
378
    {
379 23
        $attr = (string) $attr;
380 23
381 19
        if (0 === strlen($attr)) {
382 19
            return '';
383 19
        }
384
385 23
        // Don't bother if there are no specialchars - saves some processing
386
        if ( ! preg_match('/[&<>"\']/', $attr)) {
387 23
            return $attr;
388
        }
389
390
        return htmlspecialchars($attr, ENT_COMPAT);
391
    }
392
393
    /**
394
     * Render the element
395 19
     *
396
     * @return string
397 19
     */
398
    public function render()
399 19
    {
400 1
        if ($this->isSelfClosing) {
401
            $element = "<{$this->tag}".$this->getAttributesString()." />";
402
        } else {
403
            $element = "<{$this->tag}".$this->getAttributesString().">".$this->getInnerHtml()."</{$this->tag}>";
404 18
        }
405 17
        $before = !empty($this->before) ? implode(PHP_EOL, $this->before) : '';
406
        $after  = !empty($this->after) ? implode(PHP_EOL, $this->after) : '';
407
408 1
        return $before . $element . $after;
409
    }
410
411
    /**
412
     * Return the innerHTML content of the tag
413
     *
414
     * @return string
415
     */
416 23
    public function getInnerHtml()
417
    {
418 23
        return implode(PHP_EOL, $this->content);
419 11
    }
420 11
421 11
    public function __toString()
422 13
    {
423 13
        return $this->render();
424
    }
425 23
426 23
    /**
427
     * Add a string or a stringifiable object immediately before the element
428 23
     *
429
     * @param string|object $stringOrObject
430
     *
431
     * @return Tag
432
     */
433
    public function before($stringOrObject)
434
    {
435
        array_unshift($this->before, $stringOrObject);
436 13
437
        return $this;
438 13
    }
439
440
    /**
441 18
     * Add a string or a stringifiable object immediately after the element
442
     *
443 18
     * @param string|object $stringOrObject
444
     *
445
     * @return Tag
446
     */
447
    public function after($stringOrObject)
448
    {
449
        array_push($this->after, $stringOrObject);
450
451
        return $this;
452
    }
453 1
454
    /**
455 1
     * Add a string or a stringifiable object immediately as the first child of the element
456
     *
457 1
     * @param string|object $stringOrObject
458
     *
459
     * @return Tag
460
     */
461
    public function prepend($stringOrObject)
462
    {
463
        array_unshift($this->content, $stringOrObject);
464
465
        return $this;
466
    }
467 1
468
    /**
469 1
     * Add a string or a stringifiable object as the last child the element
470
     *
471 1
     * @param string|object $stringOrObject
472
     *
473
     * @return Tag
474
     */
475
    public function append($stringOrObject)
476
    {
477
        array_push($this->content, $stringOrObject);
478
479
        return $this;
480
    }
481 1
482
    /**
483 1
     * Add something before and after the element.
484
     * Proxy for calling before() and after() simoultaneously
485 1
     *
486
     * @param string|object $before
487
     * @param string|object $after
488
     *
489
     * @return Tag
490
     */
491
    public function wrap($before, $after)
492
    {
493
        return $this->before($before)->after($after);
494
    }
495 1
}
496
497