Last active
August 29, 2015 14:06
-
-
Save onitreb/0ee7773f0b3424dc9e98 to your computer and use it in GitHub Desktop.
Fountain writing tools for Google Documents (Wrastle)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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