Getting leaf-node tags using JavaScript

Much to Jim’s chagrin, I’m trying to work out how to get a list of end-node tags from DEVONthink - however, while I’m happy with old-skool JavaScript, I’m not too sure I’m talking to DEVONthink properly to extract them.

Here’s my chunk of JS that I’d hope would recurse through the root item, looking for things that look like tags that have no child nodes (and continuing to hunt through thing that do have child nodes):


function run() {
	this.console.log("---RUNNING-------------");

	this.console.log(getEndTags(0));      

}


function getEndTags(fromItem) {
	// given an itemID, recurse through items that have children and find ones that are tags
	var listOfTags=[];
	var itemsToCheck = Application("DEVONthink Pro Office").databases.byId(fromItem).children;
	this.console.log("start: FromItem = ", mydump(fromItem) );
	for(var itemToCheck in itemsToCheck) {
		var i = Application("DEVONthink Pro Office").databases.byId(itemToCheck);
		this.console.log("from: ",i);
		if( (i.excludeFromClassification===false) &&
			(i.excludeFromSearch===false) &&
			(i.excludeFromSeeAlso===false) && 
			(i.excludeFromTagging===false) &&
			(i.numberOfDuplicates===0) &&
			(i.numberOfReplicants===0) &&
			(i.children.length===0) ) {
			
			listOfTags.append(i.name);
			//listOfTagGroups[i.name]=i;		
		}
		if( (i.children.length>0) ) {
			listOfTags.append( getEndTags(i) );
		}		
	}
	return( listOfTags );
}

However, ScriptEditor just dumps out


/* ---RUNNING------------- */
/* start: FromItem =  ===>0<===(number) */
/*  */
Result:
undefined

Are there any useful pointers for using JS together with DEVONthink to do this kind of thing? Hopefully, TIA!

Best

Dave

The script shouldn’t use hard-coded IDs, it’s better to start with the root of each database. In addition, it doesn’t seem to iterate all databases.

You know I have two questions now. :slight_smile:

  1. How do I get the root node of a database?

  2. How do I get all databases?

is there a good reference guide to AS/JS for DEVONthink anywhere, short of scraping snippets from existing scripts and this forum?

“root” is a property of databases and the application object contains the databases. For more info just drag & drop DEVONthink Pro (Office) onto the AppleScript Editor app to display its script suite and change the language in the toolbar to JavaScript.

You are after the unique set of tag strings ?

(incidentally console.log() may not be the best way to get output – JXA returns a value to whatever calls it, and the value can be used for IO events like dialogs, file-writes, returns to Keyboard Maestro macros etc etc).

If it’s the set of tag strings that you want, here is one approach:

(function () {
    'use strict';

    // DEVONTHINK ------------------------------------------------------------

    // Unique set of all the tags attached to any leaf records

    // (List of records -> list of strings)
    // dtLeafTags :: [DT Record] -> [String]
    function dtLeafTags(dtRecs) {
        return sortBy(comparing(toLower), nub(concatMap(function (x) {
            var nest = x.children;
            return nest.length !== 0 ? dtLeafTags(nest()) : x.tags();
        }, dtRecs)));
    };

    // currentDBLeafTags :: () -> [String]
    function currentDBLeafTags() {
        return dtLeafTags(
            Application('DevonThink Pro')
            .currentDatabase.records()
        );
    };

    // GENERIC ---------------------------------------------------------------

    // append :: [a] -> [a] -> [a]
    function append(xs, ys) {
        return xs.concat(ys);
    };

    // comparing :: (a -> b) -> (a -> a -> Ordering)
    function comparing(f) {
        return function (x, y) {
            var a = f(x),
                b = f(y);
            return a < b ? -1 : a > b ? 1 : 0;
        };
    };

    // concatMap :: (a -> [b]) -> [a] -> [b]
    function concatMap(f, xs) {
        return xs.length > 0 ? function () {
            var unit = typeof xs[0] === 'string' ? '' : [];
            return unit.concat.apply(unit, xs.map(f));
        }() : [];
    };

    // isNull :: [a] | String -> Bool
    function isNull(xs) {
        return Array.isArray(xs) || typeof xs === 'string' ? xs.length < 1 : undefined;
    };

    // log :: a -> IO ()
    function log() {
        for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
            args[_key] = arguments[_key];
        }

        return console.log(args.map(JSON.stringify)
            .join(' -> '));
    };

    // nub :: [a] -> [a]
    function nub(xs) {
        return nubBy(function (a, b) {
            return a === b;
        }, xs);
    };

    // nubBy :: (a -> a -> Bool) -> [a] -> [a]
    function nubBy(p, xs) {
        var mbx = xs.length ? {
            just: xs[0]
        } : {
            nothing: true
        };
        return mbx.nothing ? [] : [mbx.just].concat(nubBy(p, xs.slice(1)
            .filter(function (y) {
                return !p(mbx.just, y);
            })));
    };

    // show :: Int -> a -> Indented String
    // show :: a -> String
    function show() {
        for (var _len2 = arguments.length, x = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
            x[_key2] = arguments[_key2];
        }

        return JSON.stringify.apply(null, x.length > 1 ? [x[1], null, x[0]] : x);
    };

    // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
    function sortBy(f, xs) {
        return xs.slice()
            .sort(f);
    };

    // toLower :: Text -> Text
    function toLower(s) {
        return s.toLowerCase();
    };

    // TEST ------------------------------------------------------------------

    return currentDBLeafTags();
})();

Thanks, houthakker! That’s exactly what I want! You’re amazing!

(your JavaScript skills are intense, by the way - I have to sit down in a quiet room to work out what’s going on!)

So now I’m can move ahead to my next stumbling block, which is to wrap a gui around the intersection of the tag list and matching phrases in the record text and make it easy for the user to hit immediate tags. It’s my super-cheap approach to combining ontologies, folksonomies and … shall we just say it’s a folkology? It won’t compete with hadoop clusters and machine-learning APIs but it will save me a heck of a lot of hunting around with my tag structures.

Back to script editor… and thanks again!

Dave

Sounds like an interesting project.

Re sitting down in a quiet room, unadorned Array.map() would yield a list of lists (some empty – tagless leaves – others containing tags strings).

The concat part of concatMap flattens that list of lists to a single list of strings, and the empties vanish under concatenation.

Nub prunes out duplicates.

comparing(toLower) returns a custom comparator for Array.sort()

(Lower-case versions of strings are compared, in lieu of the strings themselves).