Here, in the meanwhile (for discussion and adjustment) is a first draft of a fairly general Copy As TaskPaper script for Tinderbox 8.
This draft takes the approach of copying all key attributes for each item, (as TaskPaper tags) except where they have only a default value for that item.
(() => {
'use strict';
ObjC.import('AppKit');
// Copy As TaskPaper for Tinderbox 8
// Draft 0.6
// Copies either selected notes (with their descendants)
// or all notes if there is no selection.
// All key attributes with non-default values are copied
// as TaskPaper tags for each note.
// The plain text of any $Text value is appended to an item
// as a TaskPaper note. (i.e. with additional indentation)
// Date strings are normalized to the TaskPaper pattern.
// main :: IO ()
const main = () => (
tbxDocs => either(
alert('Copy as TaskPaper') // If there is a problem.
)(
copyText // Otherwise copy as TaskPaper text.
)(
bindLR(
0 < tbxDocs.length ? (
Right(tbxDocs.at(0))
) : Left('No documents open in Tinderbox.')
)(
tbxDoc => {
const
forest = map(
fmapPureTreeTBX(nameTextAndAttributeDict)
)(allOrSelectedTrees(tbxDoc)),
keyAttribs = allKeyAttributes(tbxDoc)(
forest
);
return Right(unlines(
indentedLinesFromForest('\t')(
tp3Item(keyAttribs)
)(forest)
));
}
)
)
)(
Application('Tinderbox 8').documents
);
// ------------- TASKPAPER OUTPUT FORMAT --------------
// iso8601Local :: Date -> String
const iso8601Local = dte =>
new Date(dte - (6E4 * dte.getTimezoneOffset()))
.toISOString();
// tpDateString :: Date -> String
const tpDateString = dte => {
const [d, t] = iso8601Local(new Date(dte)).split('T');
return [d, t.slice(0, 5)].join(' ');
};
// tp3Item :: Dict -> String -> Dict -> [String]
const tp3Item = keyAttribs =>
strIndent => dctNote => (
(blnProject, tags, strText) => [
(
blnProject ? (
dctNote.name + ':'
) : (strIndent + '- ' + dctNote.name)
) + (
0 < tags.length ? (
' ' + tags.join(' ')
) : ''
)
].concat(
0 < strText.length ? (
indented(strText)
)(strIndent + '\t') : []
)
)(
0 === strIndent.length,
dctNote.keyAttribs.flatMap(
tp3Tag(keyAttribs)(dctNote.attribs)
),
dctNote.text
);
// tp3Name :: String -> String
const tp3Name = k =>
toLower(k[0]) + k.slice(1);
// tp3TypeString :: String -> String -> String
const tp3TypeString = strType =>
strValue => 'date' === strType ? (
tpDateString(strValue)
) : strValue;
// tp3Tag :: Dict -> TBX attributes -> String -> [String]
const tp3Tag = docKeyAttribs =>
noteAttribs => k => (
(attrib, v) => {
const t = attrib.type;
return attrib.default !== v ? (
'boolean' !== t ? (
[`@${tp3Name(k)}(${tp3TypeString(t)(v)})`]
) : ['@' + tp3Name(k)]
) : []
})(
docKeyAttribs[k],
noteAttribs.byName(k).value()
);
// ------------------ TINDERBOX -------------------
// allKeyAttributes :: TBX Doc -> [Tree] ->
// [{ name :: String,
// attribs :: TBX attributes,
// keyAttribs :: [String]}]
const allKeyAttributes = doc =>
forest => (
docAttribs => Array.from(
new Set(
forest.flatMap(
foldTree(
x => xs => x.keyAttribs.concat(
concat(xs)
)
)
)
)
).reduce((a, k) => (
attrib => 0 < k.length ? (
Object.assign(
a, {
[k]: {
type: attrib.type(),
default: attrib.defaultvalue()
}
}
)
) : a
)(docAttribs.byName(k)), {})
)(doc.attributes);
// allOrSelectedTrees :: TBX Doc -> [TBX Note]
const allOrSelectedTrees = doc =>
// The top-level notes of the selection, or of
// the whole document if there is no selection.
tbxCommonAncestors((
selns => 0 < selns.length ? (
selns
) : doc.notes()
)(
doc.selections()
));
// attribVal :: String -> TBX Note -> String
const attribVal = k =>
// The value of a named Attribute
// for a given note.
note => note.attributeOf({
named: k
}).value();
// fmapPureTreeTBX :: (TBXNote -> a) ->
// TBXNote -> Tree TBXNote
const fmapPureTreeTBX = f => {
// Generic Tree model of a Tinderbox
// note and its descendants.
// Node contents are defined by the
// application of a function f to each note.
const go = x =>
Node(f(x))(
x.notes().map(go)
);
return go;
};
// nameTextAndAttributeDict :: TBX Note -> Dict
const nameTextAndAttributeDict = x => (
strKeyAttribs => ({
name: x.name(),
text: x.text(),
attribs: x.attributes,
keyAttribs: 0 < strKeyAttribs.length ? (
strKeyAttribs.split(';')
) : []
})
)(attribVal('KeyAttributes')(x));
// tbxCommonAncestors :: [TBX Notes] -> Set TBX Notes
const tbxCommonAncestors = notes => {
// A set of Tinderbox notes filtered to exclude
// those which descend from other items in
// the input list.
const go = dicts =>
2 > dicts.length ? (
dicts
) : ((dct, path) => [dct].concat(go(
dicts.slice(1).filter(
x => !x.path.startsWith(path)
)
)))(dicts[0], dicts[0].path),
ancestors = new Set(go(
sortBy(comparing(x => x.path))(
notes.map(x => ({
note: x,
path: attribVal('Path')(x)
}))
)
).map(x => x.note));
return notes.filter(x => ancestors.has(x))
};
// ---------------- GENERIC TREES -----------------
// indentedLinesFromForest :: String ->
// (String -> a -> [String]) -> [Tree a] -> [String]
const indentedLinesFromForest = strTab =>
// Indented text representation of a list of Trees.
// f is an (a -> String) function defining
// the string representation of tree nodes.
f => trees => {
const go = indent =>
node => f(indent)(node.root)
.concat(node.nest.flatMap(
go(strTab + indent)
));
return trees.flatMap(go(''));
};
// ----------------------- JXA ------------------------
// alert :: String => String -> IO String
const alert = title => s => (
sa => (
sa.activate(),
sa.displayDialog(s, {
withTitle: title,
buttons: ['OK'],
defaultButton: 'OK',
withIcon: sa.pathToResource('TaskPaper.icns', {
inBundle: 'Applications/TaskPaper.app'
})
}),
s
)
)(
Object.assign(Application('System Events'), {
includeStandardAdditions: true
})
);
// copyText :: String -> IO String
const copyText = s => (
pb => (
pb.clearContents,
pb.setStringForType(
$(s),
$.NSPasteboardTypeString
),
s
)
)($.NSPasteboard.generalPasteboard);
// GENERIC FUNCTIONS ----------------------------
// https://github.com/RobTrew/prelude-jxa
// Left :: a -> Either a b
const Left = x => ({
type: 'Either',
Left: 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 || []
});
// Right :: b -> Either a b
const Right = x => ({
type: 'Either',
Right: x
});
// bindLR (>>=) :: Either a ->
// (a -> Either b) -> Either b
const bindLR = m =>
mf => undefined !== m.Left ? (
m
) : mf(m.Right);
// comparing :: (a -> b) -> (a -> a -> Ordering)
const comparing = f =>
x => y => {
const
a = f(x),
b = f(y);
return a < b ? -1 : (a > b ? 1 : 0);
};
// concat :: [[a]] -> [a]
// concat :: [String] -> String
const concat = xs => (
ys => 0 < ys.length ? (
ys.every(Array.isArray) ? (
[]
) : ''
).concat(...ys) : ys
)(list(xs));
// either :: (a -> c) -> (b -> c) -> Either a b -> c
const either = fl =>
// Application of the function fl to the
// contents of any Left value in e, or
// the application of fr to its Right value.
fr => e => 'Either' === e.type ? (
undefined !== e.Left ? (
fl(e.Left)
) : fr(e.Right)
) : undefined;
// 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;
};
// indented :: String -> String -> [String]
const indented = s =>
strIndent => s.split(/[\r\n]/).map(
x => '' !== x ? strIndent + x : x
);
// 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);
// showLog :: a -> IO ()
const showLog = (...args) =>
console.log(
args
.map(JSON.stringify)
.join(' -> ')
);
// sortBy :: (a -> a -> Ordering) -> [a] -> [a]
const sortBy = f =>
xs => list(xs).slice()
.sort((a, b) => f(a)(b));
// toLower :: String -> String
const toLower = s =>
// Lower-case version of string.
s.toLocaleLowerCase();
// 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');
// MAIN ---
return main();
})();