fountain-js
Advanced tools
Comparing version 1.0.0 to 1.1.0
@@ -9,8 +9,26 @@ # Change Log | ||
## [1.1.0] - 2023-06-26 | ||
### Added | ||
- Support for single-letter character names. | ||
- Support for single-letter scene headings | ||
### Fixed | ||
- Lyrics can now be parsed from dialogue. | ||
- `INT./EXT` and `INT/EXT` in scene headings are now parsable. | ||
### Changed | ||
- Tokens are now OOP-based with backwards-compatibility for legacy tokens. | ||
## [1.0.0] - 2022-04-18 | ||
### Changed | ||
- Updated package and repository name to `fountain-js`. | ||
### Deprecated | ||
- `Fountain.ts` namepsace is now deprecated in favor of `fountain-js` |
export const regex = { | ||
title_page: /^((?:title|credit|author[s]?|source|notes|draft date|date|contact|copyright)\:)/gim, | ||
scene_heading: /^((?:\*{0,3}_?)?(?:(?:int|ext|est|i\/e)[. ]).+)|^(?:\.(?!\.+))(.+)/i, | ||
scene_heading: /^((?:\*{0,3}_?)?(?:(?:in|ex)t(?:\/ext)?|est|i\/e)[. ].+)|^(?:\.(?!\.+))(\S.*)/i, | ||
scene_number: /( *#(.+)# *)/, | ||
@@ -5,0 +5,0 @@ transition: /^((?:FADE (?:TO BLACK|OUT)|CUT TO BLACK)\.|.+ TO\:)|^(?:> *)(.+)/, |
import { Token } from './token'; | ||
export declare class Scanner { | ||
private tokens; | ||
private lastLineWasDualDialogue; | ||
tokenize(script: string): Token[]; | ||
} |
import { regex } from './regex'; | ||
import { ActionToken, BoneyardToken, CenteredToken, DialogueBlock, LineBreakToken, LyricsToken, NoteToken, PageBreakToken, SceneHeadingToken, SectionToken, SynopsisToken, TitlePageBlock, TransitionToken } from './token'; | ||
import { Lexer } from './lexer'; | ||
export class Scanner { | ||
constructor() { | ||
this.tokens = []; | ||
} | ||
tokenize(script) { | ||
// reverse the array so that dual dialog can be constructed bottom up | ||
const source = new Lexer().reconstruct(script).split(regex.splitter).reverse(); | ||
let line; | ||
let match; | ||
let dual; | ||
for (line of source) { | ||
const tokens = source.reduce((previous, line) => { | ||
/** title page */ | ||
if (regex.title_page.test(line)) { | ||
match = line.replace(regex.title_page, '\n$1').split(regex.splitter).reverse(); | ||
for (let item of match) { | ||
let pair = item.replace(regex.cleaner, '').split(/\:\n*/); | ||
this.tokens.push({ type: pair[0].trim().toLowerCase().replace(' ', '_'), is_title: true, text: pair[1].trim() }); | ||
} | ||
continue; | ||
if (TitlePageBlock.matchedBy(line)) { | ||
return new TitlePageBlock(line).addTo(previous); | ||
} | ||
/** scene headings */ | ||
if (match = line.match(regex.scene_heading)) { | ||
let text = match[1] || match[2]; | ||
let meta; | ||
let num; | ||
if (text.indexOf(' ') !== text.length - 2) { | ||
if (meta = text.match(regex.scene_number)) { | ||
num = meta[2]; | ||
text = text.replace(regex.scene_number, ''); | ||
} | ||
this.tokens.push({ type: 'scene_heading', text: text, scene_number: num || undefined }); | ||
} | ||
continue; | ||
if (SceneHeadingToken.matchedBy(line)) { | ||
return new SceneHeadingToken(line).addTo(previous); | ||
} | ||
/** centered */ | ||
if (match = line.match(regex.centered)) { | ||
this.tokens.push({ type: 'centered', text: match[0].replace(/>|</g, '') }); | ||
continue; | ||
if (CenteredToken.matchedBy(line)) { | ||
return new CenteredToken(line).addTo(previous); | ||
} | ||
/** transitions */ | ||
if (match = line.match(regex.transition)) { | ||
this.tokens.push({ type: 'transition', text: match[1] || match[2] }); | ||
continue; | ||
if (TransitionToken.matchedBy(line)) { | ||
return new TransitionToken(line).addTo(previous); | ||
} | ||
/** dialogue blocks - characters, parentheticals and dialogue */ | ||
if (match = line.match(regex.dialogue)) { | ||
let name = match[1] || match[2]; | ||
if (name.indexOf(' ') !== name.length - 2) { | ||
// iterating from the bottom up, so push dialogue blocks in reverse order | ||
if (match[3]) { | ||
this.tokens.push({ type: 'dual_dialogue_end' }); | ||
} | ||
this.tokens.push({ type: 'dialogue_end' }); | ||
let parts = match[4].split(/(\(.+\))(?:\n+)/).reverse(); | ||
for (let part of parts) { | ||
if (part.length > 0) { | ||
this.tokens.push({ type: regex.parenthetical.test(part) ? 'parenthetical' : 'dialogue', text: part }); | ||
} | ||
} | ||
this.tokens.push({ type: 'character', text: name.trim() }); | ||
this.tokens.push({ type: 'dialogue_begin', dual: match[3] ? 'right' : dual ? 'left' : undefined }); | ||
if (dual) { | ||
this.tokens.push({ type: 'dual_dialogue_begin' }); | ||
} | ||
dual = match[3] ? true : false; | ||
continue; | ||
} | ||
if (DialogueBlock.matchedBy(line)) { | ||
const dialogueBlock = new DialogueBlock(line, this.lastLineWasDualDialogue); | ||
this.lastLineWasDualDialogue = dialogueBlock.dual; | ||
return dialogueBlock.addTo(previous); | ||
} | ||
/** section */ | ||
if (match = line.match(regex.section)) { | ||
this.tokens.push({ type: 'section', text: match[2], depth: match[1].length }); | ||
continue; | ||
if (SectionToken.matchedBy(line)) { | ||
return new SectionToken(line).addTo(previous); | ||
} | ||
/** synopsis */ | ||
if (match = line.match(regex.synopsis)) { | ||
this.tokens.push({ type: 'synopsis', text: match[1] }); | ||
continue; | ||
if (SynopsisToken.matchedBy(line)) { | ||
return new SynopsisToken(line).addTo(previous); | ||
} | ||
/** notes */ | ||
if (match = line.match(regex.note)) { | ||
this.tokens.push({ type: 'note', text: match[1] }); | ||
continue; | ||
if (NoteToken.matchedBy(line)) { | ||
return new NoteToken(line).addTo(previous); | ||
} | ||
/** boneyard */ | ||
if (match = line.match(regex.boneyard)) { | ||
this.tokens.push({ type: match[0][0] === '/' ? 'boneyard_begin' : 'boneyard_end' }); | ||
continue; | ||
if (BoneyardToken.matchedBy(line)) { | ||
return new BoneyardToken(line).addTo(previous); | ||
} | ||
/** lyrics */ | ||
if (match = line.match(regex.lyrics)) { | ||
this.tokens.push({ type: 'lyrics', text: match[0].replace(/^~(?![ ])/gm, '') }); | ||
continue; | ||
if (LyricsToken.matchedBy(line)) { | ||
return new LyricsToken(line).addTo(previous); | ||
} | ||
/** page breaks */ | ||
if (regex.page_break.test(line)) { | ||
this.tokens.push({ type: 'page_break' }); | ||
continue; | ||
if (PageBreakToken.matchedBy(line)) { | ||
return new PageBreakToken().addTo(previous); | ||
} | ||
/** line breaks */ | ||
if (regex.line_break.test(line)) { | ||
this.tokens.push({ type: 'line_break' }); | ||
continue; | ||
if (LineBreakToken.matchedBy(line)) { | ||
return new LineBreakToken().addTo(previous); | ||
} | ||
// everything else is action -- remove `!` for forced action | ||
this.tokens.push({ type: 'action', text: line.replace(/^!(?![ ])/gm, '') }); | ||
} | ||
return this.tokens.reverse(); | ||
return new ActionToken(line).addTo(previous); | ||
}, []); | ||
return tokens.reverse(); | ||
} | ||
} | ||
//# sourceMappingURL=scanner.js.map |
@@ -8,2 +8,138 @@ export interface Token { | ||
depth?: number; | ||
addTo(tokens: Token[]): Token[]; | ||
} | ||
export interface Block { | ||
tokens: Token[]; | ||
addTo(tokens: Token[]): Token[]; | ||
} | ||
export declare class TitlePageBlock implements Block { | ||
readonly tokens: TitlePageToken[]; | ||
constructor(line: string); | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class TitlePageToken implements Token { | ||
readonly type: string; | ||
readonly is_title = true; | ||
readonly text: string; | ||
constructor(item: string); | ||
addTo(tokens: Token[]): Token[]; | ||
} | ||
export declare class SceneHeadingToken implements Token { | ||
readonly type = "scene_heading"; | ||
readonly text: string; | ||
readonly scene_number: string; | ||
constructor(line: string); | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class CenteredToken implements Token { | ||
readonly type = "centered"; | ||
readonly text: string; | ||
constructor(line: string); | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class TransitionToken implements Token { | ||
readonly type = "transition"; | ||
readonly text: string; | ||
constructor(line: string); | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class DialogueBlock implements Block { | ||
readonly tokens: Token[]; | ||
readonly dual: boolean; | ||
readonly too_short: boolean; | ||
constructor(line: string, dual: boolean); | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class DialogueBeginToken implements Token { | ||
readonly type = "dialogue_begin"; | ||
readonly dual: 'left' | 'right' | undefined; | ||
constructor(dual?: 'left' | 'right'); | ||
addTo(tokens: Token[]): Token[]; | ||
} | ||
export declare class CharacterToken implements Token { | ||
readonly type = "character"; | ||
readonly text: string; | ||
constructor(text: string); | ||
addTo(tokens: Token[]): Token[]; | ||
} | ||
export declare class DialogueToken implements Token { | ||
readonly type = "dialogue"; | ||
readonly text: string; | ||
constructor(text: string); | ||
addTo(tokens: Token[]): Token[]; | ||
} | ||
export declare class DialogueEndToken implements Token { | ||
readonly type = "dialogue_end"; | ||
addTo(tokens: Token[]): Token[]; | ||
} | ||
export declare class ParentheticalToken implements Token { | ||
readonly type = "parenthetical"; | ||
readonly text: string; | ||
constructor(text: string); | ||
addTo(tokens: Token[]): Token[]; | ||
} | ||
export declare class DualDialogueBeginToken implements Token { | ||
readonly type = "dual_dialogue_begin"; | ||
addTo(tokens: Token[]): Token[]; | ||
} | ||
export declare class DualDialogueEndToken implements Token { | ||
readonly type = "dual_dialogue_end"; | ||
addTo(tokens: Token[]): Token[]; | ||
} | ||
export declare class LyricsToken implements Token { | ||
readonly type = "lyrics"; | ||
readonly text: string; | ||
constructor(line: string); | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class SectionToken implements Token { | ||
readonly type = "section"; | ||
readonly text: string; | ||
readonly depth: number; | ||
constructor(line: string); | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class SynopsisToken implements Token { | ||
readonly type = "synopsis"; | ||
readonly text: string; | ||
constructor(line: string); | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class NoteToken implements Token { | ||
readonly type = "note"; | ||
readonly text: string; | ||
constructor(line: string); | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class BoneyardToken implements Token { | ||
readonly type: 'boneyard_begin' | 'boneyard_end'; | ||
readonly text: string; | ||
constructor(line: string); | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class PageBreakToken implements Token { | ||
readonly type = "page_break"; | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class LineBreakToken implements Token { | ||
readonly type = "line_break"; | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class ActionToken implements Token { | ||
readonly type = "action"; | ||
readonly text: string; | ||
constructor(line: string); | ||
addTo(tokens: Token[]): Token[]; | ||
} |
@@ -0,1 +1,265 @@ | ||
import { regex } from './regex'; | ||
export class TitlePageBlock { | ||
constructor(line) { | ||
this.tokens = []; | ||
const match = line.replace(regex.title_page, '\n$1').split(regex.splitter).reverse(); | ||
this.tokens = match.reduce((previous, item) => new TitlePageToken(item).addTo(previous), []); | ||
} | ||
addTo(tokens) { | ||
return [...tokens, ...this.tokens]; | ||
} | ||
static matchedBy(line) { | ||
return regex.title_page.test(line); | ||
} | ||
} | ||
export class TitlePageToken { | ||
constructor(item) { | ||
this.is_title = true; | ||
const pair = item.replace(regex.cleaner, '').split(/\:\n*/); | ||
this.type = pair[0].trim().toLowerCase().replace(' ', '_'); | ||
this.text = pair[1].trim(); | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
} | ||
export class SceneHeadingToken { | ||
constructor(line) { | ||
this.type = 'scene_heading'; | ||
const match = line.match(regex.scene_heading); | ||
this.text = match[1] || match[2]; | ||
const meta = this.text.match(regex.scene_number); | ||
if (meta) { | ||
this.scene_number = meta[2]; | ||
this.text = this.text.replace(regex.scene_number, ''); | ||
} | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
static matchedBy(line) { | ||
return regex.scene_heading.test(line); | ||
} | ||
} | ||
export class CenteredToken { | ||
constructor(line) { | ||
this.type = 'centered'; | ||
const match = line.match(regex.centered); | ||
this.text = match[0].replace(/>|</g, ''); | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
static matchedBy(line) { | ||
return regex.centered.test(line); | ||
} | ||
} | ||
export class TransitionToken { | ||
constructor(line) { | ||
this.type = 'transition'; | ||
const match = line.match(regex.transition); | ||
this.text = match[1] || match[2]; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
static matchedBy(line) { | ||
return regex.transition.test(line); | ||
} | ||
} | ||
export class DialogueBlock { | ||
constructor(line, dual) { | ||
this.tokens = []; | ||
const match = line.match(regex.dialogue); | ||
let name = match[1] || match[2]; | ||
// iterating from the bottom up, so push dialogue blocks in reverse order | ||
const isDualDialogue = !!(match[3]); | ||
if (isDualDialogue) { | ||
this.tokens.push(new DualDialogueEndToken()); | ||
} | ||
this.tokens.push(new DialogueEndToken()); | ||
const parts = match[4].split(/(\(.+\))(?:\n+)/).reverse(); | ||
this.tokens.push(...parts.reduce((p, text = '') => { | ||
if (!text.length) { | ||
return p; | ||
} | ||
if (regex.parenthetical.test(text)) { | ||
return [...p, new ParentheticalToken(text)]; | ||
} | ||
if (regex.lyrics.test(text)) { | ||
return [...p, new LyricsToken(text)]; | ||
} | ||
return [...p, new DialogueToken(text)]; | ||
}, [])); | ||
this.tokens.push(new CharacterToken(name.trim()), new DialogueBeginToken(isDualDialogue ? 'right' : dual ? 'left' : undefined)); | ||
if (dual) { | ||
this.tokens.push(new DualDialogueBeginToken()); | ||
} | ||
this.dual = isDualDialogue; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, ...this.tokens]; | ||
} | ||
static matchedBy(line) { | ||
return regex.dialogue.test(line); | ||
} | ||
} | ||
export class DialogueBeginToken { | ||
constructor(dual) { | ||
this.type = 'dialogue_begin'; | ||
this.dual = dual; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
} | ||
export class CharacterToken { | ||
constructor(text) { | ||
this.type = 'character'; | ||
this.text = text; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
} | ||
export class DialogueToken { | ||
constructor(text) { | ||
this.type = 'dialogue'; | ||
this.text = text; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
} | ||
export class DialogueEndToken { | ||
constructor() { | ||
this.type = 'dialogue_end'; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
} | ||
export class ParentheticalToken { | ||
constructor(text) { | ||
this.type = 'parenthetical'; | ||
this.text = text; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
} | ||
export class DualDialogueBeginToken { | ||
constructor() { | ||
this.type = 'dual_dialogue_begin'; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
} | ||
export class DualDialogueEndToken { | ||
constructor() { | ||
this.type = 'dual_dialogue_end'; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
} | ||
export class LyricsToken { | ||
constructor(line) { | ||
this.type = 'lyrics'; | ||
this.text = line.replace(/^~(?![ ])/gm, ''); | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
static matchedBy(line) { | ||
return regex.lyrics.test(line); | ||
} | ||
} | ||
export class SectionToken { | ||
constructor(line) { | ||
this.type = 'section'; | ||
const match = line.match(regex.section); | ||
this.text = match[2]; | ||
this.depth = match[1].length; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
static matchedBy(line) { | ||
return regex.section.test(line); | ||
} | ||
} | ||
export class SynopsisToken { | ||
constructor(line) { | ||
this.type = 'synopsis'; | ||
const match = line.match(regex.synopsis); | ||
this.text = match[1]; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
static matchedBy(line) { | ||
return regex.synopsis.test(line); | ||
} | ||
} | ||
export class NoteToken { | ||
constructor(line) { | ||
this.type = 'note'; | ||
const match = line.match(regex.note); | ||
this.text = match[1]; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
static matchedBy(line) { | ||
return regex.note.test(line); | ||
} | ||
} | ||
export class BoneyardToken { | ||
constructor(line) { | ||
const match = line.match(regex.boneyard); | ||
this.type = match[0][0] === '/' ? 'boneyard_begin' : 'boneyard_end'; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
static matchedBy(line) { | ||
return regex.boneyard.test(line); | ||
} | ||
} | ||
export class PageBreakToken { | ||
constructor() { | ||
this.type = 'page_break'; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
static matchedBy(line) { | ||
return regex.page_break.test(line); | ||
} | ||
} | ||
export class LineBreakToken { | ||
constructor() { | ||
this.type = 'line_break'; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
static matchedBy(line) { | ||
return regex.line_break.test(line); | ||
} | ||
} | ||
export class ActionToken { | ||
constructor(line) { | ||
this.type = 'action'; | ||
this.text = line.replace(/^!(?![ ])/gm, ''); | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
} | ||
function isTooShort(str) { | ||
return str.indexOf(' ') === str.length - 2; | ||
} | ||
//# sourceMappingURL=token.js.map |
@@ -6,3 +6,3 @@ "use strict"; | ||
title_page: /^((?:title|credit|author[s]?|source|notes|draft date|date|contact|copyright)\:)/gim, | ||
scene_heading: /^((?:\*{0,3}_?)?(?:(?:int|ext|est|i\/e)[. ]).+)|^(?:\.(?!\.+))(.+)/i, | ||
scene_heading: /^((?:\*{0,3}_?)?(?:(?:in|ex)t(?:\/ext)?|est|i\/e)[. ].+)|^(?:\.(?!\.+))(\S.*)/i, | ||
scene_number: /( *#(.+)# *)/, | ||
@@ -9,0 +9,0 @@ transition: /^((?:FADE (?:TO BLACK|OUT)|CUT TO BLACK)\.|.+ TO\:)|^(?:> *)(.+)/, |
import { Token } from './token'; | ||
export declare class Scanner { | ||
private tokens; | ||
private lastLineWasDualDialogue; | ||
tokenize(script: string): Token[]; | ||
} |
@@ -5,110 +5,63 @@ "use strict"; | ||
const regex_1 = require("./regex"); | ||
const token_1 = require("./token"); | ||
const lexer_1 = require("./lexer"); | ||
class Scanner { | ||
constructor() { | ||
this.tokens = []; | ||
} | ||
tokenize(script) { | ||
// reverse the array so that dual dialog can be constructed bottom up | ||
const source = new lexer_1.Lexer().reconstruct(script).split(regex_1.regex.splitter).reverse(); | ||
let line; | ||
let match; | ||
let dual; | ||
for (line of source) { | ||
const tokens = source.reduce((previous, line) => { | ||
/** title page */ | ||
if (regex_1.regex.title_page.test(line)) { | ||
match = line.replace(regex_1.regex.title_page, '\n$1').split(regex_1.regex.splitter).reverse(); | ||
for (let item of match) { | ||
let pair = item.replace(regex_1.regex.cleaner, '').split(/\:\n*/); | ||
this.tokens.push({ type: pair[0].trim().toLowerCase().replace(' ', '_'), is_title: true, text: pair[1].trim() }); | ||
} | ||
continue; | ||
if (token_1.TitlePageBlock.matchedBy(line)) { | ||
return new token_1.TitlePageBlock(line).addTo(previous); | ||
} | ||
/** scene headings */ | ||
if (match = line.match(regex_1.regex.scene_heading)) { | ||
let text = match[1] || match[2]; | ||
let meta; | ||
let num; | ||
if (text.indexOf(' ') !== text.length - 2) { | ||
if (meta = text.match(regex_1.regex.scene_number)) { | ||
num = meta[2]; | ||
text = text.replace(regex_1.regex.scene_number, ''); | ||
} | ||
this.tokens.push({ type: 'scene_heading', text: text, scene_number: num || undefined }); | ||
} | ||
continue; | ||
if (token_1.SceneHeadingToken.matchedBy(line)) { | ||
return new token_1.SceneHeadingToken(line).addTo(previous); | ||
} | ||
/** centered */ | ||
if (match = line.match(regex_1.regex.centered)) { | ||
this.tokens.push({ type: 'centered', text: match[0].replace(/>|</g, '') }); | ||
continue; | ||
if (token_1.CenteredToken.matchedBy(line)) { | ||
return new token_1.CenteredToken(line).addTo(previous); | ||
} | ||
/** transitions */ | ||
if (match = line.match(regex_1.regex.transition)) { | ||
this.tokens.push({ type: 'transition', text: match[1] || match[2] }); | ||
continue; | ||
if (token_1.TransitionToken.matchedBy(line)) { | ||
return new token_1.TransitionToken(line).addTo(previous); | ||
} | ||
/** dialogue blocks - characters, parentheticals and dialogue */ | ||
if (match = line.match(regex_1.regex.dialogue)) { | ||
let name = match[1] || match[2]; | ||
if (name.indexOf(' ') !== name.length - 2) { | ||
// iterating from the bottom up, so push dialogue blocks in reverse order | ||
if (match[3]) { | ||
this.tokens.push({ type: 'dual_dialogue_end' }); | ||
} | ||
this.tokens.push({ type: 'dialogue_end' }); | ||
let parts = match[4].split(/(\(.+\))(?:\n+)/).reverse(); | ||
for (let part of parts) { | ||
if (part.length > 0) { | ||
this.tokens.push({ type: regex_1.regex.parenthetical.test(part) ? 'parenthetical' : 'dialogue', text: part }); | ||
} | ||
} | ||
this.tokens.push({ type: 'character', text: name.trim() }); | ||
this.tokens.push({ type: 'dialogue_begin', dual: match[3] ? 'right' : dual ? 'left' : undefined }); | ||
if (dual) { | ||
this.tokens.push({ type: 'dual_dialogue_begin' }); | ||
} | ||
dual = match[3] ? true : false; | ||
continue; | ||
} | ||
if (token_1.DialogueBlock.matchedBy(line)) { | ||
const dialogueBlock = new token_1.DialogueBlock(line, this.lastLineWasDualDialogue); | ||
this.lastLineWasDualDialogue = dialogueBlock.dual; | ||
return dialogueBlock.addTo(previous); | ||
} | ||
/** section */ | ||
if (match = line.match(regex_1.regex.section)) { | ||
this.tokens.push({ type: 'section', text: match[2], depth: match[1].length }); | ||
continue; | ||
if (token_1.SectionToken.matchedBy(line)) { | ||
return new token_1.SectionToken(line).addTo(previous); | ||
} | ||
/** synopsis */ | ||
if (match = line.match(regex_1.regex.synopsis)) { | ||
this.tokens.push({ type: 'synopsis', text: match[1] }); | ||
continue; | ||
if (token_1.SynopsisToken.matchedBy(line)) { | ||
return new token_1.SynopsisToken(line).addTo(previous); | ||
} | ||
/** notes */ | ||
if (match = line.match(regex_1.regex.note)) { | ||
this.tokens.push({ type: 'note', text: match[1] }); | ||
continue; | ||
if (token_1.NoteToken.matchedBy(line)) { | ||
return new token_1.NoteToken(line).addTo(previous); | ||
} | ||
/** boneyard */ | ||
if (match = line.match(regex_1.regex.boneyard)) { | ||
this.tokens.push({ type: match[0][0] === '/' ? 'boneyard_begin' : 'boneyard_end' }); | ||
continue; | ||
if (token_1.BoneyardToken.matchedBy(line)) { | ||
return new token_1.BoneyardToken(line).addTo(previous); | ||
} | ||
/** lyrics */ | ||
if (match = line.match(regex_1.regex.lyrics)) { | ||
this.tokens.push({ type: 'lyrics', text: match[0].replace(/^~(?![ ])/gm, '') }); | ||
continue; | ||
if (token_1.LyricsToken.matchedBy(line)) { | ||
return new token_1.LyricsToken(line).addTo(previous); | ||
} | ||
/** page breaks */ | ||
if (regex_1.regex.page_break.test(line)) { | ||
this.tokens.push({ type: 'page_break' }); | ||
continue; | ||
if (token_1.PageBreakToken.matchedBy(line)) { | ||
return new token_1.PageBreakToken().addTo(previous); | ||
} | ||
/** line breaks */ | ||
if (regex_1.regex.line_break.test(line)) { | ||
this.tokens.push({ type: 'line_break' }); | ||
continue; | ||
if (token_1.LineBreakToken.matchedBy(line)) { | ||
return new token_1.LineBreakToken().addTo(previous); | ||
} | ||
// everything else is action -- remove `!` for forced action | ||
this.tokens.push({ type: 'action', text: line.replace(/^!(?![ ])/gm, '') }); | ||
} | ||
return this.tokens.reverse(); | ||
return new token_1.ActionToken(line).addTo(previous); | ||
}, []); | ||
return tokens.reverse(); | ||
} | ||
@@ -115,0 +68,0 @@ } |
@@ -8,2 +8,138 @@ export interface Token { | ||
depth?: number; | ||
addTo(tokens: Token[]): Token[]; | ||
} | ||
export interface Block { | ||
tokens: Token[]; | ||
addTo(tokens: Token[]): Token[]; | ||
} | ||
export declare class TitlePageBlock implements Block { | ||
readonly tokens: TitlePageToken[]; | ||
constructor(line: string); | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class TitlePageToken implements Token { | ||
readonly type: string; | ||
readonly is_title = true; | ||
readonly text: string; | ||
constructor(item: string); | ||
addTo(tokens: Token[]): Token[]; | ||
} | ||
export declare class SceneHeadingToken implements Token { | ||
readonly type = "scene_heading"; | ||
readonly text: string; | ||
readonly scene_number: string; | ||
constructor(line: string); | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class CenteredToken implements Token { | ||
readonly type = "centered"; | ||
readonly text: string; | ||
constructor(line: string); | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class TransitionToken implements Token { | ||
readonly type = "transition"; | ||
readonly text: string; | ||
constructor(line: string); | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class DialogueBlock implements Block { | ||
readonly tokens: Token[]; | ||
readonly dual: boolean; | ||
readonly too_short: boolean; | ||
constructor(line: string, dual: boolean); | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class DialogueBeginToken implements Token { | ||
readonly type = "dialogue_begin"; | ||
readonly dual: 'left' | 'right' | undefined; | ||
constructor(dual?: 'left' | 'right'); | ||
addTo(tokens: Token[]): Token[]; | ||
} | ||
export declare class CharacterToken implements Token { | ||
readonly type = "character"; | ||
readonly text: string; | ||
constructor(text: string); | ||
addTo(tokens: Token[]): Token[]; | ||
} | ||
export declare class DialogueToken implements Token { | ||
readonly type = "dialogue"; | ||
readonly text: string; | ||
constructor(text: string); | ||
addTo(tokens: Token[]): Token[]; | ||
} | ||
export declare class DialogueEndToken implements Token { | ||
readonly type = "dialogue_end"; | ||
addTo(tokens: Token[]): Token[]; | ||
} | ||
export declare class ParentheticalToken implements Token { | ||
readonly type = "parenthetical"; | ||
readonly text: string; | ||
constructor(text: string); | ||
addTo(tokens: Token[]): Token[]; | ||
} | ||
export declare class DualDialogueBeginToken implements Token { | ||
readonly type = "dual_dialogue_begin"; | ||
addTo(tokens: Token[]): Token[]; | ||
} | ||
export declare class DualDialogueEndToken implements Token { | ||
readonly type = "dual_dialogue_end"; | ||
addTo(tokens: Token[]): Token[]; | ||
} | ||
export declare class LyricsToken implements Token { | ||
readonly type = "lyrics"; | ||
readonly text: string; | ||
constructor(line: string); | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class SectionToken implements Token { | ||
readonly type = "section"; | ||
readonly text: string; | ||
readonly depth: number; | ||
constructor(line: string); | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class SynopsisToken implements Token { | ||
readonly type = "synopsis"; | ||
readonly text: string; | ||
constructor(line: string); | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class NoteToken implements Token { | ||
readonly type = "note"; | ||
readonly text: string; | ||
constructor(line: string); | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class BoneyardToken implements Token { | ||
readonly type: 'boneyard_begin' | 'boneyard_end'; | ||
readonly text: string; | ||
constructor(line: string); | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class PageBreakToken implements Token { | ||
readonly type = "page_break"; | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class LineBreakToken implements Token { | ||
readonly type = "line_break"; | ||
addTo(tokens: Token[]): Token[]; | ||
static matchedBy(line: string): boolean; | ||
} | ||
export declare class ActionToken implements Token { | ||
readonly type = "action"; | ||
readonly text: string; | ||
constructor(line: string); | ||
addTo(tokens: Token[]): Token[]; | ||
} |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.ActionToken = exports.LineBreakToken = exports.PageBreakToken = exports.BoneyardToken = exports.NoteToken = exports.SynopsisToken = exports.SectionToken = exports.LyricsToken = exports.DualDialogueEndToken = exports.DualDialogueBeginToken = exports.ParentheticalToken = exports.DialogueEndToken = exports.DialogueToken = exports.CharacterToken = exports.DialogueBeginToken = exports.DialogueBlock = exports.TransitionToken = exports.CenteredToken = exports.SceneHeadingToken = exports.TitlePageToken = exports.TitlePageBlock = void 0; | ||
const regex_1 = require("./regex"); | ||
class TitlePageBlock { | ||
constructor(line) { | ||
this.tokens = []; | ||
const match = line.replace(regex_1.regex.title_page, '\n$1').split(regex_1.regex.splitter).reverse(); | ||
this.tokens = match.reduce((previous, item) => new TitlePageToken(item).addTo(previous), []); | ||
} | ||
addTo(tokens) { | ||
return [...tokens, ...this.tokens]; | ||
} | ||
static matchedBy(line) { | ||
return regex_1.regex.title_page.test(line); | ||
} | ||
} | ||
exports.TitlePageBlock = TitlePageBlock; | ||
class TitlePageToken { | ||
constructor(item) { | ||
this.is_title = true; | ||
const pair = item.replace(regex_1.regex.cleaner, '').split(/\:\n*/); | ||
this.type = pair[0].trim().toLowerCase().replace(' ', '_'); | ||
this.text = pair[1].trim(); | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
} | ||
exports.TitlePageToken = TitlePageToken; | ||
class SceneHeadingToken { | ||
constructor(line) { | ||
this.type = 'scene_heading'; | ||
const match = line.match(regex_1.regex.scene_heading); | ||
this.text = match[1] || match[2]; | ||
const meta = this.text.match(regex_1.regex.scene_number); | ||
if (meta) { | ||
this.scene_number = meta[2]; | ||
this.text = this.text.replace(regex_1.regex.scene_number, ''); | ||
} | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
static matchedBy(line) { | ||
return regex_1.regex.scene_heading.test(line); | ||
} | ||
} | ||
exports.SceneHeadingToken = SceneHeadingToken; | ||
class CenteredToken { | ||
constructor(line) { | ||
this.type = 'centered'; | ||
const match = line.match(regex_1.regex.centered); | ||
this.text = match[0].replace(/>|</g, ''); | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
static matchedBy(line) { | ||
return regex_1.regex.centered.test(line); | ||
} | ||
} | ||
exports.CenteredToken = CenteredToken; | ||
class TransitionToken { | ||
constructor(line) { | ||
this.type = 'transition'; | ||
const match = line.match(regex_1.regex.transition); | ||
this.text = match[1] || match[2]; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
static matchedBy(line) { | ||
return regex_1.regex.transition.test(line); | ||
} | ||
} | ||
exports.TransitionToken = TransitionToken; | ||
class DialogueBlock { | ||
constructor(line, dual) { | ||
this.tokens = []; | ||
const match = line.match(regex_1.regex.dialogue); | ||
let name = match[1] || match[2]; | ||
// iterating from the bottom up, so push dialogue blocks in reverse order | ||
const isDualDialogue = !!(match[3]); | ||
if (isDualDialogue) { | ||
this.tokens.push(new DualDialogueEndToken()); | ||
} | ||
this.tokens.push(new DialogueEndToken()); | ||
const parts = match[4].split(/(\(.+\))(?:\n+)/).reverse(); | ||
this.tokens.push(...parts.reduce((p, text = '') => { | ||
if (!text.length) { | ||
return p; | ||
} | ||
if (regex_1.regex.parenthetical.test(text)) { | ||
return [...p, new ParentheticalToken(text)]; | ||
} | ||
if (regex_1.regex.lyrics.test(text)) { | ||
return [...p, new LyricsToken(text)]; | ||
} | ||
return [...p, new DialogueToken(text)]; | ||
}, [])); | ||
this.tokens.push(new CharacterToken(name.trim()), new DialogueBeginToken(isDualDialogue ? 'right' : dual ? 'left' : undefined)); | ||
if (dual) { | ||
this.tokens.push(new DualDialogueBeginToken()); | ||
} | ||
this.dual = isDualDialogue; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, ...this.tokens]; | ||
} | ||
static matchedBy(line) { | ||
return regex_1.regex.dialogue.test(line); | ||
} | ||
} | ||
exports.DialogueBlock = DialogueBlock; | ||
class DialogueBeginToken { | ||
constructor(dual) { | ||
this.type = 'dialogue_begin'; | ||
this.dual = dual; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
} | ||
exports.DialogueBeginToken = DialogueBeginToken; | ||
class CharacterToken { | ||
constructor(text) { | ||
this.type = 'character'; | ||
this.text = text; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
} | ||
exports.CharacterToken = CharacterToken; | ||
class DialogueToken { | ||
constructor(text) { | ||
this.type = 'dialogue'; | ||
this.text = text; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
} | ||
exports.DialogueToken = DialogueToken; | ||
class DialogueEndToken { | ||
constructor() { | ||
this.type = 'dialogue_end'; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
} | ||
exports.DialogueEndToken = DialogueEndToken; | ||
class ParentheticalToken { | ||
constructor(text) { | ||
this.type = 'parenthetical'; | ||
this.text = text; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
} | ||
exports.ParentheticalToken = ParentheticalToken; | ||
class DualDialogueBeginToken { | ||
constructor() { | ||
this.type = 'dual_dialogue_begin'; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
} | ||
exports.DualDialogueBeginToken = DualDialogueBeginToken; | ||
class DualDialogueEndToken { | ||
constructor() { | ||
this.type = 'dual_dialogue_end'; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
} | ||
exports.DualDialogueEndToken = DualDialogueEndToken; | ||
class LyricsToken { | ||
constructor(line) { | ||
this.type = 'lyrics'; | ||
this.text = line.replace(/^~(?![ ])/gm, ''); | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
static matchedBy(line) { | ||
return regex_1.regex.lyrics.test(line); | ||
} | ||
} | ||
exports.LyricsToken = LyricsToken; | ||
class SectionToken { | ||
constructor(line) { | ||
this.type = 'section'; | ||
const match = line.match(regex_1.regex.section); | ||
this.text = match[2]; | ||
this.depth = match[1].length; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
static matchedBy(line) { | ||
return regex_1.regex.section.test(line); | ||
} | ||
} | ||
exports.SectionToken = SectionToken; | ||
class SynopsisToken { | ||
constructor(line) { | ||
this.type = 'synopsis'; | ||
const match = line.match(regex_1.regex.synopsis); | ||
this.text = match[1]; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
static matchedBy(line) { | ||
return regex_1.regex.synopsis.test(line); | ||
} | ||
} | ||
exports.SynopsisToken = SynopsisToken; | ||
class NoteToken { | ||
constructor(line) { | ||
this.type = 'note'; | ||
const match = line.match(regex_1.regex.note); | ||
this.text = match[1]; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
static matchedBy(line) { | ||
return regex_1.regex.note.test(line); | ||
} | ||
} | ||
exports.NoteToken = NoteToken; | ||
class BoneyardToken { | ||
constructor(line) { | ||
const match = line.match(regex_1.regex.boneyard); | ||
this.type = match[0][0] === '/' ? 'boneyard_begin' : 'boneyard_end'; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
static matchedBy(line) { | ||
return regex_1.regex.boneyard.test(line); | ||
} | ||
} | ||
exports.BoneyardToken = BoneyardToken; | ||
class PageBreakToken { | ||
constructor() { | ||
this.type = 'page_break'; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
static matchedBy(line) { | ||
return regex_1.regex.page_break.test(line); | ||
} | ||
} | ||
exports.PageBreakToken = PageBreakToken; | ||
class LineBreakToken { | ||
constructor() { | ||
this.type = 'line_break'; | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
static matchedBy(line) { | ||
return regex_1.regex.line_break.test(line); | ||
} | ||
} | ||
exports.LineBreakToken = LineBreakToken; | ||
class ActionToken { | ||
constructor(line) { | ||
this.type = 'action'; | ||
this.text = line.replace(/^!(?![ ])/gm, ''); | ||
} | ||
addTo(tokens) { | ||
return [...tokens, this]; | ||
} | ||
} | ||
exports.ActionToken = ActionToken; | ||
function isTooShort(str) { | ||
return str.indexOf(' ') === str.length - 2; | ||
} | ||
//# sourceMappingURL=token.js.map |
{ | ||
"name": "fountain-js", | ||
"version": "1.0.0", | ||
"version": "1.1.0", | ||
"description": "A simple parser for Fountain, a markup language for formatting screenplays.", | ||
@@ -36,3 +36,4 @@ "main": "dist/index.js", | ||
"Jonny Greenwald", | ||
"Nathan Hoad" | ||
"Nathan Hoad", | ||
"Chandler Anderson" | ||
], | ||
@@ -39,0 +40,0 @@ "license": "MIT", |
@@ -53,3 +53,3 @@ # Fountain-js | ||
assert.equal(actual, expected); | ||
assert.strictEqual(actual, expected); | ||
``` | ||
@@ -56,0 +56,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
89524
1401
1