s, before callouts can inject any. if ($tier === 'article' || $tier === 'doc') { $html = self::steps($html); } // Shared transforms (safe for every web tier). $html = self::callouts($html); $html = self::wrap_tables($html); // Article + document tiers get the richer components. if ($tier === 'article' || $tier === 'doc') { $html = self::takeaways($html); $html = self::faq($html, $content_type); } $class = 'acb-content'; if ($tier === 'doc') $class .= ' acb-doc'; if ($tier === 'body') $class .= ' acb-body'; return '
' . "\n" . $html . "\n" . '
'; } /* ───────────────────────── transforms ───────────────────────── */ /** "Pro tip: …", "Note: …", "Warning: …" paragraphs → callout boxes. */ private static function callouts($html) { $variants = [ 'tip' => 'Pro tip|Tip|Hint', 'warn' => 'Warning|Caution|Important|Common mistake', 'note' => 'Note|Remember|FYI', ]; foreach ($variants as $variant => $labels) { $vclass = ($variant === 'note') ? '' : ' ' . $variant; // Case A: the label is bold — model clearly meant a callout (colon optional). $patternA = '/

\s*\s*(' . $labels . ')\s*:?\s*<\/strong>\s*:?\s*(.+?)<\/p>/is'; $r = preg_replace_callback($patternA, function ($m) use ($vclass) { return '

' . esc_html(trim($m[1])) . '

' . trim($m[2]) . '

'; }, $html); if (is_string($r)) $html = $r; // Case B: plain "Label: …" — require the colon to avoid false positives. $patternB = '/

\s*(' . $labels . ')\s*:\s*(.+?)<\/p>/is'; $r = preg_replace_callback($patternB, function ($m) use ($vclass) { return '

' . esc_html(trim($m[1])) . '

' . trim($m[2]) . '

'; }, $html); if (is_string($r)) $html = $r; } return $html; } /** Make tables horizontally scrollable on narrow screens. */ private static function wrap_tables($html) { $r = preg_replace('/(]*>.*?<\/table>)/is', '
$1
', $html); return is_string($r) ? $r : $html; } /** A "Key takeaways" / "TL;DR" heading followed by a list → highlighted box. */ private static function takeaways($html) { $pattern = '/]*>\s*(?:Key takeaways|Key points|Takeaways|TL;DR|TLDR|In summary)\s*<\/h[23]>\s*(]*>.*?<\/ul>)/is'; $r = preg_replace_callback($pattern, function ($m) { return '
Key takeaways' . $m[1] . '
'; }, $html); return is_string($r) ? $r : $html; } /** "### Step N: Title" runs → numbered process cards. */ private static function steps($html) { // Each step: an

beginning "Step N" + its body up to the next heading. $pattern = '/]*>\s*Step\s+(\d+)\s*[:.)\-]?\s*(.*?)<\/h3>(.*?)(?=]*>|$)/is'; $r = preg_replace_callback($pattern, function ($m) { $num = trim($m[1]); $title = trim($m[2]); // inner HTML kept; wp_kses cleans it later $body = trim($m[3]); $head = ($title !== '') ? '

' . $title . '

' : ''; return '
' . esc_html($num) . '' . $head . $body . '
'; }, $html); if (!is_string($r)) return $html; $html = $r; // Group consecutive cards into a .acb-steps container. Card bodies hold no // nested
at this point (callouts run after steps), so .*?
is safe. $r = preg_replace( '/(?:
.*?<\/div>\s*)+/is', '
$0
', $html ); return is_string($r) ? $r : $html; } /** * FAQ → accordions. * - faq_page: the whole body is Q/A (questions as H2). Convert all pairs. * - otherwise: find an "FAQ"/"Frequently asked questions" heading and convert * the question/answer pairs that follow it, up to the next same-or-higher heading. */ private static function faq($html, $content_type) { if ($content_type === 'faq_page') { if (!preg_match('/]*>/i', $html, $hm, PREG_OFFSET_CAPTURE)) return $html; $intro = substr($html, 0, $hm[0][1]); $body = substr($html, $hm[0][1]); $acc = self::faq_pairs($body, '[2-6]'); if ($acc === '') return $html; return $intro . '
' . $acc . '
'; } if (!preg_match('/]*>\s*(?:FAQ|FAQs|Frequently asked questions|Common questions)\s*<\/h\1>/i', $html, $m, PREG_OFFSET_CAPTURE)) { return $html; } $level = (int) $m[1][0]; $headingFull = $m[0][0]; $headingOffset = $m[0][1]; $sectionStart = $headingOffset + strlen($headingFull); $rest = substr($html, $sectionStart); // Section ends at the next heading of the same or higher level. if (preg_match('/]*>/i', $rest, $bm, PREG_OFFSET_CAPTURE)) { $sectionBody = substr($rest, 0, $bm[0][1]); $after = substr($rest, $bm[0][1]); } else { $sectionBody = $rest; $after = ''; } // Questions are headings *below* the FAQ heading's level. $qlevels = '[' . ($level + 1) . '-6]'; $acc = self::faq_pairs($sectionBody, $qlevels); if ($acc === '') return $html; $before = substr($html, 0, $sectionStart); return $before . "\n" . '
' . $acc . '
' . "\n" . $after; } /** Build
items from "question answer…" runs. */ private static function faq_pairs($section, $hlevels) { $pattern = '/]*>(.*?)<\/h' . $hlevels . '>(.*?)(?=]*>|$)/is'; if (!preg_match_all($pattern, $section, $matches, PREG_SET_ORDER)) { return ''; } $items = ''; $i = 0; foreach ($matches as $q) { $question = trim(wp_strip_all_tags($q[1])); $answer = trim($q[2]); if ($question === '' || $answer === '') continue; $open = ($i === 0) ? ' open' : ''; $items .= '' . esc_html($question) . '' . '
' . $answer . '
'; $i++; } return $items; } }