Script to convert OO to markdown, tex, and PDF using pandoc

Thanks, Rob. I actually managed to combine the three scripts I wanted into one (1, 2, 3).

Click to unfold
// any :: (a -> Bool) -> [a] -> Bool
    const any = (f, xs) => xs.some(f);

    // concat :: [[a]] -> [a] | [String] -> String
    const concat = xs =>
        xs.length > 0 ? (() => {
            const unit = typeof xs[0] === 'string' ? '' : [];
            return unit.concat.apply(unit, xs);
        })() : [];

    // 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));
    };

    // flip :: (a -> b -> c) -> b -> a -> c
    const flip = f => (a, b) => f.apply(null, [b, a]);

    // isInfixOf :: Eq a => [a] -> [a] -> Bool
    const isInfixOf = (needle, haystack) =>
        haystack.includes(needle);

    // log :: a -> IO ()
    const log = (...args) =>
        console.log(
            args
            .map(show)
            .join(' -> ') 
        ); 

    // min :: Ord a => a -> a -> a
    const min = (a, b) => b < a ? b : a; 

    // replicate :: Int -> a -> [a]
    const replicate = (n, a) => {
        let v = [a],
            o = [];
        if (n < 1) return o;
        while (n > 1) {
            if (n & 1) o = o.concat(v);
            n >>= 1;
            v = v.concat(v);
        }
        return o.concat(v);
    };

    // replicateS :: Int -> String -> String
    const replicateS = (n, s) => concat(replicate(n, s));

    // show :: a -> String
    const show = x => JSON.stringify(x, null, 2);

    // zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
    const zipWith = (f, xs, ys) =>
        Array.from({
            length: min(xs.length, ys.length)
        }, (_, i) => f(xs[i], ys[i]));
	
function run() {

// My pandoc template file; you'll need one of these to include necessary LaTeX front- and back-matter e.g. "\begin{document}." (You'll also need to look up where pandoc stores these and put yours there.)

var pandocCmd = '-drefs2 -dabntex';

// Setup

var app = Application.currentApplication();
app.includeStandardAdditions = true;
var OmniOutliner = Application('OmniOutliner');

// Get the current document

var doc = OmniOutliner.documents[0];

// Get the name (stripped of spaces) of the Omni Outliner document. The script may fail if your filename includes certain characters, e.g. parentheses.

var fileName = doc.name().replace(/\s/g, '');

// Create a directory on the desktop to hold our new files

var desktopString = app.pathTo("desktop").toString()
 app.doShellScript(`mkdir -p ${desktopString}/LaTeX/`);

// The text of the paper

var paperText = ""; 
var theRow="";
var newline = '\n';

// Loop through rows and append their text to paperText

doc.rows().forEach(function(theRow) {
 
   var pre = ""
   var post = ""
   var theSection=""; 

	if (Object.keys(theRow.style.namedStyles).length > 0) {
		switch(theRow.style.namedStyles[0].name()) {
						case "Heading 1":
							pre = newline + "# "
							post = "  " + newline
							break;
						case "Heading 2":
							pre = "## "
							post = "  " + newline
							break;
						case "Heading 3":
							pre = "### "
							post = "  " + newline
							break;                                
						case "Heading 4":
							pre = "#### "
							post = "  " + newline
							break;                                
						case "Heading 5":
							pre = "##### "
							post = "  " + newline
							break;                                
						case "Comentário":
							pre = "<!-- " 
							post = "-->  " + newline
							break;                                
						case "YAML":
							pre = ""
							post = ""
							break;                                
						case "Ordered List":
							pre = "1. "
							isList = true
							break;
						case "Unordered List":
							pre = "- "
							isList = true
							break;
						case "Task":
							pre = "1. [ ] "
							isList = true
							break;
						case "Paragraph":
							pre = newline
							post = "  " + newline
							break;
						case "YAMLitem":
							pre = ""
							post = ": | " + newline
							break;                
						case "YAMLtexto":
							pre = "  "
							post = "  "
							break;                
						case "HeadingNo":
							pre = "# "
							post = " {-}  "
							break;                
						case "Small":
							pre = newline + "<small>"
							post = "</small>  " + newline
							break;
						case "Blockquote":
							pre = "> "
							// post = newline
							break;

		}	
	}
		theSection = pre + cellTextMD(theRow) + post + newline + cellNoteMD(theRow);
		paperText += theSection;
		paperText += "\n\n";
});

// Convert the text of the paper to UTF8 encoding so pandoc can read it

paperText = $.NSString.alloc.initWithUTF8String(paperText);



// Write paperText to a new markdown file

var file = `${desktopString}/LaTeX/${fileName}.md`
paperText.writeToFileAtomicallyEncodingError(file, true, $.NSUTF8StringEncoding, null);

// Use pandoc to convert that markdown file to a tex file

shellCommand = `/usr/local/bin/pandoc ${desktopString}/LaTeX/${fileName}.md -f markdown ${pandocCmd} -o ${desktopString}/LaTeX/${fileName}.tex `;

 app.doShellScript(shellCommand);

// Compile our new tex file to PDF using xelatex

shellCommand = `/Library/TeX/texbin/xelatex --output-directory=${desktopString}/LaTeX/ ${desktopString}/LaTeX/${fileName}.tex && open ${desktopString}/LaTeX/${fileName}.pdf`;

 app.doShellScript(shellCommand);

return true;

}

