· 9 years ago · Oct 15, 2016, 09:20 AM
1<?php
2
3/**
4 * @method Tags a()
5 * @method Tags href()
6 * @method Tags target()
7 * @method Tags style()
8 * @method Tags div()
9 * @method Tags input()
10 * @method Tags value()
11 * @method Tags required()
12 * @method Tags p()
13 * @method Tags bold()
14 * @method Tags i()
15 * @method Tags em()
16 * @method Tags option()
17 * @method Tags checked
18 * @method Tags optgroup
19 * @method Tags label
20 * @method Tags ul
21 * @method Tags nl
22 * @method Tags li
23 * @method Tags table
24 * @method Tags tr
25 * @method Tags th
26 * @method Tags td
27 * @method Tags span
28 * @method Tags dl
29 * @method Tags dd
30 * @method Tags dt
31 * @method Tags h1
32 * @method Tags h2
33 * @method Tags h3
34 * @method Tags h4
35 * @method Tags form
36 * @method Tags action
37 * @method Tags method
38 * @method Tags strong
39 */
40class Tags
41{
42 /** @var null name of tag, such as span */
43 protected $tagName = null;
44
45 /** @var array array of contents */
46 protected $contents = array();
47
48 /** @var array array of attributes */
49 public $attributes = array();
50
51 /** @var bool for form element's name */
52 protected $multiple = false;
53
54 /** @var array normalize tag name */
55 public static $normalize_tag = array(
56 'b' => 'strong',
57 'bold' => 'strong',
58 'italic' => 'i',
59 'image' => 'img',
60 'item' => 'li',
61 'order' => 'ol',
62 'number' => 'nl',
63 );
64 /** @var array tags without contents */
65 public static $tag_no_body = array(
66 'br', 'img', 'input',
67 );
68 /** @var array in-line tags */
69 public static $tag_span = array(
70 'span', 'p', 'strong', 'i', 'sub', 'li', 'a', 'label',
71 );
72 /** @var array how to connect attribute values */
73 public static $attribute_connectors = array(
74 'class' => ' ',
75 'style' => '; ',
76 );
77 /** @var string encoding */
78 public static $encoding = 'UTF-8';
79
80 /** @var bool true for tags such as <img /> */
81 public $noBodyTag = false;
82 // +----------------------------------------------------------------------+
83 // constructions and static methods
84 // +----------------------------------------------------------------------+
85 /**
86 * Start Tag object, with or without tag name.
87 *
88 * @param null $tagName
89 * @param null $contents
90 * @return Tags
91 */
92 public function __invoke( $tagName=NULL, $contents=NULL ) {
93 return $this->_( $tagName, $contents );
94 }
95
96 /**
97 * construction of Tag object.
98 *
99 * @param string|null $tagName
100 * @param null|string $contents
101 * @return Tags
102 */
103 public function __construct( $tagName=null, $contents=null )
104 {
105 $this->setTagName_( $tagName );
106 $this->setContents_( $contents );
107 }
108
109 /**
110 * @param string|null $tagName
111 * @param null|string $contents
112 * @return Tags
113 */
114 public function _( $tagName=NULL, $contents=NULL )
115 {
116 $class = get_called_class();
117 return new $class( $tagName, $contents );
118 }
119
120 /**
121 * set attribute, or tagName if tagName is not set.
122 *
123 * @param string $name
124 * @param array $args
125 * @return Tags
126 */
127 public function __call( $name, $args )
128 {
129 // attribute or tag if not set.
130 if( is_null( $this->tagName ) ) { // set it as a tag name
131 return $this->_( $name, $args );
132 }
133 else {
134 $this->setAttribute_( $name, $args );
135 }
136 return $this;
137 }
138
139 /**
140 * make string VERY safe for html.
141 *
142 * @param string $value
143 * @return string
144 */
145 public static function safe_( $value ) {
146 if( empty( $value ) ) return $value;
147 return htmlentities( $value, ENT_QUOTES, static::$encoding );
148 }
149
150 /**
151 * wrap value with closure. use this to avoid encoding attribute values.
152 *
153 * @param string $value
154 * @return callable
155 */
156 public static function wrap_( $value ) {
157 return function() use( $value ) { return $value; };
158 }
159
160 public function isSpanTag() {
161 return in_array( $this->tagName, static::$tag_span );
162 }
163
164 public function isNoBodyTag() {
165 return $this->noBodyTag;
166 }
167 // +----------------------------------------------------------------------+
168 // mostly internal functions
169 // +----------------------------------------------------------------------+
170 /**
171 * set tag name.
172 *
173 * @param string $tagName
174 * @return Tags
175 */
176 protected function setTagName_( $tagName )
177 {
178 if( empty( $tagName ) ) return $this;
179 $tagName = $this->normalize_( $tagName );
180 if( array_key_exists( $tagName, static::$normalize_tag ) ) {
181 $tagName = static::$normalize_tag[ $tagName ];
182 }
183 $this->tagName = $tagName;
184 if( in_array( $this->tagName, static::$tag_no_body ) ) {
185 $this->noBodyTag = true;
186 }
187 return $this;
188 }
189
190 /**
191 * set contents.
192 *
193 * @param string|array|Tags $contents
194 * @return Tags
195 */
196 protected function setContents_( $contents ) {
197 if( empty( $contents ) ) return $this;
198 if( is_array( $contents ) ) {
199 $this->contents = array_merge( $this->contents, $contents );
200 }
201 else {
202 $this->contents[] = $contents;
203 }
204 return $this;
205 }
206
207 /**
208 * set attribute. if connector is not set, attribute is replaced.
209 *
210 * @param string $name
211 * @param string|array $value
212 * @param bool|string $connector
213 * @return Tags
214 */
215 protected function setAttribute_( $name, $value, $connector=null )
216 {
217 if( is_array( $value ) && !empty( $value ) ) {
218 foreach( $value as $val ) {
219 $this->setAttribute_( $name, $val, $connector );
220 }
221 return $this;
222 }
223 elseif( is_array( $value ) ) {
224 $value = '';
225 }
226 if( $value === false ) return $this; // ignore the property.
227 $name = $this->normalize_( $name );
228 if( $value === true ) $value = $name; // same as name (checked="checked")
229 // set connector if it is not set.
230 if( $connector === null ) {
231 $connector = false; // default is to replace value.
232 if( array_key_exists( $name, static::$attribute_connectors ) ) {
233 $connector = static::$attribute_connectors[ $name ];
234 }
235 }
236 // set attribute.
237 if( !isset( $this->attributes[ $name ] ) // new attribute.
238 || $connector === false ) { // replace with new value.
239 $this->attributes[ $name ] = $value;
240 }
241 else { // attribute is appended.
242 $this->attributes[ $name ] .= $connector . $value;
243 }
244 return $this;
245 }
246
247 /**
248 * normalize tag and attribute name: lower case, and remove first _ if exists.
249 *
250 * @param string $name
251 * @return string
252 */
253 protected function normalize_( $name ) {
254 $name = strtolower( $name );
255 if( $name[0]=='_') $name = substr( $name, 1 );
256 $name = str_replace( '_', '-', $name );
257 return $name;
258 }
259 // +----------------------------------------------------------------------+
260 // methods for setting tags, attributes, and contents.
261 // +----------------------------------------------------------------------+
262 /**
263 * set contents.
264 *
265 * @internal param array|string|Tags $contents
266 * @return Tags
267 */
268 public function contain_()
269 {
270 /** @var $args array */
271 $args = func_get_args();
272 return $this->setContents_( $args );
273 }
274
275 /**
276 * set class name. adds to the existing class.
277 *
278 * @param string $class
279 * @param string $connector set FALSE to reset class.
280 * @return Tags
281 */
282 public function _class( $class, $connector=' ' ) {
283 return $this->setAttribute_( 'class', $class, $connector );
284 }
285
286 /**
287 * set style. adds to the existing style.
288 *
289 * @param string $style
290 * @param string $connector set FALSE to reset style.
291 * @return Tags
292 */
293 public function _style( $style, $connector='; ' ) {
294 return $this->setAttribute_( 'style', $style, $connector );
295 }
296
297 /**
298 * @param \Closure $func
299 * @param string $attribute
300 */
301 public function walk( $func, $attribute=null )
302 {
303 if( !$attribute || $this->$attribute || isset( $this->attributes[ $attribute ] ) ) {
304 $func( $this );
305 }
306 if( !empty( $this->contents ) ) {
307 foreach( $this->contents as $content ) {
308 if( $content instanceof self ) {
309 $content->walk( $func, $attribute );
310 }
311 }
312 }
313 }
314 // +----------------------------------------------------------------------+
315 // convert Tags to a string.
316 // +----------------------------------------------------------------------+
317 /**
318 * @param string $head
319 * @return string
320 */
321 protected function toContents_( $head="" ) {
322 $html = '';
323 if( empty( $this->contents ) ) return $html;
324 foreach( $this->contents as $content ) {
325 if( !$this->isNoBodyTag() && !$this->isSpanTag() && $html && substr( $html, -1 ) != "\n" ) {
326 $html .= "\n";
327 }
328 if( is_object( $content ) && method_exists( $content, 'toString_' ) ) {
329 $html .= $content->toString_( $head );
330 }
331 else {
332 $html .= $head . (string) $content;
333 }
334 }
335 return $html;
336 }
337
338 /**
339 * @return string
340 */
341 protected function toAttribute_() {
342 $attr = '';
343 if( !empty( $this->attributes ) )
344 foreach( $this->attributes as $name => $value ) {
345 if( $value instanceof \Closure ) {
346 $value = $value(); // wrapped by closure. use it as is.
347 }
348 else {
349 $value = static::safe_( $value ); // make it very safe.
350 }
351 $attr .= " {$name}=\"{$value}\"";
352 }
353 return $attr;
354 }
355
356 /**
357 * @param string $head
358 * @return string
359 */
360 protected function toString_( $head='' )
361 {
362 $html = $head;
363 if( $this->isNoBodyTag() ) {
364 // create tag without content, such as <tag attributes... />
365 $html .= "<{$this->tagName}" . $this->toAttribute_() . ' />';
366 }
367 elseif( $this->isSpanTag() || count( $this->contents ) == 1 ) {
368 // short tag such as <tag>only one content</tag>
369 $html .= "<{$this->tagName}" . $this->toAttribute_() . ">";
370 $html .= $this->toContents_();
371 $html .= "</{$this->tagName}>";
372 }
373 else { // create tag with contents inside.
374 $html .= "<{$this->tagName}" . $this->toAttribute_() . ">";
375 $html .= "\n";
376 $html .= $this->toContents_( $head . ' ' );
377 if( substr( $html, -1 ) != "\n" ) $html .= "\n";
378 $html .= $head . "</{$this->tagName}>";
379 }
380 if( !$this->isSpanTag() && !$this->isNoBodyTag() ) {
381 // add new-line, except for in-line tags.
382 $html .= "\n";
383 }
384 return $html;
385 }
386 /**
387 * @return string
388 */
389 public function __toString()
390 {
391 return $this->toString_();
392 }
393 // +----------------------------------------------------------------------+
394}