Completed
Push — master ( 8fac4c...3bc63e )
by Fenz
02:33
created

TagNode::setMultinameAttribute()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 4
nc 2
nop 3
1
<?php
2
3
namespace Htsl\Parser\Node;
4
5
use Htsl\Htsl;
6
use Htsl\ReadingBuffer\Line;
7
use Htsl\Parser\Node\Contracts\ANode;
8
use ArrayAccess;
9
10
////////////////////////////////////////////////////////////////
11
12
/**
13
 * @property-read string $embed            Whether this is embedding node and embeding type.
14
 * @property-read string $attributesString Attribute string with HTML syntax.
15
 */
16
class TagNode extends ANode implements ArrayAccess
17
{
18
	/**
19
	 * Name of this node.
20
	 *
21
	 * @access private
22
	 *
23
	 * @var string
24
	 */
25
	private $name;
26
27
	/**
28
	 * The html tag name of this node.
29
	 *
30
	 * @access private
31
	 *
32
	 * @var string
33
	 */
34
	private $tagName;
35
36
	/**
37
	 * Whether the html tag is empty.
38
	 *
39
	 * @access private
40
	 *
41
	 * @var bool
42
	 */
43
	private $isEmpty;
44
45
	/**
46
	 * The attributes of this node.
47
	 *
48
	 * @access private
49
	 *
50
	 * @var array
51
	 */
52
	private $attributes=[];
53
54
	/**
55
	 * Real constructor.
56
	 *
57
	 * @access protected
58
	 *
59
	 * @return \Htsl\Parser\Node\Contracts\ANode
60
	 */
61
	protected function construct():parent
62
	{
63
64
		$name= $this->line->pregGet('/(?<=^-)[\w-:]+/');
65
		$this->name=$name;
66
67
		$this->loadConfig($name,$this->document);
68
69
		$this->tagName=$this->config['name']??$name;
70
		$this->isEmpty= $this->line->getChar(-1)==='/' || $this->document->getConfig('empty_tags',$this->tagName);
71
		isset($this->config['default_attributes']) and array_walk($this->config['default_attributes'],function( $value,$key ){ $this->setAttribute($key,$value); });
72
73
		return $this;
74
	}
75
76
	/**
77
	 * Opening this tag node, and returning node opener.
78
	 *
79
	 * @access public
80
	 *
81
	 * @return string
82
	 */
83
	public function open():string
84
	{
85
		if( isset($this->config['opener']) )
86
			{ return $this->config['opener']; }
87
88
		if( isset($this->config['params']) )
89
			{ $this->parseParams(); }
90
91
		if( isset($this->config['name_value']) )
92
			{ $this->parseNameValue(); }
93
94
		if( isset($this->config['link']) )
95
			{ $this->parseLink(); }
96
97
		if( isset($this->config['target']) )
98
			{ $this->parseTarget(); }
99
100
		if( isset($this->config['alt']) )
101
			{ $this->parseAlt(); }
102
103
		$this->parseCommonAttributes();
104
105
		if( isset($this->config['in_scope']) && isset($this->config['scope_function']) && is_callable($this->config['scope_function']) )
106
			{ $this->config['scope_function']->call($this,$this->document->scope); }
107
108
		$finisher= $this->isEmpty ? ' />' : '>';
109
110
		return "<{$this->tagName}{$this->attributesString}{$finisher}";
111
	}
112
113
	/**
114
	 * Close this tag node, and returning node closer.
115
	 *
116
	 * @access public
117
	 *
118
	 * @param  \Htsl\ReadingBuffer\Line   $closerLine  The line when node closed.
119
	 *
120
	 * @return string
121
	 */
122
	public function close( Line$closerLine ):string
123
	{
124
		return $this->isEmpty ? '' : $this->config['closer']??"</{$this->tagName}>";
125
	}
126
127
	/**
128
	 * Getting whether this is embedding node and embeding type.
129
	 *
130
	 * @access public
131
	 *
132
	 * @return string
133
	 */
134
	public function getEmbed():string
135
	{
136
		return $this->config['embedding']??'';
137
	}
138
139
	/**
140
	 * Getting whether this node contains a scope and scope name.
141
	 *
142
	 * @access public
143
	 *
144
	 * @return string | null
145
	 */
146
	public function getScope()
147
	{
148
		return $this->config['scope']??null;
149
	}
150
151
	/**
152
	 * Parsing node parameters if needed.
153
	 *
154
	 * @access protected
155
	 *
156
	 * @return \Htsl\Parser\Node\TagNode
157
	 */
158
	protected function parseParams():self
159
	{
160
		$params= preg_split('/(?<!\\\\)\\|/',$this->line->pregGet('/^-[\w-:]+\((.*?)\)(?= |(\\{>)?$)/',1));
161
162
		if( ($m= count($params)) != ($n= count($this->config['params'])) ){$this->document->throw("Tag $this->name has $n parameters $m given.");}
163
164
		array_map(function( $key, $value ){return $this->setAttribute($key,str_replace('\\|','|',$value));},$this->config['params'],$params);
165
166
		return $this;
167
	}
168
169
	/**
170
	 * Parsing <name|value> attributes.
171
	 *
172
	 * @access protected
173
	 *
174
	 * @return \Htsl\Parser\Node\TagNode
175
	 */
176
	protected function parseNameValue():self
177
	{
178
		$params= $this->line->pregGet('/ <(.*?)>(?= |$)/',1)
179
		 and $params= preg_split('/(?<!\\\\)\\|/',$params)
180
		  and array_map(function( $key, $value ){return isset($key)&&isset($value) ? $this->setAttribute($key,$this->checkExpression(str_replace('\\|','|',$value))) : '';},$this->config['name_value'],$params);
181
182
		return $this;
183
	}
184
185
	/**
186
	 * Getting tag section with given leader.
187
	 *
188
	 * @access protected
189
	 *
190
	 * @param string $leader
191
	 * @param boool  $allowSpace
192
	 *
193
	 * @return string
194
	 */
195
	protected function sectionLedBy( string$leader, bool$allowSpace=false ):string
196
	{
197
		return $this->line->pregGet(...[
1 ignored issue
show
Documentation introduced by
array('/ ' . preg_quote(...)?)+?\\)))(?= |$)/', 1) is of type array<integer,string|int...string","1":"integer"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
198
			(
199
				'/ '.preg_quote($leader)
200
				.
201
				($allowSpace?
202
					'((?!\()(?:[^ ]| (?=[a-zA-Z0-9]))+'
203
					:
204
					'([^ ]+'
205
				)
206
				.
207
				'|(?<exp>\((?:[^()]+|(?&exp)?)+?\)))(?= |$)/'
208
			),
209
			1,
210
		]);
