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