Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Fountain parser (Rust): .fountain → ProseMirror JSON + meta + scene_cards #186

Description

@stultus

Part of #184 (Fountain import). Spec findings posted in the umbrella's research comment — read that first; this issue assumes those decisions.

What

Implement a Fountain parser in Rust that consumes a .fountain file string and produces a complete ScreenplayDocument (Film shape): meta, settings, content (ProseMirror JSON), and scene_cards.

Parsing pipeline (locked, mirrors reference implementation)

  1. Boneyard pre-pass — strip every /* ... */ block (may cross double-line-breaks; this is the only Fountain syntax that does). Drop content per Fountain import (round-trip-safe) #184 scope. Increment a counter for the import summary toast.
  2. Note extraction — find every [[ ... ]] (may span lines, but a fully blank line terminates). Record (text, anchor-position) and replace with a placeholder so line-based tokenisation doesn't see them.
  3. Title page — top of file only. Recognised keys (case-insensitive, whitespace before : allowed): Title, Credit, Author, Authors, Source, Notes, Draft date, Date, Contact, Copyright. Multi-line values: continuation lines indented 3+ spaces or a tab. Title page ends at the first blank line followed by a non-key: line.
    • Map Titlemeta.title, Author/Authorsmeta.author, Creditmeta.author if blank else append, Draft date/Datemeta.draft_date, Contactmeta.contact, Copyrightmeta.registration_number if empty else meta.extra, Notesmeta.footnote if empty else meta.extra.
    • Everything else (including unknown writer-coined keys) → meta.extra verbatim with original key spelling.
  4. Body — paragraph-by-paragraph with the precedence below. Forced prefixes win over auto-detection.

Element precedence (per paragraph)

  1. Forced prefix wins: ., !, @, >, ~, =, #. Note: .. is not a forced scene heading (must be a single . not followed by another .).
  2. Page break: line is = × 3 or more and nothing else. (== is action; === 5 === is action.)
  3. Centered: line starts > and ends <. (Beats forced-transition >.)
  4. Scene heading auto: line begins with INT, EXT, EST, INT./EXT, INT/EXT, or I/E (case-insensitive) followed by . or space, with blank line before and after.
  5. Transition auto: all-caps line ending in TO:, with blank line before and after. Also: FADE IN: at top of file (reference-impl convention; Fountain.js disagrees — we follow the reference).
  6. Character auto: all-uppercase Latin line, ≥1 alphabetic char, non-empty next line, blank previous line. Lowercase inside parens is allowed (MOM (V.O.), HANS (on the radio)).
  7. Parenthetical: context-dependent — line (...) immediately after Character or Dialogue.
  8. Dialogue: context-dependent — line(s) after Character or Parenthetical, terminated by a fully blank line. A line with only trailing whitespace (e.g. two spaces) keeps the dialogue block alive (Fountain's two-space convention).
  9. Action: fallback. Tabs and leading whitespace are preserved.

Mapping to Scriptty model

Fountain Output
Scene heading scene_heading node (drop optional #1A# scene-number suffix; Scriptty auto-numbers)
Action action node (preserve internal soft line breaks)
Character (incl. forced @) character node — strip the @ prefix from text content
Parenthetical parenthetical node — parens stored in content (project convention; commit 27a126f)
Dialogue dialogue node
Transition transition node — strip the > prefix
Centered text (> ... <) action node (no centered node in Scriptty schema)
Synopsis (= ...) append to scene_cards[matching_scene].description (\\n-joined if multiple)
Section (# ... to ######) append to scene_cards[next_scene].notes prefixed with [[#section depth=N]] so #188 can re-emit
Note ([[ ... ]]) append to scene_cards[containing_scene].notes
Boneyard dropped silently, counted
Dual dialogue (^) sequential Character/Dialogue pairs (caret stripped, no marking — documented loss)
Page break (===) dropped (Scriptty has continuous-page editor; no page-break node)
Lyrics (~) action node (rare; promote to its own node later if writers need it)
Emphasis */**/***/_ strip markers, keep text. Honor \\* etc. as literal. Documented v1 data loss.

Malayalam handling (critical — see umbrella research comment)

  • A Malayalam line cannot satisfy the all-caps character rule (caseless script).
  • Auto-detect will treat unforced Malayalam character cues as Action — this is unrecoverable on import.
  • The @ prefix is the only trustworthy signal: @രമേശ്character node with text രമേശ്. Trust it regardless of caps.
  • Document this in the import-summary toast: if zero @-prefixed cues are found in a file with substantial Malayalam content, surface a warning ("Fountain file appears to contain Malayalam but no @-prefixed character cues — characters may have imported as action. The sender should re-export with forced character prefixes.").

Implementation sketch

  • New module src-tauri/src/screenplay/fountain_import.rs. Symmetric to the existing fountain.rs (export).
  • Public surface: parse_fountain(input: &str) -> Result<(ScreenplayDocument, ImportSummary), String> where ImportSummary carries boneyards_dropped, sections_count, synopses_count, notes_count, dual_dialogue_count, scene_numbers_dropped, emphasis_stripped, plus warnings list.
  • Hand-rolled parser — Fountain is simple enough that a nom/pest dependency would cost more than it saves. Plain string scanning + line iteration.
  • Pipeline stages as separate functions: strip_boneyard, extract_notes, parse_title_page, tokenize_body, fold_tokens.

Test plan

  • Round-trip fixtures: take 3–4 .screenplay files, export to Fountain via existing fountain.rs (after Fountain export: round-trip synopses, sections, inline notes, meta.extra #188 lands), re-import, diff. Must reach a fixed point after one full round.
  • Hand-authored Fountain samples in src-tauri/tests/fixtures/fountain/:
    • title page only (standard + non-standard keys)
    • all element types
    • all forced types (., !, @, >, ~, =, #)
    • nested parentheticals, multi-paragraph action
    • Malayalam content with @-forced characters
    • Malayalam content without @ (must warn in summary)
    • sections at various depths (1–6)
    • synopsis on first scene + before any scene
    • boneyard (single-line + multi-line + mid-line)
    • dual dialogue
    • two-space-empty-line in dialogue (kept-alive blocks)
    • emphasis markers (stripped)
    • escape sequences (\\*, \\_)
    • page breaks (===, ====)
    • centered text
    • FADE IN: at top
    • all-caps action like BANG! followed by blank line (must stay action)
    • all-caps action ending TO: (auto-detected as transition — verify behavior matches spec, even if "wrong" by author intent)
  • Unit tests on the lexer for ambiguous cases per the precedence list.
  • Cross-check tricky cases against nyousefi/Fountain and wildwinter/screenplay-tools outputs (don't copy code).

Out of scope

  • Inline emphasis → ProseMirror marks (data-loss tradeoff for v1)
  • Lyrics as a dedicated node
  • Custom scene numbers (#1A#) round-trip
  • Headers/footers Highland-style directives ({{HEADER: ...}})

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions