Get quoted and bracketed text from clipboard

I'm trying to create a macro that collects all text (single words and word groups) between several kinds of quotation marks and round brackets:

Zwölf “Boxkämpfer” jagen ‘Viktor’ zum „Spaß‟ quer über den (Sylter) Deich.

I'm stuck at the point where I can save the found words/word groups to variables and to list them, e.g. via the Prompt with list action (but any other way to display and select them would be fine too).

The Prompt with list could show:
a - "Boxkämpfer"
b - "Viktor"
c - "Spaß"
d - (Sylter)
etc.

When I press the leading letter (or number), the selected variable is typed (or pasted) to an editor.

How can I do this?


Get quoted and bracketet text from clipboard.kmmacros (2.7 KB)

I don't know whether this kind of thing matches what you are up to:

Choice of quoted tokens.kmmacros (6.4 KB)

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

    // Lines of quoted strings only.

    const main = () => {
        const quoteChars = "“”‘’„‟()";

        return groupBy(
                on(eq)(
                    c => quoteChars.includes(c)
                )
            )(
                " " + Application("Keyboard Maestro Engine")
                .getvariable("quotedSource")
            )
            .reduce(
                ([s, xs], cs, i) => 0 !== i % 4 ? (
                    [`${s}${cs.join("")}`, xs]
                ) : Boolean(s) ? (
                    ["", xs.concat(s)]
                ) : [s, xs],
                ["", []]
            )[1]
            .join("\n");
    };

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

    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
        // A pair of values, possibly of
        // different types.
        b => ({
            type: "Tuple",
            "0": a,
            "1": b,
            length: 2,
            *[Symbol.iterator]() {
                for (const k in this) {
                    if (!isNaN(k)) {
                        yield this[k];
                    }
                }
            }
        });


    // eq (==) :: Eq a => a -> a -> Bool
    const eq = a =>
        // True when a and b are equivalent in the terms
        // defined below for their shared data type.
        b => a === b;


    // groupBy :: (a -> a -> Bool) -> [a] -> [[a]]
    const groupBy = eqOp =>
        // A list of lists, each containing only elements
        // equal under the given equality operator,
        // such that the concatenation of these lists is xs.
        xs => 0 < xs.length ? (() => {
            const [h, ...t] = xs;
            const [groups, g] = t.reduce(
                ([gs, a], x) => eqOp(x)(a[0]) ? (
                    Tuple(gs)([...a, x])
                ) : Tuple([...gs, a])([x]),
                Tuple([])([h])
            );

            return [...groups, g];
        })() : [];

    // on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
    const on = f =>
        // e.g. groupBy(on(eq)(length))
        g => a => b => f(g(a))(g(b));

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

Thanks, Rob!

I've modified your macro to:

Demo:

1

Insert quoted or bracketet text.kmmacros (12.8 KB)

1 Like

FWIW the python itertools module supplies a pre-defined groupby (inherited from the SML and Haskell tradition – see the preface to the itertools documentation) so we could quickly sketch this kind of thing in python too,

A Python variant for the capture of quoted strings.kmmacros (3.3 KB)

Expand disclosure triangle to view PY Source
#!/usr/bin/env python
# -*- coding: utf-8 -*-

'''Lines containing the string quoted in the input'''

from os import environ
from itertools import groupby
from functools import reduce

# quotedTokens :: String -> [String]
def quotedTokens(source):
    '''List of quoted tokens in the source'''
    quoteChars = "“”‘’„‟()"

    def go(a, ipair):
        s, xs = a
        i, (_, v) = ipair

        return (s + "".join(v), xs) if bool(i % 4) else (
            "", xs + [s]
        )

    return [
        x for x in reduce(
            go,
            enumerate(
                groupby(
                    " " + source,
                    lambda c: c in quoteChars
                )
            ),
            ("", [])
        )[1] if x
    ]

if __name__ == '__main__':
    print(
        '\n'.join(
            quotedTokens(environ["KMVAR_localSource"])
        )
    )

but with JavaScript for Automation at least we know which version we are running.

(People often have more than one python on their system, installed at variable paths, and Python 2 requires a special utf8 incantation, whereas Python 3 doesn't)

How could the code me modified to precede the entries with either letters a-z or numbers 0-9?

You would like these (alphabetic or numeric) prefixes to appear only the menu ?

Or would they be pasted (on selection of a menu item) into the text with the prefixed token ?

Only in the menu

Adding the prefixes might look like this:

Insert quoted or bracketet text.kmmacros (13.0 KB)

(() => {
    "use strict";

    // Lines of quoted strings only.

    // quotedTokens :: String -> [String]
    const quotedTokens = source => {
        // A list of the quoted tokens in the source.
        const quoteChars = "“”‘’„‟()";

        return groupBy(
                on(eq)(
                    c => quoteChars.includes(c)
                )
            )(` ${source}`)
            .reduce(
                ([s, xs], cs, i) => 0 !== i % 4 ? (
                    [`${s}${cs.join("")}`, xs]
                ) : Boolean(s) ? (
                    ["", xs.concat(s)]
                ) : [s, xs],
                ["", []]
            )[1];
    };


    const main = () => {
        const
            prefixes = "abcdefghijklmnopqrstuvwxyz0123456789";

        const
            tokens = quotedTokens(
                Application("Keyboard Maestro Engine")
                .getvariable("quotedSource")
            );

        return prefixes.length >= tokens.length ? (
            zipWith(
                prefix => token => `${prefix} - ${token}`
            )(
                prefixes.split("")
            )(
                tokens
            ).join("\n")
        ) : "Not enough prefixes defined for token list";
    };

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

    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
        // A pair of values, possibly of
        // different types.
        b => ({
            type: "Tuple",
            "0": a,
            "1": b,
            length: 2,
            *[Symbol.iterator]() {
                for (const k in this) {
                    if (!isNaN(k)) {
                        yield this[k];
                    }
                }
            }
        });


    // eq (==) :: Eq a => a -> a -> Bool
    const eq = a =>
        // True when a and b are equivalent in the terms
        // defined below for their shared data type.
        b => a === b;


    // groupBy :: (a -> a -> Bool) -> [a] -> [[a]]
    const groupBy = eqOp =>
        // A list of lists, each containing only elements
        // equal under the given equality operator,
        // such that the concatenation of these lists is xs.
        xs => 0 < xs.length ? (() => {
            const [h, ...t] = xs;
            const [groups, g] = t.reduce(
                ([gs, a], x) => eqOp(x)(a[0]) ? (
                    Tuple(gs)([...a, x])
                ) : Tuple([...gs, a])([x]),
                Tuple([])([h])
            );

            return [...groups, g];
        })() : [];

    // on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
    const on = f =>
        // e.g. groupBy(on(eq)(length))
        g => a => b => f(g(a))(g(b));


    // zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
    const zipWith = f =>
        // A list constructed by zipping with a
        // custom function, rather than with the
        // default tuple constructor.
        xs => ys => xs.map(
            (x, i) => f(x)(ys[i])
        ).slice(
            0, Math.min(xs.length, ys.length)
        );

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

So then you need to remove them before pasting.

If your prefix is always 4 characters long, you could use a substring action

1 Like

That one was easy :slight_smile:

Thanks for your help!

1 Like

How could I add the straight double quotes here? Do I have to escape them?

Might you also need the 'single' quotes ?

If not, you could try writing:

const quoteChars = '"“”‘’„‟()';

Otherwise, a single backslash should suffice to enclose either straight quote (\" or \') in the quoteChars string.