excluido - no insertamos ads despues de tablas
*/
private function getTypeFromTag(string $tag): ?string
{
return match ($tag) {
'' => 'p',
'' => 'h2',
'' => 'h3',
'' => 'image',
'', '' => 'list',
'' => 'blockquote',
default => null,
};
}
/**
* Detecta imagenes que no estan dentro de figure ni zonas prohibidas
*/
private function scanStandaloneImages(string $content, int $startIndex, array $forbiddenZones = []): 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 que no este en zona prohibida
if ($this->isInForbiddenZone($imgPosition, $forbiddenZones)) {
continue;
}
// 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); // ->
// Buscar hacia atras el tag de apertura
$contentBefore = substr($content, 0, $endPos);
$lastOpen = strrpos($contentBefore, '<' . substr($openTag, 1)); //
= 3;
});
}
/**
* PASO 2: Filtra ubicaciones por campos enabled
*/
private function filterByEnabled(array $locations, array $config): array
{
$enabledTypes = [];
$typeMapping = [
'h2' => 'incontent_after_h2_enabled',
'h3' => 'incontent_after_h3_enabled',
'p' => 'incontent_after_paragraphs_enabled',
'image' => 'incontent_after_images_enabled',
'list' => 'incontent_after_lists_enabled',
'blockquote' => 'incontent_after_blockquotes_enabled',
'table' => 'incontent_after_tables_enabled',
];
foreach ($typeMapping as $type => $field) {
if ($config[$field] ?? false) {
$enabledTypes[] = $type;
}
}
return array_filter($locations, fn($loc) => in_array($loc['type'], $enabledTypes, true));
}
/**
* PASO 3: Aplica probabilidad deterministica usando seed del dia
*/
private function applyProbability(array $locations, array $config, int $postId): array
{
// Calcular seed deterministico
$seed = crc32($postId . date('Y-m-d'));
mt_srand($seed);
$probMapping = [
'h2' => 'incontent_after_h2_probability',
'h3' => 'incontent_after_h3_probability',
'p' => 'incontent_after_paragraphs_probability',
'image' => 'incontent_after_images_probability',
'list' => 'incontent_after_lists_probability',
'blockquote' => 'incontent_after_blockquotes_probability',
'table' => 'incontent_after_tables_probability',
];
return array_filter($locations, function ($loc) use ($config, $probMapping) {
$field = $probMapping[$loc['type']] ?? null;
if (!$field) {
return true;
}
$probability = (int)($config[$field] ?? 100);
$roll = mt_rand(1, 100);
return $roll <= $probability;
});
}
/**
* PASO 4-5 (modo position): Filtrar por espaciado primero, luego por prioridad
*/
private function filterByPositionFirst(array $locations, int $minSpacing, int $maxAds): array
{
// PASO 4: Ordenar por posicion y filtrar por espaciado
usort($locations, fn($a, $b) => $a['element_index'] <=> $b['element_index']);
$filtered = [];
$lastIndex = -999;
foreach ($locations as $loc) {
if ($loc['element_index'] - $lastIndex >= $minSpacing) {
$filtered[] = $loc;
$lastIndex = $loc['element_index'];
}
}
// PASO 5: Si excede max, seleccionar por prioridad
if (count($filtered) > $maxAds) {
usort($filtered, fn($a, $b) =>
(self::ELEMENT_PRIORITIES[$b['type']] ?? 0) <=> (self::ELEMENT_PRIORITIES[$a['type']] ?? 0)
);
$filtered = array_slice($filtered, 0, $maxAds);
}
return $filtered;
}
/**
* PASO 4-5 (modo priority): Ordenar por prioridad primero, luego aplicar espaciado
*/
private function filterByPriorityFirst(array $locations, int $minSpacing, int $maxAds): array
{
// PASO 4: Ordenar por prioridad
usort($locations, fn($a, $b) =>
(self::ELEMENT_PRIORITIES[$b['type']] ?? 0) <=> (self::ELEMENT_PRIORITIES[$a['type']] ?? 0)
);
// PASO 5: Filtrar por espaciado en orden de prioridad
$selected = [];
$usedIndices = [];
foreach ($locations as $loc) {
if (count($selected) >= $maxAds) {
break;
}
// Verificar espaciado con ubicaciones ya seleccionadas
$violatesSpacing = false;
foreach ($usedIndices as $usedIndex) {
if (abs($loc['element_index'] - $usedIndex) < $minSpacing) {
$violatesSpacing = true;
break;
}
}
if (!$violatesSpacing) {
$selected[] = $loc;
$usedIndices[] = $loc['element_index'];
}
}
return $selected;
}
/**
* PASO 6: Inserta los anuncios en las posiciones calculadas
*/
private function insertAds(string $content, array $locations, string $format): string
{
// Insertar de atras hacia adelante para no afectar posiciones
$locations = array_reverse($locations);
$adCount = count($locations);
$currentAd = $adCount;
foreach ($locations as $loc) {
$adHtml = $this->renderer->renderSlot($this->settings, 'post-content-adv-' . $currentAd);
$content = substr_replace($content, $adHtml, $loc['position'], 0);
$currentAd--;
}
return $content;
}
// =========================================================================
// METODOS LEGACY (sin cambios para backward compatibility)
// =========================================================================
/**
* Divide el contenido en parrafos preservando el HTML
*/
private function splitIntoParagraphs(string $content): array
{
// Dividir por , pero mantener el tag
$parts = preg_split('/(<\/p>)/i', $content, -1, PREG_SPLIT_DELIM_CAPTURE);
$paragraphs = [];
$current = '';
foreach ($parts as $part) {
$current .= $part;
if (strtolower($part) === '') {
$paragraphs[] = $current;
$current = '';
}
}
// Si hay contenido restante (sin cerrar), agregarlo
if (!empty(trim($current))) {
$paragraphs[] = $current;
}
return $paragraphs;
}
/**
* Calcula las posiciones donde insertar anuncios (modo solo parrafos)
*
* @return int[] Indices de parrafos despues de los cuales insertar ads
*/
private function calculateParagraphsOnlyPositions(
int $totalParagraphs,
int $afterFirst,
int $minBetween,
int $minAds,
int $maxAds,
bool $randomMode
): array {
// Calcular posiciones disponibles respetando el espacio minimo
$availablePositions = [];
// La primera posicion siempre es despues del parrafo indicado
if ($afterFirst < $totalParagraphs) {
$availablePositions[] = $afterFirst;
}
// Calcular posiciones adicionales respetando minBetween
$nextPosition = $afterFirst + $minBetween;
while ($nextPosition < $totalParagraphs - 1) { // -1 para no insertar al final
$availablePositions[] = $nextPosition;
$nextPosition += $minBetween;
}
// Determinar cuantos ads insertar
$maxPossible = count($availablePositions);
if ($maxPossible === 0) {
return [];
}
// Limitar por maxAds y lo que el contenido permite
$actualMax = min($maxAds, $maxPossible);
$actualMin = min($minAds, $actualMax);
// Determinar cantidad final de ads
if ($randomMode) {
// En modo random, elegir cantidad aleatoria entre min y max
$numAds = rand($actualMin, $actualMax);
} else {
// En modo fijo, usar el maximo posible
$numAds = $actualMax;
}
if ($numAds === 0) {
return [];
}
// Seleccionar posiciones
if ($randomMode && $numAds < $maxPossible) {
// Modo random: elegir posiciones aleatorias
// Siempre incluir la primera posicion
$selectedPositions = [$availablePositions[0]];
if ($numAds > 1) {
// Elegir aleatoriamente del resto
$remainingPositions = array_slice($availablePositions, 1);
shuffle($remainingPositions);
$additionalPositions = array_slice($remainingPositions, 0, $numAds - 1);
$selectedPositions = array_merge($selectedPositions, $additionalPositions);
}
// Ordenar para insertar en orden correcto
sort($selectedPositions);
return $selectedPositions;
} else {
// Modo fijo o todas las posiciones necesarias
return array_slice($availablePositions, 0, $numAds);
}
}
/**
* Reconstruye el contenido insertando anuncios en las posiciones indicadas (modo solo parrafos)
*/
private function buildParagraphsOnlyContent(array $paragraphs, array $adPositions): string
{
$newContent = '';
$adsInserted = 0;
foreach ($paragraphs as $index => $paragraph) {
$newContent .= $paragraph;
// Verificar si debemos insertar un ad despues de este parrafo
// El indice es 0-based, las posiciones son 1-based (parrafo #3 = index 2)
$paragraphNumber = $index + 1;
if (in_array($paragraphNumber, $adPositions, true)) {
$adsInserted++;
$adHtml = $this->renderer->renderSlot($this->settings, 'post-content-' . $adsInserted);
$newContent .= $adHtml;
}
}
return $newContent;
}
}