1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Groundskeeper\Tokens\Elements; |
4
|
|
|
|
5
|
|
|
use Groundskeeper\Configuration; |
6
|
|
|
use Groundskeeper\Exceptions\ValidationException; |
7
|
|
|
use Groundskeeper\Tokens\AbstractToken; |
8
|
|
|
use Groundskeeper\Tokens\Cleanable; |
9
|
|
|
use Groundskeeper\Tokens\ContainsChildren; |
10
|
|
|
use Groundskeeper\Tokens\Token; |
11
|
|
|
use Psr\Log\LoggerInterface; |
12
|
|
|
|
13
|
|
|
class Element extends AbstractToken implements Cleanable, ContainsChildren |
14
|
|
|
{ |
15
|
|
|
const ATTR_CI_ENUM = 'ci_enu';// case-insensitive enumeration |
16
|
|
|
const ATTR_JS = 'cs_jsc'; |
17
|
|
|
const ATTR_CI_STRING = 'ci_str'; // case-insensitive string |
18
|
|
|
const ATTR_CS_STRING = 'cs_str'; // case-sensitive string |
19
|
|
|
const ATTR_URI = 'cs_uri'; |
20
|
|
|
|
21
|
|
|
/** @var array */ |
22
|
|
|
private $attributes; |
23
|
|
|
|
24
|
|
|
/** @var array[Token] */ |
25
|
|
|
private $children; |
26
|
|
|
|
27
|
|
|
/** @var string */ |
28
|
|
|
private $name; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* Constructor |
32
|
|
|
*/ |
33
|
21 |
|
public function __construct(Configuration $configuration, $name, array $attributes = array(), $parent = null) |
34
|
|
|
{ |
35
|
21 |
|
parent::__construct(Token::ELEMENT, $configuration, $parent); |
36
|
|
|
|
37
|
21 |
|
$this->attributes = array(); |
38
|
21 |
|
foreach ($attributes as $key => $value) { |
39
|
10 |
|
$this->addAttribute($key, $value); |
40
|
21 |
|
} |
41
|
|
|
|
42
|
21 |
|
$this->children = array(); |
43
|
21 |
|
$this->setName($name); |
44
|
21 |
|
} |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* Getter for 'attributes'. |
48
|
|
|
*/ |
49
|
2 |
|
public function getAttributes() |
50
|
|
|
{ |
51
|
2 |
|
return $this->attributes; |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Hasser for 'attributes'. |
56
|
|
|
* |
57
|
|
|
* @param string $key |
58
|
|
|
* |
59
|
|
|
* @return boolean True if the attribute is present. |
60
|
|
|
*/ |
61
|
4 |
|
public function hasAttribute($key) |
62
|
|
|
{ |
63
|
4 |
|
return array_key_exists($key, $this->attributes); |
64
|
|
|
} |
65
|
|
|
|
66
|
12 |
|
public function addAttribute($key, $value) |
67
|
|
|
{ |
68
|
12 |
|
$key = trim(strtolower($key)); |
69
|
12 |
|
if ($key == '') { |
70
|
1 |
|
throw new \InvalidArgumentException('Invalid emtpy attribute key.'); |
71
|
|
|
} |
72
|
|
|
|
73
|
11 |
|
$this->attributes[$key] = $value; |
74
|
|
|
|
75
|
11 |
|
return $this; |
76
|
|
|
} |
77
|
|
|
|
78
|
1 |
|
public function removeAttribute($key) |
79
|
|
|
{ |
80
|
1 |
|
$key = strtolower($key); |
81
|
1 |
|
if (isset($this->attributes[$key])) { |
82
|
1 |
|
unset($this->attributes[$key]); |
83
|
|
|
|
84
|
1 |
|
return true; |
85
|
|
|
} |
86
|
|
|
|
87
|
1 |
|
return false; |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Required by ContainsChildren interface. |
92
|
|
|
*/ |
93
|
2 |
|
public function getChildren() |
94
|
|
|
{ |
95
|
2 |
|
return $this->children; |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* Required by ContainsChildren interface. |
100
|
|
|
*/ |
101
|
1 |
|
public function hasChild(Token $token) |
102
|
|
|
{ |
103
|
1 |
|
return array_search($token, $this->children) !== false; |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* Required by ContainsChildren interface. |
108
|
|
|
*/ |
109
|
12 |
|
public function addChild(Token $token) |
110
|
|
|
{ |
111
|
12 |
|
$token->setParent($this); |
112
|
12 |
|
$this->children[] = $token; |
113
|
|
|
|
114
|
12 |
|
return $this; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* Required by ContainsChildren interface. |
119
|
|
|
*/ |
120
|
1 |
View Code Duplication |
public function removeChild(Token $token) |
|
|
|
|
121
|
|
|
{ |
122
|
1 |
|
$key = array_search($token, $this->children); |
123
|
1 |
|
if ($key !== false) { |
124
|
1 |
|
unset($this->children[$key]); |
125
|
|
|
|
126
|
1 |
|
return true; |
127
|
|
|
} |
128
|
|
|
|
129
|
1 |
|
return false; |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* Getter for 'name'. |
134
|
|
|
*/ |
135
|
7 |
|
public function getName() |
136
|
|
|
{ |
137
|
7 |
|
return $this->name; |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Chainable setter for 'name'. |
142
|
|
|
*/ |
143
|
21 |
|
public function setName($name) |
144
|
|
|
{ |
145
|
21 |
|
if (!is_string($name)) { |
146
|
1 |
|
throw new \InvalidArgumentException('Element name must be string type.'); |
147
|
|
|
} |
148
|
|
|
|
149
|
21 |
|
$this->name = trim(strtolower($name)); |
150
|
|
|
|
151
|
21 |
|
return $this; |
152
|
|
|
} |
153
|
|
|
|
154
|
10 |
|
public function clean(LoggerInterface $logger = null) |
155
|
|
|
{ |
156
|
10 |
|
if ($this->configuration->get('clean-strategy') == Configuration::CLEAN_STRATEGY_NONE) { |
157
|
1 |
|
return; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
// Remove non-standard attributes. |
161
|
9 |
|
foreach ($this->attributes as $name => $value) { |
162
|
5 |
|
$attributeParameters = $this->getAttributeParameters($name); |
163
|
5 |
|
if (empty($attributeParameters)) { |
164
|
3 |
|
if ($logger !== null) { |
165
|
3 |
|
$logger->debug('Groundskeeper: Removed non-standard attribute "' . $name . '" from element "' . $this->name . '".'); |
166
|
3 |
|
} |
167
|
|
|
|
168
|
3 |
|
unset($this->attributes[$name]); |
169
|
|
|
|
170
|
3 |
|
continue; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
// Validate attribute value. |
174
|
|
|
list($caseSensitivity, $attributeType) = |
175
|
5 |
|
explode('_', $attributeParameters['valueType']); |
176
|
|
|
|
177
|
|
|
// Handle case-insensitivity. |
178
|
|
|
// Standard is case-insensitive attribute values should be lower case. |
179
|
|
|
// Not required, so don't throw if out of spec. |
180
|
5 |
|
if ($caseSensitivity == 'ci') { |
181
|
1 |
|
$newValue = strtolower($value); |
182
|
1 |
|
if ($newValue !== $value) { |
183
|
1 |
|
if ($this->configuration->get('error-strategy') == Configuration::ERROR_STRATEGY_FIX) { |
184
|
1 |
|
$this->attributes[$name] = $newValue; |
185
|
1 |
|
if ($logger !== null) { |
186
|
1 |
|
$logger->debug('Groundskeeper: The value for the attribute "' . $name . '" is case-insensitive. The value has been converted to lower case.'); |
187
|
1 |
|
} |
188
|
1 |
|
} elseif ($logger !== null) { |
189
|
|
|
$logger->debug('Groundskeeper: The value for the attribute "' . $name . '" is case-insensitive. Consider converting it to lower case.'); |
190
|
|
|
} |
191
|
1 |
|
} |
192
|
1 |
|
} |
193
|
|
|
|
194
|
5 |
|
switch (substr($attributeType, 0, 3)) { |
195
|
5 |
|
case 'enu': // enumeration |
196
|
|
|
/// @todo |
197
|
|
|
break; |
198
|
|
|
|
199
|
5 |
|
case 'uri': // URI |
200
|
|
|
/// @todo |
201
|
|
|
break; |
202
|
5 |
|
} |
203
|
9 |
|
} |
204
|
|
|
|
205
|
|
|
// Clean children. |
206
|
9 |
|
foreach ($this->children as $child) { |
207
|
5 |
|
if ($child instanceof Cleanable) { |
208
|
4 |
|
$child->clean($logger); |
209
|
4 |
|
} |
210
|
9 |
|
} |
211
|
9 |
|
} |
212
|
|
|
|
213
|
5 |
|
protected function getAllowedAttributes() |
214
|
|
|
{ |
215
|
|
|
return array( |
216
|
|
|
// Global Attributes |
217
|
5 |
|
'/^accesskey$/i' => self::ATTR_CS_STRING, |
218
|
5 |
|
'/^class$/i' => self::ATTR_CS_STRING, |
219
|
5 |
|
'/^contenteditable$/i' => self::ATTR_CS_STRING, |
220
|
5 |
|
'/^contextmenu$/i' => self::ATTR_CS_STRING, |
221
|
5 |
|
'/^data-\S/i' => self::ATTR_CS_STRING, |
222
|
5 |
|
'/^dir$/i' => self::ATTR_CI_ENUM . '("ltr","rtl"|"ltr")', |
223
|
5 |
|
'/^draggable$/i' => self::ATTR_CS_STRING, |
224
|
5 |
|
'/^dropzone$/i' => self::ATTR_CS_STRING, |
225
|
5 |
|
'/^hidden$/i' => self::ATTR_CS_STRING, |
226
|
5 |
|
'/^id$/i' => self::ATTR_CS_STRING, |
227
|
5 |
|
'/^is$/i' => self::ATTR_CS_STRING, |
228
|
5 |
|
'/^itemid$/i' => self::ATTR_CS_STRING, |
229
|
5 |
|
'/^itemprop$/i' => self::ATTR_CS_STRING, |
230
|
5 |
|
'/^itemref$/i' => self::ATTR_CS_STRING, |
231
|
5 |
|
'/^itemscope$/i' => self::ATTR_CS_STRING, |
232
|
5 |
|
'/^itemtype$/i' => self::ATTR_CS_STRING, |
233
|
5 |
|
'/^lang$/i' => self::ATTR_CI_STRING, |
234
|
5 |
|
'/^slot$/i' => self::ATTR_CS_STRING, |
235
|
5 |
|
'/^spellcheck$/i' => self::ATTR_CS_STRING, |
236
|
5 |
|
'/^style$/i' => self::ATTR_CS_STRING, |
237
|
5 |
|
'/^tabindex$/i' => self::ATTR_CS_STRING, |
238
|
5 |
|
'/^title$/i' => self::ATTR_CS_STRING, |
239
|
5 |
|
'/^translate$/i' => self::ATTR_CI_ENUM . '("yes","no",""|"yes")', |
240
|
|
|
|
241
|
|
|
// Event Handler Content Attributes |
242
|
|
|
// https://html.spec.whatwg.org/multipage/webappapis.html#event-handler-content-attributes |
243
|
5 |
|
'/^onabort$/i' => self::ATTR_JS, |
244
|
5 |
|
'/^onautocomplete$/i' => self::ATTR_JS, |
245
|
5 |
|
'/^onautocompleteerror$/i' => self::ATTR_JS, |
246
|
5 |
|
'/^onblur$/i' => self::ATTR_JS, |
247
|
5 |
|
'/^oncancel$/i' => self::ATTR_JS, |
248
|
5 |
|
'/^oncanplay$/i' => self::ATTR_JS, |
249
|
5 |
|
'/^oncanplaythrough$/i' => self::ATTR_JS, |
250
|
5 |
|
'/^onchange$/i' => self::ATTR_JS, |
251
|
5 |
|
'/^onclick$/i' => self::ATTR_JS, |
252
|
5 |
|
'/^onclose$/i' => self::ATTR_JS, |
253
|
5 |
|
'/^oncontextmenu$/i' => self::ATTR_JS, |
254
|
5 |
|
'/^oncuechange$/i' => self::ATTR_JS, |
255
|
5 |
|
'/^ondblclick$/i' => self::ATTR_JS, |
256
|
5 |
|
'/^ondrag$/i' => self::ATTR_JS, |
257
|
5 |
|
'/^ondragend$/i' => self::ATTR_JS, |
258
|
5 |
|
'/^ondragenter$/i' => self::ATTR_JS, |
259
|
5 |
|
'/^ondragexit$/i' => self::ATTR_JS, |
260
|
5 |
|
'/^ondragleave$/i' => self::ATTR_JS, |
261
|
5 |
|
'/^ondragover$/i' => self::ATTR_JS, |
262
|
5 |
|
'/^ondragstart$/i' => self::ATTR_JS, |
263
|
5 |
|
'/^ondrop$/i' => self::ATTR_JS, |
264
|
5 |
|
'/^ondurationchange$/i' => self::ATTR_JS, |
265
|
5 |
|
'/^onemptied$/i' => self::ATTR_JS, |
266
|
5 |
|
'/^onended$/i' => self::ATTR_JS, |
267
|
5 |
|
'/^onerror$/i' => self::ATTR_JS, |
268
|
5 |
|
'/^onfocus$/i' => self::ATTR_JS, |
269
|
5 |
|
'/^oninput$/i' => self::ATTR_JS, |
270
|
5 |
|
'/^oninvalid$/i' => self::ATTR_JS, |
271
|
5 |
|
'/^onkeydown$/i' => self::ATTR_JS, |
272
|
5 |
|
'/^onkeypress$/i' => self::ATTR_JS, |
273
|
5 |
|
'/^onkeyup$/i' => self::ATTR_JS, |
274
|
5 |
|
'/^onload$/i' => self::ATTR_JS, |
275
|
5 |
|
'/^onloadeddata$/i' => self::ATTR_JS, |
276
|
5 |
|
'/^onloadedmetadata$/i' => self::ATTR_JS, |
277
|
5 |
|
'/^onloadstart$/i' => self::ATTR_JS, |
278
|
5 |
|
'/^onmousedown$/i' => self::ATTR_JS, |
279
|
5 |
|
'/^onmouseenter$/i' => self::ATTR_JS, |
280
|
5 |
|
'/^onmouseleave$/i' => self::ATTR_JS, |
281
|
5 |
|
'/^onmousemove$/i' => self::ATTR_JS, |
282
|
5 |
|
'/^onmouseout$/i' => self::ATTR_JS, |
283
|
5 |
|
'/^onmouseover$/i' => self::ATTR_JS, |
284
|
5 |
|
'/^onmouseup$/i' => self::ATTR_JS, |
285
|
5 |
|
'/^onwheel$/i' => self::ATTR_JS, |
286
|
5 |
|
'/^onpause$/i' => self::ATTR_JS, |
287
|
5 |
|
'/^onplay$/i' => self::ATTR_JS, |
288
|
5 |
|
'/^onplaying$/i' => self::ATTR_JS, |
289
|
5 |
|
'/^onprogress$/i' => self::ATTR_JS, |
290
|
5 |
|
'/^onratechange$/i' => self::ATTR_JS, |
291
|
5 |
|
'/^onreset$/i' => self::ATTR_JS, |
292
|
5 |
|
'/^onresize$/i' => self::ATTR_JS, |
293
|
5 |
|
'/^onscroll$/i' => self::ATTR_JS, |
294
|
5 |
|
'/^onseeked$/i' => self::ATTR_JS, |
295
|
5 |
|
'/^onseeking$/i' => self::ATTR_JS, |
296
|
5 |
|
'/^onselect$/i' => self::ATTR_JS, |
297
|
5 |
|
'/^onshow$/i' => self::ATTR_JS, |
298
|
5 |
|
'/^onstalled$/i' => self::ATTR_JS, |
299
|
5 |
|
'/^onsubmit$/i' => self::ATTR_JS, |
300
|
5 |
|
'/^onsuspend$/i' => self::ATTR_JS, |
301
|
5 |
|
'/^ontimeupdate$/i' => self::ATTR_JS, |
302
|
5 |
|
'/^ontoggle$/i' => self::ATTR_JS, |
303
|
5 |
|
'/^onvolumechange$/i' => self::ATTR_JS, |
304
|
5 |
|
'/^onwaiting$/i' => self::ATTR_JS, |
305
|
|
|
|
306
|
|
|
// WAI-ARIA |
307
|
|
|
// https://w3c.github.io/aria/aria/aria.html |
308
|
5 |
|
'/^role$/i' => self::ATTR_CI_STRING, |
309
|
|
|
|
310
|
|
|
// ARIA global states and properties |
311
|
5 |
|
'/^aria-atomic$/i' => self::ATTR_CS_STRING, |
312
|
5 |
|
'/^aria-busy$/i' => self::ATTR_CS_STRING, |
313
|
5 |
|
'/^aria-controls$/i' => self::ATTR_CS_STRING, |
314
|
5 |
|
'/^aria-current$/i' => self::ATTR_CS_STRING, |
315
|
5 |
|
'/^aria-describedby$/i' => self::ATTR_CS_STRING, |
316
|
5 |
|
'/^aria-details$/i' => self::ATTR_CS_STRING, |
317
|
5 |
|
'/^aria-disabled$/i' => self::ATTR_CS_STRING, |
318
|
5 |
|
'/^aria-dropeffect$/i' => self::ATTR_CS_STRING, |
319
|
5 |
|
'/^aria-errormessage$/i' => self::ATTR_CS_STRING, |
320
|
5 |
|
'/^aria-flowto$/i' => self::ATTR_CS_STRING, |
321
|
5 |
|
'/^aria-grabbed$/i' => self::ATTR_CS_STRING, |
322
|
5 |
|
'/^aria-haspopup$/i' => self::ATTR_CS_STRING, |
323
|
5 |
|
'/^aria-hidden$/i' => self::ATTR_CS_STRING, |
324
|
5 |
|
'/^aria-invalid$/i' => self::ATTR_CS_STRING, |
325
|
5 |
|
'/^aria-label$/i' => self::ATTR_CS_STRING, |
326
|
5 |
|
'/^aria-labelledby$/i' => self::ATTR_CS_STRING, |
327
|
5 |
|
'/^aria-live$/i' => self::ATTR_CS_STRING, |
328
|
5 |
|
'/^aria-owns$/i' => self::ATTR_CS_STRING, |
329
|
5 |
|
'/^aria-relevant$/i' => self::ATTR_CS_STRING, |
330
|
5 |
|
'/^aria-roledescription$/i' => self::ATTR_CS_STRING, |
331
|
|
|
|
332
|
|
|
// ARIA widget attributes |
333
|
5 |
|
'/^aria-autocomplete$/i' => self::ATTR_CS_STRING, |
334
|
5 |
|
'/^aria-checked$/i' => self::ATTR_CS_STRING, |
335
|
5 |
|
'/^aria-expanded$/i' => self::ATTR_CS_STRING, |
336
|
5 |
|
'/^aria-level$/i' => self::ATTR_CS_STRING, |
337
|
5 |
|
'/^aria-modal$/i' => self::ATTR_CS_STRING, |
338
|
5 |
|
'/^aria-multiline$/i' => self::ATTR_CS_STRING, |
339
|
5 |
|
'/^aria-multiselectable$/i' => self::ATTR_CS_STRING, |
340
|
5 |
|
'/^aria-orientation$/i' => self::ATTR_CS_STRING, |
341
|
5 |
|
'/^aria-placeholder$/i' => self::ATTR_CS_STRING, |
342
|
5 |
|
'/^aria-pressed$/i' => self::ATTR_CS_STRING, |
343
|
5 |
|
'/^aria-readonly$/i' => self::ATTR_CS_STRING, |
344
|
5 |
|
'/^aria-required$/i' => self::ATTR_CS_STRING, |
345
|
5 |
|
'/^aria-selected$/i' => self::ATTR_CS_STRING, |
346
|
5 |
|
'/^aria-sort$/i' => self::ATTR_CS_STRING, |
347
|
5 |
|
'/^aria-valuemax$/i' => self::ATTR_CS_STRING, |
348
|
5 |
|
'/^aria-valuemin$/i' => self::ATTR_CS_STRING, |
349
|
5 |
|
'/^aria-valuenow$/i' => self::ATTR_CS_STRING, |
350
|
5 |
|
'/^aria-valuetext$/i' => self::ATTR_CS_STRING, |
351
|
|
|
|
352
|
|
|
// ARIA relationship attributes |
353
|
5 |
|
'/^aria-activedescendant$/i' => self::ATTR_CS_STRING, |
354
|
5 |
|
'/^aria-colcount$/i' => self::ATTR_CS_STRING, |
355
|
5 |
|
'/^aria-colindex$/i' => self::ATTR_CS_STRING, |
356
|
5 |
|
'/^aria-colspan$/i' => self::ATTR_CS_STRING, |
357
|
5 |
|
'/^aria-posinset$/i' => self::ATTR_CS_STRING, |
358
|
5 |
|
'/^aria-rowcount$/i' => self::ATTR_CS_STRING, |
359
|
5 |
|
'/^aria-rowindex$/i' => self::ATTR_CS_STRING, |
360
|
5 |
|
'/^aria-rowspan$/i' => self::ATTR_CS_STRING, |
361
|
|
|
'/^aria-setsize$/i' => self::ATTR_CS_STRING |
362
|
5 |
|
); |
363
|
|
|
} |
364
|
|
|
|
365
|
5 |
|
protected function getAttributeParameters($name) |
366
|
|
|
{ |
367
|
5 |
|
$allowedAttributes = $this->getAllowedAttributes(); |
368
|
5 |
|
foreach ($allowedAttributes as $attrRegex => $valueType) { |
369
|
5 |
|
if (preg_match($attrRegex, $name) === 1) { |
370
|
|
|
return array( |
371
|
5 |
|
'name' => $name, |
372
|
5 |
|
'regex' => $attrRegex, |
373
|
|
|
'valueType' => $valueType |
374
|
5 |
|
); |
375
|
|
|
} |
376
|
5 |
|
} |
377
|
|
|
|
378
|
3 |
|
return array(); |
379
|
|
|
} |
380
|
|
|
|
381
|
13 |
|
protected function buildHtml($prefix, $suffix) |
382
|
|
|
{ |
383
|
13 |
|
$output = $this->buildStartTag($prefix, $suffix); |
384
|
13 |
|
if (empty($this->children)) { |
385
|
6 |
|
return $output; |
386
|
|
|
} |
387
|
|
|
|
388
|
11 |
|
$output .= $this->buildChildrenHtml($prefix, $suffix); |
389
|
|
|
|
390
|
11 |
|
return $output . $prefix . '</' . $this->name . '>' . $suffix; |
391
|
|
|
} |
392
|
|
|
|
393
|
13 |
|
protected function buildStartTag($prefix, $suffix, $forceOpen = false) |
394
|
|
|
{ |
395
|
13 |
|
$output = $prefix . '<' . $this->name; |
396
|
13 |
|
foreach ($this->attributes as $key => $value) { |
397
|
7 |
|
$output .= ' ' . strtolower($key); |
398
|
7 |
|
if (is_string($value)) { |
399
|
|
|
/// @todo Escape double quotes in value. |
400
|
7 |
|
$output .= '="' . $value . '"'; |
401
|
7 |
|
} |
402
|
13 |
|
} |
403
|
|
|
|
404
|
13 |
|
if (!$forceOpen && empty($this->children)) { |
405
|
10 |
|
return $output . '/>' . $suffix; |
406
|
|
|
} |
407
|
|
|
|
408
|
11 |
|
return $output . '>' . $suffix; |
409
|
|
|
} |
410
|
|
|
|
411
|
11 |
|
protected function buildChildrenHtml($prefix, $suffix) |
412
|
|
|
{ |
413
|
11 |
|
$output = ''; |
414
|
11 |
|
foreach ($this->children as $child) { |
415
|
|
|
$newPrefix = $prefix . |
416
|
11 |
|
str_repeat( |
417
|
11 |
|
' ', |
418
|
11 |
|
$this->configuration->get('indent-spaces') |
419
|
11 |
|
); |
420
|
11 |
|
$output .= $child->buildHtml($newPrefix, $suffix); |
421
|
11 |
|
} |
422
|
|
|
|
423
|
11 |
|
return $output; |
424
|
|
|
} |
425
|
|
|
} |
426
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.