Paste snippets from Quiver

First sketch of a macro which pastes one or more code or text snippets from Quiver.

(I notice that there is a LaunchBar action which does something similar, but it depends on the installation of Node.js and npm modules - this draft macro uses a JavaScript for Automation action, which has no external dependencies, other than a standard installation of Quiver itself).

Paste one or more Quiver snippets ver 2.kmmacros (47.0 KB)

ES 6 version of source
(Macro also provides an ES5 version for use with pre Sierra macOS)

(() => {
    'use strict';

    // Copyright (c) 2016 Robin Trew
    //
    // 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 at end of this file )

    // QUIVER LIBRARY TO USE

    let strQVLibName = 'Default library',
        strQVLibPath = '~/Library/Containers/com.happenapps.Quiver' +
        '/Data/Library/Application\ Support/Quiver/Quiver.qvlibrary/';

    let strMRUName = 'quiverMenuMemory';

    // MENU LEVELS

    // Sequence of two menu-path processing functions.
    // (One function for each junction between successive menu levels).

    // Each particular menu function is compiled from the specs here by
    // the higher-order nestedMenu() function. (See below).
    // The value of .menuItems should be a function from a [(k,v)] chain of
    // given length to a list of contextual menu item pairs [(key,value)]
    // (e.g. the books in a chosen library, or the notes in a chosen book)
    // Surplus unconfirmed (k, v) pairs (remembered from previous sessions)
    // are used as initial selections in the displayed menu.
    let lstMenus = [{
            title: 'Quiver notebooks',
            msg: 'Choose a notebook:',
            menuItems: lstPath => {
                return resList(lstPath.length ? (
                        lstPath[0][1]
                    ) : defaultLibPath())
                    .map(dct => [dct.name, dct.uuid]);
            }
        }, {
            title: 'Quiver notes',
            msg: 'Choose note(s):',
            multiple: true,
            menuItems: lstPath => {
                return resList(
                        lstPath[0][1] + lstPath[1][1] + '.qvnotebook/',
                        'title'
                    )
                    .map(dct => [dct.title, dct.uuid]);
            },
            cancelName: 'Back'
        }]
        .map(nestedMenu);

    // GENERIC FUNCTIONS FOR NESTED MENUS

    // nestedMenu() generates a function from a key-value list
    // (remembered choice sequence, retrieved from a JSON file)
    //      to a new key-value sequence (longer, shorter, or adjusted)
    //      wrapped with a confirmation status flag.
    //
    //      Unconfirmed final key-value pairs are read as menu pre-selections
    //      confirmed final key-value pairs are used as menu output.
    //
    //      The second argument to nesteMenu is a menu stage index,
    //      usually provided automatically by Array.map
    //      This is used to derive the length of choice paths that are
    //      handled by particular menu-stage functions.
    //
    // nestedMenu :: {title:String, msg:String, menuItems:([(k, v)] -> {
    //                      path: [(k,v)],
    //                      confirmed: Bool
    //                  }),
    //                  multiple:Bool} -> Int ->
    //                  ( [(k, v)] -> { path: [(k,v)], confirmed: Bool} )
    function nestedMenu(dctSpec, i) {
        let nMenu = (i || 0) + 1;

        return lstPath => {

            // find :: (a -> Bool) -> [a] -> Maybe a
            let find = (p, xs) => {
                for (var i = 0, lng = xs.length; i < lng; i++) {
                    if (p(xs[i])) return xs[i];
                }
                return undefined;
            };

            let menuItems = dctSpec.menuItems,
                lstOptions = menuItems instanceof Array ? (
                    menuItems
                ) : menuItems(lstPath),
                lstMenuKeys = (lstOptions || [
                    ['x', 0],
                    ['y', 1],
                    ['z', 2]
                ])
                .map(x => x[0]);

            let blnMult = (dctSpec
                    .multiple !== undefined ? dctSpec.multiple : false),
                lstPathKeys = (lstPath || [])
                .map(x => x[0]),
                lngPathKeys = lstPathKeys.length;

            let a = Application('System Events'),
                sa = (a.includeStandardAdditions = true, a);

            let varResult = lstMenuKeys.length ? sa
                .activate() &&
                sa.chooseFromList(lstMenuKeys, {
                    withTitle: (dctSpec.title || 'dctSpec.title'),
                    withPrompt: lstPathKeys.slice(0, nMenu)
                        .join(' > ') +
                        '\n\n' + (dctSpec.msg || 'dctSpec.msg'),
                    defaultItems: (lngPathKeys > nMenu ? (
                        lstPathKeys.slice(blnMult ? nMenu : -1)
                    ) : lstMenuKeys[0]),
                    okButtonName: 'OK',
                    cancelButtonName: dctSpec.cancelName || 'Cancel',
                    multipleSelectionsAllowed: blnMult,
                    emptySelectionAllowed: true
                }) : [];

            let lstParentPath = lstPath.slice(0, nMenu);
            return {
                path: varResult !== false ? lstParentPath
                    .concat(
                        varResult
                        .map(k => find(x => x[0] === k, lstOptions))
                    ) : lstParentPath,
                confirmed: varResult !== false,
                cancelled: (nMenu === 1) && (varResult === false)
            };
        };
    }

    // Index of appropriate subMenu, given current path length and status)
    // menuIndexforPathLength  :: MPath [(k, v)] -> Int
    let menuIndexforPathLength = mPath => {
        let intStages = mPath.path.length,
            intMenus = lstMenus.length;

        // Last menu (if path already full-length)
        // or zero-index the length of the *confirmed* part of the path
        let index = (intStages >= (intMenus + 1)) ?
            (intMenus - 1) : (intStages - (mPath.confirmed ? 1 : 2));

        return index < 0 ? 0 : index;
    }

    // QUIVER-SPECIFIC FUNCTIONS

    let defaultLibPath = () => {
            let strPath = strQVLibPath +
                (strQVLibPath.charAt(-1) !== '/' ? '/' : '');

            return (doesDirectoryExist(
                    strPath) ?
                strPath : undefined);
        },

        // resList :: PathString -> maybe String -> [(String, UUID)]
        resList = (strFolderPath, strField) => {
            let strLabel = strField || 'name';

            return strFolderPath ? listDirectory(strFolderPath)
                .reduce((a, x) => {
                    let strMetaPath = strFolderPath + x + '/meta.json';

                    return (doesFileExist(strMetaPath)) ? (
                        a.concat(JSON.parse(readFile(strMetaPath)))
                    ) : a;
                }, [])
                .sort((a, b) => {
                    let strA = a[strLabel].toLowerCase(),
                        strB = b[strLabel].toLowerCase();

                    return strA < strB ? -1 : (strA > strB ? 1 : 0);
                }) : [];
        },

        // noteText :: libpath ->  bookuuid -> noteuuid -> text
        noteText = (strLibPath, strBookUUID, strNoteUUID) => {
            let strNotePath = strLibPath + strBookUUID + '.qvnotebook/' +
                strNoteUUID + '.qvnote/content.json',
                dctNote = doesFileExist(strNotePath) ? (
                    JSON.parse(readFile(strNotePath))
                ) : undefined;

            return dctNote ? (
                dctNote.cells
                .reduce(
                    (a, x) => ['code', 'text'].indexOf(x.type) !== -1 ? (
                        a + x.data + '\n\n'
                    ) : a,
                    ''
                )
            ) : 'note not found';
        };

    // GENERIC FUNCTIONS

    // show :: a -> String
    let show = x => JSON.stringify(x, null, 2),

        // until :: (a -> Bool) -> (a -> a) -> a -> a
        until = (p, f, x) => {
            let v = x;
            while (!p(v)) v = f(v);
            return v;
        };

    // Generic file-system functions

    // doesDirectoryExist :: FilePath -> (FileManager|) -> IO Bool
    let doesDirectoryExist = strPath =>
        $.NSFileManager.defaultManager
        .fileExistsAtPathIsDirectory(
            $(strPath)
            .stringByStandardizingPath, Ref()
        ) && ref[0] === 1,

        // doesFileExist :: String -> Bool
        doesFileExist = strPath => {
            let error = $();

            return (
                $.NSFileManager.defaultManager
                .attributesOfItemAtPathError(
                    ObjC.unwrap($(strPath)
                        .stringByStandardizingPath),
                    error
                ),
                error.code === undefined
            );
        },

        // listDirectory :: FilePath -> [FilePath]
        listDirectory = strPath => ObjC.unwrap(
            $.NSFileManager.defaultManager
            .contentsOfDirectoryAtPathError(
                $(strPath)
                .stringByStandardizingPath, null
            )
        )
        .map(ObjC.unwrap),

        // readFile :: FilePath -> maybe String
        readFile = strPath => {
            let error = $(),
                str = ObjC.unwrap(
                    $.NSString.stringWithContentsOfFileEncodingError(
                        $(strPath)
                        .stringByStandardizingPath,
                        $.NSUTF8StringEncoding,
                        error
                    )
                );
            return error.code ? error.localizedDescription : str;
        },

        // writeFile :: FilePath -> String -> IO ()
        writeFile = (strPath, strText) =>
        $.NSString.alloc.initWithUTF8String(strText)
        .writeToFileAtomicallyEncodingError(
            $(strPath)
            .stringByStandardizingPath, false,
            $.NSUTF8StringEncoding, null
        ),

        scriptFolder = () => {
            var a = Application.currentApplication();
            let sa = (a.includeStandardAdditions = true, a);

            return ObjC.unwrap(
                $(sa.pathTo(this)
                    .toString())
                .stringByDeletingLastPathComponent
            );
        };

    // MAIN – Read, edit, save and use a key:value chain of menu choices.

    // 1. INITIAL CHOICE PATH - retrieved from JSON MRU file, or
    //    from Keyboard Maestro MRU variable
    //      (defaulting otherwise to minimum (qvLibName, qvLibPath) pair
    let strScriptFolder = scriptFolder(),
        strMemoryFile = strScriptFolder + '/' + strMRUName + '.json',
        blnKM = strScriptFolder.indexOf('/private/var') === 0;

    if (blnKM) {
        var kme = Application('Keyboard Maestro Engine'),
            strJSON = kme.getvariable(strMRUName);
    } else {
        var strJSON = (doesFileExist(strMemoryFile) ? (
            readFile(strMemoryFile)
        ) : '');
    }

    let lstMRUPath = strJSON.length > 0 ? JSON.parse(strJSON) : [
        [strQVLibName, strQVLibPath]
    ]

    // 2. CHOICE PATH EDITING by repeated application of (path-length-specific)
    //      menu functions, until the adjusted path is confirmed or dropped
    let intMenus = lstMenus.length,
        mChosenPath = until( // p, f, x – predicate, function, value
            m => (m.cancelled ||
                (m.confirmed && m.path.length > intMenus)),
            m => lstMenus[menuIndexforPathLength(m)](m.path), {
                path: lstMRUPath,
                confirmed: false,
                cancelled: false
            }
        );

    // 3. SERIALISATION of any confirmed [(k, v)] chain to JSON file
    //      (in same folder as script, for use as MRU path in next session)
    if (blnKM) {
        kme.setvariable(strMRUName, {
            to: show(mChosenPath.path)
        })
    } else writeFile(strMemoryFile, show(mChosenPath.path));

    // 4. TRANSLATION & USE of any confirmed [(k, v)] chain.
    //      ( Here, returning concatenated text of selected Quiver snippets )
    let kvs = mChosenPath.path;

    // CONCATENATED SELECTION OF QUIVER SNIPPETS
    return kvs.slice(intMenus) // Notes
        .map(note => kvs.slice(0, intMenus) // Book paths
            .concat([note]) // Individual note [kv] appended to book stem [kv]
            .map(kv => kv[1])) // Values unzipped from key-value pairs
        .map(xs => noteText.apply(null, xs)) // (Libpath, bookUUID, noteUUID)
        .join('\n');
})();

