10, 'p' => 8, 'h3' => 7, 'image' => 6, 'list' => 5, 'blockquote' => 4, 'table' => 3, ]; public function __construct( private array $settings, private AdsensePlacementRenderer $renderer ) {} /** * Filtra the_content para insertar anuncios */ public function inject(string $content): string { // PASO 0: Validar longitud minima (aplica a todos los modos) $minLength = (int)($this->settings['forms']['min_content_length'] ?? 500); if (strlen(strip_tags($content)) < $minLength) { return $content; } // Determinar modo de operacion $mode = $this->settings['incontent_advanced']['incontent_mode'] ?? 'paragraphs_only'; // DEBUG TEMPORAL: Insertar comentario HTML para diagnosticar $debugComment = sprintf( '', $mode, isset($this->settings['incontent_advanced']) ? 'YES' : 'NO' ); if ($mode === 'paragraphs_only') { return $debugComment . $this->injectParagraphsOnly($content); } return $debugComment . $this->injectAdvanced($content); } /** * Modo solo parrafos: logica clasica que inserta anuncios unicamente despues de parrafos */ private function injectParagraphsOnly(string $content): string { if (!($this->settings['behavior']['post_content_enabled'] ?? false)) { return $content; } // Obtener configuracion de behavior (modo solo parrafos) $minAds = (int)($this->settings['behavior']['post_content_min_ads'] ?? 1); $maxAds = (int)($this->settings['behavior']['post_content_max_ads'] ?? 3); $afterParagraphs = (int)($this->settings['behavior']['post_content_after_paragraphs'] ?? 3); $minBetween = (int)($this->settings['behavior']['post_content_min_paragraphs_between'] ?? 4); $randomMode = ($this->settings['behavior']['post_content_random_mode'] ?? true) === true; // Validar min <= max if ($minAds > $maxAds) { $minAds = $maxAds; } // Dividir contenido en parrafos $paragraphs = $this->splitIntoParagraphs($content); $totalParagraphs = count($paragraphs); // Necesitamos al menos afterParagraphs + 1 parrafos if ($totalParagraphs < $afterParagraphs + 1) { return $content; } // Calcular posiciones de insercion $adPositions = $this->calculateParagraphsOnlyPositions( $totalParagraphs, $afterParagraphs, $minBetween, $minAds, $maxAds, $randomMode ); if (empty($adPositions)) { return $content; } // Reconstruir contenido con anuncios insertados return $this->buildParagraphsOnlyContent($paragraphs, $adPositions); } /** * Modo avanzado: multiples tipos de elementos */ private function injectAdvanced(string $content): string { $config = $this->settings['incontent_advanced'] ?? []; $debugSteps = []; // DEBUG // Obtener configuracion $maxAds = (int)($config['incontent_max_total_ads'] ?? 8); $minSpacing = (int)($config['incontent_min_spacing'] ?? 3); $priorityMode = $config['incontent_priority_mode'] ?? 'position'; $format = $config['incontent_format'] ?? 'in-article'; $debugSteps['config'] = "maxAds=$maxAds, minSpacing=$minSpacing, priority=$priorityMode"; // DEBUG // PASO 1: Escanear contenido para encontrar todas las ubicaciones $locations = $this->scanContent($content); $debugSteps['step1_scan'] = count($locations); // DEBUG if (empty($locations)) { return '' . $content; } // PASO 2: Filtrar por configuracion (enabled) $locations = $this->filterByEnabled($locations, $config); $debugSteps['step2_enabled'] = count($locations); // DEBUG if (empty($locations)) { return '' . $content; } // PASO 3: Aplicar probabilidad deterministica $postId = get_the_ID() ?: 0; $locations = $this->applyProbability($locations, $config, $postId); $debugSteps['step3_probability'] = count($locations); // DEBUG if (empty($locations)) { return '' . $content; } // PASO 4-5: Filtrar por espaciado y limite (segun priority_mode) if ($priorityMode === 'priority') { $locations = $this->filterByPriorityFirst($locations, $minSpacing, $maxAds); } else { $locations = $this->filterByPositionFirst($locations, $minSpacing, $maxAds); } $debugSteps['step4_spacing'] = count($locations); // DEBUG if (empty($locations)) { return '' . $content; } // Ordenar por posicion para insercion correcta usort($locations, fn($a, $b) => $a['position'] <=> $b['position']); $debugSteps['final_ads'] = count($locations); // DEBUG // Insertar debug comment $content = '' . $content; // PASO 6: Insertar anuncios return $this->insertAds($content, $locations, $format); } /** * PASO 1: Escanea el contenido para encontrar ubicaciones elegibles * * @return array{position: int, type: string, tag: string, element_index: int}[] */ private function scanContent(string $content): array { $locations = []; $elementIndex = 0; // Regex para encontrar tags de cierre de elementos de bloque $pattern = '/(<\/(?:p|h2|h3|figure|ul|ol|table|blockquote)>)/i'; // Encontrar todas las coincidencias con sus posiciones if (preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE)) { foreach ($matches[0] as $match) { $tag = strtolower($match[0]); $position = $match[1] + strlen($match[0]); // Posicion despues del tag $type = $this->getTypeFromTag($tag); if ($type) { $locations[] = [ 'position' => $position, 'type' => $type, 'tag' => $tag, 'element_index' => $elementIndex++, ]; } } } // Detectar imagenes standalone (no dentro de figure) $locations = array_merge($locations, $this->scanStandaloneImages($content, $elementIndex)); // Validar listas (minimo 3 items) $locations = $this->validateLists($content, $locations); // Ordenar por posicion usort($locations, fn($a, $b) => $a['position'] <=> $b['position']); // Reasignar indices de elemento foreach ($locations as $i => &$loc) { $loc['element_index'] = $i; } return $locations; } /** * Convierte tag de cierre a tipo de elemento */ private function getTypeFromTag(string $tag): ?string { return match ($tag) { '

' => 'p', '' => 'h2', '' => 'h3', '' => 'image', '', '' => 'list', '' => 'table', '' => 'blockquote', default => null, }; } /** * Detecta imagenes que no estan dentro de figure */ private function scanStandaloneImages(string $content, int $startIndex): array { $locations = []; // Encontrar todas las imagenes con sus posiciones if (!preg_match_all('/]*>/i', $content, $matches, PREG_OFFSET_CAPTURE)) { return $locations; } foreach ($matches[0] as $match) { $imgTag = $match[0]; $imgPosition = $match[1]; // Verificar si hay un
abierto antes de esta imagen $contentBefore = substr($content, 0, $imgPosition); $lastFigureOpen = strrpos($contentBefore, ''); // Si hay figure abierto sin cerrar, esta imagen esta dentro de figure if ($lastFigureOpen !== false && ($lastFigureClose === false || $lastFigureClose < $lastFigureOpen)) { continue; // Ignorar, se contara con
} // Imagen standalone - calcular posicion despues del tag $endPosition = $imgPosition + strlen($imgTag); // Si la imagen esta seguida de

o similar, usar esa posicion $contentAfter = substr($content, $endPosition, 20); if (preg_match('/^\s*<\/p>/i', $contentAfter, $closeMatch)) { // La imagen esta dentro de un parrafo, no es standalone continue; } $locations[] = [ 'position' => $endPosition, 'type' => 'image', 'tag' => $imgTag, 'element_index' => $startIndex++, ]; } return $locations; } /** * Valida que las listas tengan minimo 3 items */ private function validateLists(string $content, array $locations): array { return array_filter($locations, function ($loc) use ($content) { if ($loc['type'] !== 'list') { return true; } // Encontrar el contenido de la lista $endPos = $loc['position']; $tag = $loc['tag']; $openTag = str_replace('/', '', $tag); // ->