// From apple's documentation for Javascript for Automation
 
function writeTextToFile(text, file, overwriteExistingContent) {
    try {
 
        // Convert the file to a string
        var fileString = file.toString()
 
        // Open the file for writing
        var openedFile = app.openForAccess(Path(fileString), { writePermission: true })
 
        // Clear the file if content should be overwritten
        if (overwriteExistingContent) {
            app.setEof(openedFile, { to: 0 })
        }
 
        // Write the new content to the file
        app.write(text, { to: openedFile, startingAt: app.getEof(openedFile) })
 
        // Close the file
        app.closeAccess(openedFile)
 
        // Return a boolean indicating that writing was successful
        return true
    }
    catch(error) {
 
        try {
            // Close the file
            app.closeAccess(file)
        }
        catch(error) {
            // Report the error is closing failed
            console.log(`Couldn't close file: ${error}`)
        }
 
        // Return a boolean indicating that writing was successful
        return false
    }
}

// Code below written by draft8, based on code written by SGIII, in turn adapted from AppleScript code written by Rob Trew


	
// contains :: String -> String -> Bool
    const contains = curry(flip(isInfixOf));
	
// cellTextMD :: OO.Cell -> String
    const cellTextMD = row => {
        const as = row.topic.attributeRuns;
        return zipWith3((txt, fnt, style) => {
                const
                    bld = any(contains(fnt), ['Bold', 'Black']) ? (
                        '**'
                    ) : '',
                    ital = any(contains(fnt), ['Oblique', 'Italic']) ? (
                        '*'
                    ) : '',
                    url = style.attributes.byName('link')
                    .value();
                return bld + ital + (
                    isNull(url) ? txt : ('[' + txt + '](' + url + ')')
                ) + ital + bld;
            }, as.text(), as.font(), as.style())
            .join('');
    };	

    const cellNoteMD = row => {
        const as = row.note.attributeRuns;
        return zipWith3((txt, fnt, style) => {
                const
                    bld = any(contains(fnt), ['Bold', 'Black']) ? (
                        '**'
                    ) : '',
                    ital = any(contains(fnt), ['Oblique', 'Italic']) ? (
                        '*'
                    ) : '',
                    url = style.attributes.byName('link')
                    .value();
                return '\n' + bld + ital + (
                    isNull(url) ? txt : ('[' + txt + '](' + url + ')')
                ) + ital + bld + '\n';
            }, as.text(), as.font(), as.style())
            .join('');
    };	

	
// zipWith3 :: (a -> b -> c -> d) -> [a] -> [b] -> [c] -> [d]
    const zipWith3 = (f, xs, ys, zs) =>
        Array.from({
            length: Math.min(xs.length, ys.length, zs.length)
        }, (_, i) => f(xs[i], ys[i], zs[i]));	
		
		
// isNull :: [a] | String -> Bool
    const isNull = xs =>
        Array.isArray(xs) || typeof xs === 'string' ? (
            xs.length < 1
        ) : undefined;		

The only thing I could not find a way to do was to perform the regex search and replace in the final text before converting it. With this I mean that I couldn’t do it using JS, but I could add another shell script to get it done in the script.

I am much more familiar with applescript, so I also made a version in this language (using this snippet) which I will probably end up using.

Click to unfold
use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions

property includeNotes : true
property convertWithPandoc : false
property openAfter : true
set PandocPath to "export PATH=/Library/TeX/texbin:$PATH && /usr/local/bin/pandoc"
set PandocDefaults to "-drefs2 -dabntex -dpdf"

set theText to {}

