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

@wulperstd/schema

v1.5.0

Published

Framework-agnostic, DOM-free document schema and validation for the block editor.

Readme

@wulperstd/schema

Framework-agnostic, DOM-free document schema and validation for the block editor's document JSON. It defines the node/mark specs and the ProseMirror Schema built from them, plus the pure validation functions and shared scheme-allowlist predicates that both authoring (@wulperstd/editor-core) and rendering (@wulperstd/renderer-html) build on.

Install

pnpm add @wulperstd/schema @tiptap/pm

@tiptap/pm is a required peer dependency, not a bundled one: this package's blockSchema and validation are built on a ProseMirror Schema (@tiptap/pm/model), and declaring it as a peer keeps a single shared @tiptap/pm instance across your app instead of a second copy that would break instanceof checks and plugin identity.

ESM-only: this package is published as "type": "module" with no CommonJS entry in its exports map. It cannot be require()d from a CommonJS-only consumer — import it from ESM (or an async import()) instead.

Public API

import {
  NODE_TYPES,
  MARK_TYPES,
  nodeSpecs,
  markSpecs,
  blockSchema,
  validate,
  validateReport,
  isSafeHref,
  ALLOWED_HREF_SCHEMES,
  EMBED_SRC_SCHEMES,
  IMAGE_SRC_SCHEMES,
  validateEmbedSrc,
  isValidLinkTarget,
  LINK_BLANK_REL,
  ORDERED_LIST_START_MAX,
  isValidOrderedListStart,
  ORDERED_LIST_TYPES,
  isValidOrderedListType,
  MAX_COLSPAN,
  MAX_ROWSPAN,
  isTableHeaderRow,
  isValidColspan,
  isValidColwidth,
  isValidRowspan,
  parseCellSpan,
  parseColwidth,
  SCHEMA_VERSION,
  documentJsonSchema,
} from '@wulperstd/schema';

Type-only exports (NodeTypeName, MarkTypeName, BlockDocument, BlockNode, BlockMark, ValidationError, ValidationResult, BlockStatus, BlockReportEntry, ValidationReport, OrderedListType) are available from the same entry point via import type.

validate(json: unknown): ValidationResult

Validates a ProseMirror JSON document against blockSchema. Never throws: it returns { ok: true, doc } or { ok: false, errors }, where each error's path is an RFC 6901 JSON pointer into the input document.

const result = validate({
  type: 'doc',
  attrs: { schemaVersion: SCHEMA_VERSION },
  content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] }],
});

if (!result.ok) {
  console.error(result.errors);
}

validateReport(json: unknown): ValidationReport

Per-block validation report: instead of an all-or-nothing result, returns a status for every node/mark, so a caller can render "valid" content and flag only the parts that failed.

blockSchema

The ProseMirror Schema instance built from nodeSpecs/markSpecs — the same node/mark set @wulperstd/editor-core's Tiptap extensions implement and @wulperstd/renderer-html's renderer targets, so all three packages agree on one document shape.

isSafeHref, validateEmbedSrc, and the scheme constants

The scheme-allowlist predicates and constants (ALLOWED_HREF_SCHEMES, EMBED_SRC_SCHEMES, IMAGE_SRC_SCHEMES) used by this package's own link/embed/image attribute validation. @wulperstd/renderer-html imports isSafeHref directly so write-time and render-time link-scheme enforcement cannot drift apart.

SCHEMA_VERSION

The monotonic in-band document schema version, stored in every document's attrs.schemaVersion. Deliberately independent of this package's own npm version — bump it only when the document shape itself changes in a way consumers must react to.

JSON Schema for LLM structured output

documentJsonSchema is a JSON Schema (draft 2020-12) describing the same ProseMirror JSON document format validateReport()/validate() accept, intended for an LLM's structured-output / tool input_schema so a backend can have an LLM generate whole documents using any node and mark this editor supports. It is built from this package's OWN constants and nodeSpecs (enums, ranges, attribute defaults, group membership) — nothing is hand-duplicated — has no runtime dependencies, and stays DOM-free like the rest of this package. See src/json-schema.ts for the full implementation and design notes.

You can consume it two ways:

