Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save adriannier/79b0e59770b259fd8321 to your computer and use it in GitHub Desktop.
Save adriannier/79b0e59770b259fd8321 to your computer and use it in GitHub Desktop.
/* Open with Apple’s Script Editor in OS X 10.10 or later and set language to JavaScript */
function run(argv) {
try {
// Create a new Quatermain instance
var qm = new Quatermain()
// Make sure Safari is running
if (qm.safariRunning() == false) {
throw 'Make sure Safari shows an Apple Wiki page.'
}
// Capture the frontmost Safari tab
qm.setTargetTab(qm.frontmostTab())
// Inject JQuery into page
qm.injectJQuery()
// Change CSS on page
qm.do(function() {
$('div#content-inner')
.css('width', '100%')
.css('margin', '0')
.css('padding', '0')
$('div#content-primary')
.css('width', '100%')
$('div.editable.wrapchrome')
.css('width', '100%')
.css('margin', '0')
.css('padding', '0')
})
} catch (e) {
log('Error: ' + e)
}
}
function Quatermain() {
this.targettedTab = false
this.injectedJQueryVersion = '2.1.3'
this.loadFinishedTest = false
this.safariUIActionDelay = 0.3
this.safari = function() {
return Application('Safari')
}
this.systemEvents = function() {
return Application('System Events')
}
this.safariUI = function() {
return this.systemEvents().processes['Safari']
}
this.targetWindowUI = function() {
return this.safariUI().windows[this.windowIndexForTab(this.targetTab())]
}
this.activateSafari = function() {
if (!this.safariUI().frontmost()) {
this.safari().activate()
delay(this.safariUIActionDelay)
}
}
/*
Available key code or keystroke modifiers:
'command down', 'control down', 'option down', ‌'shift down'
*/
this.safariKeyCode = function(n, modifiers) {
this.activateSafari()
if (modifiers) {
this.systemEvents().keyCode(n, {using: modifiers})
} else {
this.systemEvents().keyCode(n)
}
delay(this.safariUIActionDelay)
}
this.safariKeystroke = function(s, modifiers) {
this.activateSafari()
if (modifiers) {
this.systemEvents().keystroke(s, {using: modifiers})
} else {
this.systemEvents().keystroke(s)
}
delay(this.safariUIActionDelay)
}
this.safariRunning = function() {
if (Application('System Events').processes.whose({name: 'Safari'}).length > 0) {
return true
} else {
return false
}
}
this.printAsPDF = function(saveDirectory, fileName) {
// Generate unique path
var pdfFilePath = this.uniqueFilePath(saveDirectory, fileName, '.pdf')
var previousFrontmostTab = this.raiseTab()
// For some reason the index of the target window changes from 0 to 1 as soon as the go-to-folder sheet appears. This causes problems when we need this index to be right when passed to System Events. Workaround is to save the System Events reference to the window now and reuse it throughout this function
var currentTargetWindowUI = this.targetWindowUI()
this.safariKeystroke('p', ['command down'])
this.waitFor('print sheet', function() {
if (currentTargetWindowUI.sheets().length > 0) {
return true
} else {
return false
}
}.bind(this))
// Open the PDF menu
var pdfMenuButton = currentTargetWindowUI.sheets[0].menuButtons['PDF']
pdfMenuButton.click()
// Click the Save as PDF… menu item
pdfMenuButton.menus[0].menuItems[1].click()
this.waitFor('save sheet', function() {
if (currentTargetWindowUI.sheets[0].sheets().length > 0) {
return true
} else {
return false
}
}.bind(this))
// Invoke 'Go to the folder' sheet
this.safariKeystroke('g', ['command down', 'shift down'])
this.waitFor('go-to-folder sheet', function() {
log('Target tab')
oLog(this.targetTab())
log('Window index: ' + this.windowIndexForTab(this.targetTab()))
log('Target window')
oLog(currentTargetWindowUI)
if (currentTargetWindowUI.sheets[0].sheets[0].sheets().length > 0) {
return true
} else {
return false
}
}.bind(this))
// Fill the 'Go to the folder' field with the PDF file's path
currentTargetWindowUI.sheets[0].sheets[0].sheets[0].textFields[0].value = pdfFilePath
// Dismiss 'Go to the folder' sheet
this.safariKeyCode(52) // Return key
this.waitFor('go-to-folder sheet to close', function() {
if (currentTargetWindowUI.sheets[0].sheets[0].sheets().length == 0) {
return true
} else {
return false
}
}.bind(this))
// Dismiss 'Save as' sheet
this.safariKeyCode(52) // Return key
this.waitFor('save sheet to close', function() {
try {
// Try to click the replace button if it exists
currentTargetWindowUI.sheets[0].sheets[0].sheets[0].buttons['Replace'].click()
} catch(e) {
}
if (currentTargetWindowUI.sheets[0].sheets().length == 0) {
return true
} else {
return false
}
}.bind(this))
this.waitFor('target window to have no sheets', function() {
try {
// Try to click the replace button if it exists
currentTargetWindowUI.sheets[0].sheets[0].sheets[0].buttons['Replace'].click()
} catch(e) {
}
if (currentTargetWindowUI.sheets().length == 0) {
return true
} else {
return false
}
}.bind(this))
this.raiseTab(previousFrontmostTab)
}
this.targetWindow = function() {
return this.windowForTab(this.targetTab())
}
this.windowForTab = function(tab) {
// Get the tab specifier as text
var tabPath = Automation.getDisplayString(tab)
// Get the path up to the specification of the tab
var found = tabPath.match(/(.*)\.tabs/i)
if (!found) {
// Couldn't get the path to the window
return false
} else {
// Found window path; eval it to return the actual window object
return eval(found[1])
}
}
this.windowIndexForTab = function(tab) {
return this.windowForTab(tab).index() - 1
}
this.targetTab = function() {
if (this.targettedTab !== false) {
return this.targettedTab
} else {
throw 'No target tab specified'
}
}
this.setTargetTab = function(arg) {
if (typeof arg == 'string') {
// Argument is a string; look for a tab with that name
this.targettedTab = this.firstTabWithName(arg)
} else {
var classOfArg
try {
classOfArg = arg.class()
} catch (e) {
classOfArg = ''
}
if (classOfArg == 'tab') {
this.targettedTab = arg
} else {
throw 'Please specify the tab or the tab\'s name'
}
}
log('Target tab set to \'' + this.targettedTab.name() + '\' (' + Automation.getDisplayString(this.targettedTab) + ')')
}
this.frontmostTab = function() {
return this.safari().windows[0].currentTab()
}
this.firstTabWithName = function(tabName) {
var windows = this.safari().windows()
for (var i = 0; i < windows.length; i++) {
// Calling tabs() on some windows might produce an error
var tabs
try {
tabs = windows[i].tabs()
} catch (e) {
tabs = []
}
for (var j = 0; j < tabs.length; j++) {
// Calling name() on some tabs might produce an error
var name
try {
name = tabs[j].name()
if (name == tabName) {
return tabs[j]
}
} catch (e) {
// Do nothing; not interested in errors
}
}
}
throw 'Safari has no tab with the name \'' + tabName + '\''
}
this.raiseTab = function(tab) {
try {
tab = tab || this.targetTab()
var previousFrontmostTab = this.frontmostTab()
// Get the window and its index
var window = this.windowForTab(tab)
var windowIndex = window.index() - 1
// Make sure the target tab is the current one in the window
window.currentTab = tab
// Bring the window to the front
this.safariUI().windows[windowIndex].actions['AXRaise'].perform()
delay(this.safariUIActionDelay)
return previousFrontmostTab
} catch(e) {
throw 'Could not raise tab: ' + e
}
}
this.url = function() {
return this.targetTab().url()
}
this.setURL = function(url) {
this.do(function(url) {
document.body.innerHTML = ''
window.location = url
}, url)
// Wait until page content is visible
this.waitFor('body element to have content', function() {
if (this.do("document.body.innerHTML") != '') {
return true
} else {
return false
}
}.bind(this))
if (this.do("document.body.innerHTML") != '') {
this.injectJQuery()
} else {
throw 'Failed to load ' + url
}
if (typeof this.loadFinishedTest == 'function') {
this.waitFor('page to pass custom load finished tester', function() {
try {
return this.loadFinishedTest()
} catch(e) {
throw 'Could not test if page finished loading: ' + e
}
}.bind(this))
}
}
this.do = function(code) {
var returnValues
if (typeof code == 'function') {
returnValues = this.returnValues(code)
code = this.stringifyReturns(code.toString())
code = this.makeNamedFunction(code)
code = this.insertFunctionCall(code, arguments)
} else if (typeof code == 'string') {
// Do nothing with strings
returnValues = [code]
code = 'JSON.stringify(' + code +')'
} else {
log('The do() method requires either a function or a string, but a ' + typeof code + ' was specified.')
}
code = code.replace(/\$\(/g, 'window.injectedJQuery(')
var result = this.safari().doJavaScript(code, {in: this.targetTab()})
if (returnValues.length > 0) {
if (result == '') {
return ''
} else {
return JSON.parse(result)
}
} else {
return
}
}
this.stringifyReturns = function(code) {
code = code.replace(/(\s*)return\s*([^\n;]*)/gi, "$1return JSON.stringify($2)")
return code
}
this.returnValues = function(code) {
var regex = /\s*return[ \t]+(\S+[^\n;]*)/gm
var matches = []
var found
while (found = regex.exec(code)) {
matches.push(found[1])
regex.lastIndex = found.index + found[0].length + 1
}
return matches
}
this.insertFunctionCall = function(fn, fnArgs) {
var found = fn.match(/function\s*[_a-z0-9]*\s*\(([^)]*)/i)
if (!found) {
// No arguments found
return fn
}
var functionCall
var functionName = this.functionName(fn)
var resultVarName = functionName + '_result'
if (this.trimString(found[1]) == '') {
functionCall = "var " + resultVarName + ' = ' + functionName + '()'
} else {
var argNames = found[1].split(',')
// Does the count of the function arguments and the specified values match?
if ((argNames.length + 1) != fnArgs.length) {
throw 'Argument count mismatch for function.'
}
var argumentValues = []
var argName
for (var i = 0; i < argNames.length; i++) {
argName = this.trimString(argNames[i])
argumentValues.push("JSON.parse(" + this.stringify(fnArgs[i + 1]) + ")")
}
functionCall = "var " + resultVarName + ' = ' + functionName + '(' + argumentValues.join(', ') + ')'
}
return fn + "\n" + functionCall + "\n" + functionName + ' = undefined' + "\n" + resultVarName
}
this.stringify = function(v) {
var jsonString = JSON.stringify(v)
jsonString = jsonString.replace(/[\\]/g, '\\\\')
.replace(/[\"]/g, '\\\"')
.replace(/[\/]/g, '\\/')
.replace(/[\b]/g, '\\b')
.replace(/[\f]/g, '\\f')
.replace(/[\n]/g, '\\n')
.replace(/[\r]/g, '\\r')
.replace(/[\t]/g, '\\t')
return "\"" + jsonString + "\""
}
this.makeNamedFunction = function(fn) {
var functionName = this.functionName(fn)
if (functionName === false) {
throw 'The do() method requires a function.'
} else if ( functionName == '' || functionName == ' ') {
functionName = this.randomFunctionName()
}
fn = fn.replace(/function(\s*[_a-z0-9]*\s*)\(/i, 'function ' + functionName + '(')
return fn
}
this.functionName = function(fn) {
var found = fn.match(/function(\s*[_a-z0-9]*\s*)\(/i)
if (!found) {
return false
} else {
return this.trimString(found[1])
}
}
this.randomFunctionName = function() {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_";
for(var i = 0; i < 25; i++ ) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
this.isJQueryInjected = function() {
return this.do(function(jqVersion) {
if (typeof window.injectedJQuery != 'undefined' && window.injectedJQuery.fn.jquery == jqVersion) {
return true
} else {
return false
}
}, this.injectedJQueryVersion)
}
this.injectJQuery = function() {
if (this.isJQueryInjected() === false) {
this.do(function(jqVersion) {
var s0 = document.createElement('script')
document.head.appendChild(s0)
s0.type = 'text/javascript'
s0.onload = function() { window.injectedJQuery = $.noConflict(true) }
s0.src = 'https://code.jquery.com/jquery-' + jqVersion + '.min.js'
}, this.injectedJQueryVersion)
this.waitFor('jQuery to be injected', function() {
return this.isJQueryInjected() }
.bind(this))
}
}
this.trimString = function(str) {
return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
}
this.waitFor = function(description, fn, maxSeconds) {
maxSeconds = maxSeconds || 10
// Calculate the future date at which the timeout should happen
var maxDate = new Date()
maxDate.setTime(maxDate.getTime() + maxSeconds * 1000)
// Initialize timer
var secondsElapsed = 0
/*
The way that elapsed seconds are kept track of is highly inaccurate.
For example it does not take into account the time it takes to run
the testing function. This serves only to protect against waiting
forever when the code executes in the middle of a time change.
*/
var firstTest = true
while (maxSeconds >= secondsElapsed) {
try {
if (fn()) {
log('Done waiting for ' + description)
return
}
if (firstTest && description) {
log('Waiting for ' + description)
firstTest = false
}
} catch(e) {
log('Error while waiting for ' + description + ': ' + e)
}
this.wait(0.1)
secondsElapsed += 0.1 // Keep track of elapsed time
if (new Date() >= maxDate) { throw 'Timeout while waiting for ' + description }
}
throw 'Timeout while waiting for ' + description
}
this.wait = function(s) {
delay(s)
// var app = Application.currentApplication()
// app.includeStandardAdditions = true
// app.doShellScript('/bin/sleep ' + s)
}
this.uniqueFilePath = function(parentFolderPath, fileName, suffix) {
/*
Only for use with JavaScript for Automation (JXA)
Generates a unique path by appending a number
to the end of the file name.
If suffix is not specified it is extracted
from the file name.
If neither file name nor suffix is specified,
it is assumed that the first argument holds
the complete path to file.
*/
if (!fileName) {
// Get the file name from the first argument
var components = $.NSString.alloc.initWithUTF8String(parentFolderPath).pathComponents.js // Return native array with NSStrings
// Get an array of native strings
var jsComponents = []
components.forEach(function(component) {
jsComponents.push(component.js)
})
// Get the file name
fileName = jsComponents[jsComponents.length - 1]
// Get the parent folder path
jsComponents.splice(-1, 1)
parentFolderPath = jsComponents.join('/')
}
if (!suffix) {
// Get suffix from file name
fileNameNSS = $.NSString.alloc.initWithUTF8String(fileName)
suffix = fileNameNSS.pathExtension.js
fileName = fileNameNSS.stringByDeletingPathExtension.js
}
if (suffix.indexOf('.') != 0) {
// Add dot to beginning of suffix
suffix = '.' + suffix
}
// Clean up file name
fileName = fileName.replace(/[:\/\\]/gi,'-')
// Shorten file name if necessary
var availableFileNameLength = 255 - suffix.length
if (fileName.length > availableFileNameLength) {
fileName = fileName.substr(0, availableFileNameLength )
}
// Expand tilde in parent folder path
parentFolderPath = $.NSString.alloc.initWithUTF8String(parentFolderPath).stringByExpandingTildeInPath.js
// Add trailing colon to parent folder path
if ( parentFolderPath.slice(-1) != '/') { parentFolderPath += '/' }
var loopNumber = 2
var tempFilePath = parentFolderPath + fileName + suffix
while (Application('System Events').exists(Path(tempFilePath))) {
// Shorten file name if necessary
availableFileNameLength = 255 - suffix.length - loopNumber.toString().length - 1
if (fileName.length > availableFileNameLength) {
fileName = fileName.substr(0, availableFileNameLength)
}
tempFilePath = parentFolderPath + fileName + ' ' + loopNumber + suffix
loopNumber++
}
return tempFilePath
}
}
function oLog(o) {
log(Automation.getDisplayString(o))
}
function sLog(msg) {
log(JSON.stringify(msg))
}
function log(msg) {
this.zeroPad = function(str, len, pad, padAtEnd) {
str = String(str)
len = len || 2
pad = pad || '0'
padAtEnd = padAtEnd || false
while (str.length < len) {
if (padAtEnd) {
str = str + pad
} else {
str = pad + str
}
}
return str
}
this.timestamp = function() {
var d = new Date();
var h = this.zeroPad(d.getHours())
var m = this.zeroPad(d.getMinutes())
var s = this.zeroPad(d.getSeconds())
var ms = this.zeroPad(d.getMilliseconds(), 3)
var y = d.getFullYear()
var month = this.zeroPad(d.getMonth() + 1)
var day = this.zeroPad(d.getDate() + 1)
return y + '-' + month + '-' + day + ' ' + h + ':' + m + ':' + s + '.' + ms
}
console.log(this.timestamp() + " " + msg)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment