Printing TaskPaper 3 documents with Marked 2 CSS templates

Here is a script for printing TaskPaper 3 documents through Brett Terspstra’s excellent Marked 2

Marked 2 lets you choose from a menu of alternative CSS templates, and exports to a range of formats, as well as printing directly.

The script simply:

  • Copies the active TaskPaper 3 document to the clipboard (in Markdown format), and,
  • if Marked 2 is installed and running on your system, displays a preview, allowing you to choose a CSS template and print or export.

(It doesn’t alter the TaskPaper 3 document itself – just places a markdown version in the clipboard)

// COPY THE ACTIVE TASKPAPER DOCUMENT INTO THE CLIPBOARD IN A FORM THAT CAN BE 
// PREVIEWED AND PRINTED (WITH CSS STYLESHEETS) USING MARKED 2


// appIsInstalled :: String -> Bool
function appIsInstalled(strBundleId) {
    ObjC.import('AppKit');

    return ObjC.unwrap(
        $.NSWorkspace.sharedWorkspace
        .URLForApplicationWithBundleIdentifier(
            strBundleId
        )
        .fileSystemRepresentation
    ) !== undefined;
}

var strKME = "com.stairways.keyboardmaestro.engine",
    blnKM = appIsInstalled(strKME),
    kmVars = blnKM ? Application(strKME)
    .variables : undefined;

// Marked 2 > Preview > Clipboard Preview
// http://marked2app.com/

// If Marked 2 is installed, the script opens it and displays a CSS-formatted clipboard preview


// Draft 0.09  Rob Trew 2016-04-13

// 0.09 Allows for hiding of @saved searches
// 0.08 Allows for printing only outline-visible + filter-visible lines

// 0.06 Allows for an API change in 3.2 Preview (197) 
//    (item.bodyContentString || item.bodyDisplayString)

// 0.05 adds further options (at bottom of script):
// 	- baseHeaderLevel
//  - hideProjectColon
//  - markedStyle
//  - tagEmphasis
//  - tagsToHide

// 0.02 adds option for blank line between a task and its note(s)

