1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
|
<?php /** * Class AMP_DOM_Utils. * * @package AMP */
use AmpProject\Dom\Document; use AmpProject\Tag;
/** * Class AMP_DOM_Utils * * Functionality to simplify working with Dom\Documents and DOMElements. * * @internal */ class AMP_DOM_Utils {
/** * Attribute prefix for AMP-bind data attributes. * * @since 1.2.1 * @deprecated Use AmpProject\Dom\Document::AMP_BIND_DATA_ATTR_PREFIX instead. * @internal * @var string */ const AMP_BIND_DATA_ATTR_PREFIX = Document::AMP_BIND_DATA_ATTR_PREFIX;
/** * Regular expression pattern to match events and actions within an 'on' attribute. * * @since 1.4.0 * @var string */ const AMP_EVENT_ACTIONS_REGEX_PATTERN = '/((?<event>[^:;]+):(?<actions>(?:[^;,\(]+(?:\([^\)]+\))?,?)+))+?/';
/** * Regular expression pattern to match individual actions within an event. * * @since 1.4.0 * @var string */ const AMP_ACTION_REGEX_PATTERN = '/(?<action>[^(),\s]+(?:\([^\)]+\))?)+/';
/** * Regular expression pattern to match the contents of the <body> element. * * @since 1.5.0 * @var string */ const HTML_BODY_CONTENTS_PATTERN = '#^.*?<body.*?>(.*)</body>.*?$#si';
/** * Return a valid Dom\Document representing HTML document passed as a parameter. * * @since 0.7 * @see AMP_DOM_Utils::get_content_from_dom_node() * @codeCoverageIgnore * @deprecated Use AmpProject\Dom\Document::fromHtml( $html, $encoding ) instead. * * @param string $document Valid HTML document to be represented by a Dom\Document. * @param string $encoding Optional. Encoding to use for the content. * @return Document|false Returns Dom\Document, or false if conversion failed. */ public static function get_dom( $document, $encoding = null ) { _deprecated_function( __METHOD__, '1.5.0', 'AmpProject\Dom\Document::fromHtml()' ); return Document::fromHtml( $document, $encoding ); }
/** * Determine whether a node can be in the head. * * @link https://github.com/ampproject/amphtml/blob/445d6e3be8a5063e2738c6f90fdcd57f2b6208be/validator/engine/htmlparser.js#L83-L100 * @link https://www.w3.org/TR/html5/document-metadata.html * @codeCoverageIgnore * @deprecated Use AmpProject\Dom\Document->isValidHeadNode() instead. * * @param DOMNode $node Node. * @return bool Whether valid head node. */ public static function is_valid_head_node( DOMNode $node ) { _deprecated_function( __METHOD__, '1.5.0', 'AmpProject\Dom\Document->isValidHeadNode()' ); return Document::fromNode( $node )->isValidHeadNode( $node ); }
/** * Get attribute prefix for converted amp-bind attributes. * * This contains a random string to prevent HTML content containing this data- attribute * originally from being mutated to contain an amp-bind attribute when attributes are restored. * * @since 0.7 * @see \AMP_DOM_Utils::convert_amp_bind_attributes() * @see \AMP_DOM_Utils::restore_amp_bind_attributes() * @codeCoverageIgnore * @deprecated Use AmpProject\Dom\Document::AMP_BIND_DATA_ATTR_PREFIX instead. * @link https://www.ampproject.org/docs/reference/components/amp-bind * * @return string HTML5 data-* attribute name prefix for AMP binding attributes. */ public static function get_amp_bind_placeholder_prefix() { _deprecated_function( __METHOD__, '1.2.1' ); return Document::AMP_BIND_DATA_ATTR_PREFIX; }
/** * Replace AMP binding attributes with something that libxml can parse (as HTML5 data-* attributes). * * This is necessary because attributes in square brackets are not understood in PHP and * get dropped with an error raised: * > Warning: DOMDocument::loadHTML(): error parsing attribute name * This is a reciprocal function of AMP_DOM_Utils::restore_amp_bind_attributes(). * * @since 0.7 * @codeCoverageIgnore * @deprecated This is handled automatically via AmpProject\Dom\Document. * @internal * @see \AMP_DOM_Utils::convert_amp_bind_attributes() * @link https://www.ampproject.org/docs/reference/components/amp-bind * * @param string $html HTML containing amp-bind attributes. * @return string HTML with AMP binding attributes replaced with HTML5 data-* attributes. */ public static function convert_amp_bind_attributes( $html ) { _deprecated_function( __METHOD__, '1.5.0' ); return $html; }
/** * Convert AMP bind-attributes back to their original syntax. * * This is a reciprocal function of AMP_DOM_Utils::convert_amp_bind_attributes(). * * @since 0.7 * @see \AMP_DOM_Utils::convert_amp_bind_attributes() * @codeCoverageIgnore * @deprecated This is handled automatically via AmpProject\Dom\Document. * @internal * @link https://www.ampproject.org/docs/reference/components/amp-bind * * @param string $html HTML with amp-bind attributes converted. * @return string HTML with amp-bind attributes restored. */ public static function restore_amp_bind_attributes( $html ) { _deprecated_function( __METHOD__, '1.2.1' ); return $html; }
/** * Return a valid Dom\Document representing arbitrary HTML content passed as a parameter. * * @see Reciprocal function get_content_from_dom() * * @since 0.2 * * @param string $content Valid HTML content to be represented by a Dom\Document. * @param string $encoding Optional. Encoding to use for the content. Defaults to `get_bloginfo( 'charset' )`. * * @return Document|false Returns a DOM document, or false if conversion failed. */ public static function get_dom_from_content( $content, $encoding = null ) { // Detect encoding from the current WordPress installation. if ( null === $encoding ) { $encoding = get_bloginfo( 'charset' ); }
/* * Wrap in dummy tags, since XML needs one parent node. * It also makes it easier to loop through nodes. * We can later use this to extract our nodes. */ $document = "<html><head></head><body>{$content}</body></html>";
return Document::fromHtml( $document, $encoding ); }
/** * Return valid HTML *body* content extracted from the Dom\Document passed as a parameter. * * @since 0.2 * @see AMP_DOM_Utils::get_content_from_dom_node() Reciprocal function. * * @param Document $dom Represents an HTML document from which to extract HTML content. * @return string Returns the HTML content of the body element represented in the Dom\Document. */ public static function get_content_from_dom( Document $dom ) { return preg_replace( static::HTML_BODY_CONTENTS_PATTERN, '$1', $dom->saveHTML( $dom->body ) ); }
/** * Return valid HTML content extracted from the DOMNode passed as a parameter. * * @since 0.6 * @see AMP_DOM_Utils::get_dom() Where the operations in this method are mirrored. * @see AMP_DOM_Utils::get_content_from_dom() Reciprocal function. * @codeCoverageIgnore * @deprecated Use Dom\Document->saveHtml( $node ) instead. * * @param Document $dom Represents an HTML document. * @param DOMElement $node Represents an HTML element of the $dom from which to extract HTML content. * @return string Returns the HTML content represented in the DOMNode */ public static function get_content_from_dom_node( Document $dom, $node ) { _deprecated_function( __METHOD__, '1.5.0', 'AmpProject\Dom\Document::saveHtml()' ); return $dom->saveHTML( $node ); }
/** * Create a new node w/attributes (a DOMElement) and add to the passed Dom\Document. * * @since 0.2 * * @param Document $dom A representation of an HTML document to add the new node to. * @param string $tag A valid HTML element tag for the element to be added. * @param string[] $attributes One of more valid attributes for the new node. * * @return DOMElement|false The DOMElement for the given $tag, or false on failure */ public static function create_node( Document $dom, $tag, $attributes ) { $node = $dom->createElement( $tag ); self::add_attributes_to_node( $node, $attributes );
return $node; }
/** * Extract a DOMElement node's HTML element attributes and return as an array. * * @since 0.2 * * @param DOMElement $node Represents an HTML element for which to extract attributes. * * @return string[] The attributes for the passed node, or an * empty array if it has no attributes. */ public static function get_node_attributes_as_assoc_array( $node ) { $attributes = []; if ( ! $node->hasAttributes() ) { return $attributes; }
foreach ( $node->attributes as $attribute ) { $attributes[ $attribute->nodeName ] = $attribute->nodeValue; }
return $attributes; }
/** * Add one or more HTML element attributes to a node's DOMElement. * * @since 0.2 * * @param DOMElement $node Represents an HTML element. * @param string[] $attributes One or more attributes for the node's HTML element. */ public static function add_attributes_to_node( $node, $attributes ) { foreach ( $attributes as $name => $value ) { try { $node->setAttribute( $name, $value ); } catch ( DOMException $e ) { /* * Catch a "Invalid Character Error" when libxml is able to parse attributes with invalid characters, * but it throws error when attempting to set them via DOM methods. For example, '...this' can be parsed * as an attribute but it will throw an exception when attempting to setAttribute(). */ continue; } } }
/** * Determines if a DOMElement's node is empty or not.. * * @since 0.2 * * @param DOMElement $node Represents an HTML element. * @return bool Returns true if the DOMElement has no child nodes and * the textContent property of the DOMElement is empty; * Otherwise it returns false. */ public static function is_node_empty( $node ) { return false === $node->hasChildNodes() && empty( $node->textContent ); }
/** * Forces HTML element closing tags given a Dom\Document and optional DOMElement. * * @since 0.2 * @codeCoverageIgnore * @deprecated * @internal * * @param Document $dom Represents HTML document on which to force closing tags. * @param DOMElement $node Represents HTML element to start closing tags on. * If not passed, defaults to first child of body. */ public static function recursive_force_closing_tags( $dom, $node = null ) { _deprecated_function( __METHOD__, '0.7' );
if ( null === $node ) { $node = $dom->body; }
if ( XML_ELEMENT_NODE !== $node->nodeType ) { return; }
if ( self::is_self_closing_tag( $node->nodeName ) ) { /* * Ensure there is no text content to accidentally force a child */ $node->textContent = ''; return; }
if ( self::is_node_empty( $node ) ) { $text_node = $dom->createTextNode( '' ); $node->appendChild( $text_node );
return; }
$num_children = $node->childNodes->length; for ( $i = $num_children - 1; $i >= 0; $i -- ) { $child = $node->childNodes->item( $i ); self::recursive_force_closing_tags( $dom, $child ); }
}
/** * Determines if an HTML element tag is validly a self-closing tag per W3C HTML5 specs. * * @since 0.2 * * @param string $tag Tag. * @return bool Returns true if a valid self-closing tag, false if not. */ private static function is_self_closing_tag( $tag ) { return in_array( strtolower( $tag ), Tag::SELF_CLOSING_TAGS, true ); }
/** * Check whether a given element has a specific class. * * @since 1.4.0 * * @param DOMElement $element Element to check the classes of. * @param string $class Class to check for. * @return bool Whether the element has the requested class. */ public static function has_class( DOMElement $element, $class ) { if ( ! $element->hasAttribute( 'class' ) ) { return false; }
$classes = $element->getAttribute( 'class' );
return in_array( $class, preg_split( '/\s/', $classes ), true ); }
/** * Get the ID for an element. * * If the element does not have an ID, create one first. * * @since 1.4.0 * @since 1.5.1 Deprecated for AmpProject\Dom\Document::getElementId() * * @deprecated Use AmpProject\Dom\Document::getElementId() instead. * * @param DOMElement $element Element to get the ID for. * @param string $prefix Optional. Defaults to 'amp-wp-id'. * @return string ID to use. */ public static function get_element_id( $element, $prefix = 'amp-wp-id' ) { _deprecated_function( 'AMP_DOM_Utils::get_element_id', '1.5.1', 'Use AmpProject\Amp\Dom\Document::getElementId() instead' );
return Document::fromNode( $element )->getElementId( $element, $prefix ); }
/** * Register an AMP action to an event on a given element. * * If the element already contains one or more events or actions, the method * will assemble them in a smart way. * * @since 1.4.0 * * @param DOMElement $element Element to add an action to. * @param string $event Event to trigger the action on. * @param string $action Action to add. */ public static function add_amp_action( DOMElement $element, $event, $action ) { $event_action_string = "{$event}:{$action}";
if ( ! $element->hasAttribute( 'on' ) ) { // There's no "on" attribute yet, so just add it and be done. $element->setAttribute( 'on', $event_action_string ); return; }
$element->setAttribute( 'on', self::merge_amp_actions( $element->getAttribute( 'on' ), $event_action_string ) ); }
/** * Merge two sets of AMP events & actions. * * @since 1.4.0 * * @param string $first First event/action string. * @param string $second First event/action string. * @return string Merged event/action string. */ public static function merge_amp_actions( $first, $second ) { $events = []; foreach ( [ $first, $second ] as $event_action_string ) { $matches = []; $results = preg_match_all( self::AMP_EVENT_ACTIONS_REGEX_PATTERN, $event_action_string, $matches );
if ( ! $results || ! isset( $matches['event'] ) ) { continue; }
foreach ( $matches['event'] as $index => $event ) { $events[ $event ][] = $matches['actions'][ $index ]; } }
$value_strings = []; foreach ( $events as $event => $action_strings_array ) { $actions_array = []; array_walk( $action_strings_array, static function ( $actions ) use ( &$actions_array ) { $matches = []; $results = preg_match_all( self::AMP_ACTION_REGEX_PATTERN, $actions, $matches );
if ( ! $results || ! isset( $matches['action'] ) ) { $actions_array[] = $actions; return; }
$actions_array = array_merge( $actions_array, $matches['action'] ); } );
$actions = implode( ',', array_unique( array_filter( $actions_array ) ) ); $value_strings[] = "{$event}:{$actions}"; }
return implode( ';', $value_strings ); }
/** * Copy one or more attributes from one element to the other. * * @param array|string $attributes Attribute name or array of attribute names to copy. * @param DOMElement $from DOM element to copy the attributes from. * @param DOMElement $to DOM element to copy the attributes to. * @param string $default_separator Default separator to use for multiple values if the attribute is not known. */ public static function copy_attributes( $attributes, DOMElement $from, DOMElement $to, $default_separator = ',' ) { foreach ( (array) $attributes as $attribute ) { if ( $from->hasAttribute( $attribute ) ) { $values = $from->getAttribute( $attribute ); if ( $to->hasAttribute( $attribute ) ) { switch ( $attribute ) { case 'on': $values = self::merge_amp_actions( $to->getAttribute( $attribute ), $values ); break; case 'class': $values = $to->getAttribute( $attribute ) . ' ' . $values; break; default: $values = $to->getAttribute( $attribute ) . $default_separator . $values; } } $to->setAttribute( $attribute, $values ); } } } }
|