A macro to:
- copy the selected lines of a (tab-indented) text outline as OPML,
- optionally saving them to an opml file.
The default setting it to prompt the user for a file path, and save the OPML.
To disable the prompt and file save, (so that the macro simply copies from a selected text outline, placing OPML source in the clipboard), change the value of the saveAsOPML
KM variable in the macro from true
to false
.
Copy a TAB-indented outline as OPML.kmmacros (34.9 KB)
Javascript source (Tabbed text to OPML):
(() => {
'use strict';
// MAIN -----------------------------------------------
// main :: KM IO () -> OPMLString
const main = () =>
opmlFromTrees('Translation from tab indents',
treesFromLineIndents(
indentedLines(
Application('Keyboard Maestro Engine')
.getvariable('tabOutline')
)
)
);
// GENERIC FUNCTIONS --------------------------------------
// Node :: a -> [Tree a] -> Tree a
const Node = (v, xs) => ({
type: 'Node',
root: v, // any type of value (but must be consistent across tree)
nest: xs || []
});
// Tuple (,) :: a -> b -> (a, b)
const Tuple = (a, b) => ({
type: 'Tuple',
'0': a,
'1': b,
length: 2
});
// concat :: [[a]] -> [a]
// concat :: [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));
// cons :: a -> [a] -> [a]
const cons = (x, xs) => [x].concat(xs);
// filter :: (a -> Bool) -> [a] -> [a]
const filter = (f, xs) => xs.filter(f);
// foldl :: (a -> b -> a) -> a -> [b] -> a
const foldl = (f, a, xs) => xs.reduce(f, a);
// foldl1 :: (a -> a -> a) -> [a] -> a
const foldl1 = (f, xs) =>
xs.length > 1 ? xs.slice(1)
.reduce(f, xs[0]) : xs[0];
// fst :: (a, b) -> a
const fst = tpl => tpl[0];
// isNull :: [a] -> Bool
// isNull :: String -> Bool
const isNull = xs =>
Array.isArray(xs) || typeof xs === 'string' ? (
xs.length < 1
) : undefined;
// iterateUntil :: (a -> Bool) -> (a -> a) -> a -> [a]
const iterateUntil = (p, f, x) => {
let vs = [x],
h = x;
while (!p(h))(h = f(h), vs.push(h));
return vs;
};
// levels :: Tree a -> [[a]]
const levels = tree =>
map(xs => map(x => x.root, xs),
iterateUntil(
xs => xs.length < 1,
xs => concatMap(x => x.nest, xs), [tree]
)
);
// lines :: String -> [String]
const lines = s => s.split(/[\r\n]/);
// map :: (a -> b) -> [a] -> [b]
const map = (f, xs) => xs.map(f);
// 'The mapAccumL function behaves like a combination of map and foldl;
// it applies a function to each element of a list, passing an accumulating
// parameter from left to right, and returning a final value of this
// accumulator together with the new list.' (GHC via Hoogle)
// mapAccumL :: (acc -> x -> (acc, y)) -> acc -> [x] -> (acc, [y])
const mapAccumL = (f, acc, xs) =>
xs.reduce((a, x, i) => {
const pair = f(a[0], x, i);
return Tuple(pair[0], a[1].concat(pair[1]));
}, Tuple(acc, []));
// minimum :: Ord a => [a] -> a
const minimum = xs =>
xs.length > 0 ? (
foldl1((a, x) => x < a ? x : a, xs)
) : undefined;
// quot :: Int -> Int -> Int
const quot = (n, m) => Math.floor(n / m);
// showJSON :: a -> String
const showJSON = x => JSON.stringify(x, null, 2);
// unlines :: [String] -> String
const unlines = xs => xs.join('\n');
// unwords :: [String] -> String
const unwords = xs => xs.join(' ');
// words :: String -> [String]
const words = s => s.split(/\s+/);
// zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
const zipWith = (f, xs, ys) =>
Array.from({
length: Math.min(xs.length, ys.length)
}, (_, i) => f(xs[i], ys[i], i));
// READING TABBED TEXT ----------------------------
// indentedLines :: String -> [(Int, String)]
const indentedLines = strIndented => {
const
rgxIndent = /^(\s*)(.*)$/,
xs = concatMap(
s => {
const
lr = s.match(rgxIndent).slice(1),
w = lr[1];
return isNull(w) ? (
[]
) : [Tuple(lr[0].length, w)];
},
lines(strIndented)
),
indents = concatMap(x => {
const n = x[0];
return n > 0 ? [n] : [];
}, xs),
base = minimum(indents),
unit = minimum(
filter(x => x > 0,
mapAccumL(
(a, x) => Tuple(x, Math.abs(x - a)),
0, indents
)[1]
)
);
return unit > 1 ? map(
x => Tuple(
Math.floor((x[0] - base) / unit),
x[1]
),
xs
) : xs;
};
// treesFromLineIndents :: [(Int, String)] -> [Node String]
const treesFromLineIndents = xs =>
foldl((levels, tpl) => {
const
indent = tpl[0],
iNext = indent + 1,
iMax = levels.length - 1,
node = Node({
text: tpl[1],
level: tpl[0]
})
return (
levels[
indent < iMax ? (
indent
) : iMax
].nest.push(node),
iNext > iMax ? (
levels.push(node)
) : levels[iNext] = node,
levels
);
}, [Node(undefined, [])],
xs
)[0].nest;
// WRITING OPML -----------------------------------
// opmlFromTrees :: String -> [Node] -> OPML String
const opmlFromTrees = (strTitle, xs) => {
const
// ents :: [(Regex, String)]
ents = zipWith.apply(null,
cons(
(x, y) => [new RegExp(x, 'g'), '&' + y + ';'],
map(words, ['& \' " < >', 'amp apos quot lt gt'])
)
),
// entCoded :: a -> String
entCoded = v => ents.reduce(
(a, [x, y]) => a.replace(x, y),
v.toString()
),
// Nest -> Comma-delimited row indices of all parents in tree
// expands :: [textNest] -> String
expands = xs => {
const indexAndMax = (n, xs) =>
mapAccumL((m, node) =>
node.nest.length > 0 ? (() => {
const sub = indexAndMax(m + 1, node.nest);
return [sub[0], cons(m, concat(sub[1]))];
})() : [m + 1, []], n, xs);
return concat(indexAndMax(0, xs)[1]).join(',');
};
// tnOPML :: String -> Dict -> String
const tnOPML = indent => node => {
const root = node.root || {};
return indent + '<outline ' + unwords(map(
([k, v]) => k + '="' + entCoded(v) + '"',
cons(['text', root.text || ''], node.kvs || [])
)) + (node.nest.length > 0 ? (
'>\n' +
unlines(map(tnOPML(indent + ' '), node.nest)) +
'\n' +
indent + '</outline>'
) : '/>');
};
// OPML serialization -----------------------------
return unlines(concat([
[
'<?xml version=\"1.0\" encoding=\"utf-8\"?>',
'<opml version=\"2.0\">',
' <head>',
' <title>' + strTitle + '</title>',
' <expansionState>' + expands(xs) +
'</expansionState>',
' </head>',
' <body>'
],
map(tnOPML(' '), xs), [
' </body>',
'</opml>'
]
]));
};
// MAIN -----------------------------------------------
return main();
})();