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,'track'=>1);
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 (NStrings::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: final public function setName($name, $isEmpty = NULL)
88: {
89: if ($name !== NULL && !is_string($name)) {
90: throw new InvalidArgumentException("Name must be string or NULL, " . gettype($name) ." given.");
91: }
92:
93: $this->name = $name;
94: $this->isEmpty = $isEmpty === NULL ? isset(self::$emptyElements[$name]) : (bool) $isEmpty;
95: return $this;
96: }
97:
98:
99: 100: 101: 102:
103: final public function getName()
104: {
105: return $this->name;
106: }
107:
108:
109: 110: 111: 112:
113: final 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 = $attrs + $this->attrs;
127: return $this;
128: }
129:
130:
131: 132: 133: 134: 135: 136:
137: final public function __set($name, $value)
138: {
139: $this->attrs[$name] = $value;
140: }
141:
142:
143: 144: 145: 146: 147:
148: final public function &__get($name)
149: {
150: return $this->attrs[$name];
151: }
152:
153:
154: 155: 156: 157: 158:
159: final public function __isset($name)
160: {
161: return isset($this->attrs[$name]);
162: }
163:
164:
165: 166: 167: 168: 169:
170: final public function __unset($name)
171: {
172: unset($this->attrs[$name]);
173: }
174:
175:
176: 177: 178: 179: 180: 181:
182: final 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: final 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: 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: final public function getHtml()
263: {
264: $s = '';
265: foreach ($this->children as $child) {
266: if (is_object($child)) {
267: $s .= $child->render();
268: } else {
269: $s .= $child;
270: }
271: }
272: return $s;
273: }
274:
275:
276: 277: 278: 279: 280: 281:
282: final public function setText($text)
283: {
284: if (!is_array($text)) {
285: $text = htmlspecialchars((string) $text, ENT_NOQUOTES);
286: }
287: return $this->setHtml($text);
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: final public function add($child)
307: {
308: return $this->insert(NULL, $child);
309: }
310:
311:
312: 313: 314: 315: 316: 317:
318: final public function create($name, $attrs = NULL)
319: {
320: $this->insert(NULL, $child = self::el($name, $attrs));
321: return $child;
322: }
323:
324:
325: 326: 327: 328: 329: 330: 331: 332:
333: public function insert($index, $child, $replace = FALSE)
334: {
335: if ($child instanceof NHtml || is_scalar($child)) {
336: if ($index === NULL) {
337: $this->children[] = $child;
338:
339: } else {
340: array_splice($this->children, (int) $index, $replace ? 1 : 0, array($child));
341: }
342:
343: } else {
344: throw new InvalidArgumentException("Child node must be scalar or Html object, " . (is_object($child) ? get_class($child) : gettype($child)) ." given.");
345: }
346:
347: return $this;
348: }
349:
350:
351: 352: 353: 354: 355: 356:
357: final public function offsetSet($index, $child)
358: {
359: $this->insert($index, $child, TRUE);
360: }
361:
362:
363: 364: 365: 366: 367:
368: final public function offsetGet($index)
369: {
370: return $this->children[$index];
371: }
372:
373:
374: 375: 376: 377: 378:
379: final public function offsetExists($index)
380: {
381: return isset($this->children[$index]);
382: }
383:
384:
385: 386: 387: 388: 389:
390: public function offsetUnset($index)
391: {
392: if (isset($this->children[$index])) {
393: array_splice($this->children, (int) $index, 1);
394: }
395: }
396:
397:
398: 399: 400: 401:
402: final public function count()
403: {
404: return count($this->children);
405: }
406:
407:
408: 409: 410: 411:
412: public function removeChildren()
413: {
414: $this->children = array();
415: }
416:
417:
418: 419: 420: 421: 422: 423:
424: final public function getIterator($deep = FALSE)
425: {
426: if ($deep) {
427: $deep = $deep > 0 ? RecursiveIteratorIterator::SELF_FIRST : RecursiveIteratorIterator::CHILD_FIRST;
428: return new RecursiveIteratorIterator(new NGenericRecursiveIterator(new ArrayIterator($this->children)), $deep);
429:
430: } else {
431: return new NGenericRecursiveIterator(new ArrayIterator($this->children));
432: }
433: }
434:
435:
436: 437: 438: 439:
440: final public function getChildren()
441: {
442: return $this->children;
443: }
444:
445:
446: 447: 448: 449: 450:
451: final public function render($indent = NULL)
452: {
453: $s = $this->startTag();
454:
455: if (!$this->isEmpty) {
456:
457: if ($indent !== NULL) {
458: $indent++;
459: }
460: foreach ($this->children as $child) {
461: if (is_object($child)) {
462: $s .= $child->render($indent);
463: } else {
464: $s .= $child;
465: }
466: }
467:
468:
469: $s .= $this->endTag();
470: }
471:
472: if ($indent !== NULL) {
473: return "\n" . str_repeat("\t", $indent - 1) . $s . "\n" . str_repeat("\t", max(0, $indent - 2));
474: }
475: return $s;
476: }
477:
478:
479: final public function __toString()
480: {
481: return $this->render();
482: }
483:
484:
485: 486: 487: 488:
489: final public function startTag()
490: {
491: if ($this->name) {
492: return '<' . $this->name . $this->attributes() . (self::$xhtml && $this->isEmpty ? ' />' : '>');
493:
494: } else {
495: return '';
496: }
497: }
498:
499:
500: 501: 502: 503:
504: final public function endTag()
505: {
506: return $this->name && !$this->isEmpty ? '</' . $this->name . '>' : '';
507: }
508:
509:
510: 511: 512: 513:
514: final public function attributes()
515: {
516: if (!is_array($this->attrs)) {
517: return '';
518: }
519:
520: $s = '';
521: foreach ($this->attrs as $key => $value) {
522: if ($value === NULL || $value === FALSE) {
523: continue;
524:
525: } elseif ($value === TRUE) {
526: if (self::$xhtml) {
527: $s .= ' ' . $key . '="' . $key . '"';
528: } else {
529: $s .= ' ' . $key;
530: }
531: continue;
532:
533: } elseif (is_array($value)) {
534: if ($key === 'data') {
535: foreach ($value as $k => $v) {
536: if ($v !== NULL && $v !== FALSE) {
537: $q = strpos($v, '"') === FALSE ? '"' : "'";
538: $s .= ' data-' . $k . '=' . $q . str_replace(array('&', $q), array('&', $q === '"' ? '"' : '''), $v) . $q;
539: }
540: }
541: continue;
542: }
543:
544: $tmp = NULL;
545: foreach ($value as $k => $v) {
546: if ($v != NULL) {
547:
548: $tmp[] = $v === TRUE ? $k : (is_string($k) ? $k . ':' . $v : $v);
549: }
550: }
551: if ($tmp === NULL) {
552: continue;
553: }
554:
555: $value = implode($key === 'style' || !strncmp($key, 'on', 2) ? ';' : ' ', $tmp);
556:
557: } else {
558: $value = (string) $value;
559: }
560:
561: $q = strpos($value, '"') === FALSE ? '"' : "'";
562: $s .= ' ' . $key . '=' . $q . str_replace(array('&', $q), array('&', $q === '"' ? '"' : '''), $value) . $q;
563: }
564:
565: $s = str_replace('@', '@', $s);
566: return $s;
567: }
568:
569:
570: 571: 572:
573: public function __clone()
574: {
575: foreach ($this->children as $key => $value) {
576: if (is_object($value)) {
577: $this->children[$key] = clone $value;
578: }
579: }
580: }
581:
582: }
583: