Completed
Push — master ( 3bc63e...e3814f )
by Fenz
02:51
created

TagNode::parsePrivateAttributes()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 3
nop 0
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Htsl\Parser\Node;
4
5
use Htsl\Htsl;
6
use Htsl\Helper;
7
use Htsl\ReadingBuffer\Line;
8
use Htsl\Parser\Node\Contracts\ANode;
9
use ArrayAccess;
10
11
////////////////////////////////////////////////////////////////
12
13
/**
14
 * @property-read string $embed            Whether this is embedding node and embeding type.
15
 * @property-read string $attributesString Attribute string with HTML syntax.
16
 */
17
class TagNode extends ANode implements ArrayAccess
18
{
19
	/**
20
	 * Name of this node.
21
	 *
22
	 * @access private
23
	 *
24
	 * @var string
25
	 */
26
	private $name;
27
28
	/**
29
	 * The html tag name of this node.
30
	 *
31
	 * @access private
32
	 *
33
	 * @var string
34
	 */
35
	private $tagName;
36
37
	/**
38
	 * Whether the html tag is empty.
39
	 *
40
	 * @access private
41
	 *
42
	 * @var bool
43
	 */
44
	private $isEmpty;
45
46
	/**
47
	 * The attributes of this node.
48
	 *
49
	 * @access private
50
	 *
51
	 * @var array
52
	 */
53
	private $attributes=[];
54
55
	/**
56
	 * Real constructor.
57
	 *
58
	 * @access protected
59
	 *
60
	 * @return \Htsl\Parser\Node\Contracts\ANode
61
	 */
62
	protected function construct():parent
63
	{
64
65
		$name= $this->line->pregGet('/(?<=^-)[\w-:]+/');
66
		$this->name=$name;
67
68
		$this->loadConfig($name,$this->document);
69
70
		$this->tagName=$this->config['name']??$name;
71
		$this->isEmpty= $this->line->getChar(-1)==='/' || $this->document->getConfig('empty_tags',$this->tagName);
72
		isset($this->config['default_attributes']) and array_walk($this->config['default_attributes'],function( $value,$key ){ $this->setAttribute($key,$value); });
73
74
		return $this;
75
	}
76
77
	/**
78
	 * Opening this tag node, and returning node opener.
79
	 *
80
	 * @access public
81
	 *
82
	 * @return string
83
	 */
84
	public function open():string
85
	{
86
		if( isset($this->config['opener']) )
87
			{ return $this->config['opener']; }
88
89
		$this->parsePrivateAttributes();
90
		$this->parseCommonAttributes();
91
92
		if( isset($this->config['in_scope']) && isset($this->config['scope_function']) && is_callable($this->config['scope_function']) )
93
			{ $this->config['scope_function']->call($this,$this->document->scope); }
94
95
		$finisher= $this->isEmpty ? ' />' : '>';
96
97
		return "<{$this->tagName}{$this->attributesString}{$finisher}";
98
	}
99
100
	/**
101
	 * Close this tag node, and returning node closer.
102
	 *
103
	 * @access public
104
	 *
105
	 * @param  \Htsl\ReadingBuffer\Line   $closerLine  The line when node closed.
106
	 *
107
	 * @return string
108
	 */
109
	public function close( Line$closerLine ):string
110
	{
111
		return $this->isEmpty ? '' : $this->config['closer']??"</{$this->tagName}>";
112
	}
113
114
	/**
115
	 * Getting whether this is embedding node and embeding type.
116
	 *
117
	 * @access public
118
	 *
119
	 * @return string
120
	 */
121
	public function getEmbed():string
122
	{
123
		return $this->config['embedding']??'';
124
	}
125
126
	/**
127
	 * Getting whether this node contains a scope and scope name.
128
	 *
129
	 * @access public
130
	 *
131
	 * @return string | null
132
	 */
133
	public function getScope()
134
	{
135
		return $this->config['scope']??null;
136
	}
137
138
	/**
139
	 * Parsing node parameters if needed.
140
	 *
141
	 * @access protected
142
	 *
143
	 * @return \Htsl\Parser\Node\TagNode
144
	 */
145
	protected function parsePrivateAttributes():self
146
	{
147
		foreach( self::PRIVATE_ATTRIBUTES as $attribute ){
148
			isset($this->config[$attribute]) and $this->{'parse'.Helper\camel_case($attribute)}();
149
		}
150
151
		return $this;
152
	}
153
154
	/**
155
	 * Private attributes of tag.
156
	 *
157
	 * @access public
158
	 *
159
	 * @const array
160
	 *
161
	 * @todo Make this const private when php 7.1
162
	 */
163
	const PRIVATE_ATTRIBUTES= [
164
		'params',
165
		'name_value',
166
		'link',
167
		'target',
168
		'alt',
169
	];
170
171
	/**
172
	 * Parsing node parameters if needed.
173
	 *
174
	 * @access protected
175
	 *
176
	 * @return \Htsl\Parser\Node\TagNode
177
	 */
178
	protected function parseParams():self
179
	{
180
		$params= preg_split('/(?<!\\\\)\\|/',$this->line->pregGet('/^-[\w-:]+\((.*?)\)(?= |(\\{>)?$)/',1));
181
182
		if( ($m= count($params)) != ($n= count($this->config['params'])) ){$this->document->throw("Tag $this->name has $n parameters $m given.");}
183
184
		array_map(function( $key, $value ){return $this->setAttribute($key,str_replace('\\|','|',$value));},$this->config['params'],$params);
185
186
		return $this;
187
	}
188
189
	/**
190
	 * Parsing <name|value> attributes.
191
	 *
192
	 * @access protected
193
	 *
194
	 * @return \Htsl\Parser\Node\TagNode
195
	 */
196
	protected function parseNameValue():self
197
	{
198
		$params= $this->line->pregGet('/ <(.*?)>(?= |$)/',1)
199
		 and $params= preg_split('/(?<!\\\\)\\|/',$params)
200
		  and array_map(function( $key, $value ){return isset($key)&&isset($value) ? $this->setAttribute($key,$this->checkExpression(str_replace('\\|','|',$value))) : '';},$this->config['name_value'],$params);
201
202
		return $this;
203
	}
204
205
	/**
206
	 * Getting tag section with given leader.
207
	 *
208
	 * @access protected
209
	 *
210
	 * @param string $leader
211
	 * @param boool  $allowSpace
212
	 *
213
	 * @return string
214
	 */
215
	protected function sectionLedBy( string$leader, bool$allowSpace=false ):string
216
	{
217
		return $this->line->pregGet(...[
218
			(
219
				'/ '.preg_quote($leader)
220
				.
221
				($allowSpace?
222
					'((?!\()(?:[^ ]| (?=[a-zA-Z0-9]))+'
223
					:
224
					'([^ ]+'
225
				)
226
				.
227
				'|(?<exp>\((?:[^()]+|(?&exp)?)+?\)))(?= |$)/'
228
			),
229
			1,
230
		]);
231
	}
232
233
	/**
234
	 * Parsing @links.
235
	 *
236
	 * @access protected
237
	 *
238
	 * @return \Htsl\Parser\Node\TagNode
239
	 */
240
	protected function parseLink():self
241
	{
242
		return $this->setMultinameAttribute('link',$this->sectionLedBy('@',true),function( string$link ){
243
			if( isset($this->config['target']) && ':'===$link{0} ){
244
				return 'javascript'.$link;
245
			}elseif( '//'===($firstTwoLetters=substr($link,0,2)) ){
246
				return 'http:'.$link;
247
			}elseif( '\\\\'===$firstTwoLetters ){
248
				return 'https://'.substr($link,2);
249
			}else{
250
				return $this->checkExpression($link);
251
			}
252
		});
253
	}
254
255
	/**
256
	 * Parsing >target.
257
	 *
258
	 * @access protected
259
	 *
260
	 * @return \Htsl\Parser\Node\TagNode
261
	 */
262
	protected function parseTarget():self
263
	{
264
		return $this->setMultinameAttribute('target',$this->sectionLedBy('>',true));
265
	}
266
267
	/**
268
	 * Parsing _placeholders.
269
	 *
270
	 * @access protected
271
	 *
272
	 * @return \Htsl\Parser\Node\TagNode
273
	 */
274
	protected function parseAlt():self
275
	{
276
		return $this->setMultinameAttribute('alt',$this->sectionLedBy('_',true));
277
	}
278
279
	/**
280
	 * Setting attribute whitch has same name in HTSL but different name in HTML.
281
	 *
282
	 * @access private
283
	 *
284
	 * @param string        $name      Attribute name of HTSL
285
	 * @param string        $value     Attribute value
286
	 * @param callable|null $processer
287
	 *
288
	 * @return \Htsl\Parser\Node\TagNode
289
	 */
290
	private function setMultinameAttribute( string$name, string$value, callable$processer=null ):self
291
	{
292
		if( strlen($value) && !empty($this->config[$name]) ){
293
			return $this->setAttribute($this->config[$name],call_user_func($processer??[$this,'checkExpression',],$value));
294
		}
295
296
		return $this;
297
	}
298
299
	/**
300
	 * Parsing #ids .classes ^titles [styles] %event{>listeners<} and {other attributes}
301
	 *
302
	 * @access protected
303
	 *
304
	 * @return string
305
	 */
306
	protected function parseCommonAttributes():string
307
	{
308
		$attributes= '';
309
310
		$id= $this->sectionLedBy('#')
311
		 and $this->setAttribute('id',$id);
312
313
		$classes= $this->line->pregGet('/ \.[^ ]+(?= |$)/')
314
		 and preg_match_all('/\.((?(?!\()[^.]+|(?<exp>\((?:[^()]+|(?&exp)?)+?\))))/',$classes,$matches)
315
		  and $classes= implode(' ',array_filter(array_map(function( $className ){return $this->checkExpression($className);},$matches[1])))
316
		   and $this->setAttribute('class',$classes);
317
318
		$title= $this->sectionLedBy('^',true)
319
		 and $this->setAttribute('title',$title);
320
321
		$style= $this->line->pregGet('/ \[([^\]]+;)(?=\]( |$))/',1)
322
		 and $this->setAttribute('style',$style);
323
324
		$eventListeners= $this->line->pregMap('/ %(\w+)\{>(.*?)<\}(?= |$)/',function( $string, $name, $code ){
325
			$this->setAttribute('on'.$name,str_replace('"','&quot;',$code));
326
		})
327
		 and implode('',$eventListeners);
328
329
		$other= $this->line->pregGet('/(?<=\{).*?(?=;\}( |$))/')
330
		 and array_map(function( $keyValue ){
331
			preg_replace_callback('/^([\w-:]+)(?:\?(.+?))?(?:\=(.*))?$/',function($matches){
332
				$this->setAttribute($matches[1],($matches[3]??$matches[1])?:$matches[1],$matches[2]??null);
333
			},$keyValue);
334
		},explode(';',$other));
335
336
		return $attributes;
337
	}
338
339
	/**
340
	 * Checking and parse PHP expressions.
341
	 *
342
	 * @access protected
343
	 *
344
	 * @param  string $value
345
	 *
346
	 * @return string
347
	 */
348
	protected function checkExpression( string$value ):string
349
	{
350
		return preg_match('/^\(.*\)$/',$value) ? '<?=str_replace(\'"\',\'&quot;\','.substr($value,1,-1).')?>' : str_replace('"','&quot;',$value);
351
	}
352
353
	/**
354
	 * Getting attribute string with HTML syntax.
355
	 *
356
	 * @access protected
357
	 *
358
	 * @return string
359
	 */
360
	protected function getAttributesString():string
361
	{
362
		ksort($this->attributes);
363
		return implode('',array_map(static function( string$key, array$data ){
364
			return (isset($data['condition'])&&strlen($data['condition'])?
365
				"<?php if( {$data['condition']} ){?> $key=\"{$data['value']}\"<?php }?>"
366
				:
367
				" $key=\"{$data['value']}\""
368
			);
369
		},array_keys($this->attributes),$this->attributes));
370
	}
371
372
	/**
373
	 * Setting attribute.
374
	 *
375
	 * @access protected
376
	 *
377
	 * @param string      $key       Attribute name.
378
	 * @param string      $value     Attribute value
379
	 * @param string|null $condition Optional condition, If given, attribute will seted only when condition is true.
380
	 */
381
	protected function setAttribute( string$key, string$value, string$condition=null ):self
382
	{
383
		if( isset($this->attributes[$key]) )
384
			{ $this->document->throw("Attribute $key of $this->name cannot redeclare."); }
385
386
		$this->attributes[$key]=[
387
			'value'=> $value,
388
			'condition'=> $condition,
389
		];
390
391
		return $this;
392
	}
393
394
395
	/*             *\
396
	   ArrayAccess
397
	\*             */
398
399
	/**
400
	 * Whether the attribute isset.
401
	 *
402
	 * @access public
403
	 *
404
	 * @param  mixed $offset
405
	 *
406
	 * @return bool
407
	 */
408
	public function offsetExists( $offset ):bool
409
	{
410
		return isset($this->attributes[$offset]);
411
	}
412
413
	/**
414
	 * Getting attribute with array access.
415
	 *
416
	 * @access public
417
	 *
418
	 * @param  mixed $offset
419
	 *
420
	 * @return mixed
421
	 */
422
	public function offsetGet( $offset )
423
	{
424
		return $this->attributes[$offset]??null;
425
	}
426
427
	/**
428
	 * Setting Attribute with array access.
429
	 *
430
	 * @access public
431
	 *
432
	 * @param  mixed $offset
433
	 * @param  mixed $value
434
	 */
435
	public function offsetSet( $offset, $value )
436
	{
437
		$this->setAttribute($offset,$value);
438
	}
439
440
	/**
441
	 * Unset an attribute with array access.
442
	 *
443
	 * @access public
444
	 *
445
	 * @param  mixed $offset
446
	 */
447
	public function offsetUnset( $offset )
448
	{
449
		if( isset($this->attributes[$offset]) )
450
			{ unset($this->attributes[$offset]); }
451
	}
452
}
453