El ancho útil vuelve a 74rem (1184px). Se había subido a 88rem para aprovechar los monitores anchos y salió al revés: la prosa tiene una medida de lectura y no crece con el contenedor, así que lo único que creció fue el hueco a su derecha. Un vacío al lado de un texto no se tapa estirando el texto —probado: 129 caracteres por línea, ilegible— sino poniendo algo al lado: - Los textos legales van a tres columnas: los otros documentos, el que se lee y sus apartados. El índice de apartados sale del Markdown en bruto y las anclas las pone un plugin al compilarlo, así que hay pruebas de que las dos formas de calcularlas siguen coincidiendo. - El índice legal pasa de lista de renglones a rejilla de tarjetas, como el taller y los estilos; era el único listado que no se veía como los demás. - La bio lleva al lado lo que hay publicado —temas, discos y estilos, con sus enlaces—, sacado del catálogo y no escrito a mano, para que no se contradiga sola el día que se suba una canción. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>master
parent
5c3d3371f0
commit
f248ad57da
@ -0,0 +1,64 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ancla, encabezadosDe } from './anclas';
|
||||
|
||||
describe('ancla', () => {
|
||||
it('quita las tildes y la eñe', () => {
|
||||
expect(ancla('Política de privacidad')).toBe('politica-de-privacidad');
|
||||
expect(ancla('Años y años')).toBe('anos-y-anos');
|
||||
});
|
||||
|
||||
it('tira la puntuación en vez de dejarla en el ancla', () => {
|
||||
expect(ancla('¿Qué se recoge, y para qué?')).toBe('que-se-recoge-y-para-que');
|
||||
});
|
||||
|
||||
it('no deja guiones sueltos en los extremos', () => {
|
||||
expect(ancla(' ¡Pago! ')).toBe('pago');
|
||||
});
|
||||
|
||||
it('da algo con lo que enlazar aunque el título no tenga letras', () => {
|
||||
expect(ancla('···')).toBe('seccion');
|
||||
});
|
||||
|
||||
it('lleva a la misma ancla el título escrito con tilde y sin ella', () => {
|
||||
expect(ancla('Facturación')).toBe(ancla('Facturacion'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('encabezadosDe', () => {
|
||||
it('recoge los apartados de nivel 2 en orden', () => {
|
||||
const md = ['# Título', '', '## Primero', 'texto', '', '## Segundo', 'más texto'].join('\n');
|
||||
|
||||
expect(encabezadosDe(md)).toEqual([
|
||||
{ texto: 'Primero', ancla: 'primero' },
|
||||
{ texto: 'Segundo', ancla: 'segundo' }
|
||||
]);
|
||||
});
|
||||
|
||||
it('no confunde el frontmatter con contenido', () => {
|
||||
const md = ['---', 'titulo: Algo', 'nota: "## no es un apartado"', '---', '', '## Real'].join(
|
||||
'\n'
|
||||
);
|
||||
|
||||
expect(encabezadosDe(md)).toEqual([{ texto: 'Real', ancla: 'real' }]);
|
||||
});
|
||||
|
||||
it('ignora las almohadillas dentro de un bloque de código', () => {
|
||||
const md = ['## Uno', '', '```sh', '## esto es un comentario', '```', '', '## Dos'].join('\n');
|
||||
|
||||
expect(encabezadosDe(md).map((e) => e.texto)).toEqual(['Uno', 'Dos']);
|
||||
});
|
||||
|
||||
it('deja el título limpio de énfasis y de enlaces', () => {
|
||||
const md = '## El **derecho** de [desistimiento](/legal/condiciones-de-venta)';
|
||||
|
||||
expect(encabezadosDe(md)).toEqual([
|
||||
{ texto: 'El derecho de desistimiento', ancla: 'el-derecho-de-desistimiento' }
|
||||
]);
|
||||
});
|
||||
|
||||
it('deja fuera los niveles que no pinta el índice', () => {
|
||||
const md = ['# Uno', '### Tres', '## Dos'].join('\n');
|
||||
|
||||
expect(encabezadosDe(md).map((e) => e.texto)).toEqual(['Dos']);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Anclas para los encabezados de los textos largos.
|
||||
*
|
||||
* Se usa desde dos sitios que tienen que coincidir carácter a carácter: el
|
||||
* plugin que pone el `id` al compilar el Markdown (en vite.config.ts) y el
|
||||
* cargador que construye el índice de la página. Si las dos versiones se
|
||||
* separaran, el índice enlazaría a anclas que no existen y no lo notaría
|
||||
* nadie hasta pinchar. Por eso vive aquí, en un solo sitio.
|
||||
*/
|
||||
|
||||
/** Convierte el texto de un encabezado en un ancla estable. */
|
||||
export function ancla(texto: string): string {
|
||||
return (
|
||||
texto
|
||||
.toLowerCase()
|
||||
// Separa las tildes de la letra para poder tirarlas: «política» y
|
||||
// «politica» tienen que dar la misma ancla.
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/ñ/g, 'n')
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '') || 'seccion'
|
||||
);
|
||||
}
|
||||
|
||||
/** Un encabezado de nivel 2, tal como lo pinta el índice lateral. */
|
||||
export type Encabezado = { texto: string; ancla: string };
|
||||
|
||||
/**
|
||||
* Saca los encabezados de nivel 2 del Markdown en bruto.
|
||||
*
|
||||
* Se lee el texto original y no el HTML compilado porque en el momento de
|
||||
* cargar la página el cuerpo ya es un componente de Svelte: dejó de ser texto
|
||||
* y no hay nada que recorrer.
|
||||
*/
|
||||
export function encabezadosDe(markdown: string): Encabezado[] {
|
||||
const encabezados: Encabezado[] = [];
|
||||
let enBloqueDeCodigo = false;
|
||||
let enFrontmatter = false;
|
||||
|
||||
const lineas = markdown.split(/\r?\n/);
|
||||
|
||||
for (const [i, linea] of lineas.entries()) {
|
||||
// El frontmatter abre y cierra con `---`; dentro puede haber cualquier
|
||||
// cosa, incluidas almohadillas.
|
||||
if (linea.trim() === '---') {
|
||||
if (i === 0) enFrontmatter = true;
|
||||
else if (enFrontmatter) enFrontmatter = false;
|
||||
continue;
|
||||
}
|
||||
if (enFrontmatter) continue;
|
||||
|
||||
// Un `## ` dentro de un bloque de código es código, no un encabezado.
|
||||
if (/^\s*(```|~~~)/.test(linea)) {
|
||||
enBloqueDeCodigo = !enBloqueDeCodigo;
|
||||
continue;
|
||||
}
|
||||
if (enBloqueDeCodigo) continue;
|
||||
|
||||
const encontrado = /^##\s+(.+?)\s*#*\s*$/.exec(linea);
|
||||
if (!encontrado) continue;
|
||||
|
||||
// Quita el énfasis y los enlaces del título: en el índice sobran.
|
||||
const texto = encontrado[1]
|
||||
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
|
||||
.replace(/[*_`]/g, '')
|
||||
.trim();
|
||||
|
||||
encabezados.push({ texto, ancla: ancla(texto) });
|
||||
}
|
||||
|
||||
return encabezados;
|
||||
}
|
||||
@ -1,9 +1,28 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { cuerpoPagina, obtenerPagina } from '$lib/content';
|
||||
import { albumes, canciones, cuerpoPagina, estilosConObra, obtenerPagina } from '$lib/content';
|
||||
|
||||
export function load() {
|
||||
const pagina = obtenerPagina('bio');
|
||||
if (!pagina) error(404, 'Falta src/content/paginas/bio.md');
|
||||
|
||||
return { pagina, Cuerpo: cuerpoPagina('bio') };
|
||||
/*
|
||||
* Al lado del texto va lo que hay publicado. Sale del catálogo y no de un
|
||||
* campo escrito a mano: una biografía que dice «catorce temas» y una web
|
||||
* que enseña dieciocho se contradicen el día que se sube uno nuevo, y nadie
|
||||
* se acuerda de volver aquí a corregirlo.
|
||||
*/
|
||||
const obra = estilosConObra()
|
||||
.filter((e) => e.totalCanciones > 0)
|
||||
.map((e) => ({
|
||||
slug: e.estilo.slug,
|
||||
nombre: e.estilo.nombre,
|
||||
cuantas: e.totalCanciones
|
||||
}));
|
||||
|
||||
return {
|
||||
pagina,
|
||||
Cuerpo: cuerpoPagina('bio'),
|
||||
obra,
|
||||
totales: { canciones: canciones.length, albumes: albumes.length, estilos: obra.length }
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { cuerpoLegal, legales, obtenerLegal } from '$lib/content';
|
||||
import { cuerpoLegal, indiceLegal, legales, obtenerLegal } from '$lib/content';
|
||||
|
||||
export function load({ params }) {
|
||||
const pagina = obtenerLegal(params.slug);
|
||||
if (!pagina) error(404, 'Esa página no existe.');
|
||||
|
||||
return { pagina, legales, Cuerpo: cuerpoLegal(params.slug) };
|
||||
return {
|
||||
pagina,
|
||||
legales,
|
||||
Cuerpo: cuerpoLegal(params.slug),
|
||||
indice: indiceLegal(params.slug)
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
Reference in new issue