(function (dctOptions) {
    'use strict';


    // TASKPAPER CONTEXT

    function TaskPaperContext(editor, options) {

        // tagString :: item -> String
        function tagString(item, lstHidden, strMDChar) {
            strMDChar = strMDChar || '';

            var dctAttribs = item.attributes,
                lstTags = Object.keys(dctAttribs)
                .filter(function (k) {
                    return lstHidden.indexOf(k) === -1 &&
                        k !== 'data-type' && k !== 'indent';
                })
                .map(function (k) {
                    var v = dctAttribs[k];

                    return strMDChar + '@' + k.slice(5) +
                        (v ? '(' + v + ')' : '') + strMDChar;
                });

            return ' ' + lstTags.join(' ');
        }

        var blnNoteGap = parseInt(options.noteGap, 10),
            blnShowAll = !(parseInt(options.hideFoldedAndFiltered, 10)),
            blnHideSearches = parseInt(options.hideSavedSearches, 10),
            intBaseLevel = options.baseHeaderLevel,
            strStyle = options.markedStyle,
            blnColon = !parseInt(options.hideProjectColon, 10),
            maybeHidden = options.tagsToHide,
            lstHidden = maybeHidden instanceof Array ? ['search'].concat(
                maybeHidden)
            .map(
                function (tag) {
                    return tag !== '*' ? (
                        'data-' + (tag.charAt(0) === '@' ? tag.slice(1) :
                            tag)
                    ) : '*';
                }) : ['search'],
            blnAllTagsHidden = lstHidden.indexOf('*') !== -1,
            strEmphasis = options.tagEmphasis;

        // Lines to print
        var lstVisible = editor.outline.items
            .filter(function (x) {
                return (blnShowAll || editor.isDisplayed(x));
            }),
            lstItems = blnHideSearches ? lstVisible.filter(function (x) {
                return x.bodyString.indexOf('@search') !== 0;
            }) : lstVisible;
        lngLast = lstItems.length - 1;

        return (
            intBaseLevel ? 'Base Header Level: ' +
            intBaseLevel.toString() + '\n' : ''
        ) + (
            strStyle ? 'Marked Style: ' + strStyle + '\n\n' : ''
        ) + lstItems

        // Indent levels required by Markdown format
            .reduce(function (a, item) {
                var strType = item.getAttribute('data-type'),
                    blnProj = strType === 'project';

                if (blnProj) a.depth = item.depth;
                a.lines.push(
                    blnProj ? {
                        isProject: true,
                        item: item,
                        mdIndent: 0
                    } : {
                        isNote: strType !== 'task',
                        item: item,
                        mdIndent: item.depth - a.depth
                    }
                );

                return a;
            }, {
                lines: [],
                depth: 0
            })
            .lines


        // Hash prefixes, zero indents and extra line breaks for projects,
        // adjusted indents for tasks and notes.
            .map(function (mItem, i, xs) {

                var item = mItem.item,
                    blnProject = mItem.isProject,
                    blnNote = mItem.isNote || false,
                    blnTask = !(blnProject || blnNote),
                    blnGap = blnNote && blnNoteGap,
                    blnLastGap = (blnGap && (i < lngLast)) ? (!xs[i + 1]
                        .isNote
                    ) : false
                strText = (item.bodyContentString || item.bodyDisplayString ||
                    '');

                return (
                        blnProject ? (
                            '\n' + Array(item.depth + 1)
                            .join('#') + ' '
                        ) : Array(mItem.mdIndent)
                        .join('\t')
                    ) +
                    (blnTask ? '- ' : '') +
                    (blnGap ? '<p>' : '') +
                    strText +
                    (blnProject && blnColon ? ':' : '') +
                    (blnAllTagsHidden ? '' : tagString(item, lstHidden,
                        strEmphasis)) +
                    (blnLastGap ? '<p>' : '');
            })
            .join('\n');
    }


    // JAVASCRIPT FOR AUTOMATION CONTEXT

    // menuItemClick :: String -> [String] -> IO ()
    function menuItemClick(strAppID, lstMenuPath) {
        var oApp = Application(strAppID),
            strAppName = oApp.name(),
            lngChain = lstMenuPath.length,
            blnResult = false;

        if (lngChain > 1) {

            var appSE = Application("System Events"),
                lstApps = appSE.processes.where({
                    name: strAppName
                }),
                procApp = lstApps.length ? lstApps[0] : null;

            if (procApp) {
                oApp.activate();
                var strMenu = lstMenuPath[0],
                    fnMenu = procApp.menuBars[0].menus.byName(strMenu),
                    lngLast = lngChain - 1;

                for (var i = 1; i < lngLast; i++) {
                    strMenu = lstMenuPath[i];
                    fnMenu = fnMenu.menuItems[strMenu].menus[strMenu];
                }

                fnMenu.menuItems[
                    lstMenuPath[lngLast]
                    ].click();
                blnResult = true;
            }
        }
        return blnResult;
    }

    var strMarked2 = "com.brettterpstra.marked2";

    var ds = Application("TaskPaper")
        .documents,
        d = ds.length ? ds[0] : undefined;

    var strClip = d ? d.evaluate({
        script: TaskPaperContext.toString(),
        withOptions: dctOptions
    }) : '';


    // Place Markdown copy in clipboard,
    // displaying in Marked 2 if it is installed
    if (strClip.length > 0) {
        var a = Application.currentApplication(),
            sa = (a.includeStandardAdditions = true, a);

        sa.setTheClipboardTo(strClip);

        if (appIsInstalled(strMarked2)) {
            Application(strMarked2)
                .activate();

            menuItemClick(strMarked2, ['Preview', 'Clipboard Preview']);
        }
    }

    return strClip;

})(blnKM ? { // Read Keyboard Maestro values in macro
    // Integer 1-6  Render unindented projects as H1-H6
    baseHeaderLevel: kmVars['Base header level'].value(),

    // Bool (Print outline-visible and filter-visible lines only ?)
    hideFoldedAndFiltered: kmVars['Hide folded and filtered'].value(),

    // Bool 
    hideProjectColon: kmVars['Hide project colon'].value(),

    // Bool 
    hideSavedSearches: kmVars['Hide saved searches'].value(),

    // StyleName or empty string or undefined to use Marked default
    markedStyle: kmVars['Marked 2 style'].value(),

    // blank line between a task and its note text ?
    noteGap: kmVars['Gap between notes'].value(),

    // asterisk for italic, two for bold, backtick for code
    tagEmphasis: kmVars['Tag emphasis'].value(),

    // ['*'] hides all tags in printed version
    // ['done', 'due'] hides @done and @due tags
    // [] or false hides no tags
    tagsToHide: kmVars['Tags to hide'].value()
        .split(/[\s\,\;]+/)
} : {
    // USE MANUAL VALUES (1 for Bool true, 0 for Bool false) 
    // Integer 1-6  Render unindented projects as H1-H6
    baseHeaderLevel: 4,

    // 1|0 (Print outline-visible and filter-visible lines only ?)
    hideFoldedAndFiltered: 1,

    // 1|0 (true|false) 
    hideProjectColon: 1,

    // 1|0
    hideSavedSearches: 1,

    // StyleName or empty string or undefined to use Marked default
    markedStyle: 'Antique',

    // 1|0 blank line between a task and its note text ?
    noteGap: 1,

    // asterisk for italic, two for bold, backtick for code
    tagEmphasis: '*',

    // ['*'] hides all tags in printed version
    // ['done', 'due'] hides @done and @due tags
    // [] or false hides no tags
    tagsToHide: ['alert']
});
2 Likes

A couple of ways in which you can keep the TaskPaper 3 side of things as simple as possible,
and delegate any formatting and meta-layers (when/if you need them) to Marked 2:

Automatic outline numbering (and rendering of simple inline Markdown style emphases):

Running headers and footers,
and even, if your TaskPaper files grow huge, tables of contents :slight_smile:

Added a few more printing options to ver 0.5 above:

{
    baseHeaderLevel: 4, // Integer 1-6  Render unindented projects as H1-H6
    hideProjectColon: true,
    markedStyle: 'Antique', // or empty string or undefined to use Marked default
    noteGap: true, // blank line between a task and its note text ?
    tagEmphasis : '*', // asterisk for italic, two for bold, backtick for code
    tagsToHide: ['alert']
        // ['*'] hides all tags in printed version
        // ['done', 'due'] hides @done and @due tags
        // [] hides no tags
}
1 Like

Ver 0.06 updated above to allows for a small API change in 3.2 Preview (197)

(Should be backward compatible, I think)

(item.bodyContentString || item.bodyDisplayString)

A Keyboard Maestro version (⌘⌥ P for TaskPaper 3)

Print TaskPaper 3 document in Marked 2.kmmacros.zip (12.6 KB)

JavaScript source:

// COPY THE ACTIVE TASKPAPER DOCUMENT INTO THE CLIPBOARD IN A FORM THAT CAN BE 
// PREVIEWED AND PRINTED (WITH CSS STYLESHEETS) USING MARKED 2

Ver 0.2

var kmVars = Application("com.stairways.keyboardmaestro.engine")
    .variables;

// Marked 2 > Preview > Clipboard Preview
// http://marked2app.com/

// If Marked 2 is installed, the script opens it and displays a CSS-formatted clipboard preview

// Draft 0.09  Rob Trew 2016-04-13

// 0.09 Allows for hiding of @saved searches
// 0.08 Allows for printing only outline-visible + filter-visible lines

// 0.06 Allows for an API change in 3.2 Preview (197) 
//    (item.bodyContentString || item.bodyDisplayString)

// 0.05 adds further options (at bottom of script):
// 	- baseHeaderLevel
//  - hideProjectColon
//  - markedStyle
//  - tagEmphasis
//  - tagsToHide

// 0.02 adds option for blank line between a task and its note(s)

