You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
senzapaura_es/src/lib/content/index.ts

268 lines
11 KiB

/**
* Carga del contenido.
*
* Todo el contenido se lee de `src/content/**` con `import.meta.glob`, asi que
* es estatico: Vite lo resuelve en tiempo de compilacion y no hay ni base de
* datos ni peticiones en tiempo de ejecucion para el catalogo. (La base de
* datos existe, pero solo guarda cuentas, carritos y compras.)
*
* Cada glob trae el modulo entero: el frontmatter (`metadata`) para los
* listados y el componente compilado (`default`) para el cuerpo del Markdown
* (la letra de un tema, el texto de la bio).
*
* Se probo a separarlo en dos globs, uno eager para el frontmatter y otro
* perezoso para el cuerpo, y no sirve de nada: al importar el modulo de forma
* estatica para leer `metadata`, el bundler ya no puede llevarse el cuerpo a
* otro trozo, y avisa de ello. Con un catalogo pequeno el coste es despreciable.
* Si el catalogo crece hasta cientos de temas, la salida es un plugin de Vite
* que exponga el frontmatter como modulo virtual, para que los listados no
* arrastren las letras.
*/
import type { Component } from 'svelte';
import type { Album, Cancion, EntradaDeBlog, Estilo, PaginaMeta } from '$lib/types';
import { porFechaDesc } from '$lib/utils/date';
import * as q from '$lib/catalogo/consultas';
import { slugDesdeRuta } from './fields';
import {
parsearAlbum,
parsearCancion,
parsearEntrada,
parsearEstilo,
parsearPagina,
aSlug
} from './schema';
type Frontmatter = Record<string, unknown>;
type ModuloMarkdown = { default: Component; metadata: Frontmatter };
/* -------------------------------------------------------------------------
* Globs
* ---------------------------------------------------------------------- */
const modEstilos = import.meta.glob<ModuloMarkdown>('/src/content/estilos/*.md', { eager: true });
const modAlbumes = import.meta.glob<ModuloMarkdown>('/src/content/albumes/*.md', { eager: true });
const modCanciones = import.meta.glob<ModuloMarkdown>('/src/content/canciones/*.md', {
eager: true
});
const modPaginas = import.meta.glob<ModuloMarkdown>('/src/content/paginas/*.md', { eager: true });
const modBlog = import.meta.glob<ModuloMarkdown>('/src/content/blog/*.md', { eager: true });
/** Aplica un parser al frontmatter de cada archivo del glob. */
function mapear<T>(
glob: Record<string, ModuloMarkdown>,
parser: (datos: Frontmatter, slug: string, archivo: string) => T
): T[] {
return Object.entries(glob).map(([ruta, modulo]) =>
parser(modulo.metadata ?? {}, slugDesdeRuta(ruta), ruta)
);
}
/** Busca el componente compilado de un slug dentro de un glob. */
function cuerpo(glob: Record<string, ModuloMarkdown>, slug: string): Component | undefined {
const entrada = Object.entries(glob).find(([ruta]) => slugDesdeRuta(ruta) === slug);
return entrada?.[1].default;
}
/* -------------------------------------------------------------------------
* Colecciones ya normalizadas y ordenadas
* ---------------------------------------------------------------------- */
/** Estilos ordenados: primero el principal, luego por `orden` y nombre. */
export const estilos: Estilo[] = mapear(modEstilos, parsearEstilo).sort(
(a, b) =>
Number(b.principal) - Number(a.principal) ||
a.orden - b.orden ||
a.nombre.localeCompare(b.nombre)
);
/** Albumes declarados como archivo, del mas reciente al mas antiguo. */
export const albumes: Album[] = mapear(modAlbumes, parsearAlbum).sort(porFechaDesc);
/** Todas las canciones, ordenadas por album y numero de pista. */
export const canciones: Cancion[] = mapear(modCanciones, parsearCancion).sort(
(a, b) => (a.album ?? '').localeCompare(b.album ?? '') || (a.numero ?? 0) - (b.numero ?? 0)
);
/** Paginas de texto libre (bio, aviso legal...). */
export const paginas: PaginaMeta[] = mapear(modPaginas, parsearPagina);
/**
* Entradas del blog, de la mas reciente a la mas antigua. Los borradores se
* quedan fuera aqui, no en cada pagina: asi no hay forma de olvidarse de
* filtrarlos en un listado nuevo.
*/
export const entradas: EntradaDeBlog[] = mapear(modBlog, parsearEntrada)
.filter((e) => !e.borrador)
.sort(porFechaDesc);
/*
* Los textos legales tampoco se leen ya de aquí: están en `documento_legal` y
* sus versiones, y los sirve `$lib/server/legal`. El texto en vigor no es un
* archivo sino una versión con fecha, porque quien compró en marzo se acogió a
* las condiciones de marzo y eso hay que poder demostrarlo.
*
* Los `.md` siguen en `src/content/legal` porque son de donde salió el volcado.
*/
export function obtenerEntrada(slug: string): EntradaDeBlog | undefined {
return entradas.find((e) => e.slug === slug);
}
/** La anterior y la siguiente en el tiempo, para seguir leyendo. */
export function vecinasDeEntrada(slug: string): {
anterior?: EntradaDeBlog;
siguiente?: EntradaDeBlog;
} {
const i = entradas.findIndex((e) => e.slug === slug);
if (i === -1) return {};
// El listado va de mas nueva a mas vieja: la "siguiente" en el tiempo es la
// que esta antes en el array.
return { anterior: entradas[i + 1], siguiente: entradas[i - 1] };
}
/** Entradas que hablan de un tema concreto. */
/*
* El taller ya no se lee de aquí.
*
* Vive en PostgreSQL desde que la ficha de género necesitó países, instrumentos,
* letristas, obras y linaje: relaciones que en el frontmatter de un Markdown no
* caben. Lo sirve `$lib/server/taller`.
*
* Los archivos de `src/content/taller` siguen ahí a propósito: son de dónde salió
* el volcado y con lo que se reconstruye la base. Lo que se ha quitado son los
* lectores, para que nadie los use por costumbre y edite un archivo que ya no
* pinta nada en pantalla.
*/
/* -------------------------------------------------------------------------
* Album sintetico de singles
* ---------------------------------------------------------------------- */
/* -------------------------------------------------------------------------
* Consultas
*
* No se escriben aquí: viven en `$lib/catalogo/consultas`, sin saber de dónde
* salen los datos. Este módulo solo las ata a los archivos, y
* `$lib/server/catalogo` las ata a la base. Es lo que hace que cambiar de
* fuente no pueda cambiar el resultado; dos copias «parecidas» habrían sido
* dos sitios donde un día discrepan.
* ---------------------------------------------------------------------- */
const CATALOGO: q.Catalogo = { estilos, albumes, canciones };
/* -------------------------------------------------------------------------
* Coherencia entre colecciones
* ---------------------------------------------------------------------- */
/**
* Comprueba que las referencias cruzadas del contenido existen.
*
* Se ejecuta al importar el modulo, es decir en la primera compilacion: una
* cancion que apunta a un album borrado rompe el build con un mensaje claro,
* en vez de desaparecer en silencio de los listados.
*/
function validarReferencias(): void {
const slugsEstilo = new Set(estilos.map((e) => e.slug));
const slugsAlbum = new Set(albumes.map((a) => a.slug));
for (const album of albumes) {
if (!slugsEstilo.has(album.estilo)) {
throw new Error(
`El album "${album.slug}" declara el estilo "${album.estilo}", que no existe en src/content/estilos/.`
);
}
if (album.slug.startsWith(q.PREFIJO_SINGLES)) {
throw new Error(
`El album "${album.slug}" usa el prefijo "${q.PREFIJO_SINGLES}", reservado para los albumes de singles que se generan solos.`
);
}
}
for (const cancion of canciones) {
if (cancion.album !== null && !slugsAlbum.has(cancion.album)) {
throw new Error(
`La cancion "${cancion.slug}" declara el album "${cancion.album}", que no existe en src/content/albumes/. Quitalo si es un single suelto.`
);
}
if (!slugsEstilo.has(cancion.estilo)) {
throw new Error(
`La cancion "${cancion.slug}" declara el estilo "${cancion.estilo}", que no existe en src/content/estilos/.`
);
}
}
const principales = estilos.filter((e) => e.principal);
if (principales.length > 1) {
throw new Error(
`Hay ${principales.length} estilos marcados como principal (${principales
.map((e) => e.slug)
.join(', ')}). Solo puede haber uno.`
);
}
}
validarReferencias();
export const PREFIJO_SINGLES = q.PREFIJO_SINGLES;
export const slugSingles = q.slugSingles;
export const albumDeCancion = q.albumDeCancion;
export const obtenerEstilo = (slug: string) => q.obtenerEstilo(CATALOGO, slug);
export const estiloPrincipal = () => q.estiloPrincipal(CATALOGO);
export const cancionesDeAlbum = (albumSlug: string) => q.cancionesDeAlbum(CATALOGO, albumSlug);
export const singlesDeEstilo = (estiloSlug: string) => q.singlesDeEstilo(CATALOGO, estiloSlug);
export const albumSinglesDeEstilo = (estiloSlug: string) =>
q.albumSinglesDeEstilo(CATALOGO, estiloSlug);
export const obtenerAlbum = (slug: string) => q.obtenerAlbum(CATALOGO, slug);
export const obtenerCancion = (slug: string) => q.obtenerCancion(CATALOGO, slug);
export const albumesDeEstilo = (estiloSlug: string) => q.albumesDeEstilo(CATALOGO, estiloSlug);
export const slugsDeAlbum = () => q.slugsDeAlbum(CATALOGO);
export const estilosConObra = () => q.estilosConObra(CATALOGO);
export const obtenerEstiloConObra = (estiloSlug: string) =>
q.obtenerEstiloConObra(CATALOGO, estiloSlug);
export const etiquetas = () => q.etiquetas(CATALOGO);
export const etiquetasDeEstilo = (estiloSlug: string, cuantas = 4) =>
q.etiquetasDeEstilo(CATALOGO, estiloSlug, cuantas);
export const obtenerEtiqueta = (slug: string) => q.obtenerEtiqueta(CATALOGO, slug);
export const portadaDeCancion = (cancion: Cancion) => q.portadaDeCancion(CATALOGO, cancion);
/** Slug con el que enlazar una etiqueta escrita a mano. */
export function slugDeEtiqueta(nombre: string): string {
return aSlug(nombre);
}
export function obtenerPagina(slug: string): PaginaMeta | undefined {
return paginas.find((p) => p.slug === slug);
}
/* -------------------------------------------------------------------------
* Cuerpos de Markdown
* ---------------------------------------------------------------------- */
/** Componente con el texto de una entrada del blog. */
export function cuerpoEntrada(slug: string): Component | undefined {
return cuerpo(modBlog, slug);
}
/** Componente con la letra (o las notas) de un tema. */
export function cuerpoCancion(slug: string): Component | undefined {
return cuerpo(modCanciones, slug);
}
/** Componente con las notas de un album. */
export function cuerpoAlbum(slug: string): Component | undefined {
return cuerpo(modAlbumes, slug);
}
/** Componente con el texto de un estilo (introduccion del bloque). */
export function cuerpoEstilo(slug: string): Component | undefined {
return cuerpo(modEstilos, slug);
}
/** Componente con el texto de una pagina libre. */
export function cuerpoPagina(slug: string): Component | undefined {
return cuerpo(modPaginas, slug);
}

Powered by TurnKey Linux.