Skip to content

Instantly share code, notes, and snippets.

@RobTrew
Created May 16, 2018 11:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save RobTrew/16eb25a59ce59dc0265d65f4025944ab to your computer and use it in GitHub Desktop.
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
(() => {
'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