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, IHtmlString
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(
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(' ', $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: public function __set($name, $value)
138: {
139: $this->attrs[$name] = $value;
140: }
141:
142:
143: 144: 145: 146: 147:
148: public function &__get($name)
149: {
150: return $this->attrs[$name];
151: }
152:
153:
154: 155: 156: 157: 158:
159: public function __isset($name)
160: {
161: return isset($this->attrs[$name]);
162: }
163:
164:
165: 166: 167: 168: 169:
170: public function __unset($name)
171: {
172: unset($this->attrs[$name]);
173: }
174:
175:
176: 177: 178: 179: 180: 181:
182: public function __call($m, $args)
183: {
184: $p = substr($m, 0, 3);
185: if ($p === 'get' || $p === 'set' || $p === 'add') {
186: $m = substr($m, 3);
187: $m[0] = $m[0] | "\x20";
188: if ($p === 'get') {
189: return isset($this->attrs[$m]) ? $this->attrs[$m] : NULL;
190:
191: } elseif ($p === 'add') {
192: $args[] = TRUE;
193: }
194: }
195:
196: if (count($args) === 0) {
197:
198: } elseif (count($args) === 1) {
199: $this->attrs[$m] = $args[0];
200:
201: } elseif ((string) $args[0] === '') {
202: $tmp = & $this->attrs[$m];
203:
204: } elseif (!isset($this->attrs[$m]) || is_array($this->attrs[$m])) {
205: $this->attrs[$m][$args[0]] = $args[1];
206:
207: } else {
208: $this->attrs[$m] = array($this->attrs[$m], $args[0] => $args[1]);
209: }
210:
211: return $this;
212: }
213:
214:
215: 216: 217: 218: 219: 220:
221: public function href($path, $query = NULL)
222: {
223: if ($query) {
224: $query = http_build_query($query, NULL, '&');
225: if ($query !== '') {
226: $path .= '?' . $query;
227: }
228: }
229: $this->attrs['href'] = $path;
230: return $this;
231: }
232:
233:
234: 235: 236: 237: 238: 239:
240: public function setHtml($html)
241: {
242: if (is_array($html)) {
243: throw new Nette\InvalidArgumentException(sprintf('Textual content must be a scalar, %s given.', gettype($html)));
244: }
245: $this->removeChildren();
246: $this->children[] = (string) $html;
247: return $this;
248: }
249:
250:
251: 252: 253: 254:
255: public function getHtml()
256: {
257: $s = '';
258: foreach ($this->children as $child) {
259: if (is_object($child)) {
260: $s .= $child->render();
261: } else {
262: $s .= $child;
263: }
264: }
265: return $s;
266: }
267:
268:
269: 270: 271: 272: 273: 274:
275: public function setText($text)
276: {
277: if (!is_array($text) && !$text instanceof self) {
278: $text = htmlspecialchars((string) $text, ENT_NOQUOTES, 'UTF-8');
279: }
280: return $this->setHtml($text);
281: }
282:
283:
284: 285: 286: 287:
288: public function getText()
289: {
290: return html_entity_decode(strip_tags($this->getHtml()), ENT_QUOTES, 'UTF-8');
291: }
292:
293:
294: 295: 296: 297: 298:
299: public function add($child)
300: {
301: return $this->insert(NULL, $child);
302: }
303:
304:
305: 306: 307: 308: 309: 310:
311: public function create($name, $attrs = NULL)
312: {
313: $this->insert(NULL, $child = static::el($name, $attrs));
314: return $child;
315: }
316:
317:
318: 319: 320: 321: 322: 323: 324: 325:
326: public function insert($index, $child, $replace = FALSE)
327: {
328: if ($child instanceof self || is_scalar($child)) {
329: if ($index === NULL) {
330: $this->children[] = $child;
331:
332: } else {
333: array_splice($this->children, (int) $index, $replace ? 1 : 0, array($child));
334: }
335:
336: } else {
337: throw new Nette\InvalidArgumentException(sprintf('Child node must be scalar or Html object, %s given.', is_object($child) ? get_class($child) : gettype($child)));
338: }
339:
340: return $this;
341: }
342:
343:
344: 345: 346: 347: 348: 349:
350: public function offsetSet($index, $child)
351: {
352: $this->insert($index, $child, TRUE);
353: }
354:
355:
356: 357: 358: 359: 360:
361: public function offsetGet($index)
362: {
363: return $this->children[$index];
364: }
365:
366:
367: 368: 369: 370: 371:
372: public function offsetExists($index)
373: {
374: return isset($this->children[$index]);
375: }
376:
377:
378: 379: 380: 381: 382:
383: public function offsetUnset($index)
384: {
385: if (isset($this->children[$index])) {
386: array_splice($this->children, (int) $index, 1);
387: }
388: }
389:
390:
391: 392: 393: 394:
395: public function count()
396: {
397: return count($this->children);
398: }
399:
400:
401: 402: 403: 404:
405: public function removeChildren()
406: {
407: $this->children = array();
408: }
409:
410:
411: 412: 413: 414:
415: public function getIterator()
416: {
417: if (func_num_args() && func_get_arg(0)) {
418: throw new Nette\DeprecatedException(__METHOD__ . " doesn't support deep iterator any more.");
419: }
420: return new \ArrayIterator($this->children);
421: }
422:
423:
424: 425: 426: 427:
428: public function getChildren()
429: {
430: return $this->children;
431: }
432:
433:
434: 435: 436: 437: 438:
439: public function render($indent = NULL)
440: {
441: $s = $this->startTag();
442:
443: if (!$this->isEmpty) {
444:
445: if ($indent !== NULL) {
446: $indent++;
447: }
448: foreach ($this->children as $child) {
449: if (is_object($child)) {
450: $s .= $child->render($indent);
451: } else {
452: $s .= $child;
453: }
454: }
455:
456:
457: $s .= $this->endTag();
458: }
459:
460: if ($indent !== NULL) {
461: return "\n" . str_repeat("\t", $indent - 1) . $s . "\n" . str_repeat("\t", max(0, $indent - 2));
462: }
463: return $s;
464: }
465:
466:
467: public function __toString()
468: {
469: return $this->render();
470: }
471:
472:
473: 474: 475: 476:
477: public function startTag()
478: {
479: if ($this->name) {
480: return '<' . $this->name . $this->attributes() . (static::$xhtml && $this->isEmpty ? ' />' : '>');
481:
482: } else {
483: return '';
484: }
485: }
486:
487:
488: 489: 490: 491:
492: public function endTag()
493: {
494: return $this->name && !$this->isEmpty ? '</' . $this->name . '>' : '';
495: }
496:
497:
498: 499: 500: 501: 502:
503: public function attributes()
504: {
505: if (!is_array($this->attrs)) {
506: return '';
507: }
508:
509: $s = '';
510: $attrs = $this->attrs;
511: if (isset($attrs['data']) && is_array($attrs['data'])) {
512: foreach ($attrs['data'] as $key => $value) {
513: $attrs['data-' . $key] = $value;
514: }
515: unset($attrs['data']);
516: }
517:
518: foreach ($attrs as $key => $value) {
519: if ($value === NULL || $value === FALSE) {
520: continue;
521:
522: } elseif ($value === TRUE) {
523: if (static::$xhtml) {
524: $s .= ' ' . $key . '="' . $key . '"';
525: } else {
526: $s .= ' ' . $key;
527: }
528: continue;
529:
530: } elseif (is_array($value)) {
531: if (strncmp($key, 'data-', 5) === 0) {
532: $value = Json::encode($value);
533:
534: } else {
535: $tmp = NULL;
536: foreach ($value as $k => $v) {
537: if ($v != NULL) {
538:
539: $tmp[] = $v === TRUE ? $k : (is_string($k) ? $k . ':' . $v : $v);
540: }
541: }
542: if ($tmp === NULL) {
543: continue;
544: }
545:
546: $value = implode($key === 'style' || !strncmp($key, 'on', 2) ? ';' : ' ', $tmp);
547: }
548:
549: } elseif (is_float($value)) {
550: $value = rtrim(rtrim(number_format($value, 10, '.', ''), '0'), '.');
551:
552: } else {
553: $value = (string) $value;
554: }
555:
556: $q = strpos($value, '"') === FALSE ? '"' : "'";
557: $s .= ' ' . $key . '=' . $q
558: . str_replace(
559: array('&', $q, '<'),
560: array('&', $q === '"' ? '"' : ''', self::$xhtml ? '<' : '<'),
561: $value
562: )
563: . (strpos($value, '`') !== FALSE && strpbrk($value, ' <>"\'') === FALSE ? ' ' : '')
564: . $q;
565: }
566:
567: $s = str_replace('@', '@', $s);
568: return $s;
569: }
570:
571:
572: 573: 574:
575: public function __clone()
576: {
577: foreach ($this->children as $key => $value) {
578: if (is_object($value)) {
579: $this->children[$key] = clone $value;
580: }
581: }
582: }
583:
584: }
585: