A plain-text playwriting format with a simple way to write, preview, and export PDF manuscripts.
Downstage is a plaintext markup language for writing stage plays, inspired by
Fountain (for screenplays) and the archived
TheatreScript spec. It
gives you three clear ways to start: the Web Editor, the VS Code
extension, or the command line. Files use the .ds extension.
If you just want to start, open the Web Editor.
Read the Syntax Guide.
The Pages site is built with Eleventy from the
templates in site/.
npm install
npm --prefix web install
npm run build:site
npm run serve:site# The Example Play
Author: Jane Smith
Date: 2025
Draft: First
## Dramatis Personae
HAMLET - Prince of Denmark
HORATIO - Friend to Hamlet
### Courtiers
ROSENCRANTZ - A courtier
GUILDENSTERN - A courtier
## ACT I
### SCENE 1
> The battlements of Elsinore Castle. Night.
HORATIO
Who's there?
HAMLET
(aside)
A piece of work is man, how **noble** in reason,
how *infinite* in faculty.
In form and moving, how express
and admirable; in action, how like
an angel.
HORATIO
They're here.
HAMLET ^
Then let them come.
// A line comment
> Enter GHOST
===
### SCENE 2
ROSENCRANTZ
Good my lord!
SONG 1: The Wanderer's Lament
HAMLET
O, that this too, too solid flesh
Would melt, thaw, and resolve itself
Into a dew.
SONG END
- Readable plaintext format — scripts look natural without markup noise
- Inline formatting:
*italic*,**bold**,***bold italic***,_underline_,~strikethrough~ - Verse support via indentation (2+ spaces)
- Dual dialogue with trailing
^on the second cue - Songs with
SONG/SONG ENDblocks - Comments:
// lineand/* block */ - Forced elements:
@characterand.headingfor edge cases - Character aliases:
HAMLET/HAMor[HAMLET/HAM] - Page breaks:
=== - LSP server with:
- Semantic syntax highlighting
- Document outline (acts/scenes/characters)
- Hover info on character names (shows description from dramatis personae)
- Go-to-definition (jump to character's dramatis personae entry)
- Rename Symbol for characters (updates dramatis personae entry, aliases, and matching cues — refuses on prose mentions)
- Context-aware completion for character cues
- Diagnostics (parse errors, unknown character warnings, Dramatis Personae hygiene, cue hygiene)
- Code actions (quick fixes for unknown characters, unnumbered or misnumbered acts/scenes, duplicate DP entries, and inserting a missing Dramatis Personae section)
- Neovim integration out of the box (0.11+)
- CLI tools for parsing and validation
If you are new to Downstage, you probably want the Web Editor or the VS Code extension before you want installation steps.
go install github.com/jscaltreto/downstage@latest
brew tap jscaltreto/tap
brew install downstage
downstage parse play.ds # Output AST as JSON
downstage validate play.ds # Check for errors
downstage stats play.ds # Report word/dialogue/runtime stats
downstage render play.ds # Render to PDF (default)
downstage render -f html play.ds # Render to HTML
downstage lsp # Start LSP server (stdio)
downstage version # Print version info
Use -v or --verbose to enable debug logging on any command.
downstage validate exits non-zero on parse errors. downstage parse prints
parse errors to stderr but still emits the AST JSON. downstage render exits
non-zero if parsing fails.
downstage render supports PDF (default) and HTML output via --format:
downstage render play.ds # PDF, manuscript
downstage render --format html play.ds # HTML, manuscript
downstage render --format html --style condensed play.ds
downstage render --format html -o play.html play.ds # explicit output file
Both formats support two styles via --style:
standard(default, Manuscript) — Traditional manuscript format. Character names centered above dialogue, generous margins.condensed(Acting Edition) — Acting edition format designed for rehearsal use. Character names inline with dialogue (e.g.HAMLET. To be or not...), tighter spacing.
PDF also supports --page-size letter (default) and --page-size a4.
Manuscript output renders on the selected sheet size. Acting edition output
derives its logical page from that sheet size: half-letter for Letter, A5 for
A4.
Acting edition can be imposed for print via --pdf-layout:
single(default) — one logical page per sheet2up— two logical pages per landscape sheetbooklet— duplex booklet order, padded to a multiple of 4. Print double-sided, then fold in half.--gutter <measurement>sets the inside gap (default0.125in; acceptsinandmm).
HTML produces a self-contained document with embedded CSS using semantic
.downstage-* class names for custom styling.
downstage stats reports manuscript metrics derived from the parsed AST:
word counts, dialogue breakdown, per-character speeches and lines, scene/act
counts, and a rough runtime estimate.
downstage stats play.ds # Human-readable summary
downstage stats play.ds --format json # Machine-readable output
downstage stats play.ds --rate slow # Use the 110 wpm preset
downstage stats play.ds --wpm 140 --pause 0 # Custom rate, no pause overhead
Runtime is based on spoken dialogue word count divided by a speaking rate, plus a small pause factor for pacing:
spoken_minutes = dialogue_words / words_per_minute
estimated_minutes = spoken_minutes * (1 + pause_factor)
Presets: slow (110 wpm), standard (130 wpm, default), conversational
(150 wpm). The default pause factor is 10%. Stage directions and prose do
not contribute to the runtime estimate. Treat the output as a rough guide,
not a prediction.
Install downstage.nvim with your plugin manager:
-- lazy.nvim
{ "jscaltreto/downstage.nvim", ft = "downstage" }This provides filetype detection, buffer settings, and LSP integration for .ds
files. The downstage binary must be on your PATH.
If you use nvim-cmp, enable the plugin's optional completion integration to
limit Downstage buffers to LSP-driven cue completions:
-- lazy.nvim
{
"jscaltreto/downstage.nvim",
ft = "downstage",
opts = {
cmp = true,
},
}Any LSP-compatible editor can use the Downstage language server. The server communicates over stdio using JSON-RPC 2.0 (LSP 3.17). Point your editor's LSP client at the server:
{
"command": ["downstage", "lsp"],
"filetypes": ["downstage"],
"rootPatterns": [".git"]
}The editors/vscode/ extension provides full Downstage
support: LSP-powered completions, diagnostics, and folding; live PDF preview;
render-to-PDF commands; snippets; and TextMate syntax highlighting.
See the extension README for details.
For local development:
cd editors/vscode
npm install
npm run compileThen open the editors/vscode folder in VS Code and press F5 to launch an
Extension Development Host.
Start writing in the Web Editor, a modern browser-based editor built with Vue 3, Tailwind CSS v4, and CodeMirror 6. It features live preview, adaptive syntax highlighting, completions and quick-fix code actions sourced from the Downstage LSP, spellcheck with a per-script dictionary, and PDF export. No install required; the entire pipeline runs client-side via WebAssembly.
For local development:
npm --prefix web install
make web # Build the editor for Pages output
make web-dev # Run the editor dev server with ViteSee web/README.md for details.
A Downstage document is organized around top-level # sections:
- Play Header — each play starts with
# Title, followed by optionalKey: Valuemetadata lines such asAuthor:orDraft: - Dramatis Personae — an optional
## Dramatis Personae,## Cast of Characters, or## Characterssection inside that play, with character entries and optional###subgroup headings; rendered output keeps the chosen wording - Body — the play itself: acts (
## ACT), scenes (### SCENE), dialogue (ALL CAPS character name followed by speech text), stage directions (>prefixed lines), callouts (>>prefixed lines), verse (indented 2+ spaces), songs, and comments
To mark simultaneous dialogue, put ^ at the end of the second character cue.
HORATIO
They're here.
HAMLET ^
Then let them come.
The renderer places the two dialogue blocks side by side when they fit on the page. If they do not fit cleanly, rendering falls back to sequential dialogue instead of producing broken columns.
See SPEC.md for the complete language specification.
git clone https://github.com/jscaltreto/downstage.git
cd downstage
make
To embed version information:
go build -ldflags "\
-X github.com/jscaltreto/downstage/cmd.version=1.0.0 \
-X github.com/jscaltreto/downstage/cmd.commit=$(git rev-parse HEAD) \
-X github.com/jscaltreto/downstage/cmd.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-o downstage .
Release PRs and changelog updates are managed by Release Please. Merging a
release PR creates the Git tag and GitHub release, and GoReleaser then builds
artifacts for macOS, Linux, and Windows on amd64 and arm64.
For local release validation:
make release-check
make release-snapshot
These targets require goreleaser to be
installed locally.
Publishing the Homebrew formula is handled by the release workflow, which
updates jscaltreto/homebrew-tap after GoReleaser publishes release assets.
This requires a repository secret named HOMEBREW_TAP_GITHUB_TOKEN with push
access to jscaltreto/homebrew-tap.
Release Please requires a repository secret named RELEASE_PLEASE_TOKEN with
enough access to create release PRs, tags, and releases in
jscaltreto/downstage.
Downstage source code is licensed under the MIT License.
Bundled fonts in internal/render/pdf/fonts/
remain under the SIL Open Font License 1.1; see the included OFL.txt and
OFL-LibreBaskerville.txt files.
- Fountain — screenplay markup language that inspired Downstage's plaintext philosophy
- TheatreScript — archived stage play markup spec that Downstage builds on
