A Zig library for Final Draft fdx screenplay interchange.
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Mikael Säker 44d22ea35a docs: repoint links to git.urverk.org
The referenced repos have migrated; their codeberg copies are deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 11:43:43 +02:00
src Document owns an internal arena (parse: thousands of allocs → a handful) 2026-07-01 12:12:10 +02:00
tests Initial commit: zfdx — Final Draft interchange for Zig 2026-06-30 10:28:37 +02:00
.gitignore Initial commit: zfdx — Final Draft interchange for Zig 2026-06-30 10:28:37 +02:00
build.zig Gate fuzz harnesses behind -Dfuzz (skip by default) 2026-07-01 02:52:01 +02:00
build.zig.zon Initial commit: zfdx — Final Draft interchange for Zig 2026-06-30 10:28:37 +02:00
CLAUDE.md docs: repoint links to git.urverk.org 2026-07-25 11:43:43 +02:00
LICENSE Initial commit: zfdx — Final Draft interchange for Zig 2026-06-30 10:28:37 +02:00
README.md Preserve all run styles; capture the title page as raw paragraphs 2026-06-30 23:57:14 +02:00

zfdx

A Zig library for Final Draft screenplay interchange, built around one small, neutral data model.

Final Draft scripts travel in two shapes that share the same element vocabulary:

  • .fdx — Final Draft's XML file format.
  • clipboard RTF — what Final Draft puts on the macOS pasteboard (RTF carrying FDElementName markers).

zfdx parses both into a typed list of screenplay Elements — each a Final Draft type name ("Scene Heading", "Action", "Dialogue", …), its text, and inline emphasis spans — and writes them back out, with a focus on faithful, stable round-trips. It is intentionally model-agnostic: it knows nothing about any app's document model, so each consumer maps Element ↔ its own element kinds.

Depends only on std.

Usage

const zfdx = @import("zfdx");

// Final Draft XML files
var doc = try zfdx.parse(allocator, fdx_bytes);   // .fdx → Document
defer doc.deinit();
for (doc.elements) |el| {
    // el.fd_type, el.text, el.styles
}
const out = try zfdx.write(allocator, doc);       // Document → .fdx
defer allocator.free(out);

// macOS clipboard RTF
const els = try zfdx.rtf.parse(allocator, rtf_bytes, null); // RTF → []Element
const rtf = try zfdx.rtf.emit(allocator, els);              // []Element → RTF

Data model (zfdx.types)

Element   = struct { fd_type: []const u8, text: []const u8, styles: []const StyleSpan, dual: Dual }
StyleSpan = struct { start: usize, end: usize, style: Style }   // byte offsets
Style     = struct { bold, italic, underline, all_caps, strikeout, superscript, subscript, hidden: bool }
Dual      = enum { none, left, right }                          // dual-dialogue column
TitlePara = struct { alignment: Alignment, text: []const u8, styles: []const StyleSpan }
Alignment = enum { left, center, right, full }
Document  = struct { elements: []Element, title: []TitlePara,  }    // owns its contents

Scope

  • Run styles: the full Final Draft set is preserved — Bold, Italic, Underline, AllCaps, Strikeout, SuperScript, SubScript, HiddenText. Consumers that only handle some (e.g. a Fountain writer → Bold/Italic/Underline) just ignore the rest; nothing is dropped at the library layer.
  • Dual dialogue (<DualDialogue>) is preserved: the elements stay flat, but each is tagged with its column (Element.dual = .left / .right), which maps to Fountain's ^ on the right speaker's cue.
  • Title page: captured as raw, positioned paragraphs (Document.title: []TitlePara of { alignment, text, styles }, spacers included). FD's title page is free-form layout with no field labels, so any key/value interpretation (e.g. Fountain's Title: / Author:) is left to the consumer.

Testing

zig build test runs the unit suite plus two seeded fuzz harnesses:

  • structural — random documents check that write is canonical, i.e. write(parse(write(D))) == write(D) byte-for-byte;
  • robustnessparse is required to be total on arbitrary / truncated / corrupted input (no crash, loop, or leak).

Real Final Draft files live under tests/ and are round-tripped as part of the suite.

License

MIT — see LICENSE.