211
	}
212
213
	/**
214
	 * Parsing @links.
215
	 *
216
	 * @access protected
217
	 *
218
	 * @return \Htsl\Parser\Node\TagNode
219
	 */
220
	protected function parseLink():self
221
	{
222
		return $this->setMultinameAttribute('link',$this->sectionLedBy('@',true),function( string$link ){
223
			if( isset($this->config['target']) && ':'===$link{0} ){
224
				return 'javascript'.$link;
225
			}elseif( '//'===($firstTwoLetters=substr($link,0,2)) ){
226
				return 'http:'.$link;
227
			}elseif( '\\\\'===$firstTwoLetters ){
228
				return 'https://'.substr($link,2);
229
			}else{
230
				return $this->checkExpression($link);
231
			}
232
		});
233
	}
234
235
	/**
236
	 * Parsing >target.
237
	 *
238
	 * @access protected
239
	 *
240
	 * @return \Htsl\Parser\Node\TagNode
241
	 */
242
	protected function parseTarget():self
243
	{
244
		return $this->setMultinameAttribute('target',$this->sectionLedBy('>',true));
245
	}
246
247
	/**
248
	 * Parsing _placeholders.
249
	 *
250
	 * @access protected
251
	 *
252
	 * @return \Htsl\Parser\Node\TagNode
253
	 */
254
	protected function parseAlt():self
255
	{
256
		return $this->setMultinameAttribute('alt',$this->sectionLedBy('_',true));
257
	}
258
259
	/**
260
	 * Setting attribute whitch has same name in HTSL but different name in HTML.
261
	 *
262
	 * @access private
263
	 *
264
	 * @param string        $name      Attribute name of HTSL
265
	 * @param string        $value     Attribute value
266
	 * @param callable|null $processer
267
	 *
268
	 * @return \Htsl\Parser\Node\TagNode
269
	 */
270
	private function setMultinameAttribute( string$name, string$value, callable$processer=null ):self
271
	{
272
		if( strlen($value) && !empty($this->config[$name]) ){
273
			return $this->setAttribute($this->config[$name],call_user_func($processer??[$this,'checkExpression',],$value));
274
		}
275
276
		return $this;
277
	}
278
279
	/**
280
	 * Parsing #ids .classes ^titles [styles] %event{>listeners<} and {other attributes}
281
	 *
282
	 * @access protected
283
	 *
284
	 * @return string
285
	 */
286
	protected function parseCommonAttributes():string
