Getting a list of available link types?

A couple of questions about deriving a list of available link types:

  • Is there an action code route to doing this ?
  • Should I be able to find a default linkTypes.xml somewhere in the application bundle ? I don’t immediately see one in in the Resources folder.

and finally:

  • Is the ~/Library/Application Support/Tinderbox path still searched for a custom linkTypes.xml

as described in:

https://acrobatfaq.com/atbref8/index/TinderboxFileTypes/ConfigurationFiles/linkTypesxml.html

?

(If the linkTypes.xml mechanism is not current, then I guess we can read the contents of the <linkTypes> element in the current tbx file)

  1. Action code? No. There are some operators , such a link-related ones that will filter based on a name match to a link type but they don’t enumerate the links. I think it is worth repeating something that tends to trip up those who code at this from a coding perspective - action/export codes weren’t designed as a programming-style language. Instead the system has grown over time out of simple macros. IOW, a programming perspective works against one here as one is tempted to assume feature just because they are common in coding. Instead your best bets are aTbRef and the (a bit out of date) Tinderbox cookbook. I hope that doesn’t come across as rude, it’s not meant so!

  2. This is v8.0.6:

  3. TBH, I don’t know. V busy so no time to test, but drop a custom file in there and try (if necessary re-start the app). I’ve seen nothing to say the process has changed, but changes to such background things do sometimes sneak by.

1 Like

As far as I can tell, no. Not in Tinderbox 8 or Tinderbox 7. I’ve tested modified linkTypes.xml files placed in either of these locations, with v7 and v8, with no success in any case:

~/Library/Application Support/Tinderbox
~/Library/Application Support/Tinderbox/config

If there is some other kind of foo to make this customization of link types work, I’m not sure what it is.

2 Likes

Initial experiment here suggests that any:

  • ~/Library/Application Support/Tinderbox/config/linkTypes.xml is read (after Tinderbox 8 is restarted) and is used in the creation of fresh documents,
  • and defines the contents of the <linkTypes> element when a fresh document customised in this way is first saved to .tbx.
  • BUT does not alter the linkType dropdown contents (or <linkTypes> element contents) of any existing document.

That seems quite a sensible and workable pattern. It looks as if any custom linkTypes.xml file:

  • Provides a template for fresh documents,
  • but doesn’t complicate the life of existing documents.

The upshot for scripting, if I’ve understood this properly, is that to know the options for the active document, we need to read its .tbx file and list the contents of the <linkTypes> element therein.

1 Like

Makes sense. So the contents of linkTypes.xmls is read into the new document upon creation and then not referenced thereafter.

It’s actually a cool feature to know about.

2 Likes

Somebody DM* me after next weekend when I’m not writing and remind me to add the above to/clarify existing aTbRef content.

Or just give this thread a bump.

1 Like

As a footnote in case anyone ever looks this up, one route through (JS) osascript might be:

