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 = FALSE;
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 = array_merge($this->attrs, $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 (is_array($html)) {
240: throw new Nette\InvalidArgumentException(sprintf('Textual content must be a scalar, %s given.', gettype($html)));
241: }
242: $this->removeChildren();
243: $this->children[] = (string) $html;
244: return $this;
245: }
246:
247:
248: 249: 250: 251:
252: public function getHtml()
253: {
254: $s = '';
255: foreach ($this->children as $child) {
256: if (is_object($child)) {
257: $s .= $child->render();
258: } else {
259: $s .= $child;
260: }
261: }
262: return $s;
263: }
264:
265:
266: 267: 268: 269: 270: 271:
272: public function setText($text)
273: {
274: if (!is_array($text) && !$text instanceof self) {
275: $text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8');
276: }
277: return $this->setHtml($text);
278: }
279:
280:
281: 282: 283: 284:
285: public function getText()
286: {
287: return html_entity_decode(strip_tags($this->getHtml()), ENT_QUOTES, 'UTF-8');
288: }
289:
290:
291: 292: 293: 294: 295:
296: public function add($child)
297: {
298: return $this->insert(NULL, $child);
299: }
300:
301:
302: 303: 304: 305: 306: 307:
308: public function create($name, $attrs = NULL)
309: {
310: $this->insert(NULL, $child = static::el($name, $attrs));
311: return $child;
312: }
313:
314:
315: 316: 317: 318: 319: 320: 321: 322:
323: public function insert($index, $child, $replace = FALSE)
324: {
325: if ($child instanceof Html || is_scalar($child)) {
326: if ($index === NULL) {
327: $this->children[] = $child;
328:
329: } else {
330: array_splice($this->children, (int) $index, $replace ? 1 : 0, array($child));
331: }
332:
333: } else {
334: throw new Nette\InvalidArgumentException(sprintf('Child node must be scalar or Html object, %s given.', is_object($child) ? get_class($child) : gettype($child)));
335: }
336:
337: return $this;
338: }
339:
340:
341: 342: 343: 344: 345: 346:
347: public function offsetSet($index, $child)
348: {
349: $this->insert($index, $child, TRUE);
350: }
351:
352:
353: 354: 355: 356: 357:
358: public function offsetGet($index)
359: {
360: return $this->children[$index];
361: }
362:
363:
364: 365: 366: 367: 368:
369: public function offsetExists($index)
370: {
371: return isset($this->children[$index]);
372: }
373:
374:
375: 376: 377: 378: 379:
380: public function offsetUnset($index)
381: {
382: if (isset($this->children[$index])) {
383: array_splice($this->children, (int) $index, 1);
384: }
385: }
386:
387:
388: 389: 390: 391:
392: public function count()
393: {
394: return count($this->children);
395: }
396:
397:
398: 399: 400: 401:
402: public function removeChildren()
403: {
404: $this->children = array();
405: }
406:
407:
408: 409: 410: 411: 412: 413:
414: public function getIterator($deep = FALSE)
415: {
416: if ($deep) {
417: $deep = $deep > 0 ? \RecursiveIteratorIterator::SELF_FIRST : \RecursiveIteratorIterator::CHILD_FIRST;
418: return new \RecursiveIteratorIterator(new Nette\Iterators\Recursor(new \ArrayIterator($this->children)), $deep);
419:
420: } else {
421: return new Nette\Iterators\Recursor(new \ArrayIterator($this->children));
422: }
423: }
424:
425:
426: 427: 428: 429:
430: public function getChildren()
431: {
432: return $this->children;
433: }
434:
435:
436: 437: 438: 439: 440:
441: public function render($indent = NULL)
442: {
443: $s = $this->startTag();
444:
445: if (!$this->isEmpty) {
446:
447: if ($indent !== NULL) {
448: $indent++;
449: }
450: foreach ($this->children as $child) {
451: if (is_object($child)) {
452: $s .= $child->render($indent);
453: } else {
454: $s .= $child;
455: }
456: }
457:
458:
459: $s .= $this->endTag();
460: }
461:
462: if ($indent !== NULL) {
463: return "\n" . str_repeat("\t", $indent - 1) . $s . "\n" . str_repeat("\t", max(0, $indent - 2));
464: }
465: return $s;
466: }
467:
468:
469: public function __toString()
470: {
471: return $this->render();
472: }
473:
474:
475: 476: 477: 478:
479: public function startTag()
480: {
481: if ($this->name) {
482: return '<' . $this->name . $this->attributes() . (static::$xhtml && $this->isEmpty ? ' />' : '>');
483:
484: } else {
485: return '';
486: }
487: }
488:
489:
490: 491: 492: 493:
494: public function endTag()
495: {
496: return $this->name && !$this->isEmpty ? '</' . $this->name . '>' : '';
497: }
498:
499:
500: 501: 502: 503: 504:
505: public function attributes()
506: {
507: if (!is_array($this->attrs)) {
508: return '';
509: }
510:
511: $s = '';
512: foreach ($this->attrs as $key => $value) {
513: if ($value === NULL || $value === FALSE) {
514: continue;
515:
516: } elseif ($value === TRUE) {
517: if (static::$xhtml) {
518: $s .= ' ' . $key . '="' . $key . '"';
519: } else {
520: $s .= ' ' . $key;
521: }
522: continue;
523:
524: } elseif (is_array($value)) {
525: if ($key === 'data') {
526: foreach ($value as $k => $v) {
527: if ($v !== NULL && $v !== FALSE) {
528: $q = strpos($v, '"') === FALSE ? '"' : "'";
529: $s .= ' data-' . $k . '='
530: . $q
531: . str_replace(
532: array('&', $q, '<'),
533: array('&', $q === '"' ? '"' : ''', self::$xhtml ? '<' : '<'),
534: $v
535: )
536: . (strpos($v, '`') !== FALSE && strpbrk($v, ' <>"\'') === FALSE ? ' ' : '')
537: . $q;
538: }
539: }
540: continue;
541: }
542:
543: $tmp = NULL;
544: foreach ($value as $k => $v) {
545: if ($v != NULL) {
546:
547: $tmp[] = $v === TRUE ? $k : (is_string($k) ? $k . ':' . $v : $v);
548: }
549: }
550: if ($tmp === NULL) {
551: continue;
552: }
553:
554: $value = implode($key === 'style' || !strncmp($key, 'on', 2) ? ';' : ' ', $tmp);
555:
556: } elseif (is_float($value)) {
557: $value = rtrim(rtrim(number_format($value, 10, '.', ''), '0'), '.');
558:
559: } else {
560: $value = (string) $value;
561: }
562:
563: $q = strpos($value, '"') === FALSE ? '"' : "'";
564: $s .= ' ' . $key . '='
565: . $q
566: . str_replace(
567: array('&', $q, '<'),
568: array('&', $q === '"' ? '"' : ''', self::$xhtml ? '<' : '<'),
569: $value
570: )
571: . (strpos($value, '`') !== FALSE && strpbrk($value, ' <>"\'') === FALSE ? ' ' : '')
572: . $q;
573: }
574:
575: $s = str_replace('@', '@', $s);
576: return $s;
577: }
578:
579:
580: 581: 582:
583: public function __clone()
584: {
585: foreach ($this->children as $key => $value) {
586: if (is_object($value)) {
587: $this->children[$key] = clone $value;
588: }
589: }
590: }
591:
592: }
593: