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

@notrealstudio/nr-md

v0.1.0

Published

Markdown as structured data: headings become blocks, $key: value lines become attributes. Parser and serializer for the mdd/mdz block grammar.

Readme

nr-md

Markdown as structured data: headings become blocks, $key: value lines become attributes, and everything else stays the prose it always was.

# $model gpt-4o
$temperature: 0.7
$stop: $["\n\n", "END"]
$limits: {input: 8000, output: 800}

You are a terse assistant.
import { parse, serialize } from '@notrealstudio/nr-md'

const doc = parse(text)
const model = doc.root.children[0]

model.id                // 'gpt-4o'
model.attrs[0].value    // 0.7  — coerced, not a string
model.attrs[1].value    // ['\n\n', 'END']
model.attrs[2].value    // { input: 8000, output: 800 }
model.body              // 'You are a terse assistant.'

serialize(doc)          // back to text, in canonical form

serialize emits the canonical spelling of a document, so parse(serialize(doc)) gives back the same tree — values that need no quotes lose them, blank separator lines are not preserved. Byte-identical output is guaranteed for text that is already canonical, which is what serialize produces.

The point is the file, not the API. It is a Markdown document — it renders in Obsidian, previews on GitHub, diffs by line, and a person can edit it without knowing there is a parser. It is also a tree with typed values, so a program can read it as configuration, as a prompt library, as records. You do not keep two files, one for humans and one for machines.

Install

npm install @notrealstudio/nr-md

Zero configuration, one dependency (a JSON5 microcodec), no host APIs — it runs wherever a string is a string.

The grammar

Blocks. A heading whose text starts with the sigil opens a block; the rest of the line is its name, and anything after a space is its id. Heading depth nests them.

# $character Aria      → block "character", id "Aria", level 1
## $voice              → nested block "voice"
## $                   → closing token: back to the top level

Attributes. A line starting with the sigil, up to the first :, is a key.

$name: Aria            → 'Aria'      (string)
$age: 24               → 24          (number)
$active: true          → true        (boolean)
$id: 007               → '007'       (leading zero — an id, not a number)
$phone: +79051234567   → string      (a leading + is not a sign)
$title: "42"           → '42'        (quotes force a string)

Repeating a key is reassignment: the last one wins.

Lists are flow literals — sigil, brackets, commas:

$tags: $[fantasy, "slice, of life", 3]

Objects are JSON5, on one line:

$sampler: {steps: 30, cfg: 7.5, sched: 'karras'}

Bodies. Every line that is not a heading or an attribute belongs to the current block's body, verbatim — Markdown, code fences, tables, whatever. serialize escapes a body line that would re-parse as structure, so a round-trip through a document that talks about the format is still exact.

Interpolation. ${...} is recognised but never evaluated. A value carrying one parses to { raw, placeholders }: the original text, plus the spans where interpolations sit. What the expression inside means is your language, not ours — bring your own evaluator. \${ opts out, and the marker survives parsing intact.

Two sigils

The sigil is a parameter. $ is the mdz profile, @ is mdd — same grammar, different marker, so two conventions can live in one document tree without colliding.

parse(text)                  // '$' by default
parse(text, { sigil: '@' })  // '@name: value', '# @block'
serialize(doc, { sigil: '@' })

Schema layer

A subpath, because it is a different job — laying a JSON Schema out as a document and reading it back:

import { serializeWithSchema, parseWithSchema } from '@notrealstudio/nr-md/schema'

const text = serializeWithSchema(obj, schema)   // x-storage decides attr/body/block/tbl
const back = parseWithSchema(text, schema)      // deep-equals obj

x-storage and x-mdd annotations say where each field lives: an attribute, the block body, a block of its own, a table, a flow list. Unknown fields round-trip losslessly instead of being dropped, so you can layer this over someone else's schema without owning it.

validate delegates to Ajv2020, an optional peer — install ajv if you want it, or pass your own validator. The root import never touches it.

Finding an installed ajv from ESM without importing a host module needs process.getBuiltinModule (Node 22.3+). Below that, and in the browser, auto-discovery is off and validate asks for opts.validator — everything else in the package is plain ES2022 and runs on Node 20 LTS and any current browser.

API

| | | |---|---| | parse(text, opts?) | text → Document | | serialize(doc, opts?) | Document → text | | parseAttributeValue, parseBodyValue | value-level parsing | | coerce, isFullyQuoted | scalar coercion (§3) | | serializeValue | one value → its source form | | parseTable, parseTableTyped, serializeTable, serializeTypedTable | tbl bodies | | parseHeaderCell, emitHeaderCell, coerceTyped, tableSchema | typed table headers | | isJson5Shaped, isJson5Object, parseJson5Object | JSON5 attribute values | | resolveIncludes, resolveIncludesAsync, extractSection | $[[path#section]] preprocessing |

Types: Document, Block, Attribute, AttributeValue, BodyValue, Scalar, ListItem, InterpolatedValue, InterpolationSpan, Json5Object, Json5Value, Sigil, ParseOptions, SerializeOptions, Pos.

Block and Attribute carry an optional pos slot. Nothing populates it yet; it is reserved so that adding position tracking later cannot break an exhaustive walk written against this tree today.

What this is not

It does not evaluate anything. No expressions, no includes resolved from disk (you inject the reader), no inheritance, no templating. Those belong to whoever consumes the tree, and keeping them out is what makes the tree portable — the same document can feed a different engine, in a different language, with a different idea of what ${x} means.

The normative grammar reference (format-spec, serialize-spec) is maintained with the engine that grew this format and ships alongside it.

License

MIT