Examples for 1Writer and Drafts 5 - perhaps best unencoded?

Good to see a set of JS actions for Drafts uploaded by @macautomation.

A couple of first thoughts:

  1. Perhaps worth also creating such actions for 1Writer ? 1Writer is well designed, and a good plain text match for OmniOutliner. It also has a JS API which overlaps with that of Drafts.
  2. I wasn’t quite sure why the omniJS component of examples was shown in a urlEncoded form ?

It seems a pity not to take advantage of the JS interpreters in 1Writer and Drafts, and show the JS code in legible form:

  • a bit less daunting,
  • an immediately clearer sense of what the code will do. (I was taken by surprise, for example, when I ran the tab-indented text to OO outline action (“Tabbed-Paragraphs to Outline”), and my OO document unexpectedly closed. Had the code been en clair, I would have spotted that and expected it,
  • and, more generally, much easier for users to read and adjust the code

But perhaps there is something I am missing here ?

( PS re the tab-indent to outline action – a possible complexity in Drafts 5 is that the distributed Indent action defaults to four spaces rather than tabs, so there could be some confusion when a user’s indents are not detected by the script )

1 Like

This, for reference, is what the user sees if they check the Drafts JS source of the “Tabbed-Paragraphs to Outline”.

Possibly a little scary and discouraging ?

items = draft.content.split('\n');
arrayAsString = JSON.stringify(items);
encodedArray = encodeURIComponent(arrayAsString);
scriptURL = "omnioutliner://localhost/omnijs-run?script=draftParagraphs%20%3D%20XXXXX%0Avar%20priorLevel%20%3D%20null%0Avar%20
priorItem%20%3D%20null%0Avar%20counter%0AdraftParagraphs%2EforEach%28function%28
p%29%7B%0A%09%2F%2F%20count%20the%20leading%20tab%20characters%0A%09counter%20
%3D%200%0A%09str%20%3D%20p%0A%09while%20%28str%2EindexOf%28%27%09%27%29
%20%21%3D%20-1%29%20%7B%0A%09%09counter%20%3D%20counter%20%2B%201%0A%09%09str%20
%3D%20str%2Esubstr%281%29%3B%0A%09%7D%0A%09itemLevel%20%3D%20counter%0A
%09%2F%2F%20add%20item%20based%20upon%20level%0A%09if%20%28priorLevel%20
%3D%3D%3D%20null%29%7B%0A%09%09priorItem%20%3D%20rootItem%2EaddChild%28
%0A%09%09%09null%2C%0A%09%09%09function%28item%29%7Bitem%2Etopic%20%3D
%20p%7D%0A%09%09%29%0A%09%09priorLevel%20%3D%200%0A%09%7D%20else%20
if%20%28itemLevel%20%3D%3D%3D%200%29%7B%0A%09%09priorItem%20%3D%20
rootItem%2EaddChild%28%0A%09%09%09null%2Cfunction%28item%29%7Bitem%2Etopic%20
%3D%20p%7D%0A%09%09%29%0A%09%09priorLevel%20%3D%200%0A%09%7D%20else%20
if%20%28itemLevel%20%3D%3D%3D%20priorLevel%29%7B%0A%09%09priorItem%20%3D%20
priorItem%2Eparent%2EaddChild%28%0A%09%09%09null%2C%0A%09%09%09function%28
item%29%7Bitem%2Etopic%20%3D%20p%2Etrim%28%29%7D%0A%09%09%29%0A%09%09
priorLevel%20%3D%20priorLevel%0A%09%7D%20else%20if%20%28itemLevel%20%3E%20
priorLevel%29%7B%0A%09%09priorItem%20%3D%20priorItem%2EaddChild%28%0A%09%09%09
null%2C%0A%09%09%09function%28item%29%7Bitem%2Etopic%20%3D%20p%2Etrim%28%29
%7D%0A%09%09%29%0A%09%09priorLevel%20%3D%20priorItem%2Elevel%20-%201%0A%09
%7D%20else%20if%20%28itemLevel%20%3C%20priorLevel%29%7B%0A%09%09delta%20
%3D%20priorLevel%20-%20itemLevel%0A%09%09if%20%28delta%20%3D%3D%3D%201%29
%7B%0A%09%09%09itemParent%20%3D%20priorItem%2Eparent%2Eparent%0A%09%09
%09priorItem%20%3D%20itemParent%2EaddChild%28%0A%09%09%09%09null%2C%0A
%09%09%09%09function%28item%29%7Bitem%2Etopic%20%3D%20p%2Etrim%28%29%7D%0A
%09%09%09%29%0A%09%09%09priorLevel%20%3D%20priorLevel%20-%202%0A%09%09%7D
%20else%20if%20%28delta%20%3D%3D%3D%202%29%7B%0A%09%09%09itemParent%20%3D
%20priorItem%2Eparent%2Eparent%2Eparent%0A%09%09%09priorItem%20%3D%20
itemParent%2EaddChild%28%0A%09%09%09%09null%2C%0A%09%09%09%09function%28
item%29%7Bitem%2Etopic%20%3D%20p%2Etrim%28%29%7D%0A%09%09%09%29%0A%09%09
%09priorLevel%20%3D%20priorLevel%20-%203%0A%09%09%7D%20else%20if%20
%28delta%20%3D%3D%3D%203%29%7B%0A%09%09%09itemParent%20%3D%20priorItem%2E
parent%2Eparent%2Eparent%2Eparent%0A%09%09%09priorItem%20%3D%20itemParent%2E
addChild%28%0A%09%09%09%09null%2C%0A%09%09%09%09function%28item%29%7Bitem%2E
topic%20%3D%20p%2Etrim%28%29%7D%0A%09%09%09%29%0A%09%09%09priorLevel%20%3D%20
priorLevel%20-%204%0A%09%09%7D%0A%09%7D%0A%7D%29%0Adocument%2Eclose%28%29";
scriptURL = scriptURL.replace('XXXXX', encodedArray);
app.openURL(scriptURL);

For contrast, the same script:

  • decoded,
  • tidied slightly to catch a couple of undeclared variables,
  • and adjusted to use editor.getText() in lieu of draft.content

Can run as a JS action in both 1Writer and Drafts 5, in the slightly more legible form below.

(Tho I might personally re-write it a little, for example to allow for space indents as well as tab-indents)

// Adaptation of code by @macautomation
(() => {

    const main = () => {

        const paras = editor.getText().split('\n');

        app.openURL(
            'omnioutliner:///omnijs-run?script=' +
            encodeURIComponent(
                '(' + makeOOParasFromTabbedLines.toString() +
                ')(' + JSON.stringify(paras) + ')'
            )
        );
    };

    const makeOOParasFromTabbedLines = paras => {
        var priorLevel = null
        var priorItem = null

        paras.forEach(function(p) {
            // count the leading tab characters
            var counter = 0
            var str = p
            var itemParent
            while (str.indexOf('\t') != -1) {
                counter = counter + 1
                str = str.substr(1);
            }
            var itemLevel = counter
            // add item based upon level
            if (priorLevel === null) {
                priorItem = rootItem.addChild(
                    null,
                    function(item) {
                        item.topic = p
                    }
                )
                priorLevel = 0
            } else if (itemLevel === 0) {
                priorItem = rootItem.addChild(
                    null,
                    function(item) {
                        item.topic = p
                    }
                )
                priorLevel = 0
            } else if (itemLevel === priorLevel) {
                priorItem = priorItem.parent.addChild(
                    null,
                    function(item) {
                        item.topic = p.trim()
                    }
                )
                priorLevel = priorLevel
            } else if (itemLevel > priorLevel) {
                priorItem = priorItem.addChild(
                    null,
                    function(item) {
                        item.topic = p.trim()
                    }
                )
                priorLevel = priorItem.level - 1
            } else if (itemLevel < priorLevel) {
                var delta = priorLevel - itemLevel
                if (delta === 1) {
                    itemParent = priorItem.parent.parent
                    priorItem = itemParent.addChild(
                        null,
                        function(item) {
                            item.topic = p.trim()
                        }
                    )
                    priorLevel = priorLevel - 2
                } else if (delta === 2) {
                    itemParent = priorItem.parent.parent.parent
                    priorItem = itemParent.addChild(
                        null,
                        function(item) {
                            item.topic = p.trim()
                        }
                    )
                    priorLevel = priorLevel - 3
                } else if (delta === 3) {
                    itemParent = priorItem.parent
                        .parent.parent.parent
                    priorItem = itemParent.addChild(
                        null,
                        function(item) {
                            item.topic = p.trim()
                        }
                    )
                    priorLevel = priorLevel - 4
                }
            }
        })
        document.close()
    }

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

Thank you for the insightful feedback!

The document.close() was added to work-around a drawing issue where the outline document does not show the changes until it is refreshed by closing and re-opening. When the bug has been addressed, I’ll update the posted action.

With regard to your suggestion to display the Omni Automation code as JavaScript vs. a script URL:

I see advantages/disadvantages to both the “clear” and “encapsulated” approaches and have chosen to pursue the strategy of using script URLs in the actions and posting the source for each of the script URL scripts on individual web pages at https://omni-automation.com/drafts-app/index.html. This allows for the opportunity to document each script’s strategy and (often line-by-line) explain what the scripts are doing Omni Automation-wise, and to provide a video showing the scripts in use.

I do use the “clear” approach sometimes, as with the Swift code snippet for executing Omni Automation scripts, detailed here: https://omni-automation.com/script-links.html

Perhaps if JavaScript had a triple-quote equivalent?

Cheers,

Sal

P.S. The omission of 1Writer is unintended and can only be explained as “hours in the day” :-) I’ll install it today. Thanks for the suggestion!

1 Like

I see advantages/disadvantages to both the “clear” and “encapsulated”

Tell me more ? Why do you feel a triple quote might be useful ?

What are the disadvantages that you find in the clear version ?

PS I’m not aware of any problems that can arise with nested quoting in the use of either

  • the Function.toString() method, or with
  • JSON.stringify()

See this, for example, which works in both 1Writer and Drafts.

(() => {

    const testFn = s => {
        const dlg = new Alert(
            'Nested quoting',
            `'"Hello \'${s}\' world"'`
        );
        return dlg.show(function(result) {});
    }

    const phrase = "new";

    const strURL = 'omnioutliner:///omnijs-run?script=' +
        encodeURIComponent(
            '(' + testFn.toString() + ')' +
            '(' + JSON.stringify(phrase) + ')'
        );

    app.openURL(strURL);
})();

Untitled
Untitled 2

(And adding a further level of nested quotes around ‘phrase’ still produces no problems):

(() => {

    const testFn = s =>
        new Alert(
            'Nested quoting',
            `'"Hello \'${s}\' world"'`
        ).show(() => {});

    const phrase = "'new'";

    const strURL = 'omnioutliner:///omnijs-run?script=' +
        encodeURIComponent(
            '(' + testFn.toString() + ')' +
            '(' + JSON.stringify(phrase) + ')'
        );

    app.openURL(strURL);
})();

or equally:

(() => {

    const testFn = s =>
        new Alert(
            'Nested quoting',
            `'"Hello \'${s}\' world"'`
        ).show(() => {});

    const phrase = '"new"';

    const strURL = 'omnioutliner:///omnijs-run?script=' +
        encodeURIComponent(
            '(' + testFn.toString() + ')' +
            '(' + JSON.stringify(phrase) + ')'
        );

    app.openURL(strURL);
})();

Or perhaps you are looking for JS Template Literal `backticks` ?

When composing AppleScript scripts, it’s not unusual for the script to contain multiple tell blocks, each targeting a different application. Runs fine and the tell blocks clearly delineate what code targets which application.

With the example Drafts action, I still haven’t found the technique for targeting multiple apps in the same script that has the same simplicity and understand-ability of the AppleScript tell blocks.

In a simple form, using back tacks could help to delineate the JavaScript code meant for the target Omni app. And then the content from the Draft could be inserted, and then go through the process of creating an Omni Automation script URL for execution by the Drafts app:

textFromDraft = draft.content
textFromDraft = textFromDraft.replace("'", "\'")
OmniAutomationScript = `try {
	g = document.windows[0].selection.graphics[0]
	txtSize = g.textSize
	g.text = 'XXXXX'
	g.textSize = txtSize
} catch (err){console.log(err)}`
combinedScript = OmniAutomationScript.replace("XXXXX", textFromDraft)
encodedScript = encodeURIComponent(combinedScript)
scriptURL = "omnigraffle://localhost/omnijs-run?script=" + encodedScript
app.openURL(scriptURL)

But it still doesn’t “feel” like “the” solution. Still searching as automaton on iOS begins its evolution.

As always, thank you for your insights, comments, and invaluable contributions to the Automation community.

P.S. – for those wondering what we’re discussing here, I’m including the Draft action code that uses the encapsulated Omni Automation script URL:

var docTxt = encodeURIComponent(draft.content);
docTxt = docTxt.split("'").join("%5C%27") // escaped single quote
var scriptURL = "omnigraffle://localhost/omnijs-run?script=try%20%7B%0A%09g%20%3D%20document%2Ewindows%5B0%5D%2Eselection%2Egraphics%5B0%5D%0A%09txtSize%20%3D%20g%2EtextSize%0A%09g%2Etext%20%3D%20%27XXXXX%27%0A%09g%2EtextSize%20%3D%20txtSize%0A%7D%20catch%20%28err%29%7Bconsole%2Elog%28err%29%7D"
scriptURL = scriptURL.replace("XXXXX",docTxt);
app.openURL(scriptURL);

Perhaps the attempt to use string replacements to pass an argument is over-complicating things for you there ?

(There’s no need for special handling of quotes or newlines)

All you need is:

  1. To define the OG code as a function
  2. To URL-encode a stringified application of that function to an argument.

(functionName)(argument)

So for a version which works in both 1Writer and Drafts:

(() => {
    // OMNIGRAFFLE JS Context
    // function declaration.
    const textForSelectedGraphic = txt => {
        try {
            g = document.windows[0].selection.graphics[0];
            txtSize = g.textSize;
            g.text = txt;
            g.textSize = txtSize;
        } catch (err) {
            console.log(err)
        }
    };

    // OTHER JS Context (1Writer, Drafts ...)
    const strText = editor.getText();
    app.openURL(
        'omnigraffle://localhost/omnijs-run?script=' +
        encodeURIComponent(
            '(' + textForSelectedGraphic + ')' +
            '(' + JSON.stringify(strText) + ')'
        )
    );
})();

Or, expanding it with a few comments:

(() => { // 'Module' wrapping
    // (in immediately invoked anonymous function)
    // prevents clashes in the global name-space,
    // particularly between successive script actions.

    'use strict' // This provides richer and more informative
                 // error messages for the user.

    // OMNIGRAFFLE JS Context
    // function declaration.

    // textForSelectedGraphic :: String -> OG IO()
    const textForSelectedGraphic = txt => {
        try {
            g = document.windows[0].selection.graphics[0];
            txtSize = g.textSize;
            g.text = txt;
            g.textSize = txtSize;
        } catch (err) {
            console.log(err)
        }
    };

    // OTHER JS Context (1Writer, Drafts etc)

    // Unlike `drafts.content`, editor.getText()
    // works in both 1Writer and Drafts
    const strText = editor.getText();

    app.openURL(
        'omnigraffle://localhost/omnijs-run?script=' +
        encodeURIComponent(

            // Stringified OmniGraffle function body,
            '(' + textForSelectedGraphic + ')' +

            // invoked with stringified argument.
            '(' + JSON.stringify(strText) + ')'
        )
    );
})();

When composing AppleScript scripts, it’s not unusual for the script to contain multiple tell blocks, each targeting a different application. Runs fine and the tell blocks clearly delineate what code targets which application.

We’re not actually targeting two apps in the same script here – we are writing two scripts, one of which submits the other for evaluation, to a different instance of the JS interpreter (A different JSContext).

The trick is simply to write the submitted script as a function, and make use of the fact that:

  1. coercion of functions to the String type in JS yields properly quoted source code, and
  2. JSON.stringify() yields properly quoted string translations of arguments.

(In a single script, of course, the JXA Automation library approach to targeting multiple scripts is just to prefix method and property calls with a reference to a specific Application object).

You can bundle the submission of JS functions by [1Writer, Drafts] for evaluation in [OmniOutliner, OmniGraffle] by writing a more general function to cover all those permutations, and any number of optional arguments, as in:

function omniJSFunctionWithArgs(targetAppName, f) {
    const args = Array.from(arguments);
    app.openURL(
        targetAppName.toLowerCase() +
        '://localhost/omnijs-run?script=' +
        encodeURIComponent(
            '(' + f + ')' +
            '(' +
            args.slice(2)
            .map(JSON.stringify)
            .join(', ') +
            ')'
        )
    );
};

So, for example, if we not only want to fill an OG shape with text from 1Writer or Drafts, but also want to provide any number of additional arguments, specifying, perhaps, font and font size, for a start:

(() => { // 'Module' wrapping
    // (in immediately invoked anonymous function)
    // prevents clashes in the global name-space,
    // particularly between successive script actions.

    'use strict' // This provides richer and more informative
    // error messages.

    // OMNIGRAFFLE JS Context

    // textForSelectedGraphic :: String -> OG IO()
    const textForSelectedGraphic = (txt, fontName, fontSize) => {
        try {
            g = document.windows[0].selection.graphics[0];
            g.text = txt;
            g.fontName = fontName;
            g.textSize = fontSize;
        } catch (err) {
            console.log(err)
        }
    };

    // OTHER JSContext ( 1Writer or Drafts )

    function omniJSFunctionWithArgs(targetAppName, f) {
        const args = Array.from(arguments);
        app.openURL(
            targetAppName.toLowerCase() +
            '://localhost/omnijs-run?script=' +
            encodeURIComponent(
                '(' + f + ')' +
                '(' +
                args.slice(2)
                .map(JSON.stringify)
                .join(', ') +
                ')'
            )
        );
    };

    // Unlike `drafts.content`, editor.getText()
    // works in both 1Writer and Drafts
    const strText = editor.getText();

    omniJSFunctionWithArgs(
        'omnigraffle',
        textForSelectedGraphic,

        // And as many arguments as the OO/OG function needs:
        strText,
        'Courier',
        20
    );
})();

or rearranging, if you prefer:

(() => {
    'use strict'

    const main = () => {

        // 1WRITER OR DRAFTS CONTEXT
        const strText = editor.getText();

        // Submitting a function to Omnigraffle
        // with any arguments needed
        omniJSFunctionWithArgs(
            'omnigraffle',
            textForSelectedGraphic,
            {
                text: strText,
                fontName: 'Courier',
                textSize: 24
            }
        );
    };

    // OMNIGRAFFLE JS Context

    // textForSelectedGraphic :: String -> OG IO()
    const textForSelectedGraphic = options => {
        try {
            Object.assign(
                document.windows[0].selection.graphics[0],
                options
            )
        } catch (err) {
            console.log(err)
        }
    };

    // Mechanics for other JSContext ( 1Writer or Drafts )

    function omniJSFunctionWithArgs(omniName, f) {
        var args = Array.from(arguments);
        app.openURL(omniName.toLowerCase() +
            "://localhost/omnijs-run?script=" +
            encodeURIComponent("(" + f + ")(" +
                args.slice(2).map(JSON.stringify)
                .join(", ") + ")"));
    };

    // MAIN ---
    return main();

})();

My hesitation to embrace the suggestion has been based upon the complexity of the example as it features JS techniques, like arrow functions, and function encasement, that haven’t been included in the code examples on the website. Perhaps this scenario might provide an introduction to those concepts for readers?

Regardless, I thank you again for the attention you’ve dedicated to this.

(() => {
	// OmniGraffle JavaScript Context
	// Omni Automation script as a function with text input
	function setTextForSelectedGraphic(textInput){
		try {
			graphic = document.windows[0].selection.graphics[0];
			currentTextSize = graphic.textSize;
			graphic.text = textInput;
			graphic.textSize = currentTextSize;
		} catch (err) {
			console.error(err.message)
		}
	};

	// Host Application JavaScript Context (1Writer, Drafts, etc.)
	const strText = editor.getText();
	// create and execute an Omni Automation script URL
	app.openURL(
		'omnigraffle://localhost/omnijs-run?script=' +
		encodeURIComponent(
			'(' + setTextForSelectedGraphic + ')' +
			'(' + JSON.stringify(strText) + ')'
		)
	);
})();

… the complexity of the example as it features JS techniques, like arrow functions, and function encasement, that haven’t been included in the code examples

For the beginner, complexity lies not in novelty (everything is novel) but in the number of layers and moving parts.

(An ES6 arrow function, for example, has a simpler structure than an ES5 function declaration, and both are equally novel if we are starting from scratch.

The main constraint on using arrow functions derives from their simplicity – they are cut-down light-weight structures which, unlike the full-blown and more complex function () {} declarations, don’t bind values to the special names this and arguments. In the general function below, for example, we are using a function declaration because we want to make use of the array-like arguments value, which lets us call a function with any number of optional arguments)

The problem with approaches like .replace(‘XXX’, withManuallyPreProcessedArgumentString) is that they add complexity by increasing:

  • the number of operations and moving parts
  • the number of levels of indirection
  • the number of things that can go wrong

We can reduce all this to:

  • one function definition, and
  • automatic stringifications (by JS, rather than the user).

There may also be an argument for packaging even those mechanics into a general off-the-shelf function that users can copy and paste.

The following, for example, simplifies the task imposed on the user:

(() => {
    'use strict';

    // FUNCTION FOR OMNI JS CONTEXT
    const displayMessage = (title, message) =>
        (new Alert(title, message))
        .show(() => {});

    // SUBMITTED FROM OTHER JS CONTEXT (1WRITER or DRAFTS)
    omniJSFunctionWithArgs(
        'omnioutliner',
        displayMessage,
        'Example',
        'Hello World !',
    );

	// --------------------------------------------------------

    // PRE-PACKAGED GENERAL FUNCTION TO COPY AND PASTE
    // (for use with 1Writer or Drafts)    

    // omniJSFunctionWithArgs :: AppName String ->
    //      JS Function -> optional further arguments -> IO()
    function omniJSFunctionWithArgs(omniName, f) {
        const args = Array.from(arguments).slice(2);
        return app.openURL(
            omniName.toLowerCase() +
            '://localhost/omnijs-run?script=' +
            encodeURIComponent(
                `(${f})` +
                `(${args.map(JSON.stringify).join(', ')})`
            )
        );
    };
})();

1 Like