Action
Import Taskpaper-formatted Draft into OmniOutliner
This Drafts 5 action imports a TaskPaper-formatted Draft into OmniOutliner using the new OmniJS API.
If you found this useful, you can:
Steps
-
script
const options = { content: draft.content }
-
script
// DRAFTS JS CODE -------------------------------------------------------- const draftsJSContext = () => { // main :: IO () const main = () => { return runOmniJSWithArgsFromDrafts( 'omnioutliner://localhost/omnijs-run', omniJSContext, options ) }; // OMNI JS CODE --------------------------------------- const omniJSContext = opts => { // main :: IO () const main = () => { const forest = compose( map( compose( fmapTree( x => ({ topic: x.text, note: x.note }) ), treeWithNotes ) ), forestFromTaskPaperString )(opts.content) return Document.makeNewAndShow( doc => ooRowsFromForest(doc.outline.rootItem)( forest ) ) }; // GENERIC FUNCTIONS ---------------------------------- // https://github.com/RobTrew/prelude-jxa // JS Prelude -------------------------------------------------- // Just :: a -> Maybe a const Just = x => ({ type: 'Maybe', Nothing: false, Just: x }); // 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 || [] }); // Nothing :: Maybe a const Nothing = () => ({ type: 'Maybe', Nothing: true, }); // Tuple (,) :: a -> b -> (a, b) const Tuple = a => b => ({ type: 'Tuple', '0': a, '1': b, length: 2 }); // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c const compose = (...fs) => // A function defined by the right-to-left // composition of all the functions in fs. fs.reduce( (f, g) => x => f(g(x)), x => x ); // div :: Int -> Int -> Int const div = x => y => Math.floor(x / y); // eq (==) :: Eq a => a -> a -> Bool const eq = a => // True when a and b are equivalent in the terms // defined below for their shared data type. b => { const t = typeof a; return t !== typeof b ? ( false ) : 'object' !== t ? ( 'function' !== t ? ( a === b ) : a.toString() === b.toString() ) : (() => { const kvs = Object.entries(a); return kvs.length !== Object.keys(b).length ? ( false ) : kvs.every(([k, v]) => eq(v)(b[k])); })(); }; // filter :: (a -> Bool) -> [a] -> [a] const filter = p => // The elements of xs which match // the predicate p. xs => [...xs].filter(p); // findIndices :: (a -> Bool) -> [a] -> [Int] // findIndices :: (String -> Bool) -> String -> [Int] const findIndices = p => xs => ( ys => ys.flatMap((y, i) => p(y, i, ys) ? ( [i] ) : []) )([...xs]) // first :: (a -> b) -> ((a, c) -> (b, c)) const first = f => // A simple function lifted to one which applies // to a tuple, transforming only its first item. xy => Tuple(f(xy[0]))( xy[1] ); // flip :: (a -> b -> c) -> b -> a -> c const flip = op => // The binary function op with its arguments reversed. 1 < op.length ? ( (a, b) => op(b, a) ) : (x => y => op(y)(x)); // fmapTree :: (a -> b) -> Tree a -> Tree b const fmapTree = f => { // A new tree. The result of a structure-preserving // application of f to each root in the existing tree. const go = tree => Node(f(tree.root))( tree.nest.map(go) ); return go; }; // foldTree :: (a -> [b] -> b) -> Tree a -> b const foldTree = f => { // The catamorphism on trees. A summary // value obtained by a depth-first fold. const go = tree => f(tree.root)( tree.nest.map(go) ); return go; }; // fst :: (a, b) -> a const fst = tpl => // First member of a pair. tpl[0]; // isSpace :: Char -> Bool const isSpace = c => // True if c is a white space character. /\s/.test(c); // 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; // lines :: String -> [String] const lines = s => // A list of strings derived from a single // newline-delimited string. 0 < s.length ? ( s.split(/[\r\n]/) ) : []; // list :: StringOrArrayLike b => b -> [a] const list = xs => // xs itself, if it is an Array, // or an Array derived from xs. Array.isArray(xs) ? ( xs ) : Array.from(xs || []); // map :: (a -> b) -> [a] -> [b] const map = f => // The list obtained by applying f // to each element of xs. // (The image of xs under f). xs => [...xs].map(f); // matching :: [a] -> (a -> Int -> [a] -> Bool) const matching = pat => { // A sequence-matching function for findIndices etc // findIndices(matching([2, 3]), [1, 2, 3, 1, 2, 3]) // -> [1, 4] const lng = pat.length, bln = 0 < lng, h = bln ? pat[0] : undefined; return x => i => src => bln && h == x && eq(pat)( src.slice(i, lng + i) ); }; // minimum :: Ord a => [a] -> a const minimum = xs => ( // The least value of xs. ys => 0 < ys.length ? ( ys.slice(1) .reduce((a, y) => y < a ? y : a, ys[0]) ) : undefined )(list(xs)); // nest :: Tree a -> [a] const nest = tree => { // Allowing for lazy (on-demand) evaluation. // If the nest turns out to be a function – // rather than a list – that function is applied // here to the root, and returns a list. const xs = tree.nest; return 'function' !== typeof xs ? ( xs ) : xs(root(x)); }; // Derive a function from the name of a JS infix operator // op :: String -> (a -> a -> b) const op = strOp => eval(`(a, b) => a ${strOp} b`); // partition :: (a -> Bool) -> [a] -> ([a], [a]) const partition = p => // A tuple of two lists - those elements in // xs which match p, and those which don't. xs => list(xs).reduce( (a, x) => p(x) ? ( Tuple(a[0].concat(x))(a[1]) ) : Tuple(a[0])(a[1].concat(x)), Tuple([])([]) ); // root :: Tree a -> a const root = tree => tree.root; // snd :: (a, b) -> b const snd = tpl => tpl[1]; // span, applied to a predicate p and a list xs, returns a tuple of xs of // elements that satisfy p and second element is the remainder of the list: // // > span (< 3) [1,2,3,4,1,2,3,4] == ([1,2],[3,4,1,2,3,4]) // > span (< 9) [1,2,3] == ([1,2,3],[]) // > span (< 0) [1,2,3] == ([],[1,2,3]) // // span p xs is equivalent to (takeWhile p xs, dropWhile p xs) // span :: (a -> Bool) -> [a] -> ([a], [a]) const span = p => // Longest prefix of xs consisting of elements which // all satisfy p, tupled with the remainder of xs. xs => { const ys = 'string' !== typeof xs ? ( list(xs) ) : xs, iLast = ys.length - 1; return splitAt( until( i => iLast < i || !p(ys[i]) )(i => 1 + i)(0) )(ys); }; // splitArrow (***) :: (a -> b) -> (c -> d) -> ((a, c) -> (b, d)) const splitArrow = f => // The functions f and g combined in a single function // from a tuple (x, y) to a tuple of (f(x), g(y)) // (see bimap) g => tpl => Tuple(f(tpl[0]))( g(tpl[1]) ); // splitAt :: Int -> [a] -> ([a], [a]) const splitAt = n => xs => Tuple(xs.slice(0, n))( xs.slice(n) ); // splitOn :: [a] -> [a] -> [[a]] // splitOn :: String -> String -> [String] const splitOn = pat => src => /* A list of the strings delimited by instances of a given pattern in s. */ ('string' === typeof src) ? ( src.split(pat) ) : (() => { const lng = pat.length, tpl = findIndices(matching(pat))(src).reduce( (a, i) => Tuple( fst(a).concat([src.slice(snd(a), i)]) )(lng + i), Tuple([])(0), ); return fst(tpl).concat([src.slice(snd(tpl))]); })(); // 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]; })); // uncons :: [a] -> Maybe (a, [a]) const uncons = xs => { // Just a tuple of the head of xs and its tail, // Or Nothing if xs is an empty list. const lng = length(xs); return (0 < lng) ? ( Infinity > lng ? ( Just(Tuple(xs[0])(xs.slice(1))) // Finite list ) : (() => { const nxt = take(1)(xs); return 0 < nxt.length ? ( Just(Tuple(nxt[0])(xs)) ) : Nothing(); })() // Lazy generator ) : Nothing(); }; // unlines :: [String] -> String const unlines = xs => // A single string formed by the intercalation // of a list of strings with the newline character. xs.join('\n'); // until :: (a -> Bool) -> (a -> a) -> a -> a const until = p => f => x => { let v = x; while (!p(v)) v = f(v); return v; }; // JS Trees ---------------------------------------------------- // forestFromLineIndents :: [(Int, String)] -> [Tree String] const forestFromLineIndents = tuples => { const go = xs => 0 < xs.length ? (() => { const [n, s] = Array.from(xs[0]); // Lines indented under this line, // tupled with all the rest. const [firstTreeLines, rest] = Array.from( span(x => n < x[0])(xs.slice(1)) ); // This first tree, and then the rest. return [ Node({ body: s, depth: n })(go(firstTreeLines)) ].concat(go(rest)); })() : []; return go(tuples); }; // forestFromTaskPaperString :: String -> Tree Dict const forestFromTaskPaperString = s => { const tpItemType = x => x.startsWith('- ') ? ({ text: x.slice(2), type: 'task' }) : x.endsWith(':') ? ({ text: x.slice(0, -1), type: 'project' }) : { text: x, type: 'note' }, tpTagDict = xs => xs.reduce((a, x) => { const kv = x.split('('); return Object.assign(a, { [kv[0]]: 1 < kv.length ? ( kv[1].split(')')[0] ) : '' }) }, {}), tpParse = dct => { const pair = splitArrow(tpItemType)(tpTagDict)( uncons( splitOn(' @')(dct.body) ).Just ); return Object.assign({}, dct, pair[0], { tags: pair[1] }); }; return compose( map(fmapTree(tpParse)), forestFromLineIndents, indentLevelsFromLines, filter(Boolean), lines )(s); }; // indentLevelsFromLines :: [String] -> [(Int, String)] const indentLevelsFromLines = xs => { const indentTextPairs = xs.map(compose( first(length), span(isSpace) )), indentUnit = minimum( indentTextPairs.flatMap(pair => { const w = fst(pair); return 0 < w ? [w] : []; }) ); return indentTextPairs.map( first(flip(div)(indentUnit)) ); }; // indentedLinesFromTrees :: String -> (a -> String) -> // [Tree a] -> [String] const indentedLinesFromTrees = strTab => f => trees => { const go = indent => node => [indent + f(node)] .concat(node.nest.flatMap(go(strTab + indent))); return trees.flatMap(go('')); }; // treeWithNotes :: Tree Dict -> Tree Dict const treeWithNotes = foldTree(item => subtrees => { const [withNotes, withoutNotes] = Array.from( partition( child => child.root.type === 'note' )(subtrees) ); return Node( Object.assign({}, item, { note: compose( unlines, indentedLinesFromTrees('\t')( compose( x => x.text, root ) ) )(withNotes) } ) )( withoutNotes ) }) // OmniOutliner OmniJS ----------------------------------------- // ooRowsFromForest :: OO Item -> [Tree] -> [OO Item] const ooRowsFromForest = parent => trees => { const go = parent => tree => { const item = parent.addChild( null, x => Object.assign( x, tree.root ) ); return ( tree.nest.map(go(item)), item ); }; return trees.map(go(parent)); }; // MAIN ----------------------------------------- return main() }; // runOmniJSWithArgsFromDrafts :: URL String -> Function -> [...OptionalArgs] -> a function runOmniJSWithArgsFromDrafts(baseURL, f) { const strCode = encodeURIComponent( `(${f})(${Array.from(arguments) .slice(2).map(JSON.stringify)})` ), strURL = `${baseURL}?script=${strCode}`; return app.openURL(strURL) } return main() }; draftsJSContext()
Options
-
After Success Default Notification Info Log Level Info
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.