3 Likes

Ver 0.2 above allows for selecting and pasting of multiple snippets – I realised that that was my main use case.
Ver 0.3 (fixed a menu-cancelling bug)
Ver 0.4 fast-tracks the case of a notebook with just one note (skips 2nd menu)

There is one for Alfed and Launchbar. The Launchbar one doesn’t require node, just the Alfred one. I’m currently changing it to not need node either and to be much faster. But, those workflows do more than just paste a snippet, it runs Handlebars against a predefined data set as well and has momentjs based macros. You can even define your own macros to use. You can read about them on my website: customct.com

1 Like

Thanks - look forward to that.

What is the LaunchBar mechanism for selecting multiply, so that you can paste several snippets at a time ?

(I seem to remember something about ‘staging areas’ but, I don’t seem to have internalised it)

Thanks for sharing this -- it sounds very interesting.
But I can't seem to find the actual macro -- did I miss it?
I (and I suspect many others) need to version that works with pre Sierra macOS.

Thanks.

Thanks :slight_smile: it seems to have dropped out in an update … I’ll upload it again now.

Tho it does, I think, require installation of some node_modules libraries ?

Quiver Snippets - default.js

//
// Load libraries used.
//
include("./node_modules/handlebars/dist/handlebars.js");
include("./node_modules/moment/min/moment-with-locales.js");

Thanks for posting the macro.

I have downloaded the macro, and edited the ES5 JXA script to point to my Quiver Library, which is different since I have set Quiver to sync using DropBox.

Running the script in Script Editor in macOS 10.11.4, the script returns nothing.

Here is my change:

    return a.fileExistsAtPathIsDirectory($("/Users/Shared/Dropbox/AppSync/Quiver/Quiver.qvlibrary").stringByStandardizingPath, b) && 1 === b[0] ? "/Users/Shared/Dropbox/AppSync/Quiver/Quiver.qvlibrary" : void 0;

Did I mess something up, or does your script not support sync'd libraries?
I did run the debugger in Safari, but I could not follow your code, so I have no idea why it is not working.

Suggestions?

This ES5 test should reveal whether or not the Dropbox .qvlibrary package can be read in the same way as a local filesystem folder/package.

(If it can, then you should simply be able to replace the default path string with your own.)

    (function () {
        'use strict';

        // doesDirectoryExist :: FilePath -> (FileManager|) -> IO Bool
        function doesDirectoryExist(strPath, fm) {
            let dm = fm || $.NSFileManager.defaultManager,
                ref = Ref();

            return dm
                .fileExistsAtPathIsDirectory(
                    $(strPath)
                    .stringByStandardizingPath, ref
                ) && ref[0] === 1;
        }


        // doesFileExist :: String -> Bool
        function doesFileExist(strPath) {
            var fm = $.NSFileManager.defaultManager,
                error = $();

            return (
                fm.attributesOfItemAtPathError(
                    ObjC.unwrap($(strPath)
                        .stringByStandardizingPath),
                    error
                ),
                error.code === undefined
            );
        }


        // listDirectory :: FilePath -> [FilePath]
        function listDirectory(strPath) {
            var fm = fm || $.NSFileManager.defaultManager;

            return ObjC.unwrap(
                    fm.contentsOfDirectoryAtPathError(
                        $(strPath)
                        .stringByStandardizingPath,
                        null
                    ))
                .map(ObjC.unwrap);
        }

        // readFile :: FilePath -> maybe String
        function readFile(strPath) {
            var error = $(),
                str = ObjC.unwrap(
                    $.NSString.stringWithContentsOfFileEncodingError(
                        $(strPath)
                        .stringByStandardizingPath,
                        $.NSUTF8StringEncoding,
                        error
                    )
                );
            return error.code ? error.localizedDescription : str;
        }


        // bookList :: PathString -> [(nameString, bookuuidString)]
        function bookList(strLibPath) {
            return resList(strLibPath)
        }

        // resList :: PathString -> [(String, UUID)]
        function resList(strFolderPath, strLabelField) {
            let strLabel = strLabelField || 'name';

            return listDirectory(strFolderPath)
                .reduce((a, x) => {
                    let strResPath = strFolderPath + x + '/',
                        strMetaPath = strResPath + 'meta.json';

                    return (doesFileExist(strMetaPath)) ? (
                        a.concat(JSON.parse(readFile(strMetaPath)))
                    ) : a;
                }, [])
                .sort((a, b) => {
                    let strA = a[strLabel].toLowerCase(),
                        strB = b[strLabel].toLowerCase();

                    return strA < strB ? -1 : (strA > strB ? 1 : 0);
                });
        }

        // TEST

        var strPath = '/Users/Shared/Dropbox/AppSync/Quiver/Quiver.qvlibrary';

        //var strPath = '~/Library/Containers/com.happenapps.Quiver' +
        //                '/Data/Library/Application\ Support/Quiver/Quiver.qvlibrary/';

        if (doesDirectoryExist(strPath)) {
            console.log('Exists');

            return bookList(strPath);

        } else {
            return "Wasn't found";
        };

    })();

Ah … not quite ES5 - you will need to search replace let to var, in the above