(function (dctOptions) {
    'use strict';


    // TASKPAPER CONTEXT

    function TaskPaperContext(editor, options) {

        // tagString :: item -> String
        function tagString(item, lstHidden, strMDChar) {
            strMDChar = strMDChar || '';

            var dctAttribs = item.attributes,
                lstTags = Object.keys(dctAttribs)
                .filter(function (k) {
                    return lstHidden.indexOf(k) === -1 &&
                        k !== 'data-type' && k !== 'indent';
                })
                .map(function (k) {
                    var v = dctAttribs[k];

                    return strMDChar + '@' + k.slice(5) +
                        (v ? '(' + v + ')' : '') + strMDChar;
                });

            return ' ' + lstTags.join(' ');
        }

        var blnNoteGap = options.noteGap,
            blnShowAll = !(parseInt(options.hideFoldedAndFiltered, 10)),
            blnHideSearches = parseInt(options.hideSavedSearches, 10),
            intBaseLevel = options.baseHeaderLevel,
            strStyle = options.markedStyle,
            blnColon = options.hideProjectColon === "0",
            maybeHidden = options.tagsToHide,
            lstHidden = maybeHidden instanceof Array ? ['search'].concat(
                maybeHidden)
            .map(
                function (tag) {
                    return tag !== '*' ? (
                        'data-' + (tag.charAt(0) === '@' ? tag.slice(1) :
                            tag)
                    ) : '*';
                }) : ['search'],
            blnAllTagsHidden = lstHidden.indexOf('*') !== -1,
            strEmphasis = options.tagEmphasis;

        // Lines to print
        var lstVisible = editor.outline.items
            .filter(function (x) {
                return (blnShowAll || editor.isDisplayed(x));
            }),
            lstItems = blnHideSearches ? lstVisible.filter(function (x) {
                return x.bodyString.indexOf('@search') !== 0;
            }) : lstVisible;
        lngLast = lstItems.length - 1;

        return (
            intBaseLevel ? 'Base Header Level: ' +
            intBaseLevel.toString() + '\n' : ''
        ) + (
            strStyle ? 'Marked Style: ' + strStyle + '\n\n' : ''
        ) + lstItems

        // Indent levels required by Markdown format
            .reduce(function (a, item) {
                var strType = item.getAttribute('data-type'),
                    blnProj = strType === 'project';

                if (blnProj) a.depth = item.depth;
                a.lines.push(
                    blnProj ? {
                        isProject: true,
                        item: item,
                        mdIndent: 0
                    } : {
                        isNote: strType !== 'task',
                        item: item,
                        mdIndent: item.depth - a.depth
                    }
                );

                return a;
            }, {
                lines: [],
                depth: 0
            })
            .lines


        // Hash prefixes, zero indents and extra line breaks for projects,
        // adjusted indents for tasks and notes.
            .map(function (mItem, i, xs) {

                var item = mItem.item,
                    blnProject = mItem.isProject,
                    blnNote = mItem.isNote || false,
                    blnTask = !(blnProject || blnNote),
                    blnGap = blnNote && blnNoteGap,
                    blnLastGap = (blnGap && (i < lngLast)) ? (!xs[i + 1]
                        .isNote
                    ) : false
                strText = (item.bodyContentString || item.bodyDisplayString ||
                    '');

                return (
                        blnProject ? (
                            '\n' + Array(item.depth + 1)
                            .join('#') + ' '
                        ) : (mItem.mdIndent > 1 ? Array(mItem.mdIndent) : [])
                        .join('\t')
                    ) +
                    (blnTask ? '- ' : '') +
                    (blnGap ? '<p>' : '') +
                    strText +
                    (blnProject && blnColon ? ':' : '') +
                    (blnAllTagsHidden ? '' : tagString(item, lstHidden,
                        strEmphasis)) +
                    (blnLastGap ? '<p>' : '');
            })
            .join('\n');
    }


    // JAVASCRIPT FOR AUTOMATION CONTEXT

    // appIsInstalled :: String -> Bool
    function appIsInstalled(strBundleId) {
        ObjC.import('AppKit');

        return ObjC.unwrap(
            $.NSWorkspace.sharedWorkspace
            .URLForApplicationWithBundleIdentifier(
                strBundleId
            )
            .fileSystemRepresentation
        ) !== undefined;
    }

    // menuItemClick :: String -> [String] -> IO ()
    function menuItemClick(strAppID, lstMenuPath) {
        var oApp = Application(strAppID),
            strAppName = oApp.name(),
            lngChain = lstMenuPath.length,
            blnResult = false;

        if (lngChain > 1) {

            var appSE = Application("System Events"),
                lstApps = appSE.processes.where({
                    name: strAppName
                }),
                procApp = lstApps.length ? lstApps[0] : null;

            if (procApp) {
                oApp.activate();
                var strMenu = lstMenuPath[0],
                    fnMenu = procApp.menuBars[0].menus.byName(strMenu),
                    lngLast = lngChain - 1;

                for (var i = 1; i < lngLast; i++) {
                    strMenu = lstMenuPath[i];
                    fnMenu = fnMenu.menuItems[strMenu].menus[strMenu];
                }

                fnMenu.menuItems[
                    lstMenuPath[lngLast]
                    ].click();
                blnResult = true;
            }
        }
        return blnResult;
    }

    var strMarked2 = "com.brettterpstra.marked2";

    var ds = Application("TaskPaper")
        .documents,
        d = ds.length ? ds[0] : undefined;

    var strClip = d ? d.evaluate({
        script: TaskPaperContext.toString(),
        withOptions: dctOptions
    }) : '';


    // Place Markdown copy in clipboard,
    // displaying in Marked 2 if it is installed
    if (strClip.length > 0) {
        var a = Application.currentApplication(),
            sa = (a.includeStandardAdditions = true, a);

        sa.setTheClipboardTo(strClip);

        if (appIsInstalled(strMarked2)) {
            Application(strMarked2)
                .activate();

            menuItemClick(strMarked2, ['Preview', 'Clipboard Preview']);
        }
    }

    return strClip;

})({
    // Integer 1-6  Render unindented projects as H1-H6
    baseHeaderLevel: kmVars['Base header level'].value(),

    // Bool (Print outline-visible and filter-visible lines only ?)
    hideFoldedAndFiltered: kmVars['Hide folded and filtered'].value(),

    // Bool 
    hideProjectColon: kmVars['Hide project colon'].value(),

    // Bool 
    hideSavedSearches: kmVars['Hide saved searches'].value(),

    // StyleName or empty string or undefined to use Marked default
    markedStyle: kmVars['Marked 2 style'].value(),

    // blank line between a task and its note text ?
    noteGap: kmVars['Gap between notes'].value(),

    // asterisk for italic, two for bold, backtick for code
    tagEmphasis: kmVars['Tag emphasis'].value(),

    // ['*'] hides all tags in printed version
    // ['done', 'due'] hides @done and @due tags
    // [] or false hides no tags
    tagsToHide: kmVars['Tags to hide'].value()
        .split(/[\s\,\;]+/)
});

