1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Utils;
9:
10: use Nette;
11:
12:
13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23:
24: class Html implements \ArrayAccess, \Countable, \IteratorAggregate, IHtmlString
25: {
26: use Nette\SmartObject;
27:
28:
29: public $attrs = [];
30:
31:
32: public static $xhtml = false;
33:
34:
35: public static $emptyElements = [
36: 'img' => 1, 'hr' => 1, 'br' => 1, 'input' => 1, 'meta' => 1, 'area' => 1, 'embed' => 1, 'keygen' => 1,
37: 'source' => 1, 'base' => 1, 'col' => 1, 'link' => 1, 'param' => 1, 'basefont' => 1, 'frame' => 1,
38: 'isindex' => 1, 'wbr' => 1, 'command' => 1, 'track' => 1,
39: ];
40:
41:
42: protected $children = [];
43:
44:
45: private $name;
46:
47:
48: private $isEmpty;
49:
50:
51: 52: 53: 54: 55: 56:
57: public static function el($name = null, $attrs = null)
58: {
59: $el = new static;
60: $parts = explode(' ', (string) $name, 2);
61: $el->setName($parts[0]);
62:
63: if (is_array($attrs)) {
64: $el->attrs = $attrs;
65:
66: } elseif ($attrs !== null) {
67: $el->setText($attrs);
68: }
69:
70: if (isset($parts[1])) {
71: foreach (Strings::matchAll($parts[1] . ' ', '#([a-z0-9:-]+)(?:=(["\'])?(.*?)(?(2)\\2|\s))?#i') as $m) {
72: $el->attrs[$m[1]] = isset($m[3]) ? $m[3] : true;
73: }
74: }
75:
76: return $el;
77: }
78:
79:
80: 81: 82: 83: 84: 85: 86:
87: public function setName($name, $isEmpty = null)
88: {
89: if ($name !== null && !is_string($name)) {
90: throw new Nette\InvalidArgumentException(sprintf('Name must be string or null, %s given.', gettype($name)));
91: }
92:
93: $this->name = $name;
94: $this->isEmpty = $isEmpty === null ? isset(static::$emptyElements[$name]) : (bool) $isEmpty;
95: return $this;
96: }
97:
98:
99: 100: 101: 102:
103: public function getName()
104: {
105: return $this->name;
106: }
107:
108:
109: 110: 111: 112:
113: public function isEmpty()
114: {
115: return $this->isEmpty;
116: }
117:
118:
119: 120: 121: 122: 123:
124: public function addAttributes(array $attrs)
125: {
126: $this->attrs = array_merge($this->attrs, $attrs);
127: return $this;
128: }
129:
130:
131: 132: 133: 134: 135: 136: 137:
138: public function appendAttribute($name, $value, $option = true)
139: {
140: if (is_array($value)) {
141: $prev = isset($this->attrs[$name]) ? (array) $this->attrs[$name] : [];
142: $this->attrs[$name] = $value + $prev;
143:
144: } elseif ((string) $value === '') {
145: $tmp = &$this->attrs[$name];
146:
147: } elseif (!isset($this->attrs[$name]) || is_array($this->attrs[$name])) {
148: $this->attrs[$name][$value] = $option;
149:
150: } else {
151: $this->attrs[$name] = [$this->attrs[$name] => true, $value => $option];
152: }
153: return $this;
154: }
155:
156:
157: 158: 159: 160: 161: 162:
163: public function setAttribute($name, $value)
164: {
165: $this->attrs[$name] = $value;
166: return $this;
167: }
168:
169:
170: 171: 172: 173: 174:
175: public function getAttribute($name)
176: {
177: return isset($this->attrs[$name]) ? $this->attrs[$name] : null;
178: }
179:
180:
181: 182: 183: 184: 185:
186: public function removeAttribute($name)
187: {
188: unset($this->attrs[$name]);
189: return $this;
190: }
191:
192:
193: 194: 195: 196:
197: public function removeAttributes(array $attributes)
198: {
199: foreach ($attributes as $name) {
200: unset($this->attrs[$name]);
201: }
202: return $this;
203: }
204:
205:
206: 207: 208: 209: 210: 211:
212: public function __set($name, $value)
213: {
214: $this->attrs[$name] = $value;
215: }
216:
217:
218: 219: 220: 221: 222:
223: public function &__get($name)
224: {
225: return $this->attrs[$name];
226: }
227:
228:
229: 230: 231: 232: 233:
234: public function __isset($name)
235: {
236: return isset($this->attrs[$name]);
237: }
238:
239:
240: 241: 242: 243: 244:
245: public function __unset($name)
246: {
247: unset($this->attrs[$name]);
248: }
249:
250:
251: 252: 253: 254: 255: 256:
257: public function __call($m, $args)
258: {
259: $p = substr($m, 0, 3);
260: if ($p === 'get' || $p === 'set' || $p === 'add') {
261: $m = substr($m, 3);
262: $m[0] = $m[0] | "\x20";
263: if ($p === 'get') {
264: return isset($this->attrs[$m]) ? $this->attrs[$m] : null;
265:
266: } elseif ($p === 'add') {
267: $args[] = true;
268: }
269: }
270:
271: if (count($args) === 0) {
272:
273: } elseif (count($args) === 1) {
274: $this->attrs[$m] = $args[0];
275:
276: } else {
277: $this->appendAttribute($m, $args[0], $args[1]);
278: }
279:
280: return $this;
281: }
282:
283:
284: 285: 286: 287: 288: 289:
290: public function href($path, $query = null)
291: {
292: if ($query) {
293: $query = http_build_query($query, '', '&');
294: if ($query !== '') {
295: $path .= '?' . $query;
296: }
297: }
298: $this->attrs['href'] = $path;
299: return $this;
300: }
301:
302:
303: 304: 305: 306:
307: public function data($name, $value = null)
308: {
309: if (func_num_args() === 1) {
310: $this->attrs['data'] = $name;
311: } else {
312: $this->attrs["data-$name"] = is_bool($value) ? json_encode($value) : $value;
313: }
314: return $this;
315: }
316:
317:
318: 319: 320: 321: 322: 323:
324: public function setHtml($html)
325: {
326: if (is_array($html)) {
327: throw new Nette\InvalidArgumentException(sprintf('Textual content must be a scalar, %s given.', gettype($html)));
328: }
329: $this->removeChildren();
330: $this->children[] = (string) $html;
331: return $this;
332: }
333:
334:
335: 336: 337: 338:
339: public function getHtml()
340: {
341: return implode('', $this->children);
342: }
343:
344:
345: 346: 347: 348: 349:
350: public function setText($text)
351: {
352: if (!$text instanceof IHtmlString) {
353: $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
354: }
355: return $this->setHtml($text);
356: }
357:
358:
359: 360: 361: 362:
363: public function getText()
364: {
365: return html_entity_decode(strip_tags($this->getHtml()), ENT_QUOTES, 'UTF-8');
366: }
367:
368:
369: 370: 371:
372: public function add($child)
373: {
374: trigger_error(__METHOD__ . '() is deprecated, use addHtml() or addText() instead.', E_USER_DEPRECATED);
375: return $this->addHtml($child);
376: }
377:
378:
379: 380: 381: 382: 383:
384: public function addHtml($child)
385: {
386: return $this->insert(null, $child);
387: }
388:
389:
390: 391: 392: 393: 394:
395: public function addText($text)
396: {
397: if (!$text instanceof IHtmlString) {
398: $text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8');
399: }
400: return $this->insert(null, $text);
401: }
402:
403:
404: 405: 406: 407: 408: 409:
410: public function create($name, $attrs = null)
411: {
412: $this->insert(null, $child = static::el($name, $attrs));
413: return $child;
414: }
415:
416:
417: 418: 419: 420: 421: 422: 423: 424:
425: public function insert($index, $child, $replace = false)
426: {
427: if ($child instanceof IHtmlString || is_scalar($child)) {
428: $child = $child instanceof self ? $child : (string) $child;
429: if ($index === null) {
430: $this->children[] = $child;
431:
432: } else {
433: array_splice($this->children, (int) $index, $replace ? 1 : 0, [$child]);
434: }
435:
436: } else {
437: throw new Nette\InvalidArgumentException(sprintf('Child node must be scalar or Html object, %s given.', is_object($child) ? get_class($child) : gettype($child)));
438: }
439:
440: return $this;
441: }
442:
443:
444: 445: 446: 447: 448: 449:
450: public function offsetSet($index, $child)
451: {
452: $this->insert($index, $child, true);
453: }
454:
455:
456: 457: 458: 459: 460:
461: public function offsetGet($index)
462: {
463: return $this->children[$index];
464: }
465:
466:
467: 468: 469: 470: 471:
472: public function offsetExists($index)
473: {
474: return isset($this->children[$index]);
475: }
476:
477:
478: 479: 480: 481: 482:
483: public function offsetUnset($index)
484: {
485: if (isset($this->children[$index])) {
486: array_splice($this->children, (int) $index, 1);
487: }
488: }
489:
490:
491: 492: 493: 494:
495: public function count()
496: {
497: return count($this->children);
498: }
499:
500:
501: 502: 503: 504:
505: public function removeChildren()
506: {
507: $this->children = [];
508: }
509:
510:
511: 512: 513: 514:
515: public function getIterator()
516: {
517: return new \ArrayIterator($this->children);
518: }
519:
520:
521: 522: 523: 524:
525: public function getChildren()
526: {
527: return $this->children;
528: }
529:
530:
531: 532: 533: 534: 535:
536: public function render($indent = null)
537: {
538: $s = $this->startTag();
539:
540: if (!$this->isEmpty) {
541:
542: if ($indent !== null) {
543: $indent++;
544: }
545: foreach ($this->children as $child) {
546: if ($child instanceof self) {
547: $s .= $child->render($indent);
548: } else {
549: $s .= $child;
550: }
551: }
552:
553:
554: $s .= $this->endTag();
555: }
556:
557: if ($indent !== null) {
558: return "\n" . str_repeat("\t", $indent - 1) . $s . "\n" . str_repeat("\t", max(0, $indent - 2));
559: }
560: return $s;
561: }
562:
563:
564: public function __toString()
565: {
566: try {
567: return $this->render();
568: } catch (\Exception $e) {
569: } catch (\Throwable $e) {
570: }
571: trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR);
572: }
573:
574:
575: 576: 577: 578:
579: public function startTag()
580: {
581: if ($this->name) {
582: return '<' . $this->name . $this->attributes() . (static::$xhtml && $this->isEmpty ? ' />' : '>');
583:
584: } else {
585: return '';
586: }
587: }
588:
589:
590: 591: 592: 593:
594: public function endTag()
595: {
596: return $this->name && !$this->isEmpty ? '</' . $this->name . '>' : '';
597: }
598:
599:
600: 601: 602: 603: 604:
605: public function attributes()
606: {
607: if (!is_array($this->attrs)) {
608: return '';
609: }
610:
611: $s = '';
612: $attrs = $this->attrs;
613: if (isset($attrs['data']) && is_array($attrs['data'])) {
614: trigger_error('Expanded attribute "data" is deprecated.', E_USER_DEPRECATED);
615: foreach ($attrs['data'] as $key => $value) {
616: $attrs['data-' . $key] = $value;
617: }
618: unset($attrs['data']);
619: }
620:
621: foreach ($attrs as $key => $value) {
622: if ($value === null || $value === false) {
623: continue;
624:
625: } elseif ($value === true) {
626: if (static::$xhtml) {
627: $s .= ' ' . $key . '="' . $key . '"';
628: } else {
629: $s .= ' ' . $key;
630: }
631: continue;
632:
633: } elseif (is_array($value)) {
634: if (strncmp($key, 'data-', 5) === 0) {
635: $value = Json::encode($value);
636:
637: } else {
638: $tmp = null;
639: foreach ($value as $k => $v) {
640: if ($v != null) {
641:
642: $tmp[] = $v === true ? $k : (is_string($k) ? $k . ':' . $v : $v);
643: }
644: }
645: if ($tmp === null) {
646: continue;
647: }
648:
649: $value = implode($key === 'style' || !strncmp($key, 'on', 2) ? ';' : ' ', $tmp);
650: }
651:
652: } elseif (is_float($value)) {
653: $value = rtrim(rtrim(number_format($value, 10, '.', ''), '0'), '.');
654:
655: } else {
656: $value = (string) $value;
657: }
658:
659: $q = strpos($value, '"') === false ? '"' : "'";
660: $s .= ' ' . $key . '=' . $q
661: . str_replace(
662: ['&', $q, '<'],
663: ['&', $q === '"' ? '"' : ''', self::$xhtml ? '<' : '<'],
664: $value
665: )
666: . (strpos($value, '`') !== false && strpbrk($value, ' <>"\'') === false ? ' ' : '')
667: . $q;
668: }
669:
670: $s = str_replace('@', '@', $s);
671: return $s;
672: }
673:
674:
675: 676: 677:
678: public function __clone()
679: {
680: foreach ($this->children as $key => $value) {
681: if (is_object($value)) {
682: $this->children[$key] = clone $value;
683: }
684: }
685: }
686: }
687: