1
0
Fork
You've already forked zfdx
0
A Zig library for Final Draft fdx screenplay interchange.
  • Zig 100%
Mikael Säker 5fdf01f107 Document owns an internal arena (parse: thousands of allocs → a handful)
fdx.parse built the Document with a per-field allocation for every element's
fd_type/text/styles and each ArrayList growth — thousands of small allocations,
which the Debug allocator's per-allocation tracking made very slow (a
feature-length script took ~390ms just to parse in a debug build; ~4ms in
release).
Document now owns an internal ArenaAllocator: parse allocates all contents from
it (a handful of arena chunks instead of thousands of backing calls) and deinit
is a single arena free. `arena` is optional — null for the borrowed views a
caller assembles to hand to `write` (which only reads elements/title), so
`deinit` is a safe no-op there.
Tests + gated fuzz (leak check) pass; opening Big Fish in reveal's debug build
dropped from ~2.30s to ~1.70s (the fdx-open overhead over baseline: ~1.15s → ~0.15s).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026年07月01日 12:12:10 +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 Initial commit: zfdx — Final Draft interchange for Zig 2026年06月30日 10:28:37 +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

constzfdx=@import("zfdx");// Final Draft XML filesvardoc=tryzfdx.parse(allocator,fdx_bytes);// .fdx → Documentdeferdoc.deinit();for(doc.elements)|el|{// el.fd_type, el.text, el.styles}constout=tryzfdx.write(allocator,doc);// Document → .fdxdeferallocator.free(out);// macOS clipboard RTFconstels=tryzfdx.rtf.parse(allocator,rtf_bytes,null);// RTF → []Elementconstrtf=tryzfdx.rtf.emit(allocator,els);// []Element → RTF

Data model (zfdx.types)

Element=struct{fd_type:[]constu8,text:[]constu8,styles:[]constStyleSpan,dual:Dual}StyleSpan=struct{start:usize,end:usize,style:Style}// byte offsetsStyle=struct{bold,italic,underline,all_caps,strikeout,superscript,subscript,hidden:bool}Dual=enum{none,left,right}// dual-dialogue columnTitlePara=struct{alignment:Alignment,text:[]constu8,styles:[]constStyleSpan}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.