summaryrefslogtreecommitdiff
path: root/fountain/parse.go
diff options
context:
space:
mode:
Diffstat (limited to 'fountain/parse.go')
-rw-r--r--fountain/parse.go100
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
}