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