1Writer turns out to be an excellent companion for omniJS scripting of iOS OmniGraffle 3.
It’s a good powerful text editor, and it also has a well-developed JavaScript interface, from which iOS OmniGraffle scripts can be constructed and launched.
Here is a rough draft of a 1Writer script which converts the active 1Writer (tab-indented) outline into a tree diagram in current OG3 test builds.
(NB OmniGraffle 3 is still in a test build stage - so a couple of glitches:
- After the document is generated, you have to manually click layout now in OmniGraffle 3 which doesn’t yet seem to respond to the Canvas.layout() method in scripts.
(Until you do this all this shapes are initially piled on top of each other in the top-left corner of the canvas). - For some reason, at the moment even the layout now button doesn’t work until you have manually selected another canvas, and then reselected the newly-created tree diagram canvas
I’m sure these glitches will be ironed out at some point …
Images, and 1Writer script code (which creates and runs an omniJS script) below:
(() => {
'use strict';
// Ver .030
// DRAFT SCRIPT FOR TESTING PURPOSES ONLY - NOT FOR USE WITH REAL DATA
// GENERATE A NESTED DIAGRAM FROM A 1WRITER OUTLINE -----------------------
// outlineDiagram :: Node {text:String, [Node]}
function outlineDiagram(options) {
'use strict';
// CUSTOM STYLES ------------------------------------------------------
const
dctShapeStyle = {
//strokeColor: Color.RGB(0.0, 0.0, 0.0)
},
dctLineStyle = {
//strokeColor: Color.RGB(0.0, 0.0, 1.0)
};
// DEFAULT STYLES -----------------------------------------------------
// shapeDefaults :: Dictionary
const shapeDefaults = {
textSize: 12,
textHorizontalAlignment: HorizontalTextAlignment.Center,
strokeType: null,
strokeColor: null,
shadowFuzziness: 7,
cornerRadius: 9,
magnets: [new Point(0, -1), new Point(0, 1)]
};
// lineDefaults :: Dictionary
const lineDefaults = {
lineType: LineType.Orthogonal,
shadowColor: null,
strokeThickness: 2,
strokeColor: Color.RGB(1.0, 0.0, 0.0)
};
// TREE DRAWING -------------------------------------------------------
// newGraphic ::
// Canvas -> String -> [Dictionary] -> Maybe [Float] -> Graphic
const newGraphic = (cnv, strType, lstPropDicts, lstPosnSize) => {
const g = cnv['new' + strType]();
if (strType !== 'Line') {
const xywh = lstPosnSize.concat(
[0, 0, 100, 100].slice(lstPosnSize.length)
);
g.geometry = new Rect(xywh[0], xywh[1], xywh[2], xywh[3]);
}
return stylesApplied(lstPropDicts, g);
};
// stylesApplied :: [Dictionary] -> Graphic -> Graphic
const stylesApplied = (lstDicts, g) =>
concatMap(
d => map(k => [k, d[k]], Object.keys(d)),
lstDicts
)
.forEach(kv => g[kv[0]] = kv[1]) || g;
// treeDiagram ::
// Canvas -> Dictionary -> Dictionary -> Node -> Maybe Shape -> Node
const treeDiagram = (cnv, dctShpStyle, dctLnStyle, dctNode, parent) => {
const
nest = dctNode.nest,
shp = newGraphic(
cnv,
'Shape', [{
text: dctNode.text || ''
}, dctShpStyle], []
);
//dctNode.id = shp.id;
shp.name = dctNode.id;
//shp.magnets = [new Point(0, -1), new Point(0 ,1)];
// and any link connected and styled
if (parent !== undefined) {
const lnk = stylesApplied(
[dctLnStyle],
cnv.connect(parent, shp)
);
// link positioned behind parent Shape
typeof lnk.orderBelow(parent);
//dctNode.linkID = lnk.id;
}
// Recurse with any children
if ((nest !== undefined) && (!isNull(nest))) {
dctNode.nest = map(x => treeDiagram(
cnv, dctShpStyle, dctLnStyle, x, shp
), nest);
}
return dctNode;
};
// GENERIC FUNCTIONS --------------------------------------------------
// concatMap :: (a -> [b]) -> [a] -> [b]
const concatMap = (f, xs) => [].concat.apply([], xs.map(f));
// isNull :: [a] -> Bool
const isNull = xs => (xs instanceof Array) ? xs.length < 1 : undefined;
// log :: a -> IO ()
const log = x => console.log(show(x));
// map :: (a -> b) -> [a] -> [b]
const map = (f, xs) => xs.map(f);
// Any value -> optional number of indents -> String
// show :: a -> String
// show :: a -> Int -> String
const show = (...x) =>
JSON.stringify.apply(
null, x.length > 1 ? [x[0], null, x[1]] : x
);
// OUTLINE DIAGRAM RETURN ---------------------------------------------
const
jsoTree = options.tree,
cnv = addCanvas(),
layer = cnv.layers[0],
blnForest = Array.isArray(jsoTree),
lngForest = blnForest ? jsoTree.length : 0,
combined = Object.assign;
// Canvas settings
cnv.orderBefore(document.portfolio.canvases[0]);
cnv.layoutInfo.automaticLayout = false;
['Right', 'Down'].forEach(x => cnv['autosizes' + x] = true);
// Hide layer temporarily -- doesn't work in this build
// layer.visible = false;
// log(layer.locked);
// Set active view to this canvas - perhaps defer this ?
document.windows[0].selection.view.canvas = cnv; // ????
console.log('before dctDiagram') // Not showing up in console ...
const dctDiagram = treeDiagram(
cnv,
combined({}, shapeDefaults, dctShapeStyle),
combined({}, lineDefaults, dctLineStyle),
blnForest ? (
(lngForest === 1) ? jsoTree[0] : {
id: 'Root',
text: '',
nest: jsoTree
}
) : jsoTree
),
shpRoot = cnv.graphicWithName(dctDiagram.id);
console.log('after dctDiagram'); // Not showing up in console ...
//shpRoot.name = "Root";
//dctDiagram.id = "Root";
//shpRoot.notes = show(dctDiagram);
//console.log(shpRoot.notes);
// This doesn't yet trigger an automatic layout on iOS (works on macOS)
//cnv.layoutInfo.automaticLayout = true;
// So let's try this ...
cnv.layout(); // also not currently triggering a layout event
// This doesn't yet move the selection to the new canvas ... (works on macOS)
//document.windows[0].selection.view.canvas = cnv;
// So let's try this ...
//cnv.orderBefore(document.portfolio.canvases[0])
// Restore visibility of layer
// layer.visible = true;
log(layer.locked);
};
// GENERIC ----------------------------------------------------------------
// 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)]))));
// comparing :: (a -> b) -> (a -> a -> Ordering)
const comparing = f =>
(x, y) => {
const
a = f(x),
b = f(y);
return a < b ? -1 : (a > b ? 1 : 0);
};
// concat :: [[a]] -> [a] | [String] -> String
const concat = xs =>
xs.length > 0 ? (() => {
const unit = typeof xs[0] === 'string' ? '' : [];
return unit.concat.apply(unit, xs);
})() : [];
// concatMap :: (a -> [b]) -> [a] -> [b]
const concatMap = (f, xs) => [].concat.apply([], xs.map(f));
// curry :: Function -> Function
const curry = (f, ...args) => {
const go = xs => xs.length >= f.length ? (f.apply(null, xs)) :
function () {
return go(xs.concat(Array.from(arguments)));
};
return go([].slice.call(args, 1));
};
// dropWhile :: (a -> Bool) -> [a] -> [a]
const dropWhile = (p, xs) => {
let i = 0;
for (let lng = xs.length;
(i < lng) && p(xs[i]); i++) {}
return xs.slice(i);
};
// elem :: Eq a => a -> [a] -> Bool
const elem = (x, xs) => xs.indexOf(x) !== -1;
// flip :: (a -> b -> c) -> b -> a -> c
const flip = f => (a, b) => f.apply(null, [b, a]);
// intercalate :: String -> [a] -> String
const intercalate = (s, xs) => xs.join(s);
// length :: [a] -> Int
const length = xs => xs.length;
// lines :: String -> [String]
const lines = s => s.split(/[\r\n]/);
// log :: a -> IO ()
const log = (...args) =>
console.log(
args
.map(JSON.stringify)
.join(' -> ')
);
// map :: (a -> b) -> [a] -> [b]
const map = (f, xs) => xs.map(f);
// minimumByMay :: (a -> a -> Ordering) -> [a] -> Maybe a
const minimumByMay = (f, xs) =>
xs.reduce((a, x) => a.nothing ? {
just: x,
nothing: false
} : f(x, a.just) < 0 ? {
just: x,
nothing: false
} : a, {
nothing: true
});
// isNull :: [a] | String -> Bool
const isNull = xs =>
Array.isArray(xs) || typeof xs === 'string' ? (
xs.length < 1
) : undefined;
// show :: a -> String
// show :: a -> Int -> String
const show = (...x) =>
JSON.stringify.apply(
null, x.length > 1 ? [x[0], null, x[1]] : x
);
// splitBefore :: (a -> Bool) -> [a] -> [[a]]
const splitBefore = (p, xs) => {
const dct = xs.reduce((a, x) =>
p(x) ? {
splits: a.splits.concat([a.active]),
active: [x]
} : {
splits: a.splits,
active: a.active.concat(x)
}, {
splits: [],
active: []
});
return dct.splits.concat([dct.active]);
};
// splitOn :: String -> String -> [String]
const splitOn = (cs, xs) => xs.split(cs);
// stringChars :: String -> [Char]
const stringChars = s => s.split('');
// strip :: Text -> Text
const strip = s => s.trim();
// takeWhile :: (a -> Bool) -> [a] -> [a]
const takeWhile = (f, xs) => {
for (var i = 0, lng = xs.length;
(i < lng) && f(xs[i]); i++) {}
return xs.slice(0, i);
};
// unconsMay :: [a] -> Maybe (a, [a])
const unconsMay = xs => xs.length > 0 ? {
just: [xs[0], xs.slice(1)],
nothing: false
} : {
nothing: true
};
// NESTED TEXT -----------------------------------------------------------
// nestedLines :: Int -> [(Int, String)] -> Node {text:String, nest:[Node]}
const nestedLines = (startLevel, xs) =>
concatMap(
lines => {
const mbht = unconsMay(lines);
return mbht.nothing ? [] : (() => {
const [h, t] = mbht.just;
return ([h.level === startLevel ? { // Normal node
text: h.text,
nest: isNull(t) ? [] : nestedLines(startLevel + 1, t)
} : { // Virtual root
text: '',
nest: nestedLines(startLevel, lines)
}]);
})();
},
splitBefore(x => x.level === startLevel, xs)
);
// lineIndents :: String -> [(Int, String)]
const lineIndents = s =>
length(strip(s)) > 0 ? (() => {
const
lstLines = lines(s),
lineIndent = strLine => {
const xs = takeWhile(
curry(flip(elem))([' ', '\t']),
stringChars(strLine)
);
return length(xs) > 0 ? [xs] : [];
},
minDents = minimumByMay(
comparing(length),
concatMap(lineIndent, lstLines)
),
strIndent = minDents.nothing ? '\t' : concat(minDents.just);
return map(x => {
const
xs = curry(splitOn)(strIndent, x),
rs = dropWhile(x => x === '', xs);
return {
level: length(xs) - length(rs),
text: intercalate(strIndent, rs)
};
},
lstLines
);
})() : [{
level: 0,
text: ''
}];
const oneWriterTextNest = () =>
nestedLines(0, lineIndents(editor.getText()));
// 1WRITER -> OMNIJS ----------------------------------------------------------
// evaluateOmniJS :: (Options -> ()) -> { KeyValues ..}
const evaluateOmniJS = (f, dctOptions) => {
//ap([app.setClipboard, app.openURL], //
//[
return 'omnigraffle:///omnijs-run?script=' +
encodeURIComponent(
'(' + f.toString() + ')(' +
(dctOptions && Object.keys(dctOptions)
.length > 0 ? show(dctOptions) : '') + ')'
)
//]
//);
};
const strURL = evaluateOmniJS(outlineDiagram, {
// [Node {text:String, nest:[Node]}]
tree: oneWriterTextNest()
});
app.setClipboard(strURL);
app.openURL(strURL);
//ui.alert(strURL);
//return ui.alert(
// show(
// oneWriterTextNest()
// )
//);
})();