Skip to content

Fix duplicate text-style-def ids in multi-title documents #35

Description

@leogdion

Problem

Sources/FCPKitDSL/Title.swift:64-76 hardcodes the id "ts1" in both the
TextStyle(ref:) and the TextStyleDef(id:) it emits — for every title:

let style = FCPKit.TextStyle(ref: "ts1", content: text)
let definition = FCPKit.TextStyleDef(id: "ts1", textStyle: ...)

Final Cut's own DTD (FCPXMLv1_14.dtd:581, shipped inside
Final Cut Pro.app/Contents/Frameworks/Interchange.framework) declares:

<!ATTLIST text-style-def id ID #REQUIRED>

XML requires values of type ID to be unique within a document. Any document
containing two titles therefore emits invalid XML today.
This is latent only
because no shipped document has more than one title; the presentation deck has seven.

Reproduced

This four-line document is enough:

Project(name: "Two Titles") {
  Sequence {
    Title("First", duration: FCPTime(numerator: 5))
    Title("Second", duration: FCPTime(numerator: 5))
  }
}

Exporting it emits text-style-def id="ts1" twice, and validating the output against
Final Cut's 1.14 DTD fails:

$ xmllint --noout --valid twotitles.xml
twotitles.xml:29: element text-style-def: validity error : ID ts1 already defined
                                <text-style-def id="ts1">

Real Final Cut output shows the correct behavior: in
Tests/FCPKitTests/TestData/UntitledXML.fcpxml, two titles produce ids
ts1, ts2, ts3, ts4 — numbered globally across separate titles, not restarted
per title. Match that.

Files

  • Modify Sources/FCPKitDSL/ResourceStore.swift — add a text-style id allocator
  • Modify Sources/FCPKitDSL/Title.swift — consume it in build

API

Internal only; no public surface change. ResourceStore already has a nextNumber
counter for resource ids (ResourceStore.swift:41) — model this on it:

private var nextTextStyleNumber = 1

internal mutating func textStyleID() -> String {
  defer { nextTextStyleNumber += 1 }
  return "ts\(nextTextStyleNumber)"
}

Title.build already receives inout ResourceStore, so no signature change is
needed — take one let styleID = resources.textStyleID() and use it in both places.

Tests

New Tests/FCPKitDSLTests/TextStyleIDTests.swift:

  • A document with three titles yields textStyleDef ids ["ts1", "ts2", "ts3"], all
    distinct.
  • Each title's text[0].textStyle[0].ref equals that same title's
    textStyleDef[0].id — the styles must stay correctly paired, not merely unique.
  • The multi-title document passes assertDTDValidates. This is the assertion that
    fails on main today
    and is the regression guard.

Acceptance criteria

  • A two-title document DTD-validates.
  • Single-title documents still emit exactly ts1, because the counter starts at 1 —
    so FeaturePairAcceptanceTests.titlesFeaturePairMatchesAfterNormalize passes
    unchanged, with no fixture edits. This structural diff against a real Final Cut
    export is the backward-compatibility proof.

Conventions

  • MIT header block on every new file (see Scripts/header.sh).
  • Swift Testing (import Testing, @Test) for new tests, per .claude/agent-notes.md. Include "Tests" in either the parent enum or the child struct, never both.
  • Doc comments on every public declaration.
  • Keep files under 225 lines (SwiftLint file_length); prefer Type+Modifiers.swift splits.
  • Run swift test and swift run fcpxml-diff schema-completeness Tests/FCPKitTests/TestData before each PR. Prefer opening a PR over merging.

Full spec with context and rationale: docs/planning/demo-presentation-video.md

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingready-for-agentFully specified, ready for an AFK agentv0.1.0FCPKit v0.1.0 milestone work

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions