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

@zojize/fsm-builder

v0.3.5

Published

Interactive SVG-based FSM (Finite State Machine) builder library

Readme

@zojize/fsm-builder

npm version

An interactive SVG-based finite state machine editor. Draw Moore-style DFAs and NFAs with per-state outputs directly in the browser.

Live playground →


Installation

npm install @zojize/fsm-builder

Quick start

import { createFSMBuilder } from '@zojize/fsm-builder'
import '@zojize/fsm-builder/style.css'

const api = createFSMBuilder({
  container: '#my-container',
  onChange(state) {
    console.log(JSON.stringify(state, null, 2))
  },
})

The container element can be any block-level element with a defined height. createFSMBuilder automatically adds the fsm-builder class to it.

<div id="my-container" style="height: 400px"></div>

createFSMBuilder(options)

Options

| Option | Type | Default | Description | | --------------------- | --------------------------- | --------------- | -------------------------------------------------------------------------------------- | | container | string | — | CSS selector for the host element. Required. | | initialState | FSMState | { nodes: {} } | State to preload into the editor. | | onChange | (state: FSMState) => void | — | Called whenever the diagram changes. | | readonly | boolean | false | Disables all editing interactions. | | debug | boolean | false | Forces the sidebar to render and adds a copy-to-clipboard button for the current JSON. | | sidebar | boolean | true | Shows the toolbar. | | autoValidate | boolean | false | Run validation automatically after every change. | | validate | false \| ValidateConfig | false | Inline validation for edge and node labels. See Validation. | | simulation | boolean \| { variables? } | false | Enable built-in step-through simulation. See Simulation. | | scale | number | 1 | SVG viewBox zoom factor. Values < 1 show more canvas area. | | defaultRadius | number | 30 | Default node circle radius in SVG units. | | fontFamily | string | monospace stack | Font used for all labels. | | fontSizeBreakpoints | object | — | Responsive font sizing. See Font-size breakpoints. | | maxHistory | number | — | Maximum undo/redo history depth. Unlimited by default. | | svgAttributes | object | {} | Extra attributes to set on the root <svg> element. |

Return value

createFSMBuilder returns an FSMBuilderAPI:

interface FSMBuilderAPI {
  on: <K extends keyof FSMEventMap>(event: K, handler: FSMEventHandler<K>, options?: AddEventListenerOptions) => void
  off: <K extends keyof FSMEventMap>(event: K, handler: FSMEventHandler<K>) => void
  getState: () => FSMState
  destroy: () => void
}

Data model

interface FSMState {
  start?: NodeId // ID of the start state
  nodes: Record<NodeId, FSMNode>
}

interface FSMNode {
  label: string // Outer label shown below the circle
  innerLabel: string // Inner label shown inside the circle (e.g. output bits)
  x: number
  y: number
  radius: number
  transitions: FSMTransition[]
}

interface FSMTransition {
  to: NodeId // Target node ID
  label: string // Boolean expression over input variables
  offset: number // Curve offset (non-self edges); 0 = straight
  rotation?: number // Self-loop orientation in degrees
}

FSMState is plain JSON and safe to serialize/deserialize with JSON.stringify / JSON.parse.

Interactions

| Action | How | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | Add state | Double-click the canvas, or use the Add Node toolbar button | | Create transition | Shift+drag from a source node to a target, or use the Transition tool | | Set start state | Double-click a node, or use the Start State tool then click a node | | Remove | Right-click a node or edge, or use the Remove tool | | Clear all | Red trash button in the sidebar (prompts for confirmation) | | Move states | Click-drag in Select mode | | Multi-select | Cmd/Ctrl+click to toggle; drag an empty area to box-select; Cmd/Ctrl+drag to add to selection; drag any selected node to move all together | | Pan canvas | Move Canvas tool, or Cmd/Ctrl+Shift+drag anywhere | | Undo / Redo | Cmd/Ctrl+Z / Cmd/Ctrl+Shift+Z (or Cmd/Ctrl+Y), or toolbar buttons |

Validation

Pass a validate config to enable inline label validation:

import { createFSMBuilder, validateBooleanExpression } from '@zojize/fsm-builder'

createFSMBuilder({
  container: '#editor',
  validate: {
    container: '#validation-output', // optional: element to render error list into
    edge: {
      inputAttributes: {
        pattern: '^[ab01\\(\\)\'+*]*$', // browser-level pattern attribute
      },
      validate(input) {
        return validateBooleanExpression(input, { alphabet: 'ab' })
        // return true  → valid
        // return false → invalid (no message)
        // return string → invalid, message shown in UI
      },
    },
  },
})

The validate callback receives the label string, the current FSMState, and the node or transition being edited. It should return true for valid, false for invalid, or a string error message.

Simulation

Pass simulation: true to enable a floating panel that lets you step through input symbols against the current FSM. Variables are auto-detected from edge labels; override with an explicit alphabet:

createFSMBuilder({
  container: '#editor',
  simulation: { variables: 'ab' },
})

When enabled, the toolbar gains Step and Replay buttons. Clicking Step opens a floating panel with an input box per variable; each accepts a 0/1 sequence, and successive Steps advance the active state using the leftmost bit of each input (consuming it). Replay resets to the start state. Empty inputs highlight in red; transition errors (no match, nondeterminism, missing start state) surface as a transient message below the panel.

