Files
plotzilla/internal/fountain/fountain_test.go
T
2026-09-28 14:21:06 +02:00

231 lines
7.1 KiB
Go

package fountain
import (
"reflect"
"strings"
"testing"
"plotzilla/internal/screenplay"
)
const sample = `Title: The Last Reel
Credit: Written by
Author: Jane Doe
Draft date: 1 Sept 2026
INT. PROJECTION BOOTH - NIGHT #1#
Dust hangs in the **beam** of the projector.
INSERT - THE TICKET STUB
> Paris, 1942 <
MARGOT (V.O.)
(quietly)
It always ends *here*.
Right here.
CUT TO:
.FLASHBACK
@McCLANE
Yippee.
HANS ^
Nein.
!SHOUTING IS ACTION
and continues.
> THE END <
`
func types(doc *screenplay.Document) []screenplay.ElementType {
var out []screenplay.ElementType
for _, e := range doc.Elements {
out = append(out, e.Type)
}
return out
}
func TestParse(t *testing.T) {
doc := Parse(sample)
if doc.TitlePage.Title != "The Last Reel" || doc.TitlePage.Author != "Jane Doe" {
t.Fatalf("title page: %+v", doc.TitlePage)
}
want := []screenplay.ElementType{
screenplay.Scene, screenplay.Action, screenplay.Shot, screenplay.Centered,
screenplay.Character, screenplay.Parenthetical, screenplay.Dialogue,
screenplay.Transition, screenplay.Scene,
screenplay.Character, screenplay.Dialogue,
screenplay.Character, screenplay.Dialogue,
screenplay.Action, screenplay.Intertitle,
}
if got := types(doc); !reflect.DeepEqual(got, want) {
t.Fatalf("types:\n got %v\nwant %v", got, want)
}
if doc.Elements[0].SceneNumber != "1" || doc.Elements[0].Text() != "INT. PROJECTION BOOTH - NIGHT" {
t.Errorf("scene: %+v", doc.Elements[0])
}
if r := doc.Elements[1].Runs; len(r) != 3 || !r[1].Bold || r[1].Text != "beam" {
t.Errorf("bold runs: %+v", r)
}
if got := doc.Elements[2].Text(); got != "INSERT - THE TICKET STUB" {
t.Errorf("shot: %q", got)
}
if got := doc.Elements[6].Text(); got != "It always ends here.\nRight here." {
t.Errorf("dialogue: %q", got)
}
if got := doc.Elements[9].Text(); got != "McCLANE" {
t.Errorf("forced character: %q", got)
}
if hans := doc.Elements[11]; hans.Text() != "HANS" || !hans.Dual || doc.Elements[9].Dual {
t.Errorf("dual cue: %+v", hans)
}
}
func TestInlineLiteralAsterisk(t *testing.T) {
runs := parseInline("2 * 3 = 6")
if len(runs) != 1 || runs[0].Text != "2 * 3 = 6" || runs[0].Italic {
t.Errorf("got %+v", runs)
}
}
func TestRoundTrip(t *testing.T) {
first := Parse(sample)
second := Parse(Write(first))
if !reflect.DeepEqual(first, second) {
t.Fatalf("round trip changed the document:\n%s", Write(first))
}
}
const outline = `# Act One
## The Booth
= Margot finds the reel.
INT. PROJECTION BOOTH - NIGHT
Dust hangs in the beam. [[Too dark? Try dawn.]]
[[Cut the cat
or keep it for the ending.]]
MARGOT [[she whispers]]
(quietly)
It always ends here.
`
func TestOutlineAndNotes(t *testing.T) {
doc := Parse(outline)
want := []screenplay.ElementType{
screenplay.Section, screenplay.Section, screenplay.Synopsis, screenplay.Scene,
screenplay.Action, screenplay.Action, screenplay.Character, screenplay.Parenthetical, screenplay.Dialogue,
}
if got := types(doc); !reflect.DeepEqual(got, want) {
t.Fatalf("types:\n got %v\nwant %v", got, want)
}
els := doc.Elements
if els[0].Level != 1 || els[0].Text() != "Act One" || els[1].Level != 2 || els[1].Text() != "The Booth" {
t.Errorf("sections: %+v %+v", els[0], els[1])
}
if els[2].Text() != "Margot finds the reel." {
t.Errorf("synopsis: %q", els[2].Text())
}
if r := els[4].Runs; len(r) != 2 || r[1].Text != "Too dark? Try dawn." || !r[1].Note || r[0].Note {
t.Errorf("inline note: %+v", r)
}
if r := els[5].Runs; len(r) != 1 || !r[0].Note || r[0].Text != "Cut the cat\n\nor keep it for the ending." {
t.Errorf("note paragraph: %+v", r)
}
if els[6].PrintedText() != "MARGOT " || !els[6].Runs[1].Note {
t.Errorf("note after a cue: %+v", els[6].Runs)
}
if second := Parse(Write(doc)); !reflect.DeepEqual(doc, second) {
t.Fatalf("round trip changed the document:\n%s", Write(doc))
}
}
func TestRevisionsAndLocks(t *testing.T) {
doc := &screenplay.Document{
TitlePage: screenplay.TitlePage{Title: "Reel"},
Settings: screenplay.Settings{
Paper: screenplay.A4,
Revisions: []screenplay.Revision{{Name: "Blue", Color: "blue", Date: "09/28/26"}, {Name: "Pink", Color: "pink", Date: "*/"}},
Revising: true,
Locked: true,
},
Elements: []screenplay.Element{
{Type: screenplay.Scene, Runs: []screenplay.Run{{Text: "INT. BOOTH - NIGHT"}}, Locked: []screenplay.PageLock{{Offset: 0, Page: "2"}}},
{Type: screenplay.Action, Runs: []screenplay.Run{
{Text: "Dust hangs in the "}, {Text: "golden", Rev: 2, Bold: true}, {Text: " beam.", Rev: 1},
{Text: " She waits.", Rev: 1, Note: true}, {Text: " Then leaves."},
}, Locked: []screenplay.PageLock{{Offset: 25, Page: "3"}, {Offset: 25, Page: "4"}}},
{Type: screenplay.Character, Runs: []screenplay.Run{{Text: "MARGOT"}}},
{Type: screenplay.Parenthetical, Runs: []screenplay.Run{{Text: "(quietly)", Rev: 1}}, Locked: []screenplay.PageLock{{Offset: 0, Page: "4A"}}},
{Type: screenplay.Dialogue, Runs: []screenplay.Run{{Text: "Here."}}},
{Type: screenplay.Shot, Runs: []screenplay.Run{{Text: "INSERT - ", Rev: 1}, {Text: "THE TICKET"}}},
},
}
out := Write(doc)
if !strings.Contains(out, "/*page 2*/INT. BOOTH") || !strings.Contains(out, "/*rev 2*/**golden**/*/rev*//*rev 1*/ /*page 3*//*page 4*/beam.") {
t.Errorf("markers:\n%s", out)
}
if got := Parse(out); !reflect.DeepEqual(doc, got) {
for i := range got.Elements {
if !reflect.DeepEqual(got.Elements[i], doc.Elements[i]) {
t.Errorf("element %d:\n got %#v\nwant %#v", i, got.Elements[i], doc.Elements[i])
}
}
t.Fatalf("round trip changed the document:\n%s", out)
}
}
func TestPlainScriptUnchanged(t *testing.T) {
// No Plotzilla data in a script that uses none of it.
if out := Write(Parse(sample)); strings.Contains(out, "/*") || strings.Contains(out, "[[") {
t.Errorf("unexpected markers:\n%s", out)
}
}
func TestPastedText(t *testing.T) {
els := ParseText("# Act Two\r\n\r\nJOHN\r\nHi. [[louder]]\r\n")
if len(els) != 3 || els[0].Type != screenplay.Section || els[2].PrintedText() != "Hi. " {
t.Errorf("got %+v", els)
}
}
func TestInvalidNoteStaysText(t *testing.T) {
// An empty line ends a note: without the two spaces this is not one.
doc := Parse("Wait [[for it\n\nnow]] here.\n")
if len(doc.Elements) != 2 || doc.Elements[0].Text() != "Wait [[for it" || doc.Elements[0].Runs[0].Note {
t.Errorf("got %+v", doc.Elements)
}
}
func TestForcedPageBreak(t *testing.T) {
doc := Parse("INT. HOUSE - DAY\n\nHe waits.\n\n===\n\nEXT. ROAD - NIGHT\n\nJOHN\nHi.\n\n===\nLater.\n")
want := []bool{false, false, true, false, false, true}
for i, el := range doc.Elements {
if el.NewPage != want[i] {
t.Errorf("element %d %q: NewPage = %v", i, el.Text(), el.NewPage)
}
}
out := Write(doc)
if !strings.Contains(out, "He waits.\n\n===\n\nEXT. ROAD") {
t.Errorf("page break not written:\n%s", out)
}
if second := Parse(out); !reflect.DeepEqual(doc, second) {
t.Fatalf("round trip changed the document:\n%s", out)
}
// The page break of an empty element moves to the next one written.
doc.Elements[1] = screenplay.Element{Type: screenplay.Action, NewPage: true}
if again := Parse(Write(doc)); !again.Elements[1].NewPage || again.Elements[1].Type != screenplay.Scene {
t.Errorf("carried page break: %+v", again.Elements)
}
}