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: 373:
374: public function addHtml($child)
375: {
376: return $this->insert(null, $child);
377: }
378:
379:
380: 381: 382: 383: 384:
385: public function addText($text)
386: {
387: if (!$text instanceof IHtmlString) {
388: $text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8');
389: }
390: return $this->insert(null, $text);
391: }
392:
393:
394: 395: 396: 397: 398: 399:
400: public function create($name, $attrs = null)
401: {
402: $this->insert(null, $child = static::el($name, $attrs));
403: return $child;
404: }
405:
406:
407: 408: 409: 410: 411: 412: 413: 414:
415: public function insert($index, $child, $replace = false)
416: {
417: if ($child instanceof IHtmlString || is_scalar($child)) {
418: $child = $child instanceof self ? $child : (string) $child;
419: if ($index === null) {
420: $this->children[] = $child;
421:
422: } else {
423: array_splice($this->children, (int) $index, $replace ? 1 : 0, [$child]);
424: }
425:
426: } else {
427: throw new Nette\InvalidArgumentException(sprintf('Child node must be scalar or Html object, %s given.', is_object($child) ? get_class($child) : gettype($child)));
428: }
429:
430: return $this;
431: }
432:
433:
434: 435: 436: 437: 438: 439:
440: public function offsetSet($index, $child)
441: {
442: $this->insert($index, $child, true);
443: }
444:
445:
446: 447: 448: 449: 450:
451: public function offsetGet($index)
452: {
453: return $this->children[$index];
454: }
455:
456:
457: 458: 459: 460: 461:
462: public function offsetExists($index)
463: {
464: return isset($this->children[$index]);
465: }
466:
467:
468: 469: 470: 471: 472:
473: public function offsetUnset($index)
474: {
475: if (isset($this->children[$index])) {
476: array_splice($this->children, (int) $index, 1);
477: }
478: }
479:
480:
481: 482: 483: 484:
485: public function count()
486: {
487: return count($this->children);
488: }
489:
490:
491: 492: 493: 494:
495: public function removeChildren()
496: {
497: $this->children = [];
498: }
499:
500:
501: 502: 503: 504:
505: public function getIterator()
506: {
507: return new \ArrayIterator($this->children);
508: }
509:
510:
511: 512: 513: 514:
515: public function getChildren()
516: {
517: return $this->children;
518: }
519:
520:
521: 522: 523: 524: 525:
526: public function render($indent = null)
527: {
528: $s = $this->startTag();
529:
530: if (!$this->isEmpty) {
531:
532: if ($indent !== null) {
533: $indent++;
534: }
535: foreach ($this->children as $child) {
536: if ($child instanceof self) {
537: $s .= $child->render($indent);
538: } else {
539: $s .= $child;
540: }
541: }
542:
543:
544: $s .= $this->endTag();
545: }
546:
547: if ($indent !== null) {
548: return "\n" . str_repeat("\t", $indent - 1) . $s . "\n" . str_repeat("\t", max(0, $indent - 2));
549: }
550: return $s;
551: }
552:
553:
554: public function __toString()
555: {
556: try {
557: return $this->render();
558: } catch (\Exception $e) {
559: } catch (\Throwable $e) {
560: }
561: trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR);
562: }
563:
564:
565: 566: 567: 568:
569: public function startTag()
570: {
571: if ($this->name) {
572: return '<' . $this->name . $this->attributes() . (static::$xhtml && $this->isEmpty ? ' />' : '>');
573:
574: } else {
575: return '';
576: }
577: }
578:
579:
580: 581: 582: 583:
584: public function endTag()
585: {
586: return $this->name && !$this->isEmpty ? '</' . $this->name . '>' : '';
587: }
588:
589:
590: 591: 592: 593: 594:
595: public function attributes()
596: {
597: if (!is_array($this->attrs)) {
598: return '';
599: }
600:
601: $s = '';
602: $attrs = $this->attrs;
603: foreach ($attrs as $key => $value) {
604: if ($value === null || $value === false) {
605: continue;
606:
607: } elseif ($value === true) {
608: if (static::$xhtml) {
609: $s .= ' ' . $key . '="' . $key . '"';
610: } else {
611: $s .= ' ' . $key;
612: }
613: continue;
614:
615: } elseif (is_array($value)) {
616: if (strncmp($key, 'data-', 5) === 0) {
617: $value = Json::encode($value);
618:
619: } else {
620: $tmp = null;
621: foreach ($value as $k => $v) {
622: if ($v != null) {
623:
624: $tmp[] = $v === true ? $k : (is_string($k) ? $k . ':' . $v : $v);
625: }
626: }
627: if ($tmp === null) {
628: continue;
629: }
630:
631: $value = implode($key === 'style' || !strncmp($key, 'on', 2) ? ';' : ' ', $tmp);
632: }
633:
634: } elseif (is_float($value)) {
635: $value = rtrim(rtrim(number_format($value, 10, '.', ''), '0'), '.');
636:
637: } else {
638: $value = (string) $value;
639: }
640:
641: $q = strpos($value, '"') === false ? '"' : "'";
642: $s .= ' ' . $key . '=' . $q
643: . str_replace(
644: ['&', $q, '<'],
645: ['&', $q === '"' ? '"' : ''', self::$xhtml ? '<' : '<'],
646: $value
647: )
648: . (strpos($value, '`') !== false && strpbrk($value, ' <>"\'') === false ? ' ' : '')
649: . $q;
650: }
651:
652: $s = str_replace('@', '@', $s);
653: return $s;
654: }
655:
656:
657: 658: 659:
660: public function __clone()
661: {
662: foreach ($this->children as $key => $value) {
663: if (is_object($value)) {
664: $this->children[$key] = clone $value;
665: }
666: }
667: }
668: }
669: