is_gutenberg = (strpos($content, '/s', ' ', $text); $text = preg_replace('/<(script|style)\b[^>]*>.*?<\/\1>/is', ' ', $text); $text = preg_replace('/<[^>]*>/', ' ', $text); } $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $unspaced = preg_match_all('/[\p{Han}\p{Hiragana}\p{Katakana}\p{Thai}\p{Lao}\p{Khmer}\p{Myanmar}]/u', $text); if ($unspaced === false) { return str_word_count($text); } $text = preg_replace('/[\p{Han}\p{Hiragana}\p{Katakana}\p{Thai}\p{Lao}\p{Khmer}\p{Myanmar}]/u', ' ', $text); $words = preg_match_all('/[\p{L}\p{N}][\p{L}\p{N}\p{M}\'’_-]*/u', $text); return (int) $words + (int) ceil($unspaced / 2); } public function is_gutenberg() { return $this->is_gutenberg; } public static function content_max_width() { $default = (int) ($GLOBALS['content_width'] ?? 0); return (int) apply_filters('adghost_content_max_width', $default); } public static function fits_width($size, $max_px) { if ($max_px <= 0) { return true; } $w = (int) explode('x', trim($size))[0]; return $w === 0 || $w <= $max_px; } public static function filter_sizes($sizes_str, $max_px) { if ($max_px <= 0 || $sizes_str === '') { return $sizes_str; } $list = array_map('trim', preg_split('/[;,]/', $sizes_str)); $kept = array_values(array_filter($list, function ($s) use ($max_px) { return $s !== '' && self::fits_width($s, $max_px); })); return empty($kept) ? $sizes_str : implode(';', $kept); } public function is_inside_container($offset) { foreach ($this->container_ranges as $range) { if ($offset > $range[0] && $offset < $range[1]) { return true; } } return false; } public function filter_points(array $points, ?array &$rejected = null) { $collect = is_array($rejected); $allowed = self::allowed_point_types(); $out = []; foreach ($points as $p) { $type = $p['type'] ?? ''; if (!in_array($type, $allowed, true)) { if ($collect && !empty($p['id']) && $type !== '') { $rejected[$p['id']] = sprintf('%s points are not enabled for placement', ucfirst($type)); } continue; } $reason = null; if (empty($p['validation']['is_safe'])) { $reasons = $p['validation']['reasons'] ?? []; $reason = empty($reasons) ? 'Unsafe insertion point' : implode(', ', $reasons); } elseif ($type === 'paragraph' && isset($p['word_count']) && $p['word_count'] < self::MIN_WORDS) { $reason = sprintf( 'Paragraph too short (%d words, minimum %d)', (int) $p['word_count'], self::MIN_WORDS ); } elseif ($this->is_inside_container($p['offset'])) { $reason = 'Inside a Gutenberg container block'; } if ($reason !== null) { if ($collect) { $rejected[$p['id']] = $reason; } continue; } $out[] = $p; } $kept = $this->deduplicate($out); if ($collect && count($kept) < count($out)) { $kept_ids = array_flip(array_column($kept, 'id')); foreach ($out as $p) { if (!isset($kept_ids[$p['id']])) { $rejected[$p['id']] = sprintf( 'Within %d characters of the previous insertion point', self::DEDUP_CHARS ); } } } return $kept; } public static function allowed_point_types() { $types = ['paragraph']; if (get_option('adghost_place_before_headings', false)) { $types[] = 'heading'; } return (array) apply_filters('adghost_allowed_point_types', $types); } public function strip_excluded(array $points, $content, array $selectors) { $classes = []; foreach ($selectors as $sel) { $clean = preg_replace('/[^a-z0-9_-]/i', '', (string) $sel); if ($clean !== '') { $classes[] = $clean; } } if (empty($classes)) { return $points; } $ranges = []; foreach ($classes as $cls) { $ranges = array_merge($ranges, $this->class_ranges($content, $cls)); } if (empty($ranges)) { return $points; } return array_values(array_filter($points, function ($p) use ($ranges) { foreach ($ranges as $r) { if ($p['offset'] > $r[0] && $p['offset'] < $r[1]) { return false; } } return true; })); } private function class_ranges($content, $class) { $ranges = []; $pattern = '/<([a-z0-9]+)\b[^>]*class="(?:[^"]*\s)?' . preg_quote($class, '/') . '(?:\s[^"]*)?"[^>]*>/i'; if (!preg_match_all($pattern, $content, $opens, PREG_OFFSET_CAPTURE)) { return $ranges; } foreach ($opens[0] as $i => $open) { $tag = strtolower($opens[1][$i][0]); $start = $open[1]; $cursor = $start + strlen($open[0]); $depth = 1; if (preg_match_all('/<(\/?)' . $tag . '\b[^>]*>/i', $content, $tags, PREG_OFFSET_CAPTURE, $cursor)) { foreach ($tags[0] as $j => $t) { $depth += ($tags[1][$j][0] === '/') ? -1 : 1; if ($depth === 0) { $ranges[] = [$start, $t[1] + strlen($t[0])]; break; } } } } return $ranges; } public function deduplicate(array $points) { usort($points, function ($a, $b) { return $a['offset'] <=> $b['offset']; }); $out = []; $last_offset = null; foreach ($points as $p) { if ($last_offset === null || ($p['offset'] - $last_offset) >= self::DEDUP_CHARS) { $out[] = $p; $last_offset = $p['offset']; } } return $out; } private function parse_container_ranges($content) { $ranges = []; $stack = []; if (!preg_match_all('//is', $content, $m, PREG_OFFSET_CAPTURE)) { return $ranges; } foreach ($m[0] as $i => $tag) { $is_close = ($m[1][$i][0] === '/'); $name = $m[2][$i][0]; $offset = $tag[1]; $length = strlen($tag[0]); if (!in_array($name, self::CONTAINER_BLOCKS, true)) { continue; } if (!$is_close) { if (substr(rtrim($tag[0]), -4) === '/-->') { continue; } $stack[] = ['name' => $name, 'start' => $offset]; } else { for ($s = count($stack) - 1; $s >= 0; $s--) { if ($stack[$s]['name'] === $name) { $ranges[] = [$stack[$s]['start'], $offset + $length]; array_splice($stack, $s, 1); break; } } } } return $ranges; } }