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