web-based outliners such as Workflowy and Dynalist use markdown for formatting. When I import these .opml outlines into OO, italicized text appears between the tags and (bold would of course be and ). Is there an easy way to convert these to italicized text in OmniOutliner 5?
This seems exactly the kind that the omniJS interface should in principle be well suited to. I did try something like it with the JXA interface, but had to fall back on repeated use of a slowish .duplicate()
method (couldn’t get .insert()
to add rich text anywhere but at the start of a topic or note), and couldn’t figure out how to use the RichText constructor to directly produce the .paragraphs
etc of note cells and topic cells.
The result sort of works, but is a bit too slow and cumbersome for real use, I think.
I personally mainly work in plain text – but I do recommend (MD -> Rich Text) as an omniJS project to anyone who likes to work in that medium. I have sketched a set of draft JS functions for doing a simple parse of inline MD (bold, italic, links, but not emphases within link labels), and for creating an array of text run-dictionaries for mapping through the API to the rich text. Happy to share those if anyone wants to experiment with that next stage in omniJS.
For pasting, rather than opml import, you could look at things like:
Some functions and a basic test of appending a line of Markdown (emphases, links) to the note of the selected row. This version uses the JXA/Applescript interface rather than the omniJS API, which might well execute faster.
(ES6 JS, so Sierra onwards)
Not really usable till the writing of Rich Text attribute runs is properly solved, and gets a bit quicker and less clumsy. There may well be a good clear route – but I just haven’t managed to find it.
(() => {
'use strict';
// Rob Trew (c) 2017
// Sketches of some functions towards (Markdown -> Rich Text)
// GENERIC FUNCTIONS -----------------------------------------------------
// Regex (needs g flag for multiples) -> HayStack -> [MatchParts]
// allMatches :: Regex -> Text -> [Text]
const allMatches = (rgx, strHay) =>
unfoldr(
a => {
const xs = a.exec(strHay);
return Boolean(xs) ? {
just: [xs],
new: a
} : {
nothing: true
};
},
rgx
);
// (++) :: [a] -> [a] -> [a]
const append = (xs, ys) => xs.concat(ys);
// concat :: [[a]] -> [a] | [String] -> String
const concat = xs =>
xs.length > 0 ? (() => {
const unit = typeof xs[0] === 'string' ? '' : [];
return unit.concat.apply(unit, xs);
})() : [];
// cons :: a -> [a] -> [a]
const cons = (x, xs) => [x].concat(xs);
// elem :: Eq a => a -> [a] -> Bool
const elem = (x, xs) => xs.indexOf(x) !== -1;
// even :: Integral a => a -> Bool
const even = n => n % 2 === 0;
// foldl :: (b -> a -> b) -> b -> [a] -> b
const foldl = (f, a, xs) => xs.reduce(f, a);
// headMay :: [a] -> Maybe a
const headMay = xs =>
xs.length > 0 ? {
nothing: false,
just: xs[0]
} : {
nothing: true
};
// isInfixOf :: Eq a => [a] -> [a] -> Bool
const isInfixOf = (needle, haystack) =>
haystack.includes(needle);
// lastMay :: [a] -> Maybe a
const lastMay = xs => xs.length > 0 ? {
just: xs.slice(-1)[0]
} : {
nothing: true
};
// map :: (a -> b) -> [a] -> [b]
const map = (f, xs) => xs.map(f);
// mapAccumL :: (acc -> x -> (acc, y)) -> acc -> [x] -> (acc, [y])
const mapAccumL = (f, acc, xs) =>
xs.reduce((a, x) => {
const pair = f(a[0], x);
return [pair[0], a[1].concat([pair[1]])];
}, [acc, []]);
// 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(' -> ')
);
// odd :: Integral a => a -> Bool
const odd = n => !even(n);
// quotRem :: Integral a => a -> a -> (a, a)
const quotRem = (m, n) => [Math.floor(m / n), m % n];
// show :: Int -> a -> Indented String
// show :: a -> String
const show = (...x) =>
JSON.stringify.apply(
null, x.length > 1 ? [x[1], null, x[0]] : x
);
// snd :: (a, b) -> b
const snd = tpl => Array.isArray(tpl) ? tpl[1] : undefined;
// Splitting not on a delimiter, but whenever the relationship between
// two consecutive items matches a supplied predicate function
// splitBy :: (a -> a -> Bool) -> [a] -> [[a]]
const splitBy = (f, xs) =>
(xs.length < 2) ? [xs] : (() => {
const
h = xs[0],
lstParts = xs.slice(1)
.reduce(([acc, active, prev], x) =>
f(prev, x) ? (
[acc.concat([active]), [x], x]
) : [acc, active.concat(x), x], [
[],
[h],
h
]);
return lstParts[0].concat([lstParts[1]]);
})();
// splitOn :: a -> [a] -> [[a]]
// splitOn :: String -> String -> [String]
const splitOn = (needle, haystack) =>
typeof haystack === 'string' ? (
haystack.split(needle)
) : (function sp_(ndl, hay) {
const mbi = findIndex(x => ndl === x, hay);
return mbi.nothing ? (
[hay]
) : append(
[take(mbi.just, hay)],
sp_(ndl, drop(mbi.just + 1, hay))
);
})(needle, haystack);
// splitAt :: Int -> [a] -> ([a],[a])
const splitAt = (n, xs) => [xs.slice(0, n), xs.slice(n)];
// splitEvery :: Int -> [a] -> [[a]]
const splitEvery = (n, xs) => {
if (xs.length <= n) return [xs];
const [h, t] = [xs.slice(0, n), xs.slice(n)];
return [h].concat(splitEvery(n, t));
}
// 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);
};
// unfoldr :: (b -> Maybe (a, b)) -> b -> [a]
const unfoldr = (mf, v) => {
let xs = [];
return (until(
m => m.nothing,
m => {
const m2 = mf(m.new);
return (
xs = m2.nothing ? xs : xs.concat(m2.just),
m2
);
}, {
nothing: false,
just: v,
new: v,
}
), xs);
};
// until :: (a -> Bool) -> (a -> a) -> a -> a
const until = (p, f, x) => {
let v = x;
while (!p(v)) v = f(v);
return v;
};
// MARKDOWN AND RICH TEXT ATTRIBUTE RUNS ---------------------------------
// mdEmphasisRuns :: String -> [{text::String, bold::Bool, italic::Bool}]
const mdEmphasisRuns = s => {
const ems = ['*', '_'];
return snd(Boolean(length(s)) ? mapAccumL((a, [stars, txt]) => {
const
mbStart = headMay(txt),
[q, r] = quotRem(stars.length, 2),
blnBold = q > 0 ? (
(!a.bold && (!mbStart.nothing && (mbStart.just !== ' ')))
) : a.bold,
blnItalic = r > 0 ? (
(!a.italic && (!mbStart.nothing && (mbStart.just !== ' ')))
) : a.italic;
return [{
bold: blnBold,
italic: blnItalic
},
{
text: txt,
bold: blnBold,
italic: blnItalic
}
];
}, {
bold: false,
italic: false
}, splitEvery(2,
append(elem(headMay(s)
.just, ems) ? [] : [''],
map(concat,
splitBy(
(a, b) =>
elem(a, ems) !== elem(b, ems),
s.split('')
)
)
)
)) : [{
bold: false,
italic: false
},
[]
]);
};
// mdLinkRuns :: String -> [{ text::String, link::String }]
const mdLinkRuns = s => {
const dctParse = foldl((a, [lnk, lbl, url]) => {
const parts = splitOn(lnk, a.rest);
return length(parts) > 0 ? {
runs: a.runs.concat([{
text: parts[0]
}, {
text: lbl,
link: url
}]),
rest: parts[1]
} : {
runs: a.runs,
rest: ''
};
}, {
rest: s,
runs: []
}, allMatches(/\[([^\[]*)\]\(([^\)]*)\)/g, s));
return append(dctParse.runs, {
text: dctParse.rest
});
};
// mdInlineRuns :: String ->
// [{text :: String, bold :: Bool, italic :: Bool, link: String}]
const mdInlineRuns = s =>
foldl((a, x) => append(
a,
isInfixOf('](', x.text) ? map(
dct => Object.assign(dct, {
bold: x.bold,
italic: x.italic
}),
mdLinkRuns(x.text)
) : [x]
), [],
mdEmphasisRuns(s)
);
// mdAppendedAsRT :: RichText -> String -> RichText
const mdAppendedAsRT = (oRichText, strMD) =>
foldl(newRunWithProps, oRichText, mdInlineRuns(strMD));
// The function that follows is a very unpleasing hack ...
// I haven't figured out how to do it properly ....
// newRunWithProps :: RichText ->
// {text :: String, bold :: Bool, italic :: Bool, link :: String}
// -> RichText
const newRunWithProps = (oRun, dct) => {
const para = oRun.text()
.length > 0 ? oRun.paragraphs.last : (
oRun.text = ' ',
oRun
),
lastChar = para.characters.last,
intSize = lastChar.size(),
b = dct.bold,
i = dct.italic;
return (
lastChar.duplicate(), // Hack:: .insert() seems to fail so:
para.characters.last.size = intSize + 1, // Force a distinct attribute run
(() => {
const newRun = Object.assign(
para.attributeRuns.last, {
text: dct.text,
font: 'HelveticaNeue' + ((b || i) ? '-' : '') +
(b ? 'Bold' : '') + (i ? 'Italic' : ''),
}
),
linkAttrib = newRun.style.attributes.byName('link');
return (
(linkAttrib
.value = (Boolean(dct.link) ? (
dct.link
) : linkAttrib.defaultValue())),
newRun.size = intSize,
newRun
);
})()
);
};
// TEST ---------------------------------------------------------------------------
// Select an OO5 row and run this to write the MD into the note as rich text.
// A bit slow and hacky ...
const strTest = 'Here **are** [some](http://www.apple.com) links [in the](http://www.google.com) **text *before** us*.';
const
oo = Application('OmniOutliner'),
d = oo.documents.at(0),
row = d.selectedRows.at(0),
note = (row.noteExpanded = false, row.noteCell()),
rtf = note.richText,
delta = mdAppendedAsRT(rtf, strTest);
return (
row.noteExpanded = true,
delta
);
})();