PluginProbe ʕ •ᴥ•ʔ
Atarim – Visual Feedback, Review & AI Collaboration / 5.0
Atarim – Visual Feedback, Review & AI Collaboration v5.0
5.0 trunk 3.10 3.11 3.12 3.13 3.14 3.15 3.16 3.17 3.18 3.19 3.2.0 3.2.1 3.22 3.22.1 3.22.2 3.22.3 3.22.4 3.22.5 3.22.6 3.3.0 3.3.1 3.3.2 3.3.2.1 3.3.2.2 3.3.3 3.30 3.31 3.32 3.4 3.4.1 3.4.3 3.4.4 3.5 3.5.1 3.6 3.6.1 3.7 3.8 3.9 3.9.1 3.9.2 3.9.3 3.9.4 3.9.6 3.9.6.1 4.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.2 4.1.3 4.2 4.2.1 4.2.2 4.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.4
atarim-visual-collaboration / includes / do-it.php
atarim-visual-collaboration / includes Last commit date
class-ajax-functions.php 2 weeks ago class-define-constant.php 2 weeks ago class-functions.php 2 weeks ago do-it.php 2 weeks ago inject-script.php 2 weeks ago
do-it.php
724 lines
1 <?php
2
3 if ( ! defined('ABSPATH') ) exit;
4
5 add_action('wp_enqueue_scripts', function () {
6
7 if ( ! is_singular() ) return;
8 if ( ! is_user_logged_in() ) return;
9 if ( ! current_user_can('edit_posts') ) return;
10
11 $post_id = get_queried_object_id();
12 if ( ! $post_id ) return;
13
14 $page_builder = atarim_detect_page_builder($post_id);
15 $wrapper_hint = atarim_detect_wrapper_selector_by_theme(); // '' if unknown
16
17 $handle = 'atarim-do-it';
18 wp_register_script($handle, '', [], '0.3.1', false);
19 wp_enqueue_script($handle);
20
21 $atarim_inline_data = [
22 'postId' => (int) $post_id,
23 'pageBuilder' => $page_builder,
24 'wrapperHint' => $wrapper_hint,
25 'apiGet' => esc_url_raw(rest_url('atarim/v1/content/get')),
26 'apiSave' => esc_url_raw(rest_url('atarim/v1/content/save')),
27 'nonce' => wp_create_nonce('wp_rest'),
28 ];
29
30 $atarim_inline_data = apply_filters('atarim_inline_data', $atarim_inline_data, $post_id);
31 wp_localize_script($handle, 'ATARIM_INLINE', $atarim_inline_data);
32
33 /* This is how to use filter
34 add_filter('atarim_inline_data', function ($data, $post_id) {
35 if (empty($data['wrapperHint'])) {
36 $data['wrapperHint'] = '.site-main .entry-content';
37 }
38 return $data;
39 }, 10, 2);*/
40 });
41
42 /* ===========================
43 * Block identity injection (for editors only)
44 * Marks every rendered block with data-atarim-block-name
45 * and data-atarim-anchor-index so the frontend can identify
46 * blocks reliably without DOM heuristics.
47 * =========================== */
48 add_action('template_redirect', function () {
49
50 if ( ! is_singular() ) return;
51 if ( ! is_user_logged_in() ) return;
52 if ( ! current_user_can('edit_posts') ) return;
53
54 $post_id = get_queried_object_id();
55 if ( ! $post_id ) return;
56
57 if ( atarim_detect_page_builder($post_id) !== 'block' ) return;
58
59 $GLOBALS['atarim_block_counters'] = [];
60
61 add_filter('render_block', 'atarim_inject_block_identity', 10, 2);
62 });
63
64 function atarim_inject_block_identity(string $block_content, array $block): string {
65
66 if (empty($block['blockName'])) return $block_content;
67 if (trim($block_content) === '') return $block_content;
68
69 $block_name = $block['blockName'];
70
71 if (!isset($GLOBALS['atarim_block_counters'][$block_name])) {
72 $GLOBALS['atarim_block_counters'][$block_name] = 0;
73 }
74 $anchor_index = $GLOBALS['atarim_block_counters'][$block_name];
75 $GLOBALS['atarim_block_counters'][$block_name]++;
76
77 if (class_exists('WP_HTML_Tag_Processor')) {
78 $tags = new WP_HTML_Tag_Processor($block_content);
79 if ($tags->next_tag()) {
80 $tags->set_attribute('data-atarim-block-name', $block_name);
81 $tags->set_attribute('data-atarim-anchor-index', (string) $anchor_index);
82 return $tags->get_updated_html();
83 }
84 return $block_content;
85 }
86
87 // Fallback for older WP versions
88 $attrs = sprintf(
89 ' data-atarim-block-name="%s" data-atarim-anchor-index="%d"',
90 esc_attr($block_name),
91 $anchor_index
92 );
93
94 return preg_replace(
95 '/^(\s*<[a-zA-Z][a-zA-Z0-9]*)\b/',
96 '$1' . $attrs,
97 $block_content,
98 1
99 );
100 }
101
102 add_action('rest_api_init', function () {
103
104 $permission_callback = function( WP_REST_Request $request ) {
105 if ( empty( get_option( 'avc_enable_doit', false ) ) ) {
106 return new WP_Error(
107 'avc_doit_disabled',
108 __( 'Do It via Atarim AI is disabled for this site. Enable it from the Atarim plugin settings to allow execution.', 'atarim-visual-collaboration' ),
109 [ 'status' => 403 ]
110 );
111 }
112
113 $post_id = absint($request->get_param('postId'));
114 if ($post_id) return current_user_can('edit_post', $post_id);
115 return current_user_can('edit_posts');
116 };
117
118 register_rest_route('atarim/v1', '/content/get', [
119 'methods' => 'POST',
120 'callback' => 'atarim_inline_get_handler',
121 'permission_callback' => $permission_callback,
122 ]);
123
124 register_rest_route('atarim/v1', '/content/save', [
125 'methods' => 'POST',
126 'callback' => 'atarim_inline_save_handler',
127 'permission_callback' => $permission_callback,
128 ]);
129
130 // ---------------------------------------------------------------------
131 // Core connection probe — NOT a DoIt route. Public and intentionally
132 // ungated: the Atarim app calls it cross-origin and unauthenticated,
133 // before any connection/token exists, to decide "Connect" vs "Install".
134 // It deliberately does NOT use $permission_callback (the avc_enable_doit
135 // gate) above. Lives here only because this file already registers the
136 // atarim/v1 namespace; move to its own home if more core routes appear.
137 // ---------------------------------------------------------------------
138 register_rest_route('atarim/v1', '/status', [
139 'methods' => 'GET',
140 'permission_callback' => '__return_true',
141 'callback' => function () {
142 $connected = get_option('avc_collab_active', 'no') === 'yes';
143 return [
144 'installed' => true,
145 'connected' => $connected,
146 'version' => defined('AVCF_VERSION') ? AVCF_VERSION : null,
147 'settings_url' => admin_url('options-general.php?page=atarim-visual-collaboration'),
148 'site_url' => site_url(),
149 ];
150 },
151 ]);
152 });
153
154 /* ===========================
155 * Builder + wrapper detection
156 * =========================== */
157
158 function atarim_detect_page_builder(int $post_id): string {
159
160 $elementor_data = get_post_meta($post_id, '_elementor_data', true);
161 if (is_string($elementor_data) && trim($elementor_data) !== '') return 'elementor';
162 if (is_array($elementor_data) && !empty($elementor_data)) return 'elementor';
163
164 $post = get_post($post_id);
165 if ($post && isset($post->post_content) && has_blocks($post->post_content)) return 'block';
166
167 if ($post && trim(wp_strip_all_tags($post->post_content)) !== '' && preg_match('/<[a-z][a-z0-9]*\b[^>]*>/i', $post->post_content)) {
168 return 'classic';
169 }
170
171 return '';
172 }
173
174 function atarim_detect_wrapper_selector_by_theme(): string {
175
176 $theme = wp_get_theme();
177 $template = strtolower((string) $theme->get_template());
178 $stylesheet = strtolower((string) $theme->get_stylesheet());
179 $slug = $template ?: $stylesheet;
180
181 $map = [
182 'hello-elementor' => '.page-content',
183 'oceanwp' => '.entry.clr',
184 // Expand later...
185 ];
186
187 return $map[$slug] ?? '';
188 }
189
190 /* ===========================
191 * REST: GET
192 * =========================== */
193
194 function atarim_inline_get_handler(WP_REST_Request $request) {
195
196 $post_id = absint($request->get_param('postId'));
197 $page_builder = sanitize_text_field((string) $request->get_param('pageBuilder'));
198
199 if (!$post_id) {
200 return new WP_REST_Response(['status' => false, 'message' => 'Missing postId.'], 400);
201 }
202
203 if (!in_array($page_builder, ['elementor', 'block', 'classic'], true)) {
204 return new WP_REST_Response([
205 'status' => false,
206 'message' => 'This page builder is not supported yet.',
207 ], 400);
208 }
209
210 $post = get_post($post_id);
211 if (!$post) return new WP_REST_Response(['status'=>false,'message'=>'Post not found.'], 404);
212
213 if ($page_builder === 'elementor') {
214 $widget_id = sanitize_text_field((string) $request->get_param('widgetId'));
215 if (!$widget_id) return new WP_REST_Response(['status'=>false,'message'=>'Missing widgetId.'], 400);
216
217 $elementor_data = atarim_elementor_get_document_data_array($post_id);
218 if (!is_array($elementor_data)) return new WP_REST_Response(['status'=>false,'message'=>'No valid _elementor_data found.'], 404);
219
220 $widget = atarim_elementor_find_element_by_id($elementor_data, $widget_id);
221 if (!is_array($widget)) return new WP_REST_Response(['status'=>false,'message'=>'Widget not found.'], 404);
222
223 return new WP_REST_Response([
224 'status' => true,
225 'pageBuilder' => 'elementor',
226 'postId' => $post_id,
227 'widgetId' => $widget_id,
228 'content' => $widget,
229 ], 200);
230 }
231
232 if ($page_builder === 'block') {
233
234 $block_name = sanitize_text_field((string) $request->get_param('blockName'));
235 $anchor_index = (int) $request->get_param('anchorIndex');
236 $snippet = (string) $request->get_param('snippet');
237
238 if (trim($block_name) === '') {
239 return new WP_REST_Response(['status'=>false,'message'=>'Missing blockName.'], 400);
240 }
241 if ($anchor_index < 0) {
242 return new WP_REST_Response(['status'=>false,'message'=>'Missing/invalid anchorIndex.'], 400);
243 }
244
245 $match = atarim_find_gutenberg_block_by_anchor_index(
246 $post->post_content,
247 $block_name,
248 $anchor_index,
249 $snippet
250 );
251
252 if (!$match) {
253 return new WP_REST_Response([
254 'status'=>false,
255 'message'=>'Could not find matching Gutenberg block.',
256 ], 404);
257 }
258
259 return new WP_REST_Response([
260 'status' => true,
261 'pageBuilder' => 'block',
262 'postId' => $post_id,
263 'blockName' => $block_name,
264 'anchorIndex' => $anchor_index,
265 'snippet' => $snippet,
266 'blockPath' => $match['blockPath'], // nested like "3.0.1"
267 'content' => $match['serializedBlock'], // raw serialized block string
268 ], 200);
269 }
270
271 // Classic
272 $path_string = (string) $request->get_param('path');
273 $tag = strtolower((string) $request->get_param('tag'));
274 $snippet = (string) $request->get_param('snippet');
275
276 if (trim($path_string) === '' || trim($tag) === '' || trim($snippet) === '') {
277 return new WP_REST_Response(['status'=>false,'message'=>'Missing path, tag, or snippet.'], 400);
278 }
279
280 $steps = atarim_parse_compact_path($path_string);
281 $found = atarim_classic_find_node_outer_html($post->post_content, $steps, $tag, $snippet);
282
283 if (!$found) {
284 return new WP_REST_Response(['status'=>false,'message'=>'Could not find matching HTML element in classic content.'], 404);
285 }
286
287 return new WP_REST_Response([
288 'status' => true,
289 'pageBuilder' => 'classic',
290 'postId' => $post_id,
291 'path' => $path_string,
292 'tag' => $tag,
293 'snippet' => $snippet,
294 'content' => $found,
295 ], 200);
296 }
297
298 /* ===========================
299 * REST: SAVE
300 * =========================== */
301
302 function atarim_inline_save_handler(WP_REST_Request $request) {
303
304 $post_id = absint($request->get_param('postId'));
305 $page_builder = sanitize_text_field((string) $request->get_param('pageBuilder'));
306
307 if (!$post_id) {
308 return new WP_REST_Response(['status' => false, 'message' => 'Missing postId.'], 400);
309 }
310
311 if (!in_array($page_builder, ['elementor', 'block', 'classic'], true)) {
312 return new WP_REST_Response([
313 'status' => false,
314 'message' => 'This page builder is not supported yet.',
315 ], 400);
316 }
317
318 $post = get_post($post_id);
319 if (!$post) return new WP_REST_Response(['status'=>false,'message'=>'Post not found.'], 404);
320
321 if ($page_builder === 'elementor') {
322 $widget_id = sanitize_text_field((string) $request->get_param('widgetId'));
323 $widget = $request->get_param('content');
324
325 if (!$widget_id) return new WP_REST_Response(['status'=>false,'message'=>'Missing widgetId.'], 400);
326 if (!is_array($widget)) return new WP_REST_Response(['status'=>false,'message'=>'Elementor content must be a JSON object.'], 400);
327
328 if (empty($widget['id']) || (string)$widget['id'] !== (string)$widget_id) {
329 return new WP_REST_Response(['status'=>false,'message'=>'content.id must match widgetId.'], 400);
330 }
331
332 $elementor_data = atarim_elementor_get_document_data_array($post_id);
333 if (!is_array($elementor_data)) return new WP_REST_Response(['status'=>false,'message'=>'No valid _elementor_data found.'], 404);
334
335 $replaced = false;
336 $updated_data = atarim_elementor_replace_element_by_id($elementor_data, $widget_id, $widget, $replaced);
337 if (!$replaced) return new WP_REST_Response(['status'=>false,'message'=>'Widget not found; nothing saved.'], 404);
338
339 update_post_meta($post_id, '_elementor_data', wp_slash(wp_json_encode($updated_data)));
340
341 delete_post_meta($post_id, '_elementor_element_cache');
342 delete_post_meta($post_id, '_elementor_page_assets');
343
344 if ( class_exists('\Elementor\Core\Files\CSS\Post') ) {
345 try { ( new \Elementor\Core\Files\CSS\Post($post_id) )->delete(); } catch (Throwable $e) {}
346 }
347
348 clean_post_cache($post_id);
349
350 return new WP_REST_Response(['status'=>true], 200);
351 }
352
353 $content = (string) $request->get_param('content');
354 if (trim($content) === '') {
355 return new WP_REST_Response(['status'=>false,'message'=>'Missing content to save.'], 400);
356 }
357
358 if ($page_builder === 'block') {
359 $block_path = (string) $request->get_param('blockPath');
360 $expected_block_name = sanitize_text_field((string) $request->get_param('blockName'));
361
362 if (trim($block_path) === '') {
363 return new WP_REST_Response(['status'=>false,'message'=>'Missing blockPath.'], 400);
364 }
365 if (trim($expected_block_name) === '') {
366 return new WP_REST_Response(['status'=>false,'message'=>'Missing blockName.'], 400);
367 }
368
369 // Validate: new content parses as a single block of the expected type
370 $parsed_new = parse_blocks($content);
371 if (!is_array($parsed_new) || empty($parsed_new) || !is_array($parsed_new[0])) {
372 return new WP_REST_Response([
373 'status'=>false,
374 'message'=>'Content is not a valid block.',
375 ], 400);
376 }
377 if (($parsed_new[0]['blockName'] ?? '') !== $expected_block_name) {
378 return new WP_REST_Response([
379 'status'=>false,
380 'message'=>'Content block type does not match expected blockName.',
381 ], 400);
382 }
383
384 // Validate: existing block at blockPath is the expected type (stale-edit guard)
385 $existing_blocks = parse_blocks($post->post_content);
386 $path_parts = array_values(array_filter(
387 explode('.', trim($block_path)),
388 static function($v) { return $v !== ''; }
389 ));
390 $existing_block = atarim_get_block_ref_by_path($existing_blocks, $path_parts);
391 if (!is_array($existing_block) || ($existing_block['blockName'] ?? '') !== $expected_block_name) {
392 return new WP_REST_Response([
393 'status'=>false,
394 'message'=>'Block at path no longer matches expected type. The post may have been edited elsewhere; please refresh.',
395 ], 409);
396 }
397
398 $updated = atarim_replace_gutenberg_block_by_nested_path($post->post_content, $block_path, $content);
399 if ($updated === null) {
400 return new WP_REST_Response([
401 'status'=>false,
402 'message'=>'Could not replace Gutenberg block (path not found or invalid replacement).',
403 ], 404);
404 }
405
406 wp_update_post([
407 'ID' => $post_id,
408 'post_content' => $updated,
409 ]);
410
411 clean_post_cache($post_id);
412
413 return new WP_REST_Response(['status'=>true], 200);
414 }
415
416 // Classic save
417 $path_string = (string) $request->get_param('path');
418 $tag = strtolower((string) $request->get_param('tag'));
419 $snippet = (string) $request->get_param('snippet');
420
421 if (trim($path_string) === '' || trim($tag) === '' || trim($snippet) === '') {
422 return new WP_REST_Response(['status'=>false,'message'=>'Missing path/tag/snippet for classic save.'], 400);
423 }
424
425 $steps = atarim_parse_compact_path($path_string);
426
427 $new_post_content = atarim_classic_replace_node_outer_html($post->post_content, $steps, $tag, $snippet, $content);
428 if ($new_post_content === null) {
429 return new WP_REST_Response(['status'=>false,'message'=>'Could not find matching element to replace in classic content.'], 404);
430 }
431
432 wp_update_post([
433 'ID' => $post_id,
434 'post_content' => $new_post_content,
435 ]);
436
437 clean_post_cache($post_id);
438
439 return new WP_REST_Response(['status'=>true], 200);
440 }
441
442 /* ===========================
443 * Shared helpers: compact path (classic only)
444 * =========================== */
445
446 function atarim_parse_compact_path(string $path): array {
447 $path = trim($path);
448 if ($path === '') return [];
449
450 $parts = array_map('trim', explode('>', $path));
451 $steps = [];
452
453 foreach ($parts as $part) {
454 $part = trim($part);
455 if ($part === '') continue;
456
457 if (preg_match('/^([a-z0-9]+)(?:\((\d+)\))?$/i', $part, $m)) {
458 $steps[] = [
459 'tag' => strtoupper($m[1]),
460 'index' => isset($m[2]) ? (int) $m[2] : 0,
461 ];
462 }
463 }
464
465 return $steps;
466 }
467
468 /* ===========================
469 * Elementor helpers
470 * =========================== */
471
472 function atarim_elementor_get_document_data_array(int $post_id): ?array {
473 $raw = get_post_meta($post_id, '_elementor_data', true);
474 if (empty($raw)) return null;
475
476 if (is_string($raw)) {
477 $decoded = json_decode($raw, true);
478 return is_array($decoded) ? $decoded : null;
479 }
480
481 return is_array($raw) ? $raw : null;
482 }
483
484 function atarim_elementor_find_element_by_id(array $nodes, string $target_id): ?array {
485 foreach ($nodes as $node) {
486 if (!is_array($node)) continue;
487
488 if (isset($node['id']) && (string)$node['id'] === (string)$target_id) {
489 return $node;
490 }
491
492 if (isset($node['elements']) && is_array($node['elements'])) {
493 $found = atarim_elementor_find_element_by_id($node['elements'], $target_id);
494 if ($found !== null) return $found;
495 }
496 }
497 return null;
498 }
499
500 function atarim_elementor_replace_element_by_id(array $nodes, string $target_id, array $replacement_node, bool &$replaced): array {
501 foreach ($nodes as $index => $node) {
502 if (!is_array($node)) continue;
503
504 if (isset($node['id']) && (string)$node['id'] === (string)$target_id) {
505 $nodes[$index] = $replacement_node;
506 $replaced = true;
507 return $nodes;
508 }
509
510 if (isset($node['elements']) && is_array($node['elements'])) {
511 $nodes[$index]['elements'] = atarim_elementor_replace_element_by_id(
512 $node['elements'],
513 $target_id,
514 $replacement_node,
515 $replaced
516 );
517 if ($replaced) return $nodes;
518 }
519 }
520 return $nodes;
521 }
522
523 /* ===========================
524 * Gutenberg helpers (blockName + anchorIndex + nested path replace)
525 * =========================== */
526
527 function atarim_normalize_text(string $text): string {
528 $text = wp_strip_all_tags($text);
529 $text = preg_replace('/\s+/u', ' ', $text);
530 return strtolower(trim($text));
531 }
532
533 function atarim_collect_matching_blocks(array $blocks, string $block_name, array &$out, string $parent_path = ''): void {
534 foreach ($blocks as $i => $block) {
535 if (!is_array($block)) continue;
536
537 $current_block_name = (string)($block['blockName'] ?? '');
538 $path = ($parent_path === '') ? (string)$i : ($parent_path . '.' . $i);
539
540 if ($current_block_name === $block_name) {
541 $out[] = ['path' => $path, 'block' => $block];
542 }
543
544 if (!empty($block['innerBlocks']) && is_array($block['innerBlocks'])) {
545 atarim_collect_matching_blocks($block['innerBlocks'], $block_name, $out, $path);
546 }
547 }
548 }
549
550 function atarim_find_gutenberg_block_by_anchor_index(string $post_content, string $block_name, int $anchor_index, string $snippet): ?array {
551 $blocks = parse_blocks($post_content);
552 if (!is_array($blocks)) return null;
553
554 $matches = [];
555 atarim_collect_matching_blocks($blocks, $block_name, $matches);
556
557 if (!isset($matches[$anchor_index])) return null;
558
559 $picked = $matches[$anchor_index]['block'];
560 $picked_path = $matches[$anchor_index]['path'];
561
562 $snippet_norm = atarim_normalize_text($snippet);
563
564 if ($snippet_norm !== '') {
565 $rendered = '';
566 try { $rendered = render_block($picked); } catch (Throwable $e) { $rendered = serialize_block($picked); }
567
568 if (!str_contains(atarim_normalize_text($rendered), $snippet_norm)) {
569 return null;
570 }
571 }
572
573 return [
574 'blockPath' => $picked_path,
575 'serializedBlock' => serialize_block($picked),
576 ];
577 }
578
579 function atarim_get_block_ref_by_path(array &$blocks, array $path_parts) {
580 $ref = &$blocks;
581 foreach ($path_parts as $part_index => $part) {
582 $idx = (int)$part;
583 if (!isset($ref[$idx]) || !is_array($ref[$idx])) return null;
584
585 if ($part_index === count($path_parts) - 1) {
586 return $ref[$idx];
587 }
588
589 if (!isset($ref[$idx]['innerBlocks']) || !is_array($ref[$idx]['innerBlocks'])) return null;
590 $ref = &$ref[$idx]['innerBlocks'];
591 }
592 return null;
593 }
594
595 function atarim_replace_gutenberg_block_by_nested_path(string $post_content, string $block_path, string $new_serialized_block): ?string {
596
597 $blocks = parse_blocks($post_content);
598 if (!is_array($blocks)) return null;
599
600 $replacement_blocks = parse_blocks($new_serialized_block);
601 if (!is_array($replacement_blocks) || empty($replacement_blocks) || !is_array($replacement_blocks[0])) {
602 return null;
603 }
604 $replacement_block = $replacement_blocks[0];
605
606 $parts = array_filter(explode('.', trim($block_path)), static function($v) { return $v !== ''; });
607 if (empty($parts)) return null;
608
609 $ref = &$blocks;
610
611 for ($i = 0; $i < count($parts) - 1; $i++) {
612 $idx = (int)$parts[$i];
613 if (!isset($ref[$idx]) || !is_array($ref[$idx])) return null;
614
615 if (!isset($ref[$idx]['innerBlocks']) || !is_array($ref[$idx]['innerBlocks'])) {
616 return null;
617 }
618
619 $ref = &$ref[$idx]['innerBlocks'];
620 }
621
622 $target_index = (int)$parts[count($parts) - 1];
623 if (!isset($ref[$target_index]) || !is_array($ref[$target_index])) return null;
624
625 $ref[$target_index] = $replacement_block;
626
627 return serialize_blocks($blocks);
628 }
629
630 /* ===========================
631 * Classic helpers (DOM path)
632 * =========================== */
633
634 function atarim_dom_load_fragment(string $html, string $wrap_id): array {
635 $dom = new DOMDocument();
636 $encoded = function_exists('mb_convert_encoding')
637 ? mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8')
638 : $html;
639
640 libxml_use_internal_errors(true);
641 $dom->loadHTML('<div id="'.$wrap_id.'">'.$encoded.'</div>', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
642 libxml_clear_errors();
643
644 $wrap = $dom->getElementById($wrap_id);
645 return [$dom, $wrap];
646 }
647
648 function atarim_dom_inner_html(DOMDocument $dom, DOMElement $wrap): string {
649 $out = '';
650 foreach ($wrap->childNodes as $child) {
651 $out .= $dom->saveHTML($child);
652 }
653 return html_entity_decode($out, ENT_QUOTES | ENT_HTML5, 'UTF-8');
654 }
655
656 function atarim_dom_outer_html(DOMDocument $dom, DOMNode $node): string {
657 return html_entity_decode($dom->saveHTML($node), ENT_QUOTES | ENT_HTML5, 'UTF-8');
658 }
659
660 function atarim_dom_children_by_tag(DOMNode $node, string $tag_upper): array {
661 $children = [];
662 foreach ($node->childNodes as $child) {
663 if ($child->nodeType === XML_ELEMENT_NODE && strtoupper($child->nodeName) === $tag_upper) {
664 $children[] = $child;
665 }
666 }
667 return $children;
668 }
669
670 function atarim_classic_find_node_outer_html(string $post_content, array $steps, string $tag_lower, string $snippet): ?string {
671 [$dom, $wrap] = atarim_dom_load_fragment($post_content, '__wrap__');
672 if (!$wrap) return null;
673
674 $current = $wrap;
675
676 foreach ($steps as $step) {
677 $children = atarim_dom_children_by_tag($current, $step['tag']);
678 $index = (int) $step['index'];
679 if (!isset($children[$index])) return null;
680 $current = $children[$index];
681 }
682
683 if (strtolower($current->nodeName) !== strtolower($tag_lower)) return null;
684
685 $node_text = atarim_normalize_text($current->textContent ?? '');
686 $snippet_norm = atarim_normalize_text($snippet);
687 if ($snippet_norm === '' || !str_contains($node_text, $snippet_norm)) return null;
688
689 return atarim_dom_outer_html($dom, $current);
690 }
691
692 function atarim_classic_replace_node_outer_html(string $post_content, array $steps, string $tag_lower, string $snippet, string $replacement_html): ?string {
693 [$dom, $wrap] = atarim_dom_load_fragment($post_content, '__wrap__');
694 if (!$wrap) return null;
695
696 $current = $wrap;
697
698 foreach ($steps as $step) {
699 $children = atarim_dom_children_by_tag($current, $step['tag']);
700 $index = (int) $step['index'];
701 if (!isset($children[$index])) return null;
702 $current = $children[$index];
703 }
704
705 if (strtolower($current->nodeName) !== strtolower($tag_lower)) return null;
706
707 $node_text = atarim_normalize_text($current->textContent ?? '');
708 $snippet_norm = atarim_normalize_text($snippet);
709 if ($snippet_norm === '' || !str_contains($node_text, $snippet_norm)) return null;
710
711 [$tmp_dom, $tmp_wrap] = atarim_dom_load_fragment($replacement_html, '__frag__');
712 if (!$tmp_wrap) return null;
713
714 $parent = $current->parentNode;
715 if (!$parent) return null;
716
717 foreach (iterator_to_array($tmp_wrap->childNodes) as $child) {
718 $parent->insertBefore($dom->importNode($child, true), $current);
719 }
720
721 $parent->removeChild($current);
722
723 return atarim_dom_inner_html($dom, $wrap);
724 }