Función renderMarkdownDoc — Renderizador markdown con títulos e IDs
Librería TypeScript que renderiza Markdown a HTML, extrayendo el primer # título global y generando IDs únicos para todos los headings. Centraliza la lógica de transformación compartida por visor de informes y reports del Supercontexto.
Propósito
Antes: cada visor de documentos tenía su propia lógica para:
- Detectar y extraer el primer
# título(que no debía duplicarse en el HTML) - Generar IDs en headings (para anclas del TOC flotante)
- Deduplicar slugs de headings (si hay dos ”## Resumen” →
resumen+resumen-1) - Recolectar headings h2/h3 para pasarlos al
PageToc
Ahora: una función única (renderMarkdownDoc) que maneja todo esto.
Interfaz
export interface DocHeading {
depth: number; // 2 = h2, 3 = h3 (solo h2/h3 recolectados)
text: string; // Texto del heading (sin etiquetas HTML)
slug: string; // ID único (ej: "resumen" o "resumen-1")
}
export interface MarkdownDoc {
title: string | null; // Texto extraído del primer "# " o null
html: string; // HTML renderizado (sin el # título)
headings: DocHeading[]; // Listado h2/h3 para el TOC
}
export function renderMarkdownDoc(raw: string): MarkdownDoc
Pasos internos
1. Extracción de título
- Regex:
^#\s+(.+)$/m(primer# TEXTOen línea) - Si no existe →
title = null - El primer
#se retira del cuerpo para no duplicarse en HTML
2. Renderización con marked
- Motor: librería
marked(incluida en el proyecto) - Renderer customizado solo para headings:
renderer: {
heading({ tokens, depth, text }) {
let id = slugifyHeading(text);
const n = vistos.get(id) ?? 0;
vistos.set(id, n + 1);
if (n > 0) id = `${id}-${n}`;
// Recolectar solo h2/h3
if (depth >= 2 && depth <= 3)
headings.push({ depth, text, slug: id });
return `<h${depth} id="${id}">...</h${depth}>`;
}
}
3. Slugificación
function slugifyHeading(text: string): string
// Minúsculas
// Normalización Unicode (NFD + eliminar diacríticos)
// Remover caracteres especiales (keep alfanumérico + guiones)
// Trim + collapse espacios a guiones
// Resultado: "¿Qué es esto?" → "que-es-esto"
4. Deduplicación
- Map
vistos: Map<string, number>cuenta ocurrencias por slug - Segunda ocurrencia de “resumen” →
resumen-1 - Tercera →
resumen-2, etc.
Uso
import { renderMarkdownDoc } from '../lib/markdownDoc';
const rawMarkdown = fs.readFileSync(filePath, 'utf-8');
const { title, html, headings } = renderMarkdownDoc(rawMarkdown);
// En el template Astro:
const titulo = title ?? 'Sin título';
<h1>{titulo}</h1>
<Fragment set:html={html} />
<PageToc headings={headings} actions={[...]} />
Ubicaciones en uso
- Informes:
/pages/informes/ver/[slug].astro— renderiza .md del hub - Reports Supercontexto:
/pages/supercontext/reports/[slug].astro— renderiza .md de maintenance/weekly
Nota: Las páginas wiki NO pasan por aquí. Usan el pipeline de contenido Astro (remark/rehype + componente render() de Astro). Este renderizador es solo para documentos sueltos en formato .md.
Diferencias con pipeline wiki
| Aspecto | renderMarkdownDoc | Pipeline Astro (wiki) |
|---|---|---|
| Motor | marked | remark/rehype |
| Entrada | String bruto .md | Astro Content Collection |
| Salida | { title, html, headings } | HTML renderizado + headings en frontmatter |
| IDs en headings | Generados por el renderer | Generados por rehype-autolink-headings |
| Extracción de título | Manual (regex) | En frontmatter |
| Deduplicación slugs | Integrada (Map vistos) | Integrada en rehype |
| Recolección headings | Por renderer customizado | Por Astro.props.headings |
Véase también
- [[entity—src—component—page-toc]]
- [[entity—src—page—wiki-page]]
- [[entity—src—page—informe-viewer]]
- [[entity—src—page—reports-viewer]]