Compare commits
3 Commits
a33c43a104
...
6be292e085
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6be292e085 | ||
|
|
885276aad1 | ||
|
|
1e6a076904 |
@@ -8,9 +8,10 @@ use ROITheme\Shared\Domain\Exceptions\ValidationException;
|
|||||||
/**
|
/**
|
||||||
* Value Object para ID único de snippet CSS
|
* Value Object para ID único de snippet CSS
|
||||||
*
|
*
|
||||||
* Soporta dos formatos:
|
* Soporta tres formatos:
|
||||||
* 1. Generado: css_[timestamp]_[random] (ej: "css_1701432000_a1b2c3")
|
* 1. Generado: css_[timestamp]_[random] (ej: "css_1701432000_a1b2c3")
|
||||||
* 2. Legacy/Migración: kebab-case (ej: "cls-tables-apu", "generic-tables")
|
* 2. Legacy con prefijo: css_[descriptive]_[number] (ej: "css_tablas_apu_1764624826")
|
||||||
|
* 3. Legacy kebab-case: (ej: "cls-tables-apu", "generic-tables")
|
||||||
*
|
*
|
||||||
* Esto permite migrar snippets existentes sin romper IDs.
|
* Esto permite migrar snippets existentes sin romper IDs.
|
||||||
*/
|
*/
|
||||||
@@ -18,6 +19,7 @@ final class SnippetId
|
|||||||
{
|
{
|
||||||
private const PREFIX = 'css_';
|
private const PREFIX = 'css_';
|
||||||
private const PATTERN_GENERATED = '/^css_[0-9]+_[a-z0-9]{6}$/';
|
private const PATTERN_GENERATED = '/^css_[0-9]+_[a-z0-9]{6}$/';
|
||||||
|
private const PATTERN_LEGACY_PREFIX = '/^css_[a-z0-9_]+$/';
|
||||||
private const PATTERN_LEGACY = '/^[a-z0-9]+(-[a-z0-9]+)*$/';
|
private const PATTERN_LEGACY = '/^[a-z0-9]+(-[a-z0-9]+)*$/';
|
||||||
|
|
||||||
private function __construct(
|
private function __construct(
|
||||||
@@ -47,7 +49,8 @@ final class SnippetId
|
|||||||
|
|
||||||
// Validar formato generado (css_*)
|
// Validar formato generado (css_*)
|
||||||
if (str_starts_with($id, self::PREFIX)) {
|
if (str_starts_with($id, self::PREFIX)) {
|
||||||
if (!preg_match(self::PATTERN_GENERATED, $id)) {
|
// Acepta formato nuevo (css_timestamp_random) o legacy (css_descriptivo_numero)
|
||||||
|
if (!preg_match(self::PATTERN_GENERATED, $id) && !preg_match(self::PATTERN_LEGACY_PREFIX, $id)) {
|
||||||
throw new ValidationException(
|
throw new ValidationException(
|
||||||
sprintf('Formato de ID generado inválido: %s. Esperado: css_[timestamp]_[random]', $id)
|
sprintf('Formato de ID generado inválido: %s. Esperado: css_[timestamp]_[random]', $id)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
BASE STYLES - Todas las tablas genéricas
|
BASE STYLES - Todas las tablas genéricas
|
||||||
======================================== */
|
======================================== */
|
||||||
|
|
||||||
.post-content table:not(.analisis table) {
|
.post-content table:not(.analisis table):not(.desglose table) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin: 2rem auto;
|
margin: 2rem auto;
|
||||||
@@ -23,9 +23,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Header styles - VERY OBVIOUS */
|
/* Header styles - VERY OBVIOUS */
|
||||||
.post-content table:not(.analisis table) thead tr:first-child th,
|
.post-content table:not(.analisis table):not(.desglose table) thead tr:first-child th,
|
||||||
.post-content table:not(.analisis table) tbody tr:first-child td,
|
.post-content table:not(.analisis table):not(.desglose table) tbody tr:first-child td,
|
||||||
.post-content table:not(.analisis table) tr:first-child td {
|
.post-content table:not(.analisis table):not(.desglose table) tr:first-child td {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 1.25rem 1rem;
|
padding: 1.25rem 1rem;
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Body cells */
|
/* Body cells */
|
||||||
.post-content table:not(.analisis table) tbody tr:not(:first-child) td {
|
.post-content table:not(.analisis table):not(.desglose table) tbody tr:not(:first-child) td {
|
||||||
padding: 0.875rem 1rem;
|
padding: 0.875rem 1rem;
|
||||||
border: 1px solid var(--color-neutral-100);
|
border: 1px solid var(--color-neutral-100);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
/**
|
|
||||||
* Auto-detectar y agregar clases a filas especiales de tablas APU
|
|
||||||
*
|
|
||||||
* Este script detecta automáticamente filas especiales en tablas .desglose y .analisis
|
|
||||||
* y les agrega las clases CSS correspondientes para que se apliquen los estilos correctos.
|
|
||||||
*
|
|
||||||
* Detecta:
|
|
||||||
* - Section headers: Material, Mano de Obra, Herramienta, Equipo
|
|
||||||
* - Subtotal rows: Filas que empiezan con "Suma de"
|
|
||||||
* - Total row: Costo Directo
|
|
||||||
*
|
|
||||||
* @package Apus_Theme
|
|
||||||
* @since 1.0.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Agrega clases a filas especiales de tablas APU
|
|
||||||
*/
|
|
||||||
function applyApuTableClasses() {
|
|
||||||
// Buscar todas las tablas con clase .desglose o .analisis
|
|
||||||
const tables = document.querySelectorAll('.desglose table, .analisis table');
|
|
||||||
|
|
||||||
if (tables.length === 0) {
|
|
||||||
return; // No hay tablas APU en esta página
|
|
||||||
}
|
|
||||||
|
|
||||||
let classesAdded = 0;
|
|
||||||
|
|
||||||
tables.forEach(function(table) {
|
|
||||||
const rows = table.querySelectorAll('tbody tr');
|
|
||||||
|
|
||||||
rows.forEach(function(row) {
|
|
||||||
// Evitar procesar filas que ya tienen clase
|
|
||||||
if (row.classList.contains('section-header') ||
|
|
||||||
row.classList.contains('subtotal-row') ||
|
|
||||||
row.classList.contains('total-row')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const secondCell = row.querySelector('td:nth-child(2)');
|
|
||||||
if (!secondCell) {
|
|
||||||
return; // Fila sin segunda celda
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = secondCell.textContent.trim();
|
|
||||||
|
|
||||||
// Detectar section headers
|
|
||||||
if (text === 'Material' ||
|
|
||||||
text === 'Mano de Obra' ||
|
|
||||||
text === 'Herramienta' ||
|
|
||||||
text === 'Equipo' ||
|
|
||||||
text === 'MATERIAL' ||
|
|
||||||
text === 'MANO DE OBRA' ||
|
|
||||||
text === 'HERRAMIENTA' ||
|
|
||||||
text === 'EQUIPO') {
|
|
||||||
row.classList.add('section-header');
|
|
||||||
classesAdded++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detectar subtotales (cualquier variación de "Suma de")
|
|
||||||
if (text.toLowerCase().startsWith('suma de ') ||
|
|
||||||
text.toLowerCase().startsWith('subtotal ')) {
|
|
||||||
row.classList.add('subtotal-row');
|
|
||||||
classesAdded++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detectar total final
|
|
||||||
if (text === 'Costo Directo' ||
|
|
||||||
text === 'COSTO DIRECTO' ||
|
|
||||||
text === 'Total' ||
|
|
||||||
text === 'TOTAL' ||
|
|
||||||
text === 'Costo directo') {
|
|
||||||
row.classList.add('total-row');
|
|
||||||
classesAdded++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log para debugging (solo en desarrollo)
|
|
||||||
if (classesAdded > 0 && window.console) {
|
|
||||||
console.log('[APU Tables] Clases agregadas automáticamente: ' + classesAdded);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ejecutar cuando el DOM esté listo
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', applyApuTableClasses);
|
|
||||||
} else {
|
|
||||||
// DOM ya está listo
|
|
||||||
applyApuTableClasses();
|
|
||||||
}
|
|
||||||
|
|
||||||
})();
|
|
||||||
@@ -1,342 +0,0 @@
|
|||||||
/**
|
|
||||||
* Header Navigation JavaScript
|
|
||||||
*
|
|
||||||
* This file handles:
|
|
||||||
* - Mobile hamburger menu toggle
|
|
||||||
* - Sticky header behavior
|
|
||||||
* - Smooth scroll to anchors (optional)
|
|
||||||
* - Accessibility features (keyboard navigation, ARIA attributes)
|
|
||||||
* - Body scroll locking when mobile menu is open
|
|
||||||
*
|
|
||||||
* @package ROI_Theme
|
|
||||||
* @since 1.0.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize on DOM ready
|
|
||||||
*/
|
|
||||||
function init() {
|
|
||||||
setupMobileMenu();
|
|
||||||
setupStickyHeader();
|
|
||||||
setupSmoothScroll();
|
|
||||||
setupKeyboardNavigation();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mobile Menu Functionality
|
|
||||||
*/
|
|
||||||
function setupMobileMenu() {
|
|
||||||
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
|
|
||||||
const mobileMenu = document.getElementById('mobile-menu');
|
|
||||||
const mobileMenuOverlay = document.getElementById('mobile-menu-overlay');
|
|
||||||
const mobileMenuClose = document.getElementById('mobile-menu-close');
|
|
||||||
|
|
||||||
if (!mobileMenuToggle || !mobileMenu || !mobileMenuOverlay) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open mobile menu
|
|
||||||
mobileMenuToggle.addEventListener('click', function() {
|
|
||||||
openMobileMenu();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close mobile menu via close button
|
|
||||||
if (mobileMenuClose) {
|
|
||||||
mobileMenuClose.addEventListener('click', function() {
|
|
||||||
closeMobileMenu();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close mobile menu via overlay click
|
|
||||||
mobileMenuOverlay.addEventListener('click', function() {
|
|
||||||
closeMobileMenu();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close mobile menu on Escape key
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape' && mobileMenu.classList.contains('active')) {
|
|
||||||
closeMobileMenu();
|
|
||||||
mobileMenuToggle.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close mobile menu when clicking a menu link
|
|
||||||
const mobileMenuLinks = mobileMenu.querySelectorAll('a');
|
|
||||||
mobileMenuLinks.forEach(function(link) {
|
|
||||||
link.addEventListener('click', function() {
|
|
||||||
closeMobileMenu();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle window resize - close mobile menu if switching to desktop
|
|
||||||
let resizeTimer;
|
|
||||||
window.addEventListener('resize', function() {
|
|
||||||
clearTimeout(resizeTimer);
|
|
||||||
resizeTimer = setTimeout(function() {
|
|
||||||
if (window.innerWidth >= 768 && mobileMenu.classList.contains('active')) {
|
|
||||||
closeMobileMenu();
|
|
||||||
}
|
|
||||||
}, 250);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open mobile menu
|
|
||||||
*/
|
|
||||||
function openMobileMenu() {
|
|
||||||
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
|
|
||||||
const mobileMenu = document.getElementById('mobile-menu');
|
|
||||||
const mobileMenuOverlay = document.getElementById('mobile-menu-overlay');
|
|
||||||
|
|
||||||
// Add active classes
|
|
||||||
mobileMenu.classList.add('active');
|
|
||||||
mobileMenuOverlay.classList.add('active');
|
|
||||||
document.body.classList.add('mobile-menu-open');
|
|
||||||
|
|
||||||
// Update ARIA attributes
|
|
||||||
mobileMenuToggle.setAttribute('aria-expanded', 'true');
|
|
||||||
mobileMenu.setAttribute('aria-hidden', 'false');
|
|
||||||
mobileMenuOverlay.setAttribute('aria-hidden', 'false');
|
|
||||||
|
|
||||||
// Focus trap - focus first menu item
|
|
||||||
const firstMenuItem = mobileMenu.querySelector('a');
|
|
||||||
if (firstMenuItem) {
|
|
||||||
setTimeout(function() {
|
|
||||||
firstMenuItem.focus();
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close mobile menu
|
|
||||||
*/
|
|
||||||
function closeMobileMenu() {
|
|
||||||
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
|
|
||||||
const mobileMenu = document.getElementById('mobile-menu');
|
|
||||||
const mobileMenuOverlay = document.getElementById('mobile-menu-overlay');
|
|
||||||
|
|
||||||
// Remove active classes
|
|
||||||
mobileMenu.classList.remove('active');
|
|
||||||
mobileMenuOverlay.classList.remove('active');
|
|
||||||
document.body.classList.remove('mobile-menu-open');
|
|
||||||
|
|
||||||
// Update ARIA attributes
|
|
||||||
mobileMenuToggle.setAttribute('aria-expanded', 'false');
|
|
||||||
mobileMenu.setAttribute('aria-hidden', 'true');
|
|
||||||
mobileMenuOverlay.setAttribute('aria-hidden', 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sticky Header Behavior
|
|
||||||
*/
|
|
||||||
function setupStickyHeader() {
|
|
||||||
const header = document.getElementById('masthead');
|
|
||||||
|
|
||||||
if (!header) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastScrollTop = 0;
|
|
||||||
let scrollThreshold = 100;
|
|
||||||
|
|
||||||
window.addEventListener('scroll', function() {
|
|
||||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
||||||
|
|
||||||
// Add/remove scrolled class based on scroll position
|
|
||||||
if (scrollTop > scrollThreshold) {
|
|
||||||
header.classList.add('scrolled');
|
|
||||||
} else {
|
|
||||||
header.classList.remove('scrolled');
|
|
||||||
}
|
|
||||||
|
|
||||||
lastScrollTop = scrollTop;
|
|
||||||
}, { passive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Smooth Scroll to Anchors (Optional)
|
|
||||||
*/
|
|
||||||
function setupSmoothScroll() {
|
|
||||||
// Check if user prefers reduced motion
|
|
||||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
||||||
|
|
||||||
if (prefersReducedMotion) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all anchor links
|
|
||||||
const anchorLinks = document.querySelectorAll('a[href^="#"]');
|
|
||||||
|
|
||||||
anchorLinks.forEach(function(link) {
|
|
||||||
link.addEventListener('click', function(e) {
|
|
||||||
const href = this.getAttribute('href');
|
|
||||||
|
|
||||||
// Skip if href is just "#"
|
|
||||||
if (href === '#') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = document.querySelector(href);
|
|
||||||
|
|
||||||
if (target) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// Get header height for offset
|
|
||||||
const header = document.getElementById('masthead');
|
|
||||||
const headerHeight = header ? header.offsetHeight : 0;
|
|
||||||
const targetPosition = target.getBoundingClientRect().top + window.pageYOffset - headerHeight - 20;
|
|
||||||
|
|
||||||
window.scrollTo({
|
|
||||||
top: targetPosition,
|
|
||||||
behavior: prefersReducedMotion ? 'auto' : 'smooth'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update URL hash
|
|
||||||
if (history.pushState) {
|
|
||||||
history.pushState(null, null, href);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Focus target element for accessibility
|
|
||||||
target.setAttribute('tabindex', '-1');
|
|
||||||
target.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keyboard Navigation for Menus
|
|
||||||
*/
|
|
||||||
function setupKeyboardNavigation() {
|
|
||||||
const menuItems = document.querySelectorAll('.primary-menu > li, .mobile-primary-menu > li');
|
|
||||||
|
|
||||||
menuItems.forEach(function(item) {
|
|
||||||
const link = item.querySelector('a');
|
|
||||||
const submenu = item.querySelector('.sub-menu');
|
|
||||||
|
|
||||||
if (!link || !submenu) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open submenu on Enter/Space
|
|
||||||
link.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
if (submenu) {
|
|
||||||
e.preventDefault();
|
|
||||||
toggleSubmenu(item, submenu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close submenu on Escape
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
closeSubmenu(item, submenu);
|
|
||||||
link.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close submenu when focus leaves
|
|
||||||
const submenuLinks = submenu.querySelectorAll('a');
|
|
||||||
if (submenuLinks.length > 0) {
|
|
||||||
const lastSubmenuLink = submenuLinks[submenuLinks.length - 1];
|
|
||||||
|
|
||||||
lastSubmenuLink.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Tab' && !e.shiftKey) {
|
|
||||||
closeSubmenu(item, submenu);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle submenu visibility
|
|
||||||
*/
|
|
||||||
function toggleSubmenu(item, submenu) {
|
|
||||||
const isExpanded = item.classList.contains('submenu-open');
|
|
||||||
|
|
||||||
if (isExpanded) {
|
|
||||||
closeSubmenu(item, submenu);
|
|
||||||
} else {
|
|
||||||
openSubmenu(item, submenu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open submenu
|
|
||||||
*/
|
|
||||||
function openSubmenu(item, submenu) {
|
|
||||||
item.classList.add('submenu-open');
|
|
||||||
submenu.setAttribute('aria-hidden', 'false');
|
|
||||||
|
|
||||||
const firstLink = submenu.querySelector('a');
|
|
||||||
if (firstLink) {
|
|
||||||
firstLink.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close submenu
|
|
||||||
*/
|
|
||||||
function closeSubmenu(item, submenu) {
|
|
||||||
item.classList.remove('submenu-open');
|
|
||||||
submenu.setAttribute('aria-hidden', 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trap focus within mobile menu when open
|
|
||||||
*/
|
|
||||||
function setupFocusTrap() {
|
|
||||||
const mobileMenu = document.getElementById('mobile-menu');
|
|
||||||
|
|
||||||
if (!mobileMenu) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (!mobileMenu.classList.contains('active')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === 'Tab') {
|
|
||||||
const focusableElements = mobileMenu.querySelectorAll(
|
|
||||||
'a, button, [tabindex]:not([tabindex="-1"])'
|
|
||||||
);
|
|
||||||
|
|
||||||
const firstElement = focusableElements[0];
|
|
||||||
const lastElement = focusableElements[focusableElements.length - 1];
|
|
||||||
|
|
||||||
if (e.shiftKey) {
|
|
||||||
// Shift + Tab
|
|
||||||
if (document.activeElement === firstElement) {
|
|
||||||
e.preventDefault();
|
|
||||||
lastElement.focus();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Tab
|
|
||||||
if (document.activeElement === lastElement) {
|
|
||||||
e.preventDefault();
|
|
||||||
firstElement.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize focus trap
|
|
||||||
*/
|
|
||||||
setupFocusTrap();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize when DOM is ready
|
|
||||||
*/
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
|
||||||
} else {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
})();
|
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Related Posts Functionality
|
|
||||||
*
|
|
||||||
* Provides configurable related posts functionality with Bootstrap grid support.
|
|
||||||
*
|
|
||||||
* @package ROI_Theme
|
|
||||||
* @since 1.0.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Exit if accessed directly
|
|
||||||
if (!defined('ABSPATH')) {
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get related posts based on categories
|
|
||||||
*
|
|
||||||
* @param int $post_id The post ID to get related posts for
|
|
||||||
* @return WP_Query|false Query object with related posts or false if none found
|
|
||||||
*/
|
|
||||||
function roi_get_related_posts($post_id) {
|
|
||||||
// Get post categories
|
|
||||||
$categories = wp_get_post_categories($post_id);
|
|
||||||
|
|
||||||
if (empty($categories)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get number of posts to display (default: 3)
|
|
||||||
$posts_per_page = get_option('roi_related_posts_count', 3);
|
|
||||||
|
|
||||||
// Query arguments
|
|
||||||
$args = array(
|
|
||||||
'post_type' => 'post',
|
|
||||||
'post_status' => 'publish',
|
|
||||||
'posts_per_page' => $posts_per_page,
|
|
||||||
'post__not_in' => array($post_id),
|
|
||||||
'category__in' => $categories,
|
|
||||||
'orderby' => 'rand',
|
|
||||||
'no_found_rows' => true,
|
|
||||||
'update_post_meta_cache' => false,
|
|
||||||
'update_post_term_cache' => false,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Allow filtering of query args
|
|
||||||
$args = apply_filters('roi_related_posts_args', $args, $post_id);
|
|
||||||
|
|
||||||
// Get related posts
|
|
||||||
$related_query = new WP_Query($args);
|
|
||||||
|
|
||||||
return $related_query->have_posts() ? $related_query : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display related posts section
|
|
||||||
*
|
|
||||||
* @param int|null $post_id Optional. Post ID. Default is current post.
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
function roi_display_related_posts($post_id = null) {
|
|
||||||
// Get post ID
|
|
||||||
if (!$post_id) {
|
|
||||||
$post_id = get_the_ID();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if related posts are enabled
|
|
||||||
$enabled = get_option('roi_related_posts_enabled', true);
|
|
||||||
if (!$enabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get related posts
|
|
||||||
$related_query = roi_get_related_posts($post_id);
|
|
||||||
|
|
||||||
if (!$related_query) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get configuration options
|
|
||||||
$title = get_option('roi_related_posts_title', __('Related Posts', 'roi-theme'));
|
|
||||||
$columns = get_option('roi_related_posts_columns', 3);
|
|
||||||
$show_excerpt = get_option('roi_related_posts_show_excerpt', true);
|
|
||||||
$show_date = get_option('roi_related_posts_show_date', true);
|
|
||||||
$show_category = get_option('roi_related_posts_show_category', true);
|
|
||||||
$excerpt_length = get_option('roi_related_posts_excerpt_length', 20);
|
|
||||||
$background_colors = get_option('roi_related_posts_bg_colors', array(
|
|
||||||
'#1a73e8', // Blue
|
|
||||||
'#e91e63', // Pink
|
|
||||||
'#4caf50', // Green
|
|
||||||
'#ff9800', // Orange
|
|
||||||
'#9c27b0', // Purple
|
|
||||||
'#00bcd4', // Cyan
|
|
||||||
));
|
|
||||||
|
|
||||||
// Calculate Bootstrap column class
|
|
||||||
$col_class = roi_get_column_class($columns);
|
|
||||||
|
|
||||||
// Start output
|
|
||||||
?>
|
|
||||||
<section class="related-posts-section">
|
|
||||||
<div class="related-posts-container">
|
|
||||||
|
|
||||||
<?php if ($title) : ?>
|
|
||||||
<h2 class="related-posts-title"><?php echo esc_html($title); ?></h2>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="row g-4">
|
|
||||||
<?php
|
|
||||||
$color_index = 0;
|
|
||||||
while ($related_query->have_posts()) :
|
|
||||||
$related_query->the_post();
|
|
||||||
$has_thumbnail = has_post_thumbnail();
|
|
||||||
|
|
||||||
// Get background color for posts without image
|
|
||||||
$bg_color = $background_colors[$color_index % count($background_colors)];
|
|
||||||
$color_index++;
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="<?php echo esc_attr($col_class); ?>">
|
|
||||||
<article class="related-post-card <?php echo $has_thumbnail ? 'has-thumbnail' : 'no-thumbnail'; ?>">
|
|
||||||
|
|
||||||
<a href="<?php the_permalink(); ?>" class="related-post-link">
|
|
||||||
|
|
||||||
<?php if ($has_thumbnail) : ?>
|
|
||||||
<!-- Card with Image -->
|
|
||||||
<div class="related-post-thumbnail">
|
|
||||||
<?php
|
|
||||||
the_post_thumbnail('roi-thumbnail', array(
|
|
||||||
'alt' => the_title_attribute(array('echo' => false)),
|
|
||||||
'loading' => 'lazy',
|
|
||||||
));
|
|
||||||
?>
|
|
||||||
|
|
||||||
<?php if ($show_category) : ?>
|
|
||||||
<?php
|
|
||||||
$categories = get_the_category();
|
|
||||||
if (!empty($categories)) :
|
|
||||||
$category = $categories[0];
|
|
||||||
?>
|
|
||||||
<span class="related-post-category">
|
|
||||||
<?php echo esc_html($category->name); ?>
|
|
||||||
</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<?php else : ?>
|
|
||||||
<!-- Card without Image - Color Background -->
|
|
||||||
<div class="related-post-no-image" style="background-color: <?php echo esc_attr($bg_color); ?>;">
|
|
||||||
<div class="related-post-no-image-content">
|
|
||||||
<h3 class="related-post-no-image-title">
|
|
||||||
<?php the_title(); ?>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<?php if ($show_category) : ?>
|
|
||||||
<?php
|
|
||||||
$categories = get_the_category();
|
|
||||||
if (!empty($categories)) :
|
|
||||||
$category = $categories[0];
|
|
||||||
?>
|
|
||||||
<span class="related-post-category no-image">
|
|
||||||
<?php echo esc_html($category->name); ?>
|
|
||||||
</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="related-post-content">
|
|
||||||
|
|
||||||
<?php if ($has_thumbnail) : ?>
|
|
||||||
<h3 class="related-post-title">
|
|
||||||
<?php the_title(); ?>
|
|
||||||
</h3>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($show_excerpt && $excerpt_length > 0) : ?>
|
|
||||||
<div class="related-post-excerpt">
|
|
||||||
<?php echo wp_trim_words(get_the_excerpt(), $excerpt_length, '...'); ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($show_date) : ?>
|
|
||||||
<div class="related-post-meta">
|
|
||||||
<time class="related-post-date" datetime="<?php echo esc_attr(get_the_date('c')); ?>">
|
|
||||||
<?php echo esc_html(get_the_date()); ?>
|
|
||||||
</time>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</a>
|
|
||||||
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php endwhile; ?>
|
|
||||||
</div><!-- .row -->
|
|
||||||
|
|
||||||
</div><!-- .related-posts-container -->
|
|
||||||
</section><!-- .related-posts-section -->
|
|
||||||
|
|
||||||
<?php
|
|
||||||
// Reset post data
|
|
||||||
wp_reset_postdata();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Bootstrap column class based on number of columns
|
|
||||||
*
|
|
||||||
* @param int $columns Number of columns (1-4)
|
|
||||||
* @return string Bootstrap column classes
|
|
||||||
*/
|
|
||||||
function roi_get_column_class($columns) {
|
|
||||||
$columns = absint($columns);
|
|
||||||
|
|
||||||
switch ($columns) {
|
|
||||||
case 1:
|
|
||||||
return 'col-12';
|
|
||||||
case 2:
|
|
||||||
return 'col-12 col-md-6';
|
|
||||||
case 3:
|
|
||||||
return 'col-12 col-sm-6 col-lg-4';
|
|
||||||
case 4:
|
|
||||||
return 'col-12 col-sm-6 col-lg-3';
|
|
||||||
default:
|
|
||||||
return 'col-12 col-sm-6 col-lg-4'; // Default to 3 columns
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook related posts display after post content
|
|
||||||
*/
|
|
||||||
function roi_hook_related_posts() {
|
|
||||||
if (is_single() && !is_attachment()) {
|
|
||||||
roi_display_related_posts();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add_action('roi_after_post_content', 'roi_hook_related_posts');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enqueue related posts styles
|
|
||||||
*/
|
|
||||||
function roi_enqueue_related_posts_styles() {
|
|
||||||
if (is_single() && !is_attachment()) {
|
|
||||||
$enabled = get_option('roi_related_posts_enabled', true);
|
|
||||||
|
|
||||||
if ($enabled) {
|
|
||||||
wp_enqueue_style(
|
|
||||||
'roirelated-posts',
|
|
||||||
get_template_directory_uri() . '/Assets/Css/related-posts.css',
|
|
||||||
array('roibootstrap'),
|
|
||||||
ROI_VERSION,
|
|
||||||
'all'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add_action('wp_enqueue_scripts', 'roi_enqueue_related_posts_styles');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register related posts settings
|
|
||||||
* These can be configured via theme options or customizer
|
|
||||||
*/
|
|
||||||
function roi_related_posts_default_options() {
|
|
||||||
// Set default options if they don't exist
|
|
||||||
$defaults = array(
|
|
||||||
'roi_related_posts_enabled' => true,
|
|
||||||
'roi_related_posts_title' => __('Related Posts', 'roi-theme'),
|
|
||||||
'roi_related_posts_count' => 3,
|
|
||||||
'roi_related_posts_columns' => 3,
|
|
||||||
'roi_related_posts_show_excerpt' => true,
|
|
||||||
'roi_related_posts_excerpt_length' => 20,
|
|
||||||
'roi_related_posts_show_date' => true,
|
|
||||||
'roi_related_posts_show_category' => true,
|
|
||||||
'roi_related_posts_bg_colors' => array(
|
|
||||||
'#1a73e8', // Blue
|
|
||||||
'#e91e63', // Pink
|
|
||||||
'#4caf50', // Green
|
|
||||||
'#ff9800', // Orange
|
|
||||||
'#9c27b0', // Purple
|
|
||||||
'#00bcd4', // Cyan
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ($defaults as $option => $value) {
|
|
||||||
if (get_option($option) === false) {
|
|
||||||
add_option($option, $value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add_action('after_setup_theme', 'roi_related_posts_default_options');
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Busca casos variados de problemas de listas para validación exhaustiva
|
|
||||||
*/
|
|
||||||
|
|
||||||
$conn = new mysqli("localhost", "preciosunitarios_seo", "ACl%EEFd=V-Yvb??", "preciosunitarios_seo");
|
|
||||||
$conn->set_charset("utf8mb4");
|
|
||||||
|
|
||||||
function detectIssues($html) {
|
|
||||||
$issues = [];
|
|
||||||
libxml_use_internal_errors(true);
|
|
||||||
$doc = new DOMDocument("1.0", "UTF-8");
|
|
||||||
$wrapped = '<div id="wrapper">' . $html . '</div>';
|
|
||||||
$doc->loadHTML('<?xml encoding="UTF-8">' . $wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
|
||||||
libxml_clear_errors();
|
|
||||||
|
|
||||||
$validChildren = ["li", "script", "template"];
|
|
||||||
foreach (["ul", "ol"] as $tag) {
|
|
||||||
foreach ($doc->getElementsByTagName($tag) as $list) {
|
|
||||||
foreach ($list->childNodes as $child) {
|
|
||||||
if ($child->nodeType === XML_ELEMENT_NODE) {
|
|
||||||
$childTag = strtolower($child->nodeName);
|
|
||||||
if (!in_array($childTag, $validChildren)) {
|
|
||||||
$issues[] = ["parent" => $tag, "child" => $childTag];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $issues;
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "BUSCANDO CASOS VARIADOS...\n\n";
|
|
||||||
|
|
||||||
$query = "SELECT id, page, html FROM datos_seo_pagina WHERE html IS NOT NULL AND html != '' ORDER BY id";
|
|
||||||
$result = $conn->query($query);
|
|
||||||
|
|
||||||
if (!$result) {
|
|
||||||
die("Error en query: " . $conn->error);
|
|
||||||
}
|
|
||||||
|
|
||||||
$cases = [
|
|
||||||
"many_issues" => [],
|
|
||||||
"ol_issues" => [],
|
|
||||||
"mixed_issues" => [],
|
|
||||||
"few_issues" => []
|
|
||||||
];
|
|
||||||
|
|
||||||
while ($row = $result->fetch_assoc()) {
|
|
||||||
$issues = detectIssues($row["html"]);
|
|
||||||
if (empty($issues)) continue;
|
|
||||||
|
|
||||||
$count = count($issues);
|
|
||||||
$hasOl = false;
|
|
||||||
$hasUl = false;
|
|
||||||
|
|
||||||
foreach ($issues as $issue) {
|
|
||||||
if ($issue["parent"] === "ol") $hasOl = true;
|
|
||||||
if ($issue["parent"] === "ul") $hasUl = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($count > 10 && count($cases["many_issues"]) < 3) {
|
|
||||||
$cases["many_issues"][] = ["id" => $row["id"], "url" => $row["page"], "count" => $count, "issues" => $issues];
|
|
||||||
}
|
|
||||||
if ($hasOl && !$hasUl && count($cases["ol_issues"]) < 3) {
|
|
||||||
$cases["ol_issues"][] = ["id" => $row["id"], "url" => $row["page"], "count" => $count, "issues" => $issues];
|
|
||||||
}
|
|
||||||
if ($hasOl && $hasUl && count($cases["mixed_issues"]) < 3) {
|
|
||||||
$cases["mixed_issues"][] = ["id" => $row["id"], "url" => $row["page"], "count" => $count, "issues" => $issues];
|
|
||||||
}
|
|
||||||
if ($count <= 2 && count($cases["few_issues"]) < 3) {
|
|
||||||
$cases["few_issues"][] = ["id" => $row["id"], "url" => $row["page"], "count" => $count, "issues" => $issues];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($cases as $type => $posts) {
|
|
||||||
echo "=== " . strtoupper($type) . " ===\n";
|
|
||||||
if (empty($posts)) {
|
|
||||||
echo " (ninguno encontrado)\n\n";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
foreach ($posts as $post) {
|
|
||||||
echo "ID: {$post["id"]} - {$post["count"]} problemas\n";
|
|
||||||
echo "URL: {$post["url"]}\n";
|
|
||||||
echo "Tipos: ";
|
|
||||||
$types = [];
|
|
||||||
foreach ($post["issues"] as $i) {
|
|
||||||
$types[] = "<{$i["parent"]}> contiene <{$i["child"]}>";
|
|
||||||
}
|
|
||||||
echo implode(", ", array_unique($types)) . "\n\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$conn->close();
|
|
||||||
@@ -1,411 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Corrector de Listas HTML Mal Formadas usando DOMDocument
|
|
||||||
*
|
|
||||||
* PROPÓSITO: Detectar y corregir listas con estructura inválida
|
|
||||||
* - <ul>/<ol> conteniendo elementos no-<li> como hijos directos
|
|
||||||
* - Listas anidadas que son hermanas en lugar de hijas de <li>
|
|
||||||
*
|
|
||||||
* USO:
|
|
||||||
* php fix-malformed-lists-dom.php --mode=scan # Solo escanear
|
|
||||||
* php fix-malformed-lists-dom.php --mode=test # Probar corrección (1 post)
|
|
||||||
* php fix-malformed-lists-dom.php --mode=fix # Aplicar correcciones
|
|
||||||
*
|
|
||||||
* @package ROI_Theme
|
|
||||||
* @since Phase 4.4 Accessibility
|
|
||||||
*/
|
|
||||||
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
ini_set('display_errors', 1);
|
|
||||||
ini_set('memory_limit', '512M');
|
|
||||||
set_time_limit(600);
|
|
||||||
|
|
||||||
// Configuración
|
|
||||||
$db_config = [
|
|
||||||
'host' => 'localhost',
|
|
||||||
'database' => 'preciosunitarios_seo',
|
|
||||||
'username' => 'preciosunitarios_seo',
|
|
||||||
'password' => 'ACl%EEFd=V-Yvb??',
|
|
||||||
'charset' => 'utf8mb4'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Parsear argumentos
|
|
||||||
$mode = 'scan';
|
|
||||||
foreach ($argv as $arg) {
|
|
||||||
if (strpos($arg, '--mode=') === 0) {
|
|
||||||
$mode = substr($arg, 7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "==============================================\n";
|
|
||||||
echo " CORRECTOR DE LISTAS - DOMDocument\n";
|
|
||||||
echo " Modo: $mode\n";
|
|
||||||
echo " Fecha: " . date('Y-m-d H:i:s') . "\n";
|
|
||||||
echo "==============================================\n\n";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Conectar a la base de datos
|
|
||||||
*/
|
|
||||||
function connectDatabase(array $config): ?mysqli {
|
|
||||||
$conn = new mysqli(
|
|
||||||
$config['host'],
|
|
||||||
$config['username'],
|
|
||||||
$config['password'],
|
|
||||||
$config['database']
|
|
||||||
);
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
echo "Error de conexión: " . $conn->connect_error . "\n";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$conn->set_charset($config['charset']);
|
|
||||||
return $conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Corregir listas mal formadas usando DOMDocument
|
|
||||||
*/
|
|
||||||
function fixMalformedLists(string $html): array {
|
|
||||||
$result = [
|
|
||||||
'fixed' => false,
|
|
||||||
'html' => $html,
|
|
||||||
'changes' => 0,
|
|
||||||
'details' => []
|
|
||||||
];
|
|
||||||
|
|
||||||
// Suprimir errores de HTML mal formado
|
|
||||||
libxml_use_internal_errors(true);
|
|
||||||
|
|
||||||
$doc = new DOMDocument('1.0', 'UTF-8');
|
|
||||||
|
|
||||||
// Envolver en contenedor para preservar estructura
|
|
||||||
$wrapped = '<div id="temp-wrapper">' . $html . '</div>';
|
|
||||||
$doc->loadHTML('<?xml encoding="UTF-8">' . $wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
|
||||||
|
|
||||||
libxml_clear_errors();
|
|
||||||
|
|
||||||
// Procesar todas las listas (ul y ol)
|
|
||||||
$lists = [];
|
|
||||||
foreach ($doc->getElementsByTagName('ul') as $ul) {
|
|
||||||
$lists[] = $ul;
|
|
||||||
}
|
|
||||||
foreach ($doc->getElementsByTagName('ol') as $ol) {
|
|
||||||
$lists[] = $ol;
|
|
||||||
}
|
|
||||||
|
|
||||||
$changes = 0;
|
|
||||||
|
|
||||||
foreach ($lists as $list) {
|
|
||||||
$changes += fixListChildren($list, $result['details']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($changes > 0) {
|
|
||||||
// Extraer HTML corregido
|
|
||||||
$wrapper = $doc->getElementById('temp-wrapper');
|
|
||||||
if ($wrapper) {
|
|
||||||
$innerHTML = '';
|
|
||||||
foreach ($wrapper->childNodes as $child) {
|
|
||||||
$innerHTML .= $doc->saveHTML($child);
|
|
||||||
}
|
|
||||||
$result['html'] = $innerHTML;
|
|
||||||
$result['fixed'] = true;
|
|
||||||
$result['changes'] = $changes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Corregir hijos de una lista (solo debe contener li, script, template)
|
|
||||||
*/
|
|
||||||
function fixListChildren(DOMElement $list, array &$details): int {
|
|
||||||
$changes = 0;
|
|
||||||
$validChildren = ['li', 'script', 'template'];
|
|
||||||
$nodesToProcess = [];
|
|
||||||
|
|
||||||
// Recopilar nodos que necesitan corrección
|
|
||||||
foreach ($list->childNodes as $child) {
|
|
||||||
if ($child->nodeType === XML_ELEMENT_NODE) {
|
|
||||||
$tagName = strtolower($child->nodeName);
|
|
||||||
if (!in_array($tagName, $validChildren)) {
|
|
||||||
$nodesToProcess[] = $child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Procesar cada nodo inválido
|
|
||||||
foreach ($nodesToProcess as $node) {
|
|
||||||
$tagName = strtolower($node->nodeName);
|
|
||||||
|
|
||||||
// Si es una lista anidada (ul/ol), envolverla en <li>
|
|
||||||
if ($tagName === 'ul' || $tagName === 'ol') {
|
|
||||||
$changes += wrapInLi($list, $node, $details);
|
|
||||||
}
|
|
||||||
// Otros elementos inválidos también se envuelven en <li>
|
|
||||||
else {
|
|
||||||
$changes += wrapInLi($list, $node, $details);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $changes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Envolver un nodo en <li> o moverlo al <li> anterior
|
|
||||||
*/
|
|
||||||
function wrapInLi(DOMElement $list, DOMNode $node, array &$details): int {
|
|
||||||
$doc = $list->ownerDocument;
|
|
||||||
$tagName = strtolower($node->nodeName);
|
|
||||||
|
|
||||||
// Buscar el <li> hermano anterior
|
|
||||||
$prevLi = null;
|
|
||||||
$prev = $node->previousSibling;
|
|
||||||
while ($prev) {
|
|
||||||
if ($prev->nodeType === XML_ELEMENT_NODE && strtolower($prev->nodeName) === 'li') {
|
|
||||||
$prevLi = $prev;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$prev = $prev->previousSibling;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($prevLi) {
|
|
||||||
// Mover el nodo al final del <li> anterior
|
|
||||||
$prevLi->appendChild($node);
|
|
||||||
$details[] = "Movido <$tagName> dentro del <li> anterior";
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
// No hay <li> anterior, crear uno nuevo
|
|
||||||
$newLi = $doc->createElement('li');
|
|
||||||
$list->insertBefore($newLi, $node);
|
|
||||||
$newLi->appendChild($node);
|
|
||||||
$details[] = "Envuelto <$tagName> en nuevo <li>";
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detectar problemas en HTML sin corregir
|
|
||||||
*/
|
|
||||||
function detectIssues(string $html): array {
|
|
||||||
$issues = [];
|
|
||||||
|
|
||||||
libxml_use_internal_errors(true);
|
|
||||||
$doc = new DOMDocument('1.0', 'UTF-8');
|
|
||||||
$wrapped = '<div id="temp-wrapper">' . $html . '</div>';
|
|
||||||
$doc->loadHTML('<?xml encoding="UTF-8">' . $wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
|
||||||
libxml_clear_errors();
|
|
||||||
|
|
||||||
$validChildren = ['li', 'script', 'template'];
|
|
||||||
|
|
||||||
// Revisar ul
|
|
||||||
foreach ($doc->getElementsByTagName('ul') as $ul) {
|
|
||||||
foreach ($ul->childNodes as $child) {
|
|
||||||
if ($child->nodeType === XML_ELEMENT_NODE) {
|
|
||||||
$tagName = strtolower($child->nodeName);
|
|
||||||
if (!in_array($tagName, $validChildren)) {
|
|
||||||
$issues[] = [
|
|
||||||
'list_type' => 'ul',
|
|
||||||
'invalid_child' => $tagName,
|
|
||||||
'context' => getNodeContext($child)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Revisar ol
|
|
||||||
foreach ($doc->getElementsByTagName('ol') as $ol) {
|
|
||||||
foreach ($ol->childNodes as $child) {
|
|
||||||
if ($child->nodeType === XML_ELEMENT_NODE) {
|
|
||||||
$tagName = strtolower($child->nodeName);
|
|
||||||
if (!in_array($tagName, $validChildren)) {
|
|
||||||
$issues[] = [
|
|
||||||
'list_type' => 'ol',
|
|
||||||
'invalid_child' => $tagName,
|
|
||||||
'context' => getNodeContext($child)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $issues;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtener contexto de un nodo para debug
|
|
||||||
*/
|
|
||||||
function getNodeContext(DOMNode $node): string {
|
|
||||||
$doc = $node->ownerDocument;
|
|
||||||
$html = $doc->saveHTML($node);
|
|
||||||
return substr($html, 0, 100) . (strlen($html) > 100 ? '...' : '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// EJECUCIÓN PRINCIPAL
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
$conn = connectDatabase($db_config);
|
|
||||||
if (!$conn) {
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "✓ Conexión establecida\n\n";
|
|
||||||
|
|
||||||
// Contar registros
|
|
||||||
$result = $conn->query("SELECT COUNT(*) as total FROM datos_seo_pagina WHERE html IS NOT NULL AND html != ''");
|
|
||||||
$total = $result->fetch_assoc()['total'];
|
|
||||||
echo "Total de registros: $total\n\n";
|
|
||||||
|
|
||||||
if ($mode === 'scan') {
|
|
||||||
// MODO SCAN: Solo detectar problemas
|
|
||||||
echo "MODO: ESCANEO (solo detección)\n";
|
|
||||||
echo "─────────────────────────────────\n\n";
|
|
||||||
|
|
||||||
$batch_size = 100;
|
|
||||||
$offset = 0;
|
|
||||||
$affected = 0;
|
|
||||||
$total_issues = 0;
|
|
||||||
|
|
||||||
while ($offset < $total) {
|
|
||||||
$query = "SELECT id, page, html FROM datos_seo_pagina
|
|
||||||
WHERE html IS NOT NULL AND html != ''
|
|
||||||
ORDER BY id LIMIT $batch_size OFFSET $offset";
|
|
||||||
$result = $conn->query($query);
|
|
||||||
|
|
||||||
while ($row = $result->fetch_assoc()) {
|
|
||||||
$issues = detectIssues($row['html']);
|
|
||||||
if (!empty($issues)) {
|
|
||||||
$affected++;
|
|
||||||
$total_issues += count($issues);
|
|
||||||
|
|
||||||
if ($affected <= 20) {
|
|
||||||
echo "[ID: {$row['id']}] " . count($issues) . " problema(s)\n";
|
|
||||||
echo "URL: {$row['page']}\n";
|
|
||||||
foreach (array_slice($issues, 0, 2) as $issue) {
|
|
||||||
echo " - <{$issue['list_type']}> contiene <{$issue['invalid_child']}>\n";
|
|
||||||
}
|
|
||||||
echo "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$offset += $batch_size;
|
|
||||||
|
|
||||||
if ($offset % 1000 == 0) {
|
|
||||||
echo "Procesados: $offset/$total...\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "─────────────────────────────────\n";
|
|
||||||
echo "RESUMEN:\n";
|
|
||||||
echo " Posts afectados: $affected\n";
|
|
||||||
echo " Total incidencias: $total_issues\n";
|
|
||||||
|
|
||||||
} elseif ($mode === 'test') {
|
|
||||||
// MODO TEST: Probar corrección en 1 post
|
|
||||||
echo "MODO: PRUEBA (sin guardar)\n";
|
|
||||||
echo "─────────────────────────────────\n\n";
|
|
||||||
|
|
||||||
// Buscar primer post con problemas
|
|
||||||
$query = "SELECT id, page, html FROM datos_seo_pagina
|
|
||||||
WHERE html IS NOT NULL AND html != ''
|
|
||||||
ORDER BY id LIMIT 100";
|
|
||||||
$result = $conn->query($query);
|
|
||||||
|
|
||||||
while ($row = $result->fetch_assoc()) {
|
|
||||||
$issues = detectIssues($row['html']);
|
|
||||||
if (!empty($issues)) {
|
|
||||||
echo "POST ID: {$row['id']}\n";
|
|
||||||
echo "URL: {$row['page']}\n";
|
|
||||||
echo "Problemas detectados: " . count($issues) . "\n\n";
|
|
||||||
|
|
||||||
echo "ANTES (problemas):\n";
|
|
||||||
foreach (array_slice($issues, 0, 3) as $issue) {
|
|
||||||
echo " - <{$issue['list_type']}> contiene <{$issue['invalid_child']}>\n";
|
|
||||||
echo " Contexto: " . htmlspecialchars(substr($issue['context'], 0, 80)) . "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aplicar corrección
|
|
||||||
$fixResult = fixMalformedLists($row['html']);
|
|
||||||
|
|
||||||
echo "\nDESPUÉS (corrección):\n";
|
|
||||||
echo " Cambios realizados: {$fixResult['changes']}\n";
|
|
||||||
foreach ($fixResult['details'] as $detail) {
|
|
||||||
echo " - $detail\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar que no quedan problemas
|
|
||||||
$issuesAfter = detectIssues($fixResult['html']);
|
|
||||||
echo "\nVERIFICACIÓN:\n";
|
|
||||||
echo " Problemas antes: " . count($issues) . "\n";
|
|
||||||
echo " Problemas después: " . count($issuesAfter) . "\n";
|
|
||||||
|
|
||||||
if (count($issuesAfter) < count($issues)) {
|
|
||||||
echo " ✓ Reducción de problemas\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mostrar fragmento del HTML corregido
|
|
||||||
if ($fixResult['fixed']) {
|
|
||||||
echo "\nMUESTRA HTML CORREGIDO (primeros 500 chars):\n";
|
|
||||||
echo "─────────────────────────────────\n";
|
|
||||||
echo htmlspecialchars(substr($fixResult['html'], 0, 500)) . "...\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} elseif ($mode === 'fix') {
|
|
||||||
// MODO FIX: Aplicar correcciones
|
|
||||||
echo "MODO: CORRECCIÓN (GUARDANDO CAMBIOS)\n";
|
|
||||||
echo "─────────────────────────────────\n\n";
|
|
||||||
|
|
||||||
$batch_size = 50;
|
|
||||||
$offset = 0;
|
|
||||||
$fixed_count = 0;
|
|
||||||
$error_count = 0;
|
|
||||||
|
|
||||||
while ($offset < $total) {
|
|
||||||
$query = "SELECT id, page, html FROM datos_seo_pagina
|
|
||||||
WHERE html IS NOT NULL AND html != ''
|
|
||||||
ORDER BY id LIMIT $batch_size OFFSET $offset";
|
|
||||||
$result = $conn->query($query);
|
|
||||||
|
|
||||||
while ($row = $result->fetch_assoc()) {
|
|
||||||
$issues = detectIssues($row['html']);
|
|
||||||
|
|
||||||
if (!empty($issues)) {
|
|
||||||
$fixResult = fixMalformedLists($row['html']);
|
|
||||||
|
|
||||||
if ($fixResult['fixed']) {
|
|
||||||
// Guardar HTML corregido
|
|
||||||
$stmt = $conn->prepare("UPDATE datos_seo_pagina SET html = ? WHERE id = ?");
|
|
||||||
$stmt->bind_param("si", $fixResult['html'], $row['id']);
|
|
||||||
|
|
||||||
if ($stmt->execute()) {
|
|
||||||
$fixed_count++;
|
|
||||||
echo "[ID: {$row['id']}] ✓ Corregido ({$fixResult['changes']} cambios)\n";
|
|
||||||
} else {
|
|
||||||
$error_count++;
|
|
||||||
echo "[ID: {$row['id']}] ✗ Error al guardar\n";
|
|
||||||
}
|
|
||||||
$stmt->close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$offset += $batch_size;
|
|
||||||
|
|
||||||
if ($offset % 500 == 0) {
|
|
||||||
echo "Procesados: $offset/$total (corregidos: $fixed_count)\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\n─────────────────────────────────\n";
|
|
||||||
echo "RESUMEN:\n";
|
|
||||||
echo " Posts corregidos: $fixed_count\n";
|
|
||||||
echo " Errores: $error_count\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$conn->close();
|
|
||||||
echo "\n✓ Proceso completado.\n";
|
|
||||||
@@ -1,322 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Corrector de Listas HTML Mal Formadas - WordPress Posts
|
|
||||||
*
|
|
||||||
* BASE DE DATOS: preciosunitarios_wp
|
|
||||||
* TABLA: wp_posts
|
|
||||||
* CAMPO: post_content
|
|
||||||
*
|
|
||||||
* USO:
|
|
||||||
* php fix-malformed-lists-wp-posts.php --mode=scan
|
|
||||||
* php fix-malformed-lists-wp-posts.php --mode=test
|
|
||||||
* php fix-malformed-lists-wp-posts.php --mode=fix
|
|
||||||
*
|
|
||||||
* @package ROI_Theme
|
|
||||||
*/
|
|
||||||
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
ini_set('display_errors', 1);
|
|
||||||
ini_set('memory_limit', '512M');
|
|
||||||
set_time_limit(600);
|
|
||||||
|
|
||||||
$db_config = [
|
|
||||||
'host' => 'localhost',
|
|
||||||
'database' => 'preciosunitarios_wp',
|
|
||||||
'username' => 'preciosunitarios_wp',
|
|
||||||
'password' => 'Kq#Gk%yEt+PWpVe&HZ',
|
|
||||||
'charset' => 'utf8mb4'
|
|
||||||
];
|
|
||||||
|
|
||||||
$mode = 'scan';
|
|
||||||
foreach ($argv as $arg) {
|
|
||||||
if (strpos($arg, '--mode=') === 0) {
|
|
||||||
$mode = substr($arg, 7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "==============================================\n";
|
|
||||||
echo " CORRECTOR DE LISTAS - WordPress Posts\n";
|
|
||||||
echo " Base de datos: {$db_config['database']}\n";
|
|
||||||
echo " Tabla: wp_posts (post_content)\n";
|
|
||||||
echo " Modo: $mode\n";
|
|
||||||
echo " Fecha: " . date('Y-m-d H:i:s') . "\n";
|
|
||||||
echo "==============================================\n\n";
|
|
||||||
|
|
||||||
function connectDatabase(array $config): ?mysqli {
|
|
||||||
$conn = new mysqli($config['host'], $config['username'], $config['password'], $config['database']);
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
echo "Error de conexión: " . $conn->connect_error . "\n";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$conn->set_charset($config['charset']);
|
|
||||||
return $conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectIssues(string $html): array {
|
|
||||||
$issues = [];
|
|
||||||
if (empty(trim($html))) return $issues;
|
|
||||||
|
|
||||||
libxml_use_internal_errors(true);
|
|
||||||
$doc = new DOMDocument('1.0', 'UTF-8');
|
|
||||||
$wrapped = '<div id="temp-wrapper">' . $html . '</div>';
|
|
||||||
$doc->loadHTML('<?xml encoding="UTF-8">' . $wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
|
||||||
libxml_clear_errors();
|
|
||||||
|
|
||||||
$validChildren = ['li', 'script', 'template'];
|
|
||||||
|
|
||||||
foreach (['ul', 'ol'] as $listTag) {
|
|
||||||
foreach ($doc->getElementsByTagName($listTag) as $list) {
|
|
||||||
foreach ($list->childNodes as $child) {
|
|
||||||
if ($child->nodeType === XML_ELEMENT_NODE) {
|
|
||||||
$tagName = strtolower($child->nodeName);
|
|
||||||
if (!in_array($tagName, $validChildren)) {
|
|
||||||
$issues[] = [
|
|
||||||
'list_type' => $listTag,
|
|
||||||
'invalid_child' => $tagName
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $issues;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fixMalformedLists(string $html): array {
|
|
||||||
$result = ['fixed' => false, 'html' => $html, 'changes' => 0, 'details' => []];
|
|
||||||
|
|
||||||
if (empty(trim($html))) return $result;
|
|
||||||
|
|
||||||
libxml_use_internal_errors(true);
|
|
||||||
$doc = new DOMDocument('1.0', 'UTF-8');
|
|
||||||
$wrapped = '<div id="temp-wrapper">' . $html . '</div>';
|
|
||||||
$doc->loadHTML('<?xml encoding="UTF-8">' . $wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
|
||||||
libxml_clear_errors();
|
|
||||||
|
|
||||||
$lists = [];
|
|
||||||
foreach ($doc->getElementsByTagName('ul') as $ul) { $lists[] = $ul; }
|
|
||||||
foreach ($doc->getElementsByTagName('ol') as $ol) { $lists[] = $ol; }
|
|
||||||
|
|
||||||
$changes = 0;
|
|
||||||
$validChildren = ['li', 'script', 'template'];
|
|
||||||
|
|
||||||
foreach ($lists as $list) {
|
|
||||||
$nodesToProcess = [];
|
|
||||||
foreach ($list->childNodes as $child) {
|
|
||||||
if ($child->nodeType === XML_ELEMENT_NODE) {
|
|
||||||
$tagName = strtolower($child->nodeName);
|
|
||||||
if (!in_array($tagName, $validChildren)) {
|
|
||||||
$nodesToProcess[] = $child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($nodesToProcess as $node) {
|
|
||||||
$tagName = strtolower($node->nodeName);
|
|
||||||
$prevLi = null;
|
|
||||||
$prev = $node->previousSibling;
|
|
||||||
|
|
||||||
while ($prev) {
|
|
||||||
if ($prev->nodeType === XML_ELEMENT_NODE && strtolower($prev->nodeName) === 'li') {
|
|
||||||
$prevLi = $prev;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$prev = $prev->previousSibling;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($prevLi) {
|
|
||||||
$prevLi->appendChild($node);
|
|
||||||
$result['details'][] = "Movido <$tagName> dentro del <li> anterior";
|
|
||||||
$changes++;
|
|
||||||
} else {
|
|
||||||
$newLi = $doc->createElement('li');
|
|
||||||
$list->insertBefore($newLi, $node);
|
|
||||||
$newLi->appendChild($node);
|
|
||||||
$result['details'][] = "Envuelto <$tagName> en nuevo <li>";
|
|
||||||
$changes++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($changes > 0) {
|
|
||||||
$wrapper = $doc->getElementById('temp-wrapper');
|
|
||||||
if ($wrapper) {
|
|
||||||
$innerHTML = '';
|
|
||||||
foreach ($wrapper->childNodes as $child) {
|
|
||||||
$innerHTML .= $doc->saveHTML($child);
|
|
||||||
}
|
|
||||||
$result['html'] = $innerHTML;
|
|
||||||
$result['fixed'] = true;
|
|
||||||
$result['changes'] = $changes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// EJECUCIÓN PRINCIPAL
|
|
||||||
$conn = connectDatabase($db_config);
|
|
||||||
if (!$conn) {
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "✓ Conexión establecida\n\n";
|
|
||||||
|
|
||||||
// Solo posts publicados con contenido
|
|
||||||
$countQuery = "SELECT COUNT(*) as total FROM wp_posts
|
|
||||||
WHERE post_status = 'publish'
|
|
||||||
AND post_type IN ('post', 'page')
|
|
||||||
AND post_content IS NOT NULL
|
|
||||||
AND post_content != ''";
|
|
||||||
$result = $conn->query($countQuery);
|
|
||||||
$total = $result->fetch_assoc()['total'];
|
|
||||||
echo "Total de posts/páginas publicados: $total\n\n";
|
|
||||||
|
|
||||||
if ($mode === 'scan') {
|
|
||||||
echo "MODO: ESCANEO (solo detección)\n";
|
|
||||||
echo "─────────────────────────────────\n\n";
|
|
||||||
|
|
||||||
$batch_size = 100;
|
|
||||||
$offset = 0;
|
|
||||||
$affected = 0;
|
|
||||||
$total_issues = 0;
|
|
||||||
|
|
||||||
while ($offset < $total) {
|
|
||||||
$query = "SELECT ID, post_title, post_content, guid FROM wp_posts
|
|
||||||
WHERE post_status = 'publish'
|
|
||||||
AND post_type IN ('post', 'page')
|
|
||||||
AND post_content IS NOT NULL
|
|
||||||
AND post_content != ''
|
|
||||||
ORDER BY ID LIMIT $batch_size OFFSET $offset";
|
|
||||||
$result = $conn->query($query);
|
|
||||||
|
|
||||||
while ($row = $result->fetch_assoc()) {
|
|
||||||
$issues = detectIssues($row['post_content']);
|
|
||||||
if (!empty($issues)) {
|
|
||||||
$affected++;
|
|
||||||
$total_issues += count($issues);
|
|
||||||
|
|
||||||
if ($affected <= 20) {
|
|
||||||
echo "[ID: {$row['ID']}] " . count($issues) . " problema(s)\n";
|
|
||||||
echo "Título: " . substr($row['post_title'], 0, 60) . "\n";
|
|
||||||
foreach (array_slice($issues, 0, 2) as $issue) {
|
|
||||||
echo " - <{$issue['list_type']}> contiene <{$issue['invalid_child']}>\n";
|
|
||||||
}
|
|
||||||
echo "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$offset += $batch_size;
|
|
||||||
|
|
||||||
if ($offset % 1000 == 0) {
|
|
||||||
echo "Procesados: $offset/$total...\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "─────────────────────────────────\n";
|
|
||||||
echo "RESUMEN:\n";
|
|
||||||
echo " Posts afectados: $affected\n";
|
|
||||||
echo " Total incidencias: $total_issues\n";
|
|
||||||
|
|
||||||
} elseif ($mode === 'test') {
|
|
||||||
echo "MODO: PRUEBA (sin guardar)\n";
|
|
||||||
echo "─────────────────────────────────\n\n";
|
|
||||||
|
|
||||||
$query = "SELECT ID, post_title, post_content FROM wp_posts
|
|
||||||
WHERE post_status = 'publish'
|
|
||||||
AND post_type IN ('post', 'page')
|
|
||||||
AND post_content IS NOT NULL
|
|
||||||
AND post_content != ''
|
|
||||||
ORDER BY ID LIMIT 200";
|
|
||||||
$result = $conn->query($query);
|
|
||||||
|
|
||||||
$tested = 0;
|
|
||||||
while ($row = $result->fetch_assoc()) {
|
|
||||||
$issues = detectIssues($row['post_content']);
|
|
||||||
if (!empty($issues) && $tested < 5) {
|
|
||||||
$tested++;
|
|
||||||
echo "POST ID: {$row['ID']}\n";
|
|
||||||
echo "Título: {$row['post_title']}\n";
|
|
||||||
echo "Problemas detectados: " . count($issues) . "\n\n";
|
|
||||||
|
|
||||||
$fixResult = fixMalformedLists($row['post_content']);
|
|
||||||
$issuesAfter = detectIssues($fixResult['html']);
|
|
||||||
|
|
||||||
echo "ANTES: " . count($issues) . " problemas\n";
|
|
||||||
echo "DESPUÉS: " . count($issuesAfter) . " problemas\n";
|
|
||||||
echo "Cambios: {$fixResult['changes']}\n";
|
|
||||||
|
|
||||||
// Verificar integridad
|
|
||||||
$before_ul = substr_count($row['post_content'], '<ul');
|
|
||||||
$after_ul = substr_count($fixResult['html'], '<ul');
|
|
||||||
$before_li = substr_count($row['post_content'], '<li');
|
|
||||||
$after_li = substr_count($fixResult['html'], '<li');
|
|
||||||
|
|
||||||
echo "Tags <ul>: $before_ul → $after_ul " . ($before_ul === $after_ul ? "✓" : "⚠️") . "\n";
|
|
||||||
echo "Tags <li>: $before_li → $after_li " . ($before_li === $after_li ? "✓" : "⚠️") . "\n";
|
|
||||||
|
|
||||||
if (count($issuesAfter) === 0) {
|
|
||||||
echo "✅ CORRECCIÓN EXITOSA\n";
|
|
||||||
} else {
|
|
||||||
echo "⚠️ REQUIERE REVISIÓN\n";
|
|
||||||
}
|
|
||||||
echo "─────────────────────────────────\n\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} elseif ($mode === 'fix') {
|
|
||||||
echo "MODO: CORRECCIÓN (GUARDANDO CAMBIOS)\n";
|
|
||||||
echo "─────────────────────────────────\n\n";
|
|
||||||
|
|
||||||
$batch_size = 50;
|
|
||||||
$offset = 0;
|
|
||||||
$fixed_count = 0;
|
|
||||||
$error_count = 0;
|
|
||||||
|
|
||||||
while ($offset < $total) {
|
|
||||||
$query = "SELECT ID, post_content FROM wp_posts
|
|
||||||
WHERE post_status = 'publish'
|
|
||||||
AND post_type IN ('post', 'page')
|
|
||||||
AND post_content IS NOT NULL
|
|
||||||
AND post_content != ''
|
|
||||||
ORDER BY ID LIMIT $batch_size OFFSET $offset";
|
|
||||||
$result = $conn->query($query);
|
|
||||||
|
|
||||||
while ($row = $result->fetch_assoc()) {
|
|
||||||
$issues = detectIssues($row['post_content']);
|
|
||||||
|
|
||||||
if (!empty($issues)) {
|
|
||||||
$fixResult = fixMalformedLists($row['post_content']);
|
|
||||||
|
|
||||||
if ($fixResult['fixed']) {
|
|
||||||
$stmt = $conn->prepare("UPDATE wp_posts SET post_content = ? WHERE ID = ?");
|
|
||||||
$stmt->bind_param("si", $fixResult['html'], $row['ID']);
|
|
||||||
|
|
||||||
if ($stmt->execute()) {
|
|
||||||
$fixed_count++;
|
|
||||||
echo "[ID: {$row['ID']}] ✓ Corregido ({$fixResult['changes']} cambios)\n";
|
|
||||||
} else {
|
|
||||||
$error_count++;
|
|
||||||
echo "[ID: {$row['ID']}] ✗ Error al guardar\n";
|
|
||||||
}
|
|
||||||
$stmt->close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$offset += $batch_size;
|
|
||||||
|
|
||||||
if ($offset % 500 == 0) {
|
|
||||||
echo "Procesados: $offset/$total (corregidos: $fixed_count)\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\n─────────────────────────────────\n";
|
|
||||||
echo "RESUMEN:\n";
|
|
||||||
echo " Posts corregidos: $fixed_count\n";
|
|
||||||
echo " Errores: $error_count\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$conn->close();
|
|
||||||
echo "\n✓ Proceso completado.\n";
|
|
||||||
@@ -1,307 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Script de Diagnóstico: Listas HTML Mal Formadas
|
|
||||||
*
|
|
||||||
* PROPÓSITO: Identificar posts con estructura de listas inválida
|
|
||||||
* - <ul> conteniendo <ul> como hijo directo (en lugar de dentro de <li>)
|
|
||||||
* - <ol> conteniendo <ol> como hijo directo
|
|
||||||
*
|
|
||||||
* BASE DE DATOS: preciosunitarios_seo
|
|
||||||
* TABLA: datos_seo_pagina
|
|
||||||
* CAMPO: html
|
|
||||||
*
|
|
||||||
* IMPORTANTE: Este script SOLO LEE, no modifica ningún dato.
|
|
||||||
*
|
|
||||||
* @package ROI_Theme
|
|
||||||
* @since Phase 4.4 Accessibility
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Configuración de errores para debugging
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
ini_set('display_errors', 1);
|
|
||||||
ini_set('memory_limit', '512M');
|
|
||||||
set_time_limit(300); // 5 minutos máximo
|
|
||||||
|
|
||||||
// Credenciales de base de datos (ajustar según servidor)
|
|
||||||
$db_config = [
|
|
||||||
'host' => 'localhost',
|
|
||||||
'database' => 'preciosunitarios_seo',
|
|
||||||
'username' => 'root', // Cambiar en producción
|
|
||||||
'password' => '', // Cambiar en producción
|
|
||||||
'charset' => 'utf8mb4'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Patrones regex para detectar listas mal formadas
|
|
||||||
$malformed_patterns = [
|
|
||||||
// <ul> seguido directamente de <ul> (sin estar dentro de <li>)
|
|
||||||
'ul_direct_ul' => '/<ul[^>]*>\s*(?:<li[^>]*>.*?<\/li>\s*)*<ul/is',
|
|
||||||
|
|
||||||
// Patrón más específico: </li> seguido de <ul> (hermanos en lugar de anidados)
|
|
||||||
'li_sibling_ul' => '/<\/li>\s*<ul[^>]*>/is',
|
|
||||||
|
|
||||||
// <ol> seguido directamente de <ol>
|
|
||||||
'ol_direct_ol' => '/<ol[^>]*>\s*(?:<li[^>]*>.*?<\/li>\s*)*<ol/is',
|
|
||||||
|
|
||||||
// </li> seguido de <ol> (hermanos)
|
|
||||||
'li_sibling_ol' => '/<\/li>\s*<ol[^>]*>/is',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Conectar a la base de datos
|
|
||||||
*/
|
|
||||||
function connectDatabase(array $config): ?mysqli {
|
|
||||||
$conn = new mysqli(
|
|
||||||
$config['host'],
|
|
||||||
$config['username'],
|
|
||||||
$config['password'],
|
|
||||||
$config['database']
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
echo "Error de conexión: " . $conn->connect_error . "\n";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$conn->set_charset($config['charset']);
|
|
||||||
return $conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Analizar HTML en busca de listas mal formadas
|
|
||||||
*/
|
|
||||||
function analyzeMalformedLists(string $html, array $patterns): array {
|
|
||||||
$issues = [];
|
|
||||||
|
|
||||||
foreach ($patterns as $pattern_name => $pattern) {
|
|
||||||
if (preg_match_all($pattern, $html, $matches, PREG_OFFSET_CAPTURE)) {
|
|
||||||
foreach ($matches[0] as $match) {
|
|
||||||
$position = $match[1];
|
|
||||||
$context = getContextAroundPosition($html, $position, 100);
|
|
||||||
|
|
||||||
$issues[] = [
|
|
||||||
'type' => $pattern_name,
|
|
||||||
'position' => $position,
|
|
||||||
'context' => $context
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $issues;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtener contexto alrededor de una posición
|
|
||||||
*/
|
|
||||||
function getContextAroundPosition(string $html, int $position, int $length = 100): string {
|
|
||||||
$start = max(0, $position - $length);
|
|
||||||
$end = min(strlen($html), $position + $length);
|
|
||||||
|
|
||||||
$context = substr($html, $start, $end - $start);
|
|
||||||
|
|
||||||
// Limpiar para mostrar
|
|
||||||
$context = preg_replace('/\s+/', ' ', $context);
|
|
||||||
$context = htmlspecialchars($context);
|
|
||||||
|
|
||||||
if ($start > 0) {
|
|
||||||
$context = '...' . $context;
|
|
||||||
}
|
|
||||||
if ($end < strlen($html)) {
|
|
||||||
$context .= '...';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $context;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contar total de listas en el HTML
|
|
||||||
*/
|
|
||||||
function countListElements(string $html): array {
|
|
||||||
$ul_count = preg_match_all('/<ul[^>]*>/i', $html);
|
|
||||||
$ol_count = preg_match_all('/<ol[^>]*>/i', $html);
|
|
||||||
$li_count = preg_match_all('/<li[^>]*>/i', $html);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'ul' => $ul_count,
|
|
||||||
'ol' => $ol_count,
|
|
||||||
'li' => $li_count
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// EJECUCIÓN PRINCIPAL
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
echo "==============================================\n";
|
|
||||||
echo " DIAGNÓSTICO: Listas HTML Mal Formadas\n";
|
|
||||||
echo " Base de datos: {$db_config['database']}\n";
|
|
||||||
echo " Tabla: datos_seo_pagina\n";
|
|
||||||
echo " Fecha: " . date('Y-m-d H:i:s') . "\n";
|
|
||||||
echo "==============================================\n\n";
|
|
||||||
|
|
||||||
// Conectar
|
|
||||||
$conn = connectDatabase($db_config);
|
|
||||||
if (!$conn) {
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "✓ Conexión establecida\n\n";
|
|
||||||
|
|
||||||
// Obtener estructura de la tabla
|
|
||||||
echo "Verificando estructura de tabla...\n";
|
|
||||||
$result = $conn->query("DESCRIBE datos_seo_pagina");
|
|
||||||
if ($result) {
|
|
||||||
echo "Columnas encontradas:\n";
|
|
||||||
while ($row = $result->fetch_assoc()) {
|
|
||||||
echo " - {$row['Field']} ({$row['Type']})\n";
|
|
||||||
}
|
|
||||||
echo "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contar registros totales
|
|
||||||
$result = $conn->query("SELECT COUNT(*) as total FROM datos_seo_pagina WHERE html IS NOT NULL AND html != ''");
|
|
||||||
$total = $result->fetch_assoc()['total'];
|
|
||||||
echo "Total de registros con HTML: {$total}\n\n";
|
|
||||||
|
|
||||||
// Procesar en lotes
|
|
||||||
$batch_size = 100;
|
|
||||||
$offset = 0;
|
|
||||||
$affected_posts = [];
|
|
||||||
$total_issues = 0;
|
|
||||||
$processed = 0;
|
|
||||||
|
|
||||||
echo "Iniciando análisis...\n";
|
|
||||||
echo "─────────────────────────────────────────────\n";
|
|
||||||
|
|
||||||
while ($offset < $total) {
|
|
||||||
$query = "SELECT id, page, html FROM datos_seo_pagina
|
|
||||||
WHERE html IS NOT NULL AND html != ''
|
|
||||||
ORDER BY id
|
|
||||||
LIMIT {$batch_size} OFFSET {$offset}";
|
|
||||||
|
|
||||||
$result = $conn->query($query);
|
|
||||||
|
|
||||||
if (!$result) {
|
|
||||||
echo "Error en consulta: " . $conn->error . "\n";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
while ($row = $result->fetch_assoc()) {
|
|
||||||
$processed++;
|
|
||||||
$id = $row['id'];
|
|
||||||
$url = $row['page'] ?? 'N/A';
|
|
||||||
$html = $row['html'];
|
|
||||||
|
|
||||||
$issues = analyzeMalformedLists($html, $malformed_patterns);
|
|
||||||
|
|
||||||
if (!empty($issues)) {
|
|
||||||
$list_counts = countListElements($html);
|
|
||||||
|
|
||||||
$affected_posts[] = [
|
|
||||||
'id' => $id,
|
|
||||||
'url' => $url,
|
|
||||||
'issues' => $issues,
|
|
||||||
'list_counts' => $list_counts
|
|
||||||
];
|
|
||||||
|
|
||||||
$total_issues += count($issues);
|
|
||||||
|
|
||||||
// Mostrar progreso para posts afectados
|
|
||||||
echo "\n[ID: {$id}] " . count($issues) . " problema(s) encontrado(s)\n";
|
|
||||||
echo "URL: {$url}\n";
|
|
||||||
echo "Listas: UL={$list_counts['ul']}, OL={$list_counts['ol']}, LI={$list_counts['li']}\n";
|
|
||||||
|
|
||||||
foreach ($issues as $idx => $issue) {
|
|
||||||
echo " Problema " . ($idx + 1) . ": {$issue['type']} (pos: {$issue['position']})\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mostrar progreso cada 500 registros
|
|
||||||
if ($processed % 500 == 0) {
|
|
||||||
echo "\rProcesados: {$processed}/{$total}...";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$offset += $batch_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\n\n";
|
|
||||||
echo "==============================================\n";
|
|
||||||
echo " RESUMEN DEL ANÁLISIS\n";
|
|
||||||
echo "==============================================\n\n";
|
|
||||||
|
|
||||||
echo "Registros analizados: {$processed}\n";
|
|
||||||
echo "Posts con problemas: " . count($affected_posts) . "\n";
|
|
||||||
echo "Total de incidencias: {$total_issues}\n\n";
|
|
||||||
|
|
||||||
if (count($affected_posts) > 0) {
|
|
||||||
echo "─────────────────────────────────────────────\n";
|
|
||||||
echo "DETALLE DE POSTS AFECTADOS\n";
|
|
||||||
echo "─────────────────────────────────────────────\n\n";
|
|
||||||
|
|
||||||
// Agrupar por tipo de problema
|
|
||||||
$by_type = [];
|
|
||||||
foreach ($affected_posts as $post) {
|
|
||||||
foreach ($post['issues'] as $issue) {
|
|
||||||
$type = $issue['type'];
|
|
||||||
if (!isset($by_type[$type])) {
|
|
||||||
$by_type[$type] = [];
|
|
||||||
}
|
|
||||||
$by_type[$type][] = $post['id'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Por tipo de problema:\n";
|
|
||||||
foreach ($by_type as $type => $ids) {
|
|
||||||
$unique_ids = array_unique($ids);
|
|
||||||
echo " - {$type}: " . count($unique_ids) . " posts\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\n─────────────────────────────────────────────\n";
|
|
||||||
echo "LISTA DE IDs AFECTADOS (para revisión manual)\n";
|
|
||||||
echo "─────────────────────────────────────────────\n\n";
|
|
||||||
|
|
||||||
$ids_list = array_column($affected_posts, 'id');
|
|
||||||
echo "IDs: " . implode(', ', $ids_list) . "\n";
|
|
||||||
|
|
||||||
// Generar archivo de reporte
|
|
||||||
$report_file = __DIR__ . '/malformed-lists-report-' . date('Ymd-His') . '.json';
|
|
||||||
$report_data = [
|
|
||||||
'generated_at' => date('Y-m-d H:i:s'),
|
|
||||||
'database' => $db_config['database'],
|
|
||||||
'table' => 'datos_seo_pagina',
|
|
||||||
'total_analyzed' => $processed,
|
|
||||||
'total_affected' => count($affected_posts),
|
|
||||||
'total_issues' => $total_issues,
|
|
||||||
'by_type' => array_map(function($ids) {
|
|
||||||
return array_values(array_unique($ids));
|
|
||||||
}, $by_type),
|
|
||||||
'affected_posts' => $affected_posts
|
|
||||||
];
|
|
||||||
|
|
||||||
if (file_put_contents($report_file, json_encode($report_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE))) {
|
|
||||||
echo "\n✓ Reporte JSON guardado en:\n {$report_file}\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Muestra de contexto para análisis
|
|
||||||
echo "\n─────────────────────────────────────────────\n";
|
|
||||||
echo "MUESTRA DE CONTEXTO (primeros 3 posts)\n";
|
|
||||||
echo "─────────────────────────────────────────────\n\n";
|
|
||||||
|
|
||||||
$sample = array_slice($affected_posts, 0, 3);
|
|
||||||
foreach ($sample as $post) {
|
|
||||||
echo "POST ID: {$post['id']}\n";
|
|
||||||
echo "URL: {$post['url']}\n";
|
|
||||||
foreach ($post['issues'] as $idx => $issue) {
|
|
||||||
echo " [{$issue['type']}]\n";
|
|
||||||
echo " Contexto: {$issue['context']}\n\n";
|
|
||||||
}
|
|
||||||
echo "───────────────────────\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
echo "✓ No se encontraron listas mal formadas.\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$conn->close();
|
|
||||||
echo "\n✓ Análisis completado.\n";
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Script de PRUEBA - Muestra corrección propuesta sin aplicarla
|
|
||||||
*
|
|
||||||
* IMPORTANTE: Este script SOLO MUESTRA, no modifica nada.
|
|
||||||
*/
|
|
||||||
|
|
||||||
$conn = new mysqli("localhost", "preciosunitarios_seo", "ACl%EEFd=V-Yvb??", "preciosunitarios_seo");
|
|
||||||
$conn->set_charset("utf8mb4");
|
|
||||||
|
|
||||||
echo "========================================\n";
|
|
||||||
echo "ANÁLISIS DE CORRECCIÓN PROPUESTA\n";
|
|
||||||
echo "========================================\n\n";
|
|
||||||
|
|
||||||
// Patrón que encuentra: </li></ul><li>TEXTO</li><ul>
|
|
||||||
// Este patrón captura:
|
|
||||||
// - $1: </li> inicial (con espacios)
|
|
||||||
// - $2: espacios entre </ul> y <li>
|
|
||||||
// - $3: contenido del <li> (ej: <strong>Texto</strong>)
|
|
||||||
// - $4: espacios entre </li> y <ul>
|
|
||||||
|
|
||||||
$pattern = '#(</li>\s*)</ul>(\s*)<li>(.*?)</li>(\s*)<ul>#is';
|
|
||||||
$replacement = '$1<li>$3$4<ul>';
|
|
||||||
|
|
||||||
echo "PATRÓN A BUSCAR:\n";
|
|
||||||
echo " </li>\\s*</ul>\\s*<li>CONTENIDO</li>\\s*<ul>\n\n";
|
|
||||||
|
|
||||||
echo "REEMPLAZO:\n";
|
|
||||||
echo " </li><li>CONTENIDO<ul>\n\n";
|
|
||||||
|
|
||||||
// Obtener HTML del post ID 3
|
|
||||||
$result = $conn->query("SELECT id, page, html FROM datos_seo_pagina WHERE id = 3");
|
|
||||||
$row = $result->fetch_assoc();
|
|
||||||
$html = $row["html"];
|
|
||||||
$page = $row["page"];
|
|
||||||
|
|
||||||
echo "PROBANDO CON POST ID 3:\n";
|
|
||||||
echo "URL: $page\n";
|
|
||||||
echo "────────────────────────────\n\n";
|
|
||||||
|
|
||||||
// Encontrar todas las ocurrencias
|
|
||||||
preg_match_all($pattern, $html, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
|
|
||||||
|
|
||||||
echo "Ocurrencias encontradas: " . count($matches) . "\n\n";
|
|
||||||
|
|
||||||
// Mostrar cada ocurrencia y su corrección propuesta
|
|
||||||
foreach (array_slice($matches, 0, 3) as $idx => $match) {
|
|
||||||
$full_match = $match[0][0];
|
|
||||||
$position = $match[0][1];
|
|
||||||
|
|
||||||
echo "[$idx] Posición: $position\n";
|
|
||||||
echo "ANTES:\n";
|
|
||||||
echo htmlspecialchars($full_match) . "\n\n";
|
|
||||||
|
|
||||||
$fixed = preg_replace($pattern, $replacement, $full_match);
|
|
||||||
echo "DESPUÉS:\n";
|
|
||||||
echo htmlspecialchars($fixed) . "\n";
|
|
||||||
echo "────────────────────────────\n\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aplicar corrección en memoria y contar diferencia
|
|
||||||
$html_fixed = preg_replace($pattern, $replacement, $html);
|
|
||||||
|
|
||||||
$before = preg_match_all($pattern, $html);
|
|
||||||
$after = preg_match_all($pattern, $html_fixed);
|
|
||||||
|
|
||||||
echo "========================================\n";
|
|
||||||
echo "RESUMEN DE CORRECCIÓN (sin aplicar):\n";
|
|
||||||
echo "========================================\n";
|
|
||||||
echo "Ocurrencias ANTES: $before\n";
|
|
||||||
echo "Ocurrencias DESPUÉS: $after\n";
|
|
||||||
echo "Reducción: " . ($before - $after) . "\n\n";
|
|
||||||
|
|
||||||
// Verificar que la estructura es válida después de la corrección
|
|
||||||
$ul_count_before = substr_count($html, '<ul');
|
|
||||||
$ul_count_after = substr_count($html_fixed, '<ul');
|
|
||||||
echo "Tags <ul> antes: $ul_count_before\n";
|
|
||||||
echo "Tags <ul> después: $ul_count_after\n";
|
|
||||||
|
|
||||||
$li_count_before = substr_count($html, '<li');
|
|
||||||
$li_count_after = substr_count($html_fixed, '<li');
|
|
||||||
echo "Tags <li> antes: $li_count_before\n";
|
|
||||||
echo "Tags <li> después: $li_count_after\n";
|
|
||||||
|
|
||||||
echo "\n========================================\n";
|
|
||||||
echo "NOTA: Este patrón elimina el </ul> prematuro\n";
|
|
||||||
echo "pero NO agrega el </li> faltante al final.\n";
|
|
||||||
echo "Se necesita un segundo paso para balancear.\n";
|
|
||||||
echo "========================================\n";
|
|
||||||
|
|
||||||
$conn->close();
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Prueba corrección en casos específicos variados
|
|
||||||
*/
|
|
||||||
|
|
||||||
$conn = new mysqli("localhost", "preciosunitarios_seo", "ACl%EEFd=V-Yvb??", "preciosunitarios_seo");
|
|
||||||
$conn->set_charset("utf8mb4");
|
|
||||||
|
|
||||||
// IDs a probar (casos variados)
|
|
||||||
$test_ids = [20, 23, 65, 377, 98, 107, 144];
|
|
||||||
|
|
||||||
function detectIssues($html) {
|
|
||||||
$issues = [];
|
|
||||||
libxml_use_internal_errors(true);
|
|
||||||
$doc = new DOMDocument("1.0", "UTF-8");
|
|
||||||
$doc->loadHTML('<?xml encoding="UTF-8"><div id="w">' . $html . '</div>', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
|
||||||
libxml_clear_errors();
|
|
||||||
|
|
||||||
$validChildren = ["li", "script", "template"];
|
|
||||||
foreach (["ul", "ol"] as $tag) {
|
|
||||||
foreach ($doc->getElementsByTagName($tag) as $list) {
|
|
||||||
foreach ($list->childNodes as $child) {
|
|
||||||
if ($child->nodeType === XML_ELEMENT_NODE) {
|
|
||||||
$childTag = strtolower($child->nodeName);
|
|
||||||
if (!in_array($childTag, $validChildren)) {
|
|
||||||
$issues[] = "<$tag> contiene <$childTag>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $issues;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fixMalformedLists($html) {
|
|
||||||
$result = ['fixed' => false, 'html' => $html, 'changes' => 0];
|
|
||||||
|
|
||||||
libxml_use_internal_errors(true);
|
|
||||||
$doc = new DOMDocument("1.0", "UTF-8");
|
|
||||||
$doc->loadHTML('<?xml encoding="UTF-8"><div id="w">' . $html . '</div>', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
|
||||||
libxml_clear_errors();
|
|
||||||
|
|
||||||
$lists = [];
|
|
||||||
foreach ($doc->getElementsByTagName('ul') as $ul) { $lists[] = $ul; }
|
|
||||||
foreach ($doc->getElementsByTagName('ol') as $ol) { $lists[] = $ol; }
|
|
||||||
|
|
||||||
$changes = 0;
|
|
||||||
$validChildren = ["li", "script", "template"];
|
|
||||||
|
|
||||||
foreach ($lists as $list) {
|
|
||||||
$nodesToProcess = [];
|
|
||||||
foreach ($list->childNodes as $child) {
|
|
||||||
if ($child->nodeType === XML_ELEMENT_NODE) {
|
|
||||||
$tagName = strtolower($child->nodeName);
|
|
||||||
if (!in_array($tagName, $validChildren)) {
|
|
||||||
$nodesToProcess[] = $child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($nodesToProcess as $node) {
|
|
||||||
$tagName = strtolower($node->nodeName);
|
|
||||||
$prevLi = null;
|
|
||||||
$prev = $node->previousSibling;
|
|
||||||
|
|
||||||
while ($prev) {
|
|
||||||
if ($prev->nodeType === XML_ELEMENT_NODE && strtolower($prev->nodeName) === 'li') {
|
|
||||||
$prevLi = $prev;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$prev = $prev->previousSibling;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($prevLi) {
|
|
||||||
$prevLi->appendChild($node);
|
|
||||||
$changes++;
|
|
||||||
} else {
|
|
||||||
$newLi = $doc->createElement('li');
|
|
||||||
$list->insertBefore($newLi, $node);
|
|
||||||
$newLi->appendChild($node);
|
|
||||||
$changes++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($changes > 0) {
|
|
||||||
$wrapper = $doc->getElementById('w');
|
|
||||||
if ($wrapper) {
|
|
||||||
$innerHTML = '';
|
|
||||||
foreach ($wrapper->childNodes as $child) {
|
|
||||||
$innerHTML .= $doc->saveHTML($child);
|
|
||||||
}
|
|
||||||
$result['html'] = $innerHTML;
|
|
||||||
$result['fixed'] = true;
|
|
||||||
$result['changes'] = $changes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "=====================================================\n";
|
|
||||||
echo " PRUEBA DE CORRECCIÓN EN CASOS VARIADOS\n";
|
|
||||||
echo "=====================================================\n\n";
|
|
||||||
|
|
||||||
$ids_str = implode(',', $test_ids);
|
|
||||||
$query = "SELECT id, page, html FROM datos_seo_pagina WHERE id IN ($ids_str)";
|
|
||||||
$result = $conn->query($query);
|
|
||||||
|
|
||||||
$all_passed = true;
|
|
||||||
|
|
||||||
while ($row = $result->fetch_assoc()) {
|
|
||||||
$id = $row['id'];
|
|
||||||
$url = $row['page'];
|
|
||||||
$html = $row['html'];
|
|
||||||
|
|
||||||
echo "─────────────────────────────────────────────────\n";
|
|
||||||
echo "POST ID: $id\n";
|
|
||||||
echo "URL: $url\n\n";
|
|
||||||
|
|
||||||
// Detectar problemas antes
|
|
||||||
$issues_before = detectIssues($html);
|
|
||||||
echo "ANTES:\n";
|
|
||||||
echo " Problemas: " . count($issues_before) . "\n";
|
|
||||||
$unique_types = array_unique($issues_before);
|
|
||||||
foreach ($unique_types as $type) {
|
|
||||||
echo " - $type\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aplicar corrección
|
|
||||||
$fixResult = fixMalformedLists($html);
|
|
||||||
|
|
||||||
// Detectar problemas después
|
|
||||||
$issues_after = detectIssues($fixResult['html']);
|
|
||||||
|
|
||||||
echo "\nDESPUÉS:\n";
|
|
||||||
echo " Cambios aplicados: {$fixResult['changes']}\n";
|
|
||||||
echo " Problemas restantes: " . count($issues_after) . "\n";
|
|
||||||
|
|
||||||
if (count($issues_after) > 0) {
|
|
||||||
echo " ⚠️ Problemas NO resueltos:\n";
|
|
||||||
foreach (array_unique($issues_after) as $type) {
|
|
||||||
echo " - $type\n";
|
|
||||||
}
|
|
||||||
$all_passed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar integridad del HTML
|
|
||||||
$tags_before = [
|
|
||||||
'ul' => substr_count($html, '<ul'),
|
|
||||||
'ol' => substr_count($html, '<ol'),
|
|
||||||
'li' => substr_count($html, '<li'),
|
|
||||||
];
|
|
||||||
$tags_after = [
|
|
||||||
'ul' => substr_count($fixResult['html'], '<ul'),
|
|
||||||
'ol' => substr_count($fixResult['html'], '<ol'),
|
|
||||||
'li' => substr_count($fixResult['html'], '<li'),
|
|
||||||
];
|
|
||||||
|
|
||||||
echo "\nINTEGRIDAD DE TAGS:\n";
|
|
||||||
echo " <ul>: {$tags_before['ul']} → {$tags_after['ul']} ";
|
|
||||||
echo ($tags_before['ul'] === $tags_after['ul'] ? "✓" : "⚠️ CAMBIÓ") . "\n";
|
|
||||||
echo " <ol>: {$tags_before['ol']} → {$tags_after['ol']} ";
|
|
||||||
echo ($tags_before['ol'] === $tags_after['ol'] ? "✓" : "⚠️ CAMBIÓ") . "\n";
|
|
||||||
echo " <li>: {$tags_before['li']} → {$tags_after['li']} ";
|
|
||||||
echo ($tags_before['li'] === $tags_after['li'] ? "✓" : "⚠️ CAMBIÓ") . "\n";
|
|
||||||
|
|
||||||
// Resultado
|
|
||||||
if (count($issues_after) === 0 &&
|
|
||||||
$tags_before['ul'] === $tags_after['ul'] &&
|
|
||||||
$tags_before['ol'] === $tags_after['ol']) {
|
|
||||||
echo "\n✅ RESULTADO: CORRECCIÓN EXITOSA\n";
|
|
||||||
} else {
|
|
||||||
echo "\n❌ RESULTADO: REQUIERE REVISIÓN\n";
|
|
||||||
$all_passed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\n=====================================================\n";
|
|
||||||
if ($all_passed) {
|
|
||||||
echo "✅ TODOS LOS CASOS PASARON LA PRUEBA\n";
|
|
||||||
} else {
|
|
||||||
echo "⚠️ ALGUNOS CASOS REQUIEREN REVISIÓN\n";
|
|
||||||
}
|
|
||||||
echo "=====================================================\n";
|
|
||||||
|
|
||||||
$conn->close();
|
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Validador de Correcciones - Genera archivos HTML para revisión visual
|
|
||||||
*
|
|
||||||
* PROPÓSITO: Crear archivos comparativos ANTES/DESPUÉS para validar
|
|
||||||
* que la corrección no rompe el contenido.
|
|
||||||
*
|
|
||||||
* USO: php validate-fix-lists.php
|
|
||||||
*
|
|
||||||
* GENERA:
|
|
||||||
* /tmp/list-fix-validation/
|
|
||||||
* ├── post_ID_before.html
|
|
||||||
* ├── post_ID_after.html
|
|
||||||
* └── comparison_report.html
|
|
||||||
*
|
|
||||||
* @package ROI_Theme
|
|
||||||
*/
|
|
||||||
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
ini_set('display_errors', 1);
|
|
||||||
ini_set('memory_limit', '256M');
|
|
||||||
|
|
||||||
$db_config = [
|
|
||||||
'host' => 'localhost',
|
|
||||||
'database' => 'preciosunitarios_seo',
|
|
||||||
'username' => 'preciosunitarios_seo',
|
|
||||||
'password' => 'ACl%EEFd=V-Yvb??',
|
|
||||||
'charset' => 'utf8mb4'
|
|
||||||
];
|
|
||||||
|
|
||||||
$output_dir = '/tmp/list-fix-validation';
|
|
||||||
$sample_size = 5;
|
|
||||||
|
|
||||||
echo "==============================================\n";
|
|
||||||
echo " VALIDADOR DE CORRECCIONES\n";
|
|
||||||
echo " Fecha: " . date('Y-m-d H:i:s') . "\n";
|
|
||||||
echo "==============================================\n\n";
|
|
||||||
|
|
||||||
// Crear directorio de salida
|
|
||||||
if (!is_dir($output_dir)) {
|
|
||||||
mkdir($output_dir, 0755, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Limpiar archivos anteriores
|
|
||||||
array_map('unlink', glob("$output_dir/*.html"));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detectar problemas en HTML
|
|
||||||
*/
|
|
||||||
function detectIssues(string $html): array {
|
|
||||||
$issues = [];
|
|
||||||
libxml_use_internal_errors(true);
|
|
||||||
|
|
||||||
$doc = new DOMDocument('1.0', 'UTF-8');
|
|
||||||
$wrapped = '<div id="temp-wrapper">' . $html . '</div>';
|
|
||||||
$doc->loadHTML('<?xml encoding="UTF-8">' . $wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
|
||||||
libxml_clear_errors();
|
|
||||||
|
|
||||||
$validChildren = ['li', 'script', 'template'];
|
|
||||||
|
|
||||||
foreach (['ul', 'ol'] as $listTag) {
|
|
||||||
foreach ($doc->getElementsByTagName($listTag) as $list) {
|
|
||||||
foreach ($list->childNodes as $child) {
|
|
||||||
if ($child->nodeType === XML_ELEMENT_NODE) {
|
|
||||||
$tagName = strtolower($child->nodeName);
|
|
||||||
if (!in_array($tagName, $validChildren)) {
|
|
||||||
$issues[] = "<$listTag> contiene <$tagName>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $issues;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Corregir listas mal formadas
|
|
||||||
*/
|
|
||||||
function fixMalformedLists(string $html): array {
|
|
||||||
$result = ['fixed' => false, 'html' => $html, 'changes' => 0, 'details' => []];
|
|
||||||
|
|
||||||
libxml_use_internal_errors(true);
|
|
||||||
$doc = new DOMDocument('1.0', 'UTF-8');
|
|
||||||
$wrapped = '<div id="temp-wrapper">' . $html . '</div>';
|
|
||||||
$doc->loadHTML('<?xml encoding="UTF-8">' . $wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
|
||||||
libxml_clear_errors();
|
|
||||||
|
|
||||||
$lists = [];
|
|
||||||
foreach ($doc->getElementsByTagName('ul') as $ul) { $lists[] = $ul; }
|
|
||||||
foreach ($doc->getElementsByTagName('ol') as $ol) { $lists[] = $ol; }
|
|
||||||
|
|
||||||
$changes = 0;
|
|
||||||
$validChildren = ['li', 'script', 'template'];
|
|
||||||
|
|
||||||
foreach ($lists as $list) {
|
|
||||||
$nodesToProcess = [];
|
|
||||||
foreach ($list->childNodes as $child) {
|
|
||||||
if ($child->nodeType === XML_ELEMENT_NODE) {
|
|
||||||
$tagName = strtolower($child->nodeName);
|
|
||||||
if (!in_array($tagName, $validChildren)) {
|
|
||||||
$nodesToProcess[] = $child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($nodesToProcess as $node) {
|
|
||||||
$tagName = strtolower($node->nodeName);
|
|
||||||
$prevLi = null;
|
|
||||||
$prev = $node->previousSibling;
|
|
||||||
|
|
||||||
while ($prev) {
|
|
||||||
if ($prev->nodeType === XML_ELEMENT_NODE && strtolower($prev->nodeName) === 'li') {
|
|
||||||
$prevLi = $prev;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$prev = $prev->previousSibling;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($prevLi) {
|
|
||||||
$prevLi->appendChild($node);
|
|
||||||
$result['details'][] = "Movido <$tagName> dentro del <li> anterior";
|
|
||||||
$changes++;
|
|
||||||
} else {
|
|
||||||
$newLi = $doc->createElement('li');
|
|
||||||
$list->insertBefore($newLi, $node);
|
|
||||||
$newLi->appendChild($node);
|
|
||||||
$result['details'][] = "Envuelto <$tagName> en nuevo <li>";
|
|
||||||
$changes++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($changes > 0) {
|
|
||||||
$wrapper = $doc->getElementById('temp-wrapper');
|
|
||||||
if ($wrapper) {
|
|
||||||
$innerHTML = '';
|
|
||||||
foreach ($wrapper->childNodes as $child) {
|
|
||||||
$innerHTML .= $doc->saveHTML($child);
|
|
||||||
}
|
|
||||||
$result['html'] = $innerHTML;
|
|
||||||
$result['fixed'] = true;
|
|
||||||
$result['changes'] = $changes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generar HTML wrapper para visualización
|
|
||||||
*/
|
|
||||||
function wrapForVisualization(string $content, string $title, string $status): string {
|
|
||||||
$statusColor = $status === 'error' ? '#dc3545' : '#28a745';
|
|
||||||
return <<<HTML
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="es">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>$title</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 20px; line-height: 1.6; }
|
|
||||||
.status { padding: 10px 20px; background: $statusColor; color: white; border-radius: 4px; margin-bottom: 20px; }
|
|
||||||
.content { border: 1px solid #ddd; padding: 20px; border-radius: 4px; background: #fafafa; }
|
|
||||||
ul, ol { background: #fff3cd; padding: 15px 15px 15px 35px; border-left: 4px solid #ffc107; margin: 10px 0; }
|
|
||||||
li { background: #d4edda; padding: 5px 10px; margin: 5px 0; border-left: 3px solid #28a745; }
|
|
||||||
h1, h2, h3, h4, h5, h6 { color: #333; }
|
|
||||||
p { color: #555; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="status">$status</div>
|
|
||||||
<div class="content">
|
|
||||||
$content
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
HTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Conectar a DB
|
|
||||||
$conn = new mysqli($db_config['host'], $db_config['username'], $db_config['password'], $db_config['database']);
|
|
||||||
$conn->set_charset($db_config['charset']);
|
|
||||||
|
|
||||||
if ($conn->connect_error) {
|
|
||||||
die("Error de conexión: " . $conn->connect_error);
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "✓ Conexión establecida\n\n";
|
|
||||||
|
|
||||||
// Buscar posts con problemas
|
|
||||||
$query = "SELECT id, page, html FROM datos_seo_pagina WHERE html IS NOT NULL AND html != '' ORDER BY id LIMIT 500";
|
|
||||||
$result = $conn->query($query);
|
|
||||||
|
|
||||||
$samples = [];
|
|
||||||
while ($row = $result->fetch_assoc()) {
|
|
||||||
$issues = detectIssues($row['html']);
|
|
||||||
if (!empty($issues) && count($samples) < $sample_size) {
|
|
||||||
$samples[] = $row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Encontrados " . count($samples) . " posts con problemas para validar\n\n";
|
|
||||||
|
|
||||||
$comparison_data = [];
|
|
||||||
|
|
||||||
foreach ($samples as $idx => $post) {
|
|
||||||
$id = $post['id'];
|
|
||||||
$url = $post['page'];
|
|
||||||
$html_before = $post['html'];
|
|
||||||
|
|
||||||
echo "─────────────────────────────────\n";
|
|
||||||
echo "POST $id: $url\n";
|
|
||||||
|
|
||||||
// Detectar problemas antes
|
|
||||||
$issues_before = detectIssues($html_before);
|
|
||||||
echo " Problemas ANTES: " . count($issues_before) . "\n";
|
|
||||||
|
|
||||||
// Aplicar corrección
|
|
||||||
$fixResult = fixMalformedLists($html_before);
|
|
||||||
$html_after = $fixResult['html'];
|
|
||||||
|
|
||||||
// Detectar problemas después
|
|
||||||
$issues_after = detectIssues($html_after);
|
|
||||||
echo " Problemas DESPUÉS: " . count($issues_after) . "\n";
|
|
||||||
echo " Cambios aplicados: " . $fixResult['changes'] . "\n";
|
|
||||||
|
|
||||||
// Guardar archivos HTML
|
|
||||||
$file_before = "$output_dir/post_{$id}_BEFORE.html";
|
|
||||||
$file_after = "$output_dir/post_{$id}_AFTER.html";
|
|
||||||
|
|
||||||
file_put_contents($file_before, wrapForVisualization(
|
|
||||||
$html_before,
|
|
||||||
"Post $id - ANTES (con errores)",
|
|
||||||
"ANTES: " . count($issues_before) . " problemas de listas"
|
|
||||||
));
|
|
||||||
|
|
||||||
file_put_contents($file_after, wrapForVisualization(
|
|
||||||
$html_after,
|
|
||||||
"Post $id - DESPUÉS (corregido)",
|
|
||||||
"DESPUÉS: " . count($issues_after) . " problemas - " . $fixResult['changes'] . " correcciones aplicadas"
|
|
||||||
));
|
|
||||||
|
|
||||||
echo " ✓ Archivos generados:\n";
|
|
||||||
echo " - $file_before\n";
|
|
||||||
echo " - $file_after\n";
|
|
||||||
|
|
||||||
// Guardar datos para reporte
|
|
||||||
$comparison_data[] = [
|
|
||||||
'id' => $id,
|
|
||||||
'url' => $url,
|
|
||||||
'issues_before' => count($issues_before),
|
|
||||||
'issues_after' => count($issues_after),
|
|
||||||
'changes' => $fixResult['changes'],
|
|
||||||
'file_before' => "post_{$id}_BEFORE.html",
|
|
||||||
'file_after' => "post_{$id}_AFTER.html"
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generar reporte comparativo
|
|
||||||
$report_html = <<<HTML
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="es">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Reporte de Validación - Corrección de Listas</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 20px; }
|
|
||||||
h1 { color: #333; border-bottom: 2px solid #007bff; padding-bottom: 10px; }
|
|
||||||
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
|
||||||
th, td { padding: 12px; text-align: left; border: 1px solid #ddd; }
|
|
||||||
th { background: #007bff; color: white; }
|
|
||||||
tr:nth-child(even) { background: #f8f9fa; }
|
|
||||||
.success { color: #28a745; font-weight: bold; }
|
|
||||||
.warning { color: #ffc107; font-weight: bold; }
|
|
||||||
.error { color: #dc3545; font-weight: bold; }
|
|
||||||
a { color: #007bff; text-decoration: none; }
|
|
||||||
a:hover { text-decoration: underline; }
|
|
||||||
.instructions { background: #e7f3ff; padding: 15px; border-radius: 4px; margin: 20px 0; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Reporte de Validación - Corrección de Listas HTML</h1>
|
|
||||||
|
|
||||||
<div class="instructions">
|
|
||||||
<strong>Instrucciones:</strong>
|
|
||||||
<ol>
|
|
||||||
<li>Abre cada par de archivos (ANTES/DESPUÉS) en el navegador</li>
|
|
||||||
<li>Verifica que el contenido se muestre correctamente</li>
|
|
||||||
<li>Las listas (fondo amarillo) deben contener solo items (fondo verde)</li>
|
|
||||||
<li>Si todo se ve bien, la corrección es segura</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>URL</th>
|
|
||||||
<th>Problemas Antes</th>
|
|
||||||
<th>Problemas Después</th>
|
|
||||||
<th>Cambios</th>
|
|
||||||
<th>Archivos</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
HTML;
|
|
||||||
|
|
||||||
foreach ($comparison_data as $data) {
|
|
||||||
$status_class = $data['issues_after'] == 0 ? 'success' : ($data['issues_after'] < $data['issues_before'] ? 'warning' : 'error');
|
|
||||||
|
|
||||||
$report_html .= <<<HTML
|
|
||||||
<tr>
|
|
||||||
<td>{$data['id']}</td>
|
|
||||||
<td><a href="{$data['url']}" target="_blank">{$data['url']}</a></td>
|
|
||||||
<td class="error">{$data['issues_before']}</td>
|
|
||||||
<td class="$status_class">{$data['issues_after']}</td>
|
|
||||||
<td>{$data['changes']}</td>
|
|
||||||
<td>
|
|
||||||
<a href="{$data['file_before']}" target="_blank">ANTES</a> |
|
|
||||||
<a href="{$data['file_after']}" target="_blank">DESPUÉS</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
HTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
$report_html .= <<<HTML
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<p><strong>Generado:</strong> {$_SERVER['REQUEST_TIME_FLOAT']}</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
HTML;
|
|
||||||
|
|
||||||
$report_file = "$output_dir/comparison_report.html";
|
|
||||||
file_put_contents($report_file, $report_html);
|
|
||||||
|
|
||||||
echo "\n─────────────────────────────────\n";
|
|
||||||
echo "REPORTE GENERADO:\n";
|
|
||||||
echo " $report_file\n\n";
|
|
||||||
echo "Para revisar, descarga el directorio:\n";
|
|
||||||
echo " scp -r VPSContabo:$output_dir ./validation/\n\n";
|
|
||||||
|
|
||||||
$conn->close();
|
|
||||||
echo "✓ Validación completada.\n";
|
|
||||||
@@ -1,697 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace ROITheme\Shared\Infrastructure\Ui;
|
|
||||||
|
|
||||||
use ROITheme\Shared\Domain\Entities\Component;
|
|
||||||
use ROITheme\Shared\Domain\Contracts\FormBuilderInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TopNotificationBarFormBuilder - Construye formulario de configuración
|
|
||||||
*
|
|
||||||
* RESPONSABILIDAD: Generar formulario HTML del admin para Top Notification Bar
|
|
||||||
*
|
|
||||||
* CARACTERÍSTICAS:
|
|
||||||
* - 3 secciones: Visibilidad, Contenido, Estilos
|
|
||||||
* - 19 campos configurables
|
|
||||||
* - Lógica condicional (data-conditional-field)
|
|
||||||
* - WordPress Media Library integration
|
|
||||||
* - Vista previa en tiempo real
|
|
||||||
*
|
|
||||||
* @package ROITheme\Shared\Infrastructure\Ui
|
|
||||||
*/
|
|
||||||
final class TopNotificationBarFormBuilder implements FormBuilderInterface
|
|
||||||
{
|
|
||||||
public function build(Component $component): string
|
|
||||||
{
|
|
||||||
$data = $component->getData();
|
|
||||||
$componentId = $component->getName();
|
|
||||||
|
|
||||||
$html = '<div class="roi-form-builder roi-top-notification-bar-form">';
|
|
||||||
|
|
||||||
// Sección de Visibilidad
|
|
||||||
$html .= $this->buildVisibilitySection($data, $componentId);
|
|
||||||
|
|
||||||
// Sección de Contenido
|
|
||||||
$html .= $this->buildContentSection($data, $componentId);
|
|
||||||
|
|
||||||
// Sección de Estilos
|
|
||||||
$html .= $this->buildStylesSection($data, $componentId);
|
|
||||||
|
|
||||||
// Vista previa
|
|
||||||
$html .= $this->buildPreviewSection($data);
|
|
||||||
|
|
||||||
$html .= '</div>';
|
|
||||||
|
|
||||||
// Agregar scripts de formulario
|
|
||||||
$html .= $this->buildFormScripts($componentId);
|
|
||||||
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildVisibilitySection(array $data, string $componentId): string
|
|
||||||
{
|
|
||||||
$html = '<div class="roi-form-section" data-section="visibility">';
|
|
||||||
$html .= '<h3 class="roi-form-section-title">Visibilidad</h3>';
|
|
||||||
$html .= '<div class="roi-form-section-content">';
|
|
||||||
|
|
||||||
// Is Enabled
|
|
||||||
$isEnabled = $data['visibility']['is_enabled'] ?? true;
|
|
||||||
$html .= $this->buildToggle(
|
|
||||||
'is_enabled',
|
|
||||||
'Mostrar barra de notificación',
|
|
||||||
$isEnabled,
|
|
||||||
$componentId,
|
|
||||||
'Activa o desactiva la barra de notificación superior'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Show On Pages
|
|
||||||
$showOn = $data['visibility']['show_on_pages'] ?? 'all';
|
|
||||||
$html .= $this->buildSelect(
|
|
||||||
'show_on_pages',
|
|
||||||
'Mostrar en',
|
|
||||||
$showOn,
|
|
||||||
[
|
|
||||||
'all' => 'Todas las páginas',
|
|
||||||
'home' => 'Solo página de inicio',
|
|
||||||
'posts' => 'Solo posts individuales',
|
|
||||||
'pages' => 'Solo páginas',
|
|
||||||
'custom' => 'Páginas específicas'
|
|
||||||
],
|
|
||||||
$componentId,
|
|
||||||
'Define en qué páginas se mostrará la barra'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Custom Page IDs
|
|
||||||
$customPageIds = $data['visibility']['custom_page_ids'] ?? '';
|
|
||||||
$html .= $this->buildTextField(
|
|
||||||
'custom_page_ids',
|
|
||||||
'IDs de páginas específicas',
|
|
||||||
$customPageIds,
|
|
||||||
$componentId,
|
|
||||||
'IDs de páginas separados por comas',
|
|
||||||
'Ej: 1,5,10',
|
|
||||||
['data-conditional-field' => 'show_on_pages', 'data-conditional-value' => 'custom']
|
|
||||||
);
|
|
||||||
|
|
||||||
// Hide On Mobile
|
|
||||||
$hideOnMobile = $data['visibility']['hide_on_mobile'] ?? false;
|
|
||||||
$html .= $this->buildToggle(
|
|
||||||
'hide_on_mobile',
|
|
||||||
'Ocultar en dispositivos móviles',
|
|
||||||
$hideOnMobile,
|
|
||||||
$componentId,
|
|
||||||
'Oculta la barra en pantallas menores a 768px'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Is Dismissible
|
|
||||||
$isDismissible = $data['visibility']['is_dismissible'] ?? false;
|
|
||||||
$html .= $this->buildToggle(
|
|
||||||
'is_dismissible',
|
|
||||||
'Permitir cerrar',
|
|
||||||
$isDismissible,
|
|
||||||
$componentId,
|
|
||||||
'Agrega botón X para que el usuario pueda cerrar la barra'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Dismissible Cookie Days
|
|
||||||
$cookieDays = $data['visibility']['dismissible_cookie_days'] ?? 7;
|
|
||||||
$html .= $this->buildNumberField(
|
|
||||||
'dismissible_cookie_days',
|
|
||||||
'Días antes de volver a mostrar',
|
|
||||||
$cookieDays,
|
|
||||||
$componentId,
|
|
||||||
'Días que permanece oculta después de cerrarla',
|
|
||||||
1,
|
|
||||||
365,
|
|
||||||
['data-conditional-field' => 'is_dismissible', 'data-conditional-value' => 'true']
|
|
||||||
);
|
|
||||||
|
|
||||||
$html .= '</div>';
|
|
||||||
$html .= '</div>';
|
|
||||||
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildContentSection(array $data, string $componentId): string
|
|
||||||
{
|
|
||||||
$html = '<div class="roi-form-section" data-section="content">';
|
|
||||||
$html .= '<h3 class="roi-form-section-title">Contenido</h3>';
|
|
||||||
$html .= '<div class="roi-form-section-content">';
|
|
||||||
|
|
||||||
// Icon Type
|
|
||||||
$iconType = $data['content']['icon_type'] ?? 'bootstrap';
|
|
||||||
$html .= $this->buildSelect(
|
|
||||||
'icon_type',
|
|
||||||
'Tipo de ícono',
|
|
||||||
$iconType,
|
|
||||||
[
|
|
||||||
'bootstrap' => 'Bootstrap Icons',
|
|
||||||
'custom' => 'Imagen personalizada',
|
|
||||||
'none' => 'Sin ícono'
|
|
||||||
],
|
|
||||||
$componentId,
|
|
||||||
'Selecciona el tipo de ícono a mostrar'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Bootstrap Icon
|
|
||||||
$bootstrapIcon = $data['content']['bootstrap_icon'] ?? 'bi-megaphone-fill';
|
|
||||||
$html .= $this->buildTextField(
|
|
||||||
'bootstrap_icon',
|
|
||||||
'Clase de ícono Bootstrap',
|
|
||||||
$bootstrapIcon,
|
|
||||||
$componentId,
|
|
||||||
'Nombre de la clase del ícono sin el prefijo \'bi\' (ej: megaphone-fill)',
|
|
||||||
'Ej: bi-megaphone-fill',
|
|
||||||
['data-conditional-field' => 'icon_type', 'data-conditional-value' => 'bootstrap']
|
|
||||||
);
|
|
||||||
|
|
||||||
// Custom Icon URL
|
|
||||||
$customIconUrl = $data['content']['custom_icon_url'] ?? '';
|
|
||||||
$html .= $this->buildMediaField(
|
|
||||||
'custom_icon_url',
|
|
||||||
'Imagen personalizada',
|
|
||||||
$customIconUrl,
|
|
||||||
$componentId,
|
|
||||||
'Sube una imagen personalizada (recomendado: PNG 24x24px)',
|
|
||||||
['data-conditional-field' => 'icon_type', 'data-conditional-value' => 'custom']
|
|
||||||
);
|
|
||||||
|
|
||||||
// Announcement Label
|
|
||||||
$announcementLabel = $data['content']['announcement_label'] ?? 'Nuevo:';
|
|
||||||
$html .= $this->buildTextField(
|
|
||||||
'announcement_label',
|
|
||||||
'Etiqueta del anuncio',
|
|
||||||
$announcementLabel,
|
|
||||||
$componentId,
|
|
||||||
'Texto destacado en negrita antes del mensaje',
|
|
||||||
'Ej: Nuevo:, Importante:, Aviso:'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Announcement Text
|
|
||||||
$announcementText = $data['content']['announcement_text'] ?? 'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.';
|
|
||||||
$html .= $this->buildTextArea(
|
|
||||||
'announcement_text',
|
|
||||||
'Texto del anuncio',
|
|
||||||
$announcementText,
|
|
||||||
$componentId,
|
|
||||||
'Mensaje principal del anuncio (máximo 200 caracteres)',
|
|
||||||
3
|
|
||||||
);
|
|
||||||
|
|
||||||
// Link Enabled
|
|
||||||
$linkEnabled = $data['content']['link_enabled'] ?? true;
|
|
||||||
$html .= $this->buildToggle(
|
|
||||||
'link_enabled',
|
|
||||||
'Mostrar enlace',
|
|
||||||
$linkEnabled,
|
|
||||||
$componentId,
|
|
||||||
'Activa o desactiva el enlace de acción'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Link Text
|
|
||||||
$linkText = $data['content']['link_text'] ?? 'Ver Catálogo';
|
|
||||||
$html .= $this->buildTextField(
|
|
||||||
'link_text',
|
|
||||||
'Texto del enlace',
|
|
||||||
$linkText,
|
|
||||||
$componentId,
|
|
||||||
'Texto del enlace de acción',
|
|
||||||
'',
|
|
||||||
['data-conditional-field' => 'link_enabled', 'data-conditional-value' => 'true']
|
|
||||||
);
|
|
||||||
|
|
||||||
// Link URL
|
|
||||||
$linkUrl = $data['content']['link_url'] ?? '#';
|
|
||||||
$html .= $this->buildUrlField(
|
|
||||||
'link_url',
|
|
||||||
'URL del enlace',
|
|
||||||
$linkUrl,
|
|
||||||
$componentId,
|
|
||||||
'URL de destino del enlace',
|
|
||||||
'https://',
|
|
||||||
['data-conditional-field' => 'link_enabled', 'data-conditional-value' => 'true']
|
|
||||||
);
|
|
||||||
|
|
||||||
// Link Target
|
|
||||||
$linkTarget = $data['content']['link_target'] ?? '_self';
|
|
||||||
$html .= $this->buildSelect(
|
|
||||||
'link_target',
|
|
||||||
'Abrir enlace en',
|
|
||||||
$linkTarget,
|
|
||||||
[
|
|
||||||
'_self' => 'Misma ventana',
|
|
||||||
'_blank' => 'Nueva ventana'
|
|
||||||
],
|
|
||||||
$componentId,
|
|
||||||
'Define cómo se abrirá el enlace',
|
|
||||||
['data-conditional-field' => 'link_enabled', 'data-conditional-value' => 'true']
|
|
||||||
);
|
|
||||||
|
|
||||||
$html .= '</div>';
|
|
||||||
$html .= '</div>';
|
|
||||||
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildStylesSection(array $data, string $componentId): string
|
|
||||||
{
|
|
||||||
$html = '<div class="roi-form-section" data-section="styles">';
|
|
||||||
$html .= '<h3 class="roi-form-section-title">Estilos</h3>';
|
|
||||||
$html .= '<div class="roi-form-section-content">';
|
|
||||||
|
|
||||||
// Background Color
|
|
||||||
$bgColor = $data['styles']['background_color'] ?? '#FF8600';
|
|
||||||
$html .= $this->buildColorField(
|
|
||||||
'background_color',
|
|
||||||
'Color de fondo',
|
|
||||||
$bgColor,
|
|
||||||
$componentId,
|
|
||||||
'Color de fondo de la barra (por defecto: orange primary)'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Text Color
|
|
||||||
$textColor = $data['styles']['text_color'] ?? '#FFFFFF';
|
|
||||||
$html .= $this->buildColorField(
|
|
||||||
'text_color',
|
|
||||||
'Color del texto',
|
|
||||||
$textColor,
|
|
||||||
$componentId,
|
|
||||||
'Color del texto del anuncio'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Link Color
|
|
||||||
$linkColor = $data['styles']['link_color'] ?? '#FFFFFF';
|
|
||||||
$html .= $this->buildColorField(
|
|
||||||
'link_color',
|
|
||||||
'Color del enlace',
|
|
||||||
$linkColor,
|
|
||||||
$componentId,
|
|
||||||
'Color del enlace de acción'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Font Size
|
|
||||||
$fontSize = $data['styles']['font_size'] ?? 'small';
|
|
||||||
$html .= $this->buildSelect(
|
|
||||||
'font_size',
|
|
||||||
'Tamaño de fuente',
|
|
||||||
$fontSize,
|
|
||||||
[
|
|
||||||
'extra-small' => 'Muy pequeño (0.75rem)',
|
|
||||||
'small' => 'Pequeño (0.875rem)',
|
|
||||||
'normal' => 'Normal (1rem)',
|
|
||||||
'large' => 'Grande (1.125rem)'
|
|
||||||
],
|
|
||||||
$componentId,
|
|
||||||
'Tamaño del texto del anuncio'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Padding Vertical
|
|
||||||
$padding = $data['styles']['padding_vertical'] ?? 'normal';
|
|
||||||
$html .= $this->buildSelect(
|
|
||||||
'padding_vertical',
|
|
||||||
'Padding vertical',
|
|
||||||
$padding,
|
|
||||||
[
|
|
||||||
'compact' => 'Compacto (0.5rem)',
|
|
||||||
'normal' => 'Normal (0.75rem)',
|
|
||||||
'spacious' => 'Espacioso (1rem)'
|
|
||||||
],
|
|
||||||
$componentId,
|
|
||||||
'Espaciado vertical interno de la barra'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Text Alignment
|
|
||||||
$alignment = $data['styles']['text_alignment'] ?? 'center';
|
|
||||||
$html .= $this->buildSelect(
|
|
||||||
'text_alignment',
|
|
||||||
'Alineación del texto',
|
|
||||||
$alignment,
|
|
||||||
[
|
|
||||||
'left' => 'Izquierda',
|
|
||||||
'center' => 'Centro',
|
|
||||||
'right' => 'Derecha'
|
|
||||||
],
|
|
||||||
$componentId,
|
|
||||||
'Alineación del contenido de la barra'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Animation Enabled
|
|
||||||
$animationEnabled = $data['styles']['animation_enabled'] ?? false;
|
|
||||||
$html .= $this->buildToggle(
|
|
||||||
'animation_enabled',
|
|
||||||
'Activar animación',
|
|
||||||
$animationEnabled,
|
|
||||||
$componentId,
|
|
||||||
'Activa animación de entrada al cargar la página'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Animation Type
|
|
||||||
$animationType = $data['styles']['animation_type'] ?? 'slide-down';
|
|
||||||
$html .= $this->buildSelect(
|
|
||||||
'animation_type',
|
|
||||||
'Tipo de animación',
|
|
||||||
$animationType,
|
|
||||||
[
|
|
||||||
'slide-down' => 'Deslizar desde arriba',
|
|
||||||
'fade-in' => 'Aparecer gradualmente'
|
|
||||||
],
|
|
||||||
$componentId,
|
|
||||||
'Tipo de animación de entrada',
|
|
||||||
['data-conditional-field' => 'animation_enabled', 'data-conditional-value' => 'true']
|
|
||||||
);
|
|
||||||
|
|
||||||
$html .= '</div>';
|
|
||||||
$html .= '</div>';
|
|
||||||
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildPreviewSection(array $data): string
|
|
||||||
{
|
|
||||||
$html = '<div class="roi-form-section roi-preview-section">';
|
|
||||||
$html .= '<h3 class="roi-form-section-title">Vista Previa</h3>';
|
|
||||||
$html .= '<div class="roi-form-section-content">';
|
|
||||||
$html .= '<div id="roi-component-preview" class="border rounded p-3 bg-light">';
|
|
||||||
$html .= '<p class="text-muted">La vista previa se actualizará automáticamente al modificar los campos.</p>';
|
|
||||||
$html .= '</div>';
|
|
||||||
$html .= '</div>';
|
|
||||||
$html .= '</div>';
|
|
||||||
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildToggle(string $name, string $label, bool $value, string $componentId, string $description = ''): string
|
|
||||||
{
|
|
||||||
$fieldId = "roi_{$componentId}_{$name}";
|
|
||||||
$checked = $value ? 'checked' : '';
|
|
||||||
|
|
||||||
$html = '<div class="roi-form-field roi-form-field-toggle mb-3">';
|
|
||||||
$html .= '<div class="form-check form-switch">';
|
|
||||||
$html .= sprintf(
|
|
||||||
'<input type="checkbox" class="form-check-input" id="%s" name="roi_component[%s][%s]" value="1" %s>',
|
|
||||||
esc_attr($fieldId),
|
|
||||||
esc_attr($componentId),
|
|
||||||
esc_attr($name),
|
|
||||||
$checked
|
|
||||||
);
|
|
||||||
$html .= sprintf('<label class="form-check-label" for="%s">%s</label>', esc_attr($fieldId), esc_html($label));
|
|
||||||
$html .= '</div>';
|
|
||||||
if (!empty($description)) {
|
|
||||||
$html .= sprintf('<small class="form-text text-muted">%s</small>', esc_html($description));
|
|
||||||
}
|
|
||||||
$html .= '</div>';
|
|
||||||
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildTextField(string $name, string $label, string $value, string $componentId, string $description = '', string $placeholder = '', array $attrs = []): string
|
|
||||||
{
|
|
||||||
$fieldId = "roi_{$componentId}_{$name}";
|
|
||||||
|
|
||||||
$html = '<div class="roi-form-field roi-form-field-text mb-3">';
|
|
||||||
$html .= sprintf('<label for="%s" class="form-label">%s</label>', esc_attr($fieldId), esc_html($label));
|
|
||||||
|
|
||||||
$attrString = $this->buildAttributesString($attrs);
|
|
||||||
|
|
||||||
$html .= sprintf(
|
|
||||||
'<input type="text" class="form-control" id="%s" name="roi_component[%s][%s]" value="%s" placeholder="%s"%s>',
|
|
||||||
esc_attr($fieldId),
|
|
||||||
esc_attr($componentId),
|
|
||||||
esc_attr($name),
|
|
||||||
esc_attr($value),
|
|
||||||
esc_attr($placeholder),
|
|
||||||
$attrString
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!empty($description)) {
|
|
||||||
$html .= sprintf('<small class="form-text text-muted">%s</small>', esc_html($description));
|
|
||||||
}
|
|
||||||
$html .= '</div>';
|
|
||||||
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildTextArea(string $name, string $label, string $value, string $componentId, string $description = '', int $rows = 3, array $attrs = []): string
|
|
||||||
{
|
|
||||||
$fieldId = "roi_{$componentId}_{$name}";
|
|
||||||
|
|
||||||
$html = '<div class="roi-form-field roi-form-field-textarea mb-3">';
|
|
||||||
$html .= sprintf('<label for="%s" class="form-label">%s</label>', esc_attr($fieldId), esc_html($label));
|
|
||||||
|
|
||||||
$attrString = $this->buildAttributesString($attrs);
|
|
||||||
|
|
||||||
$html .= sprintf(
|
|
||||||
'<textarea class="form-control" id="%s" name="roi_component[%s][%s]" rows="%d"%s>%s</textarea>',
|
|
||||||
esc_attr($fieldId),
|
|
||||||
esc_attr($componentId),
|
|
||||||
esc_attr($name),
|
|
||||||
$rows,
|
|
||||||
$attrString,
|
|
||||||
esc_textarea($value)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!empty($description)) {
|
|
||||||
$html .= sprintf('<small class="form-text text-muted">%s</small>', esc_html($description));
|
|
||||||
}
|
|
||||||
$html .= '</div>';
|
|
||||||
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildSelect(string $name, string $label, string $value, array $options, string $componentId, string $description = '', array $attrs = []): string
|
|
||||||
{
|
|
||||||
$fieldId = "roi_{$componentId}_{$name}";
|
|
||||||
|
|
||||||
$html = '<div class="roi-form-field roi-form-field-select mb-3">';
|
|
||||||
$html .= sprintf('<label for="%s" class="form-label">%s</label>', esc_attr($fieldId), esc_html($label));
|
|
||||||
|
|
||||||
$attrString = $this->buildAttributesString($attrs);
|
|
||||||
|
|
||||||
$html .= sprintf(
|
|
||||||
'<select class="form-select" id="%s" name="roi_component[%s][%s]"%s>',
|
|
||||||
esc_attr($fieldId),
|
|
||||||
esc_attr($componentId),
|
|
||||||
esc_attr($name),
|
|
||||||
$attrString
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ($options as $optValue => $optLabel) {
|
|
||||||
$selected = ($value === $optValue) ? 'selected' : '';
|
|
||||||
$html .= sprintf(
|
|
||||||
'<option value="%s" %s>%s</option>',
|
|
||||||
esc_attr($optValue),
|
|
||||||
$selected,
|
|
||||||
esc_html($optLabel)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$html .= '</select>';
|
|
||||||
|
|
||||||
if (!empty($description)) {
|
|
||||||
$html .= sprintf('<small class="form-text text-muted">%s</small>', esc_html($description));
|
|
||||||
}
|
|
||||||
$html .= '</div>';
|
|
||||||
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildNumberField(string $name, string $label, $value, string $componentId, string $description = '', int $min = null, int $max = null, array $attrs = []): string
|
|
||||||
{
|
|
||||||
$fieldId = "roi_{$componentId}_{$name}";
|
|
||||||
|
|
||||||
$html = '<div class="roi-form-field roi-form-field-number mb-3">';
|
|
||||||
$html .= sprintf('<label for="%s" class="form-label">%s</label>', esc_attr($fieldId), esc_html($label));
|
|
||||||
|
|
||||||
$attrs['type'] = 'number';
|
|
||||||
if ($min !== null) {
|
|
||||||
$attrs['min'] = $min;
|
|
||||||
}
|
|
||||||
if ($max !== null) {
|
|
||||||
$attrs['max'] = $max;
|
|
||||||
}
|
|
||||||
|
|
||||||
$attrString = $this->buildAttributesString($attrs);
|
|
||||||
|
|
||||||
$html .= sprintf(
|
|
||||||
'<input class="form-control" id="%s" name="roi_component[%s][%s]" value="%s"%s>',
|
|
||||||
esc_attr($fieldId),
|
|
||||||
esc_attr($componentId),
|
|
||||||
esc_attr($name),
|
|
||||||
esc_attr($value),
|
|
||||||
$attrString
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!empty($description)) {
|
|
||||||
$html .= sprintf('<small class="form-text text-muted">%s</small>', esc_html($description));
|
|
||||||
}
|
|
||||||
$html .= '</div>';
|
|
||||||
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildUrlField(string $name, string $label, string $value, string $componentId, string $description = '', string $placeholder = '', array $attrs = []): string
|
|
||||||
{
|
|
||||||
$attrs['type'] = 'url';
|
|
||||||
return $this->buildTextField($name, $label, $value, $componentId, $description, $placeholder, $attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildColorField(string $name, string $label, string $value, string $componentId, string $description = ''): string
|
|
||||||
{
|
|
||||||
$fieldId = "roi_{$componentId}_{$name}";
|
|
||||||
|
|
||||||
$html = '<div class="roi-form-field roi-form-field-color mb-3">';
|
|
||||||
$html .= sprintf('<label for="%s" class="form-label">%s</label>', esc_attr($fieldId), esc_html($label));
|
|
||||||
$html .= '<div class="input-group">';
|
|
||||||
$html .= sprintf(
|
|
||||||
'<input type="color" class="form-control form-control-color" id="%s" name="roi_component[%s][%s]" value="%s">',
|
|
||||||
esc_attr($fieldId),
|
|
||||||
esc_attr($componentId),
|
|
||||||
esc_attr($name),
|
|
||||||
esc_attr($value)
|
|
||||||
);
|
|
||||||
$html .= sprintf(
|
|
||||||
'<input type="text" class="form-control" value="%s" readonly>',
|
|
||||||
esc_attr($value)
|
|
||||||
);
|
|
||||||
$html .= '</div>';
|
|
||||||
if (!empty($description)) {
|
|
||||||
$html .= sprintf('<small class="form-text text-muted">%s</small>', esc_html($description));
|
|
||||||
}
|
|
||||||
$html .= '</div>';
|
|
||||||
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildMediaField(string $name, string $label, string $value, string $componentId, string $description = '', array $attrs = []): string
|
|
||||||
{
|
|
||||||
$fieldId = "roi_{$componentId}_{$name}";
|
|
||||||
|
|
||||||
$html = '<div class="roi-form-field roi-form-field-media mb-3">';
|
|
||||||
$html .= sprintf('<label for="%s" class="form-label">%s</label>', esc_attr($fieldId), esc_html($label));
|
|
||||||
$html .= '<div class="input-group">';
|
|
||||||
|
|
||||||
$attrString = $this->buildAttributesString($attrs);
|
|
||||||
|
|
||||||
$html .= sprintf(
|
|
||||||
'<input type="text" class="form-control" id="%s" name="roi_component[%s][%s]" value="%s" readonly%s>',
|
|
||||||
esc_attr($fieldId),
|
|
||||||
esc_attr($componentId),
|
|
||||||
esc_attr($name),
|
|
||||||
esc_attr($value),
|
|
||||||
$attrString
|
|
||||||
);
|
|
||||||
$html .= sprintf(
|
|
||||||
'<button type="button" class="btn btn-primary roi-media-upload-btn" data-target="%s">Seleccionar</button>',
|
|
||||||
esc_attr($fieldId)
|
|
||||||
);
|
|
||||||
$html .= '</div>';
|
|
||||||
|
|
||||||
if (!empty($value)) {
|
|
||||||
$html .= sprintf('<div class="mt-2"><img src="%s" alt="Preview" style="max-width: 100px; height: auto;"></div>', esc_url($value));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($description)) {
|
|
||||||
$html .= sprintf('<small class="form-text text-muted">%s</small>', esc_html($description));
|
|
||||||
}
|
|
||||||
$html .= '</div>';
|
|
||||||
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildAttributesString(array $attrs): string
|
|
||||||
{
|
|
||||||
$attrString = '';
|
|
||||||
foreach ($attrs as $key => $value) {
|
|
||||||
$attrString .= sprintf(' %s="%s"', esc_attr($key), esc_attr($value));
|
|
||||||
}
|
|
||||||
return $attrString;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildFormScripts(string $componentId): string
|
|
||||||
{
|
|
||||||
return <<<SCRIPT
|
|
||||||
<script>
|
|
||||||
(function($) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
// Conditional logic
|
|
||||||
$('[data-conditional-field]').each(function() {
|
|
||||||
const field = $(this);
|
|
||||||
const targetFieldName = field.data('conditional-field');
|
|
||||||
const targetValue = field.data('conditional-value');
|
|
||||||
const targetField = $('[name*="[' + targetFieldName + ']"]');
|
|
||||||
|
|
||||||
function updateVisibility() {
|
|
||||||
let currentValue;
|
|
||||||
if (targetField.is(':checkbox')) {
|
|
||||||
currentValue = targetField.is(':checked') ? 'true' : 'false';
|
|
||||||
} else {
|
|
||||||
currentValue = targetField.val();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentValue === targetValue) {
|
|
||||||
field.closest('.roi-form-field').show();
|
|
||||||
} else {
|
|
||||||
field.closest('.roi-form-field').hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
targetField.on('change', updateVisibility);
|
|
||||||
updateVisibility();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Media upload
|
|
||||||
$('.roi-media-upload-btn').on('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const button = $(this);
|
|
||||||
const targetId = button.data('target');
|
|
||||||
const targetField = $('#' + targetId);
|
|
||||||
|
|
||||||
const mediaUploader = wp.media({
|
|
||||||
title: 'Seleccionar imagen',
|
|
||||||
button: { text: 'Usar esta imagen' },
|
|
||||||
multiple: false
|
|
||||||
});
|
|
||||||
|
|
||||||
mediaUploader.on('select', function() {
|
|
||||||
const attachment = mediaUploader.state().get('selection').first().toJSON();
|
|
||||||
targetField.val(attachment.url);
|
|
||||||
|
|
||||||
const preview = targetField.closest('.roi-form-field-media').find('img');
|
|
||||||
if (preview.length) {
|
|
||||||
preview.attr('src', attachment.url);
|
|
||||||
} else {
|
|
||||||
targetField.closest('.input-group').after('<div class="mt-2"><img src="' + attachment.url + '" alt="Preview" style="max-width: 100px; height: auto;"></div>');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mediaUploader.open();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Color picker sync
|
|
||||||
$('.form-control-color').on('change', function() {
|
|
||||||
$(this).next('input[type="text"]').val($(this).val());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Auto-update preview
|
|
||||||
$('.roi-form-field input, .roi-form-field select, .roi-form-field textarea').on('change keyup', function() {
|
|
||||||
updatePreview();
|
|
||||||
});
|
|
||||||
|
|
||||||
function updatePreview() {
|
|
||||||
// Aquí iría la lógica para actualizar la vista previa en tiempo real
|
|
||||||
console.log('Preview updated');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})(jQuery);
|
|
||||||
</script>
|
|
||||||
SCRIPT;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function supports(string $componentType): bool
|
|
||||||
{
|
|
||||||
return $componentType === 'top-notification-bar';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Part: CTA Box Sidebar
|
|
||||||
*
|
|
||||||
* Caja de llamada a la acción naranja en el sidebar
|
|
||||||
* Abre el modal de contacto al hacer clic
|
|
||||||
*
|
|
||||||
* @package ROI_Theme
|
|
||||||
* @since 1.0.0
|
|
||||||
*/
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- DEBUG: CTA Box Template Loaded -->
|
|
||||||
<!-- CTA Box Sidebar -->
|
|
||||||
<div class="cta-box-sidebar">
|
|
||||||
<h5 class="cta-box-title">¿Listo para potenciar tus proyectos?</h5>
|
|
||||||
<p class="cta-box-text">Accede a nuestra biblioteca completa de APUs y herramientas profesionales.</p>
|
|
||||||
<button class="btn btn-cta-box w-100" data-bs-toggle="modal" data-bs-target="#contactModal">
|
|
||||||
Solicitar Información
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<!-- DEBUG: CTA Box Template End -->
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Template Part: Table of Contents (TOC)
|
|
||||||
*
|
|
||||||
* Genera automáticamente TOC desde los H2 del post
|
|
||||||
* HTML exacto del template original
|
|
||||||
*
|
|
||||||
* @package ROI_Theme
|
|
||||||
* @since 1.0.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Solo mostrar TOC si estamos en single post
|
|
||||||
if (!is_single()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtener el contenido del post actual
|
|
||||||
global $post;
|
|
||||||
$post_content = $post->post_content;
|
|
||||||
|
|
||||||
// Aplicar filtros de WordPress al contenido
|
|
||||||
$post_content = apply_filters('the_content', $post_content);
|
|
||||||
|
|
||||||
// Buscar todos los H2 con ID en el contenido
|
|
||||||
preg_match_all('/<h2[^>]*id=["\']([^"\']*)["\'][^>]*>(.*?)<\/h2>/i', $post_content, $matches);
|
|
||||||
|
|
||||||
// Si no hay H2 con ID, no mostrar TOC
|
|
||||||
if (empty($matches[1])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generar el TOC con el HTML del template
|
|
||||||
?>
|
|
||||||
<div class="toc-container">
|
|
||||||
<h4>Tabla de Contenido</h4>
|
|
||||||
<ol class="list-unstyled toc-list">
|
|
||||||
<?php foreach ($matches[1] as $index => $id) : ?>
|
|
||||||
<?php $title = strip_tags($matches[2][$index]); ?>
|
|
||||||
<li><a href="#<?php echo esc_attr($id); ?>"><?php echo esc_html($title); ?></a></li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* CTA Box Sidebar Template
|
|
||||||
*
|
|
||||||
* Aparece debajo del TOC en single posts
|
|
||||||
*
|
|
||||||
* @package ROI_Theme
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!is_single()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="cta-box-sidebar mt-3">
|
|
||||||
<h5 class="cta-box-title">¿Listo para potenciar tus proyectos?</h5>
|
|
||||||
<p class="cta-box-text">Accede a nuestra biblioteca completa de APUs y herramientas profesionales.</p>
|
|
||||||
<button class="btn btn-cta-box w-100" data-bs-toggle="modal" data-bs-target="#contactModal">
|
|
||||||
<i class="bi bi-calendar-check me-2"></i>Solicitar Demo
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Modal de Contacto - Bootstrap 5
|
|
||||||
*
|
|
||||||
* Modal activado por botón "Let's Talk" y CTA Box Sidebar
|
|
||||||
*
|
|
||||||
* @package ROI_Theme
|
|
||||||
* @since 1.0.0
|
|
||||||
*/
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- Contact Modal -->
|
|
||||||
<div class="modal fade" id="contactModal" tabindex="-1" aria-labelledby="contactModalLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="contactModalLabel">
|
|
||||||
<i class="bi bi-envelope-fill me-2" style="color: #FF8600;"></i>
|
|
||||||
<?php esc_html_e( '¿Tienes alguna pregunta?', 'roi-theme' ); ?>
|
|
||||||
</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<?php esc_attr_e( 'Cerrar', 'roi-theme' ); ?>"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p class="mb-4">
|
|
||||||
<?php esc_html_e( 'Completa el formulario y nuestro equipo te responderá en menos de 24 horas.', 'roi-theme' ); ?>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form id="modalContactForm">
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="modalFullName" class="form-label"><?php esc_html_e( 'Nombre completo', 'roi-theme' ); ?> *</label>
|
|
||||||
<input type="text" class="form-control" id="modalFullName" name="fullName" required>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="modalCompany" class="form-label"><?php esc_html_e( 'Empresa', 'roi-theme' ); ?></label>
|
|
||||||
<input type="text" class="form-control" id="modalCompany" name="company">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="modalWhatsapp" class="form-label"><?php esc_html_e( 'WhatsApp', 'roi-theme' ); ?> *</label>
|
|
||||||
<input type="tel" class="form-control" id="modalWhatsapp" name="whatsapp" required>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="modalEmail" class="form-label"><?php esc_html_e( 'Correo electrónico', 'roi-theme' ); ?> *</label>
|
|
||||||
<input type="email" class="form-control" id="modalEmail" name="email" required>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<label for="modalComments" class="form-label"><?php esc_html_e( '¿En qué podemos ayudarte?', 'roi-theme' ); ?></label>
|
|
||||||
<textarea class="form-control" id="modalComments" name="comments" rows="4"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<button type="submit" class="btn btn-primary w-100">
|
|
||||||
<i class="bi bi-send-fill me-2"></i><?php esc_html_e( 'Enviar Mensaje', 'roi-theme' ); ?>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="modalFormMessage" class="col-12 mt-2 alert" style="display: none;"></div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Top Notification Bar Component
|
|
||||||
*
|
|
||||||
* Barra de notificaciones superior del sitio
|
|
||||||
*
|
|
||||||
* @package ROI_Theme
|
|
||||||
* @since 2.0.0
|
|
||||||
*/
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="top-notification-bar">
|
|
||||||
<div class="container">
|
|
||||||
<div class="d-flex align-items-center justify-content-center">
|
|
||||||
<i class="bi bi-megaphone-fill me-2"></i>
|
|
||||||
<span><strong>Nuevo:</strong> Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.</span>
|
|
||||||
<a href="#" class="ms-2 text-white text-decoration-underline">Ver Catálogo</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -50,7 +50,7 @@ require_once get_template_directory() . '/Inc/featured-image.php';
|
|||||||
require_once get_template_directory() . '/Inc/category-badge.php';
|
require_once get_template_directory() . '/Inc/category-badge.php';
|
||||||
require_once get_template_directory() . '/Inc/adsense-delay.php';
|
require_once get_template_directory() . '/Inc/adsense-delay.php';
|
||||||
require_once get_template_directory() . '/Inc/adsense-placement.php';
|
require_once get_template_directory() . '/Inc/adsense-placement.php';
|
||||||
require_once get_template_directory() . '/Inc/related-posts.php';
|
// ELIMINADO: Inc/related-posts.php (Plan 101 - usa RelatedPostRenderer)
|
||||||
// ELIMINADO: Inc/toc.php (FASE 6 - Clean Architecture: usa TableOfContentsRenderer)
|
// ELIMINADO: Inc/toc.php (FASE 6 - Clean Architecture: usa TableOfContentsRenderer)
|
||||||
require_once get_template_directory() . '/Inc/apu-tables.php';
|
require_once get_template_directory() . '/Inc/apu-tables.php';
|
||||||
require_once get_template_directory() . '/Inc/search-disable.php';
|
require_once get_template_directory() . '/Inc/search-disable.php';
|
||||||
|
|||||||
Reference in New Issue
Block a user