Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

fountain-js

Package Overview
Dependencies
Maintainers
2
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

fountain-js - npm Package Compare versions

Comparing version 1.2.1 to 1.2.2

14

CHANGELOG.md

@@ -9,8 +9,16 @@ # Change Log

- Add line numbers to tokens.
- Allow any title field attributes.
- Better title page parsing in general.
- Add line numbers and/or paragraph numbers to tokens.
- Boneyard is preserved as a token instead of stripped form input.
- Work on options for perserving vertical space in Action per spec.
## [1.2.2] - 2023-10-28
### Added
- Allow any title field attributes that are found _underneath_ one of the following recommended attributes: `Title`, `Credit`, `Author/s`, `Source`, `Notes`, `Draft date`, `Date`, `Contact`, or `Copyright`.
### Fixed
- Better title page parsing in general.
## [1.2.1] - 2023-10-28

@@ -17,0 +25,0 @@

import { Token } from './token';
export interface Script {
title?: string;
title: string;
html: {

@@ -8,3 +8,3 @@ title_page: string;

};
tokens?: Token[];
tokens: Token[];
}

@@ -17,3 +17,3 @@ export declare class Fountain {

parse(script: string, getTokens?: boolean): Script;
to_html(token: Token): string;
to_html(token: Token): string | undefined;
}

@@ -16,7 +16,6 @@ import { Scanner } from './scanner';

