omniJS (was JXA) to access row number?


#1

I’d like to use JXA to access the outline numbers that OmniOutliner automatically creates (and best of all, re-creates as I re-order items). I’ve created a small OO-to-Markdown script that works pretty well (I have a few more questions that I’ll save for other posts), but there doesn’t seem to be a property that gives me these numbers.

I’ve tried using:

  1. item.identifier
  2. item.index
  3. item.level – this one seems promising, but only gives a single integer, and not the outline position.

As an example:

  1. Lorem
    1. Lorem ipsum
    2. Lorem ipsum
    3. Lorem ipsum
  2. Lorem
    1. Lorem ipsum
    2. Lorem ipsum
    3. Lorem ipsum

The bolded text above would be the string “2.2.”.

Is there a simple (undocumented?) property to get that string, or do I need to write my own custom code?


#2

I wonder if the existence of two separate javascript interfaces to OmniOutliner is causing some confusion here ?

Both run in a JSContext – an instance of the Safari JavaScript interpreter, but:

  • JavaScript for Automation (JXA) interacts with OO through the Apple-supplied Automation object (a JS version of the interface used by AppleScript).
  • the omniJS JSContext is embedded in OO itself, and interacts with it directly

JXA and omniJS each use quite different sets of objects properties and methods to talk to OmniOutliner.

Your title asks about JXA, which addresses the outline as a nest of:

  • row objects, which have properties including,
  • id (but not identifier)
  • index
  • level

but the detail of your question looks more likely to be talking not about JXA but about omniJS, which has a dual model of the outline, as a nest of:

  • TreeNode objects each with paired Item objects, each of which has properties including,
  • identifier (but not id)
  • index
  • level

I think the confusion may have been compounded by the fact that some of the examples posted the other day involved executing/evaluating code in both JSContexts - using JXA for more general automation and interaction with the system, and then getting JXA to launch the omniJS execution of a separate omniJS script source (in the form of a URL) inside OO itself.

In short, its possible that you really are asking about the JXA scripting interface to OmniOutliner, and that JXA answers will be of use to you, but I think you may really need to ask, instead, about the quite different omniJS interface to OmniOutliner.

That’s probably the first thing to check …


#3

Heh… I think you may be on to something.

I’m definitely not yet clear on the distinctions, and didn’t even consider that the object properties could differ between the two interfaces.

I write in TextMate and paste into the omniJS console window to test, but use Keyboard Maestro’s JXA interface to execute the code. I’ve been using the OmniOutliner Automation | API Reference menu item to read about the object properties, but it doesn’t indicate that there are two different sets of properties. Is the [omni-automation.com] oa site more complete?

A good portion of the documentation seems to be a work in progress, so I (admittedly) didn’t dig very deep into the site.


#4

OK, you are definitely talking about omniJS and not JXA :-)

( Neither omni-automation.com nor the omniJS Automation > API Reference pages will talk about the JXA interface to OO (Though I think that your experience suggests that it might be helpful for them to direct JXA interface users elsewhere. The JXA interface to OmniOutliner is documented in the same place as the AppleScript interface (Script Editor > File > Open Dictionary > omnioutiner.app is one route to it )

Probably worth a note to Omni Support to report that this can all be quite confusing, at first, for users, and that prominent (“left to JXA”, “right to omniJS”) signposting and clarification would be helpful.

Having got that far:

  • while I can see the numbering option in the JXA interface - (heading-prefix attribute of style of row),
  • and would also expect to also find it among the omniJS style attributes, you might need to ask Omni Support about whether it is yet included in the omniJS interface. I would expect to see in in the Style.Attribute list, (Item.Style then the attributes of a given style), but I haven’t spotted it yet.

Either way, I think the closest that the scripting interfaces will take you is to a combination of the Index (position among siblings), and the numbering component of the Style. You would then need to write your own function to combine those two and derive a prefix string. If it’s literally the position that you want, then you could simply get the index property, and perhaps an ancestral chain of indices if the Item of interest is nested.


#5

Here is a sample itemNumberPath :: OO Item -> [Int] function for the raw list of (zero-based) integer indexes down to a given item.

To test, select a nested item somewhere in the outline, and paste the whole of this into the Automation Console:

(() => {
    'use strict';

    // itemNumberPath :: OO Item -> [Int]
    const itemNumberPath = oItem => {
        const oParent = oItem.parent;
        return oParent !== null ? (
            itemNumberPath(oParent)
            .concat(oItem.index)
        ) : (
            []
        );
    };

    // GENERIC FUNCTION ------------------------------------------------------

    // show :: Int -> a -> Indented String
    // show :: a -> String
    const show = (...x) =>
        JSON.stringify.apply(
            null, x.length > 1 ? [x[1], null, x[0]] : x
        );

    // TEST ------------------------------------------------------------------

    // Ancestral index path of first selected item in outline:

    // Convert Array to string for legibility in Automation Console
    return show(
        itemNumberPath(document.editors[0].selectedNodes[0])
    );
})();

#6

Huh. This is all incredibly helpful. Thank you.

(Did you just knock this script out in the 15 minutes between your posts? I’m just digging into javascript, so the syntax is holding me back, but the specifics of JXA and omniJS are bigger mountains. You seem to be quite fluent!)


#7

Once you have chosen a small working subset of JavaScript (see, for example, “JavaScript - the Good Parts” then it becomes quite quick …)

The interface then falls into place too, I think. (I was embarrassed that 15 mins seemed a bit slow - I’ve been using the OmniGraffle interface more than the OO :-)

PS I’ve adjusted the code above so that the root item has index [0], rather than including the virtual root (rootItem) in the ancestral chain.


#8

Even better! Thank you!


#9

PS, pasting in a few more generic functions, you could derive that numbering prefix style from the index chain like this:

// outlinePrefix :: [Int] -> String
const outlinePrefix = xs =>
    intercalate('.', map(n => succ(n).toString(), xs));

e.g.

(() => {
    'use strict';

    // itemNumberPath :: OO Item -> [Int]
    const itemNumberPath = item =>
        Boolean(item.parent) ? (
            itemNumberPath(item.parent)
            .concat(item.index)
        ) : [];

    // outlinePrefix :: [Int] -> String
    const outlinePrefix = xs =>
        intercalate('.', map(n => succ(n)
            .toString(), xs));

    // GENERIC FUNCTIONS ------------------------------------------------------

    // intercalate :: String -> [String] -> String
    const intercalate = (s, xs) => xs.join(s);

    // isChar :: a -> Bool
    const isChar = x =>
        typeof x === 'string' && x.length === 1;

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

    // succ :: Enum a => a -> a
    const succ = x =>
        isChar(x) ? (
            chr(ord(x) + 1)
        ) : isNaN(x) ? (
            undefined
        ) : x + 1;

    // TEST ------------------------------------------------------------------

    // Ancestral index path of first selected item in outline:

    // '1.2.3' one-based string derived from chain of 0-based integers
    return outlinePrefix(
        itemNumberPath(document.editors[0].selectedNodes[0])
    );

})();


#10

You are a beast!


#11

The only beastly thing is having a library of generic abstractions to paste from – makes things a bit easier and quicker.
If you have Quiver then I can give you a link to a copy of the generic functions that I use, pasting from it like this:


#12

Oh, wow. I’ve never heard of quiver before now. This looks really useful. I’ve been using a combination of gitlab and TextMate to do similar things, but this is intriguing. I’ll at least give the free trial a shot, and would love access to your generics.

Thank you, sincerely.