Seems like there are other non-ES5 syntax. This won't compile:

            return listDirectory(strFolderPath)
                .reduce((a, x) => {
                   var strResPath = strFolderPath + x + '/',
                        strMetaPath = strResPath + 'meta.json';

It is the use of "=>"

Thanks for the macro. Runs fine here on Sierra.

1 Like

Well spotted, the ES 6 expressions,

(a, x) => { ... return someValue }

and

(a, x) => someValue

both rewrite to the ES 5 expression:

function (a, x) { ... return someValue }

They are in the Launchbar bundle. Therefore, users do not need to have
node. The internal JavaScript of Launchbar will run these libraries just
fine. Not true of many Node modules, but these do work.

The Launchbar Quiver action only does one snippet at a time. I’ve never needed more than one at a time, but my snippets are large.

A new version (updated at the top of this thread):

  • Adds MRU memory - opening the Notes sub-menu for the last Quiver notebook from which you pasted, and with the same selections
  • Allows you to move up and down between the books listing and a notes listing, before making a choice of snippet(s) to paste
  • Adds a more legible ES5 version of the JavaScript code, preserving layout and comments (generated by https://babeljs.io/repl )
  • Should make it slightly easier to edit the path of your Quiver library, if it’s not on the default path.

Note that if the script is run from Keyboard Maestro, the MRU memory is stored in a KM variable.
If the script is run stand-alone, it tries to store session memory in a JSON text file (same folder as script).

Rob, I thought I'd give your macro/script another try.
I just downloaded your update.

Unfortunately, the script gets caught in an infinite loop, requiring a ⌘PERIOD to stop in Script Editor 2.8.1 (183.1) on macOS 10.11.6.

I'd appreciate any help/suggestions you can give me (and probably others running El Capitan) to fix this issue.

I don't know if it is due to running in macOS 10.11.6, or due to by Quiver library being in a different location. I did make these changes:

(1). The location of my Quiver Library

    var strQVLibName = 'Default library';
 //JMTX   var strQVLibPath = '~/Library/Containers/com.happenapps.Quiver' + '/Data/Library/Application\ Support/Quiver/Quiver.qvlibrary/';
    
    //--- MY LIB ---
    var strQVLibPath = '/Users/Shared/Dropbox/AppSync/Quiver/Lib/Quiver.qvlibrary';

(2). Make ES5 Compliant
Started with your ES5 version, but had to make a number of additional changes to make it fully compliant (I won't bore you with the changes unless you'd like to know).

(3) Add Debug Code

  • Since it was a runaway, I added code to pause the script, show a dialog, and capture a log.
  • Added function showDialog() and called in 3 places.
  • Each time it is called, it adds to the log (a variable)
  • If you stop the script with "Copy to Clipboard", it does just that and throws an error to stop the script.

###My Script Log
Hopefully this will give you the info to determine how to fix.
I could not figure out exactly what was causing the issue.

>>> START SCRIPT TEST AT Sat Mar 25 2017 19:04:08 GMT-0500 (CDT) <<<

>>>Loop Count = 1<<<

until function

>>>Loop Count = 2<<<

ChosenPath 1st function (m)
 m.path.length: 1
 intMenus: 2
 m.path: Default library,/Users/Shared/Dropbox/AppSync/Quiver/Lib/Quiver.qvlibrary

>>>Loop Count = 3<<<

ChosenPath 2nd function (m)

>>>Loop Count = 4<<<

menuIndexforPathLength: mPath.path: Default library,/Users/Shared/Dropbox/AppSync/Quiver/Lib/Quiver.qvlibrary

>>>Loop Count = 5<<<

ChosenPath 1st function (m)
 m.path.length: 1
 intMenus: 2
 m.path: Default library,/Users/Shared/Dropbox/AppSync/Quiver/Lib/Quiver.qvlibrary

>>>Loop Count = 6<<<

ChosenPath 2nd function (m)

>>>Loop Count = 7<<<

menuIndexforPathLength: mPath.path: Default library,/Users/Shared/Dropbox/AppSync/Quiver/Lib/Quiver.qvlibrary

>>>Loop Count = 8<<<

ChosenPath 1st function (m)
 m.path.length: 1
 intMenus: 2
 m.path: Default library,/Users/Shared/Dropbox/AppSync/Quiver/Lib/Quiver.qvlibrary

###Your Script with My Changes for ES5 and Debug
This is the actual script I have been testing.

/*
###  JMTX NOTES ###

  • I cannot make this work running macOS 10.11.6.
  • Script seems to get in loop it does not exit
    • This seems to be the loop:
      var intMenus = lstMenus.length,
        mChosenPath = until( // p, f, x – predicate, function, value . . .
    • The chooseFromList dialog is NEVER displayed, because lstMenuKeys.length is always 0.
    • I have no idea why.
    
  • Although it was stated that this is the ES5 compliant script, I found it is not.
*/


function scriptFolder() {
    var a = Application.currentApplication();
    var sa = (a.includeStandardAdditions = true, a);

    return ObjC.unwrap($(sa.pathTo(this).toString()).stringByDeletingLastPathComponent);
};


(function () {
    'use strict';

    // Copyright (c) 2016 Robin Trew
    //
    // 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 at end of this file )

    // QUIVER LIBRARY TO USE
    
    // MY Quiver Lib Path:
    //  /Users/Shared/Dropbox/AppSync/Quiver/Lib/Quiver.qvlibrary

    var strQVLibName = 'Default library';
 //JMTX   var strQVLibPath = '~/Library/Containers/com.happenapps.Quiver' + '/Data/Library/Application\ Support/Quiver/Quiver.qvlibrary/';
    
    //--- MY LIB ---
    var strQVLibPath = '/Users/Shared/Dropbox/AppSync/Quiver/Lib/Quiver.qvlibrary';

    var strMRUName = 'quiverMenuMemory';

    // MENU LEVELS

    // Sequence of two menu-path processing functions.
    // (One function for each junction between successive menu levels).

    // Each particular menu function is compiled from the specs here by
    // the higher-order nestedMenu() function. (See below).
    // The value of .menuItems should be a function from a [(k,v)] chain of
    // given length to a list of contextual menu item pairs [(key,value)]
    // (e.g. the books in a chosen library, or the notes in a chosen book)
    // Surplus unconfirmed (k, v) pairs (remembered from previous sessions)
    // are used as initial selections in the displayed menu.
    
    var lstMenus = [{
        title: 'Quiver notebooks',
        msg: 'Choose a notebook:',
        menuItems: function menuItems(lstPath) {
            return resList(lstPath.length ? lstPath[0][1] : defaultLibPath())
                .map(function (dct) {
                    return [dct.name, dct.uuid];
                });
        }
    }, {
        title: 'Quiver notes',
        msg: 'Choose note(s):',
        multiple: true,
        menuItems: function menuItems(lstPath) {
            return resList(lstPath[0][1] + lstPath[1][1] + '.qvnotebook/', 'title')
                .map(function (dct) {
                    return [dct.title, dct.uuid];
                });
        },
        cancelName: 'Back'
    }].map(nestedMenu);

    // GENERIC FUNCTIONS FOR NESTED MENUS

    // nestedMenu() generates a function from a key-value list
    // (remembered choice sequence, retrieved from a JSON file)
    //      to a new key-value sequence (longer, shorter, or adjusted)
    //      wrapped with a confirmation status flag.
    //
    //      Unconfirmed final key-value pairs are read as menu pre-selections
    //      confirmed final key-value pairs are used as menu output.
    //
    //      The second argument to nesteMenu is a menu stage index,
    //      usually provided automatically by Array.map
    //      This is used to derive the length of choice paths that are
    //      handled by particular menu-stage functions.
    //
    // nestedMenu :: {title:String, msg:String, menuItems:([(k, v)] -> {
    //                      path: [(k,v)],
    //                      confirmed: Bool
    //                  }),
    //                  multiple:Bool} -> Int ->
    //                  ( [(k, v)] -> { path: [(k,v)], confirmed: Bool} )
    function nestedMenu(dctSpec, i) {
        var nMenu = (i || 0) + 1;

        return function (lstPath) {

            // find :: (a -> Bool) -> [a] -> Maybe a
            var find = function find(p, xs) {
                for (var i = 0, lng = xs.length; i < lng; i++) {
                    if (p(xs[i])) return xs[i];
                }
                return undefined;
            };

            var menuItems = dctSpec.menuItems,
                lstOptions = menuItems instanceof Array ? menuItems : menuItems(lstPath),
                lstMenuKeys = (lstOptions || [
                    ['x', 0],
                    ['y', 1],
                    ['z', 2]
                ])
                .map(function (x) {
                    return x[0];
                });

            var blnMult = dctSpec.multiple !== undefined ? dctSpec.multiple : false,
                lstPathKeys = (lstPath || [])
                .map(function (x) {
                    return x[0];
                }),
                lngPathKeys = lstPathKeys.length;

            var a = Application('System Events'),
                sa = (a.includeStandardAdditions = true, a);


            var varResult = lstMenuKeys.length ? sa.activate() && sa.chooseFromList(lstMenuKeys, {
                withTitle: dctSpec.title || 'dctSpec.title',
                withPrompt: lstPathKeys.slice(0, nMenu)
                    .join(' > ') + '\n\n' + (dctSpec.msg || 'dctSpec.msg'),
                defaultItems: lngPathKeys > nMenu ? lstPathKeys.slice(blnMult ? nMenu : -1) : lstMenuKeys[0],
                okButtonName: 'OK',
                cancelButtonName: dctSpec.cancelName || 'Cancel',
                multipleSelectionsAllowed: blnMult,
                emptySelectionAllowed: true
            }) : [];


            var lstParentPath = lstPath.slice(0, nMenu);
            return {
                path: varResult !== false ? lstParentPath.concat(varResult.map(function (k) {
                    return find(function (x) {
                        return x[0] === k;
                    }, lstOptions);
                })) : lstParentPath,
                confirmed: varResult !== false,
                cancelled: nMenu === 1 && varResult === false
            };
        };
    }

    // Index of appropriate subMenu, given current path length and status)
    // menuIndexforPathLength  :: MPath [(k, v)] -> Int
    var menuIndexforPathLength = function menuIndexforPathLength(mPath) {
        showDialog("menuIndexforPathLength: mPath.path: " + mPath.path.toString());
        
        var intStages = mPath.path.length,
            intMenus = lstMenus.length;

        // Last menu (if path already full-length)
        // or zero-index the length of the *confirmed* part of the path
        var index = intStages >= intMenus + 1 ? intMenus - 1 : intStages - (mPath.confirmed ? 1 : 2);

        return index < 0 ? 0 : index;
    };

    // QUIVER-SPECIFIC FUNCTIONS

    var defaultLibPath = function defaultLibPath() {
            var strPath = strQVLibPath + (strQVLibPath.charAt(-1) !== '/' ? '/' : '');

            return doesDirectoryExist(strPath) ? strPath : undefined;
        },


        // resList :: PathString -> maybe String -> [(String, UUID)]
        resList = function resList(strFolderPath, strField) {
            var strLabel = strField || 'name';

            return strFolderPath ? listDirectory(strFolderPath)
                .reduce(function (a, x) {
                    var strMetaPath = strFolderPath + x + '/meta.json';

                    return doesFileExist(strMetaPath) ? a.concat(JSON.parse(readFile(strMetaPath))) : a;
                }, [])
                .sort(function (a, b) {
                    var strA = a[strLabel].toLowerCase(),
                        strB = b[strLabel].toLowerCase();

                    return strA < strB ? -1 : strA > strB ? 1 : 0;
                }) : [];
        },


        // noteText :: libpath ->  bookuuid -> noteuuid -> text
        noteText = function noteText(strLibPath, strBookUUID, strNoteUUID) {
            var strNotePath = strLibPath + strBookUUID + '.qvnotebook/' + strNoteUUID + '.qvnote/content.json',
                dctNote = doesFileExist(strNotePath) ? JSON.parse(readFile(strNotePath)) : undefined;

            return dctNote ? dctNote.cells.reduce(function (a, x) {
                return ['code', 'text'].indexOf(x.type) !== -1 ? a + x.data + '\n\n' : a;
            }, '') : 'note not found';
        };

    // GENERIC FUNCTIONS

    // show :: a -> String
    var show = function show(x) {
            return JSON.stringify(x, null, 2);
        },


        // until :: (a -> Bool) -> (a -> a) -> a -> a
        until = function until(p, f, x) {
            showDialog("until function");
            
            
            var v = x;
            while (!p(v)) {
                v = f(v);
            }
            return v;
        };

    // Generic file-system functions

    // doesDirectoryExist :: FilePath -> (FileManager|) -> IO Bool
    var doesDirectoryExist = function doesDirectoryExist(strPath) {
            return $.NSFileManager.defaultManager.fileExistsAtPathIsDirectory($(strPath)
                .stringByStandardizingPath, Ref()) && ref[0] === 1;
        },


        // doesFileExist :: String -> Bool
        doesFileExist = function doesFileExist(strPath) {
            var error = $();

//debugger;


            return $.NSFileManager.defaultManager.attributesOfItemAtPathError(ObjC.unwrap($(strPath)
                .stringByStandardizingPath), error), error.code === undefined;
        },


        // listDirectory :: FilePath -> [FilePath]
        listDirectory = function listDirectory(strPath) {
            return ObjC.unwrap($.NSFileManager.defaultManager.contentsOfDirectoryAtPathError($(strPath)
                    .stringByStandardizingPath, null))
                .map(ObjC.unwrap);
        },


        // readFile :: FilePath -> maybe String
        readFile = function readFile(strPath) {
            var error = $(),
                str = ObjC.unwrap($.NSString.stringWithContentsOfFileEncodingError($(strPath)
                    .stringByStandardizingPath, $.NSUTF8StringEncoding, error));
            return error.code ? error.localizedDescription : str;
        },

        // writeFile :: FilePath -> String -> IO ()
        writeFile = function writeFile(strPath, strText) {
            return $.NSString.alloc.initWithUTF8String(strText)
                .writeToFileAtomicallyEncodingError($(strPath)
                    .stringByStandardizingPath, false, $.NSUTF8StringEncoding, null);
        };
  
/*      
        //##### I HAD TO ADD THIS IN FROM THE ES5 VERSION ######
        
        function scriptFolder() {
          var a = Application.currentApplication();
          var sa = (a.includeStandardAdditions = true, a);
      
          return ObjC.unwrap($(sa.pathTo(this).toString()).stringByDeletingLastPathComponent);
        };
*/
  
  function showDialog(pMsg) {
  
            gloopCount += 1;
            
            var MsgStr    = ">>>Loop Count = " + gloopCount.toString() + "<<<\n\n" + pMsg ;
            var TitleStr  = "Get Quiver Snippet"
            
            gTestLog += "\n\n" + MsgStr;
            

            app.beep()
            var oAns = app.displayDialog(MsgStr,
              {
                withTitle:      TitleStr
                ,withIcon:      "caution"
                ,buttons:       ["Copy to Clipboard","Cancel","OK"]
                ,defaultButton: "OK"
                ,cancelButton:  "Cancel"
              })
              
              if (oAns.buttonReturned !== "OK") {
                app.setTheClipboardTo(gTestLog);
                throw new Error("User Canceled");
              }
              
              return
  
  }
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//    MAIN SCRIPT
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


var app = Application.currentApplication()
app.includeStandardAdditions = true

var gTestLog = ">>> START SCRIPT TEST AT " + (new Date()).toString() + " <<<"
//app.setTheClipboardTo(msgStr);


    // MAIN – Read, edit, save and use a key:value chain of menu choices.

    // 1. INITIAL CHOICE PATH - retrieved from JSON MRU file, or
    //    from Keyboard Maestro MRU variable
    //      (defaulting otherwise to minimum (qvLibName, qvLibPath) pair
    var strScriptFolder = scriptFolder(),
        strMemoryFile = strScriptFolder + '/' + strMRUName + '.json',
        blnKM = strScriptFolder.indexOf('/private/var') === 0;

//debugger;

    if (blnKM) {
        var kme = Application('Keyboard Maestro Engine'),
            strJSON = kme.getvariable(strMRUName);
    } else {
        var strJSON = doesFileExist(strMemoryFile) ? readFile(strMemoryFile) : '';
    }

    var lstMRUPath = strJSON.length > 0 ? JSON.parse(strJSON) : [
        [strQVLibName, strQVLibPath]
    ];

    // 2. CHOICE PATH EDITING by repeated application of (path-length-specific)
    //      menu functions, until the adjusted path is confirmed or dropped
    var gloopCount = 0;
    
    var intMenus = lstMenus.length,
        mChosenPath = until( // p, f, x – predicate, function, value
                    
            function (m) {
              showDialog("ChosenPath 1st function (m)"
                + "\n m.path.length: " + m.path.length
                + "\n intMenus: " + intMenus
                + "\n m.path: " + m.path
                )
              
                return m.cancelled || m.confirmed && m.path.length > intMenus;
            },
            function (m) {
            
                showDialog("ChosenPath 2nd function (m)")
                
                return lstMenus[menuIndexforPathLength(m)](m.path);
            }, {
                path: lstMRUPath,
                confirmed: false,
                cancelled: false
            });

    // 3. SERIALISATION of any confirmed [(k, v)] chain to JSON file
    //      (in same folder as script, for use as MRU path in next session)
    if (blnKM) {
        kme.setvariable(strMRUName, {
            to: show(mChosenPath.path)
        });
    } else writeFile(strMemoryFile, show(mChosenPath.path));

    // 4. TRANSLATION & USE of any confirmed [(k, v)] chain.
    //      ( Here, returning concatenated text of selected Quiver snippets )
    var kvs = mChosenPath.path;

    // CONCATENATED SELECTION OF QUIVER SNIPPETS
    return kvs.slice(intMenus) // Notes
        .map(function (note) {
            return kvs.slice(0, intMenus) // Book paths
                .concat([note]) // Individual note [kv] appended to book stem [kv]
                .map(function (kv) {
                    return kv[1];
                });
        }) // Values unzipped from key-value pairs
        .map(function (xs) {
            return noteText.apply(null, xs);
        }) // (Libpath, bookUUID, noteUUID)
        .join('\n');
})();

Thanks for sharing your macro/script, and your help in resolving this issue.

The copy at the top of this thread is working here on Sierra, but I don’t, alas, have access to an el Capitan system for testing.

( first thought is that an unconstrained loop sounds like an unseen dialog somewhere waiting for user input – perhaps a choose from list control is not getting focus ? )

An update, after some time (Ver 4)

You will need to edit the line:

const strQVLibPath = '~/Dropbox/Quiver/Quiver.qvlibrary/';

at the top of the Execute JavaScript action to match the path of your Quiver library.


CHANGES

  • Thanks to @unlocked2412 for noticing that earlier drafts restricted pasting to code cells – that restriction is now relaxed to allow for pasting markdown and latex cells etc.
  • Shows the number of items in the displayed notebook menu
  • Still has an MRU memory (returns on launch to the notebook and selection(s) of the most recent paste event)
  • Code reorganised and tidied slightly.

Paste one or more snippets from Quiver ver 4.kmmacros (40.4 KB)

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

    // Rob Trew 2020

    // Pasting code snippets from macOS Quiver.app

    // Menu-browsing and leaf selections over a generic
    // (and potentially lazy) tree structure,
    // with MRU memory preserved as json between session.

    // Quiver example 0.10

    // main :: IO ()
    const main = () => {
        const
            fpQuiver = '~/Quiver/Quiver.qvlibrary',
            // Memory of most recent sub-menu selections.
            fpMRU = getTemporaryDirectory() + 'mruQuiver.json';
        return either(constant(''))(
            // Result tuple (menuPath, menuKeys, values)
            tpl => (
                writeFile(fpMRU)(
                    JSON.stringify(
                        Tuple(tpl[0])(tpl[1]),
                        null, 2
                    )
                ),
                // Returned for pasting etc.
                concatenatedNotes(tpl[2])
            )
        )(quiverMenuChoices(fpQuiver)(fpMRU));
    };

    // concatenatedNotes :: [Tree Dict] -> String
    const concatenatedNotes = chosenLeafTrees =>
        intercalate('\n\n')(
            map(x => {
                const ks = x.path
                return either(identity)(
                    dct => unlines(
                        dct.cells.map(cell => cell.data)
                    )
                )(bindLR(
                    readFileLR(
                        `${ks[0]}/${ks[1]}` +
                        `.qvnotebook/${ks[2]}` +
                        '.qvnote/content.json'
                    )
                )(jsonParseLR))
            })(chosenLeafTrees)
        );

    // quiverMenuChoices :: FilePath -> FilePath ->
    // IO [Tree Dict]
    const quiverMenuChoices = fpQuiverDB =>
        fpMRU => mruTreeMenu('Pasting code from Quiver')(
            x => x.title
        )(
            x => x.leaf
        )(
            (() => either(
                constant(Tuple([])([]))
            )(identity)(
                bindLR(readFileLR(fpMRU))(
                    jsonParseLR
                )
            ))()
        )(Node({
            title: 'Quiver',
            path: [fpQuiverDB]
        })(lazyQuiverBooks));

    // lazyQuiverBooks :: {name: String, path: [String]} ->
    // Tree {name::String, uuid::String path::[String]}
    const lazyQuiverBooks = parent =>
        // List of note book children for the
        // parent tree. Evaluated only on demand.
        quiverBookDetailsFromLibraryPath(
            parent.path[0]
        ).map(
            dct => Node(
                Object.assign({}, dct, {
                    path: [...parent.path, dct.uuid],
                    title: dct.name,
                    leaf: false
                })
            )(lazyQuiverNotes)
        );

    // lazyQuiverNotes :: {path::[String], uuid::String, ...} ->
    // Tree {title :: String, uuid :: String}
    const lazyQuiverNotes = parent => {
        const
            fpBook = parent.path[0] +
            `/${parent.uuid}.qvnotebook`;
        return listDirectory(fpBook).flatMap(
            fp => 'meta.json' !== fp ? (
                either(constant([]))(dct => [
                    Node(Object.assign({}, dct, {
                        path: parent.path.concat(dct.uuid),
                        leaf: true
                    }))([])
                ])(bindLR(
                    readFileLR(
                        `${fpBook}/${fp}/meta.json`
                    )
                )(jsonParseLR))
            ) : []
        );
    };

    // quiverBookDetailsFromLibraryPath :: FilePath ->
    // {name :: String, uuid :: String}
    const quiverBookDetailsFromLibraryPath = fpLib =>
        bindLR(getDirectoryContentsLR(fpLib))(
            xs => xs.flatMap(
                x => either(_ => [])(x => [x])(
                    bindLR(
                        readFileLR(
                            `${fpLib}/${x}/meta.json`
                        )
                    )(jsonParseLR)
                )
            )
        );

    // --------------MRU MEMORY FROM LAZY TREE--------------
// mruTreeMenu ::
// (a -> String) ->  // A menu label for the root value of a Node
// ([String], [String]) ->  // Any MRU label path to a sub-menu,
//                          // and any pre-selected labels.
// Tree a ->                // Tree (not necessarily of Strings)
// Either String ([String], [String], [a])
const mruTreeMenu = legend =>
    // Either a message or a:
    // (menuPath, chosenLabels, chosenValues) tuple, in which
    // menuPath :: is a list of strings representing
    // the path of the chosen leaf menu in the whole menu-tree, and
    // chosenLabels :: is a list of the chosen leaf-menu strings
    // chosenValues :: is a list of of the values chosen in that menu.
    // (Of type a – not necessarily String)
    keyFromRoot => leafTest => mruTuple => tree => {
        let blnInit = true; // True only for first recursion (with MRU)
        const
            fKey = compose(keyFromRoot, root),
            pLeaf = compose(leafTest, root);
        const go = mruPair => nodePath => t => {
            const
                children = nest(t),
                strTitle = fKey(t),
                mruPath = dropWhile(eq(strTitle))(
                    mruPair[0]
                ),
                // mruPath = mruPair[0],
                selns = mruPair[1];

            // menuBrowse :: IO () -> (Bool, [String])
            const menuBrowse = () =>
                until(exitOrLeafChoices)(
                    tpl => either(
                        constant(Tuple(false)([]))
                    )(Tuple(true))(
                        bindLR(menuSelectionOrExit())(
                            matchingValues
                        )
                    )
                )(Tuple(true)([]))[1];

            // exitOrLeafChoices :: (Bool, [String]) -> Bool
            const exitOrLeafChoices = tpl =>
                !fst(tpl) || !isNull(snd(tpl));

            // menuSelectionOrExit :: IO () ->
            // Either String [String]
            const menuSelectionOrExit = () => {
                const
                    choices = map(x => {
                        const r = x.root;
                        return Tuple(leafTest(r))(
                            keyFromRoot(r)
                        );
                    })(children),
                    menu = sortBy(menuOrder)(
                        map(x => Tuple(
                            x.startsWith('▶ ') ? (
                                Tuple(x.slice(0, 2))(
                                    x.slice(2)
                                )
                            ) : Tuple('')(x)
                        )(x))(
                            choices.map(tpl => (
                                tpl[0] ? (
                                    ''
                                ) : '▶ '
                            ) + tpl[1])
                        )
                    ).map(snd),
                    blnMultiSeln = 1 < children.reduce(
                        (a, x) => pLeaf(x) ? 1 + a : a,
                        0
                    );
                return blnInit && 0 < mruPath.length &&
                    elem(mruPath[0])(choices.map(snd)) ? (
                        Right([mruPath[0]]) // Any MRU path.
                    ) : showMenuSelectionLR(blnMultiSeln)(
                        1 < nodePath.length ? (
                            'Back'
                        ) : 'Cancel'
                    )(legend)(
                        nodePath.join(' > ') + '\n\n' +
                        menu.length.toString() + ' ' + (
                            blnMultiSeln ? (
                                'choice(s):'
                            ) : 'sub-menus:'
                        )
                    )(menu)( // Any selection MRU.
                        blnInit ? selns : []
                    );
            };

            // matchingValues :: [String] ->
            // Either String [a]
            const matchingValues = ks => {
                const k = ks[0];
                return maybe(Left('Not found: ' + k))(
                    menuResult(ks)
                )(find(
                    compose(eq(k), fKey)
                )(children));
            };

            // menuOrder :: ((String), String) ->
            // ((String), String) -> Ord
            const menuOrder = a => b => {
                // Parent status DESC, then A-Z ASC
                const
                    x = a[0],
                    y = b[0];
                return x[0] < y[0] ? (
                    1
                ) : x[0] > y[0] ? (
                    -1
                ) : x[1] < y[1] ? (
                    -1
                ) : x[1] > y[1] ? 1 : 0
            };

            // menuResult :: String -> Tree a ->
            // Either String ([String], [String], [Tree a])
            const menuResult = chosenKeys => subTree => {
                // (Menu path, labels, values)
                const setChosen = new Set(chosenKeys);
                return Right(
                    isNull(nest(subTree)) ? [Right(
                        TupleN(
                            nodePath, chosenKeys,
                            concatMap(
                                v => {
                                    const r = v.root;
                                    return setChosen.has(
                                        keyFromRoot(r)
                                    ) ? [r] : []
                                }
                            )(sortOn(fKey)(nest(t)))
                        )
                    )] : leafMenuResult(subTree)
                );
            };

            // leafMenuResult :: Tree a ->
            // [Either String ([String], [String], [Tree a])]
            const leafMenuResult = subTree => {
                const
                    chosenLeafKeys = go(
                        Tuple(mruPath.slice(1))(selns)
                    )(nodePath.concat(fKey(subTree)))(
                        subTree
                    );
                return ( // MRU initialisation complete.
                    blnInit = false,
                    chosenLeafKeys
                );
            };
            // Main menu process ...
            return menuBrowse();
        };

        const choices = go(mruTuple)([fKey(tree)])(tree);
        return (0 < choices.length && choices[0]) || (
            Left('User cancelled')
        );
    };

    // showMenuSelectionLR :: Bool -> String -> String ->
    // [String] -> [String] -> Either String [String]
    const showMenuSelectionLR = blnMult =>
        cancelName => title => prompt => xs => selns =>
        0 < xs.length ? (
            (() => {
                const
                    sa = Object.assign(
                        Application('System Events'), {
                            includeStandardAdditions: true
                        }),
                    v = (
                        sa.activate(),
                        sa.chooseFromList(xs, {
                            withTitle: title,
                            withPrompt: prompt,
                            defaultItems: blnMult ? (
                                selns
                            ) : 0 < selns.length ? (
                                selns[0]
                            ) : [],
                            okButtonName: 'OK',
                            cancelButtonName: cancelName,
                            multipleSelectionsAllowed: blnMult,
                            emptySelectionAllowed: false
                        })
                    );
                const pfx = /^▶ /g;
                return Array.isArray(v) ? (
                    Right(v.map(s => s.replace(pfx, '')))
                ) : Left(
                    'User cancelled ' + title + ' menu.'
                );
            })()
        ) : Left(title + ': No items to choose from.');

    // ------------------GENERIC FUNCTIONS------------------
    // https://github.com/RobTrew/prelude-jxa

    // Just :: a -> Maybe a
    const Just = x => ({
        type: 'Maybe',
        Nothing: false,
        Just: x
    });

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

    // Node :: a -> [Tree a] -> Tree a
    const Node = v =>
        // Constructor for a Tree node which connects a
        // value of some kind to a list of zero or
        // more child trees.
        xs => ({
            type: 'Node',
            root: v,
            nest: xs || []
        });

    // Nothing :: Maybe a
    const Nothing = () => ({
        type: 'Maybe',
        Nothing: true,
    });

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

    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
        b => ({
            type: 'Tuple',
            '0': a,
            '1': b,
            length: 2
        });

    // TupleN :: a -> b ...  -> (a, b ... )
    function TupleN() {
        const
            args = Array.from(arguments),
            n = args.length;
        return 1 < n ? Object.assign(
            args.reduce((a, x, i) => Object.assign(a, {
                [i]: x
            }), {
                type: 'Tuple' + (2 < n ? n.toString() : ''),
                length: n
            })
        ) : args[0];
    };

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

    // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
    const compose = (...fs) =>
        x => fs.reduceRight((a, f) => f(a), x);

    // concat :: [[a]] -> [a]
    // concat :: [String] -> String
    const concat = xs =>
        0 < xs.length ? (
            xs.every(x => 'string' === typeof x) ? (
                ''
            ) : []
        ).concat(...xs) : xs;

    // concatMap :: (a -> [b]) -> [a] -> [b]
    const concatMap = f =>
        xs => xs.flatMap(f);

    // constant :: a -> b -> a
    const constant = k =>
        _ => k;

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

    // dropWhile :: (a -> Bool) -> [a] -> [a]
    // dropWhile :: (Char -> Bool) -> String -> String
    const dropWhile = p =>
        xs => {
            const lng = xs.length;
            return 0 < lng ? xs.slice(
                until(i => i === lng || !p(xs[i]))(
                    i => 1 + i
                )(0)
            ) : [];
        };

    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        fr => e => 'Either' === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;

    // elem :: Eq a => a -> [a] -> Bool
    // elem :: Char -> String -> Bool
    const elem = x =>
        xs => {
            const t = xs.constructor.name;
            return 'Array' !== t ? (
                xs['Set' !== t ? 'includes' : 'has'](x)
            ) : xs.some(eq(x));
        };

    // eq (==) :: Eq a => a -> a -> Bool
    const eq = a =>
        // True when a and b are equivalent.
        b => a === b;

    // filter :: (a -> Bool) -> [a] -> [a]
    const filter = f => xs => xs.filter(f);

    // find :: (a -> Bool) -> [a] -> Maybe a
    const find = p => xs => {
        const i = xs.findIndex(p);
        return -1 !== i ? (
            Just(xs[i])
        ) : Nothing();
    };

    // flip :: (a -> b -> c) -> b -> a -> c
    const flip = f =>
        x => y => f(y)(x);

    // fst :: (a, b) -> a
    const fst = tpl =>
        // First member of a pair.
        tpl[0];

    // getDirectoryContentsLR :: FilePath ->
    // Either String IO [FilePath]
    const getDirectoryContentsLR = fp => {
        const
            error = $(),
            xs = $.NSFileManager.defaultManager
            .contentsOfDirectoryAtPathError(
                $(fp).stringByStandardizingPath,
                error
            );
        return xs.isNil() ? (
            Left(ObjC.unwrap(error.localizedDescription))
        ) : Right(ObjC.deepUnwrap(xs))
    };

    // getTemporaryDirectory :: IO FilePath
    const getTemporaryDirectory = () =>
        ObjC.unwrap($.NSTemporaryDirectory());

    // identity :: a -> a
    const identity = x =>
        // The identity function. (`id`, in Haskell)
        x;

    // intercalate :: String -> [String] -> String
    const intercalate = s =>
        // The concatenation of xs
        // interspersed with copies of s.
        xs => xs.join(s);

    // isNull :: [a] -> Bool
    // isNull :: String -> Bool
    const isNull = xs =>
        1 > xs.length;

    // jsonParseLR :: String -> Either String a
    const jsonParseLR = s => {
        try {
            return Right(JSON.parse(s));
        } catch (e) {
            return Left(
                e.message +
                `\n(line:${e.line} col:${e.column})`
            );
        }
    };

    // listDirectory :: FilePath -> [FilePath]
    const listDirectory = fp =>
        ObjC.unwrap(
            $.NSFileManager.defaultManager
            .contentsOfDirectoryAtPathError(
                ObjC.wrap(fp)
                .stringByStandardizingPath,
                null
            ))
        .map(ObjC.unwrap);

    // map :: (a -> b) -> [a] -> [b]
    const map = f =>
        // The list obtained by applying f
        // to each element of xs.
        // (The image of xs under f).
        xs => (
            Array.isArray(xs) ? (
                xs
            ) : xs.split('')
        ).map(f);

    // maybe :: b -> (a -> b) -> Maybe a -> b
    const maybe = v =>
        // Default value (v) if m is Nothing, or f(m.Just)
        f => m => m.Nothing ? v : f(m.Just);

    // nest :: Tree a -> [a]
    const nest = tree => {
        // Allowing for lazy (on-demand) evaluation.
        // If the nest turns out to be a function –
        // rather than a list – that function is applied
        // here to the root, and returns a list.
        const children = tree.nest;
        return 'function' !== typeof children ? (
            children
        ) : children(tree.root);
    };

    // readFileLR :: FilePath -> Either String IO String
    const readFileLR = fp => {
        const
            e = $(),
            ns = $.NSString
            .stringWithContentsOfFileEncodingError(
                $(fp).stringByStandardizingPath,
                $.NSUTF8StringEncoding,
                e
            );
        return ns.isNil() ? (
            Left(ObjC.unwrap(e.localizedDescription))
        ) : Right(ObjC.unwrap(ns));
    };

    // rights :: [Either a b] -> [b]
    const rights = xs =>
        xs.flatMap(
            x => ('Either' === x.type) && (
                undefined !== x.Right
            ) ? [x.Right] : []
        );

    // root :: Tree a -> a
    const root = tree => tree.root;

    // showJSON :: a -> String
    const showJSON = x =>
        // Indented JSON representation of the value x.
        JSON.stringify(x, null, 2);

    // showLog :: a -> IO ()
    const showLog = (...args) =>
        console.log(
            args
            .map(x => JSON.stringify(x, null, 2))
            .join(' -> ')
        );

    // snd :: (a, b) -> b
    const snd = tpl => tpl[1];

    // sort :: Ord a => [a] -> [a]
    const sort = xs => xs.slice()
        .sort((a, b) => a < b ? -1 : (a > b ? 1 : 0));

    // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
    const sortBy = f =>
        xs => xs.slice()
        .sort((a, b) => f(a)(b));

    // sortOn :: Ord b => (a -> b) -> [a] -> [a]
    const sortOn = f =>
        // Equivalent to sortBy(comparing(f)), but with f(x)
        // evaluated only once for each x in xs.
        // ('Schwartzian' decorate-sort-undecorate).
        xs => xs.map(
            x => [f(x), x]
        ).sort(
            (a, b) => a[0] < b[0] ? -1 : (a[0] > b[0] ? 1 : 0)
        ).map(x => x[1]);

    // treeFromJSON :: JSON String -> Tree a
    const treeFromJSON = json => {
        // Assumes a recursive [root, nest] JSON format,
        // in which `root` is a parseable value string, and `nest`
        // is a possibly empty list of [`root`, `nest`] pairs.
        const go = ([root, nest]) =>
            Node(root)(nest.map(go));
        return go(JSON.parse(json));
    };

    // unlines :: [String] -> String
    const unlines = xs =>
        // A linefeed-delimited string constructed
        // from the list of lines in 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;
    };

    // writeFile :: FilePath -> String -> IO ()
    const writeFile = fp => s =>
        $.NSString.alloc.initWithUTF8String(s)
        .writeToFileAtomicallyEncodingError(
            $(fp)
            .stringByStandardizingPath, false,
            $.NSUTF8StringEncoding, null
        );

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

Updated again above to use a more generic tree-browsing function. (The same JS code as in the macro, posted on these pages, for pasting MD links from selected DEVONthink 3 records).