Starting with something simple - reports which just sort and group a defined set of items as a flat list (without indented descendants), here is a draft in which you can set some options (at the foot of the script), including the path of the output file to write to.
This version is for the front TaskPaper document in the running app.
Iām not sure how self-explanatory the options at the end are, but note that the orderBy field, needs to be a [ list ] (JS array), even if it contains only one string. The first string specifies primary sort key and direction, and any further strings can specify secondary and tertiary etc sorts.
To avoid grouping, edit the groupBy field to an empty string.
(Test with dummy data from JavaScript mode of Script Editor, or from an Execute JXA action in Keyboard Maestro, etc)
// (c) 2017 Rob Trew
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// Write sorted and optionally grouped report from TaskPaper to new file
// VER 0.03
// Sample report spec (see end of script):
// {
// outPutFile: '~/Desktop/report.taskpaper',
// itemsInPath: '/@type=project except Archive',
// orderBy: [
// '@priority descending as number',
// '@effort descending as number',
// ],
// groupBy: '@priority as number descending'
// }
(dctOptions => {
'use strict';
// TASKPAPER 3 CONTEXT ---------------------------------------------------
const taskpaperContext = (editor, options) => {
// GENERIC FUNCTIONS -------------------------------------------------
// compare :: a -> a -> Ordering
const compare = (a, b) => a < b ? -1 : (a > b ? 1 : 0);
// 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);
};
// concatMap :: (a -> [b]) -> [a] -> [b]
const concatMap = (f, xs) =>
xs.length > 0 ? [].concat.apply([], xs.map(f)) : [];
// Handles two or more arguments
// curry :: ((a, b) -> c) -> a -> b -> c
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));
};
// drop :: Int -> [a] -> [a]
// drop :: Int -> String -> String
const drop = (n, xs) => xs.slice(n);
// elem :: Eq a => a -> [a] -> Bool
const elem = (x, xs) => xs.indexOf(x) !== -1;
// elems :: Dict -> [a]
const elems = Object.values;
// eq (==) :: Eq a => a -> a -> Bool
const eq = (a, b) => a === b;
// foldl :: (a -> b -> a) -> a -> [b] -> a
const foldl = (f, a, xs) => xs.reduce(f, a);
// groupBy :: (a -> a -> Bool) -> [a] -> [[a]]
const groupBy = (f, xs) => {
const dct = xs.slice(1)
.reduce((a, x) => {
const h = a.active.length > 0 ? a.active[0] : undefined;
return h !== undefined && f(h, x) ? {
active: a.active.concat([x]),
sofar: a.sofar
} : {
active: [x],
sofar: a.sofar.concat([a.active])
};
}, {
active: xs.length > 0 ? [xs[0]] : [],
sofar: []
});
return dct.sofar.concat(
dct.active.length > 0 ? [dct.active] : []
);
};
// intercalate :: String -> [String] -> String
const intercalate = (s, xs) => xs.join(s);
// isNull :: [a] -> Bool
// isNull :: String -> Bool
const isNull = xs =>
Array.isArray(xs) || typeof xs === 'string' ? (
xs.length < 1
) : undefined;
// isPrefixOf :: [a] -> [a] -> Bool
const isPrefixOf = (xs, ys) => {
const pfx = (xs, ys) => xs.length ? (
ys.length ? xs[0] === ys[0] && pfx(
xs.slice(1), ys.slice(1)
) : false
) : true;
return typeof xs !== 'string' ? pfx(xs, ys) : ys.startsWith(xs);
};
// length :: [a] -> Int
const length = xs => xs.length;
// map :: (a -> b) -> [a] -> [b]
const map = (f, xs) => xs.map(f);
// mappendComparing :: [((a -> b), Bool)] -> (a -> a -> Ordering)
const mappendComparing = fboolPairs =>
(x, y) => fboolPairs.reduce(
(ord, [f, b]) => ord !== 0 ? (
ord
) : (
b ? compare(f(x), f(y)) : compare(f(y), f(x))
), 0
);
// sortBy(on(compare,length), xs)
// on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
const on = (f, g) => (a, b) => f(g(a), g(b));
// show :: Int -> a -> Indented String
// show :: a -> String
const show = (...x) =>
JSON.stringify.apply(
null, x.length > 1 ? [x[1], null, x[0]] : x
);
// sortBy :: (a -> a -> Ordering) -> [a] -> [a]
const sortBy = (f, xs) =>
xs.slice()
.sort(f);
// tailDef :: [a] -> [a]
const tailDef = xs => xs.length > 0 ? xs.slice(1) : [];
// toLower :: String -> String
const toLower = s => s.toLowerCase();
// toTitle :: String -> String
const toTitle = s =>
(s.length ? s[0].toUpperCase() + s.slice(1)
.toLowerCase() : '');
// unlines :: [String] -> String
const unlines = xs => xs.join('\n');
// unwords :: [String] -> String
const unwords = xs => xs.join(' ');
// words :: String -> [String]
const words = s => s.split(/\s+/);
// N-ARY SORTS BY ASCENDING OR DESCENDING KEYS -------------------
// Dictionary of sort features -> (getFunction, Ascending?) pair
// customSort :: {key::String, asc::Bool, type::JS Class} ->
// ((Item -> a), Bool)
const customSort = dct => [
propValFn(dct.key, dct.type), dct.asc
];
// Blacklist -> Attributes dictionary -> String of remaining tags
// exceptTags :: [String] -> Dict -> String
const exceptTags = (exclns, dct) =>
unwords(concatMap(
dk => {
const
k = isPrefixOf('data-', dk) ? drop(5, dk) : '',
v = dct[dk];
return (isNull(k) || elem(k, exclns)) ? [] : [
'@' + k + (v !== undefined ? '(' + v + ')' : '')
];
}, Object.keys(dct)
));
// propValFn :: {key::String, asc::Bool, type::JS Class}
// -> (Item -> a)
const propValFn = (k, type) =>
item => ((k !== 'text') ? (
item.getAttribute('data-' + k, type)
) : item.bodyContentString);
const groupValFn = (k, type) =>
group => ((k !== 'text') ? (
group[0].getAttribute('data-' + k, type)
) : group[0].bodyContentString);
// pure :: a -> f a
const pure = x => [x];
// specParse :: String -> (String, Bool, maybe JS Class)
const specParse = s =>
foldl((a, k, i) => {
return k[0] !== '@' ?
elem(
k, ['asc', 'desc', 'ascending', 'descending']
) ? (
a.asc = k[0] === 'a',
a
) : (
elem(k, ['date', 'num', 'number']) ? (
a.type = ((k[0] !== 'd') ? Number : Date),
a
) : a // Unknown word - ignored
) : (
a.key = tailDef(k), // @key
a
);
}, {
key: 'text',
asc: true,
type: undefined
},
words(toLower(s))
);
const
outline = editor.outline,
itemSortFns = map(
ks => customSort(specParse(ks)),
options.orderBy
),
mbGroup = options.groupBy && (options.groupBy.length > 1) ? {
nothing: false,
just: specParse(options.groupBy)
} : {
nothing: true
},
groupProp = mbGroup.nothing ? (
_ => ''
) : propValFn(mbGroup.just.key, mbGroup.just.type),
// Grouping function - single group if no property specified
// groupByPropVal :: [a] -> [[a]]
groupByPropVal = mbGroup.nothing ? (
pure
) : curry(groupBy)(
on(eq, propValFn(mbGroup.just.key, mbGroup.just.type))
);
// itemReport :: Bool -> [String] -> TP3 Item -> String
const itemReport = (blnFull, excludedTags, item) =>
blnFull ? (
item.bodyString
) : item.bodyContentString;
// groupReport :: {key::String, asc::Bool, type:JS type} ->
// [[TP3Item]] -> String
const groupReport = (mbGroup, groups) =>
mbGroup.nothing ? (
unlines(map(
item => itemReport(true, [], item),
groups[0]
))
) : (() => {
const
dct = mbGroup.just,
k = dct.key,
pf = propValFn(k, dct.type);
return unlines(map(gp => {
const v = pf(gp[0]);
return (v !== undefined ? (
toTitle(k) + '(' + v + ')'
) : '(' + k + ' undefined)') + '\n' +
unlines(
map(
x => {
const strType = x.getAttribute(
'data-type'
);
return (strType === 'task' ? (
'- '
) : '') +
x.bodyContentString +
(strType === 'project' ? (
': '
) : ' ') +
exceptTags(
['type', k],
x.attributes
)
},
gp
)
) + '\n';
},
groups
));
})();
// return unlines(map(gp => gp[0].bodyString,
const groups = groupByPropVal(
sortBy(
mappendComparing(itemSortFns),
outline.evaluateItemPath(
options.itemsInPath
)
)
);
return groupReport(mbGroup, mbGroup.nothing ? (
groups
) : sortBy(
mappendComparing([
[groupValFn(mbGroup.just.key, mbGroup.just.type), mbGroup.asc]
]),
groups
));
};
// JXA CONTEXT------------------------------------------------------------
// standardPath :: String -> Path
const standardPath = strPath =>
Path(ObjC.unwrap($(strPath)
.stringByStandardizingPath));
// isNull :: [a] -> Bool
// isNull :: String -> Bool
const isNull = xs =>
Array.isArray(xs) || typeof xs === 'string' ? (
xs.length < 1
) : undefined;
// strip :: String -> String
const strip = s => s.trim();
// writeFileMay :: FilePath -> String -> Maybe FilePath
const writeFileMay = (strPath, strText) => {
const
e = $(),
fullPath = $(strPath)
.stringByStandardizingPath;
return $.NSString.alloc.initWithUTF8String(strText)
.writeToFileAtomicallyEncodingError(
fullPath, false,
$.NSUTF8StringEncoding, e
) ? {
nothing: false,
just: ObjC.unwrap(fullPath)
} : {
nothing: true
};
};
const
tp3 = Application('TaskPaper'),
ds = tp3.documents,
mbd = ds.length > 0 ? {
just: ds.at(0),
nothing: false
} : {
nothing: true
};
const strReport = (mbd.nothing ? (() => {
const nd = new tp3.Document();
return (
ds.push(nd), // Effect
nd // Value
);
})() : mbd.just)
.evaluate({
script: taskpaperContext.toString(),
withOptions: dctOptions
});
// EFFECT ā REPORT MAYBE WRITTEN TO FILE ---------------------------------
const mbWritten = isNull(strip(strReport)) ? {
nothing: true
} : writeFileMay(
dctOptions.outPutFile,
strReport
);
// VALUE - MESSAGE RETURNED ----------------------------------------------
return mbWritten.nothing ? (
'Nothing written to ' + standardPath(dctOptions.outPutFile)
) : (
'Written to ' + standardPath(dctOptions.outPutFile) + '\n\n' +
strReport
);
})({
outPutFile: '~/Desktop/report.taskpaper',
itemsInPath: '/@type=project except Archive',
orderBy: [
'@priority descending as number',
'@effort descending as number',
],
groupBy: '@priority as number descending'
});