Help debugging this script?

Hello all,

Can you help me debug this script?

I am getting this error on a second run: Error: SyntaxError: Can’t create duplicate variable: ‘safDoc’

And I get an error when the Links: project doesn’t exist.

'use strict';

const filePath = "~/Library/Mobile Documents/iCloud~is~workflow~my~workflows/Documents/Links.txt"; // Path to TaskPaper file

const tp = Application("TaskPaper");
tp.includeStandardAdditions = true

const d = new Date();
const dtd = d.getFullYear() + "-" + ('0' + (d.getMonth()+1)).slice(-2) + "-" + ('0' + d.getDate()).slice(-2);

const safApp = Application("Safari");
const safDoc = safApp.documents[0];
const varTitle = safDoc.name();
const varURL = safDoc.url();

const varTask = "- " + varTitle + " → " + varURL + " @link(" + dtd + ")";

function TaskPaperContext(editor, options) {
	const outline = editor.outline
	const project = outline.evaluateItemPath('/project Links')[0]
	if (project == null) {
		project = outline.createItem('Links:')
		outline.root.insertChildrenBefore([project], outline.root.firstChild)
	}
		project.insertChildrenBefore(
			outline.createItem(options.url),
			project.firstChild
	)
}

const strFullPath = ObjC.unwrap($(filePath).stringByExpandingTildeInPath);
const document = tp.open(Path(strFullPath));

document.evaluate({
    script: TaskPaperContext.toString(),
    withOptions: { url: varTask }
});

document.save();

The first thing that jumps to the eye, is that you haven’t wrapped this in an IIFE, so you are making definitions like safDoc in the JXA global namespace, which, apart from being busily crowded, persists between script runs.

(i.e. once you have declared a global constant in one script run, it gets defended in the next run by an “already exists” message and compilation error)

So the first experiment would be to get your own temporary and local namespace by pasting it all into a wrapper like:

(() => {
    "use strict";


})();

Some notes here on the rationale and various parts of that IIFE pattern:

Comparing JavaScript for Automation (JXA) and AppleScript - Tips & Tutorials - Keyboard Maestro Discourse

2 Likes

On finding an existing project, or creating one if nothing of that name is found, here is one general pattern:

Expand disclosure triangle to view JS source
// projectFoundOrCreated :: TP3Outline ->
// String -> TP3Item
const projectFoundOrCreated = outline =>
    // A reference to a TaskPaper Project item,
    // either found in the outline, or created
    // at the top of it.
    projectName => {
        const
            k = projectName.trim(),
            matches = outline.evaluateItemPath(
                `//project "${k}"`
            ),
            blnFound = 0 < matches.length,
            project = blnFound ? (
                matches[0]
            ) : outline.createItem(`${k}:`);

        return (
            blnFound || outline.insertItemsBefore(
                project,
                outline.root.firstChild
            ),
            project
        );
    };

Note that type of an evaluateItemPath match is Array, and a non-match yields an empty array, rather than JS’s special null value.


e.g. something like:

projectFoundOrCreated(editor.outline)("Links")
2 Likes

Ok. I am dropping the above, as I was tired last night, I and was trying to hastily merge old code together.

My thinking was, well, this should be easy! (famous last words!)

Here is my current attempt (based on excellent code from @complexpoint and @jessegrosjean), which is working, but I get an error: Error -1708: Message not understood.

(() => {
    "use strict";

const filePath = "~/Library/Mobile Documents/iCloud~is~workflow~my~workflows/Documents/Links.txt";
const strFullPath = ObjC.unwrap($(filePath).stringByExpandingTildeInPath);
const tp = Application("TaskPaper");
const document = tp.open(Path(strFullPath));

    // Rob Trew @2021, with parts from Jesse Grosjean and Jim Krenz

    // main :: IO ()
    const main = () => {
        const windows = Application("Safari").windows;

        return either(
            // Left channel message in case of a problem.
            msg => alert(
                "Copy Safari tab list to TaskPaper"
            )(msg)
        )(
            // Simple string value returned if all is well.
            x => x
        )(
            bindLR(
                0 < windows.length ? (
                    Right(windows.at(0).tabs)
                ) : Left("No windows open in Safari")
            )(tabs => {
                const
                    dateString = taskPaperDateString(
                        new Date()
                    ).slice(0, 10),
                    linkStrings = zipWith(
                        name => link =>
                        `- ${name} → ${link} @link(${dateString})`
                    )(
                        tabs.name()
                    )(
                        tabs.url()
                    );

                return Right(
                    Application("TaskPaper")
                    .document
                    .evaluate({
                        script: `${TaskPaperContext}`,
                        withOptions: {
                            projectName: "Links",
                            linkTexts: linkStrings
                        }
                    })
                );
            })
        );
    };

    // -------------------- TASKPAPER --------------------

    // TaskPaperContext :: Editor -> Dict -> String
    const TaskPaperContext = (editor, options) => {
        const tp3Main = () => {
            const
                outline = editor.outline,
                xs = options.linkTexts,
                targetProject = projectFoundOrCreated(
                    outline
                )(options.projectName);

		outline.groupUndoAndChanges(() => {
			let children = xs.map(txt => outline.createItem(txt))
			targetProject.insertChildrenBefore(children, targetProject.firstChild);
		});

            return xs.join("\n");
        };

        // projectFoundOrCreated :: TP3Outline ->
        // String -> Bool -> TP3Item
        const projectFoundOrCreated = outline =>
            // A reference to a TaskPaper Project item,
            // either found in the outline, or created
            // at the top of it.
            projectName => {
                const
                    k = projectName.trim(),
                    matches = outline.evaluateItemPath(
                        `//project "${k}"`
                    ),
                    blnFound = 0 < matches.length,
                    project = blnFound ? (
                        matches[0]
                    ) : outline.createItem(`${k}:`);

                return (
                    blnFound || outline.insertItemsBefore(
                        project,
                        outline.root.firstChild
                    ),
                    project
                );
            };

        return tp3Main();
    };

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

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

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

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

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

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

    // iso8601Local :: Date -> String
    const iso8601Local = dte =>
        new Date(dte - (6E4 * dte.getTimezoneOffset()))
        .toISOString();

    // taskPaperDateString :: Date -> String
    const taskPaperDateString = dte => {
        const [d, t] = iso8601Local(dte).split("T");

        return [d, t.slice(0, 5)].join(" ");
    };

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

    return main();
document.save();
})();

My other question: can the four const at the beginning be improved or condensed?

I don’t, alas, have time to go through your code in detail, now, but something does strike me at the bottom of it:

You have a document.save() line which will never be evaluated, because your IIFE returns a value (thereby concluding and terminating evaluation) just before it.

1 Like

What was the first question ? ( I think I may have missed it :slight_smile: )

It might be helpful if you could step back and explain the goal and context. What are you expecting or hoping for when you evaluate this ?

(i.e. what is the problem which you are aiming to solve here ?)

The first (implicit) question is how do I resolve the error -1708. :slight_smile:

When I look at those four const lines, I keep thinking to myself: That seems like a lot of constants. Is there a way to condense them? Or make them more efficient?

Answers to both of those really depend on knowing what problem this script is intended to solve.

What are you aiming to do with it ?

(I can see that the script produces errors, but I don’t yet know what you want it to produce.)

It takes a reader a fair amount of work to discover that inductively, so helpful to be told straight up :slight_smile:

(Never easy to avoid the XY problem)

The goal of the script:

Grab the title and URL of the frontmost tab in Safari, and place it as the top task in a Links: project in a particular TaskPaper document.

If the Links: project does not exist, it is created.

If the particular TaskPaper document is not opened, it is opened.

At the conclusion, the TaskPaper document is saved.

Got it – I’ll take a look later this evening, if no one else gets there first.

1 Like

In the meanwhile, something there doesn’t look quite right

Application("TaskPaper")
                    .document
                    .evaluate

That isn’t a reference to a particular document.

You would need something more like:

Expand disclosure triangle to view JS source
const main = () => {
        const
            doc = Application("TaskPaper")
            .documents.at(0);

        return doc.exists() ? (
            doc.evaluate({
                script: `${tp3Context}`,
                withOptions: {}
            })
        ) : "No document open in TaskPaper.";
    };
1 Like

Thanks! Have to work for the next eight hours. Will have a go at it during lunch.

I’m struggling,

I thought (probably more assumed) that if I open the document (const document = tp.open(Path(strFullPath));), it would be the default place for everything.

I’m still a newbie fumbling in the dark.

1 Like

The front document in a JavaScript for Automation model is always documents.at(0), which they also let you write as documents[0] (perhaps a little confusingly, as documents is not really the name of an array there, and won’t behave like an array in other respects)

The second document, if several are open, is documents.at(1), and so on.

