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->insert(NULL, $child);
315: }
316:
317:
318: 319: 320: 321: 322: 323:
324: public function create($name, $attrs = NULL)
325: {
326: $this->insert(NULL, $child = static::el($name, $attrs));
327: return $child;
328: }
329:
330:
331: 332: 333: 334: 335: 336: 337: 338:
339: public function insert($index, $child, $replace = FALSE)
340: {
341: if ($child instanceof self || is_scalar($child)) {
342: if ($index === NULL) {
343: $this->children[] = $child;
344:
345: } else {
346: array_splice($this->children, (int) $index, $replace ? 1 : 0, array($child));
347: }
348:
349: } else {
350: throw new Nette\InvalidArgumentException(sprintf('Child node must be scalar or Html object, %s given.', is_object($child) ? get_class($child) : gettype($child)));
351: }
352:
353: return $this;
354: }
355:
356:
357: 358: 359: 360: 361: 362:
363: public function offsetSet($index, $child)
364: {
365: $this->insert($index, $child, TRUE);
366: }
367:
368:
369: 370: 371: 372: 373:
374: public function offsetGet($index)
375: {
376: return $this->children[$index];
377: }
378:
379:
380: 381: 382: 383: 384:
385: public function offsetExists($index)
386: {
387: return isset($this->children[$index]);
388: }
389:
390:
391: 392: 393: 394: 395:
396: public function offsetUnset($index)
397: {
398: if (isset($this->children[$index])) {
399: array_splice($this->children, (int) $index, 1);
400: }
401: }
402:
403:
404: 405: 406: 407:
408: public function count()
409: {
410: return count($this->children);
411: }
412:
413:
414: 415: 416: 417:
418: public function removeChildren()
419: {
420: $this->children = array();
421: }
422:
423:
424: 425: 426: 427:
428: public function getIterator()
429: {
430: return new \ArrayIterator($this->children);
431: }
432:
433:
434: 435: 436: 437:
438: public function getChildren()
439: {
440: return $this->children;
441: }
442:
443:
444: 445: 446: 447: 448:
449: public function render($indent = NULL)
450: {
451: $s = $this->startTag();
452:
453: if (!$this->isEmpty) {
454:
455: if ($indent !== NULL) {
456: $indent++;
457: }
458: foreach ($this->children as $child) {
459: if (is_object($child)) {
460: $s .= $child->render($indent);
461: } else {
462: $s .= $child;
463: }
464: }
465:
466:
467: $s .= $this->endTag();
468: }
469:
470: if ($indent !== NULL) {
471: return "\n" . str_repeat("\t", $indent - 1) . $s . "\n" . str_repeat("\t", max(0, $indent - 2));
472: }
473: return $s;
474: }
475:
476:
477: public function __toString()
478: {
479: return $this->render();
480: }
481:
482:
483: 484: 485: 486:
487: public function startTag()
488: {
489: if ($this->name) {
490: return '<' . $this->name . $this->attributes() . (static::$xhtml && $this->isEmpty ? ' />' : '>');
491:
492: } else {
493: return '';
494: }
495: }
496:
497:
498: 499: 500: 501:
502: public function endTag()
503: {
504: return $this->name && !$this->isEmpty ? '</' . $this->name . '>' : '';
505: }
506:
507:
508: 509: 510: 511: 512:
513: public function attributes()
514: {
515: if (!is_array($this->attrs)) {
516: return '';
517: }
518:
519: $s = '';
520: foreach ($this->attrs as $key => $value) {
521: if ($value === NULL || $value === FALSE) {
522: continue;
523:
524: } elseif ($value === TRUE) {
525: if (static::$xhtml) {
526: $s .= ' ' . $key . '="' . $key . '"';
527: } else {
528: $s .= ' ' . $key;
529: }
530: continue;
531:
532: } elseif (is_array($value)) {
533: if ($key === 'data') {
534: foreach ($value as $k => $v) {
535: if ($v !== NULL && $v !== FALSE) {
536: if (is_array($v)) {
537: $v = Json::encode($v);
538: }
539: $q = strpos($v, '"') === FALSE ? '"' : "'";
540: $s .= ' data-' . $k . '='
541: . $q . str_replace(array('&', $q), array('&', $q === '"' ? '"' : '''), $v)
542: . (strpos($v, '`') !== FALSE && strpbrk($v, ' <>"\'') === FALSE ? ' ' : '')
543: . $q;
544: }
545: }
546: continue;
547:
548: } elseif (strncmp($key, 'data-', 5) === 0) {
549: $value = Json::encode($value);
550:
551: } else {
552: $tmp = NULL;
553: foreach ($value as $k => $v) {
554: if ($v != NULL) {
555:
556: $tmp[] = $v === TRUE ? $k : (is_string($k) ? $k . ':' . $v : $v);
557: }
558: }
559: if ($tmp === NULL) {
560: continue;
561: }
562:
563: $value = implode($key === 'style' || !strncmp($key, 'on', 2) ? ';' : ' ', $tmp);
564: }
565:
566: } elseif (is_float($value)) {
567: $value = rtrim(rtrim(number_format($value, 10, '.', ''), '0'), '.');
568:
569: } else {
570: $value = (string) $value;
571: }
572:
573: $q = strpos($value, '"') === FALSE ? '"' : "'";
574: $s .= ' ' . $key . '='
575: . $q . str_replace(array('&', $q), array('&', $q === '"' ? '"' : '''), $value)
576: . (strpos($value, '`') !== FALSE && strpbrk($value, ' <>"\'') === FALSE ? ' ' : '')
577: . $q;
578: }
579:
580: $s = str_replace('@', '@', $s);
581: return $s;
582: }
583:
584:
585: 586: 587:
588: public function __clone()
589: {
590: foreach ($this->children as $key => $value) {
591: if (is_object($value)) {
592: $this->children[$key] = clone $value;
593: }
594: }
595: }
596:
597: }
598: