Menu of Links Kept in Text File, or in Front TaskPaper Document

My working TaskPaper document often contains a number of MD-formatted links to local and web resources which I need to click quite often during a working session.

I also have various project files, each of which contains a number of (markdown formatted) links to various things that I need.

Here is a macro for showing a menu of links in response to some trigger like a hotkey.

It's just a lazy alternative to opening the file and hunting through it for the links.

(The menu will, of course, open any links that are selected).

It can be configured either to:

  • List the (sorted) labels of all the MD links found anywhere in the front TaskPaper document, or
  • List the (sorted) labels of all the MD links found anywhere in a text file specified by by a filePath in KM variable.

Here are two copies of this same macro, one copy configured for the TaskPaper front document, and the other for a given file path:

Sorted menus of links in a file.kmmacros (73.5 KB)

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

    // Rob Trew @2020

    // Menu of the labels of any links 
    // (assumed to be in MD format) in the front
    // TaskPaper document, or in a file at the filePath 
    // specified by a Keyboard Maestro variable.

    // If one or more labels are chosen in the menu,
    // the corresponding links are opened.


    // --------------------- SETTINGS ----------------------

    // EITHER a menu of links in the TP3 front document,
    const showLinksInTaskPaperFrontDoc = true;

    // OR a menu of links in a file at a given path,
    // specified by a Keyboard Maestro variable.

    // e.g: '~/projects/activeProjects.taskpaper'
    const filePathKMVarName = 'linkListFilePath';


    // --------------------- MENU CODE ---------------------
    // main :: IO ()
    const main = () =>
        either(
            msg => msg.startsWith('User cancelled') ? (
                msg
            ) : alert('Link menu')(msg)
        )(openLinks)(
            bindLR(
                showLinksInTaskPaperFrontDoc ? (
                    taskPaperFrontDocFilePathLR()
                ) : filePathFromKMVariableLR(
                    filePathKMVarName
                )
            )(fp => {
                const menuKVs = mdLinkValuesInFile(fp);
                return 0 < menuKVs.length ? (
                    bindLR(
                        showMenuLR(true)('Links')(
                            menuKVs.map(x => x.label)
                        )
                    )(compose(
                        Right,
                        menuChoiceValues(
                            menuKVs
                        )('label')('link')
                    ))
                ) : Left('No links found in document.');
            })
        );



    // ------------ FRONT DOCUMENT IN TASKPAPER ------------

    // taskPaperFrontDocFilePathLR :: Either String FilePath
    const taskPaperFrontDocFilePathLR = () => {
        const
            tp = Application('TaskPaper'),
            ds = tp.documents;
        return 0 < ds.length ? (
            Right(ds.at(0).file().toString())
        ) : Left('No document found in TaskPaper');
    };


    // ---- FILEPATH GIVEN IN KEYBOARD MAESTRO VARIABLE ----
    const filePathFromKMVariableLR = kmVarName => {
        const
            fp = Application('Keyboard Maestro Engine')
            .getvariable(kmVarName);
        return Boolean(fp) ? (() => {
            const fpPath = filePath(fp);
            return doesFileExist(fpPath) ? (
                Right(fpPath)
            ) : Left('No file found at: ' + fpPath);
        })() : Left(
            'No value found for KM variable: "' + (
                kmVarName + '"'
            )
        );
    };


    // ----------- CHOICE OF LINKS IN GIVEN FILE -----------

    // mdLinkValuesInFile :: FilePath -> 
    // [{label :: String, link :: String }]
    const mdLinkValuesInFile = fp =>
        sortBy(comparing(x => x.label))(
            lines(readFile(fp)).flatMap(
                x => x.includes('](') ? (
                    parse(mdLinkParse())(strip(x))
                ) : []
            ).map(fst)
        );

    // openLinks :: [URL String] -> IO [URL String]
    const openLinks = urls => {
        const
            sa = Object.assign(
                Application.currentApplication(), {
                    includeStandardAdditions: true
                });
        return urls.map(x => (
            sa.openLocation(x),
            x
        ));
    };

    // menuChoiceValues :: [Dict a] ->
    // String -> String -> [String] -> [a]
    const menuChoiceValues = menuKVs =>
        // A map from a list of keys to a list of values,
        // given a list of dictionaries, 
        // with their label and value keys,
        // and some subset of label keys.
        labelKey => valueKey => ks => {
            const
                dct = menuKVs.reduce(
                    (a, x) => Object.assign(
                        a, {
                            [x[labelKey]]: x[valueKey]
                        }
                    ), {}
                );
            return ks.flatMap(k => {
                const v = dct[k];
                return void 0 !== v ? (
                    [v]
                ) : [];
            });
        };

    // ------------------- PARSING LINKS -------------------

    // mdLinkParse :: () -> 
    // Parser {title :: String, link :: String}
    const mdLinkParse = () =>
        bindP(
            char('[')
        )(_ => bindP(
            many(noneOf(']'))
        )(title => bindP(
            string('](')
        )(_ => bindP(
            many(noneOf(')'))
        )(link => pureP({
            label: title.join(''),
            link: link.join('')
        })))));

    // ------------ GENERIC PARSER COMBINATORS -------------

    // Parser :: String -> [(a, String)] -> Parser a
    const Parser = f =>
        // A function lifted into a Parser object.
        ({
            type: 'Parser',
            parser: f
        });


    // altP (<|>) :: Parser a -> Parser a -> Parser a
    const altP = p =>
        // p, or q if p doesn't match.
        q => Parser(s => {
            const xs = parse(p)(s);
            return 0 < xs.length ? (
                xs
            ) : parse(q)(s);
        });


    // apP <*> :: Parser (a -> b) -> Parser a -> Parser b
    const apP = pf =>
        // A new parser obtained by the application 
        // of a Parser-wrapped function,
        // to a Parser-wrapped value.
        p => Parser(
            s => parse(pf)(s).flatMap(
                vr => parse(
                    fmapP(vr[0])(p)
                )(vr[1])
            )
        );


    // bindP (>>=) :: Parser a -> 
    // (a -> Parser b) -> Parser b
    const bindP = p =>
        // A new parser obtained by the application of 
        // a function to a Parser-wrapped value.
        // The function must enrich its output, lifting it 
        // into a new Parser.
        // Allows for the nesting of parsers.
        f => Parser(
            s => parse(p)(s).flatMap(
                tpl => parse(f(tpl[0]))(tpl[1])
            )
        );


    // char :: Char -> Parser Char
    const char = x =>
        // A particular single character.
        satisfy(c => x == c);


    // fmapP :: (a -> b) -> Parser a -> Parser b  
    const fmapP = f =>
        // A new parser derived by the structure-preserving 
        // application of f to the value in p.
        p => Parser(
            s => parse(p)(s).flatMap(
                vr => Tuple(f(vr[0]))(vr[1])
            )
        );


    // liftA2P :: (a -> b -> c) -> 
    // Parser a -> Parser b -> Parser c
    const liftA2P = op =>
        // The binary function op, lifted
        // to a function over two parsers.
        p => apP(fmapP(op)(p));


    // many :: Parser a -> Parser [a]
    const many = p => {
        // Zero or more instances of p.
        // Lifts a parser for a simple type of value 
        // to a parser for a list of such values.
        const some_p = p =>
            liftA2P(
                x => xs => [x].concat(xs)
            )(p)(many(p));
        return Parser(
            s => parse(
                0 < s.length ? (
                    altP(some_p(p))(pureP([]))
                ) : pureP([])
            )(s)
        );
    };

    // noneOf :: String -> Parser Char
    const noneOf = s =>
        // Any character not found in the
        // exclusion string.
        satisfy(c => !s.includes(c));


    // parse :: Parser a -> String -> [(a, String)]
    const parse = p =>
        // The result of parsing s with p.
        s => {
            // showLog('s', s)
            return p.parser([...s]);
        };


    // pureP :: a -> Parser a
    const pureP = x =>
        // The value x lifted, unchanged, 
        // into the Parser monad.
        Parser(s => [Tuple(x)(s)]);


    // satisfy :: (Char -> Bool) -> Parser Char
    const satisfy = test =>
        // Any character for which the 
        // given predicate returns true.
        Parser(
            s => 0 < s.length ? (
                test(s[0]) ? [
                    Tuple(s[0])(s.slice(1))
                ] : []
            ) : []
        );


    // sequenceP :: [Parser a] -> Parser [a]
    const sequenceP = ps =>
        // A single parser for a list of values, derived
        // from a list of parsers for single values.
        Parser(
            s => ps.reduce(
                (a, q) => a.flatMap(
                    vr => parse(q)(snd(vr)).flatMap(
                        first(xs => fst(vr).concat(xs))
                    )
                ),
                [Tuple([])(s)]
            )
        );


    // string :: String -> Parser String
    const string = s =>
        // A particular string.
        fmapP(cs => cs.join(''))(
            sequenceP([...s].map(char))
        );


    // ------------------------ JXA ------------------------

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


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

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


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


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


    // comparing :: (a -> b) -> (a -> a -> Ordering)
    const comparing = f =>
        x => y => {
            const
                a = f(x),
                b = f(y);
            return a < b ? -1 : (a > b ? 1 : 0);
        };

    // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
    const compose = (...fs) =>
        // A function defined by the right-to-left
        // composition of all the functions in fs.
        fs.reduce(
            (f, g) => x => f(g(x)),
            x => x
        );

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

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


    // filePath :: String -> FilePath
    const filePath = s =>
        // The given file path with any tilde expanded
        // to the full user directory path.
        ObjC.unwrap(ObjC.wrap(s)
            .stringByStandardizingPath);


    // first :: (a -> b) -> ((a, c) -> (b, c))
    const first = f =>
        // A simple function lifted to one which applies
        // to a tuple, transforming only its first item.
        xy => Tuple(f(xy[0]))(
            xy[1]
        );


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


    // lines :: String -> [String]
    const lines = s =>
        // A list of strings derived from a single
        // newline-delimited string.
        0 < s.length ? (
            s.split(/[\r\n]/)
        ) : [];


    // list :: StringOrArrayLike b => b -> [a]
    const list = xs =>
        // xs itself, if it is an Array,
        // or an Array derived from xs.
        Array.isArray(xs) ? (
            xs
        ) : Array.from(xs || []);


    // readFile :: FilePath -> IO String
    const readFile = fp => {
        // The contents of a text file at the
        // path file fp.
        const
            e = $(),
            ns = $.NSString
            .stringWithContentsOfFileEncodingError(
                $(fp).stringByStandardizingPath,
                $.NSUTF8StringEncoding,
                e
            );
        return ObjC.unwrap(
            ns.isNil() ? (
                e.localizedDescription
            ) : ns
        );
    };

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


    // showMenuLR :: Bool -> String -> [String] -> 
    // Either String [String]
    const showMenuLR = blnMult =>
        title => xs => 0 < xs.length ? (() => {
            const sa = Object.assign(
                Application('System Events'), {
                    includeStandardAdditions: true
                });
            sa.activate();
            const v = sa.chooseFromList(xs, {
                withTitle: title,
                withPrompt: 'Select' + (
                    blnMult ? (
                        ' one or more of ' +
                        xs.length.toString()
                    ) : ':'
                ),
                defaultItems: xs[0],
                okButtonName: 'OK',
                cancelButtonName: 'Cancel',
                multipleSelectionsAllowed: blnMult,
                emptySelectionAllowed: false
            });
            return Array.isArray(v) ? (
                Right(v)
            ) : Left('User cancelled ' + title + ' menu.');
        })() : Left(title + ': No items to choose from.');


    // snd :: (a, b) -> b
    const snd = tpl =>
        // Second member of a pair.
        tpl[1];


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


    // strip :: String -> String
    const strip = s =>
        s.trim();

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

