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