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