Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Pull Request — master (#52)
by Der Mundschenk
03:57
created

DOM::get_adjacent_textnode()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 31
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 31
ccs 10
cts 10
cp 1
rs 8.439
c 0
b 0
f 0
cc 6
eloc 10
nc 5
nop 3
crap 6
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 modify
9
 *  it under the terms of the GNU General Public License as published by
10
 *  the Free Software Foundation; either version 2 of the License, or
11
 *  (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 along
19
 *  with this program; if not, write to the Free Software Foundation, Inc.,
20
 *  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
use Masterminds\HTML5\Elements;
31
32
/**
33
 * Some static methods for DOM manipulation.
34
 *
35
 * @since 4.2.0
36
 */
37
abstract class DOM {
38
39
	/**
40
	 * An array of block tag names.
41
	 *
42
	 * @var array
43
	 */
44
	private static $block_tags;
45
46
	/**
47
	 * An array of tags that should never be modified.
48
	 *
49
	 * @var array
50
	 */
51
	private static $inappropriate_tags;
52
53
	const ADDITIONAL_INAPPROPRIATE_TAGS = [
54
		'button',
55
		'select',
56
		'optgroup',
57
		'option',
58
		'map',
59
		'head',
60
		'applet',
61
		'object',
62
		'svg',
63
		'math',
64
	];
65
66
	/**
67
	 * Retrieves an array of block tags.
68
	 *
69
	 * @param bool $reset Optional. Default false.
70
	 *
71
	 * @return array {
72
	 *         An array of boolean values indexed by tagname.
73
	 *
74
	 *         @type bool $tag `true` if the tag is a block tag.
75
	 * }
76
	 */
77 1
	public static function block_tags( $reset = false ) {
78 1
		if ( empty( self::$block_tags ) || $reset ) {
79 1
			self::$block_tags = array_merge(
80 1
				array_flip( array_filter( array_keys( Elements::$html5 ), function( $tag ) {
81 1
					return Elements::isA( $tag, Elements::BLOCK_TAG );
82 1
				} ) ),
83 1
				array_flip( [ 'li', 'td', 'dt' ] ) // not included as "block tags" in current HTML5-PHP version.
84
			);
85
		}
86
87 1
		return self::$block_tags;
88
	}
89
90
	/**
91
	 * Retrieves an array of tags that we should never touch.
92
	 *
93
	 * @param bool $reset Optional. Default false.
94
	 *
95
	 * @return array {
96
	 *         An array of boolean values indexed by tagname.
97
	 *
98
	 *         @type bool $tag `true` if the tag should never be modified in any way.
99
	 * }
100
	 */
101 1
	public static function inappropriate_tags( $reset = false ) {
102 1
		if ( empty( self::$inappropriate_tags ) || $reset ) {
103 1
			self::$inappropriate_tags = array_flip( array_merge(
104 1
				array_filter( array_keys( Elements::$html5 ), function( $tag ) {
105 1
					return Elements::isA( $tag, Elements::VOID_TAG )
106 1
						|| Elements::isA( $tag, Elements::TEXT_RAW )
107 1
						|| Elements::isA( $tag, Elements::TEXT_RCDATA );
108 1
				} ),
109 1
				self::ADDITIONAL_INAPPROPRIATE_TAGS
110
			) );
111
		}
112
113 1
		return self::$inappropriate_tags;
114
	}
115
116
	/**
117
	 * Converts \DOMNodeList to array;
118
	 *
119
	 * @param \DOMNodeList $list Required.
120
	 *
121
	 * @return array An associative array in the form ( $spl_object_hash => $node ).
122
	 */
123 1
	public static function nodelist_to_array( \DOMNodeList $list ) {
124 1
		$out = [];
125
126 1
		foreach ( $list as $node ) {
127 1
			$out[ spl_object_hash( $node ) ] = $node;
128
		}
129
130 1
		return $out;
131
	}
132
133
	/**
134
	 * Retrieves an array containing all the ancestors of the node. This could be done
135
	 * via an XPath query for "ancestor::*", but DOM walking is in all likelyhood faster.
136
	 *
137
	 * @param \DOMNode $node Required.
138
	 *
139
	 * @return array An array of \DOMNode.
140
	 */
141 1
	public static function get_ancestors( \DOMNode $node ) {
142 1
		$result = [];
143
144 1
		while ( ( $node = $node->parentNode ) && ( $node instanceof \DOMElement ) ) { // @codingStandardsIgnoreLine.
145 1
			$result[] = $node;
146
		}
147
148 1
		return $result;
149
	}
150
151
	/**
152
	 * Checks whether the \DOMNode has one of the given classes.
153
	 * If $tag is a \DOMText, the parent DOMElement is checked instead.
154
	 *
155
	 * @param \DOMNode     $tag        An element or textnode.
156
	 * @param string|array $classnames A single classname or an array of classnames.
157
	 *
158
	 * @return bool True if the element has any of the given class(es).
159
	 */
160 10
	public static function has_class( \DOMNode $tag, $classnames ) {
161 10
		if ( $tag instanceof \DOMText ) {
162 1
			$tag = $tag->parentNode; // @codingStandardsIgnoreLine.
163
		}
164
165
		// Bail if we are not working with a tag or if there is no classname.
166 10
		if ( ! ( $tag instanceof \DOMElement ) || empty( $classnames ) ) {
167 2
			return false;
168
		}
169
170
		// Ensure we always have an array of classnames.
171 8
		if ( ! is_array( $classnames ) ) {
172 5
			$classnames = [ $classnames ];
173
		}
174
175 8
		if ( $tag->hasAttribute( 'class' ) ) {
176 7
			$tag_classes = array_flip( explode( ' ', $tag->getAttribute( 'class' ) ) );
177
178 7
			foreach ( $classnames as $classname ) {
179 7
				if ( isset( $tag_classes[ $classname ] ) ) {
180 7
					return true;
181
				}
182
			}
183
		}
184
185 3
		return false;
186
	}
187
188
	/**
189
	 * Retrieves the last character of the previous \DOMText sibling (if there is one).
190
	 *
191
	 * @param \DOMNode $node The content node.
192
	 *
193
	 * @return string A single character (or the empty string).
194
	 */
195 1
	public static function get_prev_chr( \DOMNode $node ) {
196 1
		return self::get_adjacent_chr( $node, -1, 1, [ __CLASS__, 'get_previous_textnode' ] );
197
	}
198
199
	/**
200
	 * Retrieves the first character of the next \DOMText sibling (if there is one).
201
	 *
202
	 * @param \DOMNode $node The content node.
203
	 *
204
	 * @return string A single character (or the empty string).
205
	 */
206 1
	public static function get_next_chr( \DOMNode $node ) {
207 1
		return self::get_adjacent_chr( $node, 0, 1, [ __CLASS__, 'get_next_textnode' ] );
208
	}
209
210
	/**
211
	 * Retrieves a character from the given \DOMNode.
212
	 *
213
	 * @since 5.0.0
214
	 *
215
	 * @param  \DOMNode $node         Required.
216
	 * @param  int      $position     The position parameter for `substr`.
217
	 * @param  int      $length       The length parameter for `substr`.
218
	 * @param  callable $get_textnode A function to retrieve the \DOMText from the node.
219
	 *
220
	 * @return string The character or an empty string.
221
	 */
222 2
	private static function get_adjacent_chr( \DOMNode $node, $position, $length, callable $get_textnode ) {
223 2
		$textnode = $get_textnode( $node );
224
225 2
		if ( isset( $textnode ) && isset( $textnode->data ) ) {
226
			// Determine encoding.
227 2
			$func = Strings::functions( $textnode->data );
228
229 2
			if ( ! empty( $func ) ) {
230 2
				return preg_replace( '/\p{C}/Su', '', $func['substr']( $textnode->data, $position, $length ) );
231
			}
232
		}
233
234 2
		return '';
235
	}
236
237
	/**
238
	 * Retrieves the previous \DOMText sibling (if there is one).
239
	 *
240
	 * @param \DOMNode|null $node Optional. The content node. Default null.
241
	 *
242
	 * @return \DOMText|null Null if $node is a block-level element or no text sibling exists.
243
	 */
244
	public static function get_previous_textnode( \DOMNode $node = null ) {
245 2
		return self::get_adjacent_textnode( function( &$another_node = null ) {
246 1
			$another_node = $another_node->previousSibling;
247 1
			return self::get_last_textnode( $another_node );
248 2
		}, [ __CLASS__, __FUNCTION__ ], $node );
249
	}
250
251
	/**
252
	 * Retrieves the next \DOMText sibling (if there is one).
253
	 *
254
	 * @param \DOMNode|null $node Optional. The content node. Default null.
255
	 *
256
	 * @return \DOMText|null Null if $node is a block-level element or no text sibling exists.
257
	 */
258
	public static function get_next_textnode( \DOMNode $node = null ) {
259 2
		return self::get_adjacent_textnode( function( &$another_node = null ) {
260 1
			$another_node = $another_node->nextSibling;
261 1
			return self::get_first_textnode( $another_node );
262 2
		}, [ __CLASS__, __FUNCTION__ ], $node );
263
	}
264
265
	/**
266
	 * Retrieves an adjacent \DOMText sibling if there is one.
267
	 *
268
	 * @since 5.0.0
269
	 *
270
	 * @param callable      $iterate             Takes a reference \DOMElement and returns a \DOMText (or null).
271
	 * @param callable      $get_adjacent_parent Takes a single \DOMElement parameter and returns a \DOMText (or null).
272
	 * @param \DOMNode|null $node                Optional. The content node. Default null.
273
	 *
274
	 * @return \DOMText|null Null if $node is a block-level element or no text sibling exists.
275
	 */
276 4
	private static function get_adjacent_textnode( callable $iterate, callable $get_adjacent_parent, \DOMNode $node = null ) {
277 4
		if ( ! isset( $node ) || self::is_block_tag( $node ) ) {
278 4
			return null;
279
		}
280
281
		/**
282
		 * The result node.
283
		 *
284
		 * @var \DOMText|null
285
		 */
286 2
		$adjacent = null;
287
288
		/**
289
		 * The iterated node.
290
		 *
291
		 * @var \DOMNode|null
292
		 */
293 2
		$iterated_node = $node;
294
295
		// Iterate to find adjacent node.
296 2
		while ( null !== $iterated_node && null === $adjacent ) {
297 2
			$adjacent = $iterate( $iterated_node );
298
		}
299
300
		// Last ressort.
301 2
		if ( null === $adjacent ) {
302 2
			$adjacent = $get_adjacent_parent( $node->parentNode );
303
		}
304
305 2
		return $adjacent;
306
	}
307
308
	/**
309
	 * Retrieves the first \DOMText child of the element. Block-level child elements are ignored.
310
	 *
311
	 * @param \DOMNode|null $node      Optional. Default null.
312
	 * @param bool          $recursive Should be set to true on recursive calls. Optional. Default false.
313
	 *
314
	 * @return \DOMText|null The first child of type \DOMText, the element itself if it is of type \DOMText or null.
315
	 */
316 3
	public static function get_first_textnode( \DOMNode $node = null, $recursive = false ) {
317 3
		return self::get_edge_textnode( [ __CLASS__, __FUNCTION__ ], $node, $recursive, false );
318
	}
319
320
	/**
321
	 * Retrieves the last \DOMText child of the element. Block-level child elements are ignored.
322
	 *
323
	 * @param \DOMNode|null $node      Optional. Default null.
324
	 * @param bool          $recursive Should be set to true on recursive calls. Optional. Default false.
325
	 *
326
	 * @return \DOMText|null The last child of type \DOMText, the element itself if it is of type \DOMText or null.
327
	 */
328 3
	public static function get_last_textnode( \DOMNode $node = null, $recursive = false ) {
329 3
		return self::get_edge_textnode( [ __CLASS__, __FUNCTION__ ], $node, $recursive, true );
330
	}
331
332
	/**
333
	 * Retrieves an edge \DOMText child of the element specified by the callable.
334
	 * Block-level child elements are ignored.
335
	 *
336
	 * @since 5.0.0
337
	 *
338
	 * @param callable      $get_textnode Takes two parameters, a \DOMNode and a boolean flag for recursive calls.
339
	 * @param \DOMNode|null $node         Optional. Default null.
340
	 * @param bool          $recursive    Should be set to true on recursive calls. Optional. Default false.
341
	 * @param bool          $reverse      Whether to iterate forward or backward. Optional. Default false.
342
	 *
343
	 * @return \DOMText|null The last child of type \DOMText, the element itself if it is of type \DOMText or null.
344
	 */
345 6
	private static function get_edge_textnode( callable $get_textnode, \DOMNode $node = null, $recursive = false, $reverse = false ) {
346 6
		if ( ! isset( $node ) ) {
347 2
			return null;
348
		}
349
350 6
		if ( $node instanceof \DOMText ) {
351 2
			return $node;
352 6
		} elseif ( ! $node instanceof \DOMElement || $recursive && self::is_block_tag( $node ) ) {
353
			// Return null if $node is neither \DOMText nor \DOMElement or
354
			// when we are recursing and already at the block level.
355 4
			return null;
356
		}
357
358 4
		$edge_textnode = null;
359
360 4
		if ( $node->hasChildNodes() ) {
361 4
			$children    = $node->childNodes;
362 4
			$max         = $children->length;
363 4
			$index       = $reverse ? $max - 1 : 0;
364 4
			$incrementor = $reverse ? -1 : +1;
365
366 4
			while ( $index >= 0 && $index < $max && null === $edge_textnode ) {
367 4
				$edge_textnode = $get_textnode( $children->item( $index ), true );
368 4
				$index        += $incrementor;
369
			}
370
		}
371
372 4
		return $edge_textnode;
373
	}
374
375
	/**
376
	 * Returns the nearest block-level parent (or null).
377
	 *
378
	 * @param \DOMNode $node Required.
379
	 *
380
	 * @return \DOMElement|null
381
	 */
382 8
	public static function get_block_parent( \DOMNode $node ) {
383 8
		$parent = $node->parentNode;
384 8
		if ( ! $parent instanceof \DOMElement ) {
385 1
			return null;
386
		}
387
388 7
		while ( ! self::is_block_tag( $parent ) && $parent->parentNode instanceof \DOMElement ) {
389
			/**
390
			 * The parent is sure to be a \DOMElement.
391
			 *
392
			 * @var \DOMElement
393
			 */
394 4
			$parent = $parent->parentNode;
395
		}
396
397 7
		return $parent;
398
	}
399
400
	/**
401
	 * Retrieves the tag name of the nearest block-level parent.
402
	 *
403
	 * @param \DOMNode $node A node.
404
405
	 * @return string The tag name (or the empty string).
406
	 */
407 8
	public static function get_block_parent_name( \DOMNode $node ) {
408 8
		$parent = self::get_block_parent( $node );
409
410 8
		if ( ! empty( $parent ) ) {
411 7
			return $parent->tagName;
412
		} else {
413 1
			return '';
414
		}
415
	}
416
417
	/**
418
	 * Determines if a node is a block tag.
419
	 *
420
	 * @since 6.0.0
421
	 *
422
	 * @param  \DOMNode $node Required.
423
	 *
424
	 * @return bool
425
	 */
426 12
	public static function is_block_tag( \DOMNode $node ) {
427 12
		return $node instanceof \DOMElement && isset( self::$block_tags[ $node->tagName ] );
428
	}
429
}
430
431
/**
432
 *  Initialize block tags on load.
433
 */
434
DOM::block_tags(); // @codeCoverageIgnore
435