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

expressmark

v0.6.0

Published

Parse and write Expressive Markdown with its document AST.

Readme

Expressmark — Markdown you can work with.

expressmark

Capture rich document metadata and formatting without sacrificing Markdown readability. Parse Expressive Markdown into an editable JSON document, change the data, and write Markdown again.

Build rich editor → Markdown → rich editor workflows with explicit containers, attributes and YAML frontmatter. Supported block types, titles, metadata and formatting stay expressible in the text, so tools can restore that context when opening the document again.

Quick start · HTML transformers · Layout · Supported syntax · Benchmarks · Future exploration

Quick start

By default, Expressmark reads the Expressive Markdown standard (the documented draft subset). Use { syntax: "compatible" } with both parse and stringify for CommonMark/GFM conventions plus Expressive Markdown extensions. Since 0.2.0, the explicit standard-mode option is { syntax: "standard" }; the former modern name is removed. See syntax for the differences.

pnpm add expressmark
import { parse, stringify, type Document } from "expressmark";

const document: Document = parse(`::: warning [Before deploying]
Back up the database first.
:::`);

document.body[0]!.title = { type: "title", value: "Check the backup" };
console.log(stringify(document));
::: warning [Check the backup]
Back up the database first.
:::

| You write | Expressmark gives you | | --- | --- | | Ordinary Markdown | Editable paragraphs, lists, links, code and tables | | ::: warning [Title] | A semantic container with a designated title and body | | :chip[Ready]{tone=green} | An inline semantic node with authored attributes | | @[Ada]#maintainer | A mention with an entity reference separate from its occurrence ID | | Native rich HTML | Structured nodes where supported, filtered through a shared safe profile |

Make the HTML your own

Available from 0.1.2. Import the separate renderer and override only the node types you need:

import { parse } from "expressmark";
import { renderHtml } from "expressmark/render";

const html = renderHtml(parse("::: warning\nBack up first.\n:::"), {
  transformers: {
    "container:warning": (_node, context) =>
      `<aside class="notice">${context.renderChildren()}</aside>`,
  },
});
// <aside class="notice"><p>Back up first.</p></aside>

Use context.renderDefault() to wrap a node's normal output, keeping custom renderers on its children. Use context.escape(text) when inserting authored strings into HTML. Custom output passes through the same safe HTML profile as default output. See the transformer guide for lookup order, titles, omission, defaults and output-policy limits.

Render options add heading anchor IDs, leave out the designated title's <h1>, and pass every text run through a host escaping hook. A site whose pages only its own authors write can keep raw HTML and admit its components with trusted host content; the default stays the strict safe profile.

Open semantic types and attributes cover most application customization without new grammar. The Markdown reader is internal; Expressmark exposes no supported parser-plugin or reader-configuration API.

Align and float content

Use a layout container to center a group of blocks and their text:

::: layout {align=center}
![Service diagram](services.svg){width=320 responsive}

Services communicate through a shared queue.
:::

align=start|center|end controls alignment inside the group. float=start|end floats the whole group so surrounding text can flow around it. Both work with any block content, including quotations, figures and code. Directions follow the document's writing direction; set dir=rtl for an RTL group or inherit it from the surrounding HTML. lang alone does not set direction.

parse and stringify preserve layout data; renderHtml supplies native logical CSS. Layout examples and browser verification cover Arabic, Hebrew, nested direction changes and child overrides. This built-in rendering is available from 0.1.2.

