JavaScript executed in the omniJS context is very fast, already cross-platform (macOS and iOS) for omniGraffle, and with luck and large amounts of hard work, may well become cross-platform for iOS too.
For the moment, however, it doesn’t yet have access to all of the UI, full clipboard Read/Write and other forms of access to the rest of the world that JavaScript for Automation (JXA), or iOS applications like 1Writer do.
Here is an experiment in getting around this by:
- Launching an omniJS OmniOutliner script from JXA (e.g. from the Script Editor or Script menu, or from the Atom editor Script plugin, or something like Keyboard Maestro, Fastscripts etc)
- Getting a result back from to OmniOutliner omniJS to JXA and the various forms of IO
This is just an interim workaround – my understanding is that Omni is working on providing something built-in.
Two main functions in the draft script below:
- A function (selnJSON) to be evaluated in the omniJS JavaScript context
- A function (evaluateOOomniJS) to be run from JXA, which submits the omniJS function, and harvests a result from it in the form of a JSON string, which can be converted to a JS Object with the standard
JSON.parse()
function.
All that this experimental script does is to read the selected OO row and all of its descendants, and convert them into a nested JSON format, optionally with a simple Markdown version of the topic text of each (bold and italic emphases, and markdown bracketing for any links.
Note: this draft is in ES6 JavaScript, making it compatible only with Sierra and onwards.
(() => {
'use strict';
// Author: Rob Trew (Twitter @complexpoint)
// Ver 0.01
// Date: 2017-08-21 18:52
// OMNIJS CONTEXT --------------------------------------------------------
const selnJSON = options => {
// foldl :: (b -> a -> b) -> b -> [a] -> b
const foldl = (f, a, xs) => xs.reduce(f, a);
// id :: a -> a
const id = x => x;
// isNull :: [a] | String -> Bool
const isNull = xs =>
Array.isArray(xs) || typeof xs === 'string' ? (
xs.length < 1
) : undefined;
// log :: a -> IO ()
const log = (...args) =>
console.log(
args
.map(x => JSON.stringify(x, null, 2))
.join(' -> ')
);
// map :: (a -> b) -> [a] -> [b]
const map = (f, xs) => xs.map(f);
// Value simply returned
// (with adjunct storage in a temporary OO node/row, for JXA to read)
// pure :: a -> M a
const pure = v => (
document.outline.rootItem.addChild(
null, x => x.topic = '|λ|omniJSON|λ|'
)
.addChild(
null, x => x.topic = show(2, v)
),
v
);
// readDecimal :: Decimal -> Float
const readDecimal = d =>
parseFloat(d.toString());
// show :: Int -> a -> Indented String
// show :: a -> String
const show = (...x) =>
JSON.stringify.apply(
null, x.length > 1 ? [x[1], null, x[0]] : x
);
// toLower :: Text -> Text
const toLower = s => s.toLowerCase();
const
outline = document.outline,
cols = outline.columns.filter(col => col.title.length > 0);
const valueJS = (k, v) =>
v !== null ? (
k > 'enumeration' ? (
k < 'rich-text' ? (
readDecimal(v) // 'number'
) : k !== 'state' ? (
v.string // 'rich-text'
) : undefined // 'state' (user defined box – not yet working ??)
) : k < 'duration' ? (
v // 'date'
) : k !== 'duration' ? (
undefined // 'enumeration' (popup - not yet working ??)
) : readDecimal(v) // 'duration'
) : undefined;
// Array of attribute runs -> inline Markdown string
// inLineMD :: OO Text -> String
const inlineMD = ooText =>
foldl(
(a, x) => {
const
style = x.style,
[vBold, blnItal, vLink] = map(
k => style.get(Style.Attribute[k]), //
['FontWeight', 'FontItalic', 'Link']
),
blnBold = vBold > 8,
strBold = ((!a.inBold && blnBold) ? '**' : ''),
strItal = ((!a.inItal && blnItal) ? '*' : ''),
strURL = vLink.string,
blnLink = (a.link === '') && (strURL !== '');
return {
md: a.md + [
strBold, strItal,
blnLink ? (
'[' + x.string + '](' + strURL + ')'
) : x.string,
strItal, strBold
].join(''),
inBold: blnBold,
inital: blnItal,
link: strURL
};
}, {
md: '',
inBold: false,
inItal: false,
link: ''
},
ooText.attributeRuns
)
.md;
// typeString :: Column.Type -> String
const typeString = type => type.identifier.split('.')
.slice(-1)[0];
// jsoOutline :: OO.TreeNode -> TextNest {text:String, nest:[TextNest]}
const jsoOutline = node => {
const
outline = document.outline,
[textMay, noteMay, eState] = map(
k => node.valueForColumn(outline[k + 'Column']), //
['outline', 'note', 'status']
),
xs = node.children;
return cols.reduce(
(a, col) => {
const k = toLower(col.title);
return !isNull(k) && (k !== 'topic') ? (
a[k] = valueJS(
typeString(col.type), node.valueForColumn(col)
), a
) : a;
}, {
text: textMay ? textMay.string : '',
md: (options.mdText && textMay) ? (
inlineMD(textMay)
) : undefined,
nest: xs.length > 0 ? map(jsoOutline, xs) : [],
note: noteMay ? noteMay.string : undefined,
status: eState === State.Unchecked ? (
false
) : eState === State.Checked ? (
true
) : undefined
}
);
};
// strJSON :: String
const strJSON = pure(document.editors[0].selectedNodes
.map(
n => jsoOutline(n)
)
);
};
// JXA CONTEXT -----------------------------------------------------------
// A list of functions applied to a list of arguments
// <*> :: [(a -> b)] -> [a] -> [b]
const ap = (fs, xs) => //
[].concat.apply([], fs.map(f => //
[].concat.apply([], xs.map(x => [f(x)]))));
// log :: a -> IO ()
const log = (...args) =>
console.log(
args
.map(JSON.stringify)
.join(' -> ')
);
// show :: Int -> a -> Indented String
// show :: a -> String
const show = (...x) =>
JSON.stringify.apply(
null, x.length > 1 ? [x[1], null, x[0]] : x
);
// evaluateOOomniJS :: (Options -> OO Maybe JSON String) -> {KeyValues}
// -> Maybe JSON String
const evaluateOOomniJS = (f, dctOptions) => {
const
oo = Application('OmniOutliner'),
ws = (oo.activate(), oo.windows),
mw = ws.length > 0 ? {
just: ws[0],
nothing: false
} : {
nothing: true
};
return mw.nothing ? mw : (() => {
const
a = Application.currentApplication(),
sa = (a.includeStandardAdditions = true, a),
w = mw.just;
// OMNIJS URL (FOR FUNCTION AND OPTIONS) ASSEMBLED, OPENED, COPIED
return ap([sa.openLocation], // sa.setTheClipboardTo], //
['omnioutliner:///omnijs-run?script=' +
encodeURIComponent(
'(' + f.toString() + ')(' +
(dctOptions && Object.keys(dctOptions)
.length > 0 ? show(dctOptions) : '') + ')'
)
]) && w.name()
.indexOf('Automation Console') === 0 ? ({
nothing: true,
msg: 'Front window was Automation Console'
}) : (() => {
//sa.doShellScript('sleep 0.25');
const rs = w ? w.document.rows.where({
_match: [ObjectSpecifier()
.topic, '|λ|omniJSON|λ|'
]
}) : [],
cs = rs.length > 0 ? rs[0].children : [],
c = cs.length > 0 ? cs.at(0) : undefined,
v = c ? c.topic() : '';
return (
// WITHOUT ANY TEMPORARY RESULT NODES --------------------
(() => {
var i = rs.length;
while (i--) rs[i].delete();
})(),
// MAYBE JSON RETURN VALUE -------------------------------
(v === undefined || v === null) ? ({
nothing: true,
msg: "No JSON found in " +
"temporary row '|λ|omniJSON|λ|'"
}) : {
just: v,
nothing: false
}
);
})();
})();
};
const mResult = evaluateOOomniJS(selnJSON, {
mdText: true // or false - to skip making an md field
});
console.log(mResult.just);
return mResult.nothing ? mResult : (
JSON.parse(mResult.just)
);
})();