AppleScript help: move record with group selector, then copy destination group source link as markdown

I’m trying to write an Applescript that would basically have the following workflow:

  1. With a new note the script first allows me to select, with a group selector pop-up, a destination group
  2. Script then moves the note to the selected destination group
  3. Then script selects the destination group’s DT item link formatted as Markdown i.e. [Group_Name](DT-source_item_link). Clarification: this link should be to the first parent group.
  4. Then adds/appends the Markdown group item link at the end of the first line of the (Markdown) record.
  5. Finally the script changes the record name with the updated first line of the (Markdown) record, including the Markdown group link

Step 1 and 2 I believe could be achieved with the “display group selector” together with the “move” command. 3 could be achieved by combining the “group location” property of the record together with “get record with uuid” command. Step 4 would I’m somewhat lost without regex. Step 5 is somehow achieved with the “display name editor” command.

I’ve tried combining all of these steps but it is confusing and I’m getting lost in all of the window commands and lack of regex.

I would love to get some tips how to overcome my current “writer’s block” for my DT dream script!

Your steps 4 and 5 are not clear. What is “the Markdown record” here? Where does it come from? Perhaps giving short examples of what you have and what you want to have helps.

In step 3, I’d assume that you want to use group you just moved the record to. No need for “group location” then. You certainly don’t need a regular expression to append text to a markdown record. And if you want your script to change the name of a record, what’s the use of a dialog where the user enters the new name? It’s one or the other, I’d guess.

Use JavaScript instead of AppleScript if you need regular expressions.

Thanks for you reply! So an illustrative example would be:

  1. I’m writing a new note in Markdown about Granny Smith apples. My first line is: The Granny Smith is an apple cultivar that originated in Australia in 1868.
  2. I then launch my script, which opens up a group selector. I choose a group called #Apples placed in the “Research” data base, with the following location path Research/Agriculture/Fruits/#Apples.
  3. The script moves the note to the #Apples group
  4. The script appends at the end of the first line of my Markdown note the following: [\#Apples](DT-item-link) where DT-item-link is the source item link to the #Apples group
  5. The record name is then updated to: The Granny Smith is an apple cultivar that originated in Australia in 1868. #Apples

I would suggest you refrain from using such punctuation as # in filenames.

What do you do when the document is moved to another group ?

OK, the # at the beginning of the group name is optional (my internal research workflow nomenclature).

What do you do when the document is moved to another group ?

That’s it. I’m finished! The note is renamed, filed in the correct group, and the first line has a link to the group (step 4).

The regex comment was because I’m currently able to get the full location path, but would like to extract the last folder name only i.e. in regex /\#.*$

Why don’t you use the Sorter to create the Markdown note and put it in the desired location from the start?

2 Likes

Regardless of how to do that: I think you’re over-engineering here. What’s the point of having a link to the parent group in the MD document? Or why add the name of the group to the record’s name? It looks nice, but you’ll have to change a bunch of things if you find out that Granny Smith is not an apple but a pear…

Here’s an educational script in JavaScript that tries to do nearly what you want. I didn’t test it, though. Should be easy to convert to AppleScript.

Note the link is appended to the end of the MD document, not to the end of the first line. Easy to change, I think (using a regular expression)

(() => {
  const app = Application("DEVONthink 3");
  const record = app.selectedRecords[0];
  const group = app.displayGroupSelector("Select group to move record to");
  if (!group) return;
  const groupLink = `[${group.name()}](x-devonthink-item://${group.uuid()})`;
  record.plainText = record.plainText() + `\n\n${groupLink}\n`;
  record.name = `${record.name()} ${group.name()}`;
  app.move({record: record, to: group});
})()

Sorter is awesome but the location has to be selected with mouse (not keyboard). With +G/+M/group selector my own group hierarchies are quickly visible. I would want to use this feature for dozens of small mini-notes on a daily basis, hence Sorter is not as streamlined for this purpose.

Wow, thanks! This is AWESOME! I’ll try this!

But I still don’t see why you’d want the link to the group and the group name in the file (name).

1 Like

For DTTG is the simple answer. I’ve created a Zettelkasten/Wiki-style network of hundreds of small research notes. I would like to access this system on the go as well. DTTG is good but it has some flaws (I find DTTG best for browsing more than processing or creating) – whenever you open a linked note you have to press 2-3 times to see in which group the record is situated. Since I’m using group hierarchies to provide context, this creates resistance. I want to instantly see in the record title that I’ve arrived to a note on the topic of “#Apples” (especially since not all notes are downloaded). Another option would be to write all of that context every time in every note, but that seems like plenty of redundant information.

If you’re using so many Markdown documents, why not do a full sync?

My research often involves heavy PDFs in numerous piles and pipelines. I don’t know if it makes sense to download all of those 100Mbs on DTTG every time?

Likewise; context in the title
I use an applescript to process my Inbox entries
My organization is tag based and for context I append the tag-names to the title

I then launch my script, which opens up a group selector

How does the script identify the list for “group selector”?
My script has a hardcoded list of Type-Tags
after that, sub-tags lists are retrieved dynamically from the tags table

Sample code for a “selector” list is

set theNoteTypes to (choose from list notetypeList with prompt "Specify Note Type" with multiple selections allowed)

The selector is provided by DT. You can limit the list to one database, and that’s that.

I am a bit late in the game but given I am trying to teach myself AppleScript, I thought it would be nice to create a solution for this question. I have created a solution that is perhaps a bit verbose. I prefer readability.


use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions

# SYNOPSIS
#   getMatch(text, regexString) -> { overallMatch[, captureGroup1Match ...] } or {}
# DESCRIPTION
#   Matches string s against regular expression (string) regex using bash's extended regular expression language and
#   *returns the matching string and substrings matching capture groups, if any.*
#   
#   - AppleScript's case sensitivity setting is respected; i.e., matching is case-INsensitive by default, unless this subroutine is called inside
#     a 'considering case' block.
#   - The current user's locale is respected.
#   
#   IMPORTANT: 
#   
#   Unlike doesMatch(), this subroutine does NOT support shortcut character classes such as \d.
#   Instead, use one of the following POSIX classes (see `man re_format`):
#       [[:alpha:]] [[:word:]] [[:lower:]] [[:upper:]] [[:ascii:]]
#       [[:alnum:]] [[:digit:]] [[:xdigit:]]
#       [[:blank:]] [[:space:]] [[:punct:]] [[:cntrl:]] 
#       [[:graph:]]  [[:print:]] 
#   
#   Also, `\b`, '\B', '\<', and '\>' are not supported; you can use `[[:<:]]` for '\<' and `[[:>:]]` for `\>`
#   
#   Always returns a *list*:
#    - an empty list, if no match is found
#    - otherwise, the first list element contains the matching string
#       - if regex contains capture groups, additional elements return the strings captured by the capture groups; note that *named* capture groups are NOT supported.
#  EXAMPLE
#       my getMatch("127.0.0.1", "^([[:digit:]]{1,3})\\.([[:digit:]]{1,3})\\.([[:digit:]]{1,3})\\.([[:digit:]]{1,3})$") # -> { "127.0.0.1", "127", "0", "0", "1" }
#  SOURCE
#       https://stackoverflow.com/questions/997828/is-there-something-akin-to-regex-in-applescript-and-if-not-whats-the-alternat
on getMatch(s, regex)
	local ignoreCase, extraCommand
	set ignoreCase to "a" is "A"
	if ignoreCase then
		set extraCommand to "shopt -s nocasematch; "
	else
		set extraCommand to ""
	end if
	# Note: 
	#  So that classes such as [[:alpha:]] work with different locales, we need to set the shell's locale explicitly to the current user's.
	#  Since `quoted form of` encloses its argument in single quotes, we must set compatibility option `shopt -s compat31` for the =~ operator to work.
	#  Rather than let the shell command fail we return '' in case of non-match to avoid having to deal with exception handling in AppleScript.
	tell me to do shell script "export LANG='" & user locale of (system info) & ¬
		".UTF-8'; shopt -s compat31; " & extraCommand & "[[ " & quoted form of s & " =~ " & ¬
		quoted form of regex & " ]] && printf '%s\\n' \"${BASH_REMATCH[@]}\" || printf ''"
	return paragraphs of result
end getMatch


tell application "DEVONthink 3"
	-- Bail out if nothing is selected
	if selection is {} then return
	-- Only works with one record selected
	if (count of selection) > 1 then return
	-- Get a reference to the selected document
	set theDocument to first item of (selection as list)
	-- Also only accept markdown documents
	if type of theDocument is not markdown then return
	-- Get the text of the markdown document
	set theText to the plain text of theDocument
	-- Select the group
	set theGroup to display group selector
	-- Find the line that starts with a hash and at least one space
	set theNewText to ""
	set theTitle to ""
	repeat with theLine in paragraphs of theText
		set theMatch to my getMatch(theLine, "^#[[:space:]]*(.+)$")
		if (count of theMatch) > 0 then
			set theTitle to last item of theMatch
			-- Append the group link to the end of the title line
			set theLine to theLine & " [" & name of theGroup & ¬
				"](x-devonthink-item://" & uuid of theGroup & ")"
			log theLine
		end if
		set theNewText to theNewText & theLine & return
	end repeat
	-- Did we find a title
	if (count of theTitle) = 0 then
		error "Could not find the title line in the document"
	end if
	-- Move the document
	set theDocument to move record theDocument to theGroup
	-- Replace the title
	set name of theDocument to (theTitle & " " & name of theGroup)
	-- Replace the content of the document
	set plain text of theDocument to (theNewText as string)
end tell

What OS are you running?

I guess you’re asking because shopt is a bash built-in, and the current default shell under macOS is zsh? The original code for the GetMatch handler was posted to StackExchange over 10 years ago, so it is a big gray at the temples.

A more portable variant might use egrep. In any case, I don’t understand the purpose of the regular expression search there, which seems to be searching for first-level headlines. And then it’s using the last of them (I think).

The RE is, BTW, not matching first-level headlines only. It would also find #no space here and ## second level heading at the beginning of a line. The Golden Rule of REs (which I just invented) says: Do not use the * unless you really, really want to match nothing, too. Instead, use a + if you need at least one match.

But as with my script, that’s not what the OP was asking for: They wanted to append the link to “the first line” of the MD file, not the first first-level heading. So something like

set firstLine to first paragraph of theText

(if that is even AppleScript) and then appending the link to firstLine should suffice. In JavaScript, I’d do something like

const splitText = r.plainText().split('\n\n');
splitText[0] += completeLink; /* probably a bit ugly because of missing space */
r.plaintext = splitText.join('\n\n');

I am not sure if one should use one or two newlines as paragraph separator. But that’s easy to figure out.

Correct. And that should be explicitly stated as a dependency if it’s to be used.