Skip to content

Latest commit

 

History

History
193 lines (157 loc) · 4.91 KB

File metadata and controls

193 lines (157 loc) · 4.91 KB

PosterScript — Agent Guide

This file is the entry point for any AI coding agent (Claude Code, Cursor, Codex, etc).

What This Project Is

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.

Language Spec

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

Repository Layout

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

Build System

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 dev

Implementation Sequence

Build in this exact order — each step depends on the previous:

  1. packages/parser — pure TypeScript, no deps, fully tested
  2. packages/renderer — React component, uses parser output
  3. packages/playground — Vite app, wires editor + renderer
  4. packages/cli — uses renderer + Playwright for headless export

Do not skip ahead. Do not start renderer before parser tests pass.

Key Invariants

These must hold at all times:

  1. Parser never throws — unknown blocks/keys are silently ignored
  2. Renderer is pixel-perfect — all elements use position: absolute with exact px values
  3. Text is real DOM text — never rasterized. This is the whole point.
  4. Same .poster → same output — deterministic rendering, no randomness
  5. @editable = surgical edit target — only content: changes, never layout

TypeScript Types (canonical)

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
}

Testing Requirements

  • Parser: 100% block coverage, edge cases (missing keys, unknown blocks, % units, multiline)
  • Renderer: visual regression tests for each example file
  • CLI: integration test for render and export commands

Error Handling Policy

  • 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