screenplay-js
Version:
A modern Typescript, Foutain screenplay parser. Convert Final Draft (.fdx) files to Fountain, and then parse Fountain markdown to HTML.
167 lines (166 loc) • 7.64 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.tokenizer = void 0;
var sections_1 = require("./sections");
var ScriptTokenizer = /** @class */ (function () {
function ScriptTokenizer() {
}
/**
* Cleans the script text to allow the parser to break
* the script into correct tokens, pages, etc.
*
* @param { String } script
* @returns { String }
*/
ScriptTokenizer.prototype.clean = function (script) {
var s = script.replace(sections_1.sections.boneyard, '\n$1\n')
.replace(sections_1.sections.standardizer, '\n')
.replace(sections_1.sections.cleaner, '')
.replace(sections_1.sections.whitespacer, '')
// PR: Treat whitespace-only lines as empty
// https://github.com/nathanhoad/fountain-js/pull/2
.replace(/^\s+$/gm, '');
return s;
};
/**
* Return an array of token objects breaking the screenplay into type, text, etc. tokens.
*
* @param { String } script - Text of the screenplay
* @returns { Array<any> }
*/
ScriptTokenizer.prototype.tokenize = function (script) {
var script_lines = this.clean(script).split(sections_1.sections.splitter), match, parts, text, meta, tokens = [], scene_number = 0;
for (var i = 0; i < script_lines.length; i++) {
var line = script_lines[i];
// title page
if (sections_1.sections.title_page.test(line)) {
match = line.replace(sections_1.sections.title_page, '\n$1').split(sections_1.sections.splitter);
for (var x = 0, length_1 = match.length; x < length_1; x++) {
parts = match[x].replace(sections_1.sections.cleaner, '').split(/\:\n*/);
// Handle titles with colons in them
if (parts.length > 2) {
var colonTitle = parts.slice(1).map(function (p) { return p.trim(); }).join(': ');
tokens.push({ type: parts[0].trim().toLowerCase().replace(' ', '_'), text: colonTitle.trim() });
}
else {
tokens.push({ type: parts[0].trim().toLowerCase().replace(' ', '_'), text: parts[1].trim() });
}
}
continue;
}
/**
* Scene headings
*
* @todo:
* - Figure out what the match fields are returning,
* and how to update the parsed meta results
*/
if (match = line.match(sections_1.sections.scene_heading)) {
text = match[1] || match[2];
if (text.indexOf(' ') !== text.length - 2) {
if (meta = text.match(sections_1.sections.scene_number)) {
meta = meta[2];
text = text.replace(sections_1.sections.scene_number, '');
}
if (meta) {
scene_number = meta;
}
tokens.push({ type: 'scene_heading', text: text, scene_number: scene_number });
// increment scene number
scene_number += 1;
}
continue;
}
// centered
if (match = line.match(sections_1.sections.centered)) {
tokens.push({ type: 'centered', text: match[0].replace(/>|</g, '') });
continue;
}
// transitions
if (match = line.match(sections_1.sections.transition)) {
tokens.push({ type: 'transition', text: match[1] || match[2] });
continue;
}
// dialogue blocks - characters, parentheticals and dialogue
if (match = line.match(sections_1.sections.dialogue)) {
if (match[1].indexOf(' ') !== match[1].length - 2) {
// PR: Fixed the bug where parentheticals are after the dialogue
// https://github.com/nathanhoad/fountain-js/pull/7
// parts = match[3].split(/(\(.+\))(?:\n+)/).reverse();
parts = match[3].split(/(\(.+\))(?:\n+)/);
var dual_diaglogue = !!match[2];
if (dual_diaglogue) {
// If dual dialogue, we need to traverse back four indexes
// and insert those into a dual dialogue block
// Get last index of the last inserted dialogue block
var lastDialogueBeginIndex = 0;
for (var idx = tokens.length - 1; idx >= 0; idx--) {
if (tokens[idx].type === 'dialogue_begin') {
lastDialogueBeginIndex = idx;
break;
}
}
var leftDualDialogueBlocks = tokens.splice(lastDialogueBeginIndex);
tokens.push({ type: 'dual_dialogue_begin' });
// Insert previous dialogue block into dual dialogue
tokens = tokens.concat(leftDualDialogueBlocks);
}
tokens.push({ type: 'dialogue_begin' });
tokens.push({ type: 'character', text: match[1].trim() });
for (var x = 0, length_2 = parts.length; x < length_2; x++) {
text = parts[x].trim();
if (text.length > 0) {
tokens.push({ type: sections_1.sections.parenthetical.test(text) ? 'parenthetical' : 'dialogue', text: text });
}
}
tokens.push({ type: 'dialogue_end' });
if (dual_diaglogue) {
tokens.push({ type: 'dual_dialogue_end' });
}
continue;
}
}
// section
if (match = line.match(sections_1.sections.section)) {
tokens.push({ type: 'section', text: match[2], depth: match[1].length });
continue;
}
// synopsis
if (match = line.match(sections_1.sections.synopsis)) {
tokens.push({ type: 'synopsis', text: match[1] });
continue;
}
// notes
if (match = line.match(sections_1.sections.note)) {
tokens.push({ type: 'note', text: match[1] });
continue;
}
// boneyard
if (match = line.match(sections_1.sections.boneyard)) {
tokens.push({ type: match[0][0] === '/' ? 'boneyard_begin' : 'boneyard_end' });
continue;
}
// page breaks
if (sections_1.sections.page_break.test(line)) {
tokens.push({ type: 'page_break' });
continue;
}
// line breaks
if (sections_1.sections.line_break.test(line)) {
tokens.push({ type: 'line_break' });
continue;
}
// lyrics
if (sections_1.sections.lyrics.test(line)) {
tokens.push({ type: 'lyrics', text: line });
continue;
}
tokens.push({ type: 'action', text: line });
}
return tokens;
};
return ScriptTokenizer;
}());
;
var tokenizer = new ScriptTokenizer();
exports.tokenizer = tokenizer;