function* parse(text) {
const notCharacter = /^(WIDE|CUT TO|A |ON FINN|SERIES OF SHOTS)/;
const sceneHeader = /^(?:\d+\s+)?(INT|EXT)\.? (.*)/;
const dialogueNewline = /^([-A-ZÉ1-9#/. ]+)(\(O\.S\.?\)|\(V\.O\.\)|'S VOICE)?$/;
const dialogueColon = /^([-A-ZÉ1-9/#. ]+)[:\t](.*)/;
let lastLine, lastIndent, lastScene;
let lastSection = [];
for (let line of text.split("\n")) {
const indent = line.match(/^\s*/)[0].length;
const trimmed = line
.trim()
.replace(/^\(?CONTINUED\)?:?\s*/, "")
.replace(" (CONT'D)", "")
.trim();
let match;
if (
(!trimmed ||
(indent === lastIndent - 1 &&
lastSection.length > 1 &&
!lastLine.startsWith("\t"))) &&
lastSection.length
) {
if ((match = lastSection[0].match(sceneHeader))) {
const [, interior, heading] = match;
const [location, ...detail] = heading.split(" - ");
yield (lastScene = {
type: "scene",
location: location.trim(),
detail: detail
? detail.map((s) => s.trim()).filter((s) => s)
: undefined,
interior,
raw: lastSection
});
if (lastSection.length > 1) {
// ---------------------------------------- action (with scene header)
const description = paragraph(lastSection.slice(1));
const nouns = Array.from(properNouns(description));
const character =
nouns[0] && characterNouns.has(nouns[0].normalized)
? nouns[0].normalized
: undefined;
yield {
type: "action",
description,
nouns,
character,
scene: lastScene,
raw: lastSection
};
}
} else if (
lastScene &&
lastSection.length > 1 &&
(match = lastSection[0].match(dialogueNewline)) &&
!lastSection[0].endsWith(".") &&
!lastSection[0].match(notCharacter)
) {
// ---------------------------------------- dialogue (newline separated)
const [, character, modifier] = match;
const line = paragraph(lastSection.slice(1));
yield {
type: "dialogue",
character: normalize(character),
line,
nouns: Array.from(properNouns(line)),
modifier,
scene: lastScene,
raw: lastSection
};
} else if (
lastScene &&
lastSection[0].match(dialogueColon) &&
!lastSection[0].match(notCharacter)
) {
// ---------------------------------------- dialogue (colon or tab separated)
const lines = lastSection.reduce((lines, line) => {
if (line.match(dialogueColon)) lines.push(line);
else
lines[lines.length - 1] = paragraph([
lines[lines.length - 1],
line
]);
return lines;
}, []);
for (const l of lines) {
const [, character, line] = l.match(dialogueColon);
if (line) {
yield {
type: "dialogue",
character: normalize(character),
line: line.trim(),
nouns: Array.from(properNouns(line)),
scene: lastScene,
raw: lastSection
};
}
}
} else {
// ---------------------------------------- action
const description = paragraph(lastSection);
const nouns = Array.from(properNouns(description));
const character =
nouns[0] && characterNouns.has(nouns[0].normalized)
? nouns[0].normalized
: undefined;
yield {
type: "action",
description,
nouns,
character,
scene: lastScene,
raw: lastSection
};
}
lastSection = [];
}
if (trimmed) lastSection.push(trimmed);
lastLine = line;
lastIndent = indent;
}
}