This file is the entry point for any AI coding agent (Claude Code, Cursor, Codex, etc).
PosterScript is an open plain-text format for AI-generated posters.
The problem it solves: When you ask an AI to generate a poster as an image, changing a single word requires regenerating the entire image — the design shifts unpredictably.
The solution:
A .poster file holds the poster as structured data. The renderer turns it into a visual.
To change "2024" to "2025", an LLM changes one line. The renderer updates that element only.
Full spec: spec/POSTERSCRIPT.md
Quick reference:
@canvas
size: 420x600
@bg
color: #0f0f1a
@image
id: hero
src: https://...
x: 0 y: 0 w: 100% h: 60%
fit: cover
@text
id: headline
@editable ← LLM can surgically edit this
x: 40 y: 300
content: Hello World
size: 48
color: #ffffff
font: Bebas Neue
@shape
id: accent
x: 40 y: 290
w: 48 h: 3
fill: #ff3aff
posterscript/
├── spec/POSTERSCRIPT.md # Language specification
├── packages/
│ ├── parser/ # @posterscript/parser (TypeScript, zero deps)
│ │ ├── src/
│ │ │ ├── index.ts # parse(src: string): PosterAST
│ │ │ ├── lexer.ts
│ │ │ └── types.ts
│ │ └── package.json
│ ├── renderer/ # @posterscript/renderer (React 18)
│ │ ├── src/
│ │ │ ├── Renderer.tsx
│ │ │ ├── blocks/
│ │ │ └── export.ts # exportPNG(), exportPDF()
│ │ └── package.json
│ ├── cli/ # posterscript CLI
│ │ └── src/
│ │ ├── index.ts
│ │ ├── serve.ts # live preview
│ │ └── generate.ts # Claude API → .poster
│ └── playground/ # Vite web demo
│ └── src/App.tsx
├── examples/
│ ├── concert.poster
│ ├── product.poster
│ └── minimal.poster
├── CLAUDE.md
├── AGENTS.md
└── package.json
pnpm workspaces. All packages live under packages/.
pnpm install
pnpm dev # playground at localhost:5173
pnpm build
pnpm test
pnpm -F @posterscript/parser build
pnpm -F @posterscript/renderer devBuild in this exact order — each step depends on the previous:
packages/parser— pure TypeScript, no deps, fully testedpackages/renderer— React component, uses parser outputpackages/playground— Vite app, wires editor + rendererpackages/cli— uses renderer + Playwright for headless export
Do not skip ahead. Do not start renderer before parser tests pass.
These must hold at all times:
- Parser never throws — unknown blocks/keys are silently ignored
- Renderer is pixel-perfect — all elements use
position: absolutewith exact px values - Text is real DOM text — never rasterized. This is the whole point.
- Same .poster → same output — deterministic rendering, no randomness
- @editable = surgical edit target — only
content:changes, never layout
Defined in packages/parser/src/types.ts. All other packages import from here.
export interface PosterAST {
canvas: { width: number; height: number }
bg: BgBlock | null
blocks: Block[]
}
export type Block = ImageBlock | TextBlock | ShapeBlock
export interface TextBlock {
type: 'text'
id: string
editable: boolean
x: number; y: number
w: number | null; h: number | null
content: string
font: string
size: number
weight: string
color: string
align: 'left' | 'center' | 'right'
valign: 'top' | 'middle' | 'bottom'
lineHeight: number
tracking: number
uppercase: boolean
opacity: number
bg: string | null
radius: number
padding: number
}
export interface ImageBlock {
type: 'image'
id: string
src: string
x: number; y: number
w: number; h: number
fit: 'cover' | 'contain' | 'fill'
opacity: number
radius: number
}
export interface ShapeBlock {
type: 'shape'
id: string
x: number; y: number
w: number; h: number
fill: string
radius: number
opacity: number
}
export interface BgBlock {
color: string | null
image: string | null
fit: 'cover' | 'contain'
opacity: number
}- Parser: 100% block coverage, edge cases (missing keys, unknown blocks, % units, multiline)
- Renderer: visual regression tests for each example file
- CLI: integration test for
renderandexportcommands
- Parser: never throw, return partial AST with warnings array
- Renderer: show placeholder for missing images, skip invalid blocks
- CLI: exit code 1 with clear error message on invalid input