How to use JavaScript in a smart rule (finally)

It has been said several times here that one can’t use JavaScript in external scripts for Smart Rules. This is in fact not true. The problem is that in previous attemps, the entry function to the script had been written

function performSmartRule(selectedRecords)

like indicated in the documentation for AppleScript, where one uses

on performSmartRule(...)

However, AppleScript is not case sensitive, JavaScript is. So in order to use JavaScript in an external script for a smart rule, the function name has to be

function performsmartrule(...)

in all lower case.
@BLUEFROG, it would be nice if you could add this little caveat to the documentation. Of course with the warning that JXA is not officially supported for DT scripting :wink:

4 Likes

So New Year’s resolutions (in order of probability of success) then:

  • end pandemic
  • salvage marriage
  • learn {

Java. {(Script (()

)
)}

}

1 Like

Try Perl instead. Less parenthesis, but the trade-off is “leaning toothpick syndrom”.

Just what I need :crazy_face: I mean, who hasn’t missed a bit of instability in the recent past? :rofl:

This is looking awfully promising there @chrillek!

Ran into the following hiccup and wondering if I’m missing something.

In the Apple Script Editor I’ve got:

var dev=true;

function performsmartrule(records) {
  let app=Application("DEVONthink 3");
  records.forEach(r => {
  	app.logMessage(r.plainText())	
  });
}

if (dev) {
  var app = Application("DEVONthink 3");
  app.includeStandardAdditions = true 
  performsmartrule(app.selection()); 
}

Select a record in DevonThink, run in Apple Script Editor and volia… as expected we see the plain text of the record in the DT log window, the heavens open and a choir of New Years angels sing.

I change the var dev=true into var dev=false, create a Smart Rule in DT, setting actions to Execute Script External referencing the script and…

8:56:10 AM: ~/Library/Application Scripts/com.devon-technologies.think3/Smart Rules/JS Plain Text Logger.scpt on performSmartRule (Error: Error: No error.)

That’s it. No other output is logged. :man_facepalming:

function performsmartrule(records) {
  let app=Application("DEVONthink 3");
  app.logMessage("here");
  records.forEach(r => {
  	app.logMessage("before");
  	app.logMessage(r.plainText());
	app.logMessage("after");
  });
  app.logMessage("finished");
}

With this version adding obsessive logging the DT log output is:

  • here
  • before
  • on performSmartRule (Error: Error: No error.)

Hm. I tried it here again with the original script

function performsmartrule(records) {
  let app=Application("DEVONthink 3");
  records.forEach(r => {
    app.logMessage(r.plainText());
  } )
}

and three currently selected records. The text of all three is written to the log window, and the “error: no error” message appears after that. However, if I select a non-OCR’d PDF document, I see exactly the behaviour you’re describing, with an empty line just above the error message. Try this change:

    app.logMessage(r.plainText() || "NO TEXT");

I also wrote a script more along the lines of your original question in the other thread about extracting the date(s) with JavaScript:

function performsmartrule(records) {
  let app=Application("DEVONthink 3");

  records.forEach(r => {
    let p = r.plainText();	
    let maxDate = new Date("0001-01-01");
	let matches = p.match(/\d\d[.-/]\d\d[.-/]\d{4}/g);
	matches.forEach(m => {
      let d = new Date(m.split(/[-./]/).reverse().join("-"));
      if (d > maxDate) {
	    maxDate = d;
	  }
	})
	r.comment = maxDate.toString();
  })
 }

That one does no logging at all, it just writes the latest date found to the record’s comments. Works ok here, but the irritating “error” message still appears in the log window.

Well that’s rather odd (and frustrating knowing it should work).

So I copy/pasted the original script verbatim from above, saved a new external script, updated the smart rule to use it, selected 3 records (after double-checking they’re OCR’d and have text) and the only output is the irritating error, error no error. Nothing else at all.

Then created another new external script but with app.logMessage(r.plainText() || "NO TEXT"); - same results. The only log is error, error, no error. Nothing else.

Hrumph. :disappointed:

Checked for updates and got You are running DEVONthink Pro Edition 3.6.1. No newer version was found.

Ok, well the plot is thickening.

Using this:

