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.
268 lines
11 KiB
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);
|
|
}
|