afterwriting
Version:
Post-processing tools for Fountain screenplays
293 lines (251 loc) • 11.8 kB
JavaScript
define(function () {
function Liner(h) {
var module = {};
var _state = "normal"; // 'dialogue'
/**
* Split single line of text into lines
* @param text - text to split
* @param max - maximum characters per line
* @param index - character index in text (for navigation to the line in the editor)
* @param token - token type
*/
var split_text = function (text, max, index, token) {
// if the text doesn't exceed the maximum number of characters
// then just create a line with the entire text and return
if (text.length <= max) {
return [h.create_line({
type: token.type,
token: token,
text: text,
start: index,
end: index + text.length - 1
})];
}
// otherwise - find the nearest whitespace character
// the text will be split into lines at this character
var pointer = text.substr(0, max + 1).lastIndexOf(" ");
// ...unless there's no whitespace (breakPointFound=false)
// it may happen with extremely long lines
var breakPointFound;
if (pointer === -1) {
pointer = max - 1;
breakPointFound = false;
} else {
breakPointFound = true;
}
// split the text and continue splitting the rest
var skipWhiteSpaceIfNeeded = breakPointFound ? 1 : 0;
return [h.create_line({
type: token.type,
token: token,
text: text.substr(0, pointer),
start: index,
end: index + pointer
})].concat(split_text(text.substr(pointer + skipWhiteSpaceIfNeeded), max, index + pointer, token));
};
var split_token = function (token, max) {
token.lines = split_text(token.text || "", max, token.start, token);
};
var default_breaker = function (index, lines, cfg) {
var CONTD = cfg.text_contd || "(CONT'D)";
var MORE = cfg.text_more || "(MORE)";
for (var before = index - 1; before && !(lines[before].text); before--) {
}
for (var after = index + 1; after < lines.length && !(lines[after].text); after++) {
}
// possible break is after this token
var token_on_break = lines[index];
var token_after = lines[after];
var token_before = lines[before];
if (token_on_break.is("scene_heading") && token_after && !token_after.is("scene_heading")) {
return false;
} else if (token_after && token_after.is("transition") && !token_on_break.is("transition")) {
return false;
}
// action block 1,2 or 3 lines.
// don't break unless it's the last line
else if (token_on_break.is("action") &&
token_on_break.token.lines.length < 4 &&
token_on_break.token.lines.indexOf(token_on_break) !== token_on_break.token.lines.length - 1) {
return false;
}
// for and more lines
// break on any line different than first and penultimate
// ex.
// aaaaaaaaa <--- don't break after this line
// aaaaaaaaa <--- allow breaking after this line
// aaaaaaaaa <--- allow breaking after this line
// aaaaaaaaa <--- don't break after this line
// aaaaaaaaa <--- allow breaking after this line
else if (token_on_break.is("action") &&
token_on_break.token.lines.length >= 4 &&
(token_on_break.token.lines.indexOf(token_on_break) === 0 ||
token_on_break.token.lines.indexOf(token_on_break) === token_on_break.token.lines.length - 2)) {
return false;
} else if (cfg.split_dialogue && token_on_break.is("dialogue") && token_after && token_after.is("dialogue") && token_before.is("dialogue") && !(token_on_break.dual)) {
var new_page_character;
for (var character = before; lines[character].type !== "character"; character--) {
}
lines.splice(index, 0, h.create_line({
type: "parenthetical",
text: MORE,
start: token_on_break.start,
end: token_on_break.end,
token: token_on_break.token
}), new_page_character = h.create_line({
type: "character",
text: lines[character].text.trim() + " " + (lines[character].text.indexOf(CONTD) !== -1 ? "" : CONTD),
start: token_after.start,
end: token_after.end,
token: token_on_break.token
}));
if (lines[character].right_column) {
var dialogue_on_page_length = index - character;
var right_lines_on_this_page = lines[character].right_column.slice(0, dialogue_on_page_length).concat([
h.create_line({
type: "parenthetical",
text: MORE,
start: token_on_break.start,
end: token_on_break.end,
token: token_on_break.token
})
]),
right_lines_for_next_page = [h.create_line({
type: "character",
text: right_lines_on_this_page[0].text.trim() + " " + (right_lines_on_this_page[0].text.indexOf(CONTD) !== -1 ? "" : CONTD),
start: token_after.start,
end: token_after.end,
token: token_on_break.token
})
].concat(lines[character].right_column.slice(dialogue_on_page_length));
lines[character].right_column = right_lines_on_this_page;
if (right_lines_for_next_page.length > 1) {
new_page_character.right_column = right_lines_for_next_page;
}
}
return true;
} else if (lines[index].is_dialogue() && lines[after] && lines[after].is("dialogue", "parenthetical")) {
return false; // or break
}
return true;
};
var break_lines = function (lines, max, breaker, cfg) {
while (lines.length && !(lines[0].text)) {
lines.shift();
}
var s = max;
var p, internal_break = 0;
for (var i = 0; i < lines.length && i < max; i++) {
if (lines[i].type === "page_break") {
internal_break = i;
}
}
if (!internal_break) {
if (lines.length <= max) {
return lines;
}
do {
for (p = s - 1; p && !(lines[p].text); p--) {
}
s = p;
} while (p && !breaker(p, lines, cfg));
if (!p) {
p = max;
}
} else {
p = internal_break - 1;
}
var page = lines.slice(0, p + 1);
// if scene is not finished (next not empty token is not a heading) - add (CONTINUED)
var next_page_line_index = p + 1,
next_page_line = null,
scene_split = false;
while (next_page_line_index < lines.length && next_page_line === null) {
if (lines[next_page_line_index].type !== "separator" && lines[next_page_line_index].type !== "page_break") {
next_page_line = lines[next_page_line_index];
}
next_page_line_index++;
}
if (next_page_line && next_page_line.type !== "scene_heading") {
scene_split = true;
}
page.push(h.create_line({
type: "page_break",
scene_split: scene_split
}));
var append = break_lines(lines.slice(p + 1), max, breaker, cfg);
return page.concat(append);
};
var fold_dual_dialogue = function (lines) {
var any_unfolded_dual_dialogue_exists = true;
var get_first_unfolded_dual_left = function () {
for (var i = 0; i < lines.length; i++) {
if (lines[i].token &&
lines[i].token.type === "character" &&
lines[i].token.dual === "left" &&
lines[i].right_column === undefined) {
return i;
}
}
return -1;
};
var get_first_unfolded_dual_right_index_from = function (index) {
for (var i = index; i < lines.length; i++) {
if (lines[i].token &&
lines[i].token.type === "character" &&
lines[i].token.dual === "right") {
return i;
}
}
return -1;
};
var count_dialogue_tokens = function (right_index) {
var result = 0;
while (lines[right_index] && lines[right_index].is_dialogue()) {
result++;
right_index++;
}
result++; // collect separator after right dialogue
return result;
};
var fold_dual_dialogue = function (left_index, right_index) {
var dialogue_tokens = count_dialogue_tokens(right_index);
var right_lines = lines.splice(right_index, dialogue_tokens);
lines[left_index].right_column = right_lines;
};
while (any_unfolded_dual_dialogue_exists) {
var left_index = get_first_unfolded_dual_left();
var right_index = left_index === -1 ? -1 : get_first_unfolded_dual_right_index_from(left_index);
any_unfolded_dual_dialogue_exists = left_index !== -1 && right_index !== -1;
if (any_unfolded_dual_dialogue_exists) {
fold_dual_dialogue(left_index, right_index);
}
}
};
module.line = function (tokens, cfg) {
var lines = [],
global_index = 0;
_state = "normal";
tokens.forEach(function (token) {
var max = (cfg.print[token.type] || {}).max || cfg.print.action.max;
if (token.dual) {
max *= cfg.print.dual_max_factor;
}
split_token(token, max);
if (token.is("scene_heading") && lines.length) {
token.lines[0].number = token.number;
}
token.lines.forEach(function (line, index) {
line.local_index = index;
line.global_index = global_index++;
lines.push(line);
});
});
fold_dual_dialogue(lines);
lines = break_lines(lines, cfg.print.lines_per_page, cfg.lines_breaker || default_breaker, cfg);
return lines;
};
return module;
}
return Liner;
});