Automating DT with JavaScript: Amending Markdown documents

Markdown has the advantage of being a pure text format, so it is easy to manipulate in JavaScript. Some examples are about to follow

Add a CSS file to a Markdown file

You can define a global CSS file for styling Markdown files in DT. Sometimes, however, you may need another CSS file for certain files. That’s what the following script helps with.

(() => {
/*
* set cssURL to the x-devonhink-url of the CSS file you want to use. It must point
* to a record in one of your DT databases.
*/
  const cssURL = "x-devonthink-item://0841105D-70B7-4518-8E2C-68E25CF8FC38";
  const app = Application("DEVONthink 3");
  const stylesheet = `<link rel="stylesheet" href="${cssURL}" />`;  
  app.selectedRecords().forEach(r => {
    const src = r.plainText();
    r.plainText = `${stylesheet}\n\n\${src}`
  })
})()

Add a table of contents to a Markdown file

The Markdown renderer auto-generates a table of content if it encounters the string {{TOC}}. To insert that string at the start of the Markdown file (i.e. before the first headline), the next script looks for the first # at the beginning of a line. If there’s no such line, there’s no point to insert a table of contents at all, so the script does nothing.

(() => {
  const app = Application("DEVONthink 3");
  /*
  * Regular expression to find at least one # sign at the start of a line,
  * followed by a space
  */
  const headline = new RegExp("^#+ ","m");
  /*
   * Loop over all selected records
   */
  app.selectedRecords().forEach(r => {
    const src = r.plainText();
	const found = src.match(headline);
    if (found) {
	    const position = found.index; // Start position of '#' in text
		/*
		* Rebuild the text from
		*  - first part of it (before the first '#')
		*  - {{TOC}} marker
		*  - rest of the text (from the first '#') to the end
		*/
	    r.plainText = `${src.substring(0, position)}\n\n{{TOC}}\n\n${src.substring(position)}`;
    }
  })
})()

Making the scripts more robust

As they stand, these scripts just take the currently selected DT records and modify them, assuming that they do contain Markdown. However, this approach is a bit risky: If someone selects a RTF file or a normal text document by accident, the script will modify them and possibly screw them up. So it is advisable to weed out all non-Markdown documents before looping over them. That is fairly simple (though not as simple as in AppleScript) with the whose method.

Instead of app.selectedRecords.forEach() you use this construct:

  app.selectedRecords.whose({_match: [ObjectSpecifier().type, "markdown"]})().forEach()

I have no idea what the Apple guys smoked or drank when they invented that, but I’d like to give that stuff a try occasionally.

In any case, it is the only way to check for the value of the type property in a call to whose. Though it looks very complicated, it is in fact simple: whose loops through all elements of app.selectedRecords, compares there type property to the value markdown and returns only those records that match the condition. This is a bit inaccurate – the return value is not an array, but an Object Specifier. So in order to get the underlying array, we need the penultimate set of empty brackets. And then we can use forEach on the remaining records.

4 Likes