If Pashua.app is installed, this macro will display a dialog.
If not, the options can be adjusted at the end of the script.
(The same could certainly be achieved with a KM HTML dialog – I just happen to be a little more familiar with Pashua.app dialogs)
The JavaScript for Automation binding functions for Pashua used here are adapted from:
Copy from TaskPaper 3 as numbered outline.kmmacros (40.2 KB)
JavaScript source:
// Copy TaskPaper Document (or selected section)
// as numbered text
// Author: Rob Trew Twitter: @complexpoint
// Ver : 0.02
// 0.02 Minor edits - fixed a tooltip, added a reference to the bindings at
// https://github.com/RobTrew/Pashua-Binding-JXA
// License MIT
// DESCRIPTION
// Copies document or selected section to clipboard as numbered text
// Initial defaults (see end of script) are:
// - 1-based ISO 2145 (1.1.1) numbering of whole document
// - (with outline tab-indentation preserved)
// If Carsten Blum's Pashua.app is installed in the Applications folder
// the script displays a dialog with various options
// The basic 1.1.1 pattern can be varied by replacing
// 1. The numeration symbols
// 0 or any other number will start a level series with that number
// Alphabetic characters can be used at any level in place of numbers
// i or I will be interpreted as specifying roman numerals
// 2. The delimiters
// Dots can be replaced by spaces, and/or one or more other \W characters
// Trailling dots can be suppressed (as per ISO 2145)
// with the option pruneLastDot:true (or edit to false)
// 3. The outline indents
// Numbered text can be left-aligned or outline-indented.
// The relevant option is preserveIndents:true (or edit to false)
// All of these options can be chosen either through the Pashua.app dialog
// or by manually editing the options at the end of the script.
(function (dctDefaults) {
// TASKPAPER CONTEXT
function getSelectionRoot(editor, options) {
// selectionRoots :: editor -> maybe [Item]
function selectionRoots(selection) {
var lngSeln = selection.selectedItems.length,
oStart = selection.startItem;
return lngSeln === 1 ? (
oStart.hasChildren ? [oStart] : [oStart.parent]
) : (lngSeln > 1 ? (
selection.selectedItemsCommonAncestors
.filter(function (x) {
return x.hasChildren || x.bodyString !== '';
})
) : undefined);
}
var outline = editor.outline,
selnRoots = selectionRoots(editor.selection);
return selnRoots ? {
docPath: outline.getPath(),
ids: selnRoots.map(function (x) {
return x.id;
}),
texts: selnRoots.map(function (x) {
return x.bodyString;
}),
docLevels: outline.root.descendants
.reduce(function (a, x) {
var intDepth = x.depth;
return intDepth > a ? intDepth : a;
}, 0),
selectionLevels: selnRoots.reduce(function (acc, item) {
var strID = item.id,
intDeepest = item.hasChildren ? (
item.descendants
.reduce(function (a, x) {
var intDepth = x.depth;
return (intDepth > a ? intDepth : a);
}, 0) - (item.depth - (strID !== 'Birch' ? 1 : 0))
) : 1;
return (intDeepest > acc ? intDeepest : acc);
}, 0)
} : undefined;
}
// getNumberedCopy :: editor -> options -> String
function getNumberedCopy(editor, options) {
// indexSymbol :: Integer -> String -> String
function indexSymbol(n, strStart) {
if (isNaN(strStart)) {
if (['i', 'I'].indexOf(strStart) !== -1) {
var strRoman = roman(n + 1);
return strStart === 'i' ? (
strRoman.toLowerCase()
) : strRoman;
} else {
var lstPoints = strStart.split('')
.map(function (c) {
return c.codePointAt(0);
});
return String.fromCodePoint.apply(
null,
init(lstPoints)
.concat(last(lstPoints) + n)
);
}
} else {
return (n + parseInt(strStart, 10))
.toString();
}
}
// roman :: Int -> String
function roman(n) {
return [
[1000, "M"],
[900, "CM"],
[500, "D"],
[400, "CD"],
[100, "C"],
[90, "XC"],
[50, "L"],
[40, "XL"],
[10, "X"],
[9, "IX"],
[5, "V"],
[4, "IV"],
[1, "I"]
]
.reduce(function (a, lstPair) {
var m = a.remainder,
v = lstPair[0];
return (v > m ? a : {
remainder: m % v,
roman: a.roman + Array(
Math.floor(m / v) + 1
)
.join(lstPair[1])
});
}, {
remainder: n,
roman: ''
})
.roman;
}
// Simple parse of model prefix string:
// Possibly empty opening string
// then lists of symbol plus affix (delimiter) pairs.
// numberScheme :: String
// -> {start: String, [{symbol:String, delim:String}]}
function numberScheme(strModel) {
var puncts = strModel.split(/\w+/),
strInit = puncts.length ? puncts[0] : undefined;
return {
start: strInit,
levels: strModel.split(/\W+/)
.reduce(function (a, s, i) {
return (
s && a.push({
symbol: s,
affix: puncts[
strInit ? i : i + 1
]
}),
a
);
}, [])
};
}
// Prefix string as function of a particular item's index path
// and a parse of the model prefix string
// numberPrefix :: [Int] ->
// {start:String, [{symbol:String, affix:String}]}
// -> String
function numberPrefix(lstIndices, dctScheme, blnSkipLastDot) {
var strPrefix = dctScheme.start + zipWith(
function (index, dct) {
return indexSymbol(
index, dct.symbol
) + dct.affix;
},
lstIndices,
dctScheme.levels
)
.join('');
return blnSkipLastDot && strPrefix.substr(-1) === '.' ? (
strPrefix.slice(0, -1)
) : strPrefix;
}
// Tree of wrapped items enriched with indexPath property
// withIndexPaths :: mItem -> mItem
function withIndexPaths(mItem) {
if (mItem.hasChildren) {
mItem.children
.reduce(function (a, mChild) {
return (mChild.hasChildren ||
mChild.item.bodyContentString
.trim() !== '') ? (
mChild.indexPath = (
mItem.indexPath || []
)
.concat(a), // extending path with extra index
a + 1 // and incrementing only for text/parents
) : a; // otherwise stet
}, 0);
}
return mItem;
}
// withSchemePrefixes :: mItem -> {start:, [{symbol:, affix:}]} -> mItem
function withSchemePrefixes(mItem, dctScheme, blnNoFinalDot) {
if (mItem.hasChildren) {
mItem.children.forEach(function (mChild) {
if (mChild.hasChildren ||
mChild.item.bodyContentString
.trim() !== '') {
mChild.numPrefix = numberPrefix(
mChild.indexPath,
dctScheme,
blnNoFinalDot
);
}
});
}
return mItem;
}
// Function f mapped over wrapped item and its
// descendants
// fmap :: (a -> b) -> f a -> f b
function fmap(f, x) {
var v = f(x);
return (
v.children = x.hasChildren ? x.children
.map(function (c) {
return fmap(f, c);
}) : [],
v
);
}
// Item and any descendants all wrapped
// in an interface which can hold additional properties
// a -> m a
function mItemUnit(item) {
var blnChiln = item.hasChildren;
return {
item: item,
children: blnChiln ? (
item.children.map(mItemUnit)
) : [],
hasChildren: blnChiln
};
}
// (a -> b -> c) -> [a] -> [b] -> [c]
function zipWith(f, xs, ys) {
var ny = ys.length;
return (xs.length <= ny ? xs : xs.slice(0, ny))
.map(function (x, i) {
return f(x, ys[i]);
});
}
// last :: [a] -> a
function last(xs) {
return xs.length ? xs.slice(-1)[0] : undefined;
}
// init :: [a] -> [a]
function init(xs) {
return xs.length ? xs.slice(0, -1) : undefined;
}
// numberedCopy :: mItem -> Bool -> String
function numberedCopy(mItem, blnIndented) {
var item = mItem.item;
return (blnIndented ? (
Array(item.depth + 1)
.join('\t')
) : '') +
(mItem.numPrefix || '') +
(blnIndented ? '\t' : '\t\t') +
(item.bodyContentString || '') +
(item.getAttribute('data-type') === 'project' ? (
':'
) : '') + '\n' +
(mItem.hasChildren ? mItem.children
.map(function (mChild) {
return numberedCopy(mChild, blnIndented);
}) : [])
.join('');
}
// selectionRoots :: editor -> maybe [Item]
function selectionRoots(selection) {
var lngSeln = selection.selectedItems.length,
oStart = selection.startItem;
return lngSeln === 1 ? (
oStart.hasChildren ? [oStart] : [oStart.parent]
) : (lngSeln > 1 ? (
selection.selectedItemsCommonAncestors
.filter(function (x) {
return x.hasChildren || x.bodyString !== '';
})
) : undefined);
}
// MAIN (getNumberedCopy)
var dctScheme = numberScheme(options.outlineStyle),
outline = editor.outline,
root = outline.root,
lstSelnRoots = selectionRoots(editor.selection),
blnPruneLastDot = options.pruneLastDot;
// Serialisation of a numbered wrapping of the outline (all / part)
return numberedCopy(
fmap(
function (mItem) {
return withSchemePrefixes(
withIndexPaths(mItem),
dctScheme,
blnPruneLastDot
)
},
mItemUnit(
options.selectionOnly && lstSelnRoots.length ? (
lstSelnRoots[0]
) : root
)
),
options.preserveIndents
);
}
// JAVASCRIPT FOR AUTOMATION CONTEXT
// PASHUA dialog functions
// https://www.bluem.net/en/mac/pashua/
// These functions are minimised adaptations from the bindings at
// https://github.com/RobTrew/Pashua-Binding-JXA
function showPashuaDialog(d, f) {
// showPashuaDialog :: String | Object -> maybe String -> Object
// Pashua dialog display ( See https://www.bluem.net/en/mac/pashua/ )
var b = Application.currentApplication(),
a = (b.includeStandardAdditions = !0, b),
b = $.NSFileManager.defaultManager,
g = "string" === typeof d ? d : asPashuaConfigString(d);
if (f) {
var e = a.pathTo("temporary items") + "/" +
a.randomNumber().toString().substring(3),
a = ($.NSString.alloc.initWithUTF8String(g)
.writeToFileAtomicallyEncodingError(
e, !0, $.NSUTF8StringEncoding, null
), a.doShellScript(
'"' + f + '/Contents/MacOS/Pashua" "' + e + '"'));
b.removeItemAtPathError(ObjC.unwrap($(e)
.stringByStandardizingPath), null);
return a.split(/[\n\r]+/).reduce(function (a, b) {
var c = b.trim();
c && (c = c.split("="), 1 < c.length && (a[c[0]] = c
.slice(1).join("=")));
return a;
}, {});
}
}
function asPashuaConfigString(a) {
// asPashuaConfigString :: [{name:String, type:String, ... }] -> String
// JS Object -> Pashua config string
var f = /[\n\r]+/,
g = /[\n\r]/gm;
lstElements = a instanceof Array ? a : [a];
return lstElements.reduce(function (a, b, h) {
var e = b.name + ".";
return b.name.length ? a + (h ? "\n\n" : "") + Object.keys(b)
.reduce(function (a, c) {
var d = b[c];
"options" !== c ? -1 !== ["#", "comment", "comments"]
.indexOf(c.toLowerCase()) ?
a.push(d.split(f).map(function (a) {
return "# " + a;
}).join("\n")) : "name" !== c && a.push(e + c + " = " +
("string" === typeof d ? d
.replace(g, "[return]") : d)) : a
.push(b.options.map(function (a) {
return e + "option = " + a;
}).join("\n"));
return a;
}, []).join("\n") : a;
}, "");
}
function maybePashuaPath(b) {
// maybePashuaPath :: maybe String -> maybe String
var a = Application.currentApplication(),
c = (a.includeStandardAdditions = !0, a),
e = $.NSFileManager.defaultManager,
a = c.pathTo(this).toString();
return [b && b.trim() || "", a + "/Contents/Resources/MacOS/",
a.split("/").slice(0, -1).join("/")
].concat(["user", "system"].map(function (a) {
return c.pathTo("applications folder", {
from: a + " domain"
}).toString();
})).reduce(function (a, d) {
if (a) return a;
if (d) {
var b = ("/" !== d.slice(-1) ? d + "/" : d) + "Pashua.app",
c = $();
e.attributesOfItemAtPathError(ObjC.unwrap($(b)
.stringByStandardizingPath), c);
if (undefined === c.code) return b;
}
}, undefined);
}
// JXA TaskPaper call
// nreps :: Int -> String -> String
function nreps(n, s) {
var o = '';
if (n < 1) return o;
while (n > 1) {
if (n & 1) o += s;
n >>= 1;
s += s;
}
return o + s;
}
var ds = Application('com.hogbaysoftware.TaskPaper3')
.documents;
if (ds.length) {
// What is selected in the editor ?
var d = ds[0],
dctSeln = d.evaluate({
script: getSelectionRoot.toString()
});
var strPashuaPath = maybePashuaPath(),
strDocPath = ObjC.unwrap(
$(dctSeln.docPath).stringByAbbreviatingWithTildeInPath
),
intSelnLevels = dctSeln.selectionLevels,
intDocLevels = dctSeln.docLevels,
lstScopeOptions = dctDefaults.scopeOptions,
lstIndentOptions = dctDefaults.indentOptions,
iLast = (intDocLevels * 2) - 1,
strDefault = dctDefaults.outlineStyle.slice(0, iLast),
lstUIParts = [{
"Comments": "Set window title",
"name": "*",
"title": "Copy as numbered outline"
}, {
"Comments": "File path display",
"name": "fp",
"type": "text",
"default": (strDocPath || 'Untitled'),
"tooltip": "Sequence of symbols and delimiters"
}, {
"Comments": "Section selected",
"name": "sln",
"type": "text",
"label": "(Selected sections – descendants not shown)",
"default": (dctSeln.texts.length ? dctSeln.texts[0] : ''),
"tooltip": "Top level of selection"
}, {
"Comments": "Combo for numbering pattern",
"name": "outlineStyle",
"type": "combobox",
"label": "Outline numbering pattern:",
"default": strDefault,
"options": [
strDefault,
'0.' + nreps(intDocLevels - 1, '1.').slice(0, -1),
'A I 1 i'.slice(0, iLast),
'A 1.1.1'.slice(0, iLast),
],
"tooltip": "Sequence of symbols and delimiters"
}, {
"Comments": "Trim final dot",
"name": "pruneLastDot",
"type": "checkbox",
"default": dctDefaults.pruneLastDot ? 1 : 0,
"label": "Trailling dots trimmed off",
"tooltip": "Remove any trailling dots from numbering"
}, {
"Comments": "Indents preserved or removed",
"name": "preserveIndents",
"type": "radiobutton",
"default": lstIndentOptions[
dctDefaults.preserveIndents ? 0 : 1
],
"label": "Indents:",
"options": lstIndentOptions,
"tooltip": "Preserve outline indents, or left-align"
}, {
"Comments": "Default button - Copy",
"name": "db",
"type": "defaultbutton",
"label": "Copy",
"tooltip": "Copy as numbered outline"
}, {
"Comments": "Cancel button",
"name": "cb",
"type": "cancelbutton",
"label": "Cancel",
"tooltip": "Close this dialog"
}, {
"Comments": "Selection or whole document",
"name": "selectionOnly",
"type": "radiobutton",
"label": "Copy:",
"options": lstScopeOptions,
"default": lstScopeOptions[
dctDefaults.selectionOnly ? 1 : 0
],
"tooltip": "Whole doc or selection & descendants"
}],
dctChoice = (
dctDefaults.useDialogIfPashuaFound && strPashuaPath
) ? (
showPashuaDialog(
lstUIParts,
strPashuaPath
)
) : dctDefaults;
// What is the user response to the dialog ?
//return JSON.stringify(dctChoice);
// What does a numbered version look like ?
var strClip = d.evaluate({
script: getNumberedCopy.toString(),
withOptions: (
dctChoice.selectionOnly = (
dctChoice.selectionOnly === true ||
dctChoice.selectionOnly === dctDefaults.scopeOptions[1]
),
dctChoice.preserveIndents = (
dctChoice.preserveIndents === true ||
dctChoice.preserveIndents === dctDefaults.indentOptions[0]
),
dctChoice
)
});
if (strClip) {
var a = Application.currentApplication(),
sa = (a.includeStandardAdditions = true, a),
maybeFile = Application('TaskPaper').documents[0].file();
sa.setTheClipboardTo(strClip);
sa.displayNotification(
strClip, {
withTitle: 'Numbered copy',
subtitle: 'From: ' + (maybeFile ? (
ObjC.unwrap(
$(maybeFile.toString())
.stringByAbbreviatingWithTildeInPath
)
) : 'Untitled'),
soundName: 'default'
}
)
return strClip;
}
}
})({
outlineStyle: '1.1.1.1.1.1.1.1.1',
pruneLastDot: true,
preserveIndents: true,
selectionOnly: false,
scopeOptions: [
"Whole document",
"Selection & descendants"
],
indentOptions: [
"Preserve indents",
"Align numbered paras to left"
],
// https://www.bluem.net/en/mac/pashua/
useDialogIfPashuaFound: true
});