1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Groundskeeper\Tokens\Elements; |
4
|
|
|
|
5
|
|
|
use Groundskeeper\Configuration; |
6
|
|
|
use Groundskeeper\Tokens\AbstractToken; |
7
|
|
|
use Groundskeeper\Tokens\Token; |
8
|
|
|
|
9
|
|
|
class Element extends AbstractToken |
10
|
|
|
{ |
11
|
|
|
const ATTR_CI_ENUM = 'attr_ci_enum';// case-insensitive enumeration |
12
|
|
|
const ATTR_JS = 'attr_js'; |
13
|
|
|
const ATTR_CI_STRING = 'attr_ci_str'; // case-insensitive string |
14
|
|
|
const ATTR_CS_STRING = 'attr_cs_str'; // case-sensitive string |
15
|
|
|
const ATTR_URI = 'attr_uri'; |
16
|
|
|
|
17
|
|
|
/** @var array */ |
18
|
|
|
private $attributes; |
19
|
|
|
|
20
|
|
|
/** @var array[Token] */ |
21
|
|
|
private $children; |
22
|
|
|
|
23
|
|
|
/** @var string */ |
24
|
|
|
private $name; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* Constructor |
28
|
|
|
*/ |
29
|
3 |
|
public function __construct($name, array $attributes = array(), Token $parent = null) |
30
|
|
|
{ |
31
|
3 |
|
parent::__construct(Token::ELEMENT, $parent); |
32
|
|
|
|
33
|
3 |
|
$this->attributes = $attributes; |
34
|
3 |
|
$this->children = array(); |
35
|
3 |
|
$this->setName($name); |
36
|
3 |
|
} |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* Getter for 'attributes'. |
40
|
|
|
*/ |
41
|
1 |
|
public function getAttributes() |
42
|
|
|
{ |
43
|
1 |
|
return $this->attributes; |
44
|
|
|
} |
45
|
|
|
|
46
|
|
|
public function addAttribute($key, $value) |
47
|
|
|
{ |
48
|
|
|
$key = strtolower($key); |
49
|
|
|
$this->attributes[$key] = $value; |
50
|
|
|
|
51
|
|
|
return $this; |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
public function removeAttribute($key) |
55
|
|
|
{ |
56
|
|
|
$key = strtolower($key); |
57
|
|
|
if (isset($this->attributes[$key])) { |
58
|
|
|
unset($this->attributes[$key]); |
59
|
|
|
|
60
|
|
|
return true; |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
return false; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Getter for 'children'. |
68
|
|
|
*/ |
69
|
1 |
|
public function getChildren() |
70
|
|
|
{ |
71
|
1 |
|
return $this->children; |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
public function addChild(Token $token) |
75
|
|
|
{ |
76
|
|
|
$this->children[] = $token; |
77
|
|
|
|
78
|
|
|
return $this; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
public function removeChild(Token $token) |
82
|
|
|
{ |
83
|
|
|
$key = array_search($token, $this->children); |
84
|
|
|
if ($key !== false) { |
85
|
|
|
unset($this->children[$key]); |
86
|
|
|
|
87
|
|
|
return true; |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
return false; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* Getter for 'name'. |
95
|
|
|
*/ |
96
|
1 |
|
public function getName() |
97
|
|
|
{ |
98
|
1 |
|
return $this->name; |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* Chainable setter for 'name'. |
103
|
|
|
*/ |
104
|
3 |
|
public function setName($name) |
105
|
|
|
{ |
106
|
3 |
|
if (!is_string($name)) { |
107
|
|
|
throw new \InvalidArgumentException('Element name must be string type.'); |
108
|
|
|
} |
109
|
|
|
|
110
|
3 |
|
$this->name = trim(strtolower($name)); |
111
|
|
|
|
112
|
3 |
|
return $this; |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
protected function getAllowedAttrbutes() |
116
|
|
|
{ |
117
|
|
|
return array( |
118
|
|
|
// Global Attributes |
119
|
|
|
'/^accesskey$/i' => self::ATTR_CS_STRING, |
120
|
|
|
'/^contenteditable$/i' => self::ATTR_CS_STRING, |
121
|
|
|
'/^contextmenu$/i' => self::ATTR_CS_STRING, |
122
|
|
|
'/^dir$/i' => self::ATTR_CS_STRING, |
123
|
|
|
'/^draggable$/i' => self::ATTR_CS_STRING, |
124
|
|
|
'/^dropzone$/i' => self::ATTR_CS_STRING, |
125
|
|
|
'/^hidden$/i' => self::ATTR_CS_STRING, |
126
|
|
|
'/^is$/i' => self::ATTR_CS_STRING, |
127
|
|
|
'/^itemid$/i' => self::ATTR_CS_STRING, |
128
|
|
|
'/^itemprop$/i' => self::ATTR_CS_STRING, |
129
|
|
|
'/^itemref$/i' => self::ATTR_CS_STRING, |
130
|
|
|
'/^itemscope$/i' => self::ATTR_CS_STRING, |
131
|
|
|
'/^itemtype$/i' => self::ATTR_CS_STRING, |
132
|
|
|
'/^lang$/i' => self::ATTR_CI_STRING, |
133
|
|
|
'/^spellcheck$/i' => self::ATTR_CS_STRING, |
134
|
|
|
'/^style$/i' => self::ATTR_CS_STRING, |
135
|
|
|
'/^tabindex$/i' => self::ATTR_CS_STRING, |
136
|
|
|
'/^title$/i' => self::ATTR_CS_STRING, |
137
|
|
|
'/^translate$/i' => self::ATTR_CI_ENUM . '("","yes","no")', |
138
|
|
|
|
139
|
|
|
// Event Handler Content Attributes |
140
|
|
|
// https://html.spec.whatwg.org/multipage/webappapis.html#event-handler-content-attributes |
141
|
|
|
'/^onabort$/i' => self::ATTR_JS, |
142
|
|
|
'/^onautocomplete$/i' => self::ATTR_JS, |
143
|
|
|
'/^onautocompleteerror$/i' => self::ATTR_JS, |
144
|
|
|
'/^onblur$/i' => self::ATTR_JS, |
145
|
|
|
'/^oncancel$/i' => self::ATTR_JS, |
146
|
|
|
'/^oncanplay$/i' => self::ATTR_JS, |
147
|
|
|
'/^oncanplaythrough$/i' => self::ATTR_JS, |
148
|
|
|
'/^onchange$/i' => self::ATTR_JS, |
149
|
|
|
'/^onclick$/i' => self::ATTR_JS, |
150
|
|
|
'/^onclose$/i' => self::ATTR_JS, |
151
|
|
|
'/^oncontextmenu$/i' => self::ATTR_JS, |
152
|
|
|
'/^oncuechange$/i' => self::ATTR_JS, |
153
|
|
|
'/^ondblclick$/i' => self::ATTR_JS, |
154
|
|
|
'/^ondrag$/i' => self::ATTR_JS, |
155
|
|
|
'/^ondragend$/i' => self::ATTR_JS, |
156
|
|
|
'/^ondragenter$/i' => self::ATTR_JS, |
157
|
|
|
'/^ondragexit$/i' => self::ATTR_JS, |
158
|
|
|
'/^ondragleave$/i' => self::ATTR_JS, |
159
|
|
|
'/^ondragover$/i' => self::ATTR_JS, |
160
|
|
|
'/^ondragstart$/i' => self::ATTR_JS, |
161
|
|
|
'/^ondrop$/i' => self::ATTR_JS, |
162
|
|
|
'/^ondurationchange$/i' => self::ATTR_JS, |
163
|
|
|
'/^onemptied$/i' => self::ATTR_JS, |
164
|
|
|
'/^onended$/i' => self::ATTR_JS, |
165
|
|
|
'/^onerror$/i' => self::ATTR_JS, |
166
|
|
|
'/^onfocus$/i' => self::ATTR_JS, |
167
|
|
|
'/^oninput$/i' => self::ATTR_JS, |
168
|
|
|
'/^oninvalid$/i' => self::ATTR_JS, |
169
|
|
|
'/^onkeydown$/i' => self::ATTR_JS, |
170
|
|
|
'/^onkeypress$/i' => self::ATTR_JS, |
171
|
|
|
'/^onkeyup$/i' => self::ATTR_JS, |
172
|
|
|
'/^onload$/i' => self::ATTR_JS, |
173
|
|
|
'/^onloadeddata$/i' => self::ATTR_JS, |
174
|
|
|
'/^onloadedmetadata$/i' => self::ATTR_JS, |
175
|
|
|
'/^onloadstart$/i' => self::ATTR_JS, |
176
|
|
|
'/^onmousedown$/i' => self::ATTR_JS, |
177
|
|
|
'/^onmouseenter$/i' => self::ATTR_JS, |
178
|
|
|
'/^onmouseleave$/i' => self::ATTR_JS, |
179
|
|
|
'/^onmousemove$/i' => self::ATTR_JS, |
180
|
|
|
'/^onmouseout$/i' => self::ATTR_JS, |
181
|
|
|
'/^onmouseover$/i' => self::ATTR_JS, |
182
|
|
|
'/^onmouseup$/i' => self::ATTR_JS, |
183
|
|
|
'/^onwheel$/i' => self::ATTR_JS, |
184
|
|
|
'/^onpause$/i' => self::ATTR_JS, |
185
|
|
|
'/^onplay$/i' => self::ATTR_JS, |
186
|
|
|
'/^onplaying$/i' => self::ATTR_JS, |
187
|
|
|
'/^onprogress$/i' => self::ATTR_JS, |
188
|
|
|
'/^onratechange$/i' => self::ATTR_JS, |
189
|
|
|
'/^onreset$/i' => self::ATTR_JS, |
190
|
|
|
'/^onresize$/i' => self::ATTR_JS, |
191
|
|
|
'/^onscroll$/i' => self::ATTR_JS, |
192
|
|
|
'/^onseeked$/i' => self::ATTR_JS, |
193
|
|
|
'/^onseeking$/i' => self::ATTR_JS, |
194
|
|
|
'/^onselect$/i' => self::ATTR_JS, |
195
|
|
|
'/^onshow$/i' => self::ATTR_JS, |
196
|
|
|
'/^onstalled$/i' => self::ATTR_JS, |
197
|
|
|
'/^onsubmit$/i' => self::ATTR_JS, |
198
|
|
|
'/^onsuspend$/i' => self::ATTR_JS, |
199
|
|
|
'/^ontimeupdate$/i' => self::ATTR_JS, |
200
|
|
|
'/^ontoggle$/i' => self::ATTR_JS, |
201
|
|
|
'/^onvolumechange$/i' => self::ATTR_JS, |
202
|
|
|
'/^onwaiting$/i' => self::ATTR_JS, |
203
|
|
|
|
204
|
|
|
// WAI-ARIA |
205
|
|
|
// https://w3c.github.io/aria/aria/aria.html |
206
|
|
|
'/^role$/i' => self::ATTR_CI_STRING, |
207
|
|
|
|
208
|
|
|
// ARIA global states and properties |
209
|
|
|
'/^aria-atomic$/i' => self::ATTR_CS_STRING, |
210
|
|
|
'/^aria-busy$/i' => self::ATTR_CS_STRING, |
211
|
|
|
'/^aria-controls$/i' => self::ATTR_CS_STRING, |
212
|
|
|
'/^aria-current$/i' => self::ATTR_CS_STRING, |
213
|
|
|
'/^aria-describedby$/i' => self::ATTR_CS_STRING, |
214
|
|
|
'/^aria-details$/i' => self::ATTR_CS_STRING, |
215
|
|
|
'/^aria-disabled$/i' => self::ATTR_CS_STRING, |
216
|
|
|
'/^aria-dropeffect$/i' => self::ATTR_CS_STRING, |
217
|
|
|
'/^aria-errormessage$/i' => self::ATTR_CS_STRING, |
218
|
|
|
'/^aria-flowto$/i' => self::ATTR_CS_STRING, |
219
|
|
|
'/^aria-grabbed$/i' => self::ATTR_CS_STRING, |
220
|
|
|
'/^aria-haspopup$/i' => self::ATTR_CS_STRING, |
221
|
|
|
'/^aria-hidden$/i' => self::ATTR_CS_STRING, |
222
|
|
|
'/^aria-invalid$/i' => self::ATTR_CS_STRING, |
223
|
|
|
'/^aria-label$/i' => self::ATTR_CS_STRING, |
224
|
|
|
'/^aria-labelledby$/i' => self::ATTR_CS_STRING, |
225
|
|
|
'/^aria-live$/i' => self::ATTR_CS_STRING, |
226
|
|
|
'/^aria-owns$/i' => self::ATTR_CS_STRING, |
227
|
|
|
'/^aria-relevant$/i' => self::ATTR_CS_STRING, |
228
|
|
|
'/^aria-roledescription$/i' => self::ATTR_CS_STRING, |
229
|
|
|
|
230
|
|
|
// ARIA widget attributes |
231
|
|
|
'/^aria-autocomplete$/i' => self::ATTR_CS_STRING, |
232
|
|
|
'/^aria-checked$/i' => self::ATTR_CS_STRING, |
233
|
|
|
'/^aria-expanded$/i' => self::ATTR_CS_STRING, |
234
|
|
|
'/^aria-level$/i' => self::ATTR_CS_STRING, |
235
|
|
|
'/^aria-modal$/i' => self::ATTR_CS_STRING, |
236
|
|
|
'/^aria-multiline$/i' => self::ATTR_CS_STRING, |
237
|
|
|
'/^aria-multiselectable$/i' => self::ATTR_CS_STRING, |
238
|
|
|
'/^aria-orientation$/i' => self::ATTR_CS_STRING, |
239
|
|
|
'/^aria-placeholder$/i' => self::ATTR_CS_STRING, |
240
|
|
|
'/^aria-pressed$/i' => self::ATTR_CS_STRING, |
241
|
|
|
'/^aria-readonly$/i' => self::ATTR_CS_STRING, |
242
|
|
|
'/^aria-required$/i' => self::ATTR_CS_STRING, |
243
|
|
|
'/^aria-selected$/i' => self::ATTR_CS_STRING, |
244
|
|
|
'/^aria-sort$/i' => self::ATTR_CS_STRING, |
245
|
|
|
'/^aria-valuemax$/i' => self::ATTR_CS_STRING, |
246
|
|
|
'/^aria-valuemin$/i' => self::ATTR_CS_STRING, |
247
|
|
|
'/^aria-valuenow$/i' => self::ATTR_CS_STRING, |
248
|
|
|
'/^aria-valuetext$/i' => self::ATTR_CS_STRING, |
249
|
|
|
|
250
|
|
|
// ARIA relationship attributes |
251
|
|
|
'/^aria-activedescendant$/i' => self::ATTR_CS_STRING, |
252
|
|
|
'/^aria-colcount$/i' => self::ATTR_CS_STRING, |
253
|
|
|
'/^aria-colindex$/i' => self::ATTR_CS_STRING, |
254
|
|
|
'/^aria-colspan$/i' => self::ATTR_CS_STRING, |
255
|
|
|
'/^aria-posinset$/i' => self::ATTR_CS_STRING, |
256
|
|
|
'/^aria-rowcount$/i' => self::ATTR_CS_STRING, |
257
|
|
|
'/^aria-rowindex$/i' => self::ATTR_CS_STRING, |
258
|
|
|
'/^aria-rowspan$/i' => self::ATTR_CS_STRING, |
259
|
|
|
'/^aria-setsize$/i' => self::ATTR_CS_STRING |
260
|
|
|
); |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
protected function isAttributeNameValid($name) |
264
|
|
|
{ |
265
|
|
|
$allowedAttributes = $this->getAllowedAttrbutes(); |
266
|
|
|
foreach ($allowedAttributes as $attrRegex => $valueType) { |
267
|
|
|
if (preg_match($attrRegex, $name) === 1) { |
268
|
|
|
return true; |
269
|
|
|
} |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
return false; |
273
|
|
|
} |
274
|
|
|
|
275
|
2 |
|
public function validate(Configuration $configuration) |
276
|
|
|
{ |
277
|
2 |
|
parent::validate($configuration); |
278
|
|
|
|
279
|
2 |
|
if (!$this->isValid) { |
280
|
|
|
return; |
281
|
|
|
} |
282
|
|
|
|
283
|
2 |
|
if ($configuration->get('strategy') == 'standard') { |
284
|
|
|
// Remove non-standard attributes. |
285
|
2 |
|
foreach ($this->attributes as $name => $value) { |
286
|
|
|
// Validate attribute name |
287
|
|
|
if (!$this->isAttributeNameValid($name)) { |
288
|
|
|
unset($this->attributes[$name]); |
289
|
|
|
continue; |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
// Validate attributes value |
293
|
|
|
/// @todo |
294
|
2 |
|
} |
295
|
2 |
|
} |
296
|
|
|
|
297
|
2 |
|
foreach ($this->children as $child) { |
298
|
|
|
$child->validate($configuration); |
299
|
2 |
|
} |
300
|
2 |
|
} |
301
|
|
|
|
302
|
2 |
View Code Duplication |
public function toString(Configuration $configuration, $prefix = '', $suffix = '') |
|
|
|
|
303
|
|
|
{ |
304
|
2 |
|
if (!$this->isValid) { |
305
|
|
|
return ''; |
306
|
|
|
} |
307
|
|
|
|
308
|
2 |
|
$output = $this->toStringTag($configuration, $prefix, $suffix); |
309
|
2 |
|
if (empty($this->children)) { |
310
|
2 |
|
return $output; |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
foreach ($this->children as $child) { |
314
|
|
|
$newPrefix = $prefix . str_repeat(' ', $configuration->get('indent-spaces')); |
315
|
|
|
$output .= $child->toString($options, $newPrefix, $suffix); |
|
|
|
|
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
return $output . $prefix . '</' . $this->name . '>' . $suffix; |
319
|
|
|
} |
320
|
|
|
|
321
|
2 |
|
protected function toStringTag(Configuration $configuration, $prefix = '', $suffix = '', $forceOpen = false) |
|
|
|
|
322
|
|
|
{ |
323
|
2 |
|
$output = $prefix . '<' . $this->name; |
324
|
2 |
|
foreach ($this->attributes as $key => $value) { |
325
|
|
|
$output .= ' ' . strtolower($key); |
326
|
|
|
if (is_string($value)) { |
327
|
|
|
$output .= '="' . $value . '"'; |
328
|
|
|
} |
329
|
2 |
|
} |
330
|
|
|
|
331
|
2 |
|
if (!$forceOpen && empty($this->children)) { |
332
|
2 |
|
return $output . '/>' . $suffix; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
return $output . '>' . $suffix; |
336
|
|
|
} |
337
|
|
|
} |
338
|
|
|
|
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.