| 1 |
<?php |
| 2 |
if ( ! defined( 'ABSPATH' ) ) { |
| 3 |
exit; |
| 4 |
} |
| 5 |
|
| 6 |
/** |
| 7 |
* Content Rewriter Page Handler |
| 8 |
* Renders the Content Rewriter wizard UI. |
| 9 |
*/ |
| 10 |
function botwriter_rewriter_page_handler() { |
| 11 |
if (!current_user_can('manage_options')) { |
| 12 |
return; |
| 13 |
} |
| 14 |
|
| 15 |
$dir_images = plugin_dir_url(dirname(__FILE__)) . '/assets/images/'; |
| 16 |
?> |
| 17 |
|
| 18 |
<div class="wrap"> |
| 19 |
<h1><?php esc_html_e('Content Rewriter', 'botwriter'); ?></h1> |
| 20 |
<p><?php esc_html_e('Paste URLs of articles to fetch, extract and rewrite them with AI.', 'botwriter'); ?></p> |
| 21 |
|
| 22 |
<!-- Step 1: URL Input --> |
| 23 |
<div id="rewriter_step1" class="super-ia bw-flex-col"> |
| 24 |
<div class="bw-flex-row-center bw-mb-15"> |
| 25 |
<div class="super-ia-image bw-img-shrink"> |
| 26 |
<img src="<?php echo esc_url($dir_images . 'ai_cerebro.png'); ?>" alt="<?php echo esc_attr__('AI', 'botwriter'); ?>" class="bw-hue-rotate-200 bw-img-80"> |
| 27 |
</div> |
| 28 |
<div> |
| 29 |
<h2 class="super-title"><?php esc_html_e('Step 1: Paste URLs', 'botwriter'); ?></h2> |
| 30 |
<p class="super-text"><?php esc_html_e('Enter the URLs of the articles you want to rewrite (one per line). The system will fetch and extract the content.', 'botwriter'); ?></p> |
| 31 |
</div> |
| 32 |
</div> |
| 33 |
|
| 34 |
<div class="col-md-6"> |
| 35 |
<label for="rewriter_urls"><?php esc_html_e('Article URLs (one per line):', 'botwriter'); ?></label> |
| 36 |
<textarea id="rewriter_urls" name="rewriter_urls" rows="8" cols="60" placeholder="https://example.com/article-1 https://example.com/article-2 https://example.com/article-3"></textarea> |
| 37 |
</div> |
| 38 |
<br> |
| 39 |
<div> |
| 40 |
<button id="rewriter_fetch_btn" class="button-primary"><?php esc_html_e('Fetch Content', 'botwriter'); ?></button> |
| 41 |
</div> |
| 42 |
<div id="rewriter_fetch_status" class="bw-mt-10"></div> |
| 43 |
</div> |
| 44 |
|
| 45 |
<!-- Step 2: Review Extracted Content --> |
| 46 |
<div id="rewriter_step2" style="display: none;" class="bw-mt-20"> |
| 47 |
<div class="super-ia bw-flex-col"> |
| 48 |
<h2 class="super-title"><?php esc_html_e('Step 2: Review Extracted Content', 'botwriter'); ?></h2> |
| 49 |
<p class="super-text"><?php esc_html_e('Review the extracted content from each URL. You can edit, remove or manually paste content for failed extractions.', 'botwriter'); ?></p> |
| 50 |
<div id="rewriter_articles_list"></div> |
| 51 |
<br> |
| 52 |
<div> |
| 53 |
<button id="rewriter_add_manual_btn" class="button" type="button"><?php esc_html_e('+ Add Article Manually', 'botwriter'); ?></button> |
| 54 |
</div> |
| 55 |
</div> |
| 56 |
</div> |
| 57 |
|
| 58 |
<!-- Step 3: Rewrite Options + Create Task --> |
| 59 |
<div id="rewriter_step3" style="display: none;" class="bw-mt-20"> |
| 60 |
<div class="super-ia bw-flex-col"> |
| 61 |
<h2 class="super-title"><?php esc_html_e('Step 3: Rewrite Settings', 'botwriter'); ?></h2> |
| 62 |
<p class="super-text"><?php esc_html_e('Configure how the AI should rewrite the articles.', 'botwriter'); ?></p> |
| 63 |
|
| 64 |
<div class="col-md-6"> |
| 65 |
<label for="rewriter_prompt"><?php esc_html_e('Rewrite Instructions:', 'botwriter'); ?></label> |
| 66 |
<textarea id="rewriter_prompt" name="rewriter_prompt" rows="4" cols="60" placeholder="<?php esc_attr_e('e.g. Rewrite in a friendly conversational tone. Add practical examples. Make it 30% longer with more detail.', 'botwriter'); ?>"></textarea> |
| 67 |
<p class="form-text"><?php esc_html_e('Global instructions for rewriting all articles. This will be sent to the AI along with each original article.', 'botwriter'); ?></p> |
| 68 |
</div> |
| 69 |
<br> |
| 70 |
|
| 71 |
<!-- Task Properties Grid --> |
| 72 |
<div class="bw-rewriter-props-extra"> |
| 73 |
<div class="col-md-6"> |
| 74 |
<label for="rewriter_category" class="form-label"><?php esc_html_e('Category:', 'botwriter'); ?></label> |
| 75 |
<select id="rewriter_category" name="rewriter_category" class="form-select"> |
| 76 |
<option value="0"><?php esc_html_e('— Default —', 'botwriter'); ?></option> |
| 77 |
<?php |
| 78 |
$categories = get_categories(array( |
| 79 |
'orderby' => 'name', |
| 80 |
'order' => 'ASC', |
| 81 |
'hide_empty' => false |
| 82 |
)); |
| 83 |
foreach ($categories as $category) { |
| 84 |
echo '<option value="' . esc_attr($category->term_id) . '">' . esc_html($category->name) . '</option>'; |
| 85 |
} |
| 86 |
?> |
| 87 |
</select> |
| 88 |
<p class="form-text"><?php esc_html_e('Select the category where the rewritten posts will be published.', 'botwriter'); ?></p> |
| 89 |
</div> |
| 90 |
</div> |
| 91 |
<br> |
| 92 |
|
| 93 |
<!-- Reuse Super Task Properties form --> |
| 94 |
<?php |
| 95 |
$rewriter_default_item = array( |
| 96 |
/* translators: %s: current date and time */ |
| 97 |
'task_name' => sprintf(__('Rewriter Task %s', 'botwriter'), wp_date('M j, Y H:i')), |
| 98 |
'writer' => 'ai_cerebro', |
| 99 |
'narration' => 'Descriptive', |
| 100 |
'custom_style' => '', |
| 101 |
'post_language' => substr(get_locale(), 0, 2), |
| 102 |
'post_length' => '800', |
| 103 |
'custom_post_length' => '', |
| 104 |
'post_status' => 'draft', |
| 105 |
'days' => 'Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday', |
| 106 |
'times_per_day' => 1, |
| 107 |
'author_selection' => strval(get_current_user_id()), |
| 108 |
'disable_ai_images' => 0, |
| 109 |
'template_id' => null, |
| 110 |
); |
| 111 |
botwriter_super_form_meta_box_handler($rewriter_default_item); |
| 112 |
?> |
| 113 |
<!-- End Super Task Properties --> |
| 114 |
<br> |
| 115 |
|
| 116 |
<div> |
| 117 |
<button id="rewriter_create_btn" class="button-primary"><?php esc_html_e('Create Rewrite Task', 'botwriter'); ?></button> |
| 118 |
</div> |
| 119 |
<div id="rewriter_create_status" class="bw-mt-10"></div> |
| 120 |
</div> |
| 121 |
</div> |
| 122 |
|
| 123 |
</div> |
| 124 |
|
| 125 |
<?php |
| 126 |
} |
| 127 |
|
| 128 |
|
| 129 |
/** |
| 130 |
* Extract article content from a URL using DOMDocument. |
| 131 |
* |
| 132 |
* @param string $url The URL to extract content from. |
| 133 |
* @return array|WP_Error Array with 'title', 'content', 'excerpt', 'url' or WP_Error. |
| 134 |
*/ |
| 135 |
function botwriter_rewriter_extract_content($url, $prefetched_html = null) { |
| 136 |
if ($prefetched_html !== null) { |
| 137 |
// Use pre-fetched HTML (e.g. from Site Rewriter crawl) |
| 138 |
return botwriter_rewriter_extract_content_from_html($prefetched_html, $url); |
| 139 |
} |
| 140 |
|
| 141 |
// Build a Referer from the target domain (sites often block requests without one) |
| 142 |
$parsed = wp_parse_url($url); |
| 143 |
$referer = (isset($parsed['scheme']) ? $parsed['scheme'] : 'https') . '://' . ($parsed['host'] ?? ''); |
| 144 |
|
| 145 |
// Fetch the page with realistic browser headers to avoid 403 blocks |
| 146 |
$ssl_verify = get_option('botwriter_sslverify', 'yes') === 'yes'; |
| 147 |
$response = wp_remote_get($url, array( |
| 148 |
'timeout' => 30, |
| 149 |
'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', |
| 150 |
'sslverify' => $ssl_verify, |
| 151 |
'redirection' => 5, |
| 152 |
'headers' => array( |
| 153 |
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', |
| 154 |
'Accept-Language' => 'en-US,en;q=0.9,es;q=0.8', |
| 155 |
'Accept-Encoding' => 'identity', |
| 156 |
'Cache-Control' => 'no-cache', |
| 157 |
'Referer' => $referer, |
| 158 |
'Sec-Fetch-Dest' => 'document', |
| 159 |
'Sec-Fetch-Mode' => 'navigate', |
| 160 |
'Sec-Fetch-Site' => 'same-origin', |
| 161 |
'Sec-Fetch-User' => '?1', |
| 162 |
'Upgrade-Insecure-Requests' => '1', |
| 163 |
'Sec-Ch-Ua' => '"Chromium";v="131", "Not_A Brand";v="24"', |
| 164 |
'Sec-Ch-Ua-Mobile' => '?0', |
| 165 |
'Sec-Ch-Ua-Platform' => '"Windows"', |
| 166 |
), |
| 167 |
)); |
| 168 |
|
| 169 |
if (is_wp_error($response)) { |
| 170 |
return $response; |
| 171 |
} |
| 172 |
|
| 173 |
$status_code = wp_remote_retrieve_response_code($response); |
| 174 |
if ($status_code === 403) { |
| 175 |
return new WP_Error('http_403', 'Access denied (403). The website is blocking automated requests from your server. Try using "Add Article Manually" to paste the content instead.'); |
| 176 |
} |
| 177 |
if ($status_code !== 200) { |
| 178 |
return new WP_Error('http_error', sprintf('HTTP %d error fetching URL.', $status_code)); |
| 179 |
} |
| 180 |
|
| 181 |
$html = wp_remote_retrieve_body($response); |
| 182 |
if (empty($html)) { |
| 183 |
return new WP_Error('empty_response', 'Empty response from URL.'); |
| 184 |
} |
| 185 |
|
| 186 |
return botwriter_rewriter_extract_content_from_html($html, $url); |
| 187 |
} |
| 188 |
|
| 189 |
/** |
| 190 |
* Extract article content from pre-fetched HTML. |
| 191 |
* Shared logic used by both botwriter_rewriter_extract_content() and |
| 192 |
* botwriter_siterewriter_crawl_page() to avoid re-downloading pages. |
| 193 |
* |
| 194 |
* @param string $html Raw HTML. |
| 195 |
* @param string $url Original URL (used for output only). |
| 196 |
* @return array|WP_Error |
| 197 |
*/ |
| 198 |
function botwriter_rewriter_extract_content_from_html($html, $url = '') { |
| 199 |
// Suppress DOM warnings for malformed HTML |
| 200 |
libxml_use_internal_errors(true); |
| 201 |
|
| 202 |
$doc = new DOMDocument(); |
| 203 |
$doc->loadHTML('<?xml encoding="UTF-8">' . $html, LIBXML_NOWARNING | LIBXML_NOERROR); |
| 204 |
|
| 205 |
libxml_clear_errors(); |
| 206 |
|
| 207 |
$xpath = new DOMXPath($doc); |
| 208 |
|
| 209 |
// --- Extract title --- |
| 210 |
$title = ''; |
| 211 |
|
| 212 |
// Try og:title first |
| 213 |
$og_title_nodes = $xpath->query('//meta[@property="og:title"]/@content'); |
| 214 |
if ($og_title_nodes && $og_title_nodes->length > 0) { |
| 215 |
$title = trim($og_title_nodes->item(0)->nodeValue); |
| 216 |
} |
| 217 |
|
| 218 |
// Try <h1> if og:title empty |
| 219 |
if (empty($title)) { |
| 220 |
$h1_nodes = $xpath->query('//h1'); |
| 221 |
if ($h1_nodes && $h1_nodes->length > 0) { |
| 222 |
$title = trim($h1_nodes->item(0)->textContent); |
| 223 |
} |
| 224 |
} |
| 225 |
|
| 226 |
// Fallback to <title> |
| 227 |
if (empty($title)) { |
| 228 |
$title_nodes = $xpath->query('//title'); |
| 229 |
if ($title_nodes && $title_nodes->length > 0) { |
| 230 |
$title = trim($title_nodes->item(0)->textContent); |
| 231 |
} |
| 232 |
} |
| 233 |
|
| 234 |
// --- Remove unwanted tags (always safe to remove) --- |
| 235 |
$remove_tags = array('script', 'style', 'nav', 'header', 'footer', 'aside', 'iframe', 'noscript', 'form', 'svg', 'figcaption'); |
| 236 |
foreach ($remove_tags as $tag) { |
| 237 |
$elements = $xpath->query('//' . $tag); |
| 238 |
if ($elements) { |
| 239 |
// Collect into array first to avoid live NodeList issues |
| 240 |
$to_remove = array(); |
| 241 |
foreach ($elements as $element) { |
| 242 |
$to_remove[] = $element; |
| 243 |
} |
| 244 |
foreach ($to_remove as $element) { |
| 245 |
if ($element->parentNode) { |
| 246 |
$element->parentNode->removeChild($element); |
| 247 |
} |
| 248 |
} |
| 249 |
} |
| 250 |
} |
| 251 |
|
| 252 |
// Remove noise elements by class/id patterns. |
| 253 |
// Only remove elements that do NOT contain substantial paragraph text |
| 254 |
// (prevents stripping parent containers on sites that put "social" or "share" classes on wrappers). |
| 255 |
$noise_patterns = array( |
| 256 |
'//*[contains(@class, "comment")]', |
| 257 |
'//*[contains(@class, "sidebar")]', |
| 258 |
'//*[contains(@class, "widget")]', |
| 259 |
'//*[contains(@class, "share")]', |
| 260 |
'//*[contains(@class, "social")]', |
| 261 |
'//*[contains(@class, "related")]', |
| 262 |
'//*[contains(@class, "advertisement")]', |
| 263 |
'//*[contains(@class, "ad-")]', |
| 264 |
'//*[contains(@class, "cookie")]', |
| 265 |
'//*[contains(@class, "newsletter")]', |
| 266 |
'//*[contains(@class, "popup")]', |
| 267 |
'//*[contains(@class, "author-bio")]', |
| 268 |
'//*[contains(@class, "post-tags")]', |
| 269 |
'//*[contains(@class, "breadcrumb")]', |
| 270 |
'//*[contains(@class, "navigation")]', |
| 271 |
'//*[contains(@class, "paginat")]', |
| 272 |
'//*[contains(@id, "comment")]', |
| 273 |
'//*[contains(@id, "sidebar")]', |
| 274 |
'//*[contains(@id, "footer")]', |
| 275 |
'//*[contains(@id, "header")]', |
| 276 |
'//*[contains(@id, "newsletter")]', |
| 277 |
); |
| 278 |
foreach ($noise_patterns as $pattern) { |
| 279 |
$elements = $xpath->query($pattern); |
| 280 |
if ($elements) { |
| 281 |
$to_remove = array(); |
| 282 |
foreach ($elements as $element) { |
| 283 |
// Safety check: only remove if element has few real paragraphs |
| 284 |
// (avoids stripping parent containers that wrap the article) |
| 285 |
$p_count = 0; |
| 286 |
$paras = $xpath->query('.//p', $element); |
| 287 |
if ($paras) { |
| 288 |
foreach ($paras as $p) { |
| 289 |
if (strlen(trim($p->textContent)) > 40) { |
| 290 |
$p_count++; |
| 291 |
} |
| 292 |
} |
| 293 |
} |
| 294 |
if ($p_count < 3) { |
| 295 |
$to_remove[] = $element; |
| 296 |
} |
| 297 |
} |
| 298 |
foreach ($to_remove as $element) { |
| 299 |
if ($element->parentNode) { |
| 300 |
$element->parentNode->removeChild($element); |
| 301 |
} |
| 302 |
} |
| 303 |
} |
| 304 |
} |
| 305 |
|
| 306 |
// --- Extract main content --- |
| 307 |
$content = ''; |
| 308 |
|
| 309 |
// Strategy 1: Schema.org articleBody (most reliable when present) |
| 310 |
$schema_nodes = $xpath->query('//*[@itemprop="articleBody"]'); |
| 311 |
if ($schema_nodes && $schema_nodes->length > 0) { |
| 312 |
$content = botwriter_rewriter_get_text_content($schema_nodes->item(0)); |
| 313 |
} |
| 314 |
|
| 315 |
// Strategy 2: Common content container classes (including popular WP themes) |
| 316 |
if (empty(trim($content)) || strlen(trim($content)) < 200) { |
| 317 |
$content_selectors = array( |
| 318 |
// Standard WordPress |
| 319 |
'//*[contains(@class, "entry-content")]', |
| 320 |
'//*[contains(@class, "post-content")]', |
| 321 |
'//*[contains(@class, "article-content")]', |
| 322 |
'//*[contains(@class, "article-body")]', |
| 323 |
'//*[contains(@class, "post-body")]', |
| 324 |
'//*[contains(@class, "single-content")]', |
| 325 |
// TagDiv themes (Flavor, Flavor HD, Flavor Pro, Flavor Magazine) |
| 326 |
'//*[contains(@class, "td-post-content")]', |
| 327 |
'//*[contains(@class, "tdb-block-inner")]', |
| 328 |
// Flavor / flavored themes |
| 329 |
'//*[contains(@class, "flavor-content")]', |
| 330 |
// flavor theme "the_content" wrapper |
| 331 |
'//*[contains(@class, "the_content_wrapper")]', |
| 332 |
// flavor "single-post-content" |
| 333 |
'//*[contains(@class, "single-post-content")]', |
| 334 |
// flavor article text |
| 335 |
'//*[contains(@class, "flavor-text")]', |
| 336 |
// flavor article body |
| 337 |
'//*[contains(@class, "flavor-article")]', |
| 338 |
// flavor tdb |
| 339 |
'//*[contains(@class, "tdb_single_content")]', |
| 340 |
// flavor td_module_wrap |
| 341 |
'//*[contains(@class, "td-module-content")]', |
| 342 |
// flavor td |
| 343 |
'//*[contains(@class, "td-ss-main-content")]', |
| 344 |
// flavor tdi |
| 345 |
'//*[contains(@class, "wpb_text_column")]', |
| 346 |
// GeneratePress |
| 347 |
'//*[contains(@class, "inside-article")]', |
| 348 |
// Astra |
| 349 |
'//*[contains(@class, "ast-post-format-")]', |
| 350 |
// OceanWP |
| 351 |
'//*[contains(@class, "entry")]', |
| 352 |
// Flavor / flavored themes |
| 353 |
'//*[contains(@class, "flavor-article")]', |
| 354 |
// Generic / other frameworks |
| 355 |
'//*[contains(@class, "article__body")]', |
| 356 |
'//*[contains(@class, "story-body")]', |
| 357 |
'//*[contains(@class, "c-article-body")]', |
| 358 |
'//*[contains(@class, "content-area")]', |
| 359 |
'//*[@id="content"]', |
| 360 |
'//*[@role="main"]', |
| 361 |
'//main', |
| 362 |
); |
| 363 |
|
| 364 |
foreach ($content_selectors as $selector) { |
| 365 |
$nodes = $xpath->query($selector); |
| 366 |
if ($nodes && $nodes->length > 0) { |
| 367 |
$candidate = botwriter_rewriter_get_text_content($nodes->item(0)); |
| 368 |
if (strlen(trim($candidate)) > strlen(trim($content))) { |
| 369 |
$content = $candidate; |
| 370 |
} |
| 371 |
} |
| 372 |
} |
| 373 |
} |
| 374 |
|
| 375 |
// Strategy 3: Look for <article> tag (after class-based, because article |
| 376 |
// tags often include metadata/author/share buttons besides the text) |
| 377 |
if (empty(trim($content)) || strlen(trim($content)) < 200) { |
| 378 |
$article_nodes = $xpath->query('//article'); |
| 379 |
if ($article_nodes && $article_nodes->length > 0) { |
| 380 |
$candidate = botwriter_rewriter_get_text_content($article_nodes->item(0)); |
| 381 |
if (strlen(trim($candidate)) > strlen(trim($content))) { |
| 382 |
$content = $candidate; |
| 383 |
} |
| 384 |
} |
| 385 |
} |
| 386 |
|
| 387 |
// Strategy 4: Find the div with the most <p> text (simplified readability heuristic) |
| 388 |
if (empty(trim($content)) || strlen(trim($content)) < 200) { |
| 389 |
$divs = $xpath->query('//div'); |
| 390 |
$best_div = null; |
| 391 |
$best_score = 0; |
| 392 |
|
| 393 |
if ($divs) { |
| 394 |
foreach ($divs as $div) { |
| 395 |
$paragraphs = $xpath->query('.//p', $div); |
| 396 |
if (!$paragraphs) continue; |
| 397 |
|
| 398 |
$score = 0; |
| 399 |
$text_len = 0; |
| 400 |
foreach ($paragraphs as $p) { |
| 401 |
$p_text = trim($p->textContent); |
| 402 |
if (strlen($p_text) > 25) { |
| 403 |
$score++; |
| 404 |
$text_len += strlen($p_text); |
| 405 |
} |
| 406 |
} |
| 407 |
// Prefer containers with more paragraphs + text, but penalize |
| 408 |
// very large containers (likely body/wrapper) to find the tightest match |
| 409 |
$child_divs = $xpath->query('./div', $div); |
| 410 |
$nesting_penalty = $child_divs ? $child_divs->length * 10 : 0; |
| 411 |
$total_score = $score * 100 + $text_len - $nesting_penalty; |
| 412 |
if ($total_score > $best_score && $score >= 2) { |
| 413 |
$best_score = $total_score; |
| 414 |
$best_div = $div; |
| 415 |
} |
| 416 |
} |
| 417 |
} |
| 418 |
|
| 419 |
if ($best_div) { |
| 420 |
$candidate = botwriter_rewriter_get_text_content($best_div); |
| 421 |
if (strlen(trim($candidate)) > strlen(trim($content))) { |
| 422 |
$content = $candidate; |
| 423 |
} |
| 424 |
} |
| 425 |
} |
| 426 |
|
| 427 |
// Clean up content |
| 428 |
$content = trim($content); |
| 429 |
|
| 430 |
// Truncate if extremely long (e.g. over 15000 chars) |
| 431 |
if (strlen($content) > 15000) { |
| 432 |
$content = substr($content, 0, 15000) . "\n\n[Content truncated...]"; |
| 433 |
} |
| 434 |
|
| 435 |
if (empty($title) && empty($content)) { |
| 436 |
return new WP_Error('extraction_failed', 'Could not extract any content from the URL.'); |
| 437 |
} |
| 438 |
|
| 439 |
// Flag partial extractions so the frontend can warn the user |
| 440 |
$content_warning = ''; |
| 441 |
if (empty($content)) { |
| 442 |
$content_warning = 'no_content'; |
| 443 |
} elseif (strlen($content) < 200) { |
| 444 |
$content_warning = 'short_content'; |
| 445 |
} |
| 446 |
|
| 447 |
// Create excerpt from first 300 chars |
| 448 |
$excerpt = ''; |
| 449 |
if (!empty($content)) { |
| 450 |
$excerpt = wp_trim_words(wp_strip_all_tags($content), 40, '...'); |
| 451 |
} |
| 452 |
|
| 453 |
return array( |
| 454 |
'title' => sanitize_text_field($title), |
| 455 |
'content' => $content, |
| 456 |
'excerpt' => $excerpt, |
| 457 |
'url' => esc_url($url), |
| 458 |
'content_warning' => $content_warning, |
| 459 |
); |
| 460 |
} |
| 461 |
|
| 462 |
|
| 463 |
/** |
| 464 |
* Helper: Extract cleaned text content from a DOMNode. |
| 465 |
* Preserves paragraph structure but removes tags. |
| 466 |
* |
| 467 |
* @param DOMNode $node |
| 468 |
* @return string |
| 469 |
*/ |
| 470 |
function botwriter_rewriter_get_text_content($node) { |
| 471 |
$text = ''; |
| 472 |
foreach ($node->childNodes as $child) { |
| 473 |
if ($child->nodeType === XML_TEXT_NODE) { |
| 474 |
$text .= trim($child->textContent) . ' '; |
| 475 |
} elseif ($child->nodeType === XML_ELEMENT_NODE) { |
| 476 |
$tag = strtolower($child->nodeName); |
| 477 |
// Add line breaks for block elements |
| 478 |
if (in_array($tag, array('p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'br', 'blockquote', 'pre', 'tr'))) { |
| 479 |
$inner = botwriter_rewriter_get_text_content($child); |
| 480 |
if (!empty(trim($inner))) { |
| 481 |
$text .= "\n\n" . trim($inner); |
| 482 |
} |
| 483 |
} else { |
| 484 |
$text .= botwriter_rewriter_get_text_content($child); |
| 485 |
} |
| 486 |
} |
| 487 |
} |
| 488 |
return $text; |
| 489 |
} |
| 490 |
|