fountainrb 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +34 -0
- data/LICENSE.txt +21 -0
- data/README.md +373 -0
- data/exe/fountainrb +98 -0
- data/lib/fountainrb/elements.rb +271 -0
- data/lib/fountainrb/errors.rb +9 -0
- data/lib/fountainrb/html_renderer.rb +159 -0
- data/lib/fountainrb/inline.rb +356 -0
- data/lib/fountainrb/parser.rb +137 -0
- data/lib/fountainrb/plain_text_renderer.rb +137 -0
- data/lib/fountainrb/screenplay.rb +285 -0
- data/lib/fountainrb/title_page.rb +186 -0
- data/lib/fountainrb/tokenizer.rb +390 -0
- data/lib/fountainrb/validator.rb +294 -0
- data/lib/fountainrb/version.rb +6 -0
- data/lib/fountainrb.rb +65 -0
- metadata +98 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7c4e3a96631f3e62fb3011f17bda182be0cb2154a9d60bfd269afc10b6af31b9
|
|
4
|
+
data.tar.gz: 8992168df09c6cad811720c44cc94e6654f78a3f8559eb18ab12cd95b0ad3f2b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6b55bc1931fdf5bd7bc96a4b17e33a88c1bfd2467c85dbd3f4cb70994b9fecdd7c6adc75d27a920e7f1890ac9e076351397576eb2723d6b27cfb4a44cb3084ca
|
|
7
|
+
data.tar.gz: 191b0da8606a8bce16c6f100aafb393c503099e3061c344568934d80f3a572271666673fe537412661c2bba93f6714ef5fa9cc3a635d102853c084c457def22f
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `fountainrb` are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [1.0.0] - 2025-01-01
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Pure-Ruby Fountain parser with no runtime dependencies.
|
|
12
|
+
- Title page parsing with multi-line values, arbitrary keys and convenience
|
|
13
|
+
readers (`title`, `authors`, `credit`, `source`, `draft_date`, `contact`,
|
|
14
|
+
`copyright`, `notes`).
|
|
15
|
+
- Scene headings, including `INT`, `EXT`, `EST`, `INT./EXT`, `INT/EXT`, `I/E`,
|
|
16
|
+
forced headings (`.`), and scene numbers (`#1A#`).
|
|
17
|
+
- Action with preserved indentation, tab expansion and blank-line metadata.
|
|
18
|
+
- Character cues with extensions, forced cues (`@`) and dual-dialogue carets.
|
|
19
|
+
- Dialogue, parentheticals, lyrics (`~`), transitions, centered text, page
|
|
20
|
+
breaks, sections, synopses, notes and boneyards.
|
|
21
|
+
- Inline emphasis parser and renderer for italics, bold, bold italics,
|
|
22
|
+
underline, escaping and notes.
|
|
23
|
+
- `Screenplay` object model with enumerable elements, scene/character/dialogue
|
|
24
|
+
helpers, dialogue-block grouping and dual-dialogue pairing.
|
|
25
|
+
- HTML renderer with semantic classes and configurable sections, synopses,
|
|
26
|
+
notes, title page and page breaks.
|
|
27
|
+
- Plain-text renderer.
|
|
28
|
+
- JSON serialisation via `Screenplay#to_h` and `Screenplay#to_json`.
|
|
29
|
+
- Validation API: `FountainRB.valid?`, `FountainRB.validate`,
|
|
30
|
+
`Screenplay#valid?`, `Screenplay#issues`, with line-numbered
|
|
31
|
+
`FountainRB::Issue` results for unclosed notes/boneyards, stray closers,
|
|
32
|
+
malformed scene numbers and orphan dual-dialogue carets.
|
|
33
|
+
- `fountainrb` command line interface with HTML, text, JSON and AST output.
|
|
34
|
+
- 500+ RSpec examples, including two full integration fixtures.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Distark
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# fountainrb
|
|
2
|
+
|
|
3
|
+
[](https://github.com/jtoy/fountainrb/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.ruby-lang.org/)
|
|
5
|
+
[](LICENSE.txt)
|
|
6
|
+
|
|
7
|
+
A dependency-free, pure-Ruby parser for [Fountain](https://fountain.io/syntax/)
|
|
8
|
+
screenplays.
|
|
9
|
+
|
|
10
|
+
`fountainrb` turns Fountain source into a structured, enumerable object model
|
|
11
|
+
that you can render, inspect, transform, or serialise to JSON. It implements
|
|
12
|
+
the complete Fountain syntax — title pages, scene headings, action, characters
|
|
13
|
+
and extensions, dialogue, parentheticals, dual dialogue, lyrics, transitions,
|
|
14
|
+
centered text, page breaks, sections, synopses, notes, boneyards, and inline
|
|
15
|
+
emphasis — and never raises on malformed markup.
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
require "fountainrb"
|
|
19
|
+
|
|
20
|
+
screenplay = FountainRB.parse(File.read("brick_and_steel.fountain"))
|
|
21
|
+
|
|
22
|
+
screenplay.title_page.title #=> ["BRICK & STEEL", "FULL RETIRED"]
|
|
23
|
+
screenplay.title_page.authors #=> ["Stu Maschwitz"]
|
|
24
|
+
screenplay.scenes.first.text #=> "EXT. BRICK'S PATIO - DAY"
|
|
25
|
+
screenplay.character_names #=> ["STEEL", "BRICK", ...]
|
|
26
|
+
screenplay.dialogue_blocks.first.text #=> "Beer's ready!"
|
|
27
|
+
|
|
28
|
+
puts screenplay.to_html
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
Add it to your Gemfile:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
gem "fountainrb"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Until the gem is published on RubyGems, install it straight from GitHub:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
gem "fountainrb", github: "jtoy/fountainrb"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or install it directly:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
gem install fountainrb
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
There are no runtime dependencies. Ruby 2.7 or newer is required.
|
|
52
|
+
|
|
53
|
+
## Quick start
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
require "fountainrb"
|
|
57
|
+
|
|
58
|
+
source = <<~FOUNTAIN
|
|
59
|
+
Title: My First Script
|
|
60
|
+
Author: A. Writer
|
|
61
|
+
|
|
62
|
+
INT. KITCHEN - MORNING
|
|
63
|
+
|
|
64
|
+
A KETTLE boils. Steam fogs the window.
|
|
65
|
+
|
|
66
|
+
MOM (O. S.)
|
|
67
|
+
(calling upstairs)
|
|
68
|
+
Breakfast!
|
|
69
|
+
|
|
70
|
+
SAM
|
|
71
|
+
Coming!
|
|
72
|
+
FOUNTAIN
|
|
73
|
+
|
|
74
|
+
screenplay = FountainRB.parse(source)
|
|
75
|
+
|
|
76
|
+
screenplay.title_page.title #=> ["My First Script"]
|
|
77
|
+
screenplay.scenes.first.text #=> "INT. KITCHEN - MORNING"
|
|
78
|
+
screenplay.characters.map(&:name) #=> ["MOM", "SAM"]
|
|
79
|
+
|
|
80
|
+
screenplay.elements.each do |element|
|
|
81
|
+
puts "#{element.type}: #{element.text.inspect}"
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Parse a file instead:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
screenplay = FountainRB.parse_file("script.fountain")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## The object model
|
|
92
|
+
|
|
93
|
+
`FountainRB.parse` returns a `FountainRB::Screenplay`. It is `Enumerable` over
|
|
94
|
+
its `elements`, which appear in source order.
|
|
95
|
+
|
|
96
|
+
Every element offers:
|
|
97
|
+
|
|
98
|
+
| Method | Description |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| `#type` | A symbol such as `:scene_heading` or `:dialogue`. |
|
|
101
|
+
| `#text` | The element text, with the Fountain prefix removed but inline markup intact. |
|
|
102
|
+
| `#line` | 1-based source line where the element begins. |
|
|
103
|
+
| `#blank_before` | Number of blank source lines immediately before it. |
|
|
104
|
+
| `#forced?` | Whether the writer used an explicit prefix (`.`, `@`, `!`, `>` or `~`). |
|
|
105
|
+
| `#notes` | Inline note texts found in the element. |
|
|
106
|
+
| `#to_h` | A JSON-friendly hash. |
|
|
107
|
+
|
|
108
|
+
Element classes:
|
|
109
|
+
|
|
110
|
+
| Class | Type | Notes |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| `SceneHeading` | `:scene_heading` | `#scene_number` for `#1A#` style numbers. |
|
|
113
|
+
| `Action` | `:action` | Multi-line; `#lines`. |
|
|
114
|
+
| `Character` | `:character` | `#name`, `#extensions`, `#extension`, `#dual?`. |
|
|
115
|
+
| `Parenthetical` | `:parenthetical` | `#content` without the parentheses. |
|
|
116
|
+
| `Dialogue` | `:dialogue` | Multi-line; `#lines`. |
|
|
117
|
+
| `Lyrics` | `:lyrics` | Multi-line. |
|
|
118
|
+
| `Transition` | `:transition` | |
|
|
119
|
+
| `Centered` | `:centered` | Fountain's centered text. |
|
|
120
|
+
| `PageBreak` | `:page_break` | |
|
|
121
|
+
| `Section` | `:section` | `#level` (number of `#`). |
|
|
122
|
+
| `Synopsis` | `:synopsis` | |
|
|
123
|
+
| `Note` | `:note` | A note on its own line. |
|
|
124
|
+
|
|
125
|
+
### Screenplay helpers
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
screenplay.title_page # TitlePage or nil
|
|
129
|
+
screenplay.elements # Array<Element>
|
|
130
|
+
screenplay.scenes # Array<SceneHeading> (alias: scene_headings)
|
|
131
|
+
screenplay.action # Array<Action>
|
|
132
|
+
screenplay.characters # Array<Character>
|
|
133
|
+
screenplay.character_names
|
|
134
|
+
screenplay.dialogues # Array<Dialogue> (alias: dialogue)
|
|
135
|
+
screenplay.dialogue_blocks
|
|
136
|
+
screenplay.units # elements + dialogue blocks + dual-dialogue pairs
|
|
137
|
+
screenplay.parentheticals
|
|
138
|
+
screenplay.lyrics
|
|
139
|
+
screenplay.transitions
|
|
140
|
+
screenplay.centered
|
|
141
|
+
screenplay.page_breaks
|
|
142
|
+
screenplay.sections
|
|
143
|
+
screenplay.synopses
|
|
144
|
+
screenplay.notes # every note, standalone and inline
|
|
145
|
+
screenplay.boneyards # the text of every /* ... */ comment
|
|
146
|
+
screenplay.of_type(:dialogue)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Dialogue is grouped into `DialogueBlock`s:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
block = screenplay.dialogue_blocks.first
|
|
153
|
+
block.character # Character
|
|
154
|
+
block.parts # Parentheticals, Dialogue and Notes
|
|
155
|
+
block.dialogue # Array<Dialogue>
|
|
156
|
+
block.parentheticals # Array<Parenthetical>
|
|
157
|
+
block.text # all spoken text joined with newlines
|
|
158
|
+
block.dual? # true when marked with ^
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
A caret on a cue pairs that block with the previous one. `screenplay.units`
|
|
162
|
+
returns a `FountainRB::DualDialogue` for such pairs, with `#left` and `#right`.
|
|
163
|
+
The flat `#elements` list still contains every element individually.
|
|
164
|
+
|
|
165
|
+
### Title page
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
page = screenplay.title_page
|
|
169
|
+
|
|
170
|
+
page["title"] #=> ["BRICK & STEEL", "FULL RETIRED"]
|
|
171
|
+
page.title #=> ["BRICK & STEEL", "FULL RETIRED"]
|
|
172
|
+
page.fetch("title") #=> "BRICK & STEEL"
|
|
173
|
+
page.authors # "Author" + "Authors"
|
|
174
|
+
page.credit
|
|
175
|
+
page.source
|
|
176
|
+
page.draft_date
|
|
177
|
+
page.contact
|
|
178
|
+
page.copyright
|
|
179
|
+
page.notes
|
|
180
|
+
page.keys
|
|
181
|
+
page.to_h
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Keys are normalised to lowercase, and a key may appear more than once. Values
|
|
185
|
+
on following lines must be indented with three or more spaces or a tab.
|
|
186
|
+
|
|
187
|
+
## Inline markup
|
|
188
|
+
|
|
189
|
+
`FountainRB::Inline` parses and renders the markup that lives inside elements.
|
|
190
|
+
Fountain uses Markdown-style emphasis, except that underscores mean underline:
|
|
191
|
+
|
|
192
|
+
| Markup | Result |
|
|
193
|
+
| --- | --- |
|
|
194
|
+
| `*italics*` | `<em>italics</em>` |
|
|
195
|
+
| `**bold**` | `<strong>bold</strong>` |
|
|
196
|
+
| `***bold italics***` | `<strong><em>bold italics</em></strong>` |
|
|
197
|
+
| `_underline_` | `<u>underline</u>` |
|
|
198
|
+
| `[[a note]]` | a note (omitted from formatted output) |
|
|
199
|
+
| `\*escaped\*` | literal asterisks |
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
FountainRB::Inline.to_html("_Steel's face FILLS the *scope*._")
|
|
203
|
+
#=> "<u>Steel's face FILLS the <em>scope</em>.</u>"
|
|
204
|
+
|
|
205
|
+
FountainRB::Inline.to_plain_text("A *very* good day.")
|
|
206
|
+
#=> "A very good day."
|
|
207
|
+
|
|
208
|
+
nodes = FountainRB::Inline.parse("A *very* good day.")
|
|
209
|
+
nodes.map(&:to_h)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Emphasis never crosses a line break, and a backslash escapes the following
|
|
213
|
+
markup character. Notes may span a single line break, but not a blank line.
|
|
214
|
+
Notes are omitted from rendered output unless you ask for them.
|
|
215
|
+
|
|
216
|
+
## Rendering
|
|
217
|
+
|
|
218
|
+
### HTML
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
screenplay.to_html
|
|
222
|
+
# or
|
|
223
|
+
FountainRB::HTMLRenderer.new.render(screenplay)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Options:
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
screenplay.to_html(
|
|
230
|
+
notes: true, # render notes (default: false)
|
|
231
|
+
sections: true, # render sections (default: false)
|
|
232
|
+
synopses: true, # render synopses (default: false)
|
|
233
|
+
title_page: true, # render the title page (default: true)
|
|
234
|
+
page_breaks: true # render page breaks (default: true)
|
|
235
|
+
)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The output uses semantic classes such as `.fountain`, `.title-page`,
|
|
239
|
+
`.scene-heading`, `.action`, `.dialogue-block`, `.character`, `.dialogue`,
|
|
240
|
+
`.parenthetical`, `.transition`, `.centered`, `.lyrics`, `.dual-dialogue`,
|
|
241
|
+
`.page-break`, `.section`, `.synopsis` and `.note`. Bring your own CSS.
|
|
242
|
+
|
|
243
|
+
### Plain text
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
screenplay.to_plain_text
|
|
247
|
+
FountainRB::PlainTextRenderer.new(notes: true).render(screenplay)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
This renderer is intentionally simple — it indents characters, dialogue and
|
|
251
|
+
parentheticals and does not wrap lines — but it is handy for smoke tests and
|
|
252
|
+
quick inspection.
|
|
253
|
+
|
|
254
|
+
### JSON
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
screenplay.to_h
|
|
258
|
+
screenplay.to_json
|
|
259
|
+
require "json"
|
|
260
|
+
JSON.pretty_generate(screenplay.to_h)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Validation
|
|
264
|
+
|
|
265
|
+
Fountain is deliberately forgiving: `FountainRB.parse` always succeeds and
|
|
266
|
+
falls back to Action for anything it does not understand. If you want to warn
|
|
267
|
+
writers about likely mistakes, use the validator:
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
FountainRB.valid?(source) #=> false
|
|
271
|
+
|
|
272
|
+
FountainRB.validate(source).each do |issue|
|
|
273
|
+
puts "#{issue.line}: #{issue.severity}: #{issue.message}"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
screenplay = FountainRB.parse(source)
|
|
277
|
+
screenplay.valid?
|
|
278
|
+
screenplay.issues
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
`valid?` is true when validation finds no issues. `validate` returns an array
|
|
282
|
+
of `FountainRB::Issue` objects, ordered by line. Each issue exposes `code`,
|
|
283
|
+
`message`, `line`, `severity` (`:error` or `:warning`), `error?`, `warning?`,
|
|
284
|
+
`to_h` and `to_s`.
|
|
285
|
+
|
|
286
|
+
| Code | Severity | Meaning |
|
|
287
|
+
| --- | --- | --- |
|
|
288
|
+
| `unclosed_boneyard` | error | A `/*` comment is never closed and swallows the rest of the file. |
|
|
289
|
+
| `unexpected_boneyard_close` | warning | A `*/` has no matching `/*`. |
|
|
290
|
+
| `unclosed_note` | error | A `[[` note is never closed. |
|
|
291
|
+
| `unexpected_note_close` | warning | A `]]` has no matching `[[`. |
|
|
292
|
+
| `note_spans_blank_line` | warning | A note crosses a true blank line, so it will not be parsed as a note (use a two-space line to connect it). |
|
|
293
|
+
| `malformed_scene_number` | warning | A scene heading ends with a `#` number that is missing its closing `#`. |
|
|
294
|
+
| `orphan_dual_dialogue` | warning | A `^` caret has no preceding dialogue block. |
|
|
295
|
+
|
|
296
|
+
Validation is conservative: it only reports problems that the parser would
|
|
297
|
+
otherwise silently recover from. In particular, emphasis is not validated,
|
|
298
|
+
because asterisks and underscores are common literal text.
|
|
299
|
+
|
|
300
|
+
## Command line interface
|
|
301
|
+
|
|
302
|
+
The gem ships with a `fountainrb` executable.
|
|
303
|
+
|
|
304
|
+
```sh
|
|
305
|
+
fountainrb script.fountain # HTML
|
|
306
|
+
fountainrb --text script.fountain # plain text
|
|
307
|
+
fountainrb --json script.fountain # JSON AST
|
|
308
|
+
fountainrb --ast script.fountain # human-readable element dump
|
|
309
|
+
|
|
310
|
+
fountainrb --notes --sections --synopses script.fountain
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Syntax coverage
|
|
314
|
+
|
|
315
|
+
| Fountain feature | Supported |
|
|
316
|
+
| --- | --- |
|
|
317
|
+
| Title page, multi-line values, arbitrary keys | ✅ |
|
|
318
|
+
| Scene headings (`INT`, `EXT`, `EST`, `INT./EXT`, `INT/EXT`, `I/E`, plus `EXT/INT`) | ✅ |
|
|
319
|
+
| Forced scene headings (`.`) and scene numbers (`#1A#`) | ✅ |
|
|
320
|
+
| Action, forced action (`!`), indentation, tabs, vertical whitespace | ✅ |
|
|
321
|
+
| Character cues, extensions, forced cues (`@`) | ✅ |
|
|
322
|
+
| Dialogue, multi-line dialogue, two-space blank lines | ✅ |
|
|
323
|
+
| Parentheticals | ✅ |
|
|
324
|
+
| Dual dialogue (`^`) | ✅ |
|
|
325
|
+
| Lyrics (`~`) | ✅ |
|
|
326
|
+
| Transitions (`... TO:` and forced `>`) | ✅ |
|
|
327
|
+
| Centered text (`> ... <`) | ✅ |
|
|
328
|
+
| Emphasis (`*`, `**`, `***`, `_`) | ✅ |
|
|
329
|
+
| Escaping with backslashes | ✅ |
|
|
330
|
+
| Page breaks (`===`) | ✅ |
|
|
331
|
+
| Sections (`#`) and synopses (`=`) | ✅ |
|
|
332
|
+
| Notes (`[[...]]`), inline and multi-line | ✅ |
|
|
333
|
+
| Boneyards (`/* ... */`), inline and multi-line | ✅ |
|
|
334
|
+
| Punctuation is left exactly as written | ✅ |
|
|
335
|
+
|
|
336
|
+
Error handling follows Fountain's philosophy: when in doubt, return the text as
|
|
337
|
+
Action. Unclosed emphasis, notes, and boneyards are handled gracefully —
|
|
338
|
+
unclosed boneyards comment out the remainder of the file.
|
|
339
|
+
|
|
340
|
+
### Implementation notes
|
|
341
|
+
|
|
342
|
+
* Boneyards are removed before parsing. A multi-line boneyard is replaced by
|
|
343
|
+
the same number of newlines, which keeps source line numbers accurate and
|
|
344
|
+
lets the comment behave as whitespace. The removed text is available through
|
|
345
|
+
`screenplay.boneyards`.
|
|
346
|
+
* A line consisting of two or more spaces keeps dialogue open and produces an
|
|
347
|
+
empty line inside the dialogue, as described by the spec. Outside dialogue it
|
|
348
|
+
is treated as a normal blank line.
|
|
349
|
+
* Sections, synopses and standalone notes are ignored in formatted output and
|
|
350
|
+
do not disturb the blank-line context used to classify the following line.
|
|
351
|
+
* Centered text is exposed as its own `:centered` element rather than being
|
|
352
|
+
indistinguishable from Action.
|
|
353
|
+
* In addition to the scene-heading prefixes listed by the spec, `EXT/INT` and
|
|
354
|
+
`EXT./INT.` are recognised, because they appear in real screenplays (such as
|
|
355
|
+
the official Big Fish sample).
|
|
356
|
+
|
|
357
|
+
## Development
|
|
358
|
+
|
|
359
|
+
```sh
|
|
360
|
+
bundle install
|
|
361
|
+
bundle exec rake # run the specs
|
|
362
|
+
bundle exec rspec # run the specs directly
|
|
363
|
+
bundle exec rake doc # documentation formatter
|
|
364
|
+
bundle exec rake console # IRB with fountainrb loaded
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
The test suite contains 500+ examples covering every element type, inline
|
|
368
|
+
markup, validation, edge cases, both renderers, the CLI, and two full
|
|
369
|
+
integration fixtures.
|
|
370
|
+
|
|
371
|
+
## License
|
|
372
|
+
|
|
373
|
+
fountainrb is available under the MIT license. See [LICENSE.txt](LICENSE.txt).
|
data/exe/fountainrb
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "optparse"
|
|
5
|
+
|
|
6
|
+
# Allow running the executable directly from a source checkout.
|
|
7
|
+
lib = File.expand_path("../lib", __dir__)
|
|
8
|
+
$LOAD_PATH.unshift(lib) if File.exist?(File.join(lib, "fountainrb.rb"))
|
|
9
|
+
|
|
10
|
+
require "fountainrb"
|
|
11
|
+
|
|
12
|
+
options = {
|
|
13
|
+
format: :html,
|
|
14
|
+
notes: false,
|
|
15
|
+
sections: false,
|
|
16
|
+
synopses: false
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
parser = OptionParser.new do |opts|
|
|
20
|
+
opts.banner = "Usage: fountainrb [options] FILE"
|
|
21
|
+
opts.separator ""
|
|
22
|
+
opts.separator "Output formats:"
|
|
23
|
+
opts.on("--html", "Render HTML (default)") { options[:format] = :html }
|
|
24
|
+
opts.on("--text", "Render plain text") { options[:format] = :text }
|
|
25
|
+
opts.on("--json", "Emit the parsed screenplay as JSON") { options[:format] = :json }
|
|
26
|
+
opts.on("--ast", "Print a human-readable element tree") { options[:format] = :ast }
|
|
27
|
+
|
|
28
|
+
opts.separator ""
|
|
29
|
+
opts.separator "Options:"
|
|
30
|
+
opts.on("--notes", "Include notes in the output") { options[:notes] = true }
|
|
31
|
+
opts.on("--sections", "Include sections in the output") { options[:sections] = true }
|
|
32
|
+
opts.on("--synopses", "Include synopses in the output") { options[:synopses] = true }
|
|
33
|
+
|
|
34
|
+
opts.separator ""
|
|
35
|
+
opts.separator "Other:"
|
|
36
|
+
opts.on("-v", "--version", "Print the version and exit") do
|
|
37
|
+
puts FountainRB::VERSION
|
|
38
|
+
exit
|
|
39
|
+
end
|
|
40
|
+
opts.on("-h", "--help", "Show this help and exit") do
|
|
41
|
+
puts opts
|
|
42
|
+
exit
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
begin
|
|
47
|
+
parser.parse!
|
|
48
|
+
rescue OptionParser::ParseError => error
|
|
49
|
+
warn "fountainrb: #{error.message}"
|
|
50
|
+
warn parser
|
|
51
|
+
exit 1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if ARGV.empty?
|
|
55
|
+
warn "fountainrb: no input file given"
|
|
56
|
+
warn parser
|
|
57
|
+
exit 1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
path = ARGV.shift
|
|
61
|
+
|
|
62
|
+
begin
|
|
63
|
+
screenplay = FountainRB.parse_file(path)
|
|
64
|
+
rescue Errno::ENOENT
|
|
65
|
+
warn "fountainrb: no such file: #{path}"
|
|
66
|
+
exit 1
|
|
67
|
+
rescue SystemCallError => error
|
|
68
|
+
warn "fountainrb: #{error.message}"
|
|
69
|
+
exit 1
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
case options[:format]
|
|
73
|
+
when :html
|
|
74
|
+
puts screenplay.to_html(
|
|
75
|
+
notes: options[:notes],
|
|
76
|
+
sections: options[:sections],
|
|
77
|
+
synopses: options[:synopses]
|
|
78
|
+
)
|
|
79
|
+
when :text
|
|
80
|
+
puts screenplay.to_plain_text(
|
|
81
|
+
notes: options[:notes],
|
|
82
|
+
sections: options[:sections],
|
|
83
|
+
synopses: options[:synopses]
|
|
84
|
+
)
|
|
85
|
+
when :json
|
|
86
|
+
require "json"
|
|
87
|
+
puts JSON.pretty_generate(screenplay.to_h)
|
|
88
|
+
when :ast
|
|
89
|
+
if screenplay.title_page
|
|
90
|
+
puts "title_page #{screenplay.title_page.to_h.inspect}"
|
|
91
|
+
end
|
|
92
|
+
screenplay.elements.each do |element|
|
|
93
|
+
hash = element.to_h
|
|
94
|
+
details = hash.slice(*(hash.keys - %w[type line text]))
|
|
95
|
+
puts format("%4s %-14s %p%s", element.line, element.type, element.text,
|
|
96
|
+
details.empty? ? "" : " #{details.inspect}")
|
|
97
|
+
end
|
|
98
|
+
end
|