diff options
Diffstat (limited to 'fountain')
-rw-r--r-- | fountain/parse.go | 100 |
1 files changed, 83 insertions, 17 deletions
diff --git a/fountain/parse.go b/fountain/parse.go index a3e1747..c0fff65 100644 --- a/fountain/parse.go +++ b/fountain/parse.go @@ -1,46 +1,112 @@ +// Fountain is a Markdown-like language for screenplays and the main inspiration for this tool. +// Read more at fountain.io package fountain import ( - "github.com/lapingvino/lexington/lex" - "strings" "bufio" + "github.com/lapingvino/lexington/lex" "io" + "strings" ) +var Scene = []string{"INT", "EXT", "EST", "INT./EXT", "INT/EXT", "I/E"} + +func CheckScene(row string) (bool, string, string) { + var scene bool + row = strings.ToUpper(row) + for _, prefix := range Scene { + if strings.HasPrefix(row, prefix+" ") || + strings.HasPrefix(row, prefix+".") { + scene = true + } + } + return scene, "scene", row +} + +func CheckTransition(row string) (bool, string, string) { + var trans bool + row = strings.ToUpper(row) + if strings.HasPrefix(row, ">") || strings.HasSuffix(row, " TO:") { + trans = true + } + return trans, "trans", strings.Trim(row, ">< ") +} + +func CheckSynopse(row string) (bool, string, string) { + var synopse bool + if strings.HasPrefix(row, "=") { + synopse = true + } + return synopse, "synopse", strings.TrimLeft(row, "= ") +} + +func CheckSection(row string) (bool, string, string) { + var section bool + if strings.HasPrefix(row, "#") { + section = true + } + return section, "section", row +} + +// This is a Fountain parser, trying to be as close as possible to the description +// found at https://fountain.io/syntax but it can be incomplete. +// Over time more and more parts should be configurable here, e.g. INT/EXT translatable to other languages. func Parse(file io.Reader) (out lex.Screenplay) { var err error var s string - var toParse []string + var toParse []string // Fill with two to avoid out of bounds when backtracking f := bufio.NewReader(file) for err == nil { s, err = f.ReadString('\n') toParse = append(toParse, s) } + toParse = append(toParse, "") // Trigger the backtracking also for the last line + out = make(lex.Screenplay, 2, len(toParse)) for i, row := range toParse { - row = strings.TrimSpace(row) + i += 2 + row = strings.TrimRight(row, "\n\r") action := "action" if row == strings.ToUpper(row) { action = "allcaps" } if row == "" { action = "empty" - continue + + // Backtracking for elements that need a following empty line + checkfuncs := []func(string) (bool, string, string){ + CheckScene, + CheckTransition, + CheckSynopse, + CheckSection, + } + for _, checkfunc := range checkfuncs { + check, element, contents := checkfunc(out[i-1].Contents) + if check && out[i-2].Contents == "" { + out[i-1].Type = element + out[i-1].Contents = contents + break + } + } } - if i <= 0 { - continue + if out[i-1].Type != "action" { + out[i-1].Contents = strings.TrimSpace(out[i-1].Contents) } - switch out[i-1].Type { - case "allcaps": - out[i-1].Type = "speaker" - if row[0] == '(' && row[len(row)-1] == ')' { - action = "paren" - } else { - action = "dialog" + + // Backtracking to check for dialog sequence + if row != "" { + switch out[i-1].Type { + case "allcaps": + out[i-1].Type = "speaker" + fallthrough + case "paren", "dialog": + if row[0] == '(' && row[len(row)-1] == ')' { + action = "paren" + } else { + action = "dialog" + } } - case "paren", "dialog": - action = "dialog" } out = append(out, lex.Line{action, row}) } - return out + return out[2:] // Remove the safety empty rows } |