Writing a Drafts script step which can also run in macOS BBEdit


#1

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
    );
})();