summaryrefslogtreecommitdiff
path: root/fountain/parse.go
blob: c0fff65ae83b81271ea1674871233bb2eea1f306 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// Fountain is a Markdown-like language for screenplays and the main inspiration for this tool.
// Read more at fountain.io
package fountain

import (
	"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 // 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 {
		i += 2
		row = strings.TrimRight(row, "\n\r")
		action := "action"
		if row == strings.ToUpper(row) {
			action = "allcaps"
		}
		if row == "" {
			action = "empty"

			// 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 out[i-1].Type != "action" {
			out[i-1].Contents = strings.TrimSpace(out[i-1].Contents)
		}

		// 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"
				}
			}
		}
		out = append(out, lex.Line{action, row})
	}
	return out[2:] // Remove the safety empty rows
}