markdown

js/format/markdown.ts

fino:format/markdown - safe Markdown parser and HTML renderer for documentation and templates.

This module implements a practical CommonMark/GFM-oriented Markdown surface in TypeScript. It supports headings, paragraphs, blockquotes, thematic breaks, fenced code, nested ordered and unordered lists, task-list markers, GFM tables, reference links, autolinks, emphasis, strong text, code spans, strikethrough, links, images, and raw HTML with safe defaults.

Parsing and rendering are split: parseMarkdown() produces a MarkdownDocument block tree that can be inspected, transformed, or rendered multiple times with different options, while renderMarkdown() accepts either a source string or a parsed document and emits HTML. renderMarkdownInline() renders span-level Markdown without wrapping the result in block elements, which suits one-line summaries and table cells. The parser never throws — malformed constructs fall back to escaped literal text rather than errors.

Output is safe by default. Raw HTML is escaped unless allowRawHtml is enabled, and even then the GFM tagfilter neutralizes dangerous tags such as script and iframe. Link and image URLs are limited to relative URLs and http/https unless allowUnsafeLinks is set; unsafe URLs render as plain label text instead of anchors.

import { parseMarkdown, renderMarkdown } from 'fino:format/markdown';

const doc = parseMarkdown(`# Guide

See the [API reference][api] for details.

[api]: ./api.md`);

const html = renderMarkdown(doc, {
  headingOffset: 1,
  resolveLink: (href) => href.replace(/\.md$/, '.html'),
});
// <h2>Guide</h2>
// <p>See the <a href="./api.html">API reference</a> for details.</p>

Useful references:

Interfaces