try {
let title;
let title = '';
this.tokens = this.scanner.tokenize(script);
const tokenCopy = JSON.parse(JSON.stringify(this.tokens));
const titleToken = this.tokens.find(token => token.type === 'title');
if (titleToken) {
if (titleToken === null || titleToken === void 0 ? void 0 : titleToken.text) {
// lexes any inlines on the title then removes any HTML / line breaks

@@ -30,6 +29,8 @@ title = unEscapeHTML(this.inlineLex.reconstruct(titleToken.text)

html: {
title_page: tokenCopy.filter(token => token.is_title).map(token => this.to_html(token)).join(''),
script: tokenCopy.filter(token => !token.is_title).map(token => this.to_html(token)).join('')
title_page: this.tokens.filter(token => token.is_title)
.map(token => this.to_html(token)).join(''),
script: this.tokens.filter(token => !token.is_title)
.map(token => this.to_html(token)).join('')
},
tokens: getTokens ? this.tokens : undefined
tokens: getTokens ? this.tokens : []
};

@@ -44,33 +45,36 @@ }

to_html(token) {
token.text = this.inlineLex.reconstruct(token.text);
let lexedText = '';
if (token === null || token === void 0 ? void 0 : token.text) {
lexedText = this.inlineLex.reconstruct(token.text, token.type === 'action');
}
switch (token.type) {
case 'title': return '<h1>' + token.text + '</h1>';
case 'credit': return '<p class="credit">' + token.text + '</p>';
case 'author': return '<p class="authors">' + token.text + '</p>';
case 'authors': return '<p class="authors">' + token.text + '</p>';
case 'source': return '<p class="source">' + token.text + '</p>';
case 'notes': return '<p class="notes">' + token.text + '</p>';
case 'draft_date': return '<p class="draft-date">' + token.text + '</p>';
case 'date': return '<p class="date">' + token.text + '</p>';
case 'contact': return '<p class="contact">' + token.text + '</p>';
case 'copyright': return '<p class="copyright">' + token.text + '</p>';
case 'scene_heading': return '<h3' + (token.scene_number ? ' id="' + token.scene_number + '">' : '>') + token.text + '</h3>';
case 'transition': return '<h2>' + token.text + '</h2>';
case 'dual_dialogue_begin': return '<div class="dual-dialogue">';
case 'dialogue_begin': return '<div class="dialogue' + (token.dual ? ' ' + token.dual : '') + '">';
case 'character': return '<h4>' + token.text + '</h4>';
case 'parenthetical': return '<p class="parenthetical">' + token.text + '</p>';
case 'dialogue': return '<p>' + token.text + '</p>';
case 'dialogue_end': return '</div>';
case 'dual_dialogue_end': return '</div>';
case 'section': return '<p class="section" data-depth="' + token.depth + '">' + token.text + '</p>';
case 'synopsis': return '<p class="synopsis">' + token.text + '</p>';
case 'note': return '<!-- ' + token.text + ' -->';
case 'boneyard_begin': return '<!-- ';
case 'boneyard_end': return ' -->';
case 'action': return '<p>' + token.text + '</p>';
case 'centered': return '<p class="centered">' + token.text + '</p>';
case 'lyrics': return '<p class="lyrics">' + token.text + '</p>';
case 'page_break': return '<hr />';
case 'line_break': return '<br />';
case 'title': return `<h1>${lexedText}</h1>`;
case 'credit': return `<p class="credit">${lexedText}</p>`;
case 'author': return `<p class="authors">${lexedText}</p>`;
case 'authors': return `<p class="authors">${lexedText}</p>`;
case 'source': return `<p class="source">${lexedText}</p>`;
case 'notes': return `<p class="notes">${lexedText}</p>`;
case 'draft_date': return `<p class="draft-date">${lexedText}</p>`;
case 'date': return `<p class="date">${lexedText}</p>`;
case 'contact': return `<p class="contact">${lexedText}</p>`;
case 'copyright': return `<p class="copyright">${lexedText}</p>`;
case 'scene_heading': return `<h3${(token.scene_number ? ` id="${token.scene_number}">` : `>`) + lexedText}</h3>`;
case 'transition': return `<h2>${lexedText}</h2>`;
case 'dual_dialogue_begin': return `<div class="dual-dialogue">`;
case 'dialogue_begin': return `<div class="dialogue${token.dual ? ' ' + token.dual : ''}">`;
case 'character': return `<h4>${lexedText}</h4>`;
case 'parenthetical': return `<p class="parenthetical">${lexedText}</p>`;
case 'dialogue': return `<p>${lexedText}</p>`;
case 'dialogue_end': return `</div>`;
case 'dual_dialogue_end': return `</div>`;
case 'section': return;
case 'synopsis': return;
case 'note': return `<!-- ${lexedText} -->`;
case 'boneyard_begin': return `<!-- `;
case 'boneyard_end': return ` -->`;
case 'action': return `<p>${lexedText}</p>`;
case 'centered': return `<p class="centered">${lexedText}</p>`;
case 'lyrics': return `<p class="lyrics">${lexedText}</p>`;
case 'page_break': return `<hr />`;
case 'spaces': return;
}

@@ -77,0 +81,0 @@ }

export * from './fountain';
export { Token } from './token';
export { FountainTypes, rules } from './rules';
export { Lexer, InlineTypes, InlineLexer } from './lexer';
export { InlineTypes, InlineLexer } from './lexer';
export * from './fountain';
export { rules } from './rules';
export { Lexer, InlineLexer } from './lexer';
export { InlineLexer } from './lexer';
//# sourceMappingURL=index.js.map
export declare type InlineTypes = 'note' | 'line_break' | 'bold_italic_underline' | 'bold_underline' | 'italic_underline' | 'bold_italic' | 'bold' | 'italic' | 'underline' | 'escape';
export declare class Lexer {
reconstruct(script: string): string;
}
export declare class InlineLexer extends Lexer {
export declare class InlineLexer {
inline: Record<InlineTypes, string>;
reconstruct(line: string): string;
reconstruct(line: string, escapeSpaces?: boolean): string;
}
import { rules } from './rules';
import { escapeHTML } from './utilities';
export class Lexer {
reconstruct(script) {
return script.replace(rules.boneyard, '\n$1\n')
.replace(rules.standardizer, '\n')
.replace(rules.cleaner, '')
.replace(rules.whitespacer, '');
}
}
export class InlineLexer extends Lexer {
export class InlineLexer {
constructor() {
super(...arguments);
this.inline = {

@@ -27,23 +18,32 @@ note: '<!-- $1 -->',

}
reconstruct(line) {
if (!line)
return;
let match;
const styles = ['bold_italic_underline', 'bold_underline', 'italic_underline', 'bold_italic', 'bold', 'italic', 'underline'];
line = escapeHTML(line
.replace(rules.note_inline, this.inline.note)
.replace(rules.escape, '[{{{$&}}}]') // perserve escaped characters
reconstruct(line, escapeSpaces = false) {
const styles = [
'bold_italic_underline',
'bold_underline',
'italic_underline',
'bold_italic',
'bold',
'italic',
'underline'
];
line = escapeHTML(line.replace(rules.escape, '[{{{$&}}}]') // perserve escaped characters
);
if (escapeSpaces) {
line = line.replace(/^( +)/gm, (_, spaces) => {
return '&nbsp;'.repeat(spaces.length);
});
}
for (let style of styles) {
match = rules[style];
if (match.test(line)) {
line = line.replace(match, this.inline[style]);
const rule = rules[style];
if (rule.test(line)) {
line = line.replace(rule, this.inline[style]);
}
}
return line
.replace(rules.note_inline, this.inline.note)
.replace(/\n/g, this.inline.line_break)
.replace(/\[{{{\\(&.+?;|.)}}}]/g, this.inline.escape) // restore escaped chars to intended sequence
.trim();
.trimEnd();
}
}
//# sourceMappingURL=lexer.js.map

@@ -1,2 +0,2 @@

export declare type FountainTypes = 'title_page' | 'scene_heading' | 'scene_number' | 'transition' | 'dialogue' | 'parenthetical' | 'action' | 'centered' | 'lyrics' | 'synopsis' | 'section' | 'note' | 'note_inline' | 'boneyard' | 'page_break' | 'line_break' | 'bold_italic_underline' | 'bold_underline' | 'italic_underline' | 'bold_italic' | 'bold' | 'italic' | 'underline' | 'escape' | 'splitter' | 'cleaner' | 'standardizer' | 'whitespacer';
export declare type FountainTypes = 'title_page' | 'scene_heading' | 'scene_number' | 'transition' | 'dialogue' | 'parenthetical' | 'action' | 'centered' | 'lyrics' | 'synopsis' | 'section' | 'note' | 'note_inline' | 'boneyard' | 'page_break' | 'line_break' | 'bold_italic_underline' | 'bold_underline' | 'italic_underline' | 'bold_italic' | 'bold' | 'italic' | 'underline' | 'escape' | 'blank_line' | 'end_of_lines';
export declare const rules: Record<FountainTypes, RegExp>;
export const rules = {
title_page: /^((?:title|credit|authors?|source|notes|draft date|date|contact|copyright)\:)/gim,
scene_heading: /^((?:\*{0,3}_?)?(?:(?:int|i)\.?\/(?:ext|e)|int|ext|est)[. ].+)|^\.(?!\.+)(\S.*)/i,
title_page: /^\s*((?:title|credit|authors?|source|notes|draft date|date|contact|copyright)\:)/gim,
scene_heading: /^\s*((?:\*{0,3}_?)?(?:(?:int|i)\.?\/(?:ext|e)|int|ext|est)[. ].+)|^\s*\.(?!\.+)(\S.*)/i,
scene_number: /( *#(.+)# *)/,
transition: /^((?:FADE (?:TO BLACK|OUT)|CUT TO BLACK)\.|.+ TO\:)|^> *(.+)/,
dialogue: /(?!^[0-9 _*]+(?:\(.*\))?[ *_]*(?:\^?)?\s*\n)(^(?:(?!\\?@|!)[^^()\na-z]+|@[^^()\n]+)(?: *\(.*\))?[ *_]*)(\^?)?\s*\n(?!\n+)([\s\S]+)/,
transition: /^\s*((?:FADE (?:TO BLACK|OUT)|CUT TO BLACK)\.|.+ TO\:)\s*$|^\s*> *(.+)$/,
dialogue: /(?!^\s*\\@|^\s*!)(?!^\s*[0-9 _*]+(?:\(.*\))?[*_]*(?:\^?)?\s*\n)(^\s*(?:@[^^()\n]+|[^^()\na-z]+)(?: *\(.*\))?[ *_]*)(\^?)?\s*\n(?!\n+)([\s\S]+)/,
parenthetical: /^ *(?:(?<u1>_{0,1})(?<s1>\*{0,3})(?=.+\k<s1>\k<u1>)|(?<s2>\*{0,3})(?<u2>_{0,1})(?=.+\k<u2>\k<s2>))(\(.+?\))(\k<s1>\k<u1>|\k<u2>\k<s2>) *$/,
action: /^(.+)/g,
centered: /^> *(.+) *<(\n.+)*/g,
lyrics: /^~(?! ).+(?:\n~(?! ).+)*/,
section: /^(#+) *(.*)/,
synopsis: /^(?:\=(?!\=+) *)(.*)/,
centered: /^\s*>.+<[^\S\r\n]*(?:\s*>.+<[^\S\r\n]*)*/g,
lyrics: /^\s*~(?! ).+(?:\n\s*~(?! ).+)*/,
section: /^\s*(#+) *(.*)/,
synopsis: /^\s*=(?!=+) *(.*)/,
note: /^\[{2}(?!\[+)(.+)]{2}(?!\[+)$/,
note_inline: /\[{2}(?!\[+)([\s\S]+?)]{2}(?!\[+)/g,
boneyard: /(^\/\*|^\*\/)$/g,
boneyard: /\/\*[\S\s]*?\*\//g,
page_break: /^={3,}$/,

@@ -26,7 +26,5 @@ line_break: /^ {2}$/,

escape: /\\([@#!*_$~`+=.><\\\/])/g,
splitter: /\n{2,}/g,
cleaner: /^\n+|\n+$/,
standardizer: /\r\n|\r/g,
whitespacer: /^\t+|^ {3,}/gm
blank_line: /^(?: *(?:\n|$))+/,
end_of_lines: /(?:\n|$){2,}/g
};
//# sourceMappingURL=rules.js.map
import { Token } from './token';
export declare class Scanner {
private lastLineWasDualDialogue;
boneyardStripper(match: string): string;
tokenize(script: string): Token[];
}
import { rules } from './rules';
import { ActionToken, BoneyardToken, CenteredToken, DialogueBlock, LineBreakToken, LyricsToken, NoteToken, PageBreakToken, SceneHeadingToken, SectionToken, SynopsisToken, TitlePageBlock, TransitionToken } from './token';
import { Lexer } from './lexer';
import { ActionToken, CenteredToken, DialogueBlock, LyricsToken, NoteToken, PageBreakToken, SceneHeadingToken, SectionToken, SpacesToken, SynopsisToken, TitlePageBlock, TransitionToken } from './token';
export class Scanner {
boneyardStripper(match) {
const endAtStrStart = /^[^\S\n]*\*\//m;
let boneyardEnd = '';
if (endAtStrStart.test(match)) {
boneyardEnd = '\n\n';
}
return boneyardEnd;
}
tokenize(script) {
// reverse the array so that dual dialog can be constructed bottom up
const source = new Lexer().reconstruct(script).split(rules.splitter).reverse();
const source = script
.replace(rules.boneyard, this.boneyardStripper)
.replace(/\r\n|\r/g, '\n') // convert carriage return / returns to newline
.split(rules.end_of_lines)
.reverse();
const tokens = source.reduce((previous, line) => {
/** spaces */
if (SpacesToken.matchedBy(line)) {
return new SpacesToken().addTo(previous);
}
/** title page */

@@ -43,6 +58,2 @@ if (TitlePageBlock.matchedBy(line)) {

}
/** boneyard */
if (BoneyardToken.matchedBy(line)) {
return new BoneyardToken(line).addTo(previous);
}
/** lyrics */

@@ -56,7 +67,3 @@ if (LyricsToken.matchedBy(line)) {

}
/** line breaks */
if (LineBreakToken.matchedBy(line)) {
return new LineBreakToken().addTo(previous);
}
// everything else is action -- remove `!` for forced action
/** action */
return new ActionToken(line).addTo(previous);

@@ -63,0 +70,0 @@ }, []);

@@ -15,3 +15,3 @@ export interface Token {

export declare class TitlePageBlock implements Block {
readonly tokens: TitlePageToken[];
readonly tokens: Token[];
constructor(line: string);

@@ -123,9 +123,2 @@ addTo(tokens: Token[]): Token[];

}
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 {

@@ -136,4 +129,4 @@ readonly type = "page_break";

}
export declare class LineBreakToken implements Token {
readonly type = "line_break";
export declare class SpacesToken implements Token {
readonly type = "spaces";
addTo(tokens: Token[]): Token[];

@@ -140,0 +133,0 @@ static matchedBy(line: string): boolean;

@@ -5,3 +5,6 @@ import { rules } from './rules';

this.tokens = [];
const match = line.replace(rules.title_page, '\n$1').split(rules.splitter).reverse();
const match = line
.replace(/^([^\n]+:)/gm, '\n$1')
.split(rules.end_of_lines)
.reverse();
this.tokens = match.reduce((previous, item) => new TitlePageToken(item).addTo(previous), []);

@@ -19,5 +22,5 @@ }

this.is_title = true;
const pair = item.replace(rules.cleaner, '').split(/\:\n*/);
this.type = pair[0].trim().toLowerCase().replace(' ', '_');
this.text = pair[1].trim();
const [key, value] = item.split(/\:\n*/, 2);
this.type = key.trim().toLowerCase().replace(/ /g, '_');
this.text = value.replace(/^\s*/gm, '');
}

@@ -32,3 +35,5 @@ addTo(tokens) {

const match = line.match(rules.scene_heading);
this.text = match[1] || match[2];
if (match) {
this.text = match[1] || match[2];
}
const meta = this.text.match(rules.scene_number);

@@ -51,3 +56,5 @@ if (meta) {

const match = line.match(rules.centered);
this.text = match[0].replace(/ *[><] */g, '');
if (match) {
this.text = match[0].replace(/[^\S\n]*[><][^\S\n]*/g, '');
}
}

@@ -65,3 +72,5 @@ addTo(tokens) {

const match = line.match(rules.transition);
this.text = match[1] || match[2];
if (match) {
this.text = match[1] || match[2];
}
}

@@ -79,45 +88,49 @@ addTo(tokens) {

const match = line.match(rules.dialogue);
let name = match[1];
// iterating from the bottom up, so push dialogue blocks in reverse order
const isDualDialogue = !!match[2];
if (isDualDialogue) {
this.tokens.push(new DualDialogueEndToken());
}
this.tokens.push(new DialogueEndToken());
const parts = match[3].split(/\n/);
let dialogue = parts.reduce((p, text = '') => {
const lastIndex = p.length - 1;
const previousToken = p[lastIndex];
if (!text.length) {
return p;
if (match) {
let name = match[1].trim();
// iterating from the bottom up, so push dialogue blocks in reverse order
const isDualDialogue = !!match[2];
if (isDualDialogue) {
this.tokens.push(new DualDialogueEndToken());
}
if (rules.parenthetical.test(text)) {
return [...p, new ParentheticalToken(text)];
}
if (rules.lyrics.test(text)) {
if (previousToken.type === 'lyrics') {
p[lastIndex].text =
`${previousToken.text}\n${text.replace(/^~/, '')}`;
this.tokens.push(new DialogueEndToken());
const parts = match[3].split(/\n/);
let dialogue = parts.reduce((p, text = '') => {
const lastIndex = p.length - 1;
const previousToken = p[lastIndex];
if (!text.length) {
return p;
}
else {
return [...p, new LyricsToken(text)];
if (rules.line_break.test(text)) {
text = '';
}
}
if (previousToken) {
if (previousToken.type === 'dialogue') {
text = text.trim();
if (rules.parenthetical.test(text)) {
return [...p, new ParentheticalToken(text)];
}
if (rules.lyrics.test(text)) {
if (previousToken.type === 'lyrics') {
p[lastIndex].text =
`${previousToken.text}\n${text.replace(/^~/, '')}`;
return p;
}
else {
return [...p, new LyricsToken(text)];
}
}
if ((previousToken === null || previousToken === void 0 ? void 0 : previousToken.type) === 'dialogue') {
p[lastIndex].text = `${previousToken.text}\n${text}`;
return p;
}
return [...p, new DialogueToken(text)];
}, []).reverse();
this.tokens.push(...dialogue);
this.tokens.push(new CharacterToken(name.startsWith('@')
? name.replace(/^@/, '').trim()
: name.trim()), new DialogueBeginToken(isDualDialogue ? 'right' : dual ? 'left' : undefined));
if (dual) {
this.tokens.push(new DualDialogueBeginToken());
}
return [...p, new DialogueToken(text)];
}, []).reverse();
this.tokens.push(...dialogue);
this.tokens.push(new CharacterToken(name.startsWith('@')
? name.replace(/^@/, '').trim()
: name.trim()), new DialogueBeginToken(isDualDialogue ? 'right' : dual ? 'left' : undefined));
if (dual) {
this.tokens.push(new DualDialogueBeginToken());
this.dual = isDualDialogue;
}
this.dual = isDualDialogue;
}

@@ -194,3 +207,3 @@ addTo(tokens) {

this.type = 'lyrics';
this.text = line.replace(/^~(?! )/gm, '');
this.text = line.replace(/^\s*~(?! )/gm, '');
}

@@ -208,4 +221,6 @@ addTo(tokens) {

const match = line.match(rules.section);
this.text = match[2];
this.depth = match[1].length;
if (match) {
this.text = match[2];
this.depth = match[1].length;
}
}

@@ -223,3 +238,5 @@ addTo(tokens) {

const match = line.match(rules.synopsis);
this.text = match[1];
if (match) {
this.text = match[1];
}
}

@@ -237,3 +254,5 @@ addTo(tokens) {

const match = line.match(rules.note);
this.text = match[1];
if (match) {
this.text = match[1];
}
}

@@ -247,14 +266,2 @@ addTo(tokens) {

}
export class BoneyardToken {
constructor(line) {
const match = line.match(rules.boneyard);
this.type = match[0][0] === '/' ? 'boneyard_begin' : 'boneyard_end';
}
addTo(tokens) {
return [...tokens, this];
}
static matchedBy(line) {
return rules.boneyard.test(line);
}
}
export class PageBreakToken {

@@ -271,5 +278,5 @@ constructor() {

}
export class LineBreakToken {
export class SpacesToken {
constructor() {
this.type = 'line_break';
this.type = 'spaces';
}

@@ -280,3 +287,3 @@ addTo(tokens) {

static matchedBy(line) {
return rules.line_break.test(line);
return rules.blank_line.test(line);
}

@@ -287,3 +294,6 @@ }

this.type = 'action';
this.text = line.replace(/^!(?! )/gm, '');
this.text = line.replace(/^(\s*)!(?! )/gm, '$1')
.replace(/^( *)(\t+)/gm, (_, leading, tabs) => {
return leading + ' '.repeat(tabs.length);
});
}

@@ -290,0 +300,0 @@ addTo(tokens) {

@@ -46,5 +46,5 @@ "use strict";

to_html(token) {
let lexedText;
let lexedText = '';
if (token === null || token === void 0 ? void 0 : token.text) {
lexedText = this.inlineLex.reconstruct(token.text);
lexedText = this.inlineLex.reconstruct(token.text, token.type === 'action');
}

@@ -80,3 +80,3 @@ switch (token.type) {

case 'page_break': return `<hr />`;
case 'line_break': return `<br />`;
case 'spaces': return;
}

@@ -83,0 +83,0 @@ }

export declare type InlineTypes = 'note' | 'line_break' | 'bold_italic_underline' | 'bold_underline' | 'italic_underline' | 'bold_italic' | 'bold' | 'italic' | 'underline' | 'escape';
export declare class InlineLexer {
inline: Record<InlineTypes, string>;
reconstruct(line: string): string;
reconstruct(line: string, escapeSpaces?: boolean): string;
}

@@ -21,8 +21,19 @@ "use strict";

}
reconstruct(line) {
const styles = ['bold_italic_underline', 'bold_underline', 'italic_underline', 'bold_italic', 'bold', 'italic', 'underline'];
line = utilities_1.escapeHTML(line
.replace(rules_1.rules.note_inline, this.inline.note)
.replace(rules_1.rules.escape, '[{{{$&}}}]') // perserve escaped characters
reconstruct(line, escapeSpaces = false) {
const styles = [
'bold_italic_underline',
'bold_underline',
'italic_underline',
'bold_italic',
'bold',
'italic',
'underline'
];
line = utilities_1.escapeHTML(line.replace(rules_1.rules.escape, '[{{{$&}}}]') // perserve escaped characters
);
if (escapeSpaces) {
line = line.replace(/^( +)/gm, (_, spaces) => {
return '&nbsp;'.repeat(spaces.length);
});
}
for (let style of styles) {

@@ -35,5 +46,6 @@ const rule = rules_1.rules[style];

return line
.replace(rules_1.rules.note_inline, this.inline.note)
.replace(/\n/g, this.inline.line_break)
.replace(/\[{{{\\(&.+?;|.)}}}]/g, this.inline.escape) // restore escaped chars to intended sequence
.trim();
.trimEnd();
}

@@ -40,0 +52,0 @@ }

@@ -1,2 +0,2 @@

export declare type FountainTypes = 'title_page' | 'scene_heading' | 'scene_number' | 'transition' | 'dialogue' | 'parenthetical' | 'action' | 'centered' | 'lyrics' | 'synopsis' | 'section' | 'note' | 'note_inline' | 'boneyard' | 'page_break' | 'line_break' | 'bold_italic_underline' | 'bold_underline' | 'italic_underline' | 'bold_italic' | 'bold' | 'italic' | 'underline' | 'escape' | 'blank_lines';
export declare type FountainTypes = 'title_page' | 'scene_heading' | 'scene_number' | 'transition' | 'dialogue' | 'parenthetical' | 'action' | 'centered' | 'lyrics' | 'synopsis' | 'section' | 'note' | 'note_inline' | 'boneyard' | 'page_break' | 'line_break' | 'bold_italic_underline' | 'bold_underline' | 'italic_underline' | 'bold_italic' | 'bold' | 'italic' | 'underline' | 'escape' | 'blank_line' | 'end_of_lines';
export declare const rules: Record<FountainTypes, RegExp>;

@@ -5,16 +5,16 @@ "use strict";

exports.rules = {
title_page: /^((?:title|credit|authors?|source|notes|draft date|date|contact|copyright)\:)/gim,
scene_heading: /^((?:\*{0,3}_?)?(?:(?:int|i)\.?\/(?:ext|e)|int|ext|est)[. ].+)|^\.(?!\.+)(\S.*)/i,
title_page: /^\s*((?:title|credit|authors?|source|notes|draft date|date|contact|copyright)\:)/gim,
scene_heading: /^\s*((?:\*{0,3}_?)?(?:(?:int|i)\.?\/(?:ext|e)|int|ext|est)[. ].+)|^\s*\.(?!\.+)(\S.*)/i,
scene_number: /( *#(.+)# *)/,
transition: /^((?:FADE (?:TO BLACK|OUT)|CUT TO BLACK)\.|.+ TO\:)|^> *(.+)/,
dialogue: /(?!^[0-9 _*]+(?:\(.*\))?[ *_]*(?:\^?)?\s*\n)(^(?:(?!\\?@|!)[^^()\na-z]+|@[^^()\n]+)(?: *\(.*\))?[ *_]*)(\^?)?\s*\n(?!\n+)([\s\S]+)/,
transition: /^\s*((?:FADE (?:TO BLACK|OUT)|CUT TO BLACK)\.|.+ TO\:)\s*$|^\s*> *(.+)$/,
dialogue: /(?!^\s*\\@|^\s*!)(?!^\s*[0-9 _*]+(?:\(.*\))?[*_]*(?:\^?)?\s*\n)(^\s*(?:@[^^()\n]+|[^^()\na-z]+)(?: *\(.*\))?[ *_]*)(\^?)?\s*\n(?!\n+)([\s\S]+)/,
parenthetical: /^ *(?:(?<u1>_{0,1})(?<s1>\*{0,3})(?=.+\k<s1>\k<u1>)|(?<s2>\*{0,3})(?<u2>_{0,1})(?=.+\k<u2>\k<s2>))(\(.+?\))(\k<s1>\k<u1>|\k<u2>\k<s2>) *$/,
action: /^(.+)/g,
centered: /^> *(.+) *<(\n.+)*/g,
lyrics: /^~(?! ).+(?:\n~(?! ).+)*/,
section: /^(#+) *(.*)/,
synopsis: /^=(?!=+) *(.*)/,
centered: /^\s*>.+<[^\S\r\n]*(?:\s*>.+<[^\S\r\n]*)*/g,
lyrics: /^\s*~(?! ).+(?:\n\s*~(?! ).+)*/,
section: /^\s*(#+) *(.*)/,
synopsis: /^\s*=(?!=+) *(.*)/,
note: /^\[{2}(?!\[+)(.+)]{2}(?!\[+)$/,
note_inline: /\[{2}(?!\[+)([\s\S]+?)]{2}(?!\[+)/g,
boneyard: /(^\/\*|^\*\/)$/g,
boneyard: /\/\*[\S\s]*?\*\//g,
page_break: /^={3,}$/,

@@ -30,4 +30,5 @@ line_break: /^ {2}$/,

escape: /\\([@#!*_$~`+=.><\\\/])/g,
blank_lines: /((?:[\t ]*\n){2,})/g
blank_line: /^(?: *(?:\n|$))+/,
end_of_lines: /(?:\n|$){2,}/g
};
//# sourceMappingURL=rules.js.map
import { Token } from './token';
export declare class Scanner {
private lastLineWasDualDialogue;
private lastSetOfBlankLines;
boneyardStripper(match: string): string;
tokenize(script: string): Token[];
}

@@ -7,4 +7,9 @@ "use strict";

class Scanner {
constructor() {
this.lastLineWasDualDialogue = false;
boneyardStripper(match) {
const endAtStrStart = /^[^\S\n]*\*\//m;
let boneyardEnd = '';
if (endAtStrStart.test(match)) {
boneyardEnd = '\n\n';
}
return boneyardEnd;
}

@@ -14,12 +19,10 @@ tokenize(script) {

const source = script
.replace(rules_1.rules.boneyard, '\n$1\n')
.replace(/\r\n|\r/g, '\n') // standardize returns to newlines
.replace(/^\t+|^ {3,}/gm, '')
.split(rules_1.rules.blank_lines)
.replace(rules_1.rules.boneyard, this.boneyardStripper)
.replace(/\r\n|\r/g, '\n') // convert carriage return / returns to newline
.split(rules_1.rules.end_of_lines)
.reverse();
const tokens = source.reduce((previous, line) => {
/** blank lines */
if (rules_1.rules.blank_lines.test(line)) {
this.lastSetOfBlankLines = line;
return previous;
/** spaces */
if (token_1.SpacesToken.matchedBy(line)) {
return new token_1.SpacesToken().addTo(previous);
}

@@ -60,6 +63,2 @@ /** title page */

}
/** boneyard */
if (token_1.BoneyardToken.matchedBy(line)) {
return new token_1.BoneyardToken(line).addTo(previous);
}
/** lyrics */

@@ -66,0 +65,0 @@ if (token_1.LyricsToken.matchedBy(line)) {

@@ -122,11 +122,9 @@ export interface Token {

}
export declare class BoneyardToken implements Token {
readonly type: 'boneyard_begin' | 'boneyard_end';
readonly text: string;
constructor(line: string);
export declare class PageBreakToken implements Token {
readonly type = "page_break";
addTo(tokens: Token[]): Token[];
static matchedBy(line: string): boolean;
}
export declare class PageBreakToken implements Token {
readonly type = "page_break";
export declare class SpacesToken implements Token {
readonly type = "spaces";
addTo(tokens: Token[]): Token[];

@@ -133,0 +131,0 @@ static matchedBy(line: string): boolean;

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ActionToken = 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;
exports.ActionToken = exports.SpacesToken = exports.PageBreakToken = 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 rules_1 = require("./rules");

@@ -9,4 +9,4 @@ class TitlePageBlock {

const match = line
.replace(rules_1.rules.title_page, '\n$1')
.split(rules_1.rules.blank_lines)
.replace(/^([^\n]+:)/gm, '\n$1')
.split(rules_1.rules.end_of_lines)
.reverse();

@@ -26,5 +26,5 @@ this.tokens = match.reduce((previous, item) => new TitlePageToken(item).addTo(previous), []);

this.is_title = true;
const pair = item.replace(/^\n+|\n+$/, '').split(/\:\n*/);
this.type = pair[0].trim().toLowerCase().replace(' ', '_');
this.text = pair[1].trim();
const [key, value] = item.split(/\:\n*/, 2);
this.type = key.trim().toLowerCase().replace(/ /g, '_');
this.text = value.replace(/^\s*/gm, '');
}

@@ -62,3 +62,3 @@ addTo(tokens) {

if (match) {
this.text = match[0].replace(/ *[><] */g, '');
this.text = match[0].replace(/[^\S\n]*[><][^\S\n]*/g, '');
}

@@ -95,3 +95,3 @@ }

if (match) {
let name = match[1];
let name = match[1].trim();
// iterating from the bottom up, so push dialogue blocks in reverse order

@@ -113,2 +113,3 @@ const isDualDialogue = !!match[2];

}
text = text.trim();
if (rules_1.rules.parenthetical.test(text)) {

@@ -221,3 +222,3 @@ return [...p, new ParentheticalToken(text)];

this.type = 'lyrics';
this.text = line.replace(/^~(?! )/gm, '');
this.text = line.replace(/^\s*~(?! )/gm, '');
}

@@ -281,8 +282,5 @@ addTo(tokens) {

exports.NoteToken = NoteToken;
class BoneyardToken {
constructor(line) {
const match = line.match(rules_1.rules.boneyard);
if (match) {
this.type = match[0][0] === '/' ? 'boneyard_begin' : 'boneyard_end';
}
class PageBreakToken {
constructor() {
this.type = 'page_break';
}

@@ -293,9 +291,9 @@ addTo(tokens) {

static matchedBy(line) {
return rules_1.rules.boneyard.test(line);
return rules_1.rules.page_break.test(line);
}
}
exports.BoneyardToken = BoneyardToken;
class PageBreakToken {
exports.PageBreakToken = PageBreakToken;
class SpacesToken {
constructor() {
this.type = 'page_break';
this.type = 'spaces';
}

@@ -306,10 +304,13 @@ addTo(tokens) {

static matchedBy(line) {
return rules_1.rules.page_break.test(line);
return rules_1.rules.blank_line.test(line);
}
}
exports.PageBreakToken = PageBreakToken;
exports.SpacesToken = SpacesToken;
class ActionToken {
constructor(line) {
this.type = 'action';
this.text = line.replace(/^!(?! )/gm, '');
this.text = line.replace(/^(\s*)!(?! )/gm, '$1')
.replace(/^( *)(\t+)/gm, (_, leading, tabs) => {
return leading + ' '.repeat(tabs.length);
});
}

@@ -316,0 +317,0 @@ addTo(tokens) {

{
"name": "fountain-js",
"version": "1.2.1",
"version": "1.2.2",
"description": "A simple parser for Fountain, a markup language for formatting screenplays.",

@@ -5,0 +5,0 @@ "main": "dist/index.js",

@@ -9,10 +9,4 @@ # Fountain-js

## Syntax Support
Supports up to `v 1.1` of the [Fountain syntax](https://www.fountain.io/syntax#section-changes).
Currently Fountain-js supports a limited range of key-value pairs for title pages -
* `Title`, `Credit`, `Author/s`, `Source`, `Notes`, `Draft date`, `Date`, `Contact`, `Copyright`
## Install

@@ -19,0 +13,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

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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc