screenplay-js
Version:
A modern Typescript, Foutain screenplay parser. Convert Final Draft (.fdx) files to Fountain, and then parse Fountain markdown to HTML.
342 lines (341 loc) • 13.6 kB
JavaScript
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FountainParser = void 0;
var tokenizer_1 = require("./tokenizer");
var sections_1 = require("./sections");
var uuid_1 = require("uuid");
/**
* It's strongly recommended to use Normal because it comes closest to the 1-minute-per-page rule
* relied upon by readers, development people, and (in particular) production staff.
*
* Reference: https://kb.finaldraft.com/s/article/How-many-lines-per-page-does-Final-Draft-write-and-what-are-my-line-spacing-options
*/
var LINES_PER_PAGE = {
none: null,
loose: 47,
normal: 54,
tight: 58,
very_tight: 64,
};
var defaultOptions = {
paginate: true,
lines_per_page: "loose",
script_html: false,
script_html_array: false,
notes: true,
draft_date: true,
boneyard: true,
tokens: false
};
var Parser = /** @class */ (function () {
function Parser(options) {
if (options === void 0) { options = defaultOptions; }
this.options = options;
}
/**
* Parse a screenplay written in .fountain and output meta results
* for the screenplay.
*
* @param { String } script - Text of the screenplay
* @param { Object } _options - Options to pass into the parse function
*/
Parser.prototype.parse = function (script, _options) {
if (_options === void 0) { _options = this.options; }
if (_options === undefined || _options === null) {
_options = this.options;
}
// Default options
var options = __assign(__assign({}, this.options), _options);
var tokens = tokenizer_1.tokenizer.tokenize(script), title_page_html = [], script_html = [];
var output = {
title: '',
credit: '',
authors: [],
source: '',
date: '',
contact: '',
copyright: '',
scenes: [],
title_page_html: '',
script_pages: [],
script_pages_html: [],
};
var dialogueCounter = 0;
for (var _i = 0, tokens_1 = tokens; _i < tokens_1.length; _i++) {
var token = tokens_1[_i];
token.text = this.lexer(token.text);
switch (token.type) {
case 'title':
title_page_html.push('<h1>' + token.text + '</h1>');
output.title = token.text.replace('<br />', ' ').replace(/<(?:.|\n)*?>/g, '');
break;
case 'credit':
title_page_html.push('<p class="credit">' + token.text + '</p>');
output.credit = token.text;
break;
case 'author':
title_page_html.push('<p class="authors">' + token.text + '</p>');
if (output.authors) {
output.authors.push(token.text);
}
break;
case 'authors':
title_page_html.push('<p class="authors">' + token.text + '</p>');
if (output.authors) {
output.authors = output.authors.concat(token.text.replace('<br />', "\n").replace(', ', ',').split(/[\n,]/));
}
break;
case 'source':
title_page_html.push('<p class="source">' + token.text + '</p>');
output.source = (token.text);
break;
case 'notes':
if (options.notes) {
title_page_html.push('<p class="notes">' + token.text + '</p>');
output.notes = token.text;
}
break;
case 'draft_date':
if (options.draft_date) {
title_page_html.push('<p class="draft-date">' + token.text + '</p>');
output.draft_date = token.text;
}
break;
case 'date':
title_page_html.push('<p class="date">' + token.text + '</p>');
output.date = token.text;
break;
case 'contact':
title_page_html.push('<p class="contact">' + token.text + '</p>');
output.contact = token.text;
break;
case 'copyright':
title_page_html.push('<p class="copyright">' + token.text + '</p>');
output.copyright = token.text;
break;
case 'scene_heading':
script_html.push("<h6 " + (token.scene_number || token.scene_number === 0 ? 'id="scene-heading--' + token.scene_number + '"' : null) + " class=\"scene-heading\" data-scene-heading-index=\"" + token.scene_number + "\">" + token.text + "</h6>");
if (output.scenes) {
output.scenes.push(token.text);
}
break;
case 'transition':
script_html.push('<p class="transition">' + token.text + '</p>');
break;
case 'dual_dialogue_begin':
script_html.push("<div class=\"dual-dialogue\" data-dialogue-index=\"" + dialogueCounter + "\">");
dialogueCounter += 1;
break;
case 'dialogue_begin':
script_html.push("<div class=\"dialogue" + (token.dual ? ' ' + token.dual : '') + "\" data-dialogue-index=\"" + dialogueCounter + "\">");
dialogueCounter += 1;
break;
case 'character':
script_html.push('<p class="character">' + token.text.replace(/^@/, '') + '</p>');
break;
case 'parenthetical':
script_html.push('<p class="parenthetical">' + token.text + '</p>');
break;
case 'dialogue':
script_html.push('<p>' + token.text + '</p>');
break;
case 'dialogue_end':
script_html.push('</div>');
break;
case 'dual_dialogue_end':
script_html.push('</div>');
break;
case 'section':
script_html.push('<p class="section" data-depth="' + token.depth + '">' + token.text + '</p>');
break;
case 'synopsis':
script_html.push('<p class="synopsis">' + token.text + '</p>');
break;
case 'note':
if (options.notes) {
script_html.push('<!-- ' + token.text + ' -->');
}
break;
case 'boneyard_begin':
if (options.boneyard) {
script_html.push('<!-- ');
}
break;
case 'boneyard_end':
if (options.boneyard) {
script_html.push(' -->');
}
break;
case 'lyrics':
script_html.push('<p class="lyrics">' + token.text + '</p>');
break;
case 'action':
script_html.push('<p class="action">' + token.text + '</p>');
break;
case 'centered':
script_html.push('<p class="centered">' + token.text + '</p>');
break;
case 'page_break':
script_html.push('<hr class="page-break" />');
break;
case 'line_break':
script_html.push('<br />');
break;
}
}
output.title_page_html = title_page_html.join('');
if (options.script_html) {
output.script_html = script_html.join('');
}
if (options.script_html_array) {
output.script_html_array = script_html;
}
if (options.paginate) {
var _a = this.paginate(script_html, options.lines_per_page), pages = _a.pages, pages_html = _a.pages_html;
output.script_pages = pages;
output.script_pages_html = pages_html;
}
if (options.tokens) {
output.tokens = tokens;
}
;
return output;
};
/**
* Loosely paginate parsed screenplays. New pages will occur when a page
* break is identified in the script.
*/
Parser.prototype.paginate = function (script_html, lpp) {
if (lpp === void 0) { lpp = this.options.lines_per_page; }
var lines_per_page = LINES_PER_PAGE[lpp];
var pages = [];
var pages_html = [];
var page = 0;
var isOpenDiv = false;
var addNewPage = false;
var addDialogueChunk = false;
var dialogueChunk = '';
for (var i = 0; i < script_html.length; i++) {
var line = script_html[i];
/**
* If page break, increment page count, and
* push onto a new page.
*/
if (line.includes('page-break')) {
page += 1;
// Don't add the page-break html style
continue;
}
/**
* Add pagination based on the lines per page count
*/
if (isOpenDiv === false && addNewPage) {
page += 1;
addNewPage = false;
}
if (line.includes('<div ') && line !== '</div>') {
isOpenDiv = true;
// Begin dialogue chunk
if (line.includes('class="dialogue') || addDialogueChunk) {
addDialogueChunk = true;
}
}
else if (line === '</div>') {
isOpenDiv = false;
// Close dialogue chunk
if (addDialogueChunk) {
addDialogueChunk = false;
dialogueChunk += line;
}
}
/**
* Toggle new page flag
*/
if (i !== 0 && i % lines_per_page === 0) {
addNewPage = true;
}
/**
* Create a default empty array for page
*/
if (!pages[page]) {
pages[page] = [];
}
// Add html to current page index
pages[page].push(line);
/**
* Create a default empty array for pages_html
*/
if (!pages_html[page]) {
pages_html[page] = [];
}
// Add dialogue chunked html to current page index
if (dialogueChunk && !addDialogueChunk) {
pages_html[page].push(dialogueChunk);
dialogueChunk = '';
}
else if (addDialogueChunk) {
dialogueChunk += line;
}
else {
pages_html[page].push(line);
}
}
/**
* For each page, join the html into a string
*/
for (var i = 0; i < pages.length; i++) {
pages[i] = { _id: (0, uuid_1.v4)(), html: pages[i].join("") };
}
return { pages: pages, pages_html: pages_html };
};
/**
* Match and replace string elements with appropriate
* inline CSS styles.
*
* @param { String } s
*/
Parser.prototype.lexer = function (s) {
if (!s) {
return;
}
var inline = {
note: '<!-- $1 -->',
line_break: '<br />',
bold_italic_underline: '<strong><em><span style="text-decoration:underline !important">$2</span></em></strong>',
bold_underline: '<strong><span style="text-decoration:underline !important">$2</span></strong>',
italic_underline: '<em><span style="text-decoration:underline !important">$1</span></em>',
bold_italic: '<strong><em>$2</em></strong>',
bold: '<strong>$2</strong>',
italic: '<em>$2</em>',
underline: '<span style="text-decoration:underline !important">$2</span>'
};
var styles = ['bold_italic_underline', 'bold_underline', 'italic_underline', 'bold_italic', 'bold', 'italic', 'underline'], style, match;
s = s.replace(sections_1.sections.note_inline, inline.note)
.replace(/\\\*/g, '[star]')
.replace(/\\_/g, '[underline]')
.replace(/\n/g, inline.line_break);
for (var i in styles) {
style = styles[i];
match = sections_1.sections[style];
if (match.test(s)) {
s = s.replace(match, inline[style]);
}
}
return s.replace(/\[star\]/g, '*').replace(/\[underline\]/g, '_').trim();
};
return Parser;
}());
;
var FountainParser = new Parser();
exports.FountainParser = FountainParser;