Boolean expression utilities

import type { BooleanExpression } from '@zojize/fsm-builder'
import { evaluateBooleanExpression, parseBooleanExpression, validateBooleanExpression } from '@zojize/fsm-builder'

parseBooleanExpression(input, options?)

Parses a boolean expression string into an AST. Throws a SyntaxError on invalid input.

const expr = parseBooleanExpression('a\' + b', { alphabet: 'ab' })
// { type: 'add', left: { type: 'not', operand: { type: 'var', symbol: 'a' } }, right: { type: 'var', symbol: 'b' } }

validateBooleanExpression(input, options?)

Returns true if valid, or a string error message if not. Never throws.

validateBooleanExpression('a + b', { alphabet: 'ab' }) // true
validateBooleanExpression('a + c', { alphabet: 'ab' }) // "Expected ..."

evaluateBooleanExpression(expr, context)

Evaluates a parsed AST against a variable assignment.

evaluateBooleanExpression(expr, { a: true, b: false }) // true

logicOnlyFsm(state)

Strips layout data (x, y, radius, offset, rotation) from an FSMState, keeping only start, node labels, and transitions. Useful for comparing or exporting the logical structure of a diagram.

import { logicOnlyFsm } from '@zojize/fsm-builder'

const logic = logicOnlyFsm(api.getState())

Boolean expression syntax:

| Construct | Syntax | | --------- | ------------------------------- | | Variable | any letter in alphabet | | AND | adjacency (ab) or * (a*b) | | OR | + (a + b) | | NOT | apostrophe suffix (a') | | Constants | 0, 1 | | Grouping | (a + b)' |

Events

Subscribe to FSM events via the returned api:

api.on('node:added', ({ id, node }) => { /* ... */ })
api.on('edge:changed', ({ id, transition }) => { /* ... */ })
api.on('start:changed', ({ id }) => { /* ... */ })
api.on('history:changed', ({ canUndo, canRedo }) => { /* ... */ })

Full event map:

| Event | Payload | | ----------------- | --------------------------------------------- | | node:added | { id, node } | | node:removed | { id } | | node:moved | { id, node } (fires frequently during drag) | | node:move-end | { id, node } (fires on pointer up) | | node:changed | { id, node } | | node:committed | { id } | | edge:added | { id, from, transition } | | edge:removed | { id } | | edge:changed | { id, transition } | | start:changed | { id } | | history:changed | { canUndo, canRedo } |

Font-size breakpoints

Make label font sizes responsive to text length by passing a Record<number, string> where the key is the minimum character count at which the size applies:

createFSMBuilder({
  container: '#editor',
  fontSizeBreakpoints: {
    edge: { 5: '18px', 8: '15px' }, // ≥5 chars → 18px, ≥8 chars → 15px
    innerNode: { 3: '19px', 5: '16px' },
    outerNode: { 15: '19px', 25: '16px' },
  },
})

Loading and saving state

The onChange callback and api.getState() both return plain FSMState JSON. Pass it back in as initialState to restore a previous session:

// Save
localStorage.setItem('fsm', JSON.stringify(api.getState()))

// Restore
const saved = JSON.parse(localStorage.getItem('fsm') ?? '{"nodes":{}}')
createFSMBuilder({ container: '#editor', initialState: saved })

Framework integration (Vue example)

<script setup lang="ts">
import type { FSMState } from '@zojize/fsm-builder'
import { createFSMBuilder, validateBooleanExpression } from '@zojize/fsm-builder'

const state = defineModel<FSMState>()

const container = useId()
onMounted(() => {
  createFSMBuilder({
    container: `#${container}`,
    initialState: toRaw(state.value) ?? { nodes: {} },
    onChange: (newState) => { state.value = newState },
    validate: {
      edge: {
        validate: input => validateBooleanExpression(input, { alphabet: 'ab' }),
      },
    },
  })
})
</script>

<template>
  <div :id="container" style="height: 400px" />
</template>

Implementation

createFSMBuilder builds a shared FSMContext and delegates to focused sub-modules under src/fsm/:

| Module | Responsibility | | --------------- | ------------------------------------------------------------------------------------------------------------------------- | | types.ts | Public TypeScript interfaces: FSMNode, FSMState, FSMTransition, FSMOptions, NodeId, EdgeId, ValidateOptions | | events.ts | Typed event emitter with AbortSignal support; defines FSMEventMap and FSMBuilderAPI | | context.ts | Shared FSMContext bag passed into every sub-module | | math.ts | Pure geometry: Bézier curves, self-loop arcs, arrowheads, circle intersections | | dom.ts | SVG/HTML helpers: element creation, coordinate conversion, text measurement, clipboard | | nodes.ts | Node creation, drag interaction, label editors, selection, start marker | | edges.ts | Edge geometry, drag-to-curve, label editor, SVG masks for node occlusion | | sidebar.ts | Toolbar: mode and action buttons | | simulation.ts | Step-through simulation panel, variable inputs, transition evaluation | | validation.ts | Reads all label inputs, runs validateConfig, updates the error panel |

License

MIT