// 1. As a TypeScript export (plain, JSON-serializable object):
import { documentJsonSchema } from '@wulperstd/schema';
// 2. As a static JSON file, e.g. for a non-JS backend or an LLM tool-definition
//    pipeline that reads a file path directly:
import documentJsonSchema from '@wulperstd/schema/document.schema.json' with { type: 'json' };
// or, in a non-bundler/non-Node context: resolve the package and read
// `dist/document.schema.json` directly.

Both are produced from the exact same source (src/json-schema.ts); the JSON file is emitted by scripts/emit-json-schema.mjs as part of pnpm build.

You MUST still run validateReport() and retry on failure

This schema is a best-effort, possibly LOOSER superset of validateReport(). It is designed to never REJECT a document the real validator accepts, but it MAY ACCEPT some documents the real validator would reject. Constraining an LLM's structured output with this schema is not a replacement for server-side validation:

  1. Ask the LLM to produce a document conforming to documentJsonSchema.
  2. Run the result through validateReport(json) — the authoritative validator.
  3. If ok: false, feed report.issues back to the LLM and ask it to fix exactly those problems, then retry from step 2. Each issue's path is an RFC 6901 JSON pointer into the document (e.g. /content/0/attrs, /content/2/content/0/marks/1), so you can point the LLM (or your own UI) directly at the offending location.
  4. Only persist/render a document once validateReport() returns ok: true.

Rules this JSON Schema does NOT enforce

These are real validateReport() rules the schema cannot (or deliberately does not) express structurally — a document violating one of these is accepted by the schema but rejected by validateReport():

  • URL scheme safety (link.href, cardLink.href, image.src, embed.src). The real check (isSafeHref) disallows javascript:, protocol-relative URLs, embedded control characters, and a wide Unicode whitespace-obfuscation set, and restricts the scheme to an allow-list that differs per field. The schema only types these fields as string with a prose description of the allow-list — a regex here would be brittle against the real normalization rules.
  • Color grammar (highlight.color, textStyle.color, tableCell/tableHeader.backgroundColor). The real check (isValidColor) is a closed hex/rgb()/rgba() grammar; the schema types these as string | null with a prose description only.
  • tableCell/tableHeader.colwidth length vs. colspan — not enforced by validateReport() either (no cross-field check), so this is exact parity, not a looseness gap.
  • superscript/subscript mutual exclusion on the same text run (and duplicate mark types on the same run) — validateReport() rejects both via ProseMirror's own doc.check(); a JSON Schema has no practical way to express "this array must not contain two particular values together" or "no duplicate type" over an open-ended marks array.
  • Extraneous top-level keys on a node are silently ignored by validateReport(), but this schema uses additionalProperties: false on every node $def, so it is stricter than the real validator on this one dimension (a schema-valid document is never rejected by validateReport() for this reason — only the reverse direction is possible here).
  • An empty marks: [] on a block-level container node (e.g. a paragraph nested under blockquote) — validateReport() trivially accepts it, but block-level node $defs here don't declare a marks property at all, so additionalProperties: false would reject it. Not expected to occur in practice.

Random document generator (generate:docs)

For fixtures, fuzz-testing, or demos, this package includes a seeded, schema- driven random document generator (scripts/generate-docs.mjs) that walks documentJsonSchema itself, so it keeps working as the schema grows. Every generated document is checked against BOTH Ajv and validateReport() before being emitted.

# from packages/schema, after `pnpm build` (or `pnpm --filter @wulperstd/schema build`):
pnpm run generate:docs -- --count 50 --seed 1 --stdout
# or, writing files:
pnpm run generate:docs -- --count 10 --seed 1 --out ./generated

Flags: --count N (default 10), --seed S (default 1, any string is hashed to an integer), --max-depth D (default 4), --max-children C (default 5), --out DIR (default ./generated), --stdout (print NDJSON to stdout instead of writing files). All randomness is seeded (faker.seed() plus an independent seeded PRNG for structural choices) — the same --seed always reproduces byte-identical output.

test/support/random-document.ts re-exports the exact same generator for use in this package's own vitest property-based test suite — there is only ONE implementation.

Public API excludes view and plugin constructs

This package exports node/mark specs, AST types, and validation only. It never exports or requires any ProseMirror view or plugin construct, and never exports a mutator or registration function for the node/mark set — keeping it usable from any DOM-free environment (a server, a CLI, a worker).