Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
21 / 21 |
|
100.00% |
3 / 3 |
CRAP | |
100.00% |
1 / 1 |
MakeTag | |
100.00% |
21 / 21 |
|
100.00% |
3 / 3 |
11 | |
100.00% |
1 / 1 |
__invoke | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
verifyTagName | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
compileTag | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 |
1 | <?php |
2 | |
3 | /** |
4 | * Make tag view helper |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2020. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package View_Helpers |
25 | * @author Chris Hallberg <crhallberg@gmail.com> |
26 | * @author Demian Katz <demian.katz@villanova.edu> |
27 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
28 | * @link https://vufind.org/wiki/development Wiki |
29 | */ |
30 | |
31 | namespace VuFind\View\Helper\Root; |
32 | |
33 | use function in_array; |
34 | use function is_array; |
35 | |
36 | /** |
37 | * Make tag view helper |
38 | * |
39 | * @category VuFind |
40 | * @package View_Helpers |
41 | * @author Chris Hallberg <crhallberg@gmail.com> |
42 | * @author Demian Katz <demian.katz@villanova.edu> |
43 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
44 | * @link https://vufind.org/wiki/development Wiki |
45 | */ |
46 | class MakeTag extends \Laminas\View\Helper\AbstractHelper |
47 | { |
48 | /** |
49 | * List of all valid body tags |
50 | * |
51 | * Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Element |
52 | * Last checked: September 27, 2022 |
53 | * |
54 | * @var string[] |
55 | */ |
56 | protected $validBodyTags = [ |
57 | 'a', |
58 | 'abbr', |
59 | 'acronym', |
60 | 'address', |
61 | 'applet', |
62 | 'area', |
63 | 'article', |
64 | 'aside', |
65 | 'audio', |
66 | 'b', |
67 | 'base', |
68 | 'bdi', |
69 | 'bdo', |
70 | 'bgsound', |
71 | 'big', |
72 | 'blink', |
73 | 'blockquote', |
74 | 'body', |
75 | 'br', |
76 | 'button', |
77 | 'canvas', |
78 | 'caption', |
79 | 'center', |
80 | 'cite', |
81 | 'code', |
82 | 'col', |
83 | 'colgroup', |
84 | 'content', |
85 | 'data', |
86 | 'datalist', |
87 | 'dd', |
88 | 'del', |
89 | 'details', |
90 | 'dfn', |
91 | 'dialog', |
92 | 'dir', |
93 | 'div', |
94 | 'dl', |
95 | 'dt', |
96 | 'em', |
97 | 'embed', |
98 | 'fieldset', |
99 | 'figcaption', |
100 | 'figure', |
101 | 'font', |
102 | 'footer', |
103 | 'form', |
104 | 'frame', |
105 | 'frameset', |
106 | 'h1', |
107 | 'h2', |
108 | 'h3', |
109 | 'h4', |
110 | 'h5', |
111 | 'h6', |
112 | 'head', |
113 | 'header', |
114 | 'hgroup', |
115 | 'hr', |
116 | 'html', |
117 | 'i', |
118 | 'iframe', |
119 | 'image', |
120 | 'img', |
121 | 'input', |
122 | 'ins', |
123 | 'kbd', |
124 | 'keygen', |
125 | 'label', |
126 | 'legend', |
127 | 'li', |
128 | 'link', |
129 | 'main', |
130 | 'map', |
131 | 'mark', |
132 | 'marquee', |
133 | 'math', |
134 | 'menu', |
135 | 'menuitem', |
136 | 'meta', |
137 | 'meter', |
138 | 'nav', |
139 | 'nobr', |
140 | 'noembed', |
141 | 'noframes', |
142 | 'noscript', |
143 | 'object', |
144 | 'ol', |
145 | 'optgroup', |
146 | 'option', |
147 | 'output', |
148 | 'p', |
149 | 'param', |
150 | 'picture', |
151 | 'plaintext', |
152 | 'portal', |
153 | 'pre', |
154 | 'progress', |
155 | 'q', |
156 | 'rb', |
157 | 'rp', |
158 | 'rt', |
159 | 'rtc', |
160 | 'ruby', |
161 | 's', |
162 | 'samp', |
163 | 'script', |
164 | 'section', |
165 | 'select', |
166 | 'shadow', |
167 | 'slot', |
168 | 'small', |
169 | 'source', |
170 | 'spacer', |
171 | 'span', |
172 | 'strike', |
173 | 'strong', |
174 | 'style', |
175 | 'sub', |
176 | 'summary', |
177 | 'sup', |
178 | 'svg', |
179 | 'table', |
180 | 'tbody', |
181 | 'td', |
182 | 'template', |
183 | 'textarea', |
184 | 'tfoot', |
185 | 'th', |
186 | 'thead', |
187 | 'time', |
188 | 'title', |
189 | 'tr', |
190 | 'track', |
191 | 'tt', |
192 | 'u', |
193 | 'ul', |
194 | 'var', |
195 | 'video', |
196 | 'wbr', |
197 | 'xmp', |
198 | ]; |
199 | |
200 | /** |
201 | * List of all void tags (tags that access no innerHTML) |
202 | * |
203 | * Source: https://html.spec.whatwg.org/multipage/syntax.html#void-elements |
204 | * Last checked: September 27, 2022 |
205 | * |
206 | * @var string[] |
207 | */ |
208 | protected $voidElements = [ |
209 | 'area', |
210 | 'base', |
211 | 'br', |
212 | 'col', |
213 | 'embed', |
214 | 'hr', |
215 | 'img', |
216 | 'input', |
217 | 'link', |
218 | 'meta', |
219 | 'param', // deprecated, but included for back-compatibility |
220 | 'source', |
221 | 'track', |
222 | 'wbr', |
223 | ]; |
224 | |
225 | /** |
226 | * List of deprecated elements that should be replaced. |
227 | * |
228 | * Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Element |
229 | * Last checked: September 27, 2022 |
230 | * |
231 | * @var string[] |
232 | */ |
233 | protected $deprecatedElements = [ |
234 | 'acronym', |
235 | 'applet', |
236 | 'bgsound', |
237 | 'big', |
238 | 'blink', |
239 | 'center', |
240 | 'content', |
241 | 'dir', |
242 | 'font', |
243 | 'frame', |
244 | 'frameset', |
245 | 'image', |
246 | 'keygen', |
247 | 'marquee', |
248 | 'menuitem', |
249 | 'nobr', |
250 | 'noembed', |
251 | 'noframes', |
252 | 'param', |
253 | 'plaintext', |
254 | 'rb', |
255 | 'rtc', |
256 | 'shadow', |
257 | 'spacer', |
258 | 'strike', |
259 | 'tt', |
260 | 'xmp', |
261 | ]; |
262 | |
263 | /** |
264 | * Render an HTML tag |
265 | * |
266 | * A string passed into $attrs will be treated like a class. |
267 | * These two are equivalent: |
268 | * > MakeTag('div', 'Success!', 'alert alert-success') |
269 | * > MakeTag('div', 'Success!', ['class => 'alert alert-success']) |
270 | * |
271 | * Additional options |
272 | * - escapeContent: Default true, set to false to skip escaping (like for HTML). |
273 | * |
274 | * @param string $tagName Element tag name |
275 | * @param string $contents Element contents (must be properly-formed HTML) |
276 | * @param string|array $attrs Tag attributes (associative array or class name) |
277 | * @param array $options Additional options |
278 | * |
279 | * @return string HTML for an anchor tag |
280 | */ |
281 | public function __invoke( |
282 | string $tagName, |
283 | string $contents, |
284 | $attrs = [], |
285 | $options = [] |
286 | ) { |
287 | // $attrs not an object, interpret as class name |
288 | if (!is_array($attrs)) { |
289 | $attrs = !empty($attrs) ? ['class' => $attrs] : []; |
290 | } |
291 | |
292 | // Compile attributes |
293 | return $this->compileTag($tagName, $contents, $attrs, $options); |
294 | } |
295 | |
296 | /** |
297 | * Verify HTML tag matches HTML spec |
298 | * |
299 | * @param string $tagName Element tag name |
300 | * |
301 | * @return void |
302 | */ |
303 | protected function verifyTagName(string $tagName) |
304 | { |
305 | // Simplify check by making tag lowercase |
306 | $lowerTagName = mb_strtolower($tagName, 'UTF-8'); |
307 | |
308 | // Existing tag? |
309 | if (in_array($lowerTagName, $this->validBodyTags)) { |
310 | // Deprecated tag? Throw warning. |
311 | if (in_array($lowerTagName, $this->deprecatedElements)) { |
312 | trigger_error( |
313 | "'<$lowerTagName>' is deprecated and should be replaced.", |
314 | E_USER_WARNING |
315 | ); |
316 | } |
317 | |
318 | return; |
319 | } |
320 | |
321 | // Check if it's a valid custom element |
322 | // Spec: https://html.spec.whatwg.org/#autonomous-custom-element |
323 | |
324 | // All valid characters for a Potential Custom Element Name |
325 | // Concated for clarity (space not a valid character) |
326 | $PCENChar = '[' . |
327 | '\-\.0-9_a-z' . |
328 | '\x{B7}' . |
329 | '\x{C0}-\x{D6}' . |
330 | '\x{D8}-\x{F6}' . |
331 | '\x{F8}-\x{37D}' . |
332 | '\x{37F}-\x{1FFF}' . |
333 | '\x{200C}-\x{200D}' . |
334 | '\x{203F}-\x{2040}' . |
335 | '\x{2070}-\x{218F}' . |
336 | '\x{2C00}-\x{2FEF}' . |
337 | '\x{3001}-\x{D7FF}' . |
338 | '\x{F900}-\x{FDCF}' . |
339 | '\x{FDF0}-\x{FFFD}' . |
340 | '\x{10000}-\x{EFFFF}' . |
341 | ']*'; |
342 | |
343 | // First character must be a letter (uppercase or lowercase) |
344 | // Needs one hyphen to designate custom element, more groups valid |
345 | $validCustomTagPattern = '/^[a-z]' . $PCENChar . '(\-' . $PCENChar . ')+$/u'; |
346 | |
347 | // Is valid custom tag? |
348 | if (!preg_match($validCustomTagPattern, $lowerTagName)) { |
349 | throw new \InvalidArgumentException('Invalid tag name: ' . $tagName); |
350 | } |
351 | } |
352 | |
353 | /** |
354 | * Turn associative array into a string of attributes in an anchor |
355 | * |
356 | * Additional options |
357 | * - escapeContent: Default true, set to false to skip escaping (like for HTML). |
358 | * |
359 | * @param string $tagName HTML tag name |
360 | * @param string $contents InnerHTML |
361 | * @param array $attrs Tag attributes (associative array) |
362 | * @param array $options Additional options |
363 | * |
364 | * @return string |
365 | */ |
366 | protected function compileTag( |
367 | string $tagName, |
368 | string $contents, |
369 | $attrs = [], |
370 | $options = [] |
371 | ) { |
372 | $this->verifyTagName($tagName); |
373 | |
374 | $htmlAttrs = $this->getView()->plugin('htmlAttributes')($attrs); |
375 | |
376 | if (empty($contents) && in_array($tagName, $this->voidElements)) { |
377 | return '<' . $tagName . $htmlAttrs . '>'; |
378 | } |
379 | |
380 | // Special option: escape content |
381 | if ($options['escapeContent'] ?? true) { |
382 | $contents = $this->getView()->plugin('escapeHtml')($contents); |
383 | } |
384 | |
385 | return '<' . $tagName . $htmlAttrs . '>' . $contents . '</' . $tagName . '>'; |
386 | } |
387 | } |