<?php
/**
* SCRIPT DE ANÁLISIS AUTOMÁTICO DE MÉTRICA ESPAÑOLA
*
* Este script analiza versos en español calculando sílabas gramaticales y métricas,
* considerando fenómenos poéticos como sinalefa, sinéresis, diéresis y ajustes por acentuación.
*
* @category NaturalLanguageProcessing
* @package Corpus
* @subpackage Preprocessing
* @author Jaume d'Urgell <jaume@durgell.com>
* @author Endika Tapia <endikatlg@gmail.com>
* @copyright Jaume d'Urgell 陈建军
* @license MIT
* @version 3.2.0
* @since 2025-11-04
* @link https://durgell.com/metrica/
* @see https://durgell.com/metrica/
*/
// =============================================================================
// CONFIGURACIÓN INICIAL Y MANEJO DE ERRORES
// =============================================================================
/**
* Configuración de cabeceras HTTP para correcta interpretación de caracteres
* Establece el tipo de contenido como HTML con codificación UTF-8
*/
header('Content-Type: text/html; charset=UTF-8');
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
//declare(strict_types=1);
/**
* Configuración de codificación interna para funciones de cadena multibyte
* Asegura el correcto manejo de caracteres especiales del español
*/
mb_internal_encoding("UTF-8");
mb_regex_encoding("UTF-8");
/**
* Variables globales para manejo de estado
*
* @var string $error Mensaje de error para mostrar al usuario
* @var string $versoOriginal Texto del verso ingresado por el usuario
* @var array $listaDeDieresis Almacena casos de diéresis detectados para presentación
*/
$error = "";
$versoOriginal = "";
$listaDeDieresis = [];
/**
* Configuración de visualización de errores (SOLO PARA DESARROLLO)
* En producción deberían desactivarse estas opciones
*/
// ini_set('display_errors', 1);
// ini_set('display_startup_errors', 1);
// error_reporting(E_ALL);
ini_set('display_errors', 0); // Esto es para despliegue en producción.
// =============================================================================
// PROCESAMIENTO DE ENTRADA DEL USUARIO
// =============================================================================
/**
* Procesa el formulario cuando se envía via POST
* Realiza validaciones básicas de entrada
*/
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Obtiene y limpia el verso del formulario
$versoOriginal = isset($_POST['verso']) ? trim((string)$_POST['verso']) : '';
// Validación: el verso no puede estar vacío
if ($versoOriginal === "") {
$error = "Por favor, escribe un verso (no puede quedar en blanco).";
}
// Validación: longitud máxima del verso
elseif (mb_strlen($versoOriginal, 'UTF-8') > 256) {
$error = "El verso no puede superar los 256 caracteres.";
}
// Si no hay errores, continúa el análisis en las secciones posteriores
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cálculo automático de Métrica Española</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Text&display=swap" rel="stylesheet">
<style>
/* =============================================================================
ESTILOS CSS PARA LA INTERFAZ WEB
============================================================================= */
/* Reset básico para márgenes y padding */
* {
box-sizing: border-box;
}
/* Estilos generales del cuerpo */
body {
font-family: Arial, sans-serif;
margin: 0 1em;
background: #ddd;
font-size: 16px;
line-height: 1.5;
}
/* Encabezado principal */
h1 {
font-family: "DM Serif Text", serif;
font-weight: 800;
font-style: normal;
font-size: 32px;
line-height: 1;
text-align: center;
margin: 16px 0px;
}
/* Contenedor del formulario */
.form-container {
display: flex;
flex-direction: column;
gap: 1em;
}
/* Campo de entrada de texto */
input {
width: 100%;
height: 50px;
font-size: 1em;
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
}
/* Contenedor de botones */
.botones-container {
display: flex;
gap: 1em;
width: 100%;
}
/* Estilos base para botones */
button {
border: none;
border-radius: 8px;
padding: 14px 0px;
font-size: 1em;
font-weight: 600;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.08, 0.52, 0.52, 1);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
flex: 1;
}
/* Botón de analizar - Estilos específicos */
.btn-analizar {
background: linear-gradient(135deg, #27ae60 0%, #219653 100%);
color: white;
}
/* Botón de ejemplo - Estilos específicos */
.btn-ejemplo {
background: linear-gradient(135deg, #1877F2 0%, #166FE5 100%);
color: white;
}
/* Botón de borrar - Estilos específicos */
.btn-borrar {
background: linear-gradient(135deg, #E74C3C 0%, #C0392B 100%);
color: white;
}
/* Efectos hover para botones */
.btn-analizar:hover {
background: linear-gradient(135deg, #219653 0%, #1e8449 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.1);
}
.btn-ejemplo:hover {
background: linear-gradient(135deg, #166FE5 0%, #1461C8 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.1);
}
.btn-borrar:hover {
background: linear-gradient(135deg, #C0392B 0%, #A93226 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Efectos active para botones */
.btn-analizar:active {
background: linear-gradient(135deg, #1e8449 0%, #196f3d 100%);
transform: translateY(0);
}
.btn-ejemplo:active {
background: linear-gradient(135deg, #1461C8 0%, #1257B4 100%);
transform: translateY(0);
}
.btn-borrar:active {
background: linear-gradient(135deg, #A93226 0%, #922B21 100%);
transform: translateY(0);
}
/* Pie de página */
.footer {
line-height: 1.4;
padding: 0 0 1em 0;
margin-top: 10px;
text-align: center;
color: #444;
font-weight: normal;
font-size: small;
}
/* Información de proceso */
.proceso {
background: #fff;
padding: 1px 1em;
border-radius: 8px;
box-shadow: 0 0 5px #ccc;
margin-top: 1em;
word-wrap: break-word;
overflow-wrap: break-word;
line-height: 1.4;
}
/* Área de resultados */
.resultado {
background: #fff;
padding: 12px 1em;
border-radius: 8px;
box-shadow: 0 0 5px #ccc;
margin-top: 1em;
word-wrap: break-word;
overflow-wrap: break-word;
line-height: 1.4;
}
/* Panel informativo */
.panel {
background: #fff;
padding: 1px 1em;
border-radius: 8px;
box-shadow: 0 0 5px #ccc;
margin-top: 1em;
word-wrap: break-word;
overflow-wrap: break-word;
line-height: 1.4;
}
/* Panel informativo */
.mit {
background: #fff;
padding: 1px 1em;
border-radius: 8px;
box-shadow: 0 0 5px #ccc;
margin-top: 1em;
word-wrap: break-word;
overflow-wrap: break-word;
line-height: 1.4;
}
.justificado {
text-align: justify;
hyphens: auto;
-webkit-hyphens: auto; /* para compatibilidad con Chrome/Safari */
-ms-hyphens: auto; /* para navegadores antiguos de Microsoft */
text-justify: inter-word;
}
.video{
width: 100%;
margin: 16px auto 0px;
border-radius: 16px;
display: block;
border: solid 2px #000;
}
/* Espacio de presentación entre sílabas */
.gap {
color: #888;
margin: 0 2px;
}
/* Manejo de márgenes en el área de resultados */
.resultado > *:first-child {
margin-top: 0;
}
.resultado > *:last-child {
margin-bottom: 0;
}
/* Etiquetas destacadas */
.etiqueta {
font-weight: bold;
}
/* Texto monoespaciado para silabeos */
.monoespaciado {
font-family: "SFMono-Regular", Menlo, Consolas, monospace;
display: inline;
max-width: 100%;
overflow-x: auto;
}
/* Línea separadora */
hr {
border: none;
border-top: 1px dotted #888;
}
/* Estilo para código QR */
.qr_code {
width: 80px;
float: left;
padding: 0 0 8px 0;
vertical-align: middle;
}
/* Marcar palabras sin con pronunciación desconocida */
.not-found {
color: #b00;
background: rgba(255, 200, 200, 0.3);
padding: 0 .15em;
border-radius: 3px;
font-weight: 600;
}
/* =============================================================================
MEDIA QUERIES PARA DISPOSITIVOS DE ESCRITORIO
============================================================================= */
@media (min-width: 768px) {
body {
margin: 0;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.video {
width: 50%;
border: solid 1px #000;
}
.qr_code {
width: 100px;
padding: 0 0 12px 0;
}
/* Encabezado principal */
h1 {
font-family: "DM Serif Text", serif;
font-weight: 400;
font-style: normal;
font-size: 45px;
line-height: 1;
text-align: center;
margin: 16px 0px;
}
.form-container {
flex-direction: row;
align-items: center;
flex-direction: column;
gap: 1em;
}
.salto {
width: 50%;
}
input {
height: 40px;
font-size: 1.05em;
}
.proceso {
width: 50%;
margin: 16px auto 0px;
gap: 1em;
}
.botones-container {
width: 50%;
flex-shrink: 0;
gap: 1em;
}
button {
width: auto;
padding: 10px 28px;
white-space: nowrap;
}
}
</style>
</head>
<body>
<!-- =============================================================================
ESTRUCTURA HTML DE LA INTERFAZ
============================================================================= -->
<video class="video" poster="cabecera.webp" controls loop>
<source src="carmen-de-burgos.mp4" type="video/mp4">
Tu navegador no soporta el elemento de vídeo.
</video>
<h1>
<a href="https://durgell.com/metrica/" style="display: block; text-decoration: none; color: inherit;">
Cálculo automático<br/>de Métrica Española
</a>
</h1>
<!-- Mostrar mensajes de error si existen -->
<?php if ($error): ?>
<div style="color: #c00; background: #fff0f0; border: 1px solid #c66; padding: 1em; margin-bottom: 1em; border-radius: 8px;">
<?php echo htmlspecialchars($error, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>
</div>
<?php endif; ?>
<!-- Formulario principal -->
<form method="post" if="formulario" autocomplete="off">
<div class="form-container">
<input type="text"
name="verso"
id="verso"
class="salto"
required
autofocus
maxlength="256"
placeholder="Escribe un verso aquí."
value="<?= isset($_POST['verso']) ? htmlspecialchars($_POST['verso'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') : '' ?>"
autocomplete="off">
<div class="botones-container">
<button type="submit" class="btn-analizar" id="btn-analizar">Analizar</button>
<button type="button" class="btn-ejemplo" id="btn-ejemplo">Ejemplos</button>
<button type="button" class="btn-borrar" id="btn-borrar">Borrar</button>
</div>
</div>
</form>
<?php
// =============================================================================
// ESTABLECIMIENTO DE LA CONEXIÓN A LA BASE DE DATOS
// =============================================================================
// Módulo externo, por razones de seguridad.
//
// Usamos @include_once en lugar de require_once para que no yerre si el archivo
// no está, sin embargo en tal caso deberían insertarse aquí los parámetros para
// la configuración del acceso a la base de datos.
//
@include_once 'datos-de-conexion.php';
/** Contenido del script externo: datos_de_conexion.php
*
* $host = 'localhost';
* $dbname = 'nombre_de_la_base_de_datos';
* $user = 'nombre_del_usuario_autorizado';
* $pass = 'contraseña';
*/
// Crear conexión PDO
try {
$pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);
} catch (PDOException $e) {
die("Error de conexión a la base de datos: " . $e->getMessage());
}
// =============================================================================
// PRE-CARGA DE DATOS
// =============================================================================
/**
* Versos de ejemplo, para realizar pruebas al azar.
*/
//
// Usamos @include_once en lugar de require_once para que no yerre si el archivo
// no está. También comprobamos si existe la función versoAleatorio, para que el
// sistema no genere un error en caso de que dicho fichero no esté. Así se mantiene
// la posibilidad de utilizar este script de forma independiente.
//
@include_once 'versos-de-ejemplo.php';
if (!function_exists('versoAleatorio')) {
function versoAleatorio() {
$tablaDeVersosDeEjemplo = [
"En el río azul confluïa entre sueños."
];
return $tablaDeVersosDeEjemplo[array_rand($tablaDeVersosDeEjemplo)];
}
}
// =============================================================================
// FUNCIONES DE ANÁLISIS LINGÜÍSTICO
// =============================================================================
/**
* Limpia una palabra eliminando signos de puntuación
*
* @param string $palabra La palabra a limpiar
* @return string Palabra sin signos de puntuación, manteniendo letras y caracteres especiales españoles
*/
function limpiarPalabra($palabra) {
return preg_replace('/[^\p{L}áéíóúäëïöüÿÁÉÍÓÚÜÑñ]/u', '', $palabra);
}
/**
* Determina si un carácter en una posición específica es vocal fonológica
*
* Considera reglas especiales del español como:
* - 'y' al final de palabra es vocal
* - 'u' muda después de 'q' o 'g'
* - Vocales con diéresis
*
* @param string $palabraMin Palabra en minúsculas
* @param int $pos Posición del carácter a evaluar
* @return bool True si es vocal fonológica, false en caso contrario
*/
function esVocalFonologica($palabraMin, $pos) {
$len = mb_strlen($palabraMin, 'UTF-8');
$c = mb_substr($palabraMin, $pos, 1, 'UTF-8');
$prev = ($pos > 0) ? mb_substr($palabraMin, $pos-1, 1, 'UTF-8') : '';
$next = ($pos < $len-1) ? mb_substr($palabraMin, $pos+1, 1, 'UTF-8') : '';
// 'y' al final de palabra se considera vocal
if ($c === 'y') return ($pos === $len-1);
// Las vocales con diéresis siempre son vocales fonológicas
if ($c === 'ü' || $c === 'ï') return true;
// 'u' muda después de 'q' o 'g' seguida de vocal
if ($c === 'u' && ($prev === 'q' || $prev === 'g') && $next !== '') {
if (preg_match('/[eéií]/u', $next)) return false;
}
// Cualquier otra vocal
return (bool)preg_match('/[aeiouáéíóú]/u', $c);
}
/**
* Divide una palabra en sílabas aplicando las reglas del español
*
* @param string $palabra Palabra a silabear
* @return array Array de sílabas
*/
function silabearPalabra($palabra) {
// Reemplazar dígrafos con caracteres especiales temporales
$p = $palabra;
$p = preg_replace('/ch/iu', '§', $p); // ch -> §
$p = preg_replace('/ll/iu', 'Ł', $p); // ll -> Ł
$p = preg_replace('/rr/iu', 'Ŕ', $p); // rr -> Ŕ
$p = preg_replace('/qu([ieíé])/iu', 'Q$1', $p); // que, qui -> Qe, Qi
$p = preg_replace('/gu([ieíé])/iu', 'G$1', $p); // gue, gui -> Ge, Gi
$p = preg_replace('/gü([ieíé])/iu', 'Ğu$1', $p); // güe, güi -> Ğue, Ğui
// División silábica básica: separar después de cada vocal
$sil = preg_split(
'/(?<=[aeiouáéíóúüäëïöÿAEIOUÁÉÍÓÚÜÄËÏÖŸ])(?=[^aeiouáéíóúüäëïöÿAEIOUÁÉÍÓÚÜÄËÏÖŸ])/u',
$p
);
// Aplicar correcciones sucesivas
$sil = corregirPrefijosIniciales($sil);
$sil = corregirInicioSConsonante($sil);
$sil = corregirInicioCC($sil);
$sil = dividirAtaqueInterno($sil);
$sil = separarHiatos($sil);
$sil = reabsorberConsonanteSuelta($sil);
$sil = reabsorberSFinalPlural($sil);
// Restaurar los dígrafos originales
$sil = array_map(function($s){
$s = str_replace('§','ch',$s);
$s = str_replace('Ł','ll',$s);
$s = str_replace('Ŕ','rr',$s);
$s = str_replace('Q','qu',$s);
$s = str_replace('G','gu',$s);
$s = str_replace('Ğu','gü',$s);
return $s;
}, $sil);
return array_values(array_filter($sil, fn($s) => $s !== ''));
}
/**
* Separa hiatos en las sílabas
*
* Los hiatos ocurren cuando dos vocales que normalmente formarían diptongo
* se pronuncian en sílabas separadas
*
* @param array $silabas Array de sílabas a procesar
* @return array Sílabas con hiatos separados
*/
function separarHiatos($silabas) {
$res = [];
foreach ($silabas as $s) {
// Solo procesar sílabas con dos o más vocales consecutivas
if (preg_match('/[aeiouáéíóúäëïöüÿ]{2,}/iu', $s)) {
$buffer = '';
$len = mb_strlen($s,'UTF-8');
for ($i=0; $i<$len; $i++) {
$ch = mb_substr($s,$i,1,'UTF-8');
$buffer .= $ch;
if ($i<$len-1) {
$next = mb_substr($s,$i+1,1,'UTF-8');
// Si hay hiato entre caracteres actual y siguiente, separar
if (esHiato($ch,$next)) {
$res[] = $buffer;
$buffer = '';
}
}
}
if ($buffer!=='') $res[] = $buffer;
} else {
$res[] = $s;
}
}
return $res;
}
/**
* Determina si dos vocales forman hiato
*
* @param string $v1 Primera vocal
* @param string $v2 Segunda vocal
* @return bool True si forman hiato, false si forman diptongo
*/
function esHiato($v1,$v2) {
$abiertas = ['a','á','e','é','o','ó'];
$cerradas = ['i','í','u','ú','ü'];
$v1l = mb_strtolower($v1,'UTF-8');
$v2l = mb_strtolower($v2,'UTF-8');
// Dos vocales abiertas siempre forman hiato
if (in_array($v1l,$abiertas) && in_array($v2l,$abiertas)) return true;
// Vocal cerrada tónica seguida de vocal abierta
if (in_array($v1l,['í','ú']) && in_array($v2l,$abiertas)) return true;
// Vocal abierta seguida de vocal cerrada tónica
if (in_array($v1l,$abiertas) && in_array($v2l,['í','ú'])) return true;
return false;
}
/**
* Corrige prefijos iniciales como "in-" + consonante
*
* @param array $silabas Array de sílabas
* @return array Sílabas corregidas
*/
function corregirPrefijosIniciales($silabas) {
if (count($silabas) < 2) return $silabas;
$primera = $silabas[0];
$segunda = $silabas[1];
// Prefijos como "in-" seguidos de consonante nasal o líquida
if (preg_match('/^(?i)i$/u', $primera) || preg_match('/^(?i)í$/u', $primera)) {
if (preg_match('/^[nmrNMR][^aeiouáéíóúüäëïöÿAEIOUÁÉÍÓÚÄËÏÖÜŸ]/u', $segunda)) {
$c = mb_substr($segunda, 0, 1, 'UTF-8');
$silabas[0] .= $c;
$silabas[1] = mb_substr($segunda, 1, null, 'UTF-8');
if ($silabas[1] === '') {
array_splice($silabas, 1, 1);
}
}
}
return $silabas;
}
/**
* Corrige la 's' seguida de consonante al inicio de sílaba
*
* @param array $silabas Array de sílabas
* @return array Sílabas corregidas
*/
function corregirInicioSConsonante($silabas) {
$res = [];
for ($i=0; $i<count($silabas); $i++) {
$s = $silabas[$i];
// Si la sílaba empieza con 's' + consonante y no es la primera sílaba
if ($i>0 && preg_match('/^[sS][bcdfghjklmnñpqrtvwxyz]/u', $s)) {
$res[count($res)-1] .= mb_substr($s,0,1,'UTF-8');
$resto = mb_substr($s,1,null,'UTF-8');
if ($resto !== '') $res[] = $resto;
} else {
$res[] = $s;
}
}
return $res;
}
/**
* Corrige grupos de dos consonantes al inicio de sílaba
*
* @param array $silabas Array de sílabas
* @return array Sílabas corregidas
*/
function corregirInicioCC($silabas) {
// Grupos consonánticos permitidos en español
$gruposPermitidos = ['br','bl','cr','cl','dr','fl','fr','gr','gl','pr','pl','tr','tl','ch',
'Br','Bl','Cr','Cl','Dr','Fl','Fr','Gr','Gl','Pr','Pl','Tr','Tl','Ch',
'BR','BL','CR','CL','DR','FL','FR','GR','GL','PR','PL','TR','TL','CH',
'bR','bL','cR','cL','dR','fL','fR','gR','gL','pR','pL','tR','tL','cH'];
$resultado = [];
foreach ($silabas as $s) {
// Si la sílaba empieza con dos consonantes
if (preg_match('/^[^aeiouáéíóúüäëïöÿ]{2}/iu', $s)) {
$grupo = mb_substr($s, 0, 2, 'UTF-8');
// Si no es un grupo permitido, separar las consonantes
if (!in_array($grupo, $gruposPermitidos)) {
$resultado[] = mb_substr($s, 0, 1, 'UTF-8');
$resultado[] = mb_substr($s, 1, null, 'UTF-8');
continue;
}
}
$resultado[] = $s;
}
return $resultado;
}
/**
* Divide sílabas con ataque interno complejo
*
* @param array $silabas Array de sílabas
* @return array Sílabas divididas
*/
function dividirAtaqueInterno($silabas) {
$res = [];
foreach ($silabas as $s) {
$len = mb_strlen($s,'UTF-8');
$cut = -1;
// Buscar punto de división después de vocal
for ($i=0; $i<$len-1; $i++) {
$ch = mb_substr($s,$i,1,'UTF-8');
if (preg_match('/[aeiouáéíóúäëïöüÿ]/iu',$ch)) {
$rest = mb_substr($s,$i+1,null,'UTF-8');
// Si lo que sigue es un grupo consonántico que puede iniciar sílaba
if (preg_match('/^(Ŕ|r|l|§|[bcdfghjklmnñpqrtvwxyz][rl])/iu',$rest)) {
$cut = $i+1;
break;
}
}
}
// Aplicar división si se encontró punto de corte
if ($cut>0) {
$res[] = mb_substr($s,0,$cut,'UTF-8');
$res[] = mb_substr($s,$cut,null,'UTF-8');
} else {
$res[] = $s;
}
}
return $res;
}
/**
* Reabsorbe consonantes sueltas en la sílaba anterior
*
* @param array $silabas Array de sílabas
* @return array Sílabas corregidas
*/
function reabsorberConsonanteSuelta($silabas) {
$i=0;
while ($i<count($silabas)) {
$s=$silabas[$i];
// Si la sílaba es una sola consonante
if (mb_strlen($s,'UTF-8')===1 && preg_match('/[bcdfghjklmnñpqrtvwxyz]/iu',$s)) {
if ($i>0) {
// Unir a la sílaba anterior
$silabas[$i-1].=$s;
array_splice($silabas,$i,1);
continue;
}
}
$i++;
}
return $silabas;
}
/**
* Reabsorbe la 's' final de plural en la sílaba anterior
*
* @param array $silabas Array de sílabas
* @return array Sílabas corregidas
*/
function reabsorberSFinalPlural($silabas) {
$n=count($silabas);
if ($n>=2) {
$last=$silabas[$n-1];
// Si la última sílaba es solo 's' (plural)
if ($last==='s'||$last==='S') {
$silabas[$n-2].=$last;
array_pop($silabas);
}
}
return $silabas;
}
/**
* Genera el silabeo gramatical completo de un verso
*
* @param string $verso El verso a analizar
* @return string Silabeo gramatical con separadores '/'
*/
function versoSilabasGramaticales($verso) {
$palabras = preg_split('/\s+/', trim($verso));
$silabas = [];
foreach ($palabras as $p) {
$p = limpiarPalabra($p);
if ($p === '') continue;
$sils = silabearPalabra($p);
foreach ($sils as $s) {
if ($s !== '') $silabas[] = $s;
}
}
return implode('/', $silabas);
}
/**
* Cuenta el número total de sílabas gramaticales en un verso
*
* @param string $verso El verso a analizar
* @return int Número de sílabas gramaticales
*/
function contarSilabasGramaticales($verso) {
$palabras = preg_split('/\s+/', trim($verso));
$total = 0;
foreach ($palabras as $p) {
$p = limpiarPalabra($p);
if ($p === '') continue;
$sils = silabearPalabra($p);
foreach ($sils as $s) {
if ($s !== '') $total++;
}
}
return $total;
}
/**
* Genera el silabeo métrico considerando fenómenos poéticos
*
* @param string $verso El verso a analizar
* @return string Silabeo métrico con marcadores especiales
*/
function versoSilabasMetricas($verso) {
$palabras = preg_split('/\s+/', trim($verso));
$tokens = [];
for ($i = 0; $i < count($palabras); $i++) {
$p = limpiarPalabra($palabras[$i]);
if ($p === '') continue;
$silabasPalabra = silabearPalabra($p);
if (empty($silabasPalabra)) $silabasPalabra = [$p];
// Aplicar sinéresis (unión de vocales en diptongo)
$silabasPalabra = aplicarSineresis($silabasPalabra);
foreach ($silabasPalabra as $s) $tokens[] = $s;
// Verificar sinalefa entre palabras
if ($i < count($palabras) - 1) {
$a = limpiarPalabra($palabras[$i]);
$b = limpiarPalabra($palabras[$i+1]);
if ($a !== '' && $b !== '') {
$ult = ultimoCharFonetico($a);
$pri = primerCharFonetico($b);
// Tratar 'y' como 'i' para efectos de sinalefa
if (mb_strtolower($a, 'UTF-8') === 'y') $ult = 'i';
if (mb_strtolower($b, 'UTF-8') === 'y') $pri = 'i';
// Si hay vocal final y vocal inicial, posible sinalefa
if (preg_match('/[aeiouáéíóúäëïöüÿ]/u', $ult) && preg_match('/[aeiouáéíóúäëïöüÿ]/u', $pri)) {
// No hay sinalefa si hay signo de puntuación
if (preg_match('/[.,;:!?]$/u', $palabras[$i])) {
$tokens[] = '__SEP__';
} else {
$tokens[] = '__SINALEFA__';
}
} else {
$tokens[] = '__SEP__';
}
}
}
}
// Construir el resultado final con los separadores apropiados
$res = '';
$needSep = false;
for ($i = 0; $i < count($tokens); $i++) {
$t = $tokens[$i];
if ($t === '__SEP__') {
$needSep = '/';
continue;
}
if ($t === '__SINALEFA__') {
$needSep = '‿';
continue;
}
if ($res !== '' && $needSep !== false) {
$res .= $needSep;
}
$res .= $t;
$needSep = '/';
}
return $res;
}
/**
* Obtiene el último carácter fonético de una palabra
*
* @param string $w Palabra a analizar
* @return string Último carácter fonético (ignorando puntuación)
*/
function ultimoCharFonetico($w) {
$w = trim($w);
$len = mb_strlen($w, 'UTF-8');
for ($i = $len - 1; $i >= 0; $i--) {
$ch = mb_substr($w, $i, 1, 'UTF-8');
if (preg_match('/[a-záéíóúäëïöüÿñ]/iu', $ch)) return mb_strtolower($ch, 'UTF-8');
}
return '';
}
/**
* Obtiene el primer carácter fonético de una palabra
*
* @param string $w Palabra a analizar
* @return string Primer carácter fonético (ignorando puntuación y 'h' muda)
*/
function primerCharFonetico($w) {
$w = trim($w);
$len = mb_strlen($w, 'UTF-8');
$j = 0;
// Saltar caracteres no fonéticos iniciales
while ($j < $len) {
$ch = mb_substr($w, $j, 1, 'UTF-8');
if (preg_match('/[a-záéíóúäëïöüÿñ]/iu', $ch)) break;
$j++;
}
if ($j >= $len) return '';
$ch = mb_substr($w, $j, 1, 'UTF-8');
// Si es 'h' muda, tomar el siguiente carácter
if (mb_strtolower($ch, 'UTF-8') === 'h' && $j + 1 < $len) {
$next = mb_substr($w, $j + 1, 1, 'UTF-8');
if (preg_match('/[a-záéíóúäëïöüÿñ]/iu', $next)) return mb_strtolower($next, 'UTF-8');
}
return mb_strtolower($ch, 'UTF-8');
}
/**
* Identifica núcleos vocálicos en una palabra con sus posiciones
*
* @param string $p Palabra a analizar
* @return array Array de núcleos con posiciones inicio y fin
*/
function nucleosVocalicosConPos($p) {
$n = mb_strlen($p, 'UTF-8');
$nucleos = [];
$i = 0;
while ($i < $n) {
$ch = mb_substr($p, $i, 1, 'UTF-8');
if (!esVocal($ch)) {
$i++;
continue;
}
$ini = $i;
$j = $i + 1;
// Agrupar vocales consecutivas que formen diptongo
while ($j < $n) {
$v1 = mb_substr($p, $j - 1, 1, 'UTF-8');
$v2 = mb_substr($p, $j, 1, 'UTF-8');
if (!esVocal($v2)) break;
if (esHiatoEntre($v1, $v2)) {
$nucleos[] = ['ini' => $ini, 'fin' => $j - 1];
$ini = $j;
}
$j++;
}
$nucleos[] = ['ini' => $ini, 'fin' => $j - 1];
$i = $j;
}
return $nucleos;
}
/**
* Determina si un carácter es vocal
*
* @param string $c Carácter a evaluar
* @return bool True si es vocal
*/
function esVocal($c) {
$c = mb_strtolower($c, 'UTF-8');
return in_array($c, ['a','e','i','o','u','á','é','í','ó','ú','ü'], true);
}
/**
* Determina si una vocal es abierta
*
* @param string $c Carácter a evaluar
* @return bool True si es vocal abierta
*/
function esVocalAbierta($c) {
$c = mb_strtolower($c, 'UTF-8');
return in_array($c, ['a','e','o','á','é','ó'], true);
}
/**
* Determina si una vocal es cerrada tónica
*
* @param string $c Carácter a evaluar
* @return bool True si es vocal cerrada tónica
*/
function esCerradaTonica($c) {
$c = mb_strtolower($c, 'UTF-8');
return in_array($c, ['í','ú'], true);
}
/**
* Determina si dos vocales forman hiato
*
* @param string $v1 Primera vocal
* @param string $v2 Segunda vocal
* @return bool True si forman hiato
*/
function esHiatoEntre($v1, $v2) {
$v1 = mb_strtolower($v1, 'UTF-8');
$v2 = mb_strtolower($v2, 'UTF-8');
// Dos vocales abiertas
if (esVocalAbierta($v1) && esVocalAbierta($v2)) return true;
// Vocal cerrada tónica + vocal abierta
if (esCerradaTonica($v1) && esVocalAbierta($v2)) return true;
// Vocal abierta + vocal cerrada tónica
if (esVocalAbierta($v1) && esCerradaTonica($v2)) return true;
return false;
}
/**
* Aplica sinéresis (unión de vocales en diptongo) a las sílabas
*
* @param array $silabas Array de sílabas
* @return array Sílabas con sinéresis aplicada
*/
function aplicarSineresis($silabas) {
$resultado = [];
foreach ($silabas as $s) {
if (!empty($resultado)) {
$ultima = $resultado[count($resultado)-1];
$ultChar = mb_substr($ultima, -1, 1, 'UTF-8');
$len = mb_strlen($s, 'UTF-8');
$j = 0;
// Saltar 'h' muda inicial
if ($len > 1 && mb_strtolower(mb_substr($s, 0, 1, 'UTF-8')) === 'h') {
$j = 1;
}
$priChar = mb_substr($s, $j, 1, 'UTF-8');
$ultCharLower = mb_strtolower($ultChar, 'UTF-8');
$priCharLower = mb_strtolower($priChar, 'UTF-8');
$vocalesAbiertas = ['a','á','e','é','o','ó'];
$vocalesCerradasTonica = ['í','ú'];
// Condiciones para sinéresis
if (preg_match('/[aeiouáéíóúü]$/iu', $ultima) &&
preg_match('/^[aeiouáéíóúü]/iu', $priChar)) {
// Vocal abierta + vocal abierta
if (in_array($ultCharLower, $vocalesAbiertas, true) &&
in_array($priCharLower, $vocalesAbiertas, true)) {
$resultado[count($resultado)-1] = $ultima . '⁔' . $s;
continue;
}
// Vocal cerrada tónica + vocal abierta
if (in_array($ultCharLower, $vocalesCerradasTonica, true) &&
in_array($priCharLower, $vocalesAbiertas, true)) {
$resultado[count($resultado)-1] = $ultima . '⁔' . $s;
continue;
}
// Vocal abierta + vocal cerrada tónica
if (in_array($ultCharLower, $vocalesAbiertas, true) &&
in_array($priCharLower, $vocalesCerradasTonica, true)) {
$resultado[count($resultado)-1] = $ultima . '⁔' . $s;
continue;
}
}
}
$resultado[] = $s;
}
return $resultado;
}
/**
* Encuentra el índice del núcleo tónico mediante tildes
*
* @param string $p Palabra a analizar
* @return int Índice del núcleo tónico, -1 si no se encuentra
*/
function indiceNucleoTonicoPorTilde($p) {
$nucleos = nucleosVocalicosConPos($p);
$n = mb_strlen($p, 'UTF-8');
// Buscar caracteres con tilde
for ($i = 0; $i < $n; $i++) {
$ch = mb_substr($p, $i, 1, 'UTF-8');
if (preg_match('/[áéíóúÁÉÍÓÚ]/u', $ch)) {
foreach ($nucleos as $idx => $r) {
if ($i >= $r['ini'] && $i <= $r['fin']) {
return $idx;
}
}
}
}
return -1;
}
/**
* Determina el tipo de palabra (aguda/llana) cuando no hay tilde
*
* @param string $palabra Palabra a analizar
* @return string 'llana' o 'aguda'
*/
function tipoSinTilde($palabra) {
return preg_match('/[aeiouáéíóúnsy]$/u', $palabra) ? 'llana' : 'aguda';
}
/**
* Determina el tipo de palabra final (aguda, llana, esdrújula)
*
* @param string $palabra Palabra a analizar
* @return string Tipo de palabra: 'aguda', 'llana', 'esdrújula' o 'sobreesdrújula'
*/
function tipoPalabraFinal($palabra) {
$p = mb_strtolower(limpiarPalabra($palabra), 'UTF-8');
$nucleos = nucleosVocalicosConPos($p);
$num = count($nucleos);
// Palabras monosílabas son agudas
if ($num <= 1) {
return 'aguda';
}
// Buscar tilde para determinar sílaba tónica
$idxTilde = indiceNucleoTonicoPorTilde($p);
if ($idxTilde !== -1) {
$desdeFinal = $num - $idxTilde;
if ($desdeFinal === 1) return 'aguda';
if ($desdeFinal === 2) return 'llana';
if ($desdeFinal === 3) return 'esdrújula';
return 'sobreesdrújula';
}
// Sin tilde, aplicar reglas por terminación
return tipoSinTilde($p);
}
/**
* Calcula el ajuste métrico final según el tipo de palabra final
*
* @param string $verso El verso completo
* @return int Ajuste: +1 (aguda), 0 (llana), -1 (esdrújula/sobreesdrújula)
*/
function ajusteFinal($verso) {
$palabras = preg_split('/\s+/', trim($verso));
$ultima = limpiarPalabra(end($palabras));
$tipo = tipoPalabraFinal($ultima);
if ($tipo === 'aguda') return 1;
if ($tipo === 'esdrújula' || $tipo === 'sobreesdrújula') return -1;
return 0;
}
/**
* Cuenta el número total de sílabas métricas en un verso
*
* @param string $verso El verso a analizar
* @return int Número de sílabas métricas
*/
function contarSilabasMetricas($verso) {
$silabeo = versoSilabasMetricas($verso);
if (!is_string($silabeo) || trim($silabeo) === '') {
return 0;
}
$partes = preg_split('/[\/]/u', $silabeo, -1, PREG_SPLIT_NO_EMPTY);
return is_array($partes) ? count($partes) : 0;
}
/**
* Genera mensaje descriptivo para variaciones métricas
*
* @param int $valor Valor del ajuste (-1, 0, 1)
* @return string Mensaje descriptivo
*/
function mensajeVariacion(int $valor): string {
return match ($valor) {
-1 => "se le resta una sílaba métrica",
0 => "el número de sílabas métricas no varía",
1 => "se le añade una sílaba métrica",
default => "valor no esperado",
};
}
// =============================================================================
// MANEJO DE DIÉRESIS MÉTRICA
// =============================================================================
/**
* Lista de palabras con diéresis ortográfica (no suman sílabas adicionales)
*/
$palabras_con_dieresis = [
'aconcagüino','adagüe','agüe','agüé','agüen','agüera','agüeran','agüeras','agüere','agüeren','agüeres','agüero','agüeros','agüío','agüista','agüita','agüite','agüizote','alengüe','alengüé','alengüéis','alengüemos','alengüen','alengües','ambigüedad','ambigüedades','amortigüe','angüejo','antigüedad','antigüedades','antigüeño','apacigüe','apacigüé','apacigüéis','apacigüemos','apacigüen','apacigües','apirgüinarse','aragüeño','aragüirá','argüe','argüendera','argüendero','argüí','argüía','argüid','argüidor','argüir','argüís','argüitivo','atestigüe','atestigüé','atestigüéis','atestigüemos','atestigüen','atestigües','avergüence','avergüencen','avergüences','avergüenza','avergüenzan','avergüenzas','avergüenzo','averigüe','averigüé','averigüéis','averigüemos','averigüen','averigües','averigüetas','bilingüe','bilingües','bilingüismo','bilingüismos','camagüe','camagüeyano','camagüira','cangüeso','cangüesos','chagüite','changüí','chigüil','chigüín','chiquigüite','chirigüe','cigüeña','cigüeñal','cigüeñales','cigüeñas','cigüeñato','cigüeño','cigüeños','cigüeñuela','cigüeñuelas','cigüete','colchagüino','coligüe','cologüina','comayagüense','contigüidad','corregüela','curamagüey','degüella','degüellan','degüellas','degüelle','degüellen','degüelles','degüello','desagüe','desagüé','desagüéis','desagüemos','desagüen','desagües','deslengüe','deslengüé','deslengüéis','deslengüemos','deslengüen','deslengües','desvergüenza','desvergüenzas','empigüela','empigüelaba','empigüelabais','empigüelábamos','empigüelaban','empigüelabas','empigüelad','empigüelada','empigüeladas','empigüelado','empigüelados','empigüeláis','empigüelamos','empigüelan','empigüelando','empigüelar','empigüelara','empigüelará','empigüelarais','empigüeláramos','empigüelaran','empigüelarán','empigüelaras','empigüelarás','empigüelare','empigüelaré','empigüelareis','empigüelaréis','empigüelaremos','empigüeláremos','empigüelaren','empigüelares','empigüelaría','empigüelaríais','empigüelaríamos','empigüelarían','empigüelarías','empigüelaron','empigüelas','empigüelase','empigüelaseis','empigüelásemos','empigüelasen','empigüelases','empigüelaste','empigüelasteis','empigüele','empigüelé','empigüeléis','empigüelemos','empigüelen','empigüeles','empigüelo','empigüeló','enagüetas','enagüillas','engüera','engüeran','engüerar','engüeras','engüere','engüeren','engüeres','engüero','enjagüe','enjagües','etnolingüística','exangüe','exangües','exigüidad','extralingüístico','fagüeño','fagüeños','fragüe','fragüé','fragüéis','fragüemos','fragüen','fragües','fragüín','gargüero','gregüescos','guargüero','güecho','güechos','güegüecho','güeldo','güeldrés','güelfa','güelfas','güelfo','güelfos','güeña','güeñas','güera','güérmeces','güero','güeros','güey','güila','güillín','güillines','güilota','güimba','güin','güincha','güinche','güines','güipil','güira','güiras','güirila','güirís','güiro','güisaro','güisquería','güisqui','güisquil','güito','halagüeña','halagüeñamente','halagüeñas','halagüeño','halagüeños','higüela','higüera','higüero','higüeros','higüeyano','igüedo','jagüel','jagüey','jagüilla','jigüe','lengüecita','lengüeta','lengüetada','lengüetadas','lengüetas','lengüetazo','lengüeteada','lengüetear','lengüetería','lengüeterías','lengüetero','lengüicorta','lengüicortas','lengüicorto','lengüicortos','lengüilarga','lengüilargas','lengüilargo','lengüilargos','ligüística','ligüísticas','ligüístico','ligüísticos','lingüista','lingüistas','lingüística','lingüísticas','lingüístico','lingüísticos','macagüita','macagüitas','machigüe','magüeta','magüetas','magüeto','magüetos','majagüero','majagüeros','managüense','manigüero','mayagüezano','mengüe','mengüé','mengüéis','mengüemos','mengüen','mengües','metalingüísticamente','metalingüístico','monolingüe','multilingüe','multilingües','nacarigüe','nacarigües','nagüero','nicaragüense','nicaragüenses','paragüera','paragüeras','paragüería','paragüerías','paragüero','paragüeros','pedigüeña','pedigüeñas','pedigüeñería','pedigüeño','pedigüeños','pichagüero','pingüe','pingüedinosa','pingüedinosas','pingüedinoso','pingüedinosos','pingües','pingüino','pingüinos','piragüero','piragüeros','piragüismo','piragüista','pirgüín','pirgüines','plurilingüe','plurilingües','plurilingüismo','psicolingüística','psicolingüístico','quinquelingüe','quinquelingües','rancagüino','reargüí','reargüía','reargüíais','reargüíamos','reargüían','reargüías','reargüid','reargüida','reargüidas','reargüido','reargüidos','reargüimos','reargüió','reargüirá','reargüirán','reargüirás','reargüiré','reargüiréis','reargüiremos','reargüiría','reargüiríais','reargüiríamos','reargüirían','reargüirías','reargüís','reargüiste','reargüisteis','redargüir','regüeldo','regüeldos','rigüe','rompezaragüelles','sangüeño','sangüeños','sangüesa','sangüesas','sangüeso','sangüesos','santigüe','saragüete','saragüetes','sinvergüencería','sinvergüencerías','sinvergüenza','sinvergüenzas','sociolingüística','sociolingüísticas','sociolingüístico','sociolingüísticos','subigüela','subigüelas','tegüe','terigüela','terigüelas','tigüilote','tigüilotes','trarigüe','trarigües','trilingüe','trilingües','ungüentaria','ungüentarias','ungüentario','ungüentarios','ungüento','ungüentos','veragüense','vergüenza','vergüenzas','verigüeto','verigüetos','yangüés','yegüería','yegüerías','yegüerío','yegüeriza','yegüerizas','yegüerizo','yegüerizos','yegüero','yegüeros','zagüía','zaragüelles','zarigüeya'
];
/**
* Separa vocales con diéresis métrica insertando separadores
*
* @param string $cadena Texto a procesar
* @return string Texto con separadores añadidos para diéresis métrica
*/
function separarVocalesConDieresis($cadena) {
$vocales_normales = 'aeiouyAEIOUY';
$vocales_dieresis = 'äëïöüÿÄËÏÖÜŸ';
$patron = '/([' . preg_quote($vocales_normales) . '])([' . preg_quote($vocales_dieresis) . '])|([' . preg_quote($vocales_dieresis) . '])([' . preg_quote($vocales_normales) . '])/u';
$resultado = preg_replace_callback($patron, function($coincidencias) {
if (!empty($coincidencias[1]) && !empty($coincidencias[2])) {
return $coincidencias[1] . '/' . $coincidencias[2];
}
if (!empty($coincidencias[3]) && !empty($coincidencias[4])) {
return $coincidencias[3] . '/' . $coincidencias[4];
}
return $coincidencias[0];
}, $cadena);
return $resultado;
}
/**
* Acumula pares de diéresis para presentación posterior
*
* @param array $array Array actual de diéresis
* @param mixed $valor1 Primer valor del par
* @param mixed $valor2 Segundo valor del par
* @return array Array actualizado
*/
function acumularDieresis($array, $valor1, $valor2) {
$array[] = [$valor1, $valor2];
return $array;
}
/**
* Detecta y cuenta diéresis métricas en un texto
*
* @param string $texto Texto a analizar
* @param array $palabras_permitidas Palabras con diéresis ortográfica (no cuentan)
* @return int Número de diéresis métricas encontradas
*/
function dieresisMetricas(string $texto, array $palabras_permitidas): int {
$texto = mb_strtolower($texto, 'UTF-8');
$permitidas = array_flip(array_map('mb_strtolower', $palabras_permitidas));
$total = 0;
global $listaDeDieresis;
if (preg_match_all('/\b[\wáéíóúäëïöüÿñ\'\-]+\b/u', $texto, $matches, PREG_OFFSET_CAPTURE)) {
foreach ($matches[0] as $match) {
$palabra = mb_strtolower($match[0]);
// Saltar palabras con diéresis ortográfica
if (isset($permitidas[$palabra])) {
continue;
}
// Contar diéresis métricas
$total += preg_match_all('/[äëïöüÿ]/u', $palabra);
if (preg_match_all('/[äëïöüÿ]/u', $palabra) == 1) {
$sinSeparar = versoSilabasMetricas($palabra);
$separada = separarVocalesConDieresis(versoSilabasMetricas($palabra));
$listaDeDieresis = acumularDieresis($listaDeDieresis, $sinSeparar, $separada);
// echo '<b>DEPURACIÓN</b><br>';
// echo 'Evento número: '.$total.'<br>';
// echo 'Sin separar la diéresis: '.$sinSeparar.'<br>';
// echo 'Separando la diéresis: '.$separada.'<br>';
// echo '<hr>';
}
}
}
return $total;
}
// =============================================================================
// CONSULTA A LA BASE DE DATOS PARA ESTABLECER LA PRONUNCIACIÓN
// =============================================================================
/**
* Convierte un texto en su equivalente fonético aproximado
* buscando cada palabra en la tabla `pronunciacion`.
*
* Si no se encuentra la palabra, se marca con un <span class="not-found">.
*
* @param string $texto Texto original a convertir.
* @param PDO $pdo Conexión PDO a la base de datos.
* @return string Texto transformado (HTML escapado y seguro).
*/
function convertirAPronunciacion(string $texto, PDO $pdo): string {
// Normalizamos el texto de entrada, suprimiendo los puntos y comas que pudiera contener.
$texto = str_replace(['(', ')', '[', ']', '{', '}', '¡', '!', '¿', '?', '.', ',', ';'], '', $texto);
// Preparamos la consulta una sola vez
$stmt = $pdo->prepare('SELECT IPA FROM pronunciacion WHERE LOWER(palabra) = LOWER(?) LIMIT 1');
// Dividimos el texto conservando espacios y puntuación
$tokens = preg_split('/(\b)/u', $texto, -1, PREG_SPLIT_DELIM_CAPTURE);
$resultado = '';
foreach ($tokens as $token) {
// Detectamos palabras (letras y acentos)
if (preg_match('/^\p{L}+$/u', $token)) {
$stmt->execute([$token]);
$fila = $stmt->fetch(PDO::FETCH_ASSOC);
if ($fila && !empty(trim($fila['IPA']))) {
// Si se encuentra, usamos la pronunciación escapada
$resultado .= htmlspecialchars($fila['IPA'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
} else {
// Si no se encuentra, la envolvemos en <span class="not-found">
$resultado .= '<span class="not-found">' .
htmlspecialchars($token, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') .
'</span>';
}
} else {
// Cualquier otro símbolo (espacio, puntuación...)
$resultado .= htmlspecialchars($token, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
}
$resultado = '['.$resultado.']'; // convención internacional para marcar el uso de pronunciación fonética.
return $resultado;
}
// =============================================================================
// PROCESAMIENTO PRINCIPAL Y PRESENTACIÓN DE RESULTADOS
// =============================================================================
/**
* Contador de diéresis métricas para el verso actual
*/
$contarDieresis = dieresisMetricas($versoOriginal, $palabras_con_dieresis);
/**
* Procesamiento principal cuando se envía el formulario sin errores
*/
if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["verso"])) {
$versoOriginal = trim((string)$_POST["verso"]);
$palabras = preg_split('/\s+/', $versoOriginal);
// Cálculos principales
$silabasGramaticales = contarSilabasGramaticales($versoOriginal);
$ultimaPalabraOriginal = end($palabras);
$ultimaPalabraLimpia = limpiarPalabra($ultimaPalabraOriginal);
$tipoFinal = tipoPalabraFinal($ultimaPalabraLimpia);
$ajuste = ajusteFinal($versoOriginal);
$SilabasMetricas = contarSilabasMetricas($versoOriginal) + $ajuste + $contarDieresis;
$versoGram = versoSilabasGramaticales($versoOriginal);
$versoMetric = versoSilabasMetricas($versoOriginal);
$versoPronunciado = convertirAPronunciacion($versoOriginal, $pdo);
// Configuración de mensajes según el tipo de palabra final
$claseDeCierre = [
'aguda' => '. (Verso oxítono; ',
'llana' => '. (Verso paroxítono; ',
'esdrújula' => '. (Verso proparoxítono; ',
'sobreesdrújula' => '. (Verso proparoxítono; '
][$tipoFinal] ?? '';
// Clasificación poética del verso por número de sílabas
$analisisPoetico = match ($SilabasMetricas) {
default => '',
2 => 'bisílabo, de arte menor)',
3 => 'trisílabo, de arte menor)',
4 => 'tetrasílabo, de arte menor)',
5 => 'pentasílabo, de arte menor)',
6 => 'hexasílabo, de arte menor)',
7 => 'heptasílabo, de arte menor)',
8 => 'octosílabo, de arte menor)',
9 => 'eneasílabo, de arte mayor)',
10 => 'decasílabo, de arte mayor)',
11 => 'endecasílabo, de arte mayor)',
12 => 'dodecasílabo, de arte mayor)',
13 => 'tridecasílabo, de arte mayor)',
14 => 'alejandrino, de arte mayor)',
15 => 'pentadecasílabo, de arte mayor)',
16 => 'hexadecasílabo, de arte mayor)',
17 => 'heptadecasílabo, de arte mayor)',
18 => 'octodecasílabo, de arte mayor)',
19 => 'eneadecasílabo, de arte mayor)',
20 => 'icosasílabo, de arte mayor)',
21 => 'unveintisílabo, de arte mayor)',
22 => 'docosílabo, de arte mayor)'
};
/**
* Convierte números a su representación textual en español
*
* @param int $numero Número a convertir (0-999)
* @return string Representación textual del número
*/
function numeroALetras($numero) {
if (!is_numeric($numero) || $numero < 0 || $numero > 999) {
return "Error: El número debe ser un entero entre 0 y 999.";
}
$numero = (int) $numero;
$unidades = array('', 'uno', 'dos', 'tres', 'cuatro', 'cinco', 'seis', 'siete', 'ocho', 'nueve');
$decenas_especiales = array('diez', 'once', 'doce', 'trece', 'catorce', 'quince', 'dieciséis', 'diecisiete', 'dieciocho', 'diecinueve');
$decenas_base = array('', '', 'veinte', 'treinta', 'cuarenta', 'cincuenta', 'sesenta', 'setenta', 'ochenta', 'noventa');
$centenas = array('', 'ciento', 'doscientos', 'trescientos', 'cuatrocientos', 'quinientos', 'seiscientos', 'setecientos', 'ochocientos', 'novecientos');
if ($numero == 0) {
return 'cero';
}
$texto = '';
// Procesar centenas
if ($numero >= 100) {
$c = floor($numero / 100);
$resto = $numero % 100;
// Caso especial: "cien" en lugar de "ciento"
if ($c == 1 && $resto == 0) {
$texto = 'cien';
return $texto;
}
$texto .= $centenas[$c];
if ($resto > 0) {
$texto .= ' ';
$numero = $resto;
} else {
return $texto;
}
}
// Procesar decenas y unidades
if ($numero < 10) {
$texto .= $unidades[$numero];
} elseif ($numero < 20) {
$texto .= $decenas_especiales[$numero - 10];
} else {
$d = floor($numero / 10);
$u = $numero % 10;
// Casos especiales con "veinti"
if ($d == 2 && $u > 0) {
$texto .= 'veinti' . $unidades[$u];
} else {
$texto .= $decenas_base[$d];
if ($u > 0) {
$texto .= ' y ' . $unidades[$u];
}
}
}
return trim($texto);
}
/**
* Prepara el silabeo métrico para presentación con diéresis separadas
*/
$silabeoPresentableConDieresisSeparadas = $versoMetric;
if (is_array($listaDeDieresis) && !empty($listaDeDieresis)) {
foreach ($listaDeDieresis as $par) {
if (count($par) === 2) {
[$buscar, $reemplazar] = $par;
// Construimos el patrón regex insensible a mayúsculas
$pattern = '/' . preg_quote($buscar, '/') . '/i';
$silabeoPresentableConDieresisSeparadas = preg_replace_callback(
$pattern,
function ($matches) use ($buscar, $reemplazar) {
$original = $matches[0];
// Si todo el fragmento estaba en mayúsculas → mantenerlo así
if (mb_strtoupper($original, 'UTF-8') === $original) {
return mb_strtoupper($reemplazar, 'UTF-8');
}
// Si solo la primera letra estaba en mayúscula → capitalizar
elseif (mb_strtoupper(mb_substr($original, 0, 1, 'UTF-8')) === mb_substr($original, 0, 1, 'UTF-8')) {
return mb_strtoupper(mb_substr($reemplazar, 0, 1, 'UTF-8')) .
mb_substr($reemplazar, 1, null, 'UTF-8');
}
// En cualquier otro caso → devolver en minúsculas
else {
return mb_strtolower($reemplazar, 'UTF-8');
}
},
$silabeoPresentableConDieresisSeparadas
);
}
}
}
?>
<!-- =============================================================================
PRESENTACIÓN DE RESULTADOS
============================================================================= -->
<?php if ($_SERVER['REQUEST_METHOD'] === 'POST' && $error === '') { ?>
<div class="proceso" id="proceso" style="display:block;">
<p class="justificado" style="margin-bottom: -13px;"><span class="etiqueta">Razonamiento de IA…</span></p>
<p id="razonamiento" class="justificado" style="display:block;">Iniciando el análisis…</p>
</div>
<div class="resultado" id="resultado" style="display:none;">
<h2>Resultado</h2>
<p><span class="etiqueta">Verso analizado:</span> <?= htmlspecialchars($versoOriginal, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></p>
<hr>
<p><span class="etiqueta">Pronunciación fonética detallada (AFI):</span> <?= $versoPronunciado ?></p>
<hr>
<p><span class="etiqueta">Número de sílabas gramaticales:</span> <?= numeroALetras($silabasGramaticales) ?>.</p>
<p><strong>Silabeo gramatical:</strong> <span class="monoespaciado"><?= str_replace('/', '<span class="gap">/</span>', htmlspecialchars($versoGram, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')) ?></span></p>
<hr>
<p><span class="etiqueta">Número de sílabas métricas:</span> <?= numeroALetras($SilabasMetricas) ?><?= $claseDeCierre ?><?= $analisisPoetico ?></p>
<p><strong>Silabeo métrico:</strong> <span class="monoespaciado"><?= str_replace('/', '<span class="gap">/</span>', htmlspecialchars($silabeoPresentableConDieresisSeparadas, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')) ?></span></p>
<p class="justificado"><span style="display:inline-block">● Dado</span> que el verso termina con una <span class="etiqueta">palabra <?= htmlspecialchars($tipoFinal, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></span>: <?= mensajeVariacion($ajuste) ?>.</p>
<!-- Análisis de fenómenos métricos detectados -->
<?php
// Análisis de sinalefas
echo match (substr_count($versoMetric, "‿")) {
0 => '',
1 => '<p class="justificado"><span style="display:inline-block">● El</span> texto presenta una <span class="etiqueta">una sinalefa</span>, señalizada con un ‿, que resta una sílaba métrica.</p>',
default => '<p class="justificado"><span style="display:inline-block">● El</span> texto presenta <span class="etiqueta">'.numeroALetras(substr_count($versoMetric, "‿")).' sinalefas</span>, señalizadas con sendas: ‿, que restan otras tantas sílabas métricas.</p>',
};
// Explicación adicional sobre sinalefas opcionales
echo match (substr_count($versoMetric, "‿")) {
0 => '<p class="justificado"><span style="display:inline-block">● No</span> se ha encontrado ninguna <span class="etiqueta">sinalefa</span>.</p>',
1 => '<p class="justificado"><span style="display:inline-block">● Téngase</span> en cuenta que la persona autora podría tomarse la licencia de ignorar una sinalefa cuando esta se produzca entre vocales tónicas, o entre una vocal átona y una tónica, según su criterio discrecional. En tal caso, no se descontaría ninguna sílaba —en lo que respecta a las sinalefas— al calcular la métrica de este verso (esta aplicación sí las ha descontado). <span class="etiqueta"><i>cf.</i> Dialefa.</p></span>',
default => '<p class="justificado"><span style="display:inline-block">● Téngase</span> en cuenta que la persona autora podría tomarse la licencia de ignorar alguna —o incluso todas— las sinalefas que se produzcan entre vocales tónicas, o entre una vocal átona y una tónica, según su criterio discrecional, en cuyo caso, hasta '.numeroALetras(substr_count($versoMetric, "‿")).' sílabas podrían no ser descontadas al calcular la métrica de este verso (esta aplicación sí las ha descontado). <span class="etiqueta"><i>cf.</i> Dialefa.</p></span>',
};
// Análisis de sinéresis
echo match (substr_count($versoMetric, "⁔")) {
0 => '<p class="justificado"><span style="display:inline-block">● No</span> se ha encontrado ninguna <span class="etiqueta">sinéresis</span>.</p>',
1 => '<p class="justificado"><span style="display:inline-block">● El</span> texto presenta <span class="etiqueta">una sinéresis</span>, señalizada con un ⁔, cuyo cómputo queda sujeto al criterio discrecional de la persona autora, quien decide si esa sílaba debe o no ser descontada (esta aplicación sí lo ha hecho).</p>',
default => '<p class="justificado"><span style="display:inline-block">● El</span> texto presenta <span class="etiqueta">'.numeroALetras(substr_count($versoMetric, "⁔")).' sinéresis</span>, señalizadas con sendas ⁔, cuyo cómputo queda sujeto al criterio discrecional de la persona autora, quien determina cuántas de esas sílabas deben descontarse al calcular la métrica de este verso (esta aplicación las ha descontado todas).</p>',
};
// Análisis de diéresis métricas
echo match ($contarDieresis) {
0 => '<p class="justificado"><span style="display:inline-block">● No</span> se ha encontrado ninguna <span class="etiqueta">diéresis métrica</span>.</p>',
1 => '<p class="justificado"><span style="display:inline-block">● Se</span> ha detectado un caso de <span class="etiqueta">diéresis métrica</span>, señalizado con una ¨ en una palabra que no lleva diéresis ortográfica, por lo que esta aplicación ha sumado una sílaba en el cómputo de la métrica.</p>',
default => '<p class="justificado"><span style="display:inline-block">● Se</span> han detectado '.numeroALetras($contarDieresis).' casos de <span class="etiqueta">diéresis métrica</span>, señalizados con sendos ¨ en palabras que no llevan diéresis ortográfica, por lo que esta aplicación ha sumado '.numeroALetras($contarDieresis).' sílabas en el cómputo de la métrica.</p>',
};
?>
</div>
<div class="panel" id="panel" style="display:none;">
<h2>Información</h2>
<p class="justificado">La <span class="etiqueta">métrica</span> es el conjunto de reglas que determinan la medida, el ritmo y la estructura de los versos en la poesía en español. Se basa en el número de sílabas métricas, los acentos y las pausas del verso. Resulta útil porque permite al poeta crear musicalidad, equilibrio y emoción en sus composiciones, y al lector apreciar mejor el ritmo y la intención expresiva del poema. En nuestro caso, sobre la métrica de la Lengua Española.</p>
<p class="justificado">La <span class="etiqueta">sinalefa</span> es un fenómeno métrico del verso español que consiste en unir en una sola sílaba métrica la vocal final de una palabra con la vocal inicial de la siguiente, incluso si hay una o más vocales contiguas. Su función es ajustar el cómputo silábico del verso sin alterar el ritmo natural del habla, favoreciendo la fluidez y la musicalidad del poema.</p>
<p class="justificado">La <span class="etiqueta">dialefa (o azeuxis)</span> es un recurso métrico que consiste en mantener separadas, en el cómputo silábico, las vocales finales e iniciales de dos palabras consecutivas que normalmente formarían una sinalefa. Es decir, se evita la unión natural de sonidos para conservar una sílaba más en el verso, generalmente por razones rítmicas, expresivas o para resaltar la pausa entre ambas palabras.</p>
<p class="justificado">La <span class="etiqueta">sinéresis</span> se trata de un recurso métrico que consiste en unir en una sola sílaba métrica dos vocales que normalmente formarían un hiato dentro de una misma palabra. Con ella se reduce el número de sílabas del verso y se favorece su medida, sin alterar demasiado la pronunciación natural, aunque a veces modifica levemente el ritmo o la musicalidad del poema.</p>
<p class="justificado">La <span class="etiqueta">diéresis métrica:</span> es una figura poética que consiste en separar en dos sílabas las vocales de un diptongo dentro de una palabra, aumentando así el cómputo silábico del verso. Se marca a veces con el signo ¨ sobre la vocal débil (ü) para indicar su pronunciación separada. Se diferencia de la diéresis ortográfica, que solo sirve para señalar que la “u” debe pronunciarse en combinaciones como “güe” o “güi”, sin afectar la métrica del verso.</p>
<p class="justificado">El <span class="etiqueta">AFI</span>, o <span class="etiqueta">Alfabeto Fonético Internacional</span>, es un sistema de símbolos creado para representar de forma precisa los sonidos de todas las lenguas del mundo. Cada signo corresponde a un sonido específico, independientemente de cómo se escriba en una lengua. Se usa en lingüística y enseñanza de idiomas para mostrar cómo se pronuncian realmente las palabras, evitando las ambigüedades de la ortografía.</p>
</div>
<script> // Este apartado es solo a efectos de presenctación de resultados en pantalla.
(function(){
const _0xA=['507265706172616369c3b36e2079207365676d656e74616369c3b36e2064656c20746578746f2e','4c696d7069657a61206f72746f6772c3a16669636120792070756e7475616369c3b36e2e','446976697369c3b36e2064656c20706f656d6120656e20766572736f732e','4964656e7469666963616369c3b36e2064652070616c61627261732079206163656e746f732e','44657465726d696e616369c3b36e2064656c206163656e746f2066696e616c2e','53696c6162656f206f72746f6772c3a16669636f20696e696369616c2e','41706c6963616369c3b36e2064652073696e616c6566617320656e7472652070616c61627261732e','41706c6963616369c3b36e2064652073696ec3a9726573697320696e7465726e61732e','41706c6963616369c3b36e206465206469c3a9726573697320706fc3a974696361732e','436f727265636369c3b36e20706f72207469706f206465206163656e746f2066696e616c2e','43c3b36d7075746f20746f74616c2064652073c3ad6c61626173206dc3a97472696361732e','436c617369666963616369c3b36e2064656c207469706f20646520766572736f2e','5472616e73637269706369c3b36e20666f6ec3a97469636120656e204146492e','56657269666963616369c3b36e207920636f686572656e6369612064656c20616ec3a16c697369732e','4576616c75616369c3b36e2072c3ad746d696361207920657374696cc3ad73746963612066696e616c2e'
];
function _0xB(h){try{return decodeURIComponent(h.replace(/(..)/g,'%$1'))}catch(e){try{var s='';for(var i=0;i<h.length;i+=2){s+=String.fromCharCode(parseInt(h.substr(i,2),16))}return s}catch(e2){return''}}}
const _0xC=_0xA.map(_0xB),_0xD=document.getElementById('proceso'),_0xE=document.getElementById('resultado'),_0xF=document.getElementById('razonamiento'),_0xH=document.getElementById('panel');
function _0xG(){_0xD.style.display='block';_0xE.style.display='none';_0xH.style.display='none';var i=0;var T=3000;var P=Math.max(1,Math.floor(T/_0xC.length));_0xF.textContent=_0xC[0]||'Iniciando el análisis…';i=1;var ID=setInterval(function(){if(i<_0xC.length){_0xF.textContent=_0xC[i++];return}clearInterval(ID);setTimeout(function(){_0xD.style.display='none';_0xE.style.display='block';_0xH.style.display='block'},300)},P)}
window.addEventListener('load',_0xG);
})();
</script>
<?php } ?>
<?php } ?>
<div class="mit" id="licenciaMIT" style="display:none;">
<h2>Licencia MIT</h2>
<p class="justificado"><span class="etiqueta">© <?= date('Y') ?> Jaume d'Urgell 陈建军</span></p>
<p class="justificado">Por la presente se concede permiso, libre de cargos, a cualquier persona que obtenga una copia de este programa y de los archivos de documentación asociados (el "Programa"), a utilizar el Programa sin restricción, incluyendo sin limitación los derechos a usar, copiar, modificar, fusionar, publicar, distribuir, sublicenciar, y/o vender copias del Programa, y a permitir a las personas a las que se les proporcione el Programa a hacer lo mismo, sujeto a las siguientes condiciones:</p>
<p class="justificado">EL PROGRAMA SE PROPORCIONA "COMO ESTÁ", SIN GARANTÍA DE NINGÚN TIPO, EXPRESA O IMPLÍCITA, INCLUYENDO PERO NO LIMITADO A GARANTÍAS DE COMERCIALIZACIÓN, IDONEIDAD PARA UN PROPÓSITO PARTICULAR E INCUMPLIMIENTO. EN NINGÚN CASO LOS AUTORES O PROPIETARIOS DE LOS DERECHOS DE AUTOR SERÁN RESPONSABLES DE NINGUNA RECLAMACIÓN, DAÑOS U OTRAS RESPONSABILIDADES, YA SEA EN UNA ACCIÓN DE CONTRATO, AGRAVIO O CUALQUIER OTRO MOTIVO, DERIVADAS DE, FUERA DE O EN CONEXIÓN CON EL PROGRAMA O SU USO U OTRO TIPO DE ACCIONES EN EL PROGRAMA.</p>
</div>
<!-- Pie de página con información de copyright -->
<p class="footer">
<strong>I+D+i sobre Procesamiento del Lenguaje Natural.</strong></br>
© <?= date("Y") ?> Jaume d'Urgell 陈建军. <a href="https://durgell.com/">Página web</a>. <a href="mailto:jaume@durgell.com">Correo</a>.</br>
Software libre (<a href="javascript:void(0)" onclick="mit()">licencia MIT</a>). <a href="https://durgell.com/metrica/codigo-fuente.php">Código fuente</a>.
</p>
<!-- =============================================================================
SCRIPT JAVASCRIPT PARA INTERACTIVIDAD
============================================================================= -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const input = document.getElementById('verso');
const btnEjemplo = document.getElementById('btn-ejemplo');
const btnBorrar = document.getElementById('btn-borrar');
const resultado = document.getElementById('resultado');
if (input) {
// Enfocar el campo de entrada al cargar la página
input.focus();
// Colocar cursor al final si hay texto existente
const length = input.value.length;
input.setSelectionRange(length, length);
// Configurar funcionalidad del botón ejemplo
if (btnEjemplo) {
btnEjemplo.addEventListener('click', function() {
input.value = '<?php echo versoAleatorio(); ?>';
// document.getElementById('formulario').submit();
document.getElementById('btn-analizar').click();
});
}
// Configurar funcionalidad del botón borrar
if (btnBorrar) {
btnBorrar.addEventListener('click', function() {
input.value = '';
input.focus();
// Ocultar resultados anteriores
if (resultado) {
try {resultado.style.display = 'none';} catch (error) {}
try {panel.style.display = 'none';} catch (error) {}
}
try {licenciaMIT.style.display = 'none';} catch (error) {}
});
}
}
// Cuando la ventana recupere el foco, el input también lo hace.
window.addEventListener("focus", () => {
input.focus();
});
});
function mit() {
try {resultado.style.display = 'none';} catch (error) {}
try {panel.style.display = 'none';} catch (error) {}
try {licenciaMIT.style.display = 'block';} catch (error) {}
}
</script>
</body>
</html>