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

@ssota-labs/canvasdown

v0.6.1

Published

**The framework-independent core of Canvasdown** — Parse DSL text, register custom types, and generate graph data with automatic layout.

Readme

@ssota-labs/canvasdown

The framework-independent core of Canvasdown — Parse DSL text, register custom types, and generate graph data with automatic layout.

npm version License: MIT TypeScript

Overview

@ssota-labs/canvasdown is the foundation of Canvasdown. It provides:

  • DSL Parser — Chevrotain-based parser for Canvasdown DSL
  • Type Registry — Dynamic block and edge type registration
  • Graph Builder — Convert AST to graph data structures
  • Auto Layout — dagre-based automatic node positioning
  • Patch Support — Incremental updates via Patch DSL

This package is framework-independent — no React, no DOM, just pure TypeScript. Use it with any rendering framework or build your own adapter.

Installation

npm install @ssota-labs/canvasdown

Quick Start

import { CanvasdownCore } from '@ssota-labs/canvasdown';

// 1. Create core instance with optional configuration
const core = new CanvasdownCore({
  defaultExtent: 'parent', // Optional: constrain zone children to parent bounds
  // Set to undefined to allow free movement of children
});

// 2. Register block types
core.registerBlockType({
  name: 'shape',
  defaultProperties: { shapeType: 'rectangle', color: 'blue' },
  defaultSize: { width: 200, height: 100 },
});

core.registerBlockType({
  name: 'text',
  defaultProperties: { content: '' },
  defaultSize: { width: 300, height: 150 },
});

// Register zone type (group node)
core.registerBlockType({
  name: 'zone',
  isGroup: true, // Mark as group node
  defaultProperties: {
    direction: 'TB', // Default direction for children
    color: 'gray',
    padding: 20,
  },
  defaultSize: { width: 400, height: 300 },
});

// 3. Parse and layout DSL
const dsl = `
canvas LR

@shape start "Start" { shapeType: ellipse, color: green }
@text process "Process Data"
@shape end "End" { shapeType: ellipse, color: red }

start -> process : "begins"
process -> end : "completes"
`;

// Or with zones
const dslWithZones = `
canvas TB

@zone thesis "Core Thesis" {
  direction: TB,
  color: blue
}
  @shape main_thesis "Video's Main Argument" {
    shapeType: ellipse,
    color: blue
  }
@end

@zone claims "Supporting Claims" {
  direction: LR,
  color: green
}
  @shape claim1 "Claim 1" { shapeType: rectangle, color: green }
  @shape claim2 "Claim 2" { shapeType: rectangle, color: green }
@end

main_thesis -> claim1 : "supports"
claim1 -> claim2
`;

const result = core.parseAndLayout(dsl);

// result.nodes - Array of positioned graph nodes (including zones and children)
// result.edges - Array of graph edges
// result.metadata - Layout metadata (direction, engine, etc.)

Core Concepts

Block Type Registration

Register custom block types with default properties and sizes:

core.registerBlockType({
  name: 'kanban-card',
  defaultProperties: {
    status: 'todo',
    assignee: null,
    priority: 'medium',
  },
  defaultSize: { width: 300, height: 200 },
  validate: props => {
    // Optional validation function
    return props.status in ['todo', 'in-progress', 'done'];
  },
  propertySchema: {
    // Optional: Define property constraints for validation and LLM templates
    status: {
      type: 'enum',
      enum: ['todo', 'in-progress', 'done'],
      description: 'Task status',
    },
    priority: {
      type: 'enum',
      enum: ['low', 'medium', 'high'],
    },
  },
});

// Register zone type (group node)
core.registerBlockType({
  name: 'zone',
  isGroup: true, // Mark as group node - children will have parentId
  defaultProperties: {
    direction: 'TB', // Layout direction for children (LR, RL, TB, BT)
    color: 'gray',
    padding: 20, // Padding around children
  },
  defaultSize: { width: 400, height: 300 },
});

Zone/Group Notes:

  • Set isGroup: true to mark a block type as a zone/group
  • Zones can contain child blocks using @zone ... @end syntax
  • Each zone can have its own direction property for child layout
  • Children automatically get parentId set to the zone's ID
  • Use defaultExtent option in CanvasdownCore constructor to control child positioning constraints

Edge Type Registration

Register custom edge types with default styles:

core.registerEdgeType({
  name: 'flow',
  defaultShape: 'default',
  defaultStyle: { stroke: '#333', strokeWidth: 2 },
  defaultData: { animated: false },
});

Custom Properties

Define custom properties using @schema or inline:

// Using @schema
const dsl = `
@schema kanban-card {
  status: string
  assignee: string | null
  priority: "low" | "medium" | "high"
}

@kanban-card task1 "Implement Login" {
  status: "in-progress"
  assignee: "alice"
  priority: "high"
}
`;

// Inline properties (no schema needed)
const dsl2 = `
@shape node1 "Node" {
  customProp: "value"
  nested: { key: "value" }
}
`;

