Tinderbox Forum

Using evaluate (introduced in TB 8.0.4)

I’m trying to understand how to use evaluate.

I can use it perform calculations on attribute values, without first having to retrieve the value of each one. Very nice!

And I see it provides another way to retrieve the value of an attribute.

Instead of:

set theNum to value of attribute "MyNumber" of myNote

I can use:

set theNum to evaluate myNote with "$MyNumber"

But assigning the result of a calculation another attribute does not seem to work. For example, this works for me here in a stamp:

$AnotherNumberAttribute=$MyNewNumberAttribute*$MyNumber

But same expression doesn’t seem to work with evaluate in a script.

Is that as intended? And aside from tasks similar to my experiments above, what other uses of evaluate are there likely to be?

I think that evaluate expects an expression rather than an action. I’m not sure.

2 Likes

It enables you to obtain values which the osascript interface may not otherwise directly reach.

Food for thought might take the shape of some of the functions (operators) here:

https://www.acrobatfaq.com/atbref8/index/ActionsRules/Operators/FullOperatorList.html

This, for example, is I think, a faster (and arguably simpler) route to a full listing of document paths than an equivalent reading through the osacript note object hierarchy would be:

tell application "Tinderbox 8"
    tell front document to tell first note to ¬
        evaluate it with "collect(all, $Path)"
end tell

a faster (and arguably simpler) route to a full listing of document paths than an equivalent reading through the osacript note object hierarchy would be

Ah, thanks for the pointers. I’m beginning to see the power. Now can incorporate the internal Tinderbox code into external code in a script, giving the best of both, and simplifying life.

For example, a script solution to the problem posed in Trying to make a simple list of tags thread can now be a one-liner!

tell front document of application "Tinderbox 8" to set the clipboard to (evaluate first note with "collect(all, $Tags).unique.sort.format(" & quote & return & quote & ")")

Or to get the tags into an AppleScript list for manipulation in AppleScript can be something like this:

tell front document of application "Tinderbox 8"
	set theTags to evaluate first note with "collect(all, $Tags).unique.sort"
end tell

set tagsLst to makeAList(theTags)

-- handlers

to makeAList(semicolonDelimitedString) --> AppleScript list
	set text item delimiters to ";"
	return semicolonDelimitedString's text items
end makeAList
1 Like

Here FWIW, is a first JS sketch of using the .evaluate() method to batch-fetch values for a given list of attributes from each of the xpath ‘axes’ of the selected note.

(The output is a JS array of key:value dictionaries – one such dictionary for each note referenced by the xpath axis in relation to the selected note. The dictionary keys are the names of the requested attributes)

XPath axes overlap with the Tinderbox group designators and also add a few additional relationships to the selected note, derived here by using JS to assemble Tinderbox action code expressions:

(() => {
    'use strict';

    // Rob Trew 2019
    // Draft 0.02

    const main = () => {

        // TEST OF XPATH AXES IMPLEMENTED THROUGH `EVALUATE` IN
        // THE TINDERBOX 8.04 OSASCRIPT INTERFACE.

        // Select a node in the document, and run this code to see a listing
        // of the $Name and $Checked values for each note in the following
        // XPATH axes of the selected note:
        // (See simple documentation further down, taken from
        //      https://developer.mozilla.org/en-US/docs/Web/XPath/Axes
        // )
        // ancestor
        // ancestor-or-self
        // child
        // descendant
        // descendant-or-self
        // following
        // following-sibling
        // parent
        // preceding
        // preceding-sibling
        // self
        const strListings = (
            bindLR(
                bindLR(
                    tbxFrontDocLR(),
                    doc => {
                        const selns = doc.selections;
                        return 0 < selns.length ? (
                            Right(selns.at(0))
                        ) : Left(
                            'Nothing selected in document: ' + doc.name()
                        );
                    }
                ),
                // Test each XPath axis
                note => unlines(map(
                    axisName => axisName + ':\n\n' + unlines(
                        map(JSON.stringify,
                            axisAttributesFromNote(axisName)(
                                ['Name', 'Checked']
                            )(note)
                        )
                    ) + '\n\n',
                    Object.keys(xpathAxes)
                ))
            )
        );

        return (
            console.log(strListings),
            strListings
        );
    };

    // TINDERBOX ------------------------------------------

    // axisAttributesFromNote :: XPath Axis Name String ->
    //     [TBX Attribute Name String] -> [TBX Note] -> [Record]
    const axisAttributesFromNote = axisName => attribNames => note => {
        const
            index = note.evaluate({
                with: '$OutlineOrder'
            }),
            intAttrs = attribNames.length,
            strAttrs = attribNames.map(k => '$' + k).join(','),
            prefix = k => axisName.startsWith(k),
            suffix = k => axisName.endsWith(k),
            simpleAxis = k =>
            'collect(' + k + ', list(' + strAttrs + '))',
            siblingAxis = strOp => 'collect_if(sibling,' + index +
            strOp + '$OutlineOrder,list(' + strAttrs + '))',
            beforeAfterAxis = strOp => 'collect(find(' + index + strOp +
            '$OutlineOrder), list(' + strAttrs + '))';

        const strRelative = prefix('self') || ('.' === axisName) ? (
                ''
            ) : note.evaluate({
                with: prefix('anc') ? (
                    simpleAxis('ancestor')
                ) : prefix('child') ? (
                    simpleAxis('children')
                ) : prefix('desc') ? (
                    simpleAxis('descendants')
                ) : prefix('par') || ('..' === axisName) ? (
                    simpleAxis('parent')
                ) : prefix('foll') ? (
                    suffix('sibling') ? (
                        siblingAxis('<')
                    ) : beforeAfterAxis('<')
                ) : prefix('prec') ? (
                    suffix('sibling') ? (
                        siblingAxis('>')
                    ) : beforeAfterAxis('>')
                ) : ''
            }),
            strSelf = suffix('self') || ('.' === axisName) ? (
                note.evaluate({
                    with: 'list(' + strAttrs + ')'
                })
            ) : '',
            intRelative = strRelative.length,
            intSelf = strSelf.length;

        return 0 < (intRelative + intSelf) ? (
            map(kvs => foldl(
                    (a, kv) => Object.assign(a, {
                        [kv[0]]: kv[1]
                    }), {},
                    zip(attribNames, kvs)
                ),
                chunksOf(
                    intAttrs,
                    0 < intSelf ? (
                        0 < intRelative ? (
                            concatMap(
                                s => s.split(';'),
                                [strSelf, strRelative]
                            )
                        ) : strSelf.split(';')
                    ) : strRelative.split(';')
                )
            )
        ) : [];
    };


    // tbxFrontDocLR :: Tbx IO () -> Either String Tbx Doc
    const tbxFrontDocLR = () => {
        // Either the front document in Tinderbox 8, or an
        // explanatory message if no documents are strOpen,
        // or Tinderbox 8 is not running.
        const tbx = Application('Tinderbox 8');
        return tbx.running() ? (() => {
            const ds = tbx.documents;
            return 0 < ds.length ? (
                Right(ds.at(0))
            ) : Left('No documents strOpen in Tinderbox');
        })() : Left('Tinderbox 8 is not running.');
    };

    // XPATH ----------------------------------------------

    // XPATH path descriptions from:
    // https://developer.mozilla.org/en-US/docs/Web/XPath/Axes

    // xpathAxes :: {Axis Name String :: Description String}
    const xpathAxes = {
        'ancestor': 'Indicates all the ancestors of the context node ' +
            ' beginning with the parent node and traveling through to ' +
            'the root node.',
        'ancestor-or-self': 'Indicates the context node and all of ' +
            'its ancestors, including the root node.',
        'child': 'Indicates the children of the context node. If an ' +
            'XPath expression does not specify an axis, this is ' +
            'understood by default. Since only the root node or ' +
            'element nodes have children, any other use will ' +
            'select nothing.',
        'descendant': 'Indicates all of the children of the context ' +
            'node, and all of their children, and so forth. ' +
            'Attribute and namespace nodes are not included - the ' +
            'parent of an attribute node is an element node, but ' +
            'attribute nodes are not the children of their parents.',
        'descendant-or-self': 'Indicates the context node and all' +
            'of its descendants. Attribute and namespace nodes are ' +
            'no included - the parent of an attribute node is an ' +
            'element node, but attribute nodes are not the children ' +
            'of their parents.',
        'following': 'Indicates all the nodes that appear after the ' +
            'context node, except any descendant, attribute, and ' +
            'namespace nodes.',
        'following-sibling': 'Indicates all the nodes that have ' +
            'the same parent as the context node and appear after ' +
            'the context node in the source document.',
        'parent': 'Indicates the single node that is the parent ' +
            'of the context node. It can be abbreviated as ' +
            'two periods (..).',
        'preceding': 'Indicates all the nodes that precede the ' +
            'context node in the document except any ancestor, ' +
            'attribute and namespace nodes.',
        'preceding-sibling': 'Indicates all the nodes that have ' +
            'the same parent as the context node and appear ' +
            'before the context node in the source document.',
        'self': 'Indicates the context node itself. It can be ' +
            'abbreviated as a single period (.)'
    };

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

    // 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);

    // chunksOf :: Int -> [a] -> [[a]]
    const chunksOf = (n, xs) =>
        enumFromThenTo(0, n, xs.length - 1)
        .reduce(
            (a, i) => a.concat([xs.slice(i, (n + i))]),
            []
        );

    // concatMap :: (a -> [b]) -> [a] -> [b]
    const concatMap = (f, xs) =>
        xs.reduce((a, x) => a.concat(f(x)), []);


    // enumFromThenTo :: Int -> Int -> Int -> [Int]
    const enumFromThenTo = (x1, x2, y) => {
        const d = x2 - x1;
        return Array.from({
            length: Math.floor(y - x2) / d + 2
        }, (_, i) => x1 + (d * i));
    };

    // foldl :: (a -> b -> a) -> a -> [b] -> a
    const foldl = (f, a, xs) => xs.reduce(f, a);


    // 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

    // length :: [a] -> Int
    const length = xs =>
        (Array.isArray(xs) || 'string' === typeof xs) ? (
            xs.length
        ) : Infinity;

    // map :: (a -> b) -> [a] -> [b]
    const map = (f, xs) =>
        (Array.isArray(xs) ? (
            xs
        ) : xs.split('')).map(f);

    // showLog :: a -> IO ()
    const showLog = (...args) =>
        console.log(
            args
            .map(JSON.stringify)
            .join(' -> ')
        );


    // take :: Int -> [a] -> [a]
    // take :: Int -> String -> String
    const take = (n, xs) =>
        xs.slice(0, n);


    // unlines :: [String] -> String
    const unlines = xs => xs.join('\n');


    // zip :: [a] -> [b] -> [(a, b)]
    const zip = (xs, ys) => {
        const lng = Math.min(length(xs), length(ys));
        const bs = take(lng, ys);
        return take(lng, xs).map((x, i) => Tuple(x, bs[i]));
    };

    // MAIN ---
    return main();
})();