interface MarkdownOptions {

Options controlling Markdown HTML rendering, link safety, and code output.

All fields are optional; the defaults render safe HTML with no external hooks. The same options object is accepted by renderMarkdown() and renderMarkdownInline().

import { renderMarkdown, type MarkdownOptions } from 'fino:format/markdown';

const options: MarkdownOptions = {
  headingOffset: 1,
  references: { home: '/index.html' },
  renderCode: (code, lang) => `<pre data-lang="${lang}">${code}</pre>`,
};
const html = renderMarkdown('# Docs\n\nBack to [Home][home].', options);

Properties

allowRawHtml?: boolean

Render raw HTML blocks and inline spans instead of escaping them.

GFM tagfilter remains active for disallowed raw HTML tags such as xmp and script.

headingOffset?: number

Add this many levels to rendered Markdown headings.

Useful when embedding a document under an existing page heading, e.g. an offset of 2 renders # Title as <h3>. Resulting levels are clamped to the h1h6 range.

references?: Record<string, string>

Reference-style link definitions to use in addition to definitions parsed from the document.

Keys are matched case-insensitively with collapsed whitespace. Entries here override same-named definitions parsed from the source.

renderCode?: (code: string, lang: string, meta: string) => string

Render fenced code blocks, replacing the default output.

Receives the raw (unescaped) code, the language token, and any trailing info-string metadata. The returned string is inserted into the HTML as-is, so the callback is responsible for escaping. Without this hook, code renders as <pre><code class="language-…"> with HTML-escaped content.

interface MarkdownListItem {

Parsed list item content.

Each item holds its own block tree, so nested lists, code blocks, and multi-paragraph items appear as child nodes.

import { parseMarkdown } from 'fino:format/markdown';

const [list] = parseMarkdown('- [x] shipped\n- [ ] pending').nodes;
if (list?.kind === 'list') {
  const done = list.items.filter((item) => item.task === true).length;
}

Properties

nodes: MarkdownNode[]

Block nodes forming the item body, in source order.

task?: boolean

GFM task-list state: true for [x], false for [ ], and omitted for ordinary list items.

interface MarkdownDocument {

Parsed Markdown tree and reference-style link definitions.

Produced by parseMarkdown() and accepted by renderMarkdown(), allowing one parse to be inspected or rendered multiple times with different options.

import { parseMarkdown, renderMarkdown, type MarkdownDocument } from 'fino:format/markdown';

const doc: MarkdownDocument = parseMarkdown('See [Docs][docs].\n\n[docs]: /docs');
doc.references.docs;          // '/docs'
const html = renderMarkdown(doc);

Properties

nodes: MarkdownNode[]

Block nodes in source order.

references: Record<string, string>

Normalized reference-style link definitions parsed from the document.

Keys are lowercased with whitespace collapsed; duplicate definitions are last-write-wins.

Types

type MarkdownNode = { kind: 'paragraph'; text: string; } | { kind: 'heading'; level: number; text: string; } | { kind: 'list'; ordered: boolean; tight: boolean; items: MarkdownListItem[]; } | { kind: 'code'; lang: string; meta: string; code: string; } | { kind: 'blockquote'; nodes: MarkdownNode[]; } | { kind: 'thematicBreak'; } | { kind: 'htmlBlock'; html: string; } | { kind: 'table'; align: TableAlign[]; header: string[]; rows: string[][]; }

Block node in a parsed Markdown document.

The tree represents the block constructs rendered by renderMarkdown(). Inline Markdown remains in string fields (text, list item paragraphs, table cells) and is interpreted during rendering, so a node tree can be transformed before inline spans are committed to HTML.

import { parseMarkdown, type MarkdownNode } from 'fino:format/markdown';

const headings = parseMarkdown(source).nodes
  .filter((node): node is Extract<MarkdownNode, { kind: 'heading' }> => node.kind === 'heading')
  .map((node) => ({ level: node.level, text: node.text }));

type TableAlign = 'left' | 'right' | 'center' | undefined

GFM table column alignment.

Derived from colons in the table delimiter row (:---, ---:, :---:). undefined means the column declared no alignment and cells render without an align attribute.

Functions

function parseMarkdown(markdown: string): MarkdownDocument

Parse Markdown into a reusable document tree.

Splits the source into block nodes (headings, paragraphs, lists, code, blockquotes, tables, HTML blocks, thematic breaks) and collects reference-style link definitions. Inline spans are left as raw text inside the nodes and are only interpreted when the tree is rendered. Parsing never throws; unrecognized syntax becomes paragraph text.

import { parseMarkdown } from 'fino:format/markdown';

const doc = parseMarkdown(`# Changelog

- added \`renderCode\` hook
- fixed [tables][gfm]

[gfm]: https://github.github.com/gfm/`);

doc.nodes[0];               // { kind: 'heading', level: 1, text: 'Changelog' }
doc.nodes[1]?.kind;         // 'list'
doc.references.gfm;         // 'https://github.github.com/gfm/'

function renderMarkdownInline(markdown: string, options: MarkdownOptions = {}): string

Render inline Markdown spans without wrapping the result in block elements.

Interprets emphasis, strong text, code spans, strikethrough, links, images, reference links, bare http/https autolinks, backslash escapes, and raw inline HTML tags. Emphasis and strong text use asterisk delimiters only (*em*, **strong**); underscore-delimited emphasis renders as literal text. Everything else is HTML-escaped, and malformed constructs (an unclosed link, a dangling **) degrade to escaped literal text. Because block parsing never runs, reference links resolve only against options.references. Use this for single-line contexts such as titles, summaries, and table cells where a <p> wrapper would be wrong.

import { renderMarkdownInline } from 'fino:format/markdown';

renderMarkdownInline('Return the `value` as **HTML**.');
// 'Return the <code>value</code> as <strong>HTML</strong>.'

renderMarkdownInline('See [Docs][docs].', { references: { docs: '/docs' } });
// 'See <a href="/docs">Docs</a>.'

function renderMarkdown(markdown: string | MarkdownDocument, options: MarkdownOptions = {}): string

Render a Markdown document or source string to HTML.

A string argument is parsed with parseMarkdown() first; passing an already-parsed MarkdownDocument skips that step, which is useful when the same document renders more than once or was transformed after parsing. Reference definitions from the document are merged with options.references, with the options taking precedence.

Output follows the module's safety defaults: raw HTML is escaped and non-http(s), non-relative link URLs are dropped unless the corresponding options opt out.

import { renderMarkdown } from 'fino:format/markdown';

renderMarkdown('# Hello\n\nSome *emphasis* and a [link](/docs).');
// '<h1>Hello</h1>\n<p>Some <em>emphasis</em> and a <a href="/docs">link</a>.</p>'

renderMarkdown('```ts\nconst x = 1;\n```', {
  renderCode: (code, lang) => highlight(code, lang),
});