') === 0) { $parrafo = parsea_elementos_en_linea($parrafo); $resultado .= parsea_cita($parrafo); } elseif (strpos($parrafo, '- ') === 0 || strpos($parrafo, '* ') === 0 || preg_match('/^\d+\.\s/', $parrafo)) { $parrafo = parsea_elementos_en_linea($parrafo); $resultado .= parsea_listas($parrafo); } elseif (preg_match('/\n:?---:? \| :?---:?/', $parrafo)) { $parrafo = parsea_elementos_en_linea($parrafo); $resultado .= parsea_tabla($parrafo); } elseif (preg_match('/^#{1,6}\s.+/', $parrafo)) { $parrafo = parsea_elementos_en_linea($parrafo); $resultado .= parsea_titulo($parrafo); } elseif (preg_match('/^(```|~~~)/', $parrafo) && preg_match('/(```|~~~)$/', $parrafo)) { //bloque de código, no se parsean elementos en línea $resultado .= parsea_codigo($parrafo); } // Párrafo normal else { $parrafo = parsea_elementos_en_linea($parrafo); $resultado .= '
' . $parrafo . '
' . "\n"; } } return $resultado; } function parsea_cita($texto) { // Contar cuántos "> " hay al principio $nivel = 0; $html = ""; // Eliminar "> " nivel por nivel while (strpos($texto, '> ') === 0) { $nivel++; $texto = substr($texto, 2); } // Añadir los blockquote de apertura for ($i = 0; $i < $nivel; $i++) { $html .= ''; } // Añadir el contenido $html .= $texto; // Añadir los blockquote de cierre for ($i = 0; $i < $nivel; $i++) { $html .= ''; } $html .= "\n"; return $html; } function parsea_listas($texto) { // Dividir el texto en líneas $lineas = explode("\n", $texto); $total_lineas = count($lineas); // Determinar el tipo de lista principal según la primera línea (sin espacios iniciales) $primer_elemento = ltrim($lineas[0]); $tipo_lista = ''; $tipo_lista_hija = ''; if (strpos($primer_elemento, '- ') === 0 || strpos($primer_elemento, '* ') === 0) { $tipo_lista = 'ul'; } elseif (preg_match('/^\d+\.\s/', $primer_elemento)) { $tipo_lista = 'ol'; } $i = 0; while ($i < $total_lineas) { $linea = $lineas[$i]; $linea = rtrim($linea); if ($linea === '') { $i++; continue; } // Detectar si tiene dos espacios o tabulador al inicio (sublista) if (preg_match('/^( |\t)/', $linea)) { $linea_sin_indent = preg_replace('/^( |\t)/', '', $linea); $linea_sin_espacios_inicio = ltrim($linea_sin_indent); if (strpos($linea_sin_espacios_inicio, '- ') === 0) { $contenido = substr($linea_sin_espacios_inicio, 2); $marcador = '-'; } elseif (strpos($linea_sin_espacios_inicio, '* ') === 0) { $contenido = substr($linea_sin_espacios_inicio, 2); $marcador = '*'; } elseif (preg_match('/^\d+\.\s/', $linea_sin_espacios_inicio)) { $contenido = preg_replace('/^\d+\.\s/', '', $linea_sin_espacios_inicio); $marcador = 'numero'; } else { $contenido = $linea_sin_indent; $marcador = ''; } if ($marcador !== '') { $j = $i - 1; while ($j >= 0 && trim($lineas[$j]) === '') { $j--; } if ($j >= 0) { if ($tipo_lista_hija === '') { if ($marcador === 'numero') { $tipo_lista_hija = 'ol'; } else { $tipo_lista_hija = 'ul'; } $lineas[$j] .= '<' . $tipo_lista_hija . '>'; } $lineas[$j] .= '
| " . $valor . " | \n"; } $html .= "
|---|
| " . $valor . " | \n"; } $html .= "
" . $contenido_codigo . "\n";
}
function preprocesar_bloques_codigo($texto) {
// Buscar bloques de código y reemplazar \n\n por un marcador temporal
return preg_replace_callback(
'/(^|\n)(```|~~~)(?:\n.*?\n)\2(\n|$)/s',
function($matches) {
// Reemplazar \n\n por \n \n (espacio entre ellos)
return str_replace("\n\n", "\n \n", $matches[0]);
},
$texto
);
}
function sanear_listas($texto) {
$lineas = explode("\n", $texto);
$resultado = [];
$en_lista = false;
foreach ($lineas as $linea) {
$es_item = preg_match('/^(\d+\.\s)/', $linea);
$es_sangria = preg_match('/^(\s{2}|\t)/', $linea);
$es_vacia = (trim($linea) === '');
if ($es_item) {
// Si ya estamos en una lista, eliminar líneas vacías anteriores
if ($en_lista) {
while (!empty($resultado) && trim(end($resultado)) === '') {
array_pop($resultado);
}
}
$resultado[] = $linea;
$en_lista = true;
}
// Línea con sangría cuando estamos en lista
else if ($es_sangria && $en_lista) {
// Eliminar líneas vacías anteriores
while (!empty($resultado) && trim(end($resultado)) === '') {
array_pop($resultado);
}
$resultado[] = $linea;
}
// Cualquier otra línea
else {
$resultado[] = $linea;
// Si encontramos contenido que no es ítem ni línea con sangría, salimos del modo lista
if (!$es_vacia) {
$en_lista = false;
}
}
}
return implode("\n", $resultado);
}
function parsea_elementos_en_linea($texto) {
$lineas = explode("\n", $texto);
$almacen_enlaces = [];
$almacen_imagenes = [];
$almacen_codigos = [];
foreach ($lineas as &$linea) {
$linea = parsea_codigo_linea($linea, $almacen_codigos);
$linea = parsea_imagenes($linea, $almacen_imagenes);
$linea = parsea_enlaces($linea, $almacen_enlaces);
$linea = parsea_nueva_linea($linea);
$linea = escapa_caracteres($linea);
$linea = parsea_negrita_cursiva($linea);
$linea = parsea_seccion($linea);
$linea = recoloca_elementos($linea, $almacen_codigos, $almacen_enlaces, $almacen_imagenes);
}
unset($linea);
return implode("\n", $lineas);
}
function parsea_codigo_linea($linea, &$almacen_codigos) {
$resultado = '';
$longitud = strlen($linea);
for ($i = 0; $i < $longitud; $i++) {
if ($linea[$i] === '`') {
$inicio = $i;
// Contar backticks del delimitador
$num_backticks = 0;
while ($i < $longitud && $linea[$i] === '`') {
$num_backticks++;
$i++;
}
// Buscar el cierre
$contenido = '';
$cierre_encontrado = false;
while ($i < $longitud) {
if ($linea[$i] === '`') {
// Contar backticks potenciales de cierre
$temp_i = $i;
$backticks_cierre = 0;
while ($temp_i < $longitud && $linea[$temp_i] === '`') {
$backticks_cierre++;
$temp_i++;
}
// Si coincide con el número de apertura y no es parte de secuencia más larga
if ($backticks_cierre === $num_backticks &&
($temp_i >= $longitud || $linea[$temp_i] !== '`')) {
$cierre_encontrado = true;
// Verificar que no haya saltos de línea en el contenido
if (strpos($contenido, "\n") === false) {
// Almacenar el código en el array
$almacen_codigos[] = $contenido;
// Insertar marcador en lugar del código
$resultado .= MARCADOR_CODIGO;
$i = $temp_i; // Avanzar después del delimitador de cierre
} else {
// Tiene salto de línea, no es código válido
$resultado .= substr($linea, $inicio, $temp_i - $inicio);
$i = $temp_i;
}
break;
}
}
// Acumular contenido
$contenido .= $linea[$i];
$i++;
}
if (!$cierre_encontrado) {
// No se encontró cierre
$resultado .= substr($linea, $inicio, $i - $inicio);
}
// Retroceder uno porque el bucle for va a incrementar
// y la $i se dejó justo en la posición correcta detrás del último `
$i--;
} else {
$resultado .= $linea[$i];
}
}
return $resultado;
}
function parsea_nueva_linea($linea) {
$longitud = strlen($linea);
// Si la cadena está vacía, retornar vacío
if ($longitud === 0) {
return $linea;
}
// Caso 1: Verificar si termina con barra invertida (\) y el anterior no es barra invertida
if ($linea[$longitud - 1] === '\\') {
// Si solo hay un caracter o el caracter anterior no es barra invertida
if ($longitud === 1 || $linea[$longitud - 2] !== '\\') {
// Reemplazar la última barra invertida por
if (substr($linea, $i, 6) === '') {
// Añadir la etiqueta de apertura
$resultado .= '';
$i += 6;
// Buscar la etiqueta de cierre correspondiente
$pos_cierre = strpos($linea, '', $i);
if ($pos_cierre === false) {
// No hay cierre, copiar el resto y terminar
$resultado .= substr($linea, $i);
break;
}
// Copiar el contenido dentro de sin procesar
$contenido_codigo = substr($linea, $i, $pos_cierre - $i);
$resultado .= $contenido_codigo;
// Añadir la etiqueta de cierre
$resultado .= '';
$i = $pos_cierre + 7;
continue;
}
// Para el texto fuera de , procesar secuencias de escape
// Buscar \ seguido de un carácter especial
if ($i < $longitud - 1 && $linea[$i] === '\\') {
$caracter_siguiente = $linea[$i + 1];
// Verificar si el siguiente carácter está en nuestra lista de especiales
if (isset($caracteres_especiales[$caracter_siguiente])) {
// Reemplazar \caracter por la entidad HTML
$resultado .= $caracteres_especiales[$caracter_siguiente];
$i += 2; // Saltar ambos caracteres
continue;
}
}
// Si no es una secuencia de escape, copiar el carácter tal cual
$resultado .= $linea[$i];
$i++;
}
return $resultado;
}
function parsea_enlaces($linea, &$almacen_enlaces) {
$resultado = '';
$longitud = strlen($linea);
$i = 0;
while ($i < $longitud) {
// Buscamos un '[' que no esté escapado
if ($linea[$i] === '[' &&
($i === 0 || $linea[$i-1] !== '\\') &&
($i === 0 || $linea[$i-1] !== '!')) {
$inicio = $i;
$i++;
// Buscamos el cierre ']' que esté seguido inmediatamente por '('
$enlace_texto = '';
$encontrado_cierre = false;
while ($i < $longitud && !$encontrado_cierre) {
if ($linea[$i] === ']' && $i+1 < $longitud && $linea[$i+1] === '(') {
$encontrado_cierre = true;
$i += 2; // Pasamos el ']('
break;
}
$enlace_texto .= $linea[$i];
$i++;
}
if ($encontrado_cierre) {
// Buscamos el URL hasta el primer espacio o fin de línea
$url = '';
$posicion_parentesis_inicio = $i - 1; // Posición del '('
while ($i < $longitud && $linea[$i] !== ' ' && $linea[$i] !== "\t" && $linea[$i] !== "\n") {
$url .= $linea[$i];
$i++;
}
// Retrocedemos para encontrar el último ')' antes del espacio
$j = $i - 1;
$parentesis_cierre = -1;
while ($j > $posicion_parentesis_inicio) {
if ($linea[$j] === ')') {
$parentesis_cierre = $j;
break;
}
$j--;
}
if ($parentesis_cierre !== -1) {
// Extraemos el URL correcto (sin el ')')
$url = substr($linea, $posicion_parentesis_inicio + 1, $parentesis_cierre - $posicion_parentesis_inicio - 1);
// Verificamos si hay atributos CSS opcionales
$clases = [];
$id = null;
$posicion_actual = $parentesis_cierre + 1;
// Comprobamos si hay '{' justo después del ')'
if ($posicion_actual < $longitud && $linea[$posicion_actual] === '{') {
$posicion_actual++; // Pasamos la '{'
$contenido_llaves = '';
// Extraemos el contenido hasta la '}'
while ($posicion_actual < $longitud && $linea[$posicion_actual] !== '}') {
$contenido_llaves .= $linea[$posicion_actual];
$posicion_actual++;
}
if ($posicion_actual < $longitud && $linea[$posicion_actual] === '}') {
$posicion_actual++; // Pasamos la '}'
// Parseamos clases e ID
$elementos = explode(' ', $contenido_llaves);
foreach ($elementos as $elemento) {
$elemento = trim($elemento);
if ($elemento === '') continue;
if ($elemento[0] === '.') {
// Es una clase (quitamos el punto)
$clases[] = substr($elemento, 1);
} elseif ($elemento[0] === '#' && $id === null) {
// Es un ID (quitamos el #, solo el primero cuenta)
$id = substr($elemento, 1);
}
}
}
}
// Construimos la etiqueta con el marcador en href
$atributos = 'href="' . MARCADOR_URL_ENLACES . '"';
if (!empty($clases)) {
$atributos .= ' class="' . htmlspecialchars(implode(' ', $clases), ENT_QUOTES | ENT_HTML5, 'UTF-8') . '"';
}
if ($id !== null) {
$atributos .= ' id="' . htmlspecialchars($id, ENT_QUOTES | ENT_HTML5, 'UTF-8') . '"';
}
// Almacenamos la URL en el array para posterior restitución
$almacen_enlaces[] = $url;
// Añadimos la etiqueta completa al resultado (con el texto del enlace sin escapar)
$resultado .= '' . $enlace_texto . '';
// Actualizamos la posición actual
$i = $posicion_actual;
} else {
// No se encontró el cierre del URL, dejamos el texto original
$resultado .= substr($linea, $inicio, $i - $inicio);
}
} else {
// No se encontró el cierre '](', dejamos el texto original
$resultado .= substr($linea, $inicio, $i - $inicio);
}
} else {
// Carácter normal: lo añadimos tal cual y avanzamos uno
$resultado .= $linea[$i];
$i++;
}
}
return $resultado;
}
function parsea_imagenes($linea, &$almacen_imagenes) {
$resultado = '';
$longitud = strlen($linea);
$i = 0;
while ($i < $longitud) {
// Buscamos '![' que no esté escapado
if ($linea[$i] === '!' &&
$i+1 < $longitud &&
$linea[$i+1] === '[' &&
($i === 0 || $linea[$i-1] !== '\\')) {
$inicio = $i;
$i += 2; // Pasamos '!['
// Buscamos el cierre ']' que esté seguido inmediatamente por '('
$alt_text = '';
$encontrado_cierre = false;
while ($i < $longitud && !$encontrado_cierre) {
if ($linea[$i] === ']' && $i+1 < $longitud && $linea[$i+1] === '(') {
$encontrado_cierre = true;
$i += 2; // Pasamos el ']('
break;
}
$alt_text .= $linea[$i];
$i++;
}
if ($encontrado_cierre) {
// Buscamos el URL hasta el primer espacio o fin de línea
$url = '';
$posicion_parentesis_inicio = $i - 1; // Posición del '('
while ($i < $longitud && $linea[$i] !== ' ' && $linea[$i] !== "\t" && $linea[$i] !== "\n") {
$url .= $linea[$i];
$i++;
}
// Retrocedemos para encontrar el último ')' antes del espacio
$j = $i - 1;
$parentesis_cierre = -1;
while ($j > $posicion_parentesis_inicio) {
if ($linea[$j] === ')') {
$parentesis_cierre = $j;
break;
}
$j--;
}
if ($parentesis_cierre !== -1) {
// Extraemos el URL correcto (sin el ')')
$url = substr($linea, $posicion_parentesis_inicio + 1, $parentesis_cierre - $posicion_parentesis_inicio - 1);
// Verificamos si hay atributos CSS opcionales
$clases = [];
$id = null;
$posicion_actual = $parentesis_cierre + 1;
// Comprobamos si hay '{' justo después del ')'
if ($posicion_actual < $longitud && $linea[$posicion_actual] === '{') {
$posicion_actual++; // Pasamos la '{'
$contenido_llaves = '';
// Extraemos el contenido hasta la '}'
while ($posicion_actual < $longitud && $linea[$posicion_actual] !== '}') {
$contenido_llaves .= $linea[$posicion_actual];
$posicion_actual++;
}
if ($posicion_actual < $longitud && $linea[$posicion_actual] === '}') {
$posicion_actual++; // Pasamos la '}'
// Parseamos clases e ID
$elementos = explode(' ', $contenido_llaves);
foreach ($elementos as $elemento) {
$elemento = trim($elemento);
if ($elemento === '') continue;
if ($elemento[0] === '.') {
// Es una clase (quitamos el punto)
$clases[] = substr($elemento, 1);
} elseif ($elemento[0] === '#' && $id === null) {
// Es un ID (quitamos el #, solo el primero cuenta)
$id = substr($elemento, 1);
}
}
}
}
// Construimos la etiqueta
$atributos = 'src="' . MARCADOR_IMAGENES_MD . '"';
$atributos .= ' alt="' . htmlspecialchars($alt_text, ENT_QUOTES | ENT_HTML5, 'UTF-8') . '"';
if (!empty($clases)) {
$atributos .= ' class="' . htmlspecialchars(implode(' ', $clases), ENT_QUOTES | ENT_HTML5, 'UTF-8') . '"';
}
if ($id !== null) {
$atributos .= ' id="' . htmlspecialchars($id, ENT_QUOTES | ENT_HTML5, 'UTF-8') . '"';
}
// Almacenamos la URL en el array para posterior restitución
$almacen_imagenes[] = $url;
// Añadimos la etiqueta completa al resultado
$resultado .= '
';
// Actualizamos la posición actual
$i = $posicion_actual;
} else {
// No se encontró el cierre del URL, dejamos el texto original
$resultado .= substr($linea, $inicio, $i - $inicio);
}
} else {
// No se encontró el cierre '](', dejamos el texto original
$resultado .= substr($linea, $inicio, $i - $inicio);
}
} else {
// Carácter normal: lo añadimos tal cual y avanzamos uno
$resultado .= $linea[$i];
$i++;
}
}
return $resultado;
}
function parsea_negrita_cursiva($linea) {
// Procesamos desde 3 asteriscos hasta 1
for ($num_asteriscos = 3; $num_asteriscos > 0; $num_asteriscos--) {
$posiciones = [];
$longitud = strlen($linea);
$i = 0;
// Primera pasada: detectar todas las secuencias exactas de $num_asteriscos asteriscos
while ($i < $longitud) {
// Si encontramos un asterisco, no está escapado y no es parte de una secuencia más larga
if ($linea[$i] === '*' &&
($i === 0 || $linea[$i-1] !== '\\') &&
($i === 0 || $linea[$i-1] !== '*')) {
$count = 0;
$j = $i;
// Contamos asteriscos consecutivos
while ($j < $longitud && $linea[$j] === '*') {
$count++;
$j++;
}
// Si es exactamente el número que buscamos y no hay más asteriscos después
if ($count == $num_asteriscos &&
($j >= $longitud || $linea[$j] !== '*')) {
$posiciones[] = $i;
$i += $num_asteriscos; // Saltamos los asteriscos
} else {
$i++;
}
} else {
$i++;
}
}
// Asegurar que el número de posiciones sea par
if (count($posiciones) % 2 != 0) {
array_pop($posiciones);
}
// Si no hay posiciones, continuamos
if (empty($posiciones)) {
continue;
}
// Definir etiquetas según el número de asteriscos
if ($num_asteriscos == 3) {
$etiqueta_apertura = '';
$etiqueta_cierre = '';
} elseif ($num_asteriscos == 2) {
$etiqueta_apertura = '';
$etiqueta_cierre = '';
} else { // 1
$etiqueta_apertura = '';
$etiqueta_cierre = '';
}
// Segunda pasada: construir la nueva cadena
$resultado = '';
$cursor = 0;
$etiqueta_abierta = false;
foreach ($posiciones as $posicion) {
// Copiar desde el cursor hasta la posición actual
$resultado .= substr($linea, $cursor, $posicion - $cursor);
// Insertar etiqueta correspondiente
if (!$etiqueta_abierta) {
$resultado .= $etiqueta_apertura;
} else {
$resultado .= $etiqueta_cierre;
}
// Alternar estado
$etiqueta_abierta = !$etiqueta_abierta;
// Mover el cursor después de los asteriscos
$cursor = $posicion + $num_asteriscos;
}
// Copiar el resto de la cadena
if ($cursor < $longitud) { $resultado .= substr($linea, $cursor); }
// Actualizar la línea para el siguiente nivel
$linea = $resultado;
}
return $linea;
}
function parsea_seccion($linea) {
$patron = '/^S(\d+)\.\s/';
$reemplazo = '$1.- ';
return preg_replace($patron, $reemplazo, $linea, 1);
}
function recoloca_elementos($linea, &$almacen_codigos, &$almacen_enlaces, &$almacen_imagenes) {
// Recoloca los códigos en línea:
$num_marcadores = substr_count($linea, MARCADOR_CODIGO);
if ($num_marcadores > 0 && count($almacen_codigos) >= $num_marcadores) {
for ($i = 0; $i < $num_marcadores; $i++) {
$codigo = array_shift($almacen_codigos);
$codigo_procesado = '' . htmlspecialchars($codigo, ENT_QUOTES | ENT_HTML5, 'UTF-8') . '';
$posicion = strpos($linea, MARCADOR_CODIGO);
if ($posicion !== false) {
$linea = substr_replace($linea, $codigo_procesado, $posicion, strlen(MARCADOR_CODIGO));
}
}
}
// Recoloca los enlaces:
$num_marcadores_enlaces = substr_count($linea, MARCADOR_URL_ENLACES);
if ($num_marcadores_enlaces > 0 && count($almacen_enlaces) >= $num_marcadores_enlaces) {
for ($i = 0; $i < $num_marcadores_enlaces; $i++) {
$url = array_shift($almacen_enlaces);
$posicion = strpos($linea, MARCADOR_URL_ENLACES);
if ($posicion !== false) {
$linea = substr_replace($linea, $url, $posicion, strlen(MARCADOR_URL_ENLACES));
}
}
}
// Recoloca las imágenes:
$num_marcadores_imagenes = substr_count($linea, MARCADOR_IMAGENES_MD);
if ($num_marcadores_imagenes > 0 && count($almacen_imagenes) >= $num_marcadores_imagenes) {
for ($i = 0; $i < $num_marcadores_imagenes; $i++) {
$url = array_shift($almacen_imagenes);
$posicion = strpos($linea, MARCADOR_IMAGENES_MD);
if ($posicion !== false) {
$linea = substr_replace($linea, $url, $posicion, strlen(MARCADOR_IMAGENES_MD));
}
}
}
return $linea;
}
?>