Convert informal date-times to yyyy-mm-dd [HH:MM] in TaskPaper 3 Preview

You could test this variant to see if it matches what you are after – it experimentally trims the time component off interval expressions if the unit is a day or larger, but leaves it in place where N hours or N minutes are given.

Convert parenthesised informal date to ISO (draft alternative,).kmmacros (31.2 KB)

JS Source
(() => {
    'use strict';

    // Rob Trew @2020
    // Draft 0.02

    // -- Rob Trew (c) 2020
    // --
    // -- Permission is hereby granted, free of charge, 
    // -- to any person obtaining a copy of this software 
    // -- and associated documentation files (the "Software"), 
    // -- to deal in the Software without restriction, 
    // -- including without limitation the rights to use, copy, 
    // -- modify, merge, publish, distribute, sublicense, 
    // -- and/or sell copies of the Software, and to permit persons 
    // -- to whom the Software is furnished to do so, 
    // -- subject to the following conditions:

    // -- *******
    // -- The above copyright notice and this permission notice 
    // -- shall be included in ALL copies 
    // -- or substantial portions of the Software.
    // -- *******

    // -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
    // -- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
    // -- OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
    // -- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
    // -- DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
    // -- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 
    // -- OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

    // main :: IO ()
    const main = () => {
        const inner = () => {
            const
                ds = Application('TaskPaper')
                .documents;
            return either(
                alert('Problem')
            )(
                x => x
            )(
                bindLR(
                    0 < ds.length ? (
                        Right(ds.at(0))
                    ) : Left('No TaskPaper documents open')
                )(
                    d => d.evaluate({
                        script: tp3Context.toString(),
                        withOptions: {
                            tagName: 'now'
                        }
                    })
                )
            );
        };

        // -------------- TASKPAPER CONTEXT --------------

        const tp3Context = (editor, options) => {
            const
                rgxInterval = (
                    /^\s*\d+\s*(d|w|week|month|y|year)/
                );
            const main = () => {
                const lrExtended = extendedSelectionLR(
                    editor
                );
                return bindLR(
                    Boolean(lrExtended.Left) ? (
                        selectedTagValueAndPosnLR(editor)
                    ) : lrExtended
                )(
                    ([phrase, from]) => {
                        const parse = DateTime.format(phrase);
                        return bindLR(
                            'invalid date' !== parse ? (
                                Right(
                                    rgxInterval.test(phrase) ? (
                                        parse.slice(0, 10)
                                    ) : parse
                                )
                            ) : Left(`${parse} :: ${phrase}`)
                        )(
                            isoString => (
                                // ----- EDITOR TEXT -----
                                editor.outline
                                .groupUndoAndChanges(() =>
                                    editor.selection.startItem
                                    .replaceBodyRange(
                                        from,
                                        phrase.length,
                                        isoString
                                    )
                                ),
                                // ---- OUTPUT STREAM ----
                                Right(`${phrase} -> ${isoString}`)
                            )
                        );
                    }
                );
            };

            // selectedTagValueAndPosnLR :: Editor -> 
            // Either String (String, Int, Int)
            const selectedTagValueAndPosnLR = editor => {
                // Either a message or some parenthesised text.
                // From preceding opening parenthesis if any
                // to next closing parenthesis on same line, 
                // if any.
                const
                    seln = editor.selection,
                    txt = seln.startItem.bodyString,
                    iPosn = seln.startOffset,
                    iClose = [...txt.slice(iPosn)]
                    .findIndex(
                        c => ')' === c
                    ),
                    iOpen = [...txt.slice(0, iPosn)]
                    .reverse().findIndex(
                        c => '(' === c
                    ),
                    value = dropWhileEnd(
                        c => c === ')'
                    )(txt);
                return Right(
                    -1 === iOpen ? [
                        value, 0
                    ] : [
                        value.slice(
                            iPosn - iOpen,
                            -1 !== iClose ? (
                                iPosn + iClose
                            ) : undefined
                        ),
                        iPosn - iOpen
                    ]
                );
            };


            // extendedSelectionLR :: Editor -> 
            // Either String String
            const extendedSelectionLR = editor => {
                // Either a message or the text selected
                // in the first selected item.
                const
                    seln = editor.selection,
                    start = seln.startOffset;
                return seln.isCollapsed ? (
                    Left('Selection not extended')
                ) : Right([
                    seln.startItem.bodyString
                    .slice(
                        start,
                        seln.endOffset
                    ),
                    start
                ]);
            };

            // ----------------- GENERIC -----------------

            // Left :: a -> Either a b
            const Left = x => ({
                type: 'Either',
                Left: x
            });

            // Right :: b -> Either a b
            const Right = x => ({
                type: 'Either',
                Right: x
            });


            // bindLR (>>=) :: Either a -> 
            // (a -> Either b) -> Either b
            const bindLR = m =>
                mf => undefined !== m.Left ? (
                    m
                ) : mf(m.Right);


            // dropWhileEnd :: (a -> Bool) -> [a] -> [a]
            // dropWhileEnd :: (Char -> Bool) -> String -> [Char]
            const dropWhileEnd = p =>
                // xs without the longest suffix for which
                // p returns true for all elements.
                xs => {
                    let i = xs.length;
                    while (i-- && p(xs[i])) {}
                    return xs.slice(0, i + 1);
                };

            // findIndex :: (a -> Bool) -> [a] -> Maybe Int
            const findIndex = p =>
                //  Just the index of the first element in
                //  xs for which p(x) is true, or
                //  Nothing if there is no such element.
                xs => {
                    const i = [...xs].findIndex(p);
                    return -1 !== i ? (
                        Just(i)
                    ) : Nothing();
                };

            return main();
        };


        // alert :: String => String -> IO String
        const alert = title => s => {
            const
                sa = Object.assign(Application('System Events'), {
                    includeStandardAdditions: true
                });
            return (
                sa.activate(),
                sa.displayDialog(s, {
                    withTitle: title,
                    buttons: ['OK'],
                    defaultButton: 'OK',
                    withIcon: sa.pathToResource('TaskPaper.icns', {
                        inBundle: 'Applications/TaskPaper.app'
                    })
                }),
                s
            );
        };

        return inner()
    };

    // ----------------- LIBRARY IMPORT ------------------

    // Evaluate a function f :: (() -> a)
    // in the context of the JS libraries whose source
    // filePaths are listed in fps :: [FilePath]

    // 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) => {
        const gaps = fps.filter(fp => !doesFileExist(fp));
        return 1 > gaps.length ? eval(
            `(() => {
                'use strict';
                ${fps.map(readFile).join('\n\n')}
                return (${f})();
             })();`
        ) : 'Library not found at: ' + gaps.join('\n');
    };

    // doesFileExist :: FilePath -> IO Bool
    const doesFileExist = strPath => {
        const ref = Ref();
        return $.NSFileManager.defaultManager
            .fileExistsAtPathIsDirectory(
                $(strPath)
                .stringByStandardizingPath, ref
            ) && ref[0] !== 1;
    };

    // 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;
    };

    // -------- GENERIC FUNCTIONS FOR JXA CONTEXT --------

    // Left :: a -> Either a b
    const Left = x => ({
        type: 'Either',
        Left: x
    });

    // Right :: b -> Either a b
    const Right = x => ({
        type: 'Either',
        Right: x
    });

    // bindLR (>>=) :: Either a -> 
    // (a -> Either b) -> Either b
    const bindLR = m =>
        mf => undefined !== m.Left ? (
            m
        ) : mf(m.Right);

    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        // Application of the function fl to the
        // contents of any Left value in e, or
        // the application of fr to its Right value.
        fr => e => 'Either' === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;

    // ---------------------- MAIN -----------------------
    return main();
})();