npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ngx-docs-markdown-kit/parser-md

v0.1.0

Published

Parser de Markdown extensible para sitios de documentacion -- nucleo agnostico de framework (sin dependencias de Angular), basado en marked. Define el punto de extension (.use()) que consumen los paquetes parser-md-seo y parser-md-code-block.

Downloads

174

Readme

@ngx-docs-markdown-kit/parser-md

Núcleo del kit: parsea Markdown (vía marked) a una estructura de datos tipada (DocPageContent), agnóstica de framework -- sin ninguna dependencia de Angular, usable en cualquier proyecto de Node/TypeScript. Cada encabezado se convierte en un SectionSegment anidado de verdad (un ### dentro de un ## queda como hijo real, no como texto indentado), para que el consumidor pueda renderizar un <section> real por encabezado.

No conoce ni importa ninguna extensión concreta -- expone un único punto de registro, .use(extension), con 3 hooks opcionales (transformToken/buildSegment/extendMeta) para que parser-md-seo, parser-md-code-block, parser-md-image, parser-md-card (y cualquier extensión futura) se enchufen sin que este paquete tenga que importarlas jamás:

import { createParser } from '@ngx-docs-markdown-kit/parser-md';
import { seoExtension } from '@ngx-docs-markdown-kit/parser-md-seo';
import { codeBlockExtension } from '@ngx-docs-markdown-kit/parser-md-code-block';

const parser = createParser().use(seoExtension()).use(codeBlockExtension());
const result = parser.parse(markdownSource);
// result.segments (árbol de secciones), result.headings, result.meta

Qué trae

  • ParserMd/createParser() -- el parser en sí. MarkdownEngine (interfaz mínima { lex, render }) es la abstracción real detrás de marked -- si algún día hiciera falta cambiar de motor, alcanza con implementar esa interfaz (MarkedEngine es la única implementación real hoy).
  • Frontmatter propio, no YAML -- ---\nclave: valor\n--- (líneas planas, sin anidar; también soporta [og:image:url]: valor para claves con :). Deliberadamente mínimo: el formato real que se necesita no justifica traer una librería YAML completa.
  • parseKeyValueLines -- el mismo parser de campos clave: valor que usa el frontmatter, exportado para que extensiones externas (parser-md-image, parser-md-card) lo reusen en vez de reimplementarlo cada una por su cuenta.
  • slugify -- mismo algoritmo de generación de ids de encabezado que usa el parser internamente, exportado para que un consumidor externo pueda calcular el mismo id sin duplicar la lógica.
  • cardsExtension() -- reconoce fences ```cards (una línea href|título|descripción por card) y produce un CardsSegment. Vive en el núcleo (no en un paquete aparte) porque es el mecanismo que usa el propio manifiesto de contenido para las páginas de grupo autogeneradas (___content_dir.md).
  • Callouts -- > [!TIP]\n> texto se convierte en una caja de aviso destacado. También en el núcleo por ser formato de prosa genérico, no una extensión de terceros.

@ngx-docs-markdown-kit/parser-md/manifest (entry point secundario)

Escaneo real de una carpeta en disco (usa node:fs/node:path) para construir el árbol de navegación completo de un sitio -- buildContentManifest(contentDir). Es un entry point aparte para que un consumidor que solo necesita esto (típicamente un script de build) no cargue nada del resto del paquete.

Reglas del escaneo: cada carpeta con contenido navegable necesita su propio ___meta.md; el [slug] de cada página es siempre explícito en el frontmatter, nunca derivado del nombre del archivo en disco (el nombre en disco es 100% cosmético); el orden entre hermanos es alfabético por defecto, o el explícito de un ___order.md si existe en esa carpeta.

Este es el mecanismo real detrás de create-ngx-docs-site (barras de navegación, sitemap, BreadcrumbList de SEO) y de parser-md-converter (README generado a partir del mismo árbol) -- ambos consumen el mismo manifiesto como única fuente de verdad de navegación/contenido, así que nunca pueden desincronizarse entre sí.

Documentación completa

Arquitectura interna, el contrato de extensión (ParserMdExtension) con ejemplos reales de cómo escribir una extensión propia, y la referencia completa del formato de frontmatter/manifiesto: ver el sitio de documentación de este paquete, parser-md.frugocorp.com.

parser-md

parser-md

version node npm

Dependencias:

  • @angular/animations: ^22.1.0
  • @angular/cdk: ^22.1.0
  • @angular/common: ^22.1.0
  • @angular/compiler: ^22.1.0
  • @angular/core: ^22.1.0
  • @angular/forms: ^22.1.0
  • @angular/material: ^22.1.0
  • @angular/platform-browser: ^22.1.0
  • @angular/platform-server: ^22.1.0
  • @angular/router: ^22.1.0
  • @angular/ssr: ^22.1.3
  • @ngx-docs-markdown-kit/parser-md: file:./vendor/parser-md/ngx-docs-markdown-kit-parser-md-0.1.0.tgz
  • @ngx-docs-markdown-kit/parser-md-seo: file:./vendor/parser-md-seo/ngx-docs-markdown-kit-parser-md-seo-0.1.0.tgz
  • @ngx-docs-markdown-kit/parser-md-code-block: file:./vendor/parser-md-code-block/ngx-docs-markdown-kit-parser-md-code-block-0.1.0.tgz
  • @ngx-docs-markdown-kit/parser-md-code-block-themes: file:./vendor/parser-md-code-block-themes/ngx-docs-markdown-kit-parser-md-code-block-themes-0.1.0.tgz
  • @ngx-docs-markdown-kit/parser-md-image: file:./vendor/parser-md-image/ngx-docs-markdown-kit-parser-md-image-0.1.0.tgz
  • @ngx-docs-markdown-kit/parser-md-card: file:./vendor/parser-md-card/ngx-docs-markdown-kit-parser-md-card-0.1.0.tgz
  • @ngx-docs-markdown-kit/parser-md-converter: file:./vendor/parser-md-converter/ngx-docs-markdown-kit-parser-md-converter-0.1.0.tgz
  • @ngx-docs-markdown-kit/ui: file:./vendor/ui/ngx-docs-markdown-kit-ui-0.1.0.tgz
  • rxjs: ~7.8.0
  • tslib: ^2.3.0

Apartados de parser-md

  • Docs -- Documentación de @ngx-docs-markdown-kit/parser-md -- núcleo del parser de Markdown, agnóstico de framework, con punto de extensión .use().
  • Parser MD -- Parser de Markdown extensible, agnostico de framework, nucleo de todo el ecosistema @ngx-docs-markdown-kit.

Docs de parser-md

REGRESAR A APARTADOS DE parser-md


Índice Docs de parser-md

  • Primeros pasos -- Qué es parser-md y cómo instalarlo.
    • Instalación -- Cómo instalar parser-md y armar tu primer parser.
  • Uso -- El API real del núcleo -- parseo, extensiones, frontmatter y el manifiesto de contenido.
  • Arquitectura interna -- Cómo está construido parser-md por dentro -- el algoritmo de .parse() y las decisiones de diseño detrás.
    • Decisiones de diseño -- Por qué parser-md está construido así -- DIP sobre el motor, ids sin duplicar, formato mínimo propio.
    • El pipeline de .parse() -- Recorrido completo del algoritmo detrás de ParserMd.parse() -- frontmatter, tokens, secciones, prosa.
  • Ecosistema -- Quién consume parser-md y cómo se relacionan entre sí las extensiones del kit.
    • Relación con el ecosistema -- Quién depende de parser-md, la dependencia cruzada parser-md-card -> parser-md-image, y el manifiesto compartido.

Primeros pasos de parser-md

< Índice Docs de parser-md


Instalación de parser-md

< Primeros pasos de parser-md


npm install @ngx-docs-markdown-kit/parser-md

Sin dependencias de Angular -- parser-md es 100% agnóstico de framework, basado en marked por debajo. Sirve para parsear Markdown en cualquier contexto Node/browser, no solo dentro de un sitio create-ngx-docs-site.

Tu primer parser

import { createParser } from '@ngx-docs-markdown-kit/parser-md';

const parser = createParser();
const page = parser.parse(`---
[title]: Mi página
---

# Mi página

Contenido normal en **Markdown**.`);

console.log(page.meta.title);   // 'Mi página'
console.log(page.headings);     // [{ id: 'mi-pagina', text: 'Mi página', level: 1 }]
console.log(page.segments);     // [{ type: 'html', html: '...' }]

createParser() no conoce ni importa ninguna extensión específica -- expone un único punto de registro (.use(extension)) que consumen parser-md-seo, parser-md-code-block, parser-md-image, parser-md-card, etc. Ver Escribir una extensión para agregar las tuyas.

Paquete ESM puro

package.json declara "type": "module" y un mapa de exports con 2 entry points -- no hay ninguna variante CommonJS (require), así que el proyecto que lo consuma también tiene que resolver módulos ESM (import, no require):

// entry point raíz -- parseo puro, sin tocar el filesystem
import { createParser, cardsExtension, slugify, parseFrontmatter, parseKeyValueLines } from '@ngx-docs-markdown-kit/parser-md';

// entry point secundario -- usa node:fs/node:path real, aparte para no cargarlo si no hace falta
import { buildContentManifest } from '@ngx-docs-markdown-kit/parser-md/manifest';

Sin dependencia de Angular ni de ningún otro paquete del kit -- la única dependencia real es marked (^18). Sirve igual de bien parseando un string suelto en un script, un test, o (el caso real de uso) el contenido de un sitio create-ngx-docs-site.

Próximos pasos

Uso de parser-md

< Índice Docs de parser-md


Parsear Markdown de parser-md

< Uso de parser-md


parser.parse(markdown) devuelve un DocPageContent, agnóstico de framework (sin HTML de Angular, sin nada especifico de un renderer puntual):

interface DocPageContent {
  readonly segments: DocPageSegment[];
  readonly headings: DocPageHeading[];
  readonly meta: DocPageMeta;
}
  • headings -- lista plana de {id, text, level}, un item por cada heading real del cuerpo (el H1 del título de la página normalmente no cuenta -- vive fuera del cuerpo, en el frontmatter). El id sale de slugify(), la misma función que exporta este paquete.
  • meta -- Record<string, unknown> acumulado por las extensiones instaladas a partir del frontmatter -- parser-md no le da significado a ningún campo en particular, solo lo expone (ver parser-md-seo para el uso real de description/keywords/etc.).
  • segments -- el cuerpo ya estructurado, cada uno con su propio type:
    • { type: 'html', html } -- un tramo de prosa normal, ya renderizado.
    • { type: 'list', ordered, items } -- una lista con contenido anidado (código, otro segmento no-prosa dentro de un item) -- items es un array de DocPageSegment[], recursivo.
    • { type: 'section', id, level, titleHtml, segments } -- un heading + TODO su contenido, de forma recursiva: un h3 dentro de un h2 aparece como un SectionSegment DENTRO de segments del h2, no solo indentado -- pensado para que el consumidor renderice un <section> real por cada uno (requisito de SEO, ver parser-md-seo).
    • { type: 'cards', cards } -- una grilla de tarjetas, la produce cardsExtension() (ver más abajo) a partir de un fence ```cards.
    • Cualquier extensión instalada (parser-md-image, parser-md-code-block, etc.) puede agregar sus propios tipos -- el consumidor discrimina por type con un @switch/switch.

Extensión incluida: cardsExtension()

import { createParser, cardsExtension } from '@ngx-docs-markdown-kit/parser-md';

const parser = createParser().use(cardsExtension());

Reconoce un fence ```cards cuyo cuerpo es una línea href|título|descripción por card (sin escape de |) y produce un CardsSegment. No está pensado para escribirse a mano -- lo genera buildContentManifest() (ver El manifiesto de contenido) por cada carpeta de contenido, como grilla de sus hijos directos.

Cómo funciona por dentro

El anidamiento de SectionSegment es enteramente FABRICADO por parser-md (marked solo entrega una lista plana de tokens, sin relación estructural entre un ## y el ### que sigue), y el id de cada heading se calcula una sola vez y se comparte entre los dos lugares que lo necesitan. Ver Arquitectura interna para el algoritmo completo de anidamiento y Decisiones de diseño para el detalle del cacheo de id.

Frontmatter y parseKeyValueLines de parser-md

< Uso de parser-md


El frontmatter usa la misma convención que Jekyll/Hugo/Astro (--- ... --- al principio del archivo) pero DELIBERADAMENTE no usa una librería de YAML real -- el formato soportado es mínimo a propósito: líneas planas clave: valor, nada anidado.

import { parseFrontmatter } from '@ngx-docs-markdown-kit/parser-md';

const { frontmatter, body } = parseFrontmatter(`---
[title]: Mi página
[slug]: mi-pagina
---

# Mi página`);

// frontmatter = { title: 'Mi página', slug: 'mi-pagina' }
// body = '\n# Mi página'

Los valores quedan CRUDOS, sin interpretar -- convertir keywords a un array, por ejemplo, es responsabilidad de la extensión que consuma ese campo puntual (ver parser-md-seo), no del núcleo.

Dos formatos por línea, ambos válidos

parseFrontmatter/parseKeyValueLines (exportada aparte, la reusa cualquier extensión con su propio bloque de campos, ej. el fence ```image de parser-md-image) aceptan:

  • clave: valor -- el de siempre, corta en el primer : de la línea.
  • [clave]: valor -- con corchetes -- la clave puede tener CUALQUIER carácter, incluidos : (ej. [og:image:url]: /x.jpg), porque el corte es por el ]: que cierra el corchete, no por el primer : de la línea. Ambos formatos conviven en el mismo bloque sin problema.

Una línea sin clave o sin valor (a cualquiera de los 2 lados del separador) se ignora, no rompe el parseo del resto.

Por qué parseKeyValueLines vive en el núcleo

No es solo el algoritmo del frontmatter -- se exporta aparte porque más de una extensión externa necesita el MISMO formato clave: valor para su propio bloque de campos dentro de un fence propio (ej. src/alt/align en el fence ```image de parser-md-image). Antes de esta extracción, cada extensión tenía su propia copia casi idéntica del algoritmo, sin nada que las mantuviera sincronizadas si una corregía una regla de borde (comentarios, escapes, líneas vacías) y la otra no -- ahora hay una sola fuente de verdad, y las extensiones la importan desde parser-md en vez de reimplementarla.

slugify

import { slugify } from '@ngx-docs-markdown-kit/parser-md';

slugify('Política de Privacidad'); // 'politica-de-privacidad'

Quita acentos (normaliza NFD, descarta la marca de acento combinante) y cualquier carácter que no sea letra/número/guion, después baja a minúsculas y cambia espacios por guiones -- la misma función que arma el id de cada heading en DocPageHeading. Exportada para que un consumidor externo (ej. un panel "en esta página", o un test de integridad de links) calcule el MISMO id sin duplicar el algoritmo.

El manifiesto de contenido de parser-md

< Uso de parser-md


@ngx-docs-markdown-kit/parser-md/manifest (entry point APARTE del núcleo -- usa node:fs/ node:path reales, así que un consumidor que solo necesita parsear un string de Markdown no carga nada de esto):

import { buildContentManifest } from '@ngx-docs-markdown-kit/parser-md/manifest';

const manifest = buildContentManifest('./content-parser-md');
// { pages: ManifestPage[], groups: ManifestGroup[] }

Escanea recursivamente una carpeta real en disco y devuelve la estructura completa -- lo usa generate-content-manifest.mjs (el script que corre create-ngx-docs-site antes de cada npm start/npm run build) para armar el manifiesto de navegación del sitio, pero es reusable por cualquier consumidor que necesite lo mismo (ej. parser-md-seo para el sitemap.xml).

Reglas reales

  • El slug es SIEMPRE explícito -- [slug]: valor en el frontmatter de la página, o en el ___meta.md de una carpeta -- NUNCA se deriva del nombre de archivo. El nombre en disco es 100% cosmético (un equipo puede usar prefijos de orden propios, mayúsculas, lo que prefiera) sin que eso rompa ninguna URL. Solo acepta minúsculas/números/guiones simples entre segmentos (/^[a-z0-9]+(-[a-z0-9]+)*$/) -- un slug inválido o faltante frena el escaneo con un error que dice exactamente qué archivo lo necesita, nunca una URL rota en silencio.
  • El orden entre hermanos es alfabético por nombre CRUDO de archivo/carpeta por default, o el orden explícito de un ___order.md en esa carpeta (una lista de nombres de archivo, uno por línea) si existe -- lo que no aparezca en la lista cae al final, alfabético.
  • Una carpeta con contenido navegable necesita su propio ___meta.md -- mismo formato de frontmatter que cualquier página ([slug]/[title]/[description]), obligatorio (nunca {}).
  • sourcePath vs. group/slug -- sourcePath es la ruta REAL en disco (para quien necesite leer/re-escribir el archivo), group/slug son para navegación/URLs -- deliberadamente independientes, no se puede reconstruir uno a partir del otro.

Headings con jerarquía (ManifestHeading)

Cada ManifestPage.headings extiende DocPageHeading (ver Parsear Markdown) con parentId: string | null -- el heading anterior más cercano con nivel MENOR (null para el primero de la página, o si no hay ninguno con nivel menor antes). Se calcula en el manifiesto, no en ParserMd, porque "quién es hijo de quién" es una necesidad de navegación/manifiesto, no del renderizado del cuerpo en sí. El cálculo es un solo recorrido con una pila de "ancestros candidatos": cada vez que aparece un heading nuevo, se descartan de la pila todos los que tengan nivel >= al suyo (ya no pueden ser ancestro de nada que venga después), y el que quede arriba (si queda alguno) es su padre.

lastmod

Cada ManifestPage.lastmod sale de la fecha de modificación REAL del archivo en disco (statSync(entryPath).mtime, recortada a AAAA-MM-DD) -- no de un campo de frontmatter que un autor tenga que mantener a mano. Es el dato que usa parser-md-seo para el <lastmod> de cada URL en el sitemap.xml generado.

Ver Relación con el ecosistema para quién más consume este mismo manifiesto (navegación, sitemap, README generado) y por qué nunca pueden desincronizarse entre sí.

Escribir una extensión de parser-md

< Uso de parser-md


parser-md no conoce ni importa ninguna extensión específica -- expone un único punto de registro:

const parser = createParser().use(miExtension()).use(otraExtension());

Cada extensión implementa ParserMdExtension (los 3 métodos son todos opcionales):

interface ParserMdExtension {
  readonly name: string;
  transformToken?(token: Token, ctx: ParserMdContext): Token;
  buildSegment?(token: Token, ctx: ParserMdContext): DocPageSegment | undefined;
  extendMeta?(meta: DocPageMeta, frontmatter: Readonly<Record<string, string>>, ctx: ParserMdContext): void;
}
  • transformToken -- transforma un token en otro ANTES de construir segmentos (ej. un blockquote > [!TIP] que se convierte en el HTML de un aviso destacado). Se aplica en el orden en que las extensiones se registraron con .use(). Devolvé el mismo token si no aplica.
  • buildSegment -- intercepta un token para producir un segmento propio -- el caso más común es un fence con un lenguaje reservado (```image, ```code-block, etc.): token.type === 'code' y token.lang matchea tu convención. Devolvé undefined si no te corresponde -- el núcleo sigue probando con la siguiente extensión, y si ninguna lo reclama, el fence cae al render por default de Markdown.
  • extendMeta -- corre una vez por página, ANTES de tokenizar el cuerpo, para completar meta a partir del frontmatter ya parseado (valores crudos, sin interpretar -- interpretarlos es responsabilidad tuya).

Ejemplo mínimo real

Una extensión que reconoce ```highlight y lo baja a una nota en negrita:

import type { DocPageSegment, ParserMdExtension } from '@ngx-docs-markdown-kit/parser-md';
import type { Token, Tokens } from 'marked';

export function highlightExtension(): ParserMdExtension {
  return {
    name: 'highlight',
    buildSegment(token: Token): DocPageSegment | undefined {
      if (token.type !== 'code') return undefined;
      const codeToken = token as Tokens.Code;
      if ((codeToken.lang ?? '').trim() !== 'highlight') return undefined;
      return { type: 'html', html: `<strong>${codeToken.text}</strong>` };
    },
  };
}

Mismo patrón real que usan parser-md-image/parser-md-code-block/parser-md-card -- ver sus propios sitios de documentación para ejemplos más completos (fences con múltiples campos, ctx.lex para re-analizar contenido anidado).

El orden de .use() importa

  • buildSegment -- el núcleo prueba las extensiones EN EL ORDEN en que se registraron y se queda con la primera que devuelve un segmento; las siguientes ni se llaman para ese token. Si dos extensiones reconocieran el mismo lang de fence, gana la que se registró primero -- en la práctica no pasa porque cada una usa su propio nombre reservado (image, code-block, content-card, cards), pero el orden de registro es lo que decide un empate si alguna vez lo hubiera.
  • transformToken -- se aplica en cadena, TODAS las extensiones, siempre en el orden de registro (nunca hay "la primera gana" acá) -- cada una recibe el token ya transformado por la anterior. El callout > [!TIP] del núcleo (transformCallout) corre primero, antes que cualquier extensión instalada.
  • extendMeta -- también en cadena, todas, mismo orden -- cada extensión ve el meta ya completado por las anteriores, así que puede leer (no solo escribir) lo que otra extensión registrada antes ya puso ahí.

ParserMdContext

Las 3 funciones que le pasa el núcleo a cada extensión, para operar con el MISMO motor (nunca instanciar marked por tu cuenta dentro de una extensión):

  • ctx.lex(markdown) -- tokeniza un fragmento (ej. el interior de tu propio fence).
  • ctx.render(tokens) -- renderiza tokens a HTML, con el mismo renderer de heading-con-id que usa el resto de la página.
  • ctx.buildSegments(tokens) -- vuelve a aplicar el pipeline COMPLETO de construcción de segmentos (útil si tu fence contiene Markdown libre que a su vez puede usar otras extensiones).

Arquitectura interna de parser-md

< Índice Docs de parser-md


Decisiones de diseño de parser-md

< Arquitectura interna de parser-md


Un recorrido de las decisiones menos obvias del núcleo -- el "por qué", no solo el "qué" (eso ya está en Uso y en El pipeline de .parse()).

MarkdownEngine -- Inversión de Dependencias real, no de nombre

ParserMd nunca importa marked directo -- depende de la interfaz MarkdownEngine { lex, render } (markdown-engine.ts), y recibe una implementación concreta por constructor (MarkedEngine por default). No es una abstracción decorativa: hay un test (parser.spec.ts) que le pasa a createParser() un MarkdownEngine FALSO (que ni siquiera usa marked) y confirma que ParserMd sigue funcionando igual -- si algún día marked dejara de servir (rendimiento, tamaño de bundle, lo que sea), alcanza con escribir un MarkdownEngine nuevo, sin tocar parser.ts ni ninguna extensión (todas ven ctx.lex/ctx.render, nunca el motor directo).

Por qué el heading nunca lleva id en el HTML

MarkedEngine sobreescribe el renderer de heading para que devuelva <h2>Texto</h2>, nunca <h2 id="...">. La razón es evitar que el mismo id termine escrito en 2 lugares del DOM: el SectionSegment que envuelve a ese heading ya lleva su propio id (ver paso 5 de El pipeline de .parse()), pensado para que el consumidor lo ponga en el elemento <section id="..."> que envuelve al heading, no en el heading mismo. Si el <h2> también trajera id, un ancla (#mi-seccion) podría matchear cualquiera de los 2 elementos según cuál quedara primero en el DOM -- una ambigüedad evitable con una sola regla: el id vive en la sección, punto.

Por qué el frontmatter no es YAML real

El formato soportado es deliberadamente mínimo -- líneas planas clave: valor (o [clave]: valor con corchetes), sin anidamiento, sin listas, sin tipos. No es una limitación técnica (agregar una dependencia de YAML sería trivial) sino una decisión: todo el contenido real del ecosistema (títulos, descripciones, slugs, keywords, campos de un fence de imagen) es una lista corta de pares planos, y un regex + split (parseKeyValueLines, ver Frontmatter y parseKeyValueLines) alcanza para eso sin arrastrar un parser YAML completo (con todo lo que implica: tipos implícitos, anclas, fechas parseadas solas, etc.) para un caso de uso que nunca lo necesita.

WeakMap a nivel de módulo, no de instancia

headingIds (parser.ts) es un WeakMap<Tokens.Heading, string> declarado FUERA de la clase ParserMd, a nivel de módulo. La clave es el objeto token en sí (identidad, no texto) -- un mismo heading pasa por 2 caminos independientes dentro de un solo .parse() (la lista headings y su SectionSegment), y ambos necesitan el id IDÉNTICO. Ser un WeakMap (no un Map) es intencional: no retiene el token en memoria más allá de lo que ya lo retiene el resto del árbol de tokens -- una vez que .parse() termina y el árbol se descarta, la entrada se libera sola, sin necesitar limpieza manual.

Por qué callouts.ts y cards-extension.ts viven en el núcleo, no en paquetes aparte

Ambos podrían haber sido extensiones externas (implementan el mismo contrato, transformToken/buildSegment), pero se quedaron en parser-md por razones distintas:

  • Callouts (> [!TIP]) es formato de PROSA genérico -- no depende de ningún concepto externo (SEO, resaltado de código, imágenes), es una convención de Markdown extendido comparable a negrita o listas. No tiene sentido instalarlo aparte cuando cualquier consumidor de parser-md lo va a querer.
  • Cards (fence ```cards) vive acá porque su único consumidor real, ___content_dir.md (generado automáticamente, ver El manifiesto de contenido), es parte del MISMO mecanismo de manifiesto que ya vive en este paquete (buildContentManifest) -- separarlo en un paquete aparte hubiera significado que el propio núcleo (content-manifest.ts) dependiera de una extensión externa para generar contenido consumible por sí mismo, una dependencia circular de diseño que se evita manteniendo ambas piezas juntas.