it is very slow in processing - and it tends to hang when it encounters an @search on my machine. I am running the latest (production) release of both.

The updated Keyboard Maestro version above now adds two options:

  • Hide saved searches
  • Hide folded and filtered

On speed, the script itself should execute fast.

Marked 2’s webkit technology needs a good amount of resource (it essentially contains a browser) so make sure that RAM is not full of many other programs.

To test the script alone, without Marked 2, temporarily edit out the bundle identifier in the following line, so that the strMarked2 variable name is just bound to an empty string;

 //var strMarked2 = "com.brettterpstra.marked2";
var strMarked2 = "";

The script will then simply generate a MultiMarkdown version of the TaskPaper document, and I think you should find that it does that fairly fast.

(If not, you could look in the Console.app logs for anything unusual, and let us know what hardware and OS X version you are using. If the bottleneck appears to be in Marked 2, you could ask for advice on the Marked 2 support forum).

I can’t seem to get this to work either. I’m running TaskPaper v3.2 (200) on Mac OS X 10.11.4 (15E65) using Keyboard Maestro v7.1.

When I try to run this nothing happens & I don’t see anything related in my console. Any thoughts?

First check is to run the source in Script Editor and see whether it places a Markdown version (with some HTML tags) in your clipboard

I got: “Error on line 235: Error: Can’t get object.”

That means that the KM script hasn’t yet created the relevant variables

I’ve just downloaded the copy above and successfully run it from KM:

I suggest that you:

  • Delete the copy currently in KM
  • Download and install again
  • Try to run it
  • Then try a different hot-key to launch it (in case of some clash)
  • and also run the dialog action with the KM Try button to make sure that it is creating the set of variables

(You can inspect the variables which KM knows about through the KM menu system: Preferences > Variables )

Tried all of your suggestions, but still no luck. Variables are created tho.

http://quick.as/qmp9T9P1v

Can’t view your link from where I am but the next questions would be:

  • Is the KM dialog displaying ?
  • Is the clipboard content changing
  • If you check that you have copied every line of the source script (not always easy :slight_smile: and run in Script Editor, what does it now do, with the KM variables in place.

No, KM dialog is not displaying & the clipboard content is not changing.

When I run the script again (with every line copied first) & the variables in place I get the same error: “Error on line 235: Error: Can’t get object.”

You are running this KM macro and the dialog is not showing ?

Possibly then a question for forum.keyboardmaestro.com

Just sounds like some difficulty in getting a macro to launch from Command-Option-P

Hmmm…I’m stumped. Yeah, that’s the macro I’m running. What’s weird is I use KM macros like 300 times a day and have some pretty complex ones too, so this one shouldn’t be any different. Are you running KM 7?

Also, does Marked need to be open?

Just killed the KM engine & restarted it and the dialog box is now showing up & everything is working just fine!

Should’ve tried that first. Thanks for your help! Doh!

1 Like

Can’t get this to work either. The resultant text won’t parse with any of my markdown editors. It places the text on the clipboard, but Marked 2 just shows a window loading and never actually loads. I’ve tried pasting the text in a text document and opening it, but Marked 2, ia writer and multimarkdown composer all crash when trying to load the document.

I’ve just refreshed the .zip at Printing TaskPaper 3 documents with Marked 2 CSS templates - #5 by complexpoint above

Seems to be working fine here on:

  • OS X 10.11.5 with
  • TaskPaper 3.3 Preview (213) and
  • Marked 2 Version 2.5.6 (922)

If you are still having difficulties, and your versions are the same as these, perhaps you could send me, through forum mail, the simplest TaskPaper 3 file (input and clipboard output) that you are seeing this with, together with any error messages (e.g. in Console.app).