Zone/Group Syntax

Create hierarchical structures with zones:

const dsl = `
canvas TB

@zone zone1 "Zone 1" {
  direction: LR,  // Children layout direction
  color: blue,
  padding: 20
}
  @shape child1 "Child 1" { color: green }
  @shape child2 "Child 2" { color: green }
  @shape child3 "Child 3" { color: green }
@end

@zone zone2 "Zone 2" {
  direction: TB,
  color: red
}
  @shape child4 "Child 4" { color: orange }
  @shape child5 "Child 5" { color: orange }
@end

// Edges can connect nodes inside and outside zones
child1 -> child2
child2 -> child4
child4 -> child5
`;

const result = core.parseAndLayout(dsl);
// result.nodes includes:
// - zone1 and zone2 (with isGroup: true)
// - child1, child2, child3 (with parentId: "zone1")
// - child4, child5 (with parentId: "zone2")

Zone Features:

  • Nested Structure: Zones can contain any blocks, including other zones
  • Independent Direction: Each zone can have its own direction (LR, RL, TB, BT)
  • Automatic Layout: Children are automatically positioned within their parent zone
  • Child Constraints: Use defaultExtent: 'parent' option to constrain children within zone boundaries

Patch DSL

Update diagrams incrementally without regenerating the entire diagram:

// Initial diagram
const initialDsl = `
canvas LR
@shape a "Node A"
@shape b "Node B"
a -> b
`;

const result = core.parseAndLayout(initialDsl);

// Apply patch
const patchDsl = `
@add [shape:c] "Node C" { color: purple }
@connect b -> c
@update a { color: red }
@delete b
`;

const patchedResult = core.parseAndLayout(patchDsl, {
  baseGraph: result, // Use previous result as base
});

API Reference

CanvasdownCore

Main class for parsing and layout.

Constructor

const core = new CanvasdownCore(options?: CanvasdownCoreOptions);

Options:

interface CanvasdownCoreOptions {
  defaultExtent?: 'parent' | [[number, number], [number, number]] | null;
}
  • defaultExtent: Controls default positioning constraint for zone children
    • 'parent': Constrain children within parent zone boundaries (default for React Flow group nodes)
    • undefined or null: Allow free movement of children
    • Custom extent: [[minX, minY], [maxX, maxY]] for custom boundaries

Methods

registerBlockType(definition: BlockTypeDefinition)

Register a new block type.

core.registerBlockType({
  name: 'my-block',
  defaultProperties: {
    /* ... */
  },
  defaultSize: { width: 200, height: 100 },
  validate: props => boolean,
  propertySchema: Record<string, PropertySchema>,
});
getBlockTypeSchema(name: string)

Get property schema for a block type. Useful for generating LLM template prompts.

const schema = core.getBlockTypeSchema('shape');
// Returns: { shapeType: { type: 'enum', enum: [...] }, ... }
registerEdgeType(definition: EdgeTypeDefinition)

Register a new edge type.

core.registerEdgeType({
  name: 'my-edge',
  defaultShape: 'default' | 'straight' | 'step' | 'smoothstep' | 'simplebezier',
  defaultStyle?: { stroke: string; strokeWidth: number },
  defaultData?: Record<string, unknown>,
});
parseAndLayout(dsl: string, options?: ParseOptions)

Parse DSL text and apply automatic layout.

const result = core.parseAndLayout(dsl, {
  baseGraph: GraphOutput, // For patch mode
  direction: 'LR' | 'RL' | 'TB' | 'BT',
});

Note: For zone children, positions are calculated relative to their parent zone. React Flow adapters automatically handle parentId and extent properties.

Returns:

{
  nodes: GraphNode[];
  edges: GraphEdge[];
  metadata: {
    direction: string;
    layoutEngine: 'dagre';
  };
}
parse(dsl: string)

Parse DSL text only (no layout).

const ast = core.parse(dsl);
layout(graph: GraphOutput, direction?: Direction)

Apply layout to an existing graph.

const laidOut = core.layout(graph, 'LR');

Type Definitions

CanvasdownCoreOptions

interface CanvasdownCoreOptions {
  defaultExtent?: 'parent' | [[number, number], [number, number]] | null;
}

Controls default behavior for zone children positioning.

BlockTypeDefinition

interface BlockTypeDefinition<TProps = Record<string, unknown>> {
  name: string;
  defaultProperties: TProps;
  defaultSize: { width: number; height: number };
  isGroup?: boolean; // Set to true for zone/group nodes
  validate?: (props: TProps) => boolean;
  propertySchema?: Record<string, PropertySchema>;
}

isGroup Property:

  • Set isGroup: true to mark a block type as a zone/group
  • Group nodes can contain child nodes (via @zone ... @end syntax)
  • Children automatically get parentId set to the group's ID
  • React Flow adapters will render these as group nodes

PropertySchema

Define property constraints for validation and LLM template generation.

