|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* This file is part of PHP-Typography. |
|
4
|
|
|
* |
|
5
|
|
|
* Copyright 2014-2017 Peter Putzer. |
|
6
|
|
|
* Copyright 2009-2011 KINGdesk, LLC. |
|
7
|
|
|
* |
|
8
|
|
|
* This program is free software; you can redistribute it and/or |
|
9
|
|
|
* modify it under the terms of the GNU General Public License |
|
10
|
|
|
* as published by the Free Software Foundation; either version 2 |
|
11
|
|
|
* of the License, or (at your option) any later version. |
|
12
|
|
|
* |
|
13
|
|
|
* This program is distributed in the hope that it will be useful, |
|
14
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
15
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
16
|
|
|
* GNU General Public License for more details. |
|
17
|
|
|
* |
|
18
|
|
|
* You should have received a copy of the GNU General Public License |
|
19
|
|
|
* along with this program; if not, write to the Free Software |
|
20
|
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
21
|
|
|
* |
|
22
|
|
|
* *** |
|
23
|
|
|
* |
|
24
|
|
|
* @package mundschenk-at/php-typography |
|
25
|
|
|
* @license http://www.gnu.org/licenses/gpl-2.0.html |
|
26
|
|
|
*/ |
|
27
|
|
|
|
|
28
|
|
|
namespace PHP_Typography; |
|
29
|
|
|
|
|
30
|
|
|
/** |
|
31
|
|
|
* Some static methods for DOM manipulation. |
|
32
|
|
|
* |
|
33
|
|
|
* @since 4.2.0 |
|
34
|
|
|
*/ |
|
35
|
|
|
abstract class DOM { |
|
36
|
|
|
|
|
37
|
|
|
/** |
|
38
|
|
|
* An array of block tag names. |
|
39
|
|
|
* |
|
40
|
|
|
* @var array |
|
41
|
|
|
*/ |
|
42
|
|
|
private static $block_tags; |
|
43
|
|
|
|
|
44
|
|
|
/** |
|
45
|
|
|
* Retrieves an array of block tag names. |
|
46
|
|
|
* |
|
47
|
|
|
* @param bool $reset Optional. Default false. |
|
48
|
|
|
* |
|
49
|
|
|
* @return array |
|
50
|
|
|
*/ |
|
51
|
|
|
public static function block_tags( $reset = false ) { |
|
52
|
|
|
if ( empty( self::$block_tags ) || $reset ) { |
|
53
|
|
|
self::$block_tags = array_merge( |
|
54
|
|
|
array_flip( array_filter( array_keys( \Masterminds\HTML5\Elements::$html5 ), function( $tag ) { |
|
55
|
|
|
return \Masterminds\HTML5\Elements::isA( $tag, \Masterminds\HTML5\Elements::BLOCK_TAG ); |
|
56
|
|
|
} ) ), |
|
57
|
|
|
array_flip( [ 'li', 'td', 'dt' ] ) // not included as "block tags" in current HTML5-PHP version. |
|
58
|
|
|
); |
|
59
|
|
|
} |
|
60
|
|
|
|
|
61
|
|
|
return self::$block_tags; |
|
62
|
|
|
} |
|
63
|
|
|
|
|
64
|
|
|
|
|
65
|
|
|
/** |
|
66
|
|
|
* Converts \DOMNodeList to array; |
|
67
|
|
|
* |
|
68
|
|
|
* @param \DOMNodeList $list Required. |
|
69
|
|
|
* |
|
70
|
|
|
* @return array An associative array in the form ( $spl_object_hash => $node ). |
|
71
|
|
|
*/ |
|
72
|
|
|
public static function nodelist_to_array( \DOMNodeList $list ) { |
|
73
|
|
|
$out = []; |
|
74
|
|
|
|
|
75
|
|
|
foreach ( $list as $node ) { |
|
76
|
|
|
$out[ spl_object_hash( $node ) ] = $node; |
|
77
|
|
|
} |
|
78
|
|
|
|
|
79
|
|
|
return $out; |
|
80
|
|
|
} |
|
81
|
|
|
|
|
82
|
|
|
/** |
|
83
|
|
|
* Retrieves an array containing all the ancestors of the node. This could be done |
|
84
|
|
|
* via an XPath query for "ancestor::*", but DOM walking is in all likelyhood faster. |
|
85
|
|
|
* |
|
86
|
|
|
* @param \DOMNode $node Required. |
|
87
|
|
|
* |
|
88
|
|
|
* @return array An array of \DOMNode. |
|
89
|
|
|
*/ |
|
90
|
|
|
public static function get_ancestors( \DOMNode $node ) { |
|
91
|
|
|
$result = []; |
|
92
|
|
|
|
|
93
|
|
|
while ( ( $node = $node->parentNode ) && ( $node instanceof \DOMElement ) ) { // @codingStandardsIgnoreLine. |
|
94
|
|
|
$result[] = $node; |
|
95
|
|
|
} |
|
96
|
|
|
|
|
97
|
|
|
return $result; |
|
98
|
|
|
} |
|
99
|
|
|
|
|
100
|
|
|
/** |
|
101
|
|
|
* Checks whether the \DOMNode has one of the given classes. |
|
102
|
|
|
* If $tag is a \DOMText, the parent DOMElement is checked instead. |
|
103
|
|
|
* |
|
104
|
|
|
* @param \DOMNode $tag An element or textnode. |
|
105
|
|
|
* @param string|array $classnames A single classname or an array of classnames. |
|
106
|
|
|
* |
|
107
|
|
|
* @return boolean True if the element has any of the given class(es). |
|
108
|
|
|
*/ |
|
109
|
|
|
public static function has_class( \DOMNode $tag, $classnames ) { |
|
110
|
|
|
if ( $tag instanceof \DOMText ) { |
|
111
|
|
|
$tag = $tag->parentNode; // @codingStandardsIgnoreLine. |
|
112
|
|
|
} |
|
113
|
|
|
|
|
114
|
|
|
// Bail if we are not working with a tag or if there is no classname. |
|
115
|
|
|
if ( ! ( $tag instanceof \DOMElement ) || empty( $classnames ) ) { |
|
116
|
|
|
return false; |
|
117
|
|
|
} |
|
118
|
|
|
|
|
119
|
|
|
// Ensure we always have an array of classnames. |
|
120
|
|
|
if ( ! is_array( $classnames ) ) { |
|
121
|
|
|
$classnames = [ $classnames ]; |
|
122
|
|
|
} |
|
123
|
|
|
|
|
124
|
|
|
if ( $tag->hasAttribute( 'class' ) ) { |
|
125
|
|
|
$tag_classes = array_flip( explode( ' ', $tag->getAttribute( 'class' ) ) ); |
|
126
|
|
|
|
|
127
|
|
|
foreach ( $classnames as $classname ) { |
|
128
|
|
|
if ( isset( $tag_classes[ $classname ] ) ) { |
|
129
|
|
|
return true; |
|
130
|
|
|
} |
|
131
|
|
|
} |
|
132
|
|
|
} |
|
133
|
|
|
|
|
134
|
|
|
return false; |
|
135
|
|
|
} |
|
136
|
|
|
|
|
137
|
|
|
/** |
|
138
|
|
|
* Retrieves the last character of the previous \DOMText sibling (if there is one). |
|
139
|
|
|
* |
|
140
|
|
|
* @param \DOMNode $node The content node. |
|
141
|
|
|
* |
|
142
|
|
|
* @return string A single character (or the empty string). |
|
143
|
|
|
*/ |
|
144
|
|
|
public static function get_prev_chr( \DOMNode $node ) { |
|
145
|
|
|
return self::get_adjacent_chr( $node, -1, 1, [ __CLASS__, 'get_previous_textnode' ] ); |
|
146
|
|
|
} |
|
147
|
|
|
|
|
148
|
|
|
/** |
|
149
|
|
|
* Retrieves the first character of the next \DOMText sibling (if there is one). |
|
150
|
|
|
* |
|
151
|
|
|
* @param \DOMNode $node The content node. |
|
152
|
|
|
* |
|
153
|
|
|
* @return string A single character (or the empty string). |
|
154
|
|
|
*/ |
|
155
|
|
|
public static function get_next_chr( \DOMNode $node ) { |
|
156
|
|
|
return self::get_adjacent_chr( $node, 0, 1, [ __CLASS__, 'get_next_textnode' ] ); |
|
157
|
|
|
} |
|
158
|
|
|
|
|
159
|
|
|
/** |
|
160
|
|
|
* Retrieves a character from the given \DOMNode. |
|
161
|
|
|
* |
|
162
|
|
|
* @since 5.0.0 |
|
163
|
|
|
* |
|
164
|
|
|
* @param \DOMNode $node Required. |
|
165
|
|
|
* @param int $position The position parameter for `substr`. |
|
166
|
|
|
* @param int $length The length parameter for `substr`. |
|
167
|
|
|
* @param callable $get_textnode A function to retrieve the \DOMText from the node. |
|
168
|
|
|
* |
|
169
|
|
|
* @return string The character or an empty string. |
|
170
|
|
|
*/ |
|
171
|
|
|
private static function get_adjacent_chr( \DOMNode $node, $position, $length, callable $get_textnode ) { |
|
172
|
|
|
$textnode = $get_textnode( $node ); |
|
173
|
|
|
|
|
174
|
|
|
if ( isset( $textnode ) && isset( $textnode->data ) ) { |
|
175
|
|
|
// Determine encoding. |
|
176
|
|
|
$func = Strings::functions( $textnode->data ); |
|
177
|
|
|
|
|
178
|
|
|
if ( ! empty( $func ) ) { |
|
179
|
|
|
return preg_replace( '/\p{C}/Su', '', $func['substr']( $textnode->data, $position, $length ) ); |
|
180
|
|
|
} |
|
181
|
|
|
} |
|
182
|
|
|
|
|
183
|
|
|
return ''; |
|
184
|
|
|
} |
|
185
|
|
|
|
|
186
|
|
|
/** |
|
187
|
|
|
* Retrieves the previous \DOMText sibling (if there is one). |
|
188
|
|
|
* |
|
189
|
|
|
* @param \DOMNode|null $node Optional. The content node. Default null. |
|
190
|
|
|
* |
|
191
|
|
|
* @return \DOMText|null Null if $node is a block-level element or no text sibling exists. |
|
192
|
|
|
*/ |
|
193
|
|
|
public static function get_previous_textnode( \DOMNode $node = null ) { |
|
194
|
|
|
return self::get_adjacent_textnode( function( &$another_node = null ) { |
|
195
|
|
|
$another_node = $another_node->previousSibling; |
|
196
|
|
|
return self::get_last_textnode( $another_node ); |
|
197
|
|
|
}, __METHOD__, $node ); |
|
198
|
|
|
} |
|
199
|
|
|
|
|
200
|
|
|
/** |
|
201
|
|
|
* Retrieves the next \DOMText sibling (if there is one). |
|
202
|
|
|
* |
|
203
|
|
|
* @param \DOMNode|null $node Optional. The content node. Default null. |
|
204
|
|
|
* |
|
205
|
|
|
* @return \DOMText|null Null if $node is a block-level element or no text sibling exists. |
|
206
|
|
|
*/ |
|
207
|
|
|
public static function get_next_textnode( \DOMNode $node = null ) { |
|
208
|
|
|
return self::get_adjacent_textnode( function( &$another_node = null ) { |
|
209
|
|
|
$another_node = $another_node->nextSibling; |
|
210
|
|
|
return self::get_first_textnode( $another_node ); |
|
211
|
|
|
}, __METHOD__, $node ); |
|
212
|
|
|
} |
|
213
|
|
|
|
|
214
|
|
|
/** |
|
215
|
|
|
* Retrieves an adjacent \DOMText sibling if there is one. |
|
216
|
|
|
* |
|
217
|
|
|
* @since 5.0.0 |
|
218
|
|
|
* |
|
219
|
|
|
* @param callable $iterate Takes a reference \DOMElement and returns a \DOMText (or null). |
|
220
|
|
|
* @param callable $get_adjacent_parent Takes a single \DOMElement parameter and returns a \DOMText (or null). |
|
221
|
|
|
* @param \DOMNode|null $node Optional. The content node. Default null. |
|
222
|
|
|
* |
|
223
|
|
|
* @return \DOMText|null Null if $node is a block-level element or no text sibling exists. |
|
224
|
|
|
*/ |
|
225
|
|
|
private static function get_adjacent_textnode( callable $iterate, callable $get_adjacent_parent, \DOMNode $node = null ) { |
|
226
|
|
View Code Duplication |
if ( ! isset( $node ) ) { |
|
227
|
|
|
return null; |
|
228
|
|
|
} elseif ( $node instanceof \DOMElement && isset( self::$block_tags[ $node->tagName ] ) ) { |
|
229
|
|
|
return null; |
|
230
|
|
|
} |
|
231
|
|
|
|
|
232
|
|
|
/** |
|
233
|
|
|
* The result node. |
|
234
|
|
|
* |
|
235
|
|
|
* @var \DOMText|null |
|
236
|
|
|
*/ |
|
237
|
|
|
$adjacent = null; |
|
238
|
|
|
$iterated_node = $node; |
|
239
|
|
|
|
|
240
|
|
|
// Iterate to find adjacent node. |
|
241
|
|
|
while ( null !== $iterated_node && null === $adjacent ) { |
|
242
|
|
|
$adjacent = $iterate( $iterated_node ); |
|
243
|
|
|
} |
|
244
|
|
|
|
|
245
|
|
|
// Last ressort. |
|
246
|
|
|
if ( null === $adjacent ) { |
|
247
|
|
|
$adjacent = $get_adjacent_parent( $node->parentNode ); |
|
248
|
|
|
} |
|
249
|
|
|
|
|
250
|
|
|
return $adjacent; |
|
251
|
|
|
} |
|
252
|
|
|
|
|
253
|
|
|
/** |
|
254
|
|
|
* Retrieves the first \DOMText child of the element. Block-level child elements are ignored. |
|
255
|
|
|
* |
|
256
|
|
|
* @param \DOMNode|null $node Optional. Default null. |
|
257
|
|
|
* @param bool $recursive Should be set to true on recursive calls. Optional. Default false. |
|
258
|
|
|
* |
|
259
|
|
|
* @return \DOMText|null The first child of type \DOMText, the element itself if it is of type \DOMText or null. |
|
260
|
|
|
*/ |
|
261
|
|
View Code Duplication |
public static function get_first_textnode( \DOMNode $node = null, $recursive = false ) { |
|
|
|
|
|
|
262
|
|
|
return self::get_edge_textnode( function( \DOMNodeList $children, \DOMText &$first_textnode = null ) { |
|
263
|
|
|
$i = 0; |
|
264
|
|
|
|
|
265
|
|
|
while ( $i < $children->length && empty( $first_textnode ) ) { |
|
266
|
|
|
$first_textnode = self::get_first_textnode( $children->item( $i ), true ); |
|
267
|
|
|
$i++; |
|
268
|
|
|
} |
|
269
|
|
|
}, $node, $recursive ); |
|
270
|
|
|
} |
|
271
|
|
|
|
|
272
|
|
|
/** |
|
273
|
|
|
* Retrieves the last \DOMText child of the element. Block-level child elements are ignored. |
|
274
|
|
|
* |
|
275
|
|
|
* @param \DOMNode|null $node Optional. Default null. |
|
276
|
|
|
* @param bool $recursive Should be set to true on recursive calls. Optional. Default false. |
|
277
|
|
|
* |
|
278
|
|
|
* @return \DOMText|null The last child of type \DOMText, the element itself if it is of type \DOMText or null. |
|
279
|
|
|
*/ |
|
280
|
|
View Code Duplication |
public static function get_last_textnode( \DOMNode $node = null, $recursive = false ) { |
|
|
|
|
|
|
281
|
|
|
return self::get_edge_textnode( function( \DOMNodeList $children, \DOMText &$last_textnode = null ) { |
|
282
|
|
|
$i = $children->length - 1; |
|
283
|
|
|
|
|
284
|
|
|
while ( $i >= 0 && empty( $last_textnode ) ) { |
|
285
|
|
|
$last_textnode = self::get_last_textnode( $children->item( $i ), true ); |
|
286
|
|
|
$i--; |
|
287
|
|
|
} |
|
288
|
|
|
}, $node, $recursive ); |
|
289
|
|
|
} |
|
290
|
|
|
|
|
291
|
|
|
/** |
|
292
|
|
|
* Retrieves an edge \DOMText child of the element specified by the callable. |
|
293
|
|
|
* Block-level child elements are ignored. |
|
294
|
|
|
* |
|
295
|
|
|
* @since 5.0.0 |
|
296
|
|
|
* |
|
297
|
|
|
* @param callable $iteration Takes two parameters, a \DOMNodeList and |
|
298
|
|
|
* a reference to the \DOMText used as the result. |
|
299
|
|
|
* @param \DOMNode|null $node Optional. Default null. |
|
300
|
|
|
* @param bool $recursive Should be set to true on recursive calls. Optional. Default false. |
|
301
|
|
|
* |
|
302
|
|
|
* @return \DOMText|null The last child of type \DOMText, the element itself if it is of type \DOMText or null. |
|
303
|
|
|
*/ |
|
304
|
|
|
private static function get_edge_textnode( callable $iteration, \DOMNode $node = null, $recursive = false ) { |
|
305
|
|
|
if ( ! isset( $node ) ) { |
|
306
|
|
|
return null; |
|
307
|
|
|
} |
|
308
|
|
|
|
|
309
|
|
View Code Duplication |
if ( $node instanceof \DOMText ) { |
|
|
|
|
|
|
310
|
|
|
return $node; |
|
311
|
|
|
} elseif ( ! $node instanceof \DOMElement ) { |
|
312
|
|
|
// Return null if $node is neither \DOMText nor \DOMElement. |
|
313
|
|
|
return null; |
|
314
|
|
|
} elseif ( $recursive && isset( self::$block_tags[ $node->tagName ] ) ) { |
|
315
|
|
|
return null; |
|
316
|
|
|
} |
|
317
|
|
|
|
|
318
|
|
|
$edge_textnode = null; |
|
319
|
|
|
|
|
320
|
|
|
if ( $node->hasChildNodes() ) { |
|
321
|
|
|
$iteration( $node->childNodes, $edge_textnode ); |
|
322
|
|
|
} |
|
323
|
|
|
|
|
324
|
|
|
return $edge_textnode; |
|
325
|
|
|
} |
|
326
|
|
|
|
|
327
|
|
|
/** |
|
328
|
|
|
* Returns the nearest block-level parent (or null). |
|
329
|
|
|
* |
|
330
|
|
|
* @param \DOMNode $node Required. |
|
331
|
|
|
* |
|
332
|
|
|
* @return \DOMElement|null |
|
333
|
|
|
*/ |
|
334
|
|
|
public static function get_block_parent( \DOMNode $node ) { |
|
335
|
|
|
$parent = $node->parentNode; |
|
336
|
|
|
if ( ! $parent instanceof \DOMElement ) { |
|
337
|
|
|
return null; |
|
338
|
|
|
} |
|
339
|
|
|
|
|
340
|
|
|
while ( ! isset( self::$block_tags[ $parent->tagName ] ) && $parent->parentNode instanceof \DOMElement ) { |
|
341
|
|
|
/** |
|
342
|
|
|
* The parent is sure to be a \DOMElement. |
|
343
|
|
|
* |
|
344
|
|
|
* @var \DOMElement |
|
345
|
|
|
*/ |
|
346
|
|
|
$parent = $parent->parentNode; |
|
347
|
|
|
} |
|
348
|
|
|
|
|
349
|
|
|
return $parent; |
|
350
|
|
|
} |
|
351
|
|
|
|
|
352
|
|
|
/** |
|
353
|
|
|
* Retrieves the tag name of the nearest block-level parent. |
|
354
|
|
|
* |
|
355
|
|
|
* @param \DOMNode $node A node. |
|
356
|
|
|
|
|
357
|
|
|
* @return string The tag name (or the empty string). |
|
358
|
|
|
*/ |
|
359
|
|
|
public static function get_block_parent_name( \DOMNode $node ) { |
|
360
|
|
|
$parent = self::get_block_parent( $node ); |
|
361
|
|
|
|
|
362
|
|
|
if ( ! empty( $parent ) ) { |
|
363
|
|
|
return $parent->tagName; |
|
364
|
|
|
} else { |
|
365
|
|
|
return ''; |
|
366
|
|
|
} |
|
367
|
|
|
} |
|
368
|
|
|
} |
|
369
|
|
|
|
|
370
|
|
|
/** |
|
371
|
|
|
* Initialize block tags on load. |
|
372
|
|
|
*/ |
|
373
|
|
|
DOM::block_tags(); // @codeCoverageIgnore |
|
374
|
|
|
|
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.