287
	{
288
		$attributes= '';
289
290
		$id= $this->sectionLedBy('#')
291
		 and $this->setAttribute('id',$id);
292
293
		$classes= $this->line->pregGet('/ \.[^ ]+(?= |$)/')
294
		 and preg_match_all('/\.((?(?!\()[^.]+|(?<exp>\((?:[^()]+|(?&exp)?)+?\))))/',$classes,$matches)
295
		  and $classes= implode(' ',array_filter(array_map(function( $className ){return $this->checkExpression($className);},$matches[1])))
296
		   and $this->setAttribute('class',$classes);
297
298
		$title= $this->sectionLedBy('^',true)
299
		 and $this->setAttribute('title',$title);
300
301
		$style= $this->line->pregGet('/ \[([^\]]+;)(?=\]( |$))/',1)
302
		 and $this->setAttribute('style',$style);
303
304
		$eventListeners= $this->line->pregMap('/ %(\w+)\{>(.*?)<\}(?= |$)/',function( $string, $name, $code ){
305
			$this->setAttribute('on'.$name,str_replace('"','&quot;',$code));
306
		})
307
		 and implode('',$eventListeners);
308
309
		$other= $this->line->pregGet('/(?<=\{).*?(?=;\}( |$))/')
310
		 and array_map(function( $keyValue ){
311
			preg_replace_callback('/^([\w-:]+)(?:\?(.+?))?(?:\=(.*))?$/',function($matches){
312
				$this->setAttribute($matches[1],($matches[3]??$matches[1])?:$matches[1],$matches[2]??null);
313
			},$keyValue);
314
		},explode(';',$other));
315
316
		return $attributes;
317
	}
318
319
	/**
320
	 * Checking and parse PHP expressions.
321
	 *
322
	 * @access protected
323
	 *
324
	 * @param  string $value
325
	 *
326
	 * @return string
327
	 */
328
	protected function checkExpression( string$value ):string
329
	{
330
		return preg_match('/^\(.*\)$/',$value) ? '<?=str_replace(\'"\',\'&quot;\','.substr($value,1,-1).')?>' : str_replace('"','&quot;',$value);
331
	}
332
333
	/**
334
	 * Getting attribute string with HTML syntax.
335
	 *
336
	 * @access protected
337
	 *
338
	 * @return string
339
	 */
340
	protected function getAttributesString():string
341
	{
342
		ksort($this->attributes);
343
		return implode('',array_map(static function( string$key, array$data ){
344
			return (isset($data['condition'])&&strlen($data['condition'])?
345
				"<?php if( {$data['condition']} ){?> $key=\"{$data['value']}\"<?php }?>"
346
				:
347
				" $key=\"{$data['value']}\""
348
			);
349
		},array_keys($this->attributes),$this->attributes));
350
	}
351
352
	/**
353
	 * Setting attribute.
354
	 *
355
	 * @access protected
356
	 *
357
	 * @param string      $key       Attribute name.
358
	 * @param string      $value     Attribute value
359
	 * @param string|null $condition Optional condition, If given, attribute will seted only when condition is true.
360
	 */
361
	protected function setAttribute( string$key, string$value, string$condition=null ):self
362
	{
363
		if( isset($this->attributes[$key]) )
364
			{ $this->document->throw("Attribute $key of $this->name cannot redeclare."); }
365
366
		$this->attributes[$key]=[
367
			'value'=> $value,
368
			'condition'=> $condition,
369
		];
370
371
		return $this;
372
	}
373
374
375
	/*             *\
376
	   ArrayAccess
377
	\*             */
378
379
	/**
380
	 * Whether the attribute isset.
381
	 *
382
	 * @access public
383
	 *
384
	 * @param  mixed $offset
385
	 *
386
	 * @return bool
387
	 */
388
	public function offsetExists( $offset ):bool
389
	{
390
		return isset($this->attributes[$offset]);
391
	}
392
393
	/**
394
	 * Getting attribute with array access.
395
	 *
396
	 * @access public
397
	 *
398
	 * @param  mixed $offset
399
	 *
400
	 * @return mixed
401
	 */
402
	public function offsetGet( $offset )
403
	{
404
		return $this->attributes[$offset]??null;
405
	}
406
407
	/**
408
	 * Setting Attribute with array access.
409
	 *
410
	 * @access public
411
	 *
412
	 * @param  mixed $offset
413
	 * @param  mixed $value
414
	 */
415
	public function offsetSet( $offset, $value )
416
	{
417
		$this->setAttribute($offset,$value);
418
	}
419
420
	/**
421
	 * Unset an attribute with array access.
422
	 *
423
	 * @access public
424
	 *
425
	 * @param  mixed $offset
426
	 */
427
	public function offsetUnset( $offset )
428
	{
429
		if( isset($this->attributes[$offset]) )
430
			{ unset($this->attributes[$offset]); }
431
	}
432
}
433