List 10 largest notes in a Tinderbox 8 (.tbx) file

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();
})();