Thanks, this is useful. But, his works only if I don't use the -- as the starting point for a task. In Takspaper you have to press - and space for it to be marked as a task. So, is there a way around to achieve this?

You want to mark links as 'tasks' rather than notes ?

You would probably need to show me, between

```
triple backticks to preserve literal text format
```

a specific example of the kind of (MD link-containing) TaskPaper outline which you have in mind.

I’ve got links or todo items like this
Dan Oliver - Myrmikan Capital - Supply Side

One on One with A and Z #9: The Big Questions around Remote Work — a16z Live — Overcast

Now if I specify them as tasks, they don’t show up. I want them to - these are MD formatted as in xyz

OK, but without the triple backticks I mentioned, we can't see actual pattern of the TaskPaper text – the wiki software renders them as HTML.

You need to show us this kind of thing:

[Menu of links kept in text file](https://forum.keyboardmaestro.com/t/menu-of-links-kept-in-text-file-or-in-front-taskpaper-document/19301/3)

,,,[misc](https://www.bloomberg.com/asia?sref=XA0RNUr9
I’ve left the last bracket open, so that the formatting doesn’t render

You want to have three leading commas ?

so that the formatting doesn’t render

To prevent formatting from rendering, you type three backticks on a preceding line,

```
Markdown between 'fenced' triple backticks is not rendered.
```

and three backticks on a following line

I can’t find the back tic, sorry about that,
[Dan Oliver - Myrmikan Capital - Supply Side] (Dan Oliver - Myrmikan Capital - Supply Side)

[One on One with A and Z #9: The Big Questions around Remote Work — a16z Live — Overcast] (One on One with A and Z #9: The Big Questions around Remote Work — a16z Live — Overcast)

These are sample links

[Dan Oliver - Myrmikan Capital - Supply Side](https://supplysidepartners.com/dan-oliver-myrmikan-capital/)

Here is a mapping of the backtick character to ⌥' (option and single quote)

Type backtick.kmmacros (1.5 KB)

I think I got it, hope so at least, so what changes do I make to the macro?

That example should already work ...

( it appears to be just an MD link on its own line, and that's working here)

Here is a version of the script (JS source at end of this post) which adds a further option value, near the start:

    // If this is false, links with text or hyphens
    // before the opening [label] are ignored
    const blnIncludePrefixed = false;

If you edit the value of blnIncludePrefixed to true, then in-line MD links, including links with hyphen prefixes, should be included in the menu.

When blnIncludePrefixed = false then only links without printable prefixes (only links preceded by white space or the start of a line) will be included.

Note that this version will still only take the first link found in any given line, even if blnIncludePrefixed = true.

It would be possible to write a variant which picked up any number of MD links from a single line or paragraph, but this is not that variant : - )

Expand disclosure triangle to view JS Source
(() => {
    "use strict";

    // Rob Trew @2020, @2021

    // ver 0.02

    // Menu of the labels of any links
    // (assumed to be in MD format) in the front
    // TaskPaper document, or in a file at the filePath
    // specified by a Keyboard Maestro variable.

    // If one or more labels are chosen in the menu,
    // the corresponding links are opened.


    // -------------------- SETTINGS ---------------------

    // EITHER a menu of links in the TP3 front document,
    const showLinksInTaskPaperFrontDoc = true;

    // OR a menu of links in a file at a given path,
    // specified by a Keyboard Maestro variable.

    // e.g: '~/projects/activeProjects.taskpaper'
    const filePathKMVarName = "linkListFilePath";

    // If this is false, links with text or hyphens
    // before the opening [label] are ignored
    const blnIncludePrefixed = false;


    // -------------------- MENU CODE --------------------
    // main :: IO ()
    const main = () =>
        either(
            msg => msg.startsWith("User cancelled") ? (
                msg
            ) : alert("Link menu")(msg)
        )(openLinks)(
            bindLR(
                showLinksInTaskPaperFrontDoc ? (
                    taskPaperFrontDocFilePathLR()
                ) : filePathFromKMVariableLR(
                    filePathKMVarName
                )
            )(fp => {
                const
                    menuKVs = mdLinkValuesInFile(
                        blnIncludePrefixed
                    )(fp);

                return 0 < menuKVs.length ? (
                    bindLR(
                        showMenuLR(true)("Links")(
                            "Choose link(s):"
                        )(menuKVs[0].title)(
                            menuKVs.map(x => x.title)
                        )
                    )(
                        compose(
                            Right,
                            menuChoiceValues(
                                menuKVs
                            )("title")("link")
                        )
                    )
                ) : Left("No links found in document.");
            })
        );


    // ----------- FRONT DOCUMENT IN TASKPAPER -----------

    // taskPaperFrontDocFilePathLR :: Either String FilePath
    const taskPaperFrontDocFilePathLR = () => {
        const
            tp = Application("TaskPaper"),
            ds = tp.documents;

        return 0 < ds.length ? (
            Right(`${ds.at(0).file()}`)
        ) : Left("No document found in TaskPaper");
    };


    // --- FILEPATH GIVEN IN KEYBOARD MAESTRO VARIABLE ---
    const filePathFromKMVariableLR = kmVarName => {
        const
            fp = Application("Keyboard Maestro Engine")
            .getvariable(kmVarName);

        return Boolean(fp) ? (() => {
            const fpPath = filePath(fp);

            return doesFileExist(fpPath) ? (
                Right(fpPath)
            ) : Left(`No file found at: ${fpPath}`);
        })() : Left(
            `No value found for KM variable: "${kmVarName}"`
        );
    };


    // ---------- CHOICE OF LINKS IN GIVEN FILE ----------

    // mdLinkValuesInFile :: Bool -> FilePath ->
    // [{label :: String, link :: String }]
    const mdLinkValuesInFile = stripPrefix =>
        fp => sortBy(comparing(x => x.label))(
            lines(readFile(fp)).flatMap(
                x => x.includes("](") ? (
                    parse(mdLinkP())(
                        (
                            stripPrefix ? s => {
                                const mbi = s.indexOf("[");

                                return s.slice(
                                    0 <= mbi ? (
                                        mbi
                                    ) : 0
                                );
                            } : strip
                        )(x)
                    )
                ) : []
            )
            .map(fst)
        );

    // openLinks :: [URL String] -> IO [URL String]
    const openLinks = urls => {
        const
            sa = Object.assign(
                Application.currentApplication(), {
                    includeStandardAdditions: true
                });

        return urls.map(x => (
            sa.openLocation(x),
            x
        ));
    };

    // menuChoiceValues :: [Dict a] ->
    // String -> String -> [String] -> [a]
    const menuChoiceValues = menuKVs =>
        // A map from a list of keys to a list of values,
        // given a list of dictionaries,
        // with their label and value keys,
        // and some subset of label keys.
        labelKey => valueKey => ks => {
            const
                dct = menuKVs.reduce(
                    (a, x) => Object.assign(
                        a, {
                            [x[labelKey]]: x[valueKey]
                        }
                    ), {}
                );

            return ks.flatMap(k => {
                const v = dct[k];

                return void 0 !== v ? (
                    [v]
                ) : [];
            });
        };

    // ------------------ PARSING LINKS ------------------

    // mdLinkP :: () -> Parser Dict
    const mdLinkP = () =>
        thenBindP(
            char("[")
        )(
            takeWhileP(ne("]"))
        )(title => thenBindP(
            string("](")
        )(
            takeWhileP(ne(")"))
        )(link => thenP(
            char(")")
        )(
            pureP({
                title,
                link
            })
        )));

    // ----------- GENERIC PARSER COMBINATORS ------------

    // Parser :: String -> [(a, String)] -> Parser a
    const Parser = f =>
        // A function lifted into a Parser object.
        ({
            type: "Parser",
            parser: f
        });


    // altP (<|>) :: Parser a -> Parser a -> Parser a
    const altP = p =>
        // p, or q if p doesn't match.
        q => Parser(s => {
            const xs = parse(p)(s);

            return 0 < xs.length ? (
                xs
            ) : parse(q)(s);
        });


    // apP <*> :: Parser (a -> b) -> Parser a -> Parser b
    const apP = pf =>
        // A new parser obtained by the application
        // of a Parser-wrapped function,
        // to a Parser-wrapped value.
        p => Parser(
            s => parse(pf)(s).flatMap(
                vr => parse(
                    fmapP(vr[0])(p)
                )(vr[1])
            )
        );


    // char :: Char -> Parser Char
    const char = x =>
        // A particular single character.
        satisfy(c => x === c);


    // fmapP :: (a -> b) -> Parser a -> Parser b
    const fmapP = f =>
        // A new parser derived by the structure-preserving
        // application of f to the value in p.
        p => Parser(
            s => parse(p)(s).flatMap(
                first(f)
            )
        );


    // liftA2P :: (a -> b -> c) ->
    // Parser a -> Parser b -> Parser c
    const liftA2P = op =>
        // The binary function op, lifted
        // to a function over two parsers.
        p => apP(fmapP(op)(p));


    // many :: Parser a -> Parser [a]
    const many = p => {
        // Zero or more instances of p.
        // Lifts a parser for a simple type of value
        // to a parser for a list of such values.
        const someP = q =>
            liftA2P(
                x => xs => [x].concat(xs)
            )(q)(many(q));

        return Parser(
            s => parse(
                0 < s.length ? (
                    altP(someP(p))(pureP([]))
                ) : pureP([])
            )(s)
        );
    };


    // parse :: Parser a -> String -> [(a, String)]
    const parse = p =>
        // The result of parsing s with p.
        s => p.parser([...s]);


    // pureP :: a -> Parser a
    const pureP = x =>
        // The value x lifted, unchanged,
        // into the Parser monad.
        Parser(s => [Tuple(x)(s)]);


    // satisfy :: (Char -> Bool) -> Parser Char
    const satisfy = test =>
        // Any character for which the
        // given predicate returns true.
        Parser(
            s => 0 < s.length ? (
                test(s[0]) ? [
                    Tuple(s[0])(s.slice(1))
                ] : []
            ) : []
        );


    // sequenceP :: [Parser a] -> Parser [a]
    const sequenceP = ps =>
        // A single parser for a list of values, derived
        // from a list of parsers for single values.
        Parser(
            s => ps.reduce(
                (a, q) => a.flatMap(
                    vr => parse(q)(snd(vr)).flatMap(
                        first(xs => fst(vr).concat(xs))
                    )
                ),
                [Tuple([])(s)]
            )
        );


    // string :: String -> Parser String
    const string = s =>
        // A particular string.
        fmapP(cs => cs.join(""))(
            sequenceP([...s].map(char))
        );


    // takeWhileP :: (Char -> Bool) -> Parser String
    const takeWhileP = p =>
        // The largest prefix in which p is
        // true over all the characters.
        Parser(
            compose(
                pureList,
                first(concat),
                span(p)
            )
        );


    // thenBindP :: Parser a -> Parser b ->
    // (b -> Parser c) Parser c
    const thenBindP = o =>
        // A combination of thenP and bindP in which a
        // preliminary  parser consumes text and discards
        // its output, before any output of a subsequent
        // parser is bound.
        p => f => Parser(
            s => parse(o)(s).flatMap(
                vr => parse(p)(vr[1]).flatMap(
                    tpl => parse(f(tpl[0]))(tpl[1])
                )
            )
        );


    // thenP (>>) :: Parser a -> Parser b -> Parser b
    const thenP = o =>
        // A composite parser in which o just consumes text
        // and then p consumes more and returns a value.
        p => Parser(
            s => parse(o)(s).flatMap(
                vr => parse(p)(vr[1])
            )
        );

    // ----------------------- JXA -----------------------

    // 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"
                }),
                s
            );
        };


    // doesFileExist :: FilePath -> IO Bool
    const doesFileExist = fp => {
        const ref = Ref();

        return $.NSFileManager.defaultManager
            .fileExistsAtPathIsDirectory(
                $(fp)
                .stringByStandardizingPath, ref
            ) && 1 !== ref[0];
    };


    // filePath :: String -> FilePath
    const filePath = s =>
        // The given file path with any tilde expanded
        // to the full user directory path.
        ObjC.unwrap(ObjC.wrap(s)
            .stringByStandardizingPath);


    // readFile :: FilePath -> IO String
    const readFile = fp => {
        // The contents of a text file at the
        // filepath fp.
        const
            e = $(),
            ns = $.NSString
            .stringWithContentsOfFileEncodingError(
                $(fp).stringByStandardizingPath,
                $.NSUTF8StringEncoding,
                e
            );

        return ObjC.unwrap(
            ns.isNil() ? (
                e.localizedDescription
            ) : ns
        );
    };


    // showMenuLR :: Bool -> String -> String ->
    // [String] -> String -> Either String [String]
    const showMenuLR = blnMult =>
        // An optionally multi-choice menu, with
        // a given title and prompt string.
        // Listing the strings in xs, with
        // the string `selected` pre-selected
        // if found in xs.
        menuTitle => prompt => selected => xs =>
        0 < xs.length ? (() => {
            const sa = Object.assign(
                Application("System Events"), {
                    includeStandardAdditions: true
                });

            sa.activate();

            const v = sa.chooseFromList(xs, {
                withTitle: menuTitle,
                withPrompt: prompt,
                defaultItems: xs.includes(selected) ? (
                    [selected]
                ) : [xs[0]],
                okButtonName: "OK",
                cancelButtonName: "Cancel",
                multipleSelectionsAllowed: blnMult,
                emptySelectionAllowed: false
            });

            return Array.isArray(v) ? (
                Right(v)
            ) : Left(`User cancelled ${menuTitle} menu.`);
        })() : Left(`${menuTitle}: No items to choose from.`);


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

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


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


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


    // comparing :: (a -> b) -> (a -> a -> Ordering)
    const comparing = f =>
        x => y => {
            const
                a = f(x),
                b = f(y);

            return a < b ? -1 : (a > b ? 1 : 0);
        };


    // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
    const compose = (...fs) =>
        // A function defined by the right-to-left
        // composition of all the functions in fs.
        fs.reduce(
            (f, g) => x => f(g(x)),
            x => x
        );


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


    // 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 => "Left" in e ? (
            fl(e.Left)
        ) : fr(e.Right);


    // first :: (a -> b) -> ((a, c) -> (b, c))
    const first = f =>
        // A simple function lifted to one which applies
        // to a tuple, transforming only its first item.
        xy => Tuple(f(xy[0]))(
            xy[1]
        );


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


    // identity :: a -> a
    const identity = x =>
        // The identity function.
        x;


    // lines :: String -> [String]
    const lines = s =>
        // A list of strings derived from a single
        // newline-delimited string.
        0 < s.length ? (
            s.split(/[\r\n]/u)
        ) : [];


    // list :: StringOrArrayLike b => b -> [a]
    const list = xs =>
        // xs itself, if it is an Array,
        // or an Array derived from xs.
        Array.isArray(xs) ? (
            xs
        ) : Array.from(xs || []);


    // ne :: a -> a -> Bool
    const ne = a =>
        b => a !== b;


    // pureList :: a -> [a]
    const pureList = x => [x];


    // showLog :: a -> IO ()
    const showLog = (...args) =>
        // eslint-disable-next-line no-console
        console.log(
            args
            .map(JSON.stringify)
            .join(" -> ")
        );


    // snd :: (a, b) -> b
    const snd = tpl =>
        // Second member of a pair.
        tpl[1];


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


    // span :: (a -> Bool) -> [a] -> ([a], [a])
    const span = p =>
        // Longest prefix of xs consisting of elements which
        // all satisfy p, tupled with the remainder of xs.
        xs => {
            const i = xs.findIndex(x => !p(x));

            return -1 !== i ? (
                Tuple(xs.slice(0, i))(
                    xs.slice(i)
                )
            ) : Tuple(xs)([]);
        };


    // strip :: String -> String
    const strip = s =>
        s.trim();

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

Sorry about my ignorance. I'll just copy and paste the sample links here, tell me wha to do

- [Amazon.com: TAPE SUCKS: Inside Data Domain, A Silicon Valley Growth Story eBook: Slootman, Frank: Kindle Store](https://www.amazon.com/TAPE-SUCKS-Inside-Domain-Silicon-ebook/dp/B004XMXYX6) - mentioned by Patrick O'Shaughnessy in his interview with Rich Barton and Brad Gerstner

- [Accounting for Growth: Terry Smith: 8601416750460: Amazon.com: Books](https://www.amazon.com/Accounting-Growth-Terry-Smith/dp/0712675949) #[[Terry Smith]]

- [Amy Klobuchar is coming for the App Store tax - The Verge](https://www.theverge.com/2021/4/27/22404493/amy-klobuchar-interview-antitrust-book-hearings-big-tech)

Thanks, that's much more helpful.

Here is a version with the settings in the source code of the Execute JavaScript for Automation action set for:

  • front TaskPaper document (rather than named path)
  • including MD Links which are preceded by printing characters (text, hyphens etc) rather than filtering them out

Menu of MD links in TaskPaper front document.kmmacros (38.8 KB)

Settings used in JS action:

// -------------------- SETTINGS ---------------------

// EITHER a menu of links in the TP3 front document,
const showLinksInTaskPaperFrontDoc = true;

// OR a menu of links in a file at a given path,
// specified by a Keyboard Maestro variable.

// e.g: '~/projects/activeProjects.taskpaper'
const filePathKMVarName = "linkListFilePath";

// If this is false, links with text or hyphens
// before the opening [label] are ignored
const blnIncludePrefixed = true;

Thanks - exactly what I wanted. And I am sorry for the dumb behaviour, I don't code, that is the problem. This script of yours has multiple utilities and I was wondering if you you can tell me what this one is used for - 'Menu of MD links in File specified by path'.

If you keep a file containing a set of current working links, that version can always open the links in a specific file, (specified by file-path in a KM variable, and it doesn't require TaskPaper to be running)

(rather than opening the links in the front document that happens to be open in TaskPaper)

Got it - thanks