tell application "OmniOutliner"
	
	set theFile to get file of front document
	set thePath to POSIX path of theFile
	
	tell front document
		set theRows to rows
		
		-- set theRow to item 1 of theRows
		
		repeat with theRow in theRows
			set {theTxt, theNoteTxt, theStyles, thePre, thePos} to {"", "", "", "", ""} -- clean variables
			
			set theStyles to name of named styles of style of theRow -- get Named Styles
			set {thePre, thePos} to my translateStyle(theStyles) -- get what comes before the row text and what comes after
			
			set {lstText, lstFont} to {its text, its font} of topic's attribute runs of theRow
			set theTxt to my rtftomd(lstText, lstFont) -- check for italics and bold in row text
			
			set {lstNoteText, lstNoteFont} to {its text, its font} of note's attribute runs of theRow
			set theNoteTxt to my rtftomd(lstNoteText, lstNoteFont) -- check for italics and bold in note text
			
			set theTxt to thePre & theTxt & thePos & linefeed
			if includeNotes then
				if theNoteTxt is not "" then set theTxt to theTxt & linefeed & theNoteTxt & "  " & linefeed
			end if
			set theText to theText & theTxt
			
			
			
		end repeat
		
		--return theText as text
		--	set theRow to item 1 of theRows		
		--return theText as text
		
		set the clipboard to (theText as text)
		
	end tell
end tell


if convertWithPandoc then
	set theMDPath to thePath & ".md"
	set thePDFPath to thePath & ".pdf"
	
	
	do shell script "touch " & quoted form of theMDPath & " && LANG=pt_BR.UTF-8 pbpaste > " & quoted form of theMDPath
	set theSH to PandocPath & space & "-s" & space & quoted form of theMDPath & space & PandocDefaults & space & "-o" & space & quoted form of thePDFPath
	if openAfter then set theSH to theSH & "&& open " & quoted form of thePDFPath
	do shell script theSH
	
end if



on translateStyle(theStyles)
	set pre to linefeed
	set pos to "  "
	if theStyles contains "Heading 1" then set {pre, pos} to {linefeed & linefeed & "# ", "  "}
	if theStyles contains "Heading 2" then set {pre, pos} to {linefeed & linefeed & "# ", "  "}
	if theStyles contains "Heading 3" then set {pre, pos} to {linefeed & linefeed & "## ", "  "}
	if theStyles contains "Heading 4" then set {pre, pos} to {linefeed & linefeed & "### ", "  "}
	if theStyles contains "Heading 5" then set {pre, pos} to {linefeed & linefeed & "#### ", "  "}
	if theStyles contains "Heading 6" then set {pre, pos} to {linefeed & linefeed & "##### ", "  "}
	if theStyles contains "Heading 7" then set {pre, pos} to {linefeed & linefeed & "####### ", "  "}
	if theStyles contains "Comentário" then set {pre, pos} to {linefeed & "<!--", "-->" & linefeed}
	if theStyles contains "YAML" then set {pre, pos} to {"", ""}
	if theStyles contains "Ordered List" then set {pre, pos} to {"1. ", ""}
	if theStyles contains "Unordered List" then set {pre, pos} to {"- ", ""}
	if theStyles contains "Paragraph" then set {pre, pos} to {linefeed & "", "  "}
	if theStyles contains "YAMLitem" then set {pre, pos} to {"", ": |"}
	if theStyles contains "YAMLtexto" then set {pre, pos} to {linefeed & "  ", "  "}
	if theStyles contains "HeadingNo" then set {pre, pos} to {linefeed & "# ", " {-}  "}
	if theStyles contains "Small" then set {pre, pos} to {linefeed & "<small>", "</small>"}
	if theStyles contains "Blockquote" then set {pre, pos} to {linefeed & "> ", "  "}
	if theStyles contains "Code" then set {pre, pos} to {linefeed, ""}
	return {pre, pos}
end translateStyle


on rtftomd(lstText, lstFont)
	set outTxt to ""
	repeat with i from 1 to lstText's length
		set {aChunk, aFont} to {lstText's item i, lstFont's item i}
		if aFont contains "bold" or aFont contains "black" then
			set outTxt to outTxt & "**" & aChunk & "** "
		else if lstFont's item i contains "italic" then
			set outTxt to outTxt & "*" & aChunk & "*"
		else if lstFont's item i contains "link" then
			set outTxt to outTxt & "*" & aChunk & "*"
		else
			set outTxt to outTxt & aChunk
		end if
	end repeat
	return outTxt
end rtftomd


on fixHomePath(thePath)
	if thePath contains "~/" then
		set thePath to replaceText(thePath, "~/", "$HOME/")
	else
		set HomePath to (POSIX path of (path to home folder))
		set thePath to replaceText(thePath, HomePath, "$HOME/")
	end if
	return thePath
end fixHomePath


on replaceText(theString, old, new)
	set {TID, text item delimiters} to {text item delimiters, old}
	set theStringItems to text items of theString
	set text item delimiters to new
	set theString to theStringItems as text
	set text item delimiters to TID
	return theString
end replaceText
1 Like