List 10 largest notes in a Tinderbox 8 .tbx file
For those moments when a database of Tinderbox 8 notes has grown large, for example from graphic inclusions, and you wonder where the main bulk lies.
10 Largest notes in a Tinderbox .tbx file.kmmacros (35 KB)
JS Source
(() => {
'use strict';
// Report N largest leaf notes (childless notes)
// Prompts for selection of a TBX file,
// and lists the 10 largest leaf-level notes
// the selected file.
// Rob Trew 2019
// Ver 0.02
ObjC.import('AppKit');
const intLargest = 10;
const
strTitle = 'Largest leaf notes in TBX file',
strDefaultFolder = '~/Desktop',
strXQuery = `declare function local:outlineFromTbxForest(
$indent as xs:string,
$forest as node()*
) as xs:string {
if (fn:empty($forest)) then '' else
string-join(
for $item in $forest
return concat(
$indent, $item/attribute[@name='Name']/text(),
'\t' , string(string-length($item)) ,'\n',
local:outlineFromTbxForest(
concat('\t', $indent),
$item/item
)
),
''
)
};
local:outlineFromTbxForest(
"", /*/item
)`;
// main :: IO ()
const main = () =>
either(
msg => 'User cancelled.' !== msg ? (
alert(strTitle)(msg)
) : msg
)(
report => (
copyText(report),
alert('Copied to clipboard')(
'Names of ' + str(intLargest) +
' largest leaf notes, with innerXML note sizes:\n\n' +
report
),
report
)
)(
bindLR(
bindLR(
pathChoiceLR(strDefaultFolder)(
'Choose TBX file'
)('public.xml'),
)(readFileLR)
)(
strXML => bindLR(xQueryLR(strXQuery)(strXML))(
s => {
// A forest in which each tree node
// is a dictionary of type
// { text :: String, size :: Int }
const
largerNotes = take(intLargest)(
sortBy(
flip(comparing(x => x.size))
)(leafPaths(Node('')(
forestFromLineIndents(
indentLevelsFromLines(lines(s))
).map(fmapTree(strLabel => {
const tokens = strLabel.split(/\t/);
return {
text: tokens[0],
size: parseInt(tokens[1])
};
}))
)))
),
w = 4 + str(largerNotes[0].size).length;
return Right(unlines(map(
x => '- (' + justifyLeft(w)(' ')(
str(x.size) + ')'
) + x.path
)(largerNotes)));
}
)
)
);
// NSXML XQuery ---------------------------------------
// xQueryLR :: String -> String -> Either String String
const xQueryLR = strXQuery => strXML => {
const
uw = ObjC.unwrap,
e = $(),
node = $.NSXMLDocument.alloc
.initWithXMLStringOptionsError(
strXML, 0, e
);
return bindLR(
undefined !== uw(node) ? (
Right(node)
) : Left(uw(e.localizedDescription))
)(
oNode => {
const
e = $(),
xs = uw(oNode.objectsForXQueryError(
strXQuery, e
));
return undefined !== uw(xs) ? (
Right(unlines(map(uw)(xs)))
) : Left(uw(e.localizedDescription));
}
);
};
// JXA ------------------------------------------------
// alert :: String => String -> IO String
const alert = title => s => {
const
sa = Object.assign(Application('System Events'), {
includeStandardAdditions: true
});
return (
sa.activate(),
sa.displayDialog(s, {
withTitle: title,
buttons: ['OK'],
defaultButton: 'OK'
}),
s
);
};
// String copied to general pasteboard
// copyText :: String -> IO Bool
const copyText = s => {
const pb = $.NSPasteboard.generalPasteboard;
return (
pb.clearContents,
pb.setStringForType(
$(s),
$.NSPasteboardTypeString
),
s
);
};
// String -> String -> Either String FilePath
const pathChoiceLR = fpDefault => strPrompt => strType => {
const sa = Application('System Events');
try {
sa.activate();
return Right(
(sa.includeStandardAdditions = true, sa)
.chooseFile({
withPrompt: strPrompt,
ofType: strType,
defaultLocation: filePath(fpDefault)
}).toString()
);
} catch (e) {
return Left(e.message)
}
};
// GENERIC FUNCTIONS ----------------------------------
// https://github.com/RobTrew/prelude-jxa
// Left :: a -> Either a b
const Left = x => ({
type: 'Either',
Left: x
});
// Right :: b -> Either a b
const Right = x => ({
type: 'Either',
Right: x
});
// Node :: a -> [Tree a] -> Tree a
const Node = v => xs => ({
type: 'Node',
root: v, // any type of value (consistent across tree)
nest: xs || []
});
// Tuple (,) :: a -> b -> (a, b)
const Tuple = a => b => ({
type: 'Tuple',
'0': a,
'1': b,
length: 2
});
// 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 =>
0 < xs.length ? (() => {
const unit = 'string' !== typeof xs[0] ? (
[]
) : '';
return unit.concat.apply(unit, xs);
})() : [];
// compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
const compose = (...fs) =>
x => fs.reduceRight((a, f) => f(a), x);
// div :: Int -> Int -> Int
const div = x => y => Math.floor(x / y);
// either :: (a -> c) -> (b -> c) -> Either a b -> c
const either = fl => fr => e =>
'Either' === e.type ? (
undefined !== e.Left ? (
fl(e.Left)
) : fr(e.Right)
) : undefined;
// filePath :: String -> FilePath
const filePath = s =>
ObjC.unwrap(ObjC.wrap(s)
.stringByStandardizingPath);
// Lift a simple function to one which applies to a tuple,
// transforming only the first item of the tuple
// firstArrow :: (a -> b) -> ((a, c) -> (b, c))
const firstArrow = f =>
// A simple function lifted to one which applies
// to a tuple, transforming only its first item.
xy => Tuple(f(xy[0]))(
xy[1]
);
// flip :: (a -> b -> c) -> b -> a -> c
const flip = f =>
x => y => f(y)(x);
// fmapTree :: (a -> b) -> Tree a -> Tree b
const fmapTree = f => tree => {
const go = node => Node(f(node.root))(
node.nest.map(go)
);
return go(tree);
};
// foldl1 :: (a -> a -> a) -> [a] -> a
const foldl1 = f => xs =>
1 < xs.length ? xs.slice(1)
.reduce(uncurry(f), xs[0]) : xs[0];
// foldTree :: (a -> [b] -> b) -> Tree a -> b
const foldTree = f => tree => {
const go = node => f(node.root)(
node.nest.map(go)
);
return go(tree);
};
// forestFromLineIndents :: [(Int, String)] -> [Tree String]
const forestFromLineIndents = tuples => {
const go = xs =>
0 < xs.length ? (() => {
const [n, s] = Array.from(xs[0]);
// Lines indented under this line,
// tupled with all the rest.
const [firstTreeLines, rest] = Array.from(
span(x => n < x[0])(xs.slice(1))
);
// This first tree, and then the rest.
return [Node(s)(go(firstTreeLines))]
.concat(go(rest));
})() : [];
return go(tuples);
};
// fst :: (a, b) -> a
const fst = tpl => tpl[0];
// identity :: a -> a
const identity = x => x;
// indentLevelsFromLines :: [String] -> [(Int, String)]
const indentLevelsFromLines = xs => {
const
indentTextPairs = xs.map(compose(
firstArrow(length), span(isSpace)
)),
indentUnit = minimum(indentTextPairs.flatMap(pair => {
const w = fst(pair);
return 0 < w ? [w] : [];
}));
return indentTextPairs.map(
firstArrow(flip(div)(indentUnit))
);
};
// isSpace :: Char -> Bool
const isSpace = c => /\s/.test(c);
// justifyLeft :: Int -> Char -> String -> String
const justifyLeft = n => cFiller => s =>
n > s.length ? (
s.padEnd(n, cFiller)
) : s;
// Returns Infinity over objects without finite length.
// This enables zip and zipWith to choose the shorter
// argument when one is non-finite, like cycle, repeat etc
// leafList :: Tree a -> [a]
const leafList = tree =>
foldTree(x => xs =>
0 < xs.length ? (
concat(xs)
) : [x]
)(tree);
// leafPaths :: Tree { text :: String, size :: Integer } ->
// [{ path :: String, size :: Integer }]
const leafPaths = tree =>
// Full paths of all leaf nodes, together with
// their innerXML sizes in the .tbx file
foldTree(x => xs =>
0 < xs.length ? (
xs.flatMap(map(
t => Boolean(x.text) ? ({
path: x.text + '/' + t.path,
size: t.size
}) : t
))
) : [{
path: x.text,
size: x.size
}]
)(tree);
// length :: [a] -> Int
const length = xs =>
(Array.isArray(xs) || 'string' === typeof xs) ? (
xs.length
) : Infinity;
// lines :: String -> [String]
const lines = s =>
s.split(/[\r\n]/);
// map :: (a -> b) -> [a] -> [b]
const map = f => xs =>
(Array.isArray(xs) ? (
xs
) : xs.split('')).map(f);
// minimum :: Ord a => [a] -> a
const minimum = xs =>
0 < xs.length ? (
foldl1(a => x => x < a ? x : a)(xs)
) : undefined;
// readFileLR :: FilePath -> Either String String
const readFileLR = fp => {
const
e = $(),
uw = ObjC.unwrap,
s = uw(
$.NSString.stringWithContentsOfFileEncodingError(
$(fp)
.stringByStandardizingPath,
$.NSUTF8StringEncoding,
e
)
);
return undefined !== s ? (
Right(s)
) : Left(uw(e.localizedDescription));
};
// Abbreviation for quick testing - any 2nd arg interpreted as indent size
// showLog :: a -> IO ()
const showLog = (...args) =>
console.log(
args
.map(JSON.stringify)
.join(' -> ')
);
// sortBy :: (a -> a -> Ordering) -> [a] -> [a]
const sortBy = f => xs =>
xs.slice()
.sort(uncurry(f));
// str :: a -> String
const str = x => x.toString();
// take :: Int -> [a] -> [a]
// take :: Int -> String -> String
const take = n => xs =>
'GeneratorFunction' !== xs.constructor.constructor.name ? (
xs.slice(0, n)
) : [].concat.apply([], Array.from({
length: n
}, () => {
const x = xs.next();
return x.done ? [] : [x.value];
}));
// sj :: a -> String
function sj() {
const args = Array.from(arguments);
return JSON.stringify.apply(
null,
1 < args.length && !isNaN(args[0]) ? [
args[1], null, args[0]
] : [args[0], null, 2]
);
}
// span, applied to a predicate p and a list xs, returns a tuple of xs of
// elements that satisfy p and second element is the remainder of the list:
//
// > span (< 3) [1,2,3,4,1,2,3,4] == ([1,2],[3,4,1,2,3,4])
// > span (< 9) [1,2,3] == ([1,2,3],[])
// > span (< 0) [1,2,3] == ([],[1,2,3])
//
// span p xs is equivalent to (takeWhile p xs, dropWhile p xs)
// span :: (a -> Bool) -> [a] -> ([a], [a])
const span = p => xs => {
const iLast = xs.length - 1;
return splitAt(
until(i => iLast < i || !p(xs[i]))(
succ
)(0)
)(xs);
};
// splitAt :: Int -> [a] -> ([a], [a])
const splitAt = n => xs =>
Tuple(xs.slice(0, n))(
xs.slice(n)
);
// succ :: Enum a => a -> a
const succ = x =>
1 + x;
// uncurry :: (a -> b -> c) -> ((a, b) -> c)
const uncurry = f =>
function() {
const
args = Array.from(arguments),
a = 1 < args.length ? (
args
) : args[0]; // Tuple object.
return f(a[0])(a[1]);
};
// unlines :: [String] -> String
const unlines = xs => xs.join('\n');
// until :: (a -> Bool) -> (a -> a) -> a -> a
const until = p => f => x => {
let v = x;
while (!p(v)) v = f(v);
return v;
};
// MAIN ---
return main();
})();