UNPKG

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
"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;