Created
May 16, 2018 11:40
-
-
Save RobTrew/16eb25a59ce59dc0265d65f4025944ab to your computer and use it in GitHub Desktop.
A BBEDIT version of a Drafts 5 action which toggles the case of selected text
This file contains hidden or 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
(() => { | |
'use strict'; | |
/* | |
A JavaScript for Automation script for BBEDIT | |
Cycle the case of selected text :: Mixed -> Upper -> Lower -> Mixed | |
(If the selection is collapsed, the nearest word will be selected) | |
This is an example of a JXA script which uses | |
iOS Drafts 5 (See http://getdrafts.com/) | |
functions to target BBEDIT, so that code written for one | |
of these editors can also run on the other editor. | |
This is the BBEDIT version, | |
(save as an .scpt file, and test from Script Editor) | |
the Drafts 5 iOS version is at : http://actions.getdrafts.com/a/1I9 | |
*/ | |
// main :: () -> IO String | |
const main = () => { | |
const | |
e = editor, | |
strReCased = ( | |
expandSelnByWord(), | |
caseToggled( | |
e.getSelectedText() | |
) | |
); | |
return strReCased.length > 0 ? ( | |
e.setSelectedText( | |
strReCased | |
), | |
strReCased | |
) : ''; | |
}; | |
// DRAFTS 5 EDITOR FUNCTIONS FOR BBEDIT --- | |
// drafts :: () -> Drafts | |
function drafts() { | |
const bb = Application('BBEdit'); | |
// Left :: a -> Either a b | |
const Left = x => ({ | |
type: 'Either', | |
Left: x | |
}); | |
// Right :: b -> Either a b | |
const Right = x => ({ | |
type: 'Either', | |
Right: x | |
}); | |
// Just :: a -> Just a | |
const Just = x => ({ | |
type: 'Maybe', | |
Nothing: false, | |
Just: x | |
}); | |
// Nothing :: () -> Nothing | |
const Nothing = () => ({ | |
type: 'Maybe', | |
Nothing: true, | |
}); | |
// bindMay (>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b | |
const bindMay = (mb, mf) => | |
mb.Nothing ? mb : mf(mb.Just); | |
// JXA --- (for a case where we invoke some Applescript) | |
// evalASLR :: String -> Either String a | |
const evalASLR = s => { | |
const | |
error = $(), | |
result = $.NSAppleScript.alloc.initWithSource(s) | |
.executeAndReturnError(error), | |
e = ObjC.deepUnwrap(error); | |
return e ? ( | |
Left(e.NSAppleScriptErrorBriefMessage) | |
) : Right(ObjC.unwrap(result.stringValue)); | |
}; | |
// References shared by functions ----------------- | |
const | |
ws = bb.windows, | |
mbWin = ws.length > 0 ? ( | |
Just(ws.at(0)) | |
) : Nothing(); | |
return { | |
editor: { | |
// Get the full text currently loaded in the editor. | |
// getText :: () -> String | |
getText: () => | |
bindMay( | |
mbWin, | |
w => Just(w.text()) | |
).Just || '', | |
// Replace the contents of the editor with a string. | |
// setText :: String -> IO () | |
setText: s => | |
bindMay( | |
mbWin, | |
w => w.text = s | |
), | |
// Get text of range that was last selected | |
// getSelectedText :: () -> String | |
getSelectedText: () => | |
bindMay( | |
mbWin, | |
w => Just(w.selection.contents()) | |
).Just || '', | |
// Replace the contents of the last text selection | |
// with a string. | |
// setSelectedText :: String -> IO () | |
setSelectedText: s => | |
bindMay( | |
mbWin, | |
w => w.selection.contents = s | |
), | |
// Get the last selected range in the editor. | |
// Returns an array with the start location of the range | |
// and the length of the selection. | |
// getSelectedRange :: () -> (Int, Int) | |
getSelectedRange: () => | |
bindMay( | |
mbWin, | |
w => { | |
const seln = w.selection; | |
return Just([ | |
seln.characteroffset() - 1, | |
seln.length() | |
]); | |
} | |
).Just || undefined, | |
// Get the current selected text range extended to the | |
// beginning and end of the lines it encompasses. | |
// getSelectedLineRange :: () -> (Int, Int) | |
getSelectedLineRange: () => | |
bindMay( | |
mbWin, | |
w => { | |
const | |
seln = w.selection, | |
intStart = w.text.lines.at( | |
seln.startline() - 1 | |
).characteroffset() - 1, | |
toLine = w.text.lines.at( | |
seln.endline() - 1 | |
); | |
return Just([ | |
intStart, | |
(toLine.characteroffset() - 1) + | |
(toLine.length() - intStart) | |
]); | |
} | |
).Just || undefined, | |
// Update the text selection in the editor by passing the | |
// start location and the length of the new selection. | |
// setSelectedRange :: Int -> Int -> IO () | |
setSelectedRange: (intFrom, intLength) => | |
bindMay( | |
mbWin, | |
w => ( | |
// Dialling out to AS ( haven't yet found | |
// the right selection reference incantation | |
// for the JXA Automation object ). | |
evalASLR([ | |
'tell application "BBEdit" to ', | |
'tell front window to select ', | |
`(characters ${intFrom + 1} thru `, | |
`${intFrom + intLength})` | |
].join('')), [ | |
intFrom, intLength | |
] | |
) | |
), | |
// Get the substring in a range from the text in the editor. | |
// getTextInRange :: Int -> Int -> String | |
getTextInRange: (intFrom, intLength) => | |
bindMay( | |
mbWin, | |
w => Just(w.text().slice( | |
intFrom, | |
intFrom + intLength | |
)) | |
).Just || '', | |
// Replace the text in the given range with new text. | |
// setTextInRange :: Int -> Int -> String -> IO () | |
setTextInRange: (intFrom, intLength, s) => | |
bindMay( | |
mbWin, | |
w => { | |
const t = w.text(); | |
return ( | |
w.text = t.slice(0, intFrom) + s + | |
t.slice(intFrom + intLength, -1), | |
Just(s) | |
) | |
} | |
).Just || '' | |
} | |
}; | |
}; | |
// caseToggled :: String -> String | |
const caseToggled = s => { | |
const cs = chars(s); | |
return !any(isUpper, cs) ? ( | |
toInitialCaps(s) | |
) : !any(isLower, cs) ? ( | |
toLower(s) | |
) : toUpper(s); | |
}; | |
// GENERIC FUNCTIONS ------------------------------------ | |
// Tuple (,) :: a -> b -> (a, b) | |
const Tuple = (a, b) => ({ | |
type: 'Tuple', | |
'0': a, | |
'1': b, | |
length: 2 | |
}); | |
// any :: (a -> Bool) -> [a] -> Bool | |
const any = (p, xs) => xs.some(p); | |
// chars :: String -> [Char] | |
const chars = s => s.split(''); | |
// concatMap :: (a -> [b]) -> [a] -> [b] | |
const concatMap = (f, xs) => [].concat.apply([], xs.map(f)); | |
// isLower :: Char -> Bool | |
const isLower = c => | |
/[a-z]/.test(c); | |
// isUpper :: Char -> Bool | |
const isUpper = c => | |
/[A-Z]/.test(c); | |
// regexMatches :: String -> String -> [[String]] | |
const regexMatches = (strRgx, strHay) => { | |
const rgx = new RegExp(strRgx, 'g'); | |
let m = rgx.exec(strHay), | |
xs = []; | |
while (m)(xs.push(m), m = rgx.exec(strHay)); | |
return xs; | |
}; | |
// splitAt :: Int -> [a] -> ([a],[a]) | |
const splitAt = (n, xs) => Tuple(xs.slice(0, n), xs.slice(n)); | |
// toLower :: String -> String | |
const toLower = s => s.toLowerCase(); | |
// toInitialCaps :: String -> String | |
const toInitialCaps = s => { | |
const rgx = /([A-Za-z\u00C0-\u00FF])([A-Za-z\u00C0-\u00FF]*)(\b[^[A-Za-z\u00C0-\u00FF]]*|$)/g; | |
return regexMatches(rgx, s) | |
.map(ms => ms[1].toUpperCase() + ms[2].toLowerCase() + ms[3]) | |
.join('') | |
}; | |
// toUpper :: String -> String | |
const toUpper = s => s.toUpperCase(); | |
// SELECTING WORD | |
// expandSelnByWord :: () -> IO () | |
const expandSelnByWord = (blnMultiWord, blnLeft) => { | |
const | |
e = editor, | |
tplSeln = e.getSelectedRange(), | |
tplLine = e.getSelectedLineRange(), | |
strLine = e.getTextInRange(...tplLine), | |
intPosn = tplSeln[0], | |
xy = splitAt( | |
intPosn - tplLine[0], | |
strLine | |
), | |
[dl, dr] = concatMap( | |
x => x !== null ? ( | |
[x[0].length] | |
) : [0], // | |
[/\b[\S]*$/.exec(xy[0]), /^[\S]*\b/.exec(xy[1])] | |
); | |
return (tplSeln[1] === 0 || dl > 0 && dr > 0) ? ( | |
e.setSelectedRange(intPosn - dl, dl + dr), | |
'extended' | |
) : blnMultiWord ? [ | |
// additionalWord( | |
// blnLeft, tplSeln, tplLine, strLine | |
// ) | |
] : 'No further'; | |
}; | |
// MAIN --- | |
const editor = drafts().editor; | |
return main(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment