An example:
Toggling a date-stamped @done
tag on all selected lines, in either:
- iOS Drafts 5 TaskPaper mode, or
- macOS BBEdit
http://actions.getdrafts.com/a/1JF
JS Source
For use as a JXA script for BBEdit, save:
BBDrafts.js to ~/Library/Script Libraries/BBDrafts.js
on the macOS machine.
(() => {
'use strict';
// A script for toggling date-stamped @done(2018-05-20 18) tags,
// Either:
//
// 1. In a script step for a Drafts 5 action for TaskPaper mode, or
// 2. as a JXA script for macOS BBEdit.
// BBEdit use requires the library at:
// https://gist.github.com/RobTrew/675b0f14f87b77ee025755e067022c62
// saved on macOS as: ~/Library/Script Libraries/BBDrafts.js
// Rob Trew (c) 2018
// Ver 0.1
// MAIN -----------------------------------------------
// toggleDone :: Drafts IO () -> String
const toggleDone = () => {
const
e = editor,
rngLines = e.getSelectedLineRange(),
strLines = e.getTextInRange(...rngLines),
rgxDone = tagRegex('done'),
dcts = map(s => ({
line: s,
mbRange: tagRangeMay(rgxDone, s)
}),
lines(strLines)
),
lng = dcts.length,
lrLines = bindLR(
lng > 0 ? (
Right(dcts)
) : Left('No lines found'),
xs => {
const
blnDone = !xs[0].mbRange.Nothing,
strTag = blnDone ? (
''
) : ` @done(${
take(10, (new Date())
.toISOString())
})`;
return Right(map(
x => {
const
mb = x.mbRange,
range = mb.Just,
strLine = x.line;
return (
mb.Nothing ? (
strLine
) : ( // @done removed
take(range[0] - 1, strLine) +
drop(
range[0] + range[1],
strLine
)
)
) + (
(blnDone || (
strip(strLine).length < 1
)) ? '' : strTag
);
},
xs
));
}
);
return lrLines.Left || (() => {
const str = unlines(lrLines.Right);
return (
e.setTextInRange(
...rngLines,
str
),
e.setSelectedRange(
rngLines[0], str.length
),
unlines(lrLines.Right)
);
})();
};
// TAG TOGGLING FUNCTIONS -----------------------------
// tagRegex :: String -> Regex
const tagRegex = strTag =>
new RegExp('(^|\\s*)(@' + strTag + ')(\\(.*\\)|)');
// A Range tuple: (startIndex :: Int, length :: Int)
// Range :: (Int, Int)
// tagRangeMay :: Regex -> String -> Maybe Range
const tagRangeMay = (rgxTag, strLine) => {
const m = strLine.match(rgxTag);
return m ? (
Just([m.index + 1, m[0].length - 1])
) : Nothing();
};
// GENERIC FUNCTIONS ----------------------------------
// Just :: a -> Just a
const Just = x => ({
type: 'Maybe',
Nothing: false,
Just: x
});
// Left :: a -> Either a b
const Left = x => ({
type: 'Either',
Left: x
});
// Nothing :: () -> Nothing
const Nothing = () => ({
type: 'Maybe',
Nothing: true,
});
// Right :: b -> Either a b
const Right = x => ({
type: 'Either',
Right: x
});
// Determines whether all elements of the structure
// satisfy the predicate.
// all :: (a -> Bool) -> [a] -> Bool
const all = (p, xs) => xs.every(p);
// bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
const bindLR = (m, mf) =>
m.Right !== undefined ? (
mf(m.Right)
) : m;
// concatMap :: (a -> [b]) -> [a] -> [b]
const concatMap = (f, xs) => [].concat.apply([], xs.map(f));
// doesFileExist :: FilePath -> IO Bool
const doesFileExist = strPath => {
const ref = Ref();
return $.NSFileManager.defaultManager
.fileExistsAtPathIsDirectory(
$(strPath)
.stringByStandardizingPath, ref
) && ref[0] !== 1;
};
// drop :: Int -> [a] -> [a]
// drop :: Int -> String -> String
const drop = (n, xs) => xs.slice(n);
// dropWhileEnd :: (Char -> Bool) -> String -> String
// dropWhileEnd :: (a -> Bool) -> [a] -> [a]
const dropWhileEnd = (p, s) => {
let i = s.length;
while (i-- && p(s[i])) {}
return s.slice(0, i + 1);
};
// isSpace :: Char -> Bool
const isSpace = c => /\s/.test(c);
// lines :: String -> [String]
const lines = s => s.split(/[\r\n]/);
// map :: (a -> b) -> [a] -> [b]
const map = (f, xs) => xs.map(f);
// readFile :: FilePath -> IO String
const readFile = strPath => {
let error = $(),
str = ObjC.unwrap(
$.NSString.stringWithContentsOfFileEncodingError(
$(strPath)
.stringByStandardizingPath,
$.NSUTF8StringEncoding,
error
)
);
return Boolean(error.code) ? (
ObjC.unwrap(error.localizedDescription)
) : str;
};
// strip :: String -> String
const strip = s => s.trim();
// stripEnd :: String -> String
const stripEnd = s => dropWhileEnd(isSpace, s);
// take :: Int -> [a] -> [a]
const take = (n, xs) => xs.slice(0, n);
// unlines :: [String] -> String
const unlines = xs => xs.join('\n');
// LIBRARY IMPORT --------------------------------------
// Evaluate a function f :: (() -> a)
// in the context of the JS libraries whose source
// filePaths are listed in fps :: [FilePath]
// usingLibs :: [FilePath] -> (() -> a) -> a
const usingLibs = (fps, f) =>
all(doesFileExist, fps) ? (
eval(`(() => {
'use strict';
${fps.map(readFile).join('\n\n')}
return (${f})();
})();`)
) : (() => {
const sa = standardSEAdditions();
return (
sa.activate(),
sa.displayDialog(
`Library not found at:
${concatMap(
fp => doesFileExist(fp) ? [
] : [fp],
fps).join('\n')}`, {
withTitle: 'Library file needed',
buttons: ['OK']
}
)
);
})();
// standardSEAdditions :: () -> Application
const standardSEAdditions = () =>
Object.assign(Application('System Events'), {
includeStandardAdditions: true
});
// iOS Drafts 5 ?
return Boolean(this.editor) ? (
toggleDone()
// macOS JXA, using the library at:
//
// https://gist.github.com/RobTrew/675b0f14f87b77ee025755e067022c62
//
// Saved as ~/Library/Script Libraries/BBDrafts.js
) : usingLibs(
[
'~/Library/Script Libraries/BBDrafts.js'
],
toggleDone
);
})();