interface PropertySchema {
  type: 'string' | 'number' | 'boolean' | 'enum';
  enum?: string[]; // Required when type is 'enum'
  min?: number; // For number type
  max?: number; // For number type
  pattern?: string; // Regex pattern for string type
  description?: string; // Description for template prompts
}

EdgeTypeDefinition

interface EdgeTypeDefinition<TData = Record<string, unknown>> {
  name: string;
  defaultShape: 'default' | 'straight' | 'step' | 'smoothstep' | 'simplebezier';
  defaultStyle?: { stroke: string; strokeWidth: number };
  defaultData?: TData;
}

GraphNode

interface GraphNode<TNodeData = Record<string, unknown>> {
  id: string;
  type: string;
  position: { x: number; y: number };
  size: { width: number; height: number };
  parentId?: string; // Set for children of zones/groups
  data: TNodeData & {
    extent?: 'parent' | [[number, number], [number, number]] | null;
  };
}

parentId Property:

  • Automatically set for children of zones/groups
  • Position is relative to parent zone when parentId is present
  • React Flow adapters use this to render group nodes

data.extent Property:

  • Controls whether child nodes are constrained within parent boundaries
  • Set via defaultExtent option in CanvasdownCore constructor
  • Can be overridden per-node in DSL using extent property

GraphEdge

interface GraphEdge<TEdgeData = Record<string, unknown>> {
  id: string;
  source: string;
  target: string;
  label?: string;
  shape?: string;
  style?: { stroke: string; strokeWidth: number };
  data: TEdgeData;
}

Architecture

core/
├── parser/          # Chevrotain-based DSL parser
│   ├── lexer.ts     # Tokenizer
│   ├── parser.ts    # Grammar parser
│   ├── visitor.ts   # AST visitor
│   └── patch-parser.ts  # Patch DSL parser
├── registry/        # Type registration
│   ├── block-type-registry.ts
│   └── edge-type-registry.ts
├── builder/         # AST → Graph conversion
│   └── graph-builder.ts
└── layout/          # Auto layout
    └── dagre-layout.ts

Framework Independence

This package has zero framework dependencies. It's pure TypeScript and can be used with:

  • React (via @ssota-labs/canvasdown-reactflow)
  • Vue (build your own adapter)
  • Svelte (build your own adapter)
  • Vanilla JavaScript
  • Node.js
  • Any other framework or runtime

The output is simple graph data structures ({ nodes, edges }) that any rendering system can consume.

Examples

Basic Flowchart

const dsl = `
canvas LR

@shape start "Start" { shapeType: ellipse, color: green }
@shape process "Process" { color: blue }
@shape decision "Decision?" { shapeType: diamond, color: yellow }
@shape end "End" { shapeType: ellipse, color: red }

start -> process
process -> decision
decision -> end : "Yes"
decision -> process : "No"
`;

const result = core.parseAndLayout(dsl);

With Zones (Groups)

const dsl = `
canvas TB

@zone thesis "Core Thesis" {
  direction: TB,
  color: blue
}
  @shape main_thesis "Video's Main Argument" {
    shapeType: ellipse,
    color: blue
  }
@end

@zone claims "Supporting Claims" {
  direction: LR,
  color: green
}
  @shape claim1 "Claim 1" { shapeType: rectangle, color: green }
  @shape claim2 "Claim 2" { shapeType: rectangle, color: green }
  @shape claim3 "Claim 3" { shapeType: rectangle, color: green }
@end

@zone evidence "Evidence" {
  direction: TB,
  color: gray
}
  @shape ev1 "Evidence 1" {
    shapeType: rectangle,
    borderStyle: dashed,
    color: gray
  }
  @shape ev2 "Evidence 2" {
    shapeType: rectangle,
    borderStyle: dashed,
    color: gray
  }
@end

main_thesis -> claim1 : "supports"
main_thesis -> claim2 : "supports"
claim1 -> ev1 : "based on"
claim2 -> ev2 : "based on"
`;

const result = core.parseAndLayout(dsl);
// Zones and children are automatically laid out with multi-pass layout

With Custom Properties

const dsl = `
@schema task {
  status: "todo" | "in-progress" | "done"
  assignee: string | null
  dueDate: string
}

@task task1 "Implement Feature" {
  status: "in-progress"
  assignee: "alice"
  dueDate: "2025-01-20"
}

@task task2 "Write Tests" {
  status: "todo"
  assignee: null
  dueDate: "2025-01-25"
}

task1 -> task2
`;

Edge Labels and Properties

const dsl = `
canvas TB

@shape a "Node A"
@shape b "Node B"

a -> b : "main flow" {
  sourceLabel: "from"
  targetLabel: "to"
  animated: true
  style: { stroke: "#ff0000", strokeWidth: 3 }
}
`;

Development

# Install dependencies
pnpm install

# Build
pnpm build

# Test
pnpm test

# Test with coverage
pnpm test:coverage

# Type check
pnpm typecheck

# Lint
pnpm lint

Related Packages

License

MIT