Skip to content

Instantly share code, notes, and snippets.

@onitreb
Last active August 29, 2015 14:06
Show Gist options
  • Save onitreb/0ee7773f0b3424dc9e98 to your computer and use it in GitHub Desktop.
Save onitreb/0ee7773f0b3424dc9e98 to your computer and use it in GitHub Desktop.
Fountain writing tools for Google Documents (Wrastle)
THIS IS SOME SAMPLE SCREENWRITING TEXT GOBBLEDEEGOOK. USED TO TEST WRASTLE.
fade in:
int. mom’s house outside - day
Lydia holds up her hand to block the light.
But that’s not all.
simon
Liddy?
...and she likes chocolate.
lydia
Simon!
cut to:
ext. grocery store - night
Simon sits down next to her, takes her hand in his.
.lydia lifts her binocs
simon
Do you know what day it is?
dissolve to:
int. lower case slug - night
lydia
No… I can’t see…
jump cut to:
simon
What’s your mother’s name?
ext. weird case slug - day
lydia
My mother? Claudia.
(getting flustered)
Simon --
whatever to:
/**
* Copyright (c) 2014 by Mogsdad (David Bingham)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Google Doc add-on menu will inherit the name of the script project.
*/
function onOpen() {
DocumentApp.getUi().createAddonMenu()
.addItem('Toolbar', 'showSidebar')
.addItem("UPPER CASE", 'toUpperCase')
.addItem("lower case", 'toLowerCase')
.addItem("Title Case", 'toTitleCase')
.addItem("Sentence case", 'toSentenceCase')
.addItem("camelCase", 'toCamelCase')
.addSeparator()
.addItem("Fountain-lite", 'fountainLite')
.addToUi();
}
function showSidebar() {
var html = HtmlService.createHtmlOutputFromFile('Sidebar')
.setTitle('Wrastle Tools')
.setWidth(300);
DocumentApp.getUi() // Or DocumentApp or FormApp.
.showSidebar(html);
}
/**
* Dispatcher functions to provide case-specific
* callback functions to generic _changeCase().
*/
function toUpperCase() {
_changeCase(_toUpperCase);
}
function toLowerCase() {
_changeCase(_toLowerCase);
}
function toSentenceCase() {
_changeCase(_toSentenceCase);
}
function toTitleCase() {
_changeCase(_toTitleCase);
}
function toCamelCase() {
_changeCase(_toCamelCase);
}
/**
* Generic function to implement case change function in Google Docs.
* In case of error, alert window is opened in Google Docs UI with an
* explanation for the user. Exceptions are not caught, but pass through
* to Google Doc UI.
*
* Caveat: formatting is lost, due to operation of replaceText().
*
* @parameter {function} newCase Callback function, reflects an input
* string after case change.
*/
function _changeCase(newCase) {
newCase = newCase || _toUpperCase;
var doc = DocumentApp.getActiveDocument();
var selection = doc.getSelection();
var ui = DocumentApp.getUi();
var report = ""; // Assume success
if (!selection) {
report = "Select text to be modified.";
} else {
var elements = selection.getSelectedElements();
if (elements.length > 1) {
report = "Select text in one paragraph only.";
} else {
var element = elements[0].getElement();
//Logger.log( element.getType() );
var startOffset = elements[0].getStartOffset(); // -1 if whole element
var endOffset = elements[0].getEndOffsetInclusive(); // -1 if whole element
var elementText = element.asText().getText(); // All text from element
// Is only part of the element selected?
if (elements[0].isPartial())
var selectedText = elementText.substring(startOffset, endOffset + 1);
else
selectedText = elementText;
// Google Doc UI "word selection" (double click)
// selects trailing spaces - trim them
selectedText = selectedText.trim();
//endOffset = startOffset + selectedText.length - 1; // Not necessary w/ replaceText
// Convert case of selected text.
var convertedText = newCase(selectedText);
var regexEscaped = selectedText.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); // http://stackoverflow.com/a/3561711/1677912
element.replaceText(regexEscaped, convertedText);
}
}
if (report !== '') ui.alert(report);
}
/**
* Case change callbacks for customization of generic _changeCase().
* Source credits as noted.
*/
function _toUpperCase(str) {
return str.toUpperCase();
}
function _toLowerCase(str) {
return str.toLowerCase();
}
// http://stackoverflow.com/a/196991/1677912
function _toTitleCase(str) {
return str.replace(/\w\S*/g, function(txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
}
// http://stackoverflow.com/a/19089667/1677912
function _toSentenceCase(str) {
var rg = /(^\s*\w{1}|\.\s*\w{1})/gi;
return str.toLowerCase().replace(rg, function(toReplace) {
return toReplace.toUpperCase();
});
}
// http://stackoverflow.com/a/2970667/1677912
function _toCamelCase(str) {
return str.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function(match, index) {
if (+match === 0) return ""; // or if (/\s+/.test(match)) for white spaces
return index == 0 ? match.toLowerCase() : match.toUpperCase();
});
}
/**
* Scan Google doc, applying fountain syntax rules.
* Caveat: this is a partial implementation.
*
* Supported:
* Character names ahead of speech.
* - automatic capitalization
* Sluglines beginning in EXT., INT., and forced sluglines beginning with '.'
* - automatic capitalization
* - automatic bolding
* Transitions
* - automatic capitalization
* - automatic right-alignment
*
* Not supported:
* Everything else. See http://fountain.io/syntax
*/
function getPreferences() {
var userProperties = PropertiesService.getUserProperties();
var userPrefs = {
capCharacters: userProperties.getProperty('capCharacters'),
capSlugs: userProperties.getProperty('capSlugs')
};
return userPrefs;
}
function fountainLite() {
// Store user preferences (for use with a toolbar) //
var userProperties = PropertiesService.getUserProperties();
// Check if script has ever been run before by seeing if a user property exists.
// If it doesn't exist then set DEFAULT properties.
var propsExist = userProperties.getProperty('capCharacters');
if (propsExist === null) {
userProperties.setProperties({
'capCharacters': 'true',
'capSlugs': 'true',
'capTransitions': 'true',
'alignTransitions': 'true',
'boldSlugs': 'true',
'lightSyntax': 'true',
'tabCharacters': 'true',
'tabParanthetical': 'true'
});
}
// for testing //
/* userProperties.deleteAllProperties(); */
/* var preferences = userProperties.getProperties();
for (var key in preferences) {
Logger.log('Key: %s, Value: %s', key, preferences[key]);
} */
var capCharacters = userProperties.getProperty('capCharacters');
var capSlugs = userProperties.getProperty('capSlugs');
var boldSlugs = userProperties.getProperty('boldSlugs');
var capTransitions = userProperties.getProperty('capTransitions');
var alignTransitions = userProperties.getProperty('alignTransitions');
// Private helper function; find text length of paragraph
function paragraphLen(par) {
if (!par) return 0;
else
return par.asText().getText().length;
}
var doc = DocumentApp.getActiveDocument();
var paragraphs = doc.getBody().getParagraphs();
var numParagraphs = paragraphs.length;
// Scan document
for (var i = 0; i < numParagraphs; i++) {
if (paragraphLen(paragraphs[i]) > 0) {
paragraphText = paragraphs[i].asText().getText();
}
/*
** CHARACTER names are in UPPERCASE during dialog, so we do that automatically here.
** Dialogue comes right after Character.
*/
if (capCharacters === 'true') {
// This paragraph has text. If the preceeding one was blank and the following
// one has text, then this paragraph might be a character name.
if ((i == 0 || paragraphLen(paragraphs[i - 1]) == 0) && (i < numParagraphs && paragraphLen(paragraphs[i + 1]) > 0)) {
// If no power-user overrides, convert Character to UPPERCASE
if (paragraphText.charAt(0) != '!' && paragraphText.charAt(0) != '@' && (paragraphText.charAt(paragraphText.length - 1) != '.')) {
var convertedText = _toUpperCase(paragraphText);
var regexEscaped = paragraphText.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); // http://stackoverflow.com/a/3561711/1677912
paragraphs[i].replaceText(regexEscaped, convertedText);
}
/* // If INDENT DIALOG is set in user prefs
if (tabCharacters) {
var charTabbed = "\t\t\t";
paragraphs[i].replaceText(regexEscaped, charTabbed);
} */
}
}
/*
** SLUGLINES begin with EXT. or INT. with a blank line above and below.
** FORCED SLUGS begin with a '.'
** They should always be capitalized, so we do that here.
** And BOLD slugs if set in user prefs.
*/
if (capSlugs === 'true' || boldSlugs === 'true') {
var internal = "int. ";
var external = "ext. ";
var forcedSlug = ".";
// Lowercase the text to make searching simpler
var lcParagraph = paragraphText.toLowerCase();
/* var alphNum = lcParagraph.match(/a-zA-Z0-9/gi);
Logger.log(alphNum); */
// This paragraph has text. If the preceeding one was blank and the following one is blank, this might be a slugline.
if ((i == 0 || paragraphLen(paragraphs[i - 1]) == 0) && (i < numParagraphs && paragraphLen(paragraphs[i + 1]) == 0)) {
// If the line begins with INT. EXT., or a period,
// it is a SLUGLINE.
if ((lcParagraph.lastIndexOf(internal, 0) === 0) || (lcParagraph.lastIndexOf(external, 0) === 0) || ((lcParagraph.lastIndexOf(forcedSlug, 0) === 0) && (lcParagraph.lastIndexOf(forcedSlug, 1) != 1))) { //http://stackoverflow.com/a/4579228/3846634
if (capSlugs === 'true' && boldSlugs === 'true') {
var convertedText = _toUpperCase(paragraphText);
var regexEscaped = paragraphText.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); // http://stackoverflow.com/a/3561711/1677912
paragraphs[i].replaceText(regexEscaped, convertedText);
paragraphs[i].setBold(true);
}
else if (capSlugs === 'true' && boldSlugs === 'false') {
//convert it to UPPERCASE
var convertedText = _toUpperCase(paragraphText);
var regexEscaped = paragraphText.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); // http://stackoverflow.com/a/3561711/1677912
paragraphs[i].replaceText(regexEscaped, convertedText);
}
else {
//convert it to BOLD
paragraphs[i].setBold(true);
}
}
}
}
/*
** TRANSITIONS typically end in ' TO:' (ie. CUT TO:), and are always capitalized.
** with a blank line above and below.
** According to Fountain spec anything ending in TO: is treated as a transition.
** The spec also states that transitions may be forced by starting a line with > (ie. > BURN TO WHITE)
** We can fix the case of those as well, but leave the '>' alone.
*/
// This paragraph has text. If the preceeding one was blank and the following one is blank, this might be a transition.
// Check user settings - if we don't want to operate on Transitions, don't go any further.
if (capTransitions === 'true' || alignTransitions === 'true') {
if (((paragraphLen(paragraphs[i]) > 0) && ((i == 0 || paragraphLen(paragraphs[i - 1]) == 0) && (i < numParagraphs && paragraphLen(paragraphs[i + 1]) == 0)))) {
var transition = " to:";
// Lowercase the text to make searching simpler
var lcParagraph = paragraphText.toLowerCase();
// If the line ends in ' to:', convert line to UPPERCASE
var last4 = lcParagraph.slice(-4);
if (capTransitions === 'true') {
if (last4 === transition) {
var convertedText = _toUpperCase(paragraphText);
var regexEscaped = paragraphText.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); // http://stackoverflow.com/a/3561711/1677912
paragraphs[i].replaceText(regexEscaped, convertedText);
}
}
// and if user settings for aligning transitions to the right margin is true
if (alignTransitions === 'true') {
/* var last4 = lcParagraph.slice(-4); */
if (last4 === transition) {
paragraphs[i].setAlignment(DocumentApp.HorizontalAlignment.RIGHT);
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment