-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathinsert-content.php
440 lines (378 loc) · 14.2 KB
/
insert-content.php
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
<?php
/**
* Insert Content Between HTML Paragraphs.
*
* Functions to insert content after a number of HTML paragraphs in a string containing HTML (with paragraphs).
*
* For more information visit: https://github.com/keesiemeijer/insert-content
*
* Insert Content Between HTML Paragraphs is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* any later version.
*
* Additional Content is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Additional Content. If not, see <http://www.gnu.org/licenses/>.
*
* @license GPL-2.0+
* @author keesiemeijer
* @version 3.0.0
*/
namespace keesiemeijer\Insert_Content;
/**
* Get default arguments.
*
* @return array Array with default arguments.
*/
function get_defaults() {
return array(
'parent_element_id' => '',
'insert_element' => 'p',
'insert_after_p' => '',
'insert_every_p' => '',
'insert_if_no_p' => true,
'top_level_p_only' => true,
);
}
/**
* Insert content in HTML (containing HTML paragraphs).
*
* By default content is inserted in the middle of all paragraphs.
* i.e. If the HTML contains two paragraphs it will be inserted after the first.
*
* If no paragraphs are found in the HTML the inserted contend will be appended to the HTML.
*
* Note: The content you want to insert will be wrapped a HTML paragraph element (<p></p>) by default.
* Use the $args['insert_element'] parameter to change it to another Block-level HTML element.
*
* @param string $content String of content (with paragraphs) where you want to insert content in.
* @param string|array $insert_content String of content you want to insert or array of content strings for the $insert_every_p argument.
* @param array $args {
* Optional. Array with optional arguments.
*
* @type string $parent_element_id Parent element id to search paragraphs in.
* Default: empty string. Search for paragraphs in the entire content.
* @type string $insert_element Block-level HTML element the inserted content ($insert_content) is wrapped in.
* Default 'p'. (e.g. 'p', 'div', etc.)
* @type int $insert_after_p Insert content after a number of paragraphs.
* Default empty string. The content is inserted after the middle paragraph.
* @type bool $insert_if_no_p Insert content even if the required number of paragraphs from 'insert_after_p' (above) are not found.
* Default true. Insert after the last paragraph.
* Or insert after the content if no paragraphs are found.
* @type int $insert_every_p Insert content after every number of paragraphs.
* @type bool $top_level_p_only Insert content after top level paragraphs only.
* Default true (recommended)
* }
* @return string String with inserted content.
*/
function insert_content( $content, $insert_content = '', $args = array() ) {
$insert_content = is_string( $insert_content ) ? array( $insert_content ) : $insert_content;
if ( empty( $insert_content ) || ! is_array( $insert_content ) ) {
return $content;
}
$insert_content = array_values( $insert_content );
$args = array_merge( get_defaults(), (array) $args );
// Validate arguments.
$args['parent_element_id'] = trim( (string) $args['parent_element_id'] );
$args['insert_element'] = trim( (string) $args['insert_element'] );
$args['insert_element'] = $args['insert_element'] ? $args['insert_element'] : 'p';
$args['insert_after_p'] = abs( intval( $args['insert_after_p'] ) );
$args['insert_every_p'] = abs( intval( $args['insert_every_p'] ) );
$parent_element = false;
$nodes = new \DOMDocument();
// Load the HTML nodes from the content.
@$nodes->loadHTML( mb_convert_encoding( $content, 'HTML-ENTITIES', 'UTF-8' ) );
if ( $args['parent_element_id'] ) {
$parent_element = $nodes->getElementById( $args['parent_element_id'] );
if ( ! $parent_element ) {
// Parent element not found.
return $content;
}
// Get all paragraphs from the parent element.
$p = $parent_element->getElementsByTagName( 'p' );
} else {
// Get all paragraphs from the content.
$p = $nodes->getElementsByTagName( 'p' );
}
$insert_elements = get_insert_elements( $insert_content, $args );
if ( ! $insert_elements ) {
return $content;
}
// Get paragraph indexes where content could be inserted after.
$nodelist = get_node_indexes( $p, $args );
// Check if paragraphs are found.
if ( ! empty( $nodelist ) ) {
$inserted = insert_nodes( $nodes, $insert_elements, $p, $nodelist, $args );
if ( ! $inserted ) {
return $content;
}
$html = get_HTML( $nodes );
if ( $html ) {
$content = $html;
}
} else {
// No paragraphs found.
if ( (bool) $args['insert_if_no_p'] ) {
if ( $parent_element ) {
// Insert content after parent element.
insert_content_element( $nodes, $parent_element, $insert_elements[0] );
$html = get_HTML( $nodes );
if ( $html ) {
$content = $html;
}
} else {
$insert_content = "<{$args['insert_element']}>{$insert_content[0]}</{$args['insert_element']}>";
// Add insert content after the content.
$content .= $insert_content;
}
}
}
return $content;
}
/**
* Inserts nodes
*
* @param object $nodes DOMNodeList instance containing all elements.
* @param array $insert_elements Array of DOMElement objects to insert.
* @param object $p DOMNodeList instance containing all the p elements.
* @param array $nodelist Array with HTML paragraph indexes.
* @param array $args Optional arguments. See: insert_content().
* @return bool True if the node was inserted.
*/
function insert_nodes( $nodes, $insert_elements, $p, $nodelist, $args ) {
// Back compat
$insert_elements = is_string( $insert_elements ) ? array( $insert_elements ) : $insert_elements;
$insert_elements = array_values( $insert_elements );
$_args = $args;
if ( $args['insert_every_p'] ) {
$node_count = count( $nodelist );
$step = (int) $args['insert_every_p'];
if ( ( $step + $step ) > $node_count ) {
$args['insert_every_p'] = '';
$args['insert_after_p'] = $step;
return insert_node( $nodes, $insert_elements[0], $p, $nodelist, $args );
}
$range = range( $step, $node_count, $step );
$inserted = false;
$el_index = 0;
$last_index = 0;
// Find the last index
foreach ( $range as $index ) {
$el_index = isset( $insert_elements[ $el_index ] ) ? $el_index : 0;
$last_index = $el_index;
$el_index++;
}
// Loop backwards when inserting to not change the nodes indexes.
$range = array_values( array_reverse( $range ) );
// Set start index to last index
$el_index = $last_index;
foreach ( $range as $key => $index ) {
$_args['insert_after_p'] = $index;
$el_index = isset( $insert_elements[ $el_index ] ) ? $el_index : 0;
$inserted = insert_node( $nodes, $insert_elements[ $el_index ], $p, $nodelist, $_args );
if ( ! $inserted ) {
break;
}
$el_index++;
}
return $inserted;
} else {
return insert_node( $nodes, $insert_elements[0], $p, $nodelist, $args );
}
}
/**
* Inserts a node.
*
* @param object $nodes DOMNodeList instance containing all elements.
* @param object $insert_element DOMElement object to insert.
* @param object $p DOMNodeList instance containing all the p elements.
* @param array $nodelist Array with HTML paragraph indexes.
* @param array $args Optional arguments. See: insert_content().
* @return bool True if the node was inserted
*/
function insert_node( $nodes, $insert_element, $p, $nodelist, $args ) {
$insert_index = get_item_index( $nodelist, $args );
if ( false === $insert_index ) {
return false;
}
// Insert content after this (paragraph) node.
$insert_node = $p->item( $insert_index );
// Insert the nodes.
insert_content_element( $nodes, $insert_node, $insert_element );
return true;
}
/**
* Insert an element (and it's child elements) in the content.
*
* @param object $nodes DOMDocument Object for the content.
* @param object $insert_node DOMElement object to insert nodes after.
* @param object $insert_element DOMElement object to insert.
* @return void
*/
function insert_content_element( $nodes, $insert_node, $insert_element ) {
$next_sibling = isset( $insert_node->nextSibling ) ? $insert_node->nextSibling : false;
if ( $next_sibling ) {
// get sibling element (exluding text nodes and whitespace).
$next_sibling = nextElementSibling( $insert_node );
}
if ( $next_sibling ) {
// Insert before next sibling.
$next_sibling->parentNode->insertBefore( $nodes->importNode( $insert_element, true ), $next_sibling );
} else {
// Insert as child of parent element.
$insert_node->parentNode->appendChild( $nodes->importNode( $insert_element, true ) );
}
}
/**
* Returns the next sibling of a node.
*
* @param object $node DOMElement paragraph object.
* @return object Next sibling object.
*/
function nextElementSibling( $node ) {
while ( $node && ( $node = $node->nextSibling ) ) {
if ( $node instanceof \DOMElement ) {
break;
}
}
return $node;
}
/**
* Get Dom elements with inserted content.
*
* @param string|array $insert_content String of content you want to insert or
* array of content strings.
* @param array $args Optional arguments. See: insert_content().
* @return array Array of DOMElement objects.
*/
function get_insert_elements( $insert_content, $args ) {
$insert_elements = is_string( $insert_content ) ? array( $insert_content ) : $insert_content;
if ( ! is_array( $insert_elements ) || ! $insert_elements ) {
return array();
}
foreach ( $insert_elements as $key => $element ) {
$el = get_insert_element( $element, $args );
if ( ! $el ) {
unset( $insert_elements[ $key ] );
continue;
}
$insert_elements[ $key ] = $el;
}
return array_values( $insert_elements );
}
/**
* Get element with content to insert.
*
* @param string $insert_content Content to insert.
* @param array $args Optional arguments. See: insert_content().
* @return object|bool DOMElement object with inserted content or false.
*/
function get_insert_element( $insert_content, $args ) {
if ( ! is_string( $insert_content ) ) {
return '';
}
$insert_nodes = new \DOMDocument();
// Content wrapped in the parent HTML element (to be inserted).
$insert_content = "<{$args['insert_element']}>{$insert_content}</{$args['insert_element']}>";
// Load the HTML nodes from the content to insert.
@$insert_nodes->loadHTML( mb_convert_encoding( $insert_content, 'HTML-ENTITIES', 'UTF-8' ) );
$insert_element = $insert_nodes->getElementsByTagName( $args['insert_element'] );
return $insert_element->length ? $insert_element->item( 0 ) : false;
}
/**
* Returns indexes from a DOMNodeList instance containing HTML paragraphs.
* Nested HTML paragraphs are excluded if $args['top_level_p_only'] is set to true.
*
* @param object $nodes DOMNodeList instance containing all the p elements.
* @param array $args Optional arguments. See: insert_content().
* @return array Array with HTML paragraph indexes.
*/
function get_node_indexes( $nodes, $args ) {
$args = array_merge( get_defaults(), (array) $args );
$nodelist = array();
$length = isset( $nodes->length ) ? $nodes->length : 0;
$parent_id = trim( $args['parent_element_id'] );
for ( $i = 0; $i < $length; ++$i ) {
$nodelist[ $i ] = $i;
$parent = false;
$node = $nodes->item( $i );
if ( $parent_id ) {
if ( $node->parentNode->hasAttribute( 'id' ) ) {
$parent_id_attr = $node->parentNode->getAttribute( 'id' );
$parent = ( $parent_id === $parent_id_attr );
}
} else {
$parent = ( 'body' === $node->parentNode->nodeName );
}
if ( (bool) $args['top_level_p_only'] && ! $parent ) {
// Remove nested paragraphs from the list.
unset( $nodelist[ $i ] );
}
}
return array_values( $nodelist );
}
/**
* Returns the index (for the paragraph) to insert content after.
* Uses $args['insert_after_p'] to calculate the index.
*
* @param array $nodelist Array with HTML paragraph indexes.
* @param array $args Optional arguments. See: insert_content().
* @return int|false Index of the (paragraph) node or false.
*/
function get_item_index( $nodelist, $args ) {
if ( empty( $nodelist ) ) {
return false;
}
$args = array_merge( get_defaults(), (array) $args );
$count = count( $nodelist );
$insert_index = abs( intval( $args['insert_after_p'] ) );
end( $nodelist );
$last = key( $nodelist );
reset( $nodelist );
if ( ! $insert_index ) {
if ( 1 < $count ) {
// More than one paragraph found.
// Get middle position to insert the HTML.
$insert_index = $nodelist[ floor( $count / 2 ) - 1 ];
} else {
// One paragraph.
$insert_index = $last;
}
} else {
// start counting at 0.
--$insert_index;
--$count;
if ( $insert_index > $count ) {
if ( (bool) $args['insert_if_no_p'] ) {
// insert after last paragraph.
$insert_index = $last;
} else {
return false;
}
}
}
return $nodelist[ $insert_index ];
}
/**
* Returns the HTML from a DOMDocument object as a string.
* Returns only the HTML from the body element (added by DOMDocument->saveHTML()).
*
* @param object $nodes DOMDocument Object for the content.
* @return string Html.
*/
function get_HTML( $nodes ) {
$body_node = $nodes->getElementsByTagName( 'body' )->item( 0 );
if ( $body_node ) {
// Convert nodes from the body element to a string containing HTML.
$content = $nodes->saveHTML( $body_node );
// Remove first body element only.
$replace_count = 1;
return str_replace( array( '<body>', '</body>' ) , array( '', '' ), $content, $replace_count );
}
return '';
}