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: private $name;
30:
31:
32: private $isEmpty;
33:
34:
35: public $attrs = [];
36:
37:
38: protected $children = [];
39:
40:
41: public static $xhtml = FALSE;
42:
43:
44: public static $emptyElements = [
45: 'img' => 1, 'hr' => 1, 'br' => 1, 'input' => 1, 'meta' => 1, 'area' => 1, 'embed' => 1, 'keygen' => 1,
46: 'source' => 1, 'base' => 1, 'col' => 1, 'link' => 1, 'param' => 1, 'basefont' => 1, 'frame' => 1,
47: 'isindex' => 1, 'wbr' => 1, 'command' => 1, 'track' => 1,
48: ];
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: 198:
199: public function __set($name, $value)
200: {
201: $this->attrs[$name] = $value;
202: }
203:
204:
205: 206: 207: 208: 209:
210: public function &__get($name)
211: {
212: return $this->attrs[$name];
213: }
214:
215:
216: 217: 218: 219: 220:
221: public function __isset($name)
222: {
223: return isset($this->attrs[$name]);
224: }
225:
226:
227: 228: 229: 230: 231:
232: public function __unset($name)
233: {
234: unset($this->attrs[$name]);
235: }
236:
237:
238: 239: 240: 241: 242: 243:
244: public function __call($m, $args)
245: {
246: $p = substr($m, 0, 3);
247: if ($p === 'get' || $p === 'set' || $p === 'add') {
248: $m = substr($m, 3);
249: $m[0] = $m[0] | "\x20";
250: if ($p === 'get') {
251: return isset($this->attrs[$m]) ? $this->attrs[$m] : NULL;
252:
253: } elseif ($p === 'add') {
254: $args[] = TRUE;
255: }
256: }
257:
258: if (count($args) === 0) {
259:
260: } elseif (count($args) === 1) {
261: $this->attrs[$m] = $args[0];
262:
263: } else {
264: $this->appendAttribute($m, $args[0], $args[1]);
265: }
266:
267: return $this;
268: }
269:
270:
271: 272: 273: 274: 275: 276:
277: public function href($path, $query = NULL)
278: {
279: if ($query) {
280: $query = http_build_query($query, '', '&');
281: if ($query !== '') {
282: $path .= '?' . $query;
283: }
284: }
285: $this->attrs['href'] = $path;
286: return $this;
287: }
288:
289:
290: 291: 292: 293:
294: public function data($name, $value = NULL)
295: {
296: if (func_num_args() === 1) {
297: $this->attrs['data'] = $name;
298: } else {
299: $this->attrs["data-$name"] = is_bool($value) ? json_encode($value) : $value;
300: }
301: return $this;
302: }
303:
304:
305: 306: 307: 308: 309: 310:
311: public function setHtml($html)
312: {
313: if (is_array($html)) {
314: throw new Nette\InvalidArgumentException(sprintf('Textual content must be a scalar, %s given.', gettype($html)));
315: }
316: $this->removeChildren();
317: $this->children[] = (string) $html;
318: return $this;
319: }
320:
321:
322: 323: 324: 325:
326: public function getHtml()
327: {
328: $s = '';
329: foreach ($this->children as $child) {
330: if (is_object($child)) {
331: $s .= $child->render();
332: } else {
333: $s .= $child;
334: }
335: }
336: return $s;
337: }
338:
339:
340: 341: 342: 343: 344: 345:
346: public function setText($text)
347: {
348: if (!is_array($text) && !$text instanceof self) {
349: $text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8');
350: }
351: return $this->setHtml($text);
352: }
353:
354:
355: 356: 357: 358:
359: public function getText()
360: {
361: return html_entity_decode(strip_tags($this->getHtml()), ENT_QUOTES, 'UTF-8');
362: }
363:
364:
365: 366: 367:
368: public function add($child)
369: {
370: trigger_error(__METHOD__ . '() is deprecated, use addHtml() or addText() instead.', E_USER_DEPRECATED);
371: return $this->addHtml($child);
372: }
373:
374:
375: 376: 377: 378: 379:
380: public function addHtml($child)
381: {
382: return $this->insert(NULL, $child);
383: }
384:
385:
386: 387: 388: 389: 390:
391: public function addText($text)
392: {
393: $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
394: return $this->insert(NULL, $text);
395: }
396:
397:
398: 399: 400: 401: 402: 403:
404: public function create($name, $attrs = NULL)
405: {
406: $this->insert(NULL, $child = static::el($name, $attrs));
407: return $child;
408: }
409:
410:
411: 412: 413: 414: 415: 416: 417: 418:
419: public function insert($index, $child, $replace = FALSE)
420: {
421: if ($child instanceof self || is_scalar($child)) {
422: if ($index === NULL) {
423: $this->children[] = $child;
424:
425: } else {
426: array_splice($this->children, (int) $index, $replace ? 1 : 0, [$child]);
427: }
428:
429: } else {
430: throw new Nette\InvalidArgumentException(sprintf('Child node must be scalar or Html object, %s given.', is_object($child) ? get_class($child) : gettype($child)));
431: }
432:
433: return $this;
434: }
435:
436:
437: 438: 439: 440: 441: 442:
443: public function offsetSet($index, $child)
444: {
445: $this->insert($index, $child, TRUE);
446: }
447:
448:
449: 450: 451: 452: 453:
454: public function offsetGet($index)
455: {
456: return $this->children[$index];
457: }
458:
459:
460: 461: 462: 463: 464:
465: public function offsetExists($index)
466: {
467: return isset($this->children[$index]);
468: }
469:
470:
471: 472: 473: 474: 475:
476: public function offsetUnset($index)
477: {
478: if (isset($this->children[$index])) {
479: array_splice($this->children, (int) $index, 1);
480: }
481: }
482:
483:
484: 485: 486: 487:
488: public function count()
489: {
490: return count($this->children);
491: }
492:
493:
494: 495: 496: 497:
498: public function removeChildren()
499: {
500: $this->children = [];
501: }
502:
503:
504: 505: 506: 507:
508: public function getIterator()
509: {
510: return new \ArrayIterator($this->children);
511: }
512:
513:
514: 515: 516: 517:
518: public function getChildren()
519: {
520: return $this->children;
521: }
522:
523:
524: 525: 526: 527: 528:
529: public function render($indent = NULL)
530: {
531: $s = $this->startTag();
532:
533: if (!$this->isEmpty) {
534:
535: if ($indent !== NULL) {
536: $indent++;
537: }
538: foreach ($this->children as $child) {
539: if (is_object($child)) {
540: $s .= $child->render($indent);
541: } else {
542: $s .= $child;
543: }
544: }
545:
546:
547: $s .= $this->endTag();
548: }
549:
550: if ($indent !== NULL) {
551: return "\n" . str_repeat("\t", $indent - 1) . $s . "\n" . str_repeat("\t", max(0, $indent - 2));
552: }
553: return $s;
554: }
555:
556:
557: public function __toString()
558: {
559: try {
560: return $this->render();
561: } catch (\Throwable $e) {
562: } catch (\Exception $e) {
563: }
564: trigger_error("Exception in " . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR);
565: }
566:
567:
568: 569: 570: 571:
572: public function startTag()
573: {
574: if ($this->name) {
575: return '<' . $this->name . $this->attributes() . (static::$xhtml && $this->isEmpty ? ' />' : '>');
576:
577: } else {
578: return '';
579: }
580: }
581:
582:
583: 584: 585: 586:
587: public function endTag()
588: {
589: return $this->name && !$this->isEmpty ? '</' . $this->name . '>' : '';
590: }
591:
592:
593: 594: 595: 596: 597:
598: public function attributes()
599: {
600: if (!is_array($this->attrs)) {
601: return '';
602: }
603:
604: $s = '';
605: $attrs = $this->attrs;
606: if (isset($attrs['data']) && is_array($attrs['data'])) {
607: trigger_error('Expanded attribute "data" is deprecated.', E_USER_DEPRECATED);
608: foreach ($attrs['data'] as $key => $value) {
609: $attrs['data-' . $key] = $value;
610: }
611: unset($attrs['data']);
612: }
613:
614: foreach ($attrs as $key => $value) {
615: if ($value === NULL || $value === FALSE) {
616: continue;
617:
618: } elseif ($value === TRUE) {
619: if (static::$xhtml) {
620: $s .= ' ' . $key . '="' . $key . '"';
621: } else {
622: $s .= ' ' . $key;
623: }
624: continue;
625:
626: } elseif (is_array($value)) {
627: if (strncmp($key, 'data-', 5) === 0) {
628: $value = Json::encode($value);
629:
630: } else {
631: $tmp = NULL;
632: foreach ($value as $k => $v) {
633: if ($v != NULL) {
634:
635: $tmp[] = $v === TRUE ? $k : (is_string($k) ? $k . ':' . $v : $v);
636: }
637: }
638: if ($tmp === NULL) {
639: continue;
640: }
641:
642: $value = implode($key === 'style' || !strncmp($key, 'on', 2) ? ';' : ' ', $tmp);
643: }
644:
645: } elseif (is_float($value)) {
646: $value = rtrim(rtrim(number_format($value, 10, '.', ''), '0'), '.');
647:
648: } else {
649: $value = (string) $value;
650: }
651:
652: $q = strpos($value, '"') === FALSE ? '"' : "'";
653: $s .= ' ' . $key . '=' . $q
654: . str_replace(
655: ['&', $q, '<'],
656: ['&', $q === '"' ? '"' : ''', self::$xhtml ? '<' : '<'],
657: $value
658: )
659: . (strpos($value, '`') !== FALSE && strpbrk($value, ' <>"\'') === FALSE ? ' ' : '')
660: . $q;
661: }
662:
663: $s = str_replace('@', '@', $s);
664: return $s;
665: }
666:
667:
668: 669: 670:
671: public function __clone()
672: {
673: foreach ($this->children as $key => $value) {
674: if (is_object($value)) {
675: $this->children[$key] = clone $value;
676: }
677: }
678: }
679:
680: }
681: