CreaRack-SL

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 # TEXTO en 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

AspectorenderMarkdownDocPipeline Astro (wiki)
Motormarkedremark/rehype
EntradaString bruto .mdAstro Content Collection
Salida{ title, html, headings }HTML renderizado + headings en frontmatter
IDs en headingsGenerados por el rendererGenerados por rehype-autolink-headings
Extracción de títuloManual (regex)En frontmatter
Deduplicación slugsIntegrada (Map vistos)Integrada en rehype
Recolección headingsPor renderer customizadoPor 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]]