What you can write

  • Two syntaxes: standard, the default, follows the Expressive Markdown standard (the documented draft subset), with Markdown inside HTML elements and no indented code. Choose compatible explicitly for CommonMark 0.31.2 and GFM conventions, including tables, tasks, strikethrough and autolinks, plus Expressive Markdown extensions. The syntax option chooses, and a document's markdownSyntax frontmatter overrides it; see syntax.
  • Lint warnings for spellings other than the preferred form, and a writer option that writes the preferred form; see lint.
  • YAML frontmatter, a designated document title/ID, and JSON-compatible custom metadata.
  • Nested colon containers, typeless generic containers, inline/leaf directives, open semantic types and shared string/flag attributes, including #id and .class shorthand.
  • Bracketed spans, [text]{.note}, for inline text with attributes. The longhand {class="note" id="x"} works the same as {.note #x}.
  • Alerts, > [!note] Title, as a supported second spelling of a callout, written back as authored.
  • Unbracketed container titles, ::: warning Before you upgrade, written back as authored.
  • Mention shorthand, chips and authored TOC placeholders.
  • Footnotes: [^label] references and [^label]: definitions, kept where they were written and rendered as one numbered, back-linked notes section.
  • Reference-style links and images with their [label]: url definitions, kept in their authored form and resolved when rendering.
  • Definition lists: a one-line term, then : descriptions holding blocks, rendered as <dl>, <dt> and <dd>.
  • Adjacent tab grouping and explicit tabs settings, including shared selection names.
  • Leading-pipe line blocks, code captions and presentation attributes.
  • Named and positional list tables, including rich cells, headers, alignment, transpose and rectangular merges.
  • Safe native HTML for supported rich text, wrappers, figures, disclosures and rich table cells.

This implements a changing Stage 0 proposal. It does not claim complete conformance. The normative syntax, AST and parser/writer rules live in nextwebwg/specs.

Converter contract

parse(markdown, options?) synchronously returns a version-1 Document. stringify(document, options?) synchronously returns Markdown. An unedited parse writes back byte for byte, in both syntaxes, spacing and line endings included: the document records its source text and each top-level block's range in source, and an edited or new block is written in canonical form with the source's spacing kept around its unedited neighbours. A top-level list writes back item by item, and a pipe table cell by cell: an edit rewrites the items or cells it changed and the rest keep their source. A document built without a source is written in canonical form with a terminal newline, or "" when empty. In the safe profile, a block holding HTML is written through the profile. Neither executes code, loads assets, resolves mentions, generates a TOC, or renders HTML. renderHtml is a separate operation.

const document = parse(source, {
  onDiagnostic(diagnostic) {
    console.warn(diagnostic.code, diagnostic.message);
  },
});
const markdown = stringify(document, { onDiagnostic: console.warn });

parseDoc(markdown, options?) parses for editing. Its document is what parse returns, and edit(from, to, replacement) returns the document for the edited text: it re-reads from the top-level block before the edit until the blocks after it start where they started before, and reuses every other block's nodes. An edit that changes the frontmatter or touches a link or footnote definition re-reads the whole document, as does every edit with lint or onDiagnostic, whose findings span the document, and every edit of text holding a carriage return. In standard syntax, an edit touching an HTML tag re-reads the whole document, since an element reads through its end tag. A definition list directly before the edited block is re-read too, since the block may become its next term. Re-reading stops where a block starts as before and no re-read list or definition list continues onto it. In stringify, an unedited block whose source would read as part of an edited block before it is written canonically. Treat a document as read-only while you edit it.

let doc = parseDoc(source);
doc = doc.edit(offset, offset, "x"); // one keystroke
const html = renderHtml(doc.document);

Recoverable source errors become literal nodes with optional warnings. Duplicate attributes and YAML keys use the final value; duplicate authored IDs stay unchanged. Invalid ASTs, unsupported versions, unknown structural nodes, and unsupported source mappings throw ConversionError with a code. Unknown semantic subtypes remain valid. Content stays where it was written, and writers keep the author's spelling: parse records each choice between accepted forms in the node's source, such as _ emphasis, * bullets, setext headings, tilde fences, pipe tables or an attribute list's separators, and stringify writes it back while it still reads as the node. New or edited content uses the preferred form. Whitespace inside a construct and character escapes follow the writer's canonical form.

Exported JSON types include Document, Node, Attributes, Json, Position, Point, Diagnostic, ConversionOptions and Syntax. Content uses exclusive body, children, or value slots; title is a separate designated slot. The writer validates child contexts and merged table grids. Source positions are not currently generated.

The safe HTML profile applies while parsing, writing, and rendering. Scripts and their contents are discarded; literal code containing HTML stays code. Filtering can remove presentation and produces a warning. Omitting the diagnostic callback does not disable the policy.

Rich wrappers can use inert data-attributes data to retain scalar types HTML cannot express, such as a custom false flag. That payload never stores content nodes or restores executable fields. Unsupported structures fail explicitly; unrecognized safe HTML may remain opaque with its accepted markup preserved.

Performance you can reproduce

The comparison suite measures parsing, Markdown writing, round trips and reference HTML pipelines against pinned implementations. Results include workload sizes, versions, runtime, hardware and individual samples. Expressmark does additional AST conversion, extension handling and validation; raw tokenization timings are a different output contract.

There is no blanket “fastest parser” claim. Use the recorded workloads to assess your use case. The agent exploration backlog explains how to investigate a complete Parseman grammar and broader speed comparisons.

Development

Use Corepack, the committed pnpm 12 version, and a supported Node LTS release:

corepack enable
corepack pnpm install --frozen-lockfile
corepack pnpm verify:pr

In Conductor, add this repository with Open project. The checked-in settings install dependencies and provide verification, watch and benchmark commands. Shared settings take effect locally after merging them to the default branch.

TypeScript 7 builds strict ESM declarations. Parseman grammars recognize Markdown's constructs, the extension tokens and bare URLs; the reader in src/markdown applies CommonMark's block and emphasis algorithms to what they recognize, and reads block quotes and list items as Parseman line runs. YAML handles metadata, and sanitize-html with htmlparser2 handles the safe native profile. Oxlint and Vitest check conversions, unsafe input, malformed data, round trips and a fresh packed-package consumer.

The manual Release workflow publishes reviewed main through the protected npm environment using trusted publishing with provenance. The requested version must match package.json. Foundation owns policy files listed in threadlabs.config.json.

MIT licensed. See LICENSE.