JavaScript code
(() => {
    'use strict';

    // Tinderbox 8 :: Listing link types available in front document.
    // (JavaScript for Automation)

    // Rob Trew 2019

    // main :: IO ()
    const main = () => {
        const inner = () => {
            const
                tbx = Application('Tinderbox 8'),
                ds = tbx.documents;
            return either(alert('Problem reading link types'))(
                xs => alert('Links types in front document')(
                    unlines(xs)
                )
            )(bindLR(
                0 < ds.length ? (
                    Right(ds.at(0))
                ) : Left('No documents open in Tinderbox 8.')
            )(doc => {
                const
                    uw = ObjC.unwrap,
                    eXML = $(),
                    docXML = $.NSXMLDocument.alloc
                    .initWithXMLStringOptionsError(
                        readFile(doc.file().toString()),
                        0, eXML
                    );
                return bindLR(
                    docXML.isNil() ? (
                        Left(uw(eXML.localizedDescription))
                    ) : Right(docXML)
                )(
                    doc => {
                        const
                            eXQ = $(),
                            xs = doc.objectsForXQueryError(
                                'for $path in //linkType\n' +
                                'return string($path/@name)',
                                eXQ
                            );
                        return xs.isNil() ? (
                            Left(uw(eXQ.localizedDescription))
                        ) : Right(uw(xs).map(uw));
                    }
                );
            }));
        };

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

        return inner();
    };

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

    // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
    const bindLR = m => mf =>
        undefined !== m.Left ? (
            m
        ) : mf(m.Right);

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

    // readFile :: FilePath -> IO String
    const readFile = fp => {
        const
            e = $(),
            ns = $.NSString.stringWithContentsOfFileEncodingError(
                $(fp).stringByStandardizingPath,
                $.NSUTF8StringEncoding,
                e
            );
        return ObjC.unwrap(
            ns.isNil() ? (
                e.localizedDescription
            ) : ns
        );
    };

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

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

–>


or

“color” and “label” are important properties of link-types, so including them in the table would be helpful, if possible.

FWIW, to give a crosscut on the data, an XML <linkType> tags holds this info. AFICT, the tag attributes are stored in the same order though some may be if just using default values.

The same info, as seen in the Inspector.

Yes: Tinderbox looks at

  ~/Library/Application Support/Tinderbox/config/linkTypes.xml

for a file that, if present, will override the application’s built-in file. This populates link types in new documents, but not in existing documents.

2 Likes

To read several attributes of a <linkType> node into, for example, a list of a tab-delimited lines, we could amend the XQuery expression to something like:

let $tab := "&#9;" (: tab :)
for $path in //linkType
return concat(
    $path/@name, $tab,
    $path/@label, $tab,
    $path/@color
)

So the updated JS might be as below:

JS Source ver 2 – name, label, color
(() => {
    'use strict';

    // Tinderbox 8 :: Listing link types available in front document.
    // (JavaScript for Automation)

    // Rob Trew 2019
    // Ver 0.02 returning tab-delimited name, label, color for each link.

    // main :: IO ()
    const main = () => {
        const inner = () => {
            const
                tbx = Application('Tinderbox 8'),
                ds = tbx.documents;
            return either(alert('Problem reading link types'))(
                xs => alert('Links types in front document')(
                    unlines(xs)
                )
            )(bindLR(
                0 < ds.length ? (
                    Right(ds.at(0))
                ) : Left('No documents open in Tinderbox 8.')
            )(doc => {
                const
                    uw = ObjC.unwrap,
                    eXML = $(),
                    docXML = $.NSXMLDocument.alloc
                    .initWithXMLStringOptionsError(
                        readFile(doc.file().toString()),
                        0, eXML
                    );
                return bindLR(
                    docXML.isNil() ? (
                        Left(uw(eXML.localizedDescription))
                    ) : Right(docXML)
                )(doc => {
                    const
                        eXQ = $(),
                        xs = doc.objectsForXQueryError(
                            `let $tab := "&#9;" (: tab :)
                             for $path in //linkType
                             return concat(
                                 $path/@name, $tab,
                                 $path/@label, $tab,
                                 $path/@color
                             )`,
                            eXQ
                        );
                    return xs.isNil() ? (
                        Left(uw(eXQ.localizedDescription))
                    ) : Right(uw(xs).map(uw));
                });
            }));
        };

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

        return inner();
    };

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

    // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
    const bindLR = m => mf =>
        undefined !== m.Left ? (
            m
        ) : mf(m.Right);

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

    // readFile :: FilePath -> IO String
    const readFile = fp => {
        const
            e = $(),
            ns = $.NSString.stringWithContentsOfFileEncodingError(
                $(fp).stringByStandardizingPath,
                $.NSUTF8StringEncoding,
                e
            );
        return ObjC.unwrap(
            ns.isNil() ? (
                e.localizedDescription
            ) : ns
        );
    };

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

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

Throws an error:

Error on line 30: TypeError: null is not an object (evaluating 'doc.file().toString')

Yes, that, I think, may the case in which the document hasn’t been saved, so there is not yet any XML to read.

I’ve diverted that to the Left channel (messages to user) in later versions, which I’ll share in due course.

which I’ll share in due course.

Here is a new draft which returns an array of JS key-value dictionaries, one dictionary for each linkType. It:

  • allows us to specify any subset of the available linkType attributes,
  • and alerts the user in the case where a document has not yet been saved to a .tbx file.

Full set of attributes of a linkType:

[
    'name', 'label', 'visible', 'showLabel',
    'required', 'color', 'colorString',
    'style', 'arrowType', 'onLink'
]

Sample output of main function – array of JS dictionaries
[
  {
    "name": "*untitled",
    "label": "vanilla",
    "visible": "1",
    "showLabel": "0",
    "required": "1",
    "color": "#000000",
    "colorString": "#000000",
    "style": "0",
    "arrowType": "0",
    "onLink": ""
  },
  {
    "name": "clarify",
    "label": "ie",
    "visible": "1",
    "showLabel": "1",
    "required": "0",
    "color": "#c0c0c0",
    "colorString": "#c0c0c0",
    "style": "0",
    "arrowType": "0",
    "onLink": ""
  },
  {
    "name": "narrate",
    "label": "then",
    "visible": "1",
    "showLabel": "1",
    "required": "0",
    "color": "#0000c0",
    "colorString": "#0000c0",
    "style": "0",
    "arrowType": "0",
    "onLink": ""
  },
  {
    "name": "persuade",
    "label": "cos",
    "visible": "1",
    "showLabel": "1",
    "required": "0",
    "color": "#ff0000",
    "colorString": "#ff0000",
    "style": "0",
    "arrowType": "0",
    "onLink": ""
  }
]

JavaScript source
(() => {
    'use strict';

    // Listing the link types available for the front document
    // in Tinderbox 8.

    // This version returns a JS dictionary for each linkType.
    // Any subset of the available linkType attributes can be specified.

    // Rob Trew 2019
    // Ver 0.04

    // Added dialog for displaying a result derived
    // from a list of key-value dictionaries.

    const main = () => {

        // 'name' is always read and doesn't need to
        // be specified, tho it can be.
        const additionalAttributesToRead = [
            'label', 'color', 'colorString'
        ];
        const
            tbx = Application('Tinderbox 8'),
            ds = tbx.documents;
        return either(
            alert('Problem reading link types')
        )(linkTypes => {
            const
                cols = ['name'].concat(
                    additionalAttributesToRead
                ),
                strTable = cols.join(' -> ') + '\n\n' +
                unlines(linkTypes.map(
                    dct => cols
                    .map(k => dct[k])
                    .join(' -> ')
                ));
            return alert('Link types in ' + ds.at(0).name())(
                strTable
            );
        })(bindLR(
            0 < ds.length ? (
                Right(ds.at(0))
            ) : Left('No documents open in Tinderbox 8.')
        )(doc => {
            const docFile = doc.file;
            return bindLR(
                docFile.exists() ? (
                    Right(docFile().toString())
                ) : Left(
                    'Document "' + doc.name() +
                    '" not yet saved to file.'
                )
            )(tbxLinkTypesFromFilePathLR(
                additionalAttributesToRead
            ))
        }));
    };

    // tbxLinkTypesFromFilePathLR :: [String] -> FilePath ->
    //  Either String [Dict]
    const tbxLinkTypesFromFilePathLR = attributeNames => fp => {
        // The @name attribute is always read. Additional
        // attribute names can include any/all of those in
        // knownAttribs below:
        const knownAttribs = [
            'name', 'label', 'visible', 'showLabel',
            'required', 'color', 'colorString',
            'style', 'arrowType', 'onLink'
        ];
        const
            mbUnknown = find(k => !knownAttribs.includes(k))(
                attributeNames
            );
        return bindLR(
            mbUnknown.Nothing ? (
                Right(attributeNames)
            ) : Left(
                'Unrecognized attribute name: "' +
                mbUnknown.Just + '"'
            )
        )(ks =>
            bindLR(
                doesFileExist(fp) ? (
                    Right(fp)
                ) : Left('File not found: ' + fp)
            )(fp => {
                const
                    uw = ObjC.unwrap,
                    eXML = $(),
                    docXML = $.NSXMLDocument.alloc
                    .initWithXMLStringOptionsError(
                        readFile(fp),
                        0, eXML
                    );
                return bindLR(
                    docXML.isNil() ? (
                        Left(
                            uw(eXML.localizedDescription) +
                            'Not recognized as Tinderbox 8 XML:\n    ' +
                            fp
                        )
                    ) : Right(docXML)
                )(doc => {
                    const attribQuery = k =>
                        `'${k}', local:if-empty($x/@${k}, "")`;
                    const
                        eXQ = $(),
                        xq = `
                                declare function local:if-empty
                                    ($k as item()?,
                                     $v as item()*)  as item()* {
                                    if (string($k) != '')
                                    then data($k)
                                    else $v
                                } ;
                                let $tab := "&#9;" (: tab :)
                                for $x in //linkType
                                return string-join(
                                    ('name', $x/@name${
                                         0 < ks.length ? (
                                                ', ' + ks.map(attribQuery)
                                                .join(', ')
                                         ) : ''
                                    }), $tab
                                )`;
                    const xs = doc.objectsForXQueryError(xq, eXQ);
                    return xs.isNil() ? (
                        Left(uw(eXQ.localizedDescription))
                    ) : Right(
                        uw(xs).map(
                            node => chunksOf(2)(
                                uw(node).split('\t')
                            ).reduce(
                                (a, [k, v]) =>
                                Object.assign(a, {
                                    [k]: v
                                }), {}
                            )
                        )
                    );
                })
            })
        );
    };

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

    // GENERIC FUNCTIONS ----------------------------------
    // https://github.com/RobTrew/prelude-jxa

    // Just :: a -> Maybe a
    const Just = x => ({
        type: 'Maybe',
        Nothing: false,
        Just: x
    });

    // Left :: a -> Either a b
    const Left = x => ({
        type: 'Either',
        Left: x
    });

    // Nothing :: Maybe a
    const Nothing = () => ({
        type: 'Maybe',
        Nothing: true,
    });

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

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

    // doesFileExist :: FilePath -> IO Bool
    const doesFileExist = strPath => {
        const ref = Ref();
        return $.NSFileManager.defaultManager
            .fileExistsAtPathIsDirectory(
                $(strPath)
                .stringByStandardizingPath, ref
            ) && 1 !== ref[0];
    };

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

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

    // find :: (a -> Bool) -> [a] -> Maybe a
    const find = p => xs => {
        const i = xs.findIndex(p);
        return -1 !== i ? (
            Just(xs[i])
        ) : Nothing();
    };

    // readFile :: FilePath -> IO String
    const readFile = fp => {
        const
            e = $(),
            ns = $.NSString.stringWithContentsOfFileEncodingError(
                $(fp).stringByStandardizingPath,
                $.NSUTF8StringEncoding,
                e
            );
        return ObjC.unwrap(
            ns.isNil() ? (
                e.localizedDescription
            ) : ns
        );
    };

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

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