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