$b['paragraph_number']; }); $rule = $unit['insertionRule'] ?? 'every_n'; $value = (int) ($unit['insertionValue'] ?? $frequency); switch ($rule) { case 'after_n': case 'before_n': $selected = self::rule_paragraphs($safe, self::paragraph_list($unit, (int) ($opts['last_paragraph'] ?? 0)), $rule === 'before_n'); return $max > 0 ? array_slice($selected, 0, $max) : $selected; case 'before_last': $selected = self::rule_before_last($safe, (int) ($opts['last_paragraph'] ?? 0)); break; case 'auto': $selected = self::rule_auto($safe, $frequency, $min_words, $placed); break; case 'every_n': default: $selected = self::rule_every_n($safe, $value > 0 ? $value : $frequency, $min_words, $placed); break; } if (empty($selected) && !empty($safe) && in_array($rule, ['every_n', 'auto'], true)) { $selected = [self::highest_scored($safe)]; } $selected = self::enforce_min_gap($selected, $rule === 'auto' ? max(1, $min_gap - 1) : $min_gap, $min_words); if ($max > 0 && count($selected) > $max) { $selected = array_slice($selected, 0, $max); } return $selected; } private static function rule_every_n(array $safe, $n, $min_words = 0, array $placed = []) { $n = max(1, (int) $n); $out = []; $last_para = 0; $last_words = null; foreach ($safe as $p) { self::reset_on_placed($placed, $p, $last_para, $last_words); if ($p['paragraph_number'] - $last_para < $n) { continue; } if ($last_words !== null && $min_words > 0 && self::words($p) - $last_words < $min_words) { continue; } $out[] = $p; $last_para = $p['paragraph_number']; $last_words = self::words($p); } return $out; } private static function reset_on_placed(array $placed, array $p, &$last_para, &$last_words) { foreach ($placed as [$para, $words]) { if ($para <= $p['paragraph_number'] && $para > $last_para) { $last_para = (int) $para; $last_words = $words === null ? $last_words : (int) $words; } } } private static function words(array $p) { return (int) ($p['words_before'] ?? 0); } const POSITION_TOKEN = '/^(?:\d{1,3}|-\d{1,3}|0?\.\d{1,3}|\[\d{1,3}\s*-\s*\d{1,3}\])$/'; public static function sanitize_positions($raw) { $out = []; foreach (self::position_tokens($raw) as $token) { if ($token !== '' && preg_match(self::POSITION_TOKEN, $token) && !in_array($token, $out, true)) { $out[] = $token; } } return implode(', ', array_slice($out, 0, 30)); } private static function position_tokens($raw) { $raw = preg_replace('/\[\s*(\d+)\s*-\s*(\d+)\s*\]/', '[$1-$2]', (string) $raw); return array_values(array_filter(preg_split('/[\s,]+/', $raw), 'strlen')); } public static function paragraph_list(array $unit, $total = 0) { $raw = trim((string) ($unit['insertionParagraphs'] ?? '')); if ($raw === '') { $raw = (string) ($unit['insertionValue'] ?? ''); } $total = (int) $total; $list = []; foreach (self::position_tokens($raw) as $token) { if (preg_match('/^\[(\d{1,3})-(\d{1,3})\]$/', $token, $m)) { $lo = min((int) $m[1], (int) $m[2]); $hi = max((int) $m[1], (int) $m[2]); if ($total > 0) { $hi = min($hi, $total); } $n = $lo <= $hi ? wp_rand($lo, $hi) : 0; } elseif (preg_match('/^-(\d{1,3})$/', $token, $m)) { $n = $total > 0 ? $total + 1 - (int) $m[1] : 0; } elseif (preg_match('/^0?\.(\d{1,3})$/', $token)) { $n = $total > 0 ? max(1, (int) round((float) $token * $total)) : 0; } elseif (preg_match('/^\d+$/', $token)) { $n = (int) $token; } else { preg_match_all('/\d+/', $token, $digits); foreach ($digits[0] as $d) { $list[] = (int) $d; } continue; } $list[] = $n; } $list = array_values(array_unique(array_filter($list, function ($n) { return $n >= 1 && $n <= 999; }))); sort($list); return array_slice($list, 0, 30); } public static function filter_by_text(array $points, array $unit, ?array &$rejected = null) { $mode = (string) ($unit['paragraphTextMode'] ?? ''); $words = array_values(array_filter(array_map('trim', explode(',', (string) ($unit['paragraphText'] ?? ''))))); if (!$words || !in_array($mode, ['contain', 'exclude'], true)) { return $points; } $out = []; foreach ($points as $p) { $text = (string) ($p['text'] ?? ''); $found = false; foreach ($words as $word) { if (mb_stripos($text, $word) !== false) { $found = true; break; } } if ($found === ($mode === 'contain')) { $out[] = $p; } elseif (is_array($rejected) && isset($p['id'])) { $rejected[$p['id']] = $mode === 'contain' ? sprintf('%s: the paragraph above does not contain %s', $unit['name'] ?? 'Unit', implode(' or ', $words)) : sprintf('%s: the paragraph above contains an excluded word', $unit['name'] ?? 'Unit'); } } return $out; } private static function rule_paragraphs(array $safe, array $list, $before) { $out = []; $taken = []; $count = count($list); foreach ($list as $i => $n) { if ($before) { $want = $n - 1; $floor = $i > 0 ? $list[$i - 1] : 0; for ($j = count($safe) - 1; $j >= 0; $j--) { $pn = $safe[$j]['paragraph_number']; if ($pn <= $want && $pn >= $floor && !isset($taken[$pn])) { $out[] = $safe[$j]; $taken[$pn] = true; break; } } } else { $ceiling = $i + 1 < $count ? $list[$i + 1] - 1 : PHP_INT_MAX; foreach ($safe as $p) { $pn = $p['paragraph_number']; if ($pn >= $n && $pn <= $ceiling && !isset($taken[$pn])) { $out[] = $p; $taken[$pn] = true; break; } } } } usort($out, function ($a, $b) { return $a['paragraph_number'] <=> $b['paragraph_number']; }); return $out; } private static function rule_before_last(array $safe, $last = 0) { $count = count($safe); if ($count === 0) { return []; } if ($last > 1) { for ($j = $count - 1; $j >= 0; $j--) { if ($safe[$j]['paragraph_number'] <= $last - 1) { return [$safe[$j]]; } } return []; } if ($count === 1) { return [$safe[0]]; } return [$safe[$count - 2]]; } private static function rule_auto(array $safe, $min_gap, $min_words = 0, array $placed = []) { $min_gap = max(1, (int) $min_gap); $rank = ['heading' => 0, 'text' => 1, 'end' => 2, 'media' => 3, 'image' => 3]; $out = []; $last_para = 0; $last_words = null; $count = count($safe); for ($i = 0; $i < $count; $i++) { $p = $safe[$i]; self::reset_on_placed($placed, $p, $last_para, $last_words); if ($p['paragraph_number'] - $last_para < $min_gap) { continue; } if ($last_words !== null && $min_words > 0 && self::words($p) - $last_words < $min_words) { continue; } $best = $i; if ($i + 1 < $count && $safe[$i + 1]['paragraph_number'] === $p['paragraph_number'] + 1 && ($rank[$safe[$i + 1]['next'] ?? 'text'] ?? 1) < ($rank[$p['next'] ?? 'text'] ?? 1)) { $best = $i + 1; } $out[] = $safe[$best]; $last_para = $p['paragraph_number']; $last_words = self::words($safe[$best]); $i = $best; } return $out; } private static function enforce_min_gap(array $points, $min_gap, $min_words = 0) { if (count($points) <= 1) { return $points; } usort($points, function ($a, $b) { return $a['paragraph_number'] <=> $b['paragraph_number']; }); $out = []; $last = null; $last_words = 0; foreach ($points as $p) { if ($last === null || (($p['paragraph_number'] - $last) >= $min_gap && ($min_words <= 0 || self::words($p) - $last_words >= $min_words))) { $out[] = $p; $last = $p['paragraph_number']; $last_words = self::words($p); } } return $out; } private static function highest_scored(array $points) { usort($points, function ($a, $b) { $sa = ($a['score']['ux'] ?? 0) + ($a['score']['performance'] ?? 0); $sb = ($b['score']['ux'] ?? 0) + ($b['score']['performance'] ?? 0); return $sb <=> $sa; }); return $points[0]; } }