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