A Zig library for Final Draft fdx screenplay interchange.
- Zig 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
The referenced repos have migrated; their codeberg copies are deleted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
| src | ||
| tests | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| CLAUDE.md | ||
| LICENSE | ||
| README.md | ||
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
FDElementNamemarkers).
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: []TitleParaof{ 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'sTitle:/Author:) is left to the consumer.
Testing
zig build test runs the unit suite plus two seeded fuzz harnesses:
- structural — random documents check that
writeis canonical, i.e.write(parse(write(D))) == write(D)byte-for-byte; - robustness —
parseis 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.