A customisable macro for toggling a prefix in selected lines.
You can specify:
- The prefix string itself, including any following whitespace e.g.
'// '
or'# '
etc - Whether you want indents (before any prefix) to be ignored and left intact, or would rather clear/add prefixes only at the very beginning of a line.
Toggle Prefix in selected lines.kmmacros (26.4 KB)
The central function in the Execute Javascript action is:
// leaveIndent? -> Prefix -> Lines -> Toggled Lines
// prefixToggled :: Bool -> String -> String -> String
const prefixToggled = (bln, pfx, strLines) => {
const
n = pfx.length,
tpl = unzip(
map(x => bln ? (
(() => {
const s = takeWhile(isSpace, x);
return Tuple(s, drop(s.length)(x));
})()
) : Tuple('', x),
lines(strLines)
)
),
f = any(isPrefixOf(pfx), snd(tpl)) ? (
until(compose(not, isPrefixOf(pfx)))
(drop(n))
) : x => pfx + x;
return unlines(
bln ? (
zipWith(
(a, b) => a + b,
fst(tpl),
map(f, snd(tpl))
)
) : map(f, snd(tpl))
);
};
Full source
(() => {
'use strict';
const main = () => {
const
kme = Application('Keyboard Maestro Engine'),
pfx = kme.getvariable('pfxToToggle'),
blnLeaveIndent = Boolean(eval(
kme.getvariable('pfxLeaveIndent')
)),
sa = standardSEAdditions(),
strClip = sa.theClipboard(),
strToggled = (
(1 > pfx.length) ||
('string' !== typeof strClip) ||
(1 > strClip.length)) ? (
''
) : prefixToggled(
blnLeaveIndent,
pfx,
strClip,
);
return 0 < strToggled.length ? (
sa.setTheClipboardTo(strToggled),
strToggled
) : '';
};
// leaveIndent? -> Prefix -> Lines -> Toggled Lines
// prefixToggled :: Bool -> String -> String -> String
const prefixToggled = (bln, pfx, strLines) => {
const
n = pfx.length,
tpl = unzip(
map(x => bln ? (() => {
const s = takeWhile(isSpace, x);
return Tuple(s, drop(s.length)(x));
})() : Tuple('', x),
lines(strLines)
)
),
f = any(isPrefixOf(pfx), snd(tpl)) ? (
until(compose(not, isPrefixOf(pfx)))
(drop(n))
) : x => pfx + x;
return unlines(
bln ? (
zipWith(
(a, b) => a + b,
fst(tpl),
map(f, snd(tpl))
)
) : map(f, snd(tpl))
);
};
// GENERIC FUNCTIONS --------------------------------------
// https://github.com/RobTrew/prelude-jxa
// | True if any contained element satisfies the predicate.
// any :: (a -> Bool) -> [a] -> Bool
const any = (p, xs) => xs.some(p);
// compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
const compose = (f, g) => x => f(g(x));
// drop :: Int -> [a] -> [a]
// drop :: Int -> String -> String
const drop = n => xs => xs.slice(n);
// fst :: (a, b) -> a
const fst = tpl => tpl[0];
// isPrefixOf takes two lists or strings and returns
// true iff the first is a prefix of the second.
// isPrefixOf :: String -> String -> Bool
const isPrefixOf = xs => ys =>
ys.startsWith(xs);
// isSpace :: Char -> Bool
const isSpace = c => ' ' === c || '\t' === c;
// lines :: String -> [String]
const lines = s => s.split(/[\r\n]/);
// map :: (a -> b) -> [a] -> [b]
const map = (f, xs) => xs.map(f);
// min :: Ord a => a -> a -> a
const min = (a, b) => b < a ? b : a;
// not :: Bool -> Bool
const not = b => !b;
// showLog :: a -> IO ()
const showLog = (...args) =>
console.log(
args
.map(JSON.stringify)
.join(' -> ')
);
// snd :: (a, b) -> b
const snd = tpl => tpl[1];
// takeWhile :: (a -> Bool) -> [a] -> [a]
// takeWhile :: (Char -> Bool) -> String -> String
const takeWhile = (p, xs) => {
let i = 0;
const lng = xs.length;
while ((i < lng) && p(xs[i]))(i = i + 1);
return xs.slice(0, i);
};
// Tuple (,) :: a -> b -> (a, b)
const Tuple = (a, b) => ({
type: 'Tuple',
'0': a,
'1': b,
length: 2
});
// unlines :: [String] -> String
const unlines = xs => xs.join('\n');
// until :: (a -> Bool) -> (a -> a) -> a -> a
const until = p => f => x => {
let v = x;
while (!p(v)) v = f(v);
return v;
};
// unzip :: [(a,b)] -> ([a],[b])
const unzip = xys =>
xys.reduce(
(a, x) => Tuple.apply(null, [0, 1].map(
i => a[i].concat(x[i])
)),
Tuple([], [])
);
// zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
const zipWith = (f, xs, ys) =>
Array.from({
length: Math.min(xs.length, ys.length)
}, (_, i) => f(xs[i], ys[i], i));
// JXA ------------------------------------------------
// standardSEAdditions :: () -> Application
const standardSEAdditions = () =>
Object.assign(Application('System Events'), {
includeStandardAdditions: true
});
// MAIN ---
return main();
})();