Copy Zettel as link in DEVONthink

Following up on my recent article on cleaning up Zettelkasten WikiLinks in DEVONthink, here’s another script to solve the problem of linking notes.

Backing up to the problem. In the Zettelkasten (or archive) - Zettel (or notes) are stored as list of Markdown files. But what happens when I want to add a link to another note into one that I’m writing? Since DEVONthink recognizes WikiLinks, I can just start typing but then I have to remember the exact date so that I can pick the item out of the contextual list that DEVONthink offers as links. Also, if I took that approach I would end up with the same problem of excessive text in the link that I would then have to clean up with the previous script.

Instead, the easier approach is to use AppleScript again, triggered by a Keyboard Maestro macro to extract the UID from the title and put it on the clipboard, ready to go into the note I’m writing. Simple, but small inconveniences amplied over time become impediments to efficient learning and writing.

(*
Copyright © 2020 Alan Duncan

Permission is hereby granted, free of charge, to any person obtaining a copy of this 
software and associated documentation files (the “Software”), to deal in the Software 
without restriction, including without limitation the rights to use, copy, modify, merge, 
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 
to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies 
or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
DEALINGS IN THE SOFTWARE.
*)

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

tell application id "DNtp"
   set theSelections to the selection
   if theSelections is {} then
      error localized string "Please select at least one document, then try again."
   end if
   repeat with theRecord in theSelections
      set theTitle to name of theRecord
      set the clipboard to "[[" & extractLink(theTitle) of me & "]]"
   end repeat
end tell

on extractLink(theText)
   return do shell script "echo " & quoted form of theText & " | grep -Eo '[0-9]{14}'"
end extractLink

N.B. In order to effectively use links in the form of [[20201207215041]], you’ll need to store the 14 digit timestamp as an alias for the actual Zettel tile. See the previous article that discusses it further or this article by Mark Honomichl on using aliases in a DEVONthink-based Zettelkasten.

References:

Cleaning up Zettelkasten WikiLinks in DEVONthink Pro

Organizing and reorganizing knowledge is one my seemingly endless tasks. For years, I’ve used DEVONthink as my primary knowledge repository. Recently, though I began to lament the fact that while I seemed to be collecting and storing knowledge in a raw form in DEVONthink, that I wasn’t really processing and engaging with it intellectually.1 In other words, I found myself collecting content but not really synthesizing, personalizing and using it. While researching note-taking systems in the search for a better way to process and absord the information I had been collecting, I discovered the Zettelkasten method. I’m not going to go into it here because it has been exhaustively described elsewhere. But I’ll just say that:

  • The Zettelkasten method is an effective way of organizing, learning, processng and refactoring knowledge.
  • Maintaing a Zettelkasten becomes a central component of converting passive collection of material to the active use of the material.
  • DEVONthink Pro is an effective and flexible tool for implementing the Zettelkasten method.

One methodological detail that I need to explain about Zettelkasten is the linkage between notes. Since every note gets a unique identifier (for me, just a date/timestamp), then notes get linked together in logical ways by the user. For example, I have a Zettel with the title 20201202204635 Present passive participle -авать verbs. Its unique identifier is 20201202204635. Now in DEVONthink, notes can be linked with WikiLinks. If you begin typing [[ followed by the title of a note you get a popup list of matching selections. It’s a fast way of inserting a link to another Zettel.

The problem is this: I don’t want the entire title of the linked note in the WikiLink. Using the example above, I want my link to look like [[20201202204635]] not like [[20201202204635 Present passive participle -авать verbs]]

The solution is two-part

  • Create a document alias in DEVONthink by extracting just the unique ID from the title
  • Parse notes for outgoing WikiLinks and reformat them to use just the aliased unique ID

Extracting unique identifier as a document alias

This part of the solution belongs to Mark Honomichl. His post Using Aliases in DEVONthink for Zettelkasten describes a Smart Rule in DEVONthink that extracts the unique ID from the document title and uses that for the alias.

The second part of the solution requires the first. In other words, every Zettel must have an alias formed from its unique ID.

Having established that, you can fix WikiLinks in existing Zettels to use the alias. Again, converting a WikiLink like [[20201202204635 Present passive participle -авать verbs]] to [[20201202204635]] Like the first part of the solution, this is implemented in AppleScript. For now, I’ve chosen to run it on demand, but it could easily be reconfigured as a Smart Rule in DEVONthink.

(*
Copyright © 2020 Alan Duncan

Permission is hereby granted, free of charge, to any person obtaining a copy of this 
software and associated documentation files (the “Software”), to deal in the Software 
without restriction, including without limitation the rights to use, copy, modify, merge, 
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 
to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies 
or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
DEALINGS IN THE SOFTWARE.
*)


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

property NSRegularExpressionCaseInsensitive : a reference to 1
property NSRegularExpression : a reference to current application's NSRegularExpression
property NSNotFound : a reference to 9.22337203685477E+18 + 5807 -- see http://latenightsw.com/high-sierra-applescriptobjc-bugs/


tell application id "DNtp"
   set theSelections to the selection
   if theSelections is {} then
      error localized string "Please select at least one document, then try again."
   end if
   repeat with theRecord in theSelections
      set recordText to (plain text of theRecord)
      set newText to fixLinks(recordText) of me as string
      set plain text of theRecord to newText
   end repeat
end tell

on fixLinks(theText)
   set linkPattern to "\\[\\[(\\d{14})\\s[^\\]]+\\]\\]"
   set replacementPattern to "\\[\\[$1\\]\\]"
   set theRegEx to NSRegularExpression's regularExpressionWithPattern:linkPattern options:NSRegularExpressionCaseInsensitive |error|:(missing value)
   set retString to theRegEx's stringByReplacingMatchesInString:theText options:0 range:[0, theText's length] withTemplate:replacementPattern
   return retString
end fixLinks

To illustrate, here’s a note before and after the conversion process:

To use the script, just select one or more documents and run.

References

Notes

  • 2020-12-08 04-44-56 A previous version of this script used a buggy regular expression. This has been corrected.

  1. This is the so-called collector’s fallacy wherein we tend to feel fulfilled and successful solely by collecting lots and lots of information. But at some point, we begin to realize that knowledge work isn’t a passive process at all. Anyone can collect information. To be of use, we have to engage with it in an active ongoing way. Building mental models is an active, not passive proces. ↩︎

Regex to match a cloze

Anki and some other platforms use a particular format to signify cloze deletions in flashcard text. It has a format like any of the following: {{c1::dog::}} {{c2::dog::domestic canine}} Here’s a regular expression that matches the content of cloze deletions in an arbitrary string, keeping only the main clozed word (in this case dog.) {{c\d::(.*?)(::[^:]+)?}} To see it in action, here it is in action in a Python script:

Removing stress marks from Russian text

Previously, I wrote about adding syllabic stress marks to Russian text. Here’s a method for doing the opposite - that is, removing such marks (ударение) from Russian text. Although there may well be a more sophisticated approach, regex is well-suited to this task. The problem is that def string_replace(dict,text): sorted_dict = {k: dict[k] for k in sorted(dict)} for n in sorted_dict.keys(): text = text.replace(n,dict[n]) return text dict = { "а́" : "а", "е́" : "е", "о́" : "о", "у́" : "у", "я́" : "я", "ю́" : "ю", "ы́" : "ы", "и́" : "и", "ё́" : "ё", "А́" : "А", "Е́" : "Е", "О́" : "О", "У́" : "У", "Я́" : "Я", "Ю́" : "Ю", "Ы́" : "Ы", "И́" : "И", "Э́" : "Э", "э́" : "э" } print(string_replace(dict, "Существи́тельные в шве́дском обычно де́лятся на пять склоне́ний.

"Delete any app that makes money off your attention."

Listening to Cal Newport interviewed on a recent podcast, something he said resonated. I’m probably paraphrasing, but a key piece of advice was: “Delete any app that makes money off your attention." Seems like really good advice. A smartphone is a collection of tools embedded in a tool. Use it like a tool and not an entertainment device and you’ll be find. For a while, in an effort to pry myself loose from the psychic hold of the smartphone I went back to using some kind of old flip phone.

URL-encoding URLs in AppleScript

The AppleScript Safari API is apparently quite finicky and rejects Russian Cyrillic characters when loading URLs. For example, the following URL https://en.wiktionary.org/wiki/стоять#Russian throws an error in AppleScript. Instead, Safari requires URL’s of the form https://en.wiktionary.org/wiki/%D1%81%D1%82%D0%BE%D1%8F%D1%82%D1%8C#Russian whereas Chrome happily consumes whatever comes along. So, we just need to encode the URL thusly: use framework "Foundation" – encode Cyrillic test as "%D0" type strings on urlEncode(input) tell current application's NSString to set rawUrl to stringWithString_(input) – 4 is NSUTF8StringEncoding set theEncodedURL to rawUrl's stringByAddingPercentEscapesUsingEncoding:4 return theEncodedURL as Unicode text end urlEncode When researching Russian words for vocabulary study, I use the URL encoding handler to load the appropriate words into several reference sites in sequential Safari tabs.

Consume media outside one's bubble?

That “reality bubbles” contribute heavily to increasing political polarization is well-known. Customized media diets at scale and social media feeds that are tailored to individual proclivities progressively narrow our understanding of perspectives other than our own. Yet, the cures are difficult and uncertain. Often, though, we’re advised to consume media from the other side of the political divide. A sentence from a recent piece in The Atlantic encapsulates why I think this is such a fraught idea:

Свидетельство того или тому?

I was puzzled by this sentence on the BBC Russian Service: Нет свидетельств тому, что на нынешних выборах дело обстоит иначе. ББС Мошенничество на выборах в США? Проверяем факты в речи Трампа It means “There is no evidence that in the current election things are any different." but the puzzle isn’t the meaning, it’s the grammatical case in which the author has placed the demonstrative pronoun то , which is dative here тому .

Escaping "Anki hell" by direct manipulation of the Anki sqlite3 database

There’s a phenomenon that verteran Anki users are familiar with - the so-called “Anki hell” or “ease hell.” Origins of ease hell The descent into ease hell has to do with the way Anki handles correct and incorrect answers when it presents cards for review. Ease is a numerical score associated with every card in the database and represents a valuation of the difficulty of the card. By default, when cards graduate from the learning phase, an ease of 250% is applied to the card.

Typing Russian stress marks on macOS

While Russian text intended for native speakers doesn’t show accented vowel characters to point out the syllabic stress (ударение) , many texts intended for learners often do have these marks. But how to apply these marks when typing? Typically, for Latin keyboards on macOS, you can hold down the key (like long-press on iOS) and a popup dialog will show you options for that character. But in the standard Russian phonetic keyboard it doesn’t work.