PS having made that adjustment, your code seems to work here (modulo the iCloud file reference, which, of course, I can’t test : )

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

    const
        fpMobile = "~/Library/Mobile Documents/",
        fpWorkflows = "iCloud~is~workflow~my~workflows/",
        fpLinks = "Documents/Links.txt",
        fp = `${fpMobile}${fpWorkflows}${fpLinks}`,
        // fp = "~/Desktop/Links.txt",
        strFullPath = ObjC.unwrap(
            $(fp).stringByExpandingTildeInPath
        );

    const
        tp = Application("TaskPaper"),
        doc = tp.open(Path(strFullPath));


    // main :: IO ()
    const main = () => {
        const windows = Application("Safari").windows;

        return either(
            // Left channel message in case of a problem.
            msg => alert(
                "Copy Safari tab list to TaskPaper"
            )(msg)
        )(
            // Simple string value returned if all is well.
            x => x
        )(
            bindLR(
                0 < windows.length ? (
                    Right(windows.at(0).tabs)
                ) : Left("No windows open in Safari")
            )(tabs => {
                const
                    dateString = taskPaperDateString(
                        new Date()
                    ).slice(0, 10),
                    linkStrings = zipWith(
                        name => link =>
                            `- ${name} → ${link} @link(${dateString})`
                    )(
                        tabs.name()
                    )(
                        tabs.url()
                    );

                const result = doc.evaluate({
                    script: `${TaskPaperContext}`,
                    withOptions: {
                        projectName: "Links",
                        linkTexts: linkStrings
                    }
                });

                return Right(
                    (doc.save(), result)
                );
            })
        );
    };

    // -------------------- TASKPAPER --------------------

    // TaskPaperContext :: Editor -> Dict -> String
    const TaskPaperContext = (editor, options) => {
        const tp3Main = () => {
            const
                outline = editor.outline,
                xs = options.linkTexts,
                targetProject = projectFoundOrCreated(
                    outline
                )(options.projectName);

            outline.groupUndoAndChanges(() => {
                const children = xs.map(
                    txt => outline.createItem(txt)
                );

                targetProject.insertChildrenBefore(
                    children, targetProject.firstChild
                );
            });

            return xs.join("\n");
        };

        // projectFoundOrCreated :: TP3Outline ->
        // String -> Bool -> TP3Item
        const projectFoundOrCreated = outline =>
            // A reference to a TaskPaper Project item,
            // either found in the outline, or created
            // at the top of it.
            projectName => {
                const
                    k = projectName.trim(),
                    matches = outline.evaluateItemPath(
                        `//project "${k}"`
                    ),
                    blnFound = 0 < matches.length,
                    project = blnFound ? (
                        matches[0]
                    ) : outline.createItem(`${k}:`);

                return (
                    blnFound || outline.insertItemsBefore(
                        project,
                        outline.root.firstChild
                    ),
                    project
                );
            };

        return tp3Main();
    };

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

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

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

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

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

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

    // iso8601Local :: Date -> String
    const iso8601Local = dte =>
        new Date(dte - (6E4 * dte.getTimezoneOffset()))
        .toISOString();

    // taskPaperDateString :: Date -> String
    const taskPaperDateString = dte => {
        const [d, t] = iso8601Local(dte).split("T");

        return [d, t.slice(0, 5)].join(" ");
    };

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

    return main();
})();
1 Like

Brilliant @complexpoint! And yes, the iCloud file reference works.

I am going tout these two side by side and see what I can learn from comparing them.

Some questions, if I may:

I see that you broke the file path into four const declarations.

Was this a style choice? To make it easier to read? So that it would not wrap?

Does JavaScript care about the quantity of const declarations in a script?
(should one aim for less when coding?)

When you looked at my code, what did you look at first? And when/how did you spot the documents[0] issue? I’m trying to figure out priorities when analyzing JavaScript code.

Yes – just a habit driven by readability – I just don’t like scanning too far left and right, and prefer code not to go much over the 60 width.

(Cognitive research on people reading notes under pressure suggests that horizontal eye-scanning is more expensive than vertical scanning up and down the page)

I don’t think the interpreter has any preferences :slight_smile:

(but you can learn a lot from linters – ESLint, for example, is good)

As a writer/reader of code I have a reflex of reducing noise or redundant ink wherever I can except in names for the values – I happen to prefer fairly “German” naming conventions

Not sure, just pattern-recognition mainly. I like to be told what problem the code is trying to solve before I look at it. That reduces the effort required to get clarity, and informs an attempt at a top-down scan

i.e. I probably look first at the main bottle-necks, and the point where the JS source is passed from JXA to the TaskPaper JSContent is obviously the main one.

As the central function (evaluate) is a method on a document, it probably sprang quickly to my eye that we didn’t yet have a reference to a specific document there.

2 Likes

Much appreciated @complexpoint!

1 Like

@complexpoint — putting aside my JavaScript practice for the moment—how easy would it be to take the code that you just did, keep the current features intact, but change it from grabbing all of the open tabs to just the frontmost tab in Safari? And put that one (tab title → link @date) line at the root of the TaskPaper document (no need for the Links: project).

That would complete my dream workflow for link management. If I had that, well, it would make my Christmas that much more merrier.

If you would consider writing such, I will buy you something off of your Amazon wishlist (keeping in mind that my budget is minimal, but I want to pay you back for all of your help, instructions and inspirations).

Sincerely,

Jim

the … document still refers to a named (Links) file ?

Or just to any document that happens to be at the front when the script is run ?

    const
        tp = Application("TaskPaper"),
        doc = tp.open(Path(strFullPath));

    //  or just the front document ?
    //      doc = tp.documents.at(0)

A minimal edit to take just the first of the tabs might look like:

linkStrings = zipWith(
    name => link =>
        `- ${name} → ${link} @link(${dateString})`
)(
    // tabs.name()
    [tabs.at(0).name()]
)(
    // tabs.url()
    [tabs.at(0).url()]
);

A minimal edit to redefine the target parent from a given project to the document root:

const
    outline = editor.outline,
    xs = options.linkTexts,
    // targetProject = projectFoundOrCreated(
    //     outline
    // )(options.projectName);
    target = outline.root;

outline.groupUndoAndChanges(() => {
    const children = xs.map(
        txt => outline.createItem(txt)
    );

    // targetProject.insertChildrenBefore(
    target.insertChildrenBefore(
        children, target.firstChild
    );
});
1 Like