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

@plim/core

v0.4.0

Published

Plim core: schema, transactions, validation, and built-in block/mark descriptors for the Plim block editor.

Downloads

1,776

Readme

@plim/core

The runtime-agnostic heart of the Plim block editor: the document model, transactions, validation, the action / extension / trigger system, history, snapshots, and the built-in block & mark library. It has no DOM dependencies and is consumed by @plim/editor, @plim/react, @plim/markdown, @plim/ledger, and the rest of the stack.

If you are building an editor UI you will install this alongside @plim/editor (and optionally @plim/react). If you are building tooling — a server-side document transform, a custom sync layer, a validator — you can use @plim/core on its own.

Install

pnpm add @plim/core

The driver & document model

PlimDriver is the configuration object for an editor: which theme, marks, blocks, actions, and extensions are available. It owns no DOM — a view layer (@plim/editor) mounts against it.

import {
  PlimDriver,
  paragraphBlock, headingBlock, bulletedListBlock, numberedListBlock, todoListBlock,
  quoteBlock, codeBlock, horizontalRuleBlock,
  boldMark, italicMark, underlineMark, strikethroughMark, codeMark, linkMark,
} from '@plim/core';

const plim = new PlimDriver({
  theme: 'light',                       // 'light' | 'dark'
  registeredMarks: [boldMark, italicMark, underlineMark, strikethroughMark, codeMark, linkMark],
  registeredBlocks: [
    paragraphBlock, headingBlock,
    bulletedListBlock, numberedListBlock, todoListBlock,
    quoteBlock, codeBlock, horizontalRuleBlock,
  ],
  registeredActions: [],
  extensions: [],
});

PlimDriverConfig fields are all optional: theme, registeredMarks, registeredBlocks, registeredActions, extensions.

A mounted editor exposes an EditorHandle — the live API you read state from and dispatch edits through:

interface EditorHandle {
  readonly plim: PlimDriver;
  readonly isReady: boolean;
  getState(): EditorState;
  setState(s: EditorState): void;
  dispatch(tx: Transaction): void;
  createTransaction(): Transaction;
  onTransaction(cb: (tx, state) => void): () => void;
  onAsyncEvent<T>(name: string, handler): () => void;
  triggerAsyncEvent<T>(name: string, payload?): Promise<T>;
  whenReady(cb: () => void): void;
  restoreSnapshot(snap: Snapshot): void;
  readonly history: History;
  readonly blocks: BlockDescriptor[];
  readonly marks: MarkDescriptor[];
  readonly actions: ActionDescriptor[];
  supportsDecoration(blockType: string): boolean;
}

Transactions

Every change to the document is a Transaction: a batch of operations applied atomically and (where possible) invertibly. Create one from the handle, mutate, then commit().

const tx = editor.createTransaction();
tx.insertText([0], 0, 'Hello');
tx.setBlockType([0], 'heading', { level: 1 });
tx.toggleMark('bold', { path: [0], from: 0, to: 5 });
tx.commit();

Transaction methods include insertText, replaceRange, splitBlock, joinBackward, setBlockType, setBlockAttrs, insertBlock, removeBlock, moveBlock, toggleMark / addMark / removeMark, setSelection, and setMeta. Set tx.setMeta('addToHistory', false) to keep an ephemeral edit out of undo history.

Built-in blocks & marks

Importable descriptors, ready to register on a driver:

  • MarksboldMark, italicMark, underlineMark, strikethroughMark, codeMark, linkMark (with popover toolbar), highlightMark, mentionMark. The arrays builtInMarks give you all of them.
  • BlocksparagraphBlock, headingBlock, bulletedListBlock, numberedListBlock, todoListBlock, quoteBlock, codeBlock, toggleBlock, horizontalRuleBlock, imageBlock, embeddedMediaBlock, tableBlock, rawHTMLBlock. The array builtInBlocks gives you all of them.

Defining custom blocks

defineBlock registers a structural unit — a paragraph, heading, callout, embed, or anything you invent. A block renders to DOM (toDOM) or to a component via a host bridge (toComponent).

import { defineBlock, type BlockPayload } from '@plim/core';

export const calloutBlock = defineBlock({
  name: 'callout',
  type: 'standalone',
  toDOM: (payload: BlockPayload) => {
    const wrap = document.createElement('div');
    wrap.className = 'plim-callout';
    // payload.content[0] is the editor-owned [data-block-content] slot.
    wrap.appendChild((payload.content as HTMLElement[])[0]);
    return wrap;
  },
});

Key descriptor fields: type ('standalone' | 'inline'), nestable, atomic, supportsDecoration, multilineText, continueAs, toolbar, and toMarkdown / fromMarkdown for Markdown round-tripping. defineBlock also accepts a factory (editor) => descriptor so a block can close over editor.createTransaction().

Defining custom marks

defineMark registers an inline annotation — bold, a link, a badge. Marks are wrappers; the editor inserts the text and nested marks inside whatever element you return.

import { defineMark } from '@plim/core';

export const highlightMark = defineMark({
  name: 'highlight',
  toDOM: () => {
    const el = document.createElement('mark');
    el.className = 'plim-highlight';
    return el;
  },
  toolbar: {
    name: 'highlight', label: 'Highlight', icon: 'H', shortcut: '⌘⇧H', group: 'mark',
    perform: ({ state, editor }) => {
      const tx = editor.createTransaction();
      tx.toggleMark('highlight', { from: state.selection.anchor, to: state.selection.head });
      tx.commit();
    },
  },
});

Actions & triggers

Actions are behaviour units bound to triggers — they power keyboard shortcuts, slash menus, mention pop-ups, clipboard handling, and undo/redo.

import { defineAction, triggers } from '@plim/core';

defineAction('bold', {
  trigger: triggers.keyboard.shortcut('Mod+b'),
  triggerValidationRules: ({ and }) => and(['selectionNotEmpty', 'blockSupportsDecoration']),
  perform: async (state, ctx) => {
    const tx = ctx.createTransaction();
    tx.toggleMark('bold', { from: state.selection.anchor, to: state.selection.head });
    tx.commit();
  },
});

Triggers: triggers.keyboard.shortcut('Mod+b') (Mod = Cmd/Ctrl), triggers.keyboard.character('/'), triggers.keyboard.key('Escape'), and triggers.clipboard.action('cut' | 'copy' | 'paste'). Validation rule names — selectionNotEmpty, blockSupportsDecoration, startOfBlock, endOfBlock, precededByWhitespace, inTextBlock — compose via and, or, not, predicate, markActiveInSelection, blockTypeIs.

Extensions

defineExtension bundles blocks, marks, and actions behind one registration so reusable features ship as a unit.

import { defineAction, defineExtension, mentionMark, triggers } from '@plim/core';

export const mentionExtension = defineExtension(() => ({
  name: 'mention',
  registeredMarks: [mentionMark],
  registeredActions: [
    defineAction('mention', {
      trigger: triggers.keyboard.character('@'),
      triggerValidationRules: ({ or }) => or(['startOfBlock', 'precededByWhitespace']),
      cancellationTriggers: [triggers.keyboard.key('Escape')],
      perform: async (_state, ctx) => ctx.triggerAsyncEvent('showMentionSuggestions'),
    }),
  ],
  onTransaction: (tx, ctx) => { /* observe commits */ },
  transformPaste: (data, ctx) => { /* return true to claim the paste */ },
}));

The factory receives the live EditorHandle and is cached per-factory, so passing one extension to multiple drivers is cheap.

History

Every dispatched transaction is captured in a bounded history (default 200 entries).

const history = plim.getHistory(); // or editor.history from a handle
history.undo();
history.redo();
history.canUndo;   // boolean
history.canRedo;   // boolean
const off = history.onChange(({ canUndo, canRedo, past, future }) => { /* re-render buttons */ });

Snapshots

A Snapshot is a full, serializable state capture — for autosave/restore, "revert to here", or shipping a document over the wire.

import { Snapshot } from '@plim/core';

const snap = new Snapshot(editor);     // or new Snapshot(editor.getState())
const json = snap.serialize();
const restored = Snapshot.deserialize(json);
editor.restoreSnapshot(restored);      // replaces state; does not push history

Where to go next

License

See the LICENSE file in this package.