Hiya! I’m a long time lurker, first time poster.
TL;DR: I created a AppleScript that adds incoming links to the bottom of a DEVONthink Markdown note. It’s loosely based on Bernardo_V’s work which can be found here.
I’m working to go all-in on using DEVONthink 3 (DT3) as my tool for persisting evergreen notes or slip-box notes. One of the things I really like about Andy’s notes is the “Links to this note” section he includes at the bottom of each note, which include a short summary of the note as well as the linked title.
Suffice it to say, I endeavored to mimic this sort of functionality in DT3 and the initial results are very good. Here’s a screen grab of my slip-box note that includes to auto-generated “Incoming Links” section:
In order to make this work, I opted to go with the wiki-style inter-note linking method as (a) I wanted to be able to easily differentiate between external and internal links and (b) this method doesn’t tightly couple my inter-note linking to DEVONthink.
Here’s the AppleScript that is responsible for adding the “Incoming Links” section, which is executed when the note is opened:
(*
This script adds Incoming links to a Markdown note executed via a DEVONthink 3 Smart Rule.
WARNING: It's assumed that the "Incoming Links" section is _always_ the last section
in the selected note. If the note includes text _after_ the "Incoming Links" section
it will be **removed**.
The script is loosely based on a similar script created by Bernardo_V and posted
to the DEVONthink Discourse forums.
See: https://discourse.devontechnologies.com/t/return-links-back-links/54390/2
*)
use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
property _sectionTitle : "Incoming Links"
property _newLine : "
"
on performSmartRule(theRecords)
repeat with _record in theRecords
my updateIncomingLinks(_record)
end repeat
end performSmartRule
on updateIncomingLinks(_record)
tell application id "DNtp"
if _record is not equal to missing value then
set _text to plain text of _record
set _database to current database
-- Search for all notes that reference the selected note.
set _terms to {name of _record, reference URL of _record}
set _terms to _terms & my stringToList(aliases of _record, {", ", "; "})
set _searchTerms to my buildSearchString(_terms)
set _results to search _searchTerms in _database
set _incomingLinks to {}
repeat with _result in _results
-- Note: There doesn't appear to be a way to exclude from the search results
-- records where the given terms appear before the "Incoming Links" section
-- using the build-in search operators.
--
-- Therefore, we check to see if the result contains one or more of the
-- given search terms by redacting the incoming links secion.
if _result's id is not equal to _record's id and my textContains(my removeIncomingLinks(_result's plain text), _terms) then
set _summaryText to every paragraph of rich text of _result
set item 1 of _summaryText to ""
set the end of _incomingLinks to my generateReferenceAndSummary(_result's name, _terms, _summaryText as text)
end if
end repeat
set _incomingLinks to my sortlist(_incomingLinks)
-- Remove and replace the "Incoming Links" section from the selected note
set _text to my removeIncomingLinks(_text)
if (count of _incomingLinks) is greater than 0 then
set _text to _text & return & return & "## " & _sectionTitle & _incomingLinks as text
set the plain text of _record to _text
else if plain text of _record is not equal to _text then
set the plain text of _record to _text
end if
end if
end tell
end updateIncomingLinks
on generateReferenceAndSummary(_aliases, _terms, _text)
return my generateReference(_aliases, _text) & my generateSummary(_terms, _text)
end generateReferenceAndSummary
on generateReference(_aliases, _text)
return (return & "* [[" & first item of my removeNonNumericValues(my stringToList(_aliases, {", ", "; "})) & "]]")
end generateReference
on generateSummary(_terms, _text)
set _summary to ""
repeat with _term in _terms
if _text contains _term then
set _text to my replaceText(_text, {_newLine, " "}, " ")
set _text to my replaceText(_text, {"[", "]", "•", "↩"}, "")
set _text to my replaceText(_text, "'", "'\\''")
try
set _summary to (do shell script "grep -o -E -i '((\\w+\\W+){0,15}" & _term & "(\\W+\\w+){0,15})' <<< '" & _text & "' | head -1") as Unicode text
--set _summary to my replaceText(_summary, _term, "**" & _term & "**")
set _summary to " <small>" & _summary & "...</small>"
exit repeat
end try
end if
end repeat
return _summary
end generateSummary
(* Removes the incoming links section for a given record *)
on removeIncomingLinks(_text)
set _old to AppleScript's text item delimiters
try
set AppleScript's text item delimiters to {return & return & "## " & _sectionTitle}
set _sections to every text item of _text
set AppleScript's text item delimiters to _old
set _text to item 1 of _sections
on error
set AppleScript's text item delimiters to _old
end try
return _text
end removeIncomingLinks
(* Returns the non-numeric values from a list *)
on removeNonNumericValues(_list)
set _newList to {}
repeat with _item in _list
try
set _result to do shell script "grep -E '^\\d+$' <<< \"" & _item & "\"" as string
on error
set _newList to _newList & _item
end try
end repeat
return _newList
end removeNonNumericValues
(* Checks to see if a given string contains a list of other strings *)
on textContains(_text, _strings)
repeat with _string in _strings
if _text contains _string then return true
end repeat
return false
end textContains
(* Converts a delimited string to a list *)
on stringToList(_text, _delims)
set _list to {}
if _text is not equal to "" and _text is not equal to missing value then
set _old to AppleScript's text item delimiters
set AppleScript's text item delimiters to _delims
set _list to every text item of _text
set AppleScript's text item delimiters to _old
end if
return _list
end stringToList
(* Builds a search string to find incoming links based on the list of terms provided *)
on buildSearchString(_terms)
set _searchStr to "content:"
set _total to count of _terms
set _cur to 0
repeat with _term in _terms
set _cur to _cur + 1
--set _searchStr to _searchStr & "(\"" & _term & "\" AND (\"" & _term & "\" NOT AFTER \"" & _sectionTitle & "\"))"
-- set _searchStr to _searchStr & "(\"" & _term & "\" NOT AFTER \"" & _sectionTitle & "\")"
set _searchStr to _searchStr & "(\"" & _term & "\")"
if _cur is not equal to _total then
set _searchStr to _searchStr & " OR "
end if
end repeat
return _searchStr & " kind:markdown"
end buildSearchString
on replaceText(theString, old, new)
set {TID, text item delimiters} to {text item delimiters, old}
set theStringItems to text items of theString
set text item delimiters to new
set theString to theStringItems as text
set text item delimiters to TID
return theString
end replaceText
on sortlist(theList)
set theIndexList to {}
set theSortedList to {}
repeat (length of theList) times
set theLowItem to ""
repeat with a from 1 to (length of theList)
if a is not in theIndexList then
set theCurrentItem to item a of theList as text
if theLowItem is "" then
set theLowItem to theCurrentItem
set theLowItemIndex to a
else if theCurrentItem comes before theLowItem then
set theLowItem to theCurrentItem
set theLowItemIndex to a
end if
end if
end repeat
set end of theSortedList to theLowItem
set end of theIndexList to theLowItemIndex
end repeat
return theSortedList
end sortlist
WARNING: Should you try using this script, please know that it assumes the “Incoming Links” section is always the last section in the selected note. If the note includes text after the “Incoming Links” section it will be removed by the script.
Here’s how I setup the Smart Rule that executes the script:
Enjoy!