feat: first release !
This commit is contained in:
1 parent
77a333e8d9
commit
664527182d
66 files changed
+13812
-16
No files matched your search
@@ -0,0 +1,258 @@
|
||||
// Package screenplay holds the in-memory model of a screenplay: an ordered
|
||||
// list of typed elements (scene heading, action, character, ...) plus a
|
||||
// title page. Every file format and renderer converts to and from this model.
|
||||
package screenplay
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type ElementType string
|
||||
|
||||
const (
|
||||
Scene ElementType = "scene"
|
||||
Action ElementType = "action"
|
||||
Character ElementType = "character"
|
||||
Parenthetical ElementType = "parenthetical"
|
||||
Dialogue ElementType = "dialogue"
|
||||
Transition ElementType = "transition"
|
||||
Centered ElementType = "centered"
|
||||
// Shot is an insert or camera direction ("INSERT - THE LETTER",
|
||||
// "CLOSE ON", "POV"): set like a scene heading but not a scene.
|
||||
Shot ElementType = "shot"
|
||||
// Intertitle is on-screen text: a title card, centred in capitals.
|
||||
Intertitle ElementType = "intertitle"
|
||||
// Section is an outline heading (Fountain "#", "##"…) and Synopsis a
|
||||
// summary line (Fountain "="). Neither is printed.
|
||||
Section ElementType = "section"
|
||||
Synopsis ElementType = "synopsis"
|
||||
)
|
||||
|
||||
// Printed reports whether the element appears on the printed page.
|
||||
func (t ElementType) Printed() bool {
|
||||
return t != Section && t != Synopsis
|
||||
}
|
||||
|
||||
// Uppercase reports whether the element is always printed in capitals.
|
||||
func (t ElementType) Uppercase() bool {
|
||||
return t == Scene || t == Character || t == Transition || t == Shot || t == Intertitle
|
||||
}
|
||||
|
||||
// InDialogue reports whether the element belongs to a dialogue block
|
||||
// (the lines that follow a character cue).
|
||||
func (t ElementType) InDialogue() bool {
|
||||
return t == Parenthetical || t == Dialogue
|
||||
}
|
||||
|
||||
// Run is a span of text sharing the same emphasis. A "\n" inside Text is a
|
||||
// hard line break within the element.
|
||||
type Run struct {
|
||||
Text string `json:"text"`
|
||||
Bold bool `json:"b,omitempty"`
|
||||
Italic bool `json:"i,omitempty"`
|
||||
Underline bool `json:"u,omitempty"`
|
||||
// Note marks a writer's note (Fountain "[[…]]"): kept and shown in the
|
||||
// editor, never printed.
|
||||
Note bool `json:"n,omitempty"`
|
||||
// Rev is the revision set (1 = the first, see Settings.Revisions) in
|
||||
// which the text was added or changed; 0 is original text.
|
||||
Rev int `json:"rev,omitempty"`
|
||||
}
|
||||
|
||||
func (r Run) SameStyle(o Run) bool {
|
||||
return r.Bold == o.Bold && r.Italic == o.Italic && r.Underline == o.Underline && r.Note == o.Note && r.Rev == o.Rev
|
||||
}
|
||||
|
||||
type Element struct {
|
||||
Type ElementType `json:"type"`
|
||||
Runs []Run `json:"runs"`
|
||||
// SceneNumber is a locked scene number ("12A"). Empty means automatic.
|
||||
SceneNumber string `json:"sceneNumber,omitempty"`
|
||||
// Dual, on a character cue, puts its dialogue block beside the
|
||||
// previous one (dual dialogue, Fountain's "^").
|
||||
Dual bool `json:"dual,omitempty"`
|
||||
// Level is the depth of a section, from 1 ("#").
|
||||
Level int `json:"level,omitempty"`
|
||||
// NewPage forces a page break before the element (Fountain "===").
|
||||
// On a dialogue part it applies to the whole dialogue block.
|
||||
NewPage bool `json:"newPage,omitempty"`
|
||||
// Locked lists the locked pages that start inside this element, in
|
||||
// offset order (see Settings.Locked).
|
||||
Locked []PageLock `json:"locked,omitempty"`
|
||||
}
|
||||
|
||||
// PageLock fixes where a page starts once pages are locked: the page
|
||||
// labelled Page starts at rune Offset of the element's text.
|
||||
type PageLock struct {
|
||||
Offset int `json:"offset"`
|
||||
Page string `json:"page"`
|
||||
}
|
||||
|
||||
func NewElement(t ElementType, text string) Element {
|
||||
e := Element{Type: t, Runs: []Run{}}
|
||||
if text != "" {
|
||||
e.Runs = append(e.Runs, Run{Text: text})
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// Text returns the element's plain text, without emphasis.
|
||||
func (e Element) Text() string {
|
||||
var b strings.Builder
|
||||
for _, r := range e.Runs {
|
||||
b.WriteString(r.Text)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (e Element) IsEmpty() bool {
|
||||
return strings.TrimSpace(e.Text()) == ""
|
||||
}
|
||||
|
||||
// PrintedText returns the text that is printed: without notes.
|
||||
func (e Element) PrintedText() string {
|
||||
var b strings.Builder
|
||||
for _, r := range e.Runs {
|
||||
if !r.Note {
|
||||
b.WriteString(r.Text)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
type TitlePage struct {
|
||||
Title string `json:"title"`
|
||||
Credit string `json:"credit"`
|
||||
Author string `json:"author"`
|
||||
Source string `json:"source"`
|
||||
DraftDate string `json:"draftDate"`
|
||||
Contact string `json:"contact"`
|
||||
Notes string `json:"notes"`
|
||||
}
|
||||
|
||||
func (t TitlePage) IsEmpty() bool {
|
||||
return strings.TrimSpace(t.Title+t.Author+t.Source+t.DraftDate+t.Contact+t.Notes) == ""
|
||||
}
|
||||
|
||||
type Document struct {
|
||||
TitlePage TitlePage `json:"titlePage"`
|
||||
Elements []Element `json:"elements"`
|
||||
Settings Settings `json:"settings"`
|
||||
}
|
||||
|
||||
// Paper is the page size: "" for US Letter, or A4.
|
||||
type Paper string
|
||||
|
||||
const (
|
||||
Letter Paper = ""
|
||||
A4 Paper = "a4"
|
||||
)
|
||||
|
||||
// Settings are the document's production settings. The zero value is a
|
||||
// US Letter script without revisions.
|
||||
type Settings struct {
|
||||
Paper Paper `json:"paper,omitempty"`
|
||||
// Revisions are the revision sets issued so far, in order: the text
|
||||
// of set n carries Run.Rev == n.
|
||||
Revisions []Revision `json:"revisions,omitempty"`
|
||||
// Revising marks new text with the latest revision set.
|
||||
Revising bool `json:"revising,omitempty"`
|
||||
// Locked keeps the page numbers of Element.Locked: added material goes
|
||||
// to A pages (12A), removed material leaves shorter or merged pages.
|
||||
Locked bool `json:"locked,omitempty"`
|
||||
}
|
||||
|
||||
func (s Settings) IsZero() bool {
|
||||
return s.Paper == Letter && len(s.Revisions) == 0 && !s.Revising && !s.Locked
|
||||
}
|
||||
|
||||
// Revision is a revision set, named after the colour of its pages.
|
||||
type Revision struct {
|
||||
Name string `json:"name"` // "Blue", "Double Pink"
|
||||
Color string `json:"color"` // "blue", "pink"…: the paper colour
|
||||
Date string `json:"date"`
|
||||
}
|
||||
|
||||
// revisionColors is the industry order of revision pages. The first cycle
|
||||
// starts at Blue (the original draft is White); later cycles start again
|
||||
// at White, as "Double White", "Double Blue"…
|
||||
var revisionColors = []string{"White", "Blue", "Pink", "Yellow", "Green", "Goldenrod", "Buff", "Salmon", "Cherry"}
|
||||
|
||||
// RevisionSet returns the name and colour of revision set n (from 1).
|
||||
func RevisionSet(n int) Revision {
|
||||
n = max(n, 1)
|
||||
cycle, i := n/len(revisionColors), n%len(revisionColors)
|
||||
name := revisionColors[i]
|
||||
r := Revision{Name: name, Color: strings.ToLower(name)}
|
||||
switch cycle {
|
||||
case 0:
|
||||
case 1:
|
||||
r.Name = "Double " + name
|
||||
case 2:
|
||||
r.Name = "Triple " + name
|
||||
default:
|
||||
r.Name = ordinal(cycle+1) + " " + name
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func ordinal(n int) string {
|
||||
suffix := "th"
|
||||
if n%100 < 11 || n%100 > 13 {
|
||||
switch n % 10 {
|
||||
case 1:
|
||||
suffix = "st"
|
||||
case 2:
|
||||
suffix = "nd"
|
||||
case 3:
|
||||
suffix = "rd"
|
||||
}
|
||||
}
|
||||
return strconv.Itoa(n) + suffix
|
||||
}
|
||||
|
||||
// New returns an empty screenplay, ready to receive its first scene heading.
|
||||
func New() *Document {
|
||||
return &Document{
|
||||
TitlePage: TitlePage{Credit: "Written by"},
|
||||
Elements: []Element{NewElement(Scene, "")},
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeRuns drops empty runs and merges neighbours with the same style.
|
||||
func NormalizeRuns(runs []Run) []Run {
|
||||
out := make([]Run, 0, len(runs))
|
||||
for _, r := range runs {
|
||||
if r.Text == "" {
|
||||
continue
|
||||
}
|
||||
if n := len(out); n > 0 && out[n-1].SameStyle(r) {
|
||||
out[n-1].Text += r.Text
|
||||
continue
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// CharacterName extracts the bare name from a character cue:
|
||||
// "John (V.O.)" -> "JOHN".
|
||||
func CharacterName(cue string) string {
|
||||
cue = strings.TrimSuffix(strings.TrimSpace(cue), "^")
|
||||
if i := strings.Index(cue, "("); i >= 0 {
|
||||
cue = cue[:i]
|
||||
}
|
||||
return strings.ToUpper(strings.TrimSpace(cue))
|
||||
}
|
||||
|
||||
// UpperRunes upper-cases rune by rune so that offsets are preserved
|
||||
// (strings.ToUpper may change the length, e.g. "ß" -> "SS").
|
||||
func UpperRunes(s string) string {
|
||||
r := []rune(s)
|
||||
for i, c := range r {
|
||||
r[i] = unicode.ToUpper(c)
|
||||
}
|
||||
return string(r)
|
||||
}
|
||||
Reference in new issue
Block a user