Here is an example which adds three different descendant counts (for the selected row).
(In this version it adds them as column values).
There are a number of different approaches, but I personally use a general-purpose foldTree
function for any kind of bottom-to-top counting or summation which produces a single summary value for a whole nested structure like an outline.
It looks like this:
// foldTree :: (a -> [b] -> b) -> Tree a -> b
const foldTree = f => {
// A summary value obtained
// by a depth-first fold.
const go = tree => f(tree.root)(
tree.nest.map(go)
);
return go;
};
and it assumes that the tree structure has been wrapped up in a standard format with each parent node connected to a (possibly empty) list of its child nodes. The ‘constructor’ which I use to manufacture these nodes looks like this:
// Node :: a -> [Tree a] -> Tree a
const Node = v =>
// Constructor for a Tree node which connects a
// value of some kind to a list of zero or
// more child trees.
xs => ({
type: 'Node',
root: v,
nest: xs || []
});
We can wrap up the sub-tree of an OmniOutliner row in a nest of these nodes with this function:
// pureTreeOO :: OOItem -> Tree OOItem
const pureTreeOO = item => {
const go = x =>
Node(x)(x.hasChildren ? x.children.map(go) : []);
return go(item);
};
Once the sub-tree of a selected row has been captured in a standard Tree structure of these nested node objects, we can get various kinds of sum by writing things like this:
const
row = selectedItems[0],
tree = pureTreeOO(row),
childCount = tree.nest.length,
leafCount = 0 < childCount ? (
foldTree(
_ => xs => 0 !== xs.length ? (
sum(xs)
) : 1
)(tree)
) : 0,
descendantCount = 0 < childCount ? (
foldTree(
_ => xs => 1 + sum(xs)
)(tree)
) : 0;
and we could either add one or more of these values to the .topic
string of the selected row, or add them as separate column values.
To create or find a numeric column with a given name:
// columnFoundOrCreated :: [Column] ->
// Column.Type -> String -> Column
const columnFoundOrCreated = editor => outline =>
type => name => {
const
columns = outline.columns,
iFound = columns.findIndex(
col => (type === col.type) && (
name === col.title
)
);
return -1 !== iFound ? (
columns[iFound]
) : outline.addColumn(
type,
editor.afterColumn(columns[columns.length]),
col => col.title = name
);
};
and to assign a value to a column entry for a given row:
row.setValueForColumn(n, col)
Pulling it all together, to add counts to columns (for selected rows) in this kind of pattern:
We could write something like the source code (for a file with an .omnijs
extension), behind the disclosure triangle below:
JS Source
/*{
"author": "Rob Trew",
"targets": ["omnioutliner"],
"type": "action",
"identifier": "com.robtrew.nodecounts",
"version": "0.1",
"description": "Updated columns of child, descendant, and leaf counts",
"label": "nodeCounts",
"mediumLabel": "nodeCounts",
"paletteLabel": "nodeCounts",
}*/
(() => Object.assign(
new PlugIn.Action(selection => {
const main = () => {
const
outline = selection.outline,
editor = selection.editor,
selectedItems = selection.items;
return 0 < selectedItems.length ? (
(() => {
const
row = selectedItems[0],
tree = pureTreeOO(row),
childCount = tree.nest.length,
leafCount = 0 < childCount ? (
foldTree(
_ => xs => 0 !== xs.length ? (
sum(xs)
) : 1
)(tree)
) : 0,
descendantCount = foldTree(
_ => xs => 1 + sum(xs)
)(tree) - 1;
console.log(
zipWith(columnName => n => {
const
col = columnFoundOrCreated(
editor
)(outline)(
Column.Type.Number
)(columnName);
return (
row.setValueForColumn(n, col),
[columnName, n]
);
})([
'Children', 'Leaves', 'Descendants'
])([
childCount, leafCount, descendantCount
])
);
})()
) : (
new Alert(
'Count of descendants',
'\nSelect a row in OmniOutliner, and try again.'
).show()
);
};
// ----------- OMNIOUTLINER FUNCTIONS ------------
// pureTreeOO :: OOItem -> Tree OOItem
const pureTreeOO = item => {
const go = x =>
Node(x)(x.hasChildren ? x.children.map(go) : []);
return go(item);
};
// columnFoundOrCreated :: [Column] ->
// Column.Type -> String -> Column
const columnFoundOrCreated = editor => outline =>
type => name => {
const
columns = outline.columns,
iFound = columns.findIndex(
col => (type === col.type) && (
name === col.title
)
);
return -1 !== iFound ? (
columns[iFound]
) : outline.addColumn(
type,
editor.afterColumn(columns[columns.length]),
col => col.title = name
);
};
// ------------------- GENERIC -------------------
// Node :: a -> [Tree a] -> Tree a
const Node = v =>
// Constructor for a Tree node which connects a
// value of some kind to a list of zero or
// more child trees.
xs => ({
type: 'Node',
root: v,
nest: xs || []
});
// foldTree :: (a -> [b] -> b) -> Tree a -> b
const foldTree = f => {
// A summary value obtained
// by a depth-first fold.
const go = tree => f(tree.root)(
tree.nest.map(go)
);
return go;
};
// length :: [a] -> Int
const length = xs =>
// Returns Infinity over objects without finite
// length. This enables zip and zipWith to choose
// the shorter argument when one is non-finite,
// like cycle, repeat etc
'GeneratorFunction' !== xs.constructor
.constructor.name ? (
xs.length
) : Infinity;
// showLog :: a -> IO ()
const showLog = (...args) =>
console.log(
args
.map(JSON.stringify)
.join(' -> ')
);
// sum :: [Num] -> Num
const sum = xs =>
// The numeric sum of all values in xs.
xs.reduce((a, x) => a + x, 0);
// take :: Int -> [a] -> [a]
// take :: Int -> String -> String
const take = n =>
// The first n elements of a list,
// string of characters, or stream.
xs => 'GeneratorFunction' !== xs
.constructor.constructor.name ? (
xs.slice(0, n)
) : [].concat.apply([], Array.from({
length: n
}, () => {
const x = xs.next();
return x.done ? [] : [x.value];
}));
// 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_, ys_) => {
const lng = Math.min(length(xs_), length(ys_));
return take(lng)(xs_).map(
(x, i) => f(x)(ys_[i])
);
})(xs, ys);
// MAIN ---
main();
}), {
validate: selection => true
}
))();
and for other routes to such counts through API methods:
item.descendants.length
item.leaves.length
item.children.length
So, again in full:
JS Source B
/*{
"author": "Rob Trew",
"targets": ["omnioutliner"],
"type": "action",
"identifier": "com.robtrew.nodecountsB",
"version": "0.1",
"description": "Updated columns of child, descendant, and leaf counts",
"label": "nodeCountsB",
"mediumLabel": "nodeCountsB",
"paletteLabel": "nodeCountsB",
}*/
(() => Object.assign(
new PlugIn.Action(selection => {
const main = () => {
const
editor = selection.editor,
outline = selection.outline,
selectedItems = selection.items;
return 0 < selectedItems.length ? (
(() => {
const
row = selectedItems[0],
childCount = row.children.length,
leafCount = row.leaves.length,
descendantCount = row.descendants.length;
console.log(
zipWith(columnName => n => {
const
col = columnFoundOrCreated(
editor
)(outline)(
Column.Type.Number
)(columnName);
return (
row.setValueForColumn(n, col),
[columnName, n]
);
})([
'Children', 'Leaves', 'Descendants'
])([
childCount, leafCount, descendantCount
])
);
})()
) : (
new Alert(
'Count of descendants',
'\nSelect a row in OmniOutliner, and try again.'
).show()
);
};
// ----------- OMNIOUTLINER FUNCTIONS ------------
// columnFoundOrCreated :: [Column] ->
// Column.Type -> String -> Column
const columnFoundOrCreated = editor => outline =>
type => name => {
const
columns = outline.columns,
iFound = columns.findIndex(
col => (type === col.type) && (
name === col.title
)
);
return -1 !== iFound ? (
columns[iFound]
) : outline.addColumn(
type,
editor.afterColumn(columns[columns.length]),
col => col.title = name
);
};
// ------------------- GENERIC -------------------
// length :: [a] -> Int
const length = xs =>
// Returns Infinity over objects without finite
// length. This enables zip and zipWith to choose
// the shorter argument when one is non-finite,
// like cycle, repeat etc
'GeneratorFunction' !== xs.constructor
.constructor.name ? (
xs.length
) : Infinity;
// take :: Int -> [a] -> [a]
// take :: Int -> String -> String
const take = n =>
// The first n elements of a list,
// string of characters, or stream.
xs => 'GeneratorFunction' !== xs
.constructor.constructor.name ? (
xs.slice(0, n)
) : [].concat.apply([], Array.from({
length: n
}, () => {
const x = xs.next();
return x.done ? [] : [x.value];
}));
// 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_, ys_) => {
const lng = Math.min(length(xs_), length(ys_));
return take(lng)(xs_).map(
(x, i) => f(x)(ys_[i])
);
})(xs, ys);
// MAIN ---
main();
}), {
validate: selection => true
}
))();