function performsmartrule(records) {
  let app=Application("DEVONthink 3");
  records.forEach(r => {
	str = JSON.stringify(records);
	app.logMessage('records->'+str);
	str = JSON.stringify(r);
	app.logMessage('r->'+str);
    app.logMessage('before');
  	app.logMessage(r.plainText() || "NO TEXT");
	app.logMessage('after');
  } )
}

Spits out (in the log) window:

|5:05:57 PM: records->[null,null,null]||
|5:05:57 PM: r->undefined||
|5:05:57 PM: before||
|5:05:57 PM: ~/Library/Application Scripts/com.devon-technologies.think3/Smart Rules/JS Plain Text Orig2.scpt|on performSmartRule (Error: Error: No error.)|

Uh, so the record in variable r is null, but the records array isn’t null but actually has 3 items each with a null value!?

P.S. (if I select 5 items, the records array ends up with 5 nulls)

Could you try a simple

app.logMessage(r.name());

inside the forEach? That should always work because every record has a name.
I’m not sure that the JSON.stringify works with these rather special objects,

Sure, actually that’s what led me to JSON.stringify to try and figure out what the heck is going on.

  let app=Application("DEVONthink 3");
  records.forEach(r => {
	app.logMessage(r.name());
  } )
}

5:37:19 PM: ~/Library/Application Scripts/com.devon-technologies.think3/Smart Rules/JS Plain Text Orig2.scpt on performSmartRule (Error: Error: No error.)

Looks like those nulls in the records list aren’t really nulls, but seems like they are a function…

function performsmartrule(records) {
  let app=Application("DEVONthink 3");
  app.logMessage(app.name());
  app.logMessage( typeof records );
  records.forEach(r => {
	app.logMessage( typeof r );  
	app.logMessage(r.name());
  } )
}

Produces log of:

  • DEVONthink 3
  • object
  • function
  • on performSmartRule (Error: Error: No error.)

I’m curious why you want to do it in JavaScript. Didn’t moving the AppleScriptObjC part into a script library (see here) work for you?

Yes I do now have a handful of simpler but more complex than out of the box regexes working via AppleScriptObjC in an home-spun external script library script thanks to all the help here on the forums and in particular your awesome post. Once the dust settles and I have my scripts tidied up am hoping to post a complete follow-up example with what I ended up with.

However, that is just the tip of the iceberg. The AppleScript + ObjC solution is painfully unwieldy, verbose, brittle, obtuse and discourages further development. I can see the unmaintainable monster lurking and countless hours burnt once I start digging into more complex use cases.

Here’s an example use-case that I have in mind longer term.

  • import of invoice (via email or scan w/OCR) into DT inbox
  • script parses/extracts for basic data, such as identifying vendor, date and filing actions
  • further parsing to extract net, taxes, totals, description
  • using the extracted data invoice is then input into accounting software using REST calls, CLI, cough ApplesScript, MWScript, or generated CSV/XML

For an even more pipe-dream of a use-case:

  • business credit card statement (PDF with text) imported into DT inbox
  • script parses/extracts for basic data, filing/classification within DT
  • line items of transactions are parsed, looking for existing entries in accounting software, if no line exists an entry is made (and or dots are connected together)
  • e.g. Amazon CC line item could use amount to find the Amazon invoice/order summary previously imported into DT where it might be able to connect the order # by matching amounts
  • generates REST, CLI, CSV or XML for import into bookeeping software (or perhaps a report designed to make the manual entry less painful)

Considering the scripting yoga poses that were needed using DT + AppleScript + ObjC simply to capture a date using a no-so-complex regular expression… I can only imagine (in waking nightmarish form) what an AppleScript + ObjC solution might look like for these more interesting use cases.

Looking through the posts on the forums over the last few days, it sure feels as if AppleScript, the accompanying brittle-ness and, outright needless coding suffering is a road-block for so many to really unleash the power of DT and bring it to the next level.

If Javascript can be made to work natively, integrating more automation and intelligence becomes something one wants to explore and get excited about and opens up new possibilities.

IMO, getting Javascript working natively in DT is a massive game-changer.

2 Likes

Sounds like JavaScript to me :wink:

By the way, AppleScript is the preferred language of many, many Mac users, DEVONtech clients included. JavaScript is terse and indecipherable for many people with little programming experience. We use and support AppleScript natively as it provides less of a barrier to entry for everyday people.

1 Like

Sounds like JavaScript to me

Facile and baseless comment.

AppleScript less of a barrier to entry for everyday people.

Completely untrue. You may be confused by the fact that you are familiar with it.

  • The pattern of AppleScript is actually quite tricky to master
  • Its records are harder to work with, and more liable to explode in your face than any language I have encountered in 40 years
  • AppleScript is a total and unpassable barrier to macOS ⇄ iOS cross-platform scripting.

It’s lazy (and unhelpful to beginners) to be unrealistic about it on the grounds that it’s a language in which you have personally invested a lot of time.

1 Like

Please, let’s not get into language wars here. Everyone has their preference (I’m actually more of a Perl person).
JavaScript is as much a first class citizen of MacOS automation as AppleScript and others. Or as much third class as others, given Apple’s attitude to automation.
So if someone is having trouble with it, they deserve the same help as someone struggling with another problem. I think.

Slightly off-topic: JS is available across platforms. AppleScript is Mac-only. That, at least, is not a matter of taste :wink:

1 Like

JS is available across platforms. AppleScript is Mac-only. That, at least, is not a matter of taste :wink:

That’s right, and here’s another objective difference:

If we need a group of (key, value) pairs we can (in AppleScript or JavaScript) give a variable name to a value like:

{Alpha:70, Beta:60, Gamma:50, Epsilon:30}

AppleScript calls that a record, JavaScript calls it an Object.

When we need a list of the keys in our record/object.

In JavaScript:

let keyValues = {Alpha:70, Beta:60, Gamma:50, Epsilon:30}

Object.keys(keyValues)

// --> ["Alpha", "Beta", "Gamma", "Epsilon"]

in AppleScript, it quickly gets difficult, we have to import a library, and learn a verbose and exotic syntax:

use framework "Foundation"

set keyValues to {Alpha:70, Beta:60, Gamma:50, Epsilon:30}

(current application's NSDictionary's dictionaryWithDictionary:keyValues)'s allKeys() as list

And if we want to check the value of a key like 'Delta', which the record/object may or may not have (we’re not sure, and need to check):

In JavaScript:

keyValues.Delta
// -> undefined

keyValues.Gamma
// -> 50

But in AppleScript, the script just blows up with an error if we even try a key which the record doesn’t have:

To avoid tripping errors in AppleScript, we would have to use a ‘Foreign Function Interface’ to Objective C functions.

use framework "Foundation"


set keyValues to {Alpha:70, Beta:60, Gamma:50, Epsilon:30}

(current application's NSDictionary's dictionaryWithDictionary:keyValues)'s objectForKey:("Delta")

-- > missing value

OK, missing value is better … but if we use that same interface, when the record does have the key we tried, what we get back is a raw C pointer (don’t ask) and the poor beginner is now expected to handle its conversion to an AppleScript value:

use framework "Foundation"


set keyValues to {Alpha:70, Beta:60, Gamma:50, Epsilon:30}

(current application's NSDictionary's dictionaryWithDictionary:keyValues)'s objectForKey:("Beta")

--> «class ocid» id «data optr00000000D9FEBB59DFB6CA9F»

Checking the keys in a record, testing a key we’re not sure about – these are simple basics …

For example:

  • What is the full list of student names ?
  • Do I have an entry for this student ?

Very simple in nearly all languages. AppleScript is very unusual in making such basic things hard.

I have had great fun with AppleScript for over 20 years, and invested a huge amount of time in it. But I’m not going to pretend, just because I’m heavily invested in it, that it’s “easy”.

To be honest, it just ain’t.

When TaskPaper became JavaScript only, I experimented with learning a little JavaScript.

I soon noticed that it makes the basics quite a lot easier.

  • Not just easier records (see above),
  • but also much easier work with Strings and Lists (far more built-in operations for free),
  • and much richer libraries of things like:
    • regular expressions for pattern matching
    • url encoding and decoding
    • basic Math functions

Try to do some simple trigonometry for diagrams in OmniGraffle for example. Reach for AppleScript and you will be expected to write or bring your own sin and cos :frowning:

Reach for JS, and you will find them built in.

AppleScript “easy” ?

Fine if you’ve used it for years. Not a good place start. Especially now, in the context of iOS, and with AppleScript in sunset mode, and no longer actively developed.

2 Likes