Action

Toggle @done

Posted by @ComplexPoint, Last update over 5 years ago

Toggle the @done tag (with date stamp) on all selected lines.
(All additional lines are toggled in sync with the first selected line)

Can also be run in macOS BBEdit as a JXA script.

To enable macOS use, first save the library code at:

https://gist.github.com/RobTrew/675b0f14f87b77ee025755e067022c62

as ~/Library/Script Libraries/BBDrafts.js on the macOS machine.

Steps

  • script

    (() => {
        'use strict';
    
        // A script for toggling date-stamped @done(2018-05-20 18) tags,
    
        // Either:
        //
        // 1. In a Drafts 5 script 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.2
    
        // 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})();
            })();`)
        ) : libraryRequest(fps);
    
    // libraryRequest :: [FilePath] -> IO [FilePath]
    const libraryRequest = fps => {
        const
            sa = standardSEAdditions(),
            gaps = concatMap(
                fp => doesFileExist(fp) ? (
                    []
                ) : [fp],
                fps
            );
        return (
            sa.activate(),
            sa.displayDialog(
                `Library not found at:
    
            ${gaps.join('\n')}`, {
                    withTitle: 'Library file needed',
                    buttons: ['OK']
                }
            ),
            gaps
        );
    };
    
        // 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
        );
    })();
    

Options

  • After Success Default
    Notification Error
    Log Level Info
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.