Por qué key-value-lines.ts es un módulo propio (y no parte de frontmatter.ts)

Aunque el frontmatter es su único consumidor DENTRO de este paquete, parseKeyValueLines se exporta desde parser-md (no queda privado dentro de frontmatter.ts) porque extensiones externas la necesitan para sus propios bloques de campos dentro de un fence (parser-md-image, por ejemplo, con src/alt/align/etc. dentro de un fence ```image). Antes de esta extracción cada extensión tenía su propia copia casi idéntica del algoritmo; separarlo en su propio módulo, con su propia exportación pública, es lo que hace posible que sea una sola fuente de verdad en vez de 2 o 3 copias que corren el riesgo de divergir con el tiempo.

El pipeline de .parse() de parser-md

< Arquitectura interna de parser-md


ParserMd.parse(markdown) (parser.ts, ~200 líneas) es la única función que le importa a un consumidor, pero por dentro es una cadena de pasos bien separados. Esta página recorre esa cadena en orden real.

1. Separar frontmatter

parseFrontmatter(markdown) corta el bloque ---\n...\n--- inicial (si existe) del resto del cuerpo -- ver Frontmatter y parseKeyValueLines. De acá en adelante, frontmatter y body viajan por separado: el frontmatter nunca se tokeniza como Markdown, y el body nunca vuelve a mirarse en busca de metadatos.

2. extendMeta -- una pasada por extensión, antes de tocar el cuerpo

Cada extensión registrada (en el orden de .use()) recibe el frontmatter crudo y puede escribir en meta. Corre completo ANTES de tokenizar el body a propósito: así ninguna extensión necesita esperar a que el cuerpo esté parseado para saber, por ejemplo, si la página declaró [title]/[description] -- son 2 fases independientes, no una sola pasada mezclada.

3. Tokenizar + applyTokenTransforms

engine.lex(body) (MarkedEngine por debajo, ver Decisiones de diseño) produce la lista de tokens de marked -- siempre plana: un heading nunca aparece "dentro" de otro en el árbol de tokens, sin importar qué tan anidado se vea el Markdown fuente. Esa lista pasa, token por token, por applyTokenTransforms: primero transformCallout (del núcleo, convierte > [!TIP] en HTML de aviso), después el transformToken de cada extensión instalada, en cadena, en el orden en que se registraron -- cada una ve el token ya transformado por la anterior.

4. Extraer headings

Sobre esa misma lista (ya transformada) se recorre una vez más, tomando solo los tokens type === 'heading', para armar la lista plana DocPageHeading[] -- el id de cada uno sale de getHeadingId(), que cachea por identidad del token en un WeakMap (ver Decisiones de diseño para el porqué).

5. buildSectionedSegments -- fabricar la jerarquía

Acá es donde el árbol de secciones se arma de verdad, porque los tokens NO vienen anidados (paso 3). El algoritmo recorre la lista top-level:

  • Si el token es un heading de nivel N: busca hacia adelante hasta el próximo heading de nivel <= N (o el final de la lista) -- todo lo que quedó en el medio es "el contenido de esta sección". Ese contenido se vuelve a procesar con la MISMA función, recursivamente -- así un ### que cae dentro de ese rango termina como SectionSegment HIJO real del ##, no como hermano. El resultado es un SectionSegment con id/level/titleHtml (el heading renderizado solo, vía engine.render([heading])) y segments (el resultado de la recursión).
  • Si el token NO es un heading (contenido antes del primer heading, o una página sin ninguno): se junta el tramo contiguo sin heading y se delega a buildSegments (paso 6).

6. buildSegments -- agrupar prosa, delegar a extensiones

Dentro de cada tramo sin headings (el contenido de una sección, o el tramo inicial de la página), buildSegments recorre token por token:

  1. Primero prueba cada extensión (tryBuildExtensionSegment) -- si alguna reclama el token vía buildSegment (típicamente un fence con un lang reservado), ese token se vuelve su propio segmento y CUALQUIER prosa acumulada hasta ahí se cierra (flushProseGroup) como un HtmlSegment aparte -- así un párrafo, un fence de imagen, y otro párrafo quedan como 3 segmentos distintos, no mezclados en un solo bloque de HTML.
  2. Caso especial: listas con código anidado. Si el token es una lista y hasNestedCode detecta un bloque de código en cualquier profundidad (dentro de un item, dentro de un item anidado, etc.), la lista completa se convierte en un ListSegment cuyos items son, cada uno, buildSegments(item.tokens) recursivo -- esto es lo que permite que un fence ```code-block DENTRO de un item de lista siga siendo interceptado por parser-md-code-block como su propio segmento, en vez de quedar atrapado como HTML crudo dentro del <li> (que es lo que haría el render por default de marked para una lista completa).
  3. Todo lo demás se acumula como prosa en proseGroup, hasta que algo lo corta (un segmento de extensión, una lista con código, o el final del tramo) -- en ese punto se renderiza junto (engine.render(proseGroup)) como un único HtmlSegment. Es deliberado: dos párrafos seguidos sin nada en el medio no necesitan 2 llamadas a render ni 2 segmentos, uno alcanza.

El resultado

{ segments, headings, meta } -- segments ya tiene la jerarquía real de secciones (paso 5), con prosa agrupada y fences reclamados por extensiones adentro de cada una (paso 6); headings es la lista plana calculada en el paso 4, con los mismos id que ya quedaron en los SectionSegment del paso 5 (gracias al WeakMap del paso 4). El consumidor nunca necesita re-tokenizar ni re-calcular nada de esto -- todo el trabajo de estructura ya está hecho.

Ecosistema de parser-md

< Índice Docs de parser-md


Relación con el ecosistema de parser-md

< Ecosistema de parser-md


@ngx-docs-markdown-kit/parser-md es el núcleo -- todo lo demás en el kit depende de él, nunca al revés. parser-md no importa, no conoce, y no tiene ninguna dependencia (ni en tiempo de compilación ni en tiempo de ejecución) de ninguno de los paquetes que lo consumen -- es lo que hace posible que el punto de extensión (.use()) sea real y no solo nominal: cualquiera puede escribir una extensión nueva sin tocar este paquete ni esperar que lo "sepa" de antemano.

Quién lo consume hoy

  • parser-md-seo -- extrae metadatos de SEO del frontmatter (meta) y expone servicios de Angular (meta tags, JSON-LD) + funciones (sitemap.xml/robots.txt) a partir del mismo ContentManifest que genera este paquete.
  • parser-md-code-block -- extensión que reconoce fences code-block/card-code-block y los renderiza con componentes de Angular.
  • parser-md-image -- extensión que reconoce un fence image (src/alt/align/width/caption, parseado con parseKeyValueLines de este mismo paquete) y lo renderiza como figura enriquecida.
  • parser-md-card -- extensión que reconoce un fence content-card y lo renderiza con <mat-card> real de Angular Material.
  • parser-md-converter -- genera un README.md multi-flavor (GitHub/npmjs/NuGet) a partir del ContentManifest de un sitio; depende de parser-md en tiempo de ejecución para reusar parseKeyValueLines en vez de reimplementar el formato clave: valor por su cuenta.
  • create-ngx-docs-site -- el consumidor final: el CLI que genera un sitio completo instala parser-md más las extensiones que necesite, y las encadena con .use() en su plantilla (core/).

Todas las extensiones anteriores declaran @ngx-docs-markdown-kit/parser-md como dependencia real (^0.0.1) e implementan ParserMdExtension importando sus tipos (DocPageSegment, ParserMdContext, etc.) desde este paquete -- nunca definen su propia versión de esos contratos.

Uso real combinado (create-ngx-docs-site)

Ninguna extensión se usa sola en producción -- un sitio real las encadena todas:

const parser = createParser()
  .use(seoExtension())
  .use(codeBlockExtension())
  .use(cardsExtension())
  .use(imageExtension())
  .use(contentCardExtension());

El orden importa para transformToken/extendMeta (se aplican todos, en cadena, en ese orden) y decide un eventual empate en buildSegment (la primera que reclama un token gana) -- ver El orden de .use() importa para el detalle completo.

La dependencia cruzada: parser-md-cardparser-md-image

No todas las dependencias del ecosistema apuntan hacia este núcleo -- parser-md-card depende además, como dependencia real de paquete, de parser-md-image (para reusar su lógica de imagen dentro de la card). Es la única dependencia cruzada ENTRE extensiones hoy: todas las demás solo dependen de parser-md, nunca entre sí.

El manifiesto compartido

buildContentManifest() (@ngx-docs-markdown-kit/parser-md/manifest, ver El manifiesto de contenido) es la única fuente de verdad sobre la estructura de contenido de un sitio -- la navegación y el BreadcrumbList de create-ngx-docs-site, el sitemap.xml de parser-md-seo, y el README.md de parser-md-converter leen exactamente el mismo árbol de pages/groups, calculado UNA sola vez a partir de la misma carpeta public/content-parser-md/. Ningún consumidor mantiene su propio escaneo aparte -- si tuvieran cada uno el suyo, nada garantizaría que la navegación, el sitemap y el README describieran la misma estructura de páginas.

Parser MD de parser-md

REGRESAR A APARTADOS DE parser-md


GitHub | Sitio | FrugoCorp

parser-md es el nucleo del ecosistema: un parser de Markdown extensible, construido sobre marked, sin ninguna dependencia de Angular. Cada libreria del kit (SEO, bloques de codigo, imagenes, tarjetas) es una extension real que se registra sobre este mismo parser via su punto de extension (.use()) -- no un fork, no una reimplementacion paralela.

Por que un nucleo sin Angular

Separar el parseo del render deja el motor de Markdown testeable y reusable sin levantar un proyecto Angular completo -- el mismo manifiesto de contenido que arma parser-md (frontmatter, jerarquia de carpetas, encabezados) lo consume tanto el navegador (barras de navegacion, on-this- page) como scripts de build plano (sitemap.xml, README multi-flavor), corriendo con node sin bundler.

Que trae de base

Frontmatter tipado con slugs SIEMPRE explicitos (nunca adivinados del nombre de archivo/carpeta), manifiesto de contenido navegable con cards autogeneradas por grupo, callouts (> [!TIP], > [!WARNING], etc.) y slugify de encabezados listo para anclas/on-this-page. Todo lo demas (SEO, bloques de codigo, imagenes, tarjetas de contenido) es opcional y vive en su propio paquete.