Part of workflow discussed here: Automatically capture and annotate items (to use with Obsidian)
Applescript: Markdown Annotation helper
use AppleScript version "2.4" -- Yosemite (10.10) or later
use script "RegexAndStuffLib" version "1.0.7"
use DT : script "DEVONthink helper"
use scripting additions
property obsidianVault : "Notes"
on getSections(maText)
-- item 1: frontmatter
-- item 2: info (title and excerpt)
-- item 3: content
-- set maSections to regex split maText search pattern "^---$"
set maSections to regex search maText search pattern "^---\n(.*?)\n---\n*(.*?# .*?\n(?:\n*> [Ee]xcerpt::.*?\n)?)*\n*(.*)" capture groups {1, 2, 3} with dot matches all
if maSections is equal to {} or ((count of maSections) > 0 and ((count of (item 1 of maSections)) < 2)) then error "No sections found"
return item 1 of maSections
end getSections
on joinSections(maSections)
set theFrontmatter to getFrontmatterFromList(maSections)
set theInfo to getInfoFromList(maSections)
set theContent to getContentFromList(maSections)
if theInfo is not equal to "" and theContent is not equal to "" then
return "---\n" & theFrontmatter & "\n---\n\n" & theInfo & "\n\n" & theContent
else if (theInfo is not equal to "") and (theContent is equal to "") then
return "---\n" & theFrontmatter & "\n---\n\n" & theInfo
else if (theInfo is equal to "") and (theContent is not equal to "") then
return "---\n" & theFrontmatter & "\n---\n\n" & theContent
else
return "---\n" & theFrontmatter & "\n---"
end if
end joinSections
on updateSections(maSections, maFrontmatter, maInfo, maContent)
if maFrontmatter is not equal to missing value and maFrontmatter is not equal to "" then
set item 1 of maSections to maFrontmatter
end if
if maInfo is not equal to missing value and maInfo is not equal to "" then
set item 2 of maSections to maInfo
end if
if maContent is not equal to missing value then
set item 3 of maSections to maContent
end if
(*
if maFrontmatter is not equal to missing value and maFrontmatter is not equal to "" then
set item 2 of maSections to maFrontmatter
end if
if maInfo is not equal to missing value and maInfo is not equal to "" then
set item 3 of maSections to maInfo
end if
if maContent is not equal to missing value then
set item 4 of maSections to maContent
end if
*)
return maSections
end updateSections
on getFrontmatterFromList(maSections)
--set theResult to item 2 of maSections
set theResult to item 1 of maSections
if theResult is equal to "" then error "No frontmatter found"
return theResult
end getFrontmatterFromList
on getFrontmatterFromText(maText)
set maSections to getSections(maText)
return my getFrontmatterFromList(maSections)
end getFrontmatterFromText
on getInfoFromList(maSections)
return item 2 of maSections
end getInfoFromList
on getInfoFromText(maText)
set maSections to getSections(maText)
return my getInfoFromList(maSections)
end getInfoFromText
on getContentFromList(maSections)
return item 3 of maSections
end getContentFromList
on getContentFromText(maText)
set maSections to getSections(maText)
return my getContentFromList(maSections)
end getContentFromText
on getURL(maText)
set theResult to regex search once maText search pattern "^url:\\s*(.*)\\s*$" capture groups 1
-- if theResult is equal to "" then error "No URL found"
return theResult
end getURL
on getDate(maText)
set theResult to regex search once maText search pattern "^date:\\s*(.*)\\s*$" capture groups 1
if theResult is equal to "" then error "No date attribute found"
return theResult
end getDate
on getTitle(maText)
(*
set theResult to regex search once maText search pattern "^# (.*)$" capture groups 1
if theResult is equal to "" then
set theResult to regex search once maText search pattern "^title: (.*)$" capture groups 1
end if
if theResult is equal to "" then error "No title found"
return theResult
*)
return
end getTitle
on getItemURL(maText)
set theResult to regex search once maText search pattern "^itemurl:\\s*(.*)\\s*$" capture groups 1
if theResult is equal to "" then error "No item URL found"
return theResult
end getItemURL
on getItemUUID(maText)
set theResult to regex search once maText search pattern "^itemurl: x-devonthink-item://(.*)\\s*$" capture groups 1
if theResult is equal to "" then error "No item UUID found"
return theResult
end getItemUUID
on getAnnotationURL(maText)
set theResult to regex search once maText search pattern "^annotationurl:\\s*(.*)\\s*$" capture groups 1
if theResult is equal to "" then error "No annotation URL found"
return theResult
end getAnnotationURL
on getAnnotationUUID(maText)
set theResult to regex search once maText search pattern "^annotationurl:\\s*x-devonthink-item://(.*)\\s*$" capture groups 1
if theResult is equal to "" then error "No annotation UUID found"
return theResult
end getAnnotationUUID
on getTags(maText)
set maSections to my getSections(maText)
set maFrontmatter to my getFrontmatterFromList(maSections)
set maContent to my getContentFromList(maSections)
-- get tags from a list in the front matter
-- a tag list (in frontmatter) is formatted as:
--
-- tags:
-- - tagA
-- - tagB
-- - tagC
set tagsFromFrontmatterList to regex search maFrontmatter search pattern "tags:\n(- (.*)?)\n(?:[^-])" capture groups 1 with dot matches all
if (count of tagsFromFrontmatterList) is greater than 0 then
set tagsFromFrontmatterList to regex split (item 1 of tagsFromFrontmatterList) search pattern "\n" with dot matches all
set tagsFromFrontmatterList to regex change tagsFromFrontmatterList search pattern "^- (.*)" replace template "$1"
end if
-- get tags from an array in the front matter
-- a tags array (in frontmatter) is formatted as :
--
-- tags: [tagA, tagB, tagC]
set tagsFromFrontmatterArray to regex search maFrontmatter search pattern "^tags:\\s*\\[(.*?)\\]\\s*$" capture groups 1
if (count of tagsFromFrontmatterArray) is greater than 0 then
set tagsFromFrontmatterArray to split string (item 1 of tagsFromFrontmatterArray) using delimiters ","
end if
-- get tags from content
--
-- #tagA random text #tagB
-- #tagC
-- more text
set tagsFromContent to regex search maContent search pattern "\\s*#([a-zA-Z-0-9_-]*)" capture groups 1
-- combine all the tags - this can still contain duplicates!
set theResult to tagsFromFrontmatterList & tagsFromFrontmatterArray & tagsFromContent
-- return all tags as lowercase
set theResult to lowercase from theResult
-- result is a list
return theResult
end getTags
on getExcerpt(maText)
--set maInfo to my getInfoFromText(maText)
set theExcerpt to regex search once maText search pattern "^> [Ee]xcerpt::\\s*(.*)\\s*$" replace template "$1" capture groups 1
end getExcerpt
on createText(theDate, theTitle, theExcerpt, theURL, theItemURL, theAnnotationURL, fullPath, theTags)
if (theExcerpt is not missing value and theExcerpt is not equal to "") then
set theExcerpt to regex batch theExcerpt change pairs {{"\n", " "}, {"^\\s*", ""}, {"\\s*$", ""}, {"\\[", "\\\\["}, {"\\]", "\\\\]"}}
set theExcerpt to "\n\n> excerpt:: " & theExcerpt
else
set theExcerpt to ""
end if
if (theURL is not missing value and theURL is not equal to "") then
set theURL to "\nurl: " & theURL
else
set theURL to ""
end if
set shortPath to my shortPath(fullPath)
if (theTags is not missing value and (count of theTags) > 0) then
set theTagsString to join strings theTags using delimiter ","
set theTags to "\ntags: [" & theTagsString & "]"
else
set theTags to ""
end if
--set theMacURL to my getMacURL(thePath)
--set theIosURL to my getIosURL(thePath)
--set theObsidianURL to my getObsidianURL(theTitle)
--set theLinks to theDate & " | Item [DT](" & theItemURL & ") [Mac](" & theMacURL & ") [iOS](" & theIosURL & ") | Annotation [DT](" & theAnnotationURL & ") [Obsidian](" & theObsidianURL & ")"
--set theResult to "---\ndate: " & theDate & "\ntitle: " & theTitle & "\nurl: " & theURL & "\nitemurl: " & theItemURL & "\nannotationurl: " & theAnnotationURL & "\nmacurl: " & theMacURL & "\niosurl: " & theIosURL & "\ntags: [" & theTagsString & "]\n---" & "\n\n" & theLinks & "\n\n# " & theTitle & theExcerpt & "\n"
set theResult to "---\ndate: " & theDate & theURL & "\nitemurl: " & theItemURL & "\nannotationurl: " & theAnnotationURL & "\npath: " & shortPath & theTags & "\n---" & theExcerpt
return theResult
end createText
on updateText(maText, theDate, theTitle, theExcerpt, theURL, theItemURL, theAnnotationURL, fullPath, theTags, stripTagsFromContent)
set shortPath to my shortPath(fullPath)
if (maText is equal to missing value or maText is equal to "") then
set maText to my createText(theDate, theTitle, theExcerpt, theURL, theItemURL, theAnnotationURL, fullPath, theTags)
else
set maInput to my getSections(maText)
if (theDate is not equal to missing value and theDate is not equal to "") then
set maSections to updateDate(maInput, theDate, true)
end if
if (theTitle is not equal to missing value and theTitle is not equal to "") then
set maSections to my updateTitle(maInput, theTitle, fullPath, true)
else if (shortPath is not equal to missing value and shortPath is not equal to "") then
set maSections to my updatePath(maInput, fullPath, true)
end if
if (theURL is not equal to missing value and theURL is not equal to "") then
set maSections to my updateURL(maInput, theURL, true)
end if
if theExcerpt is not equal to missing value then
set maSections to my updateExcerpt(maInput, theExcerpt, true)
end if
if (theItemURL is not equal to missing value and theItemURL is not equal to "") then
set maSections to my updateItemURL(maInput, theItemURL, true)
end if
if (theAnnotationURL is not equal to missing value and theAnnotationURL is not equal to "") then
set maSections to my updateAnnotationURL(maInput, theAnnotationURL, true)
end if
if (theTags is not equal to missing value and theTags is not equal to {}) then
set maSections to my updateTags(maInput, theTags, stripTagsFromContent, true)
end if
set maText to my joinSections(maSections)
end if
return maText
end updateText
on updateDate(maInput, theDate, returnList)
if class of maInput is list then
set maSections to maInput
else
set maSections to my getSections(maInput)
end if
set maFrontmatter to my getFrontmatterFromList(maSections)
set maInfo to my getInfoFromList(maSections)
-- Update date in frontmatter
set maFrontmatter to regex change maFrontmatter search pattern "^date: (.*)$" replace template "date: " & theDate
-- Update date in info
set maInfo to regex change maInfo search pattern "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}" replace template theDate
set maSections to my updateSections(maSections, maFrontmatter, maInfo, missing value)
if returnList is true then
return maSections
else
return joinSections(maSections)
end if
end updateDate
on updateTitle(maInput, theTitle, thePath, returnList)
if class of maInput is list then
set maSections to maInput
else
set maSections to my getSections(maInput)
end if
set maFrontmatter to my getFrontmatterFromList(maSections)
set maInfo to my getInfoFromList(maSections)
-- Update title: in frontmatter
--set maFrontmatter to regex change maFrontmatter search pattern "^title: (.*)$" replace template "title: " & theTitle
-- Update path
if thePath is not missing value and thePath is not equal to "" then
set maFrontmatter to regex change maFrontmatter search pattern "^path: (.*)$" replace template "path: " & my shortPath(thePath)
end if
-- Update title: in info
--set maInfo to regex change maInfo search pattern "^# .*" replace template "# " & theTitle
-- Update Obsidian URL in info
--set obsidianURL to getObsidianURL(theTitle)
--set maInfo to regex change maInfo search pattern "\\[Obsidian annotation\\]\\(.*?\\)" replace template "[Obsidian annotation](" & obsidianURL & ")"
set maSections to my updateSections(maSections, maFrontmatter, maInfo, missing value)
if returnList is true then
return maSections
else
return joinSections(maSections)
end if
end updateTitle
on updateExcerpt(maInput, theExcerpt, returnList)
if class of maInput is list then
set maSections to maInput
else
set maSections to my getSections(maInput)
end if
set maFrontmatter to my getFrontmatterFromList(maSections)
set maInfo to my getInfoFromList(maSections)
if theExcerpt is not equal to missing value and theExcerpt is not equal to "" then
set theExcerpt to regex batch theExcerpt change pairs {{"\n", " "}, {"^\\s*", ""}, {"\\s*$", ""}, {"\\[", "\\\\["}, {"\\]", "\\\\]"}}
set maInfo to regex change maInfo search pattern "^> [Ee]xcerpt::.*\\s*$" replace template "> excerpt:: " & theExcerpt
end if
set maSections to my updateSections(maSections, missing value, maInfo, missing value)
if returnList is true then
return maSections
else
return joinSections(maSections)
end if
end updateExcerpt
on updateURL(maInput, theURL, returnList)
if class of maInput is list then
set maSections to maInput
else
set maSections to my getSections(maInput)
end if
set maFrontmatter to my getFrontmatterFromList(maSections)
set maInfo to my getInfoFromList(maSections)
set maFrontmatter to regex change maFrontmatter search pattern "^url:.*$" replace template "url: " & theURL
--set maInfo to regex change maInfo search pattern "\\[URL\\]\\(.*?\\)" replace template "[URL](" & theURL & ")"
set maSections to my updateSections(maSections, maFrontmatter, maInfo, missing value)
if returnList is true then
return maSections
else
return joinSections(maSections)
end if
end updateURL
on updateItemURL(maInput, theItemURL, returnList)
if class of maInput is list then
set maSections to maInput
else
set maSections to my getSections(maInput)
end if
set maFrontmatter to my getFrontmatterFromList(maSections)
set maInfo to my getInfoFromList(maSections)
set maFrontmatter to regex change maFrontmatter search pattern "^itemurl:.*$" replace template "itemurl: " & theItemURL
--set maInfo to regex change maInfo search pattern "Item \\[DT\\]\\(.*?\\)" replace template "Item [DT](" & theItemURL & ")"
set maSections to my updateSections(maSections, maFrontmatter, maInfo, missing value)
if returnList is true then
return maSections
else
return joinSections(maSections)
end if
end updateItemURL
on updateAnnotationURL(maInput, theAnnotationURL, returnList)
if class of maInput is list then
set maSections to maInput
else
set maSections to my getSections(maInput)
end if
set maFrontmatter to my getFrontmatterFromList(maSections)
set maInfo to my getInfoFromList(maSections)
set maFrontmatter to regex change maFrontmatter search pattern "^annotationurl:.*$" replace template "annotationurl: " & theAnnotationURL
--set maInfo to regex change maInfo search pattern "Annotation \\[DT\\]\\(.*?\\)" replace template "Annotation [DT](" & theAnnotationURL & ")"
set maSections to my updateSections(maSections, maFrontmatter, maInfo, missing value)
if returnList is true then
return maSections
else
return joinSections(maSections)
end if
end updateAnnotationURL
on updatePath(maInput, fullPath, returnList)
if class of maInput is list then
set maSections to maInput
else
set maSections to my getSections(maInput)
end if
set maFrontmatter to my getFrontmatterFromList(maSections)
set maInfo to my getInfoFromList(maSections)
set shortPath to my shortPath(fullPath)
--set theMacURL to my getMacURL(thePath)
--set theIosURL to my getIosURL(thePath)
-- Update path: in frontmatter
--set maFrontmatter to regex change maFrontmatter search pattern "^macurl: (.*)$" replace template "macurl: " & theMacURL
--set maFrontmatter to regex change maFrontmatter search pattern "^iosurl: (.*)$" replace template "iosurl: " & theIosURL
set maFrontmatter to regex change maFrontmatter search pattern "^path:.*$" replace template "path: " & shortPath
-- Update path: in info
--set maInfo to regex change maInfo search pattern "\\[Mac\\]\\(.*?\\)" replace template "[Mac](" & theMacURL & ")"
--set maInfo to regex change maInfo search pattern "\\[iOS\\]\\(.*?\\)" replace template "[iOS](" & theIosURL & ")"
set maSections to my updateSections(maSections, maFrontmatter, maInfo, missing value)
if returnList is true then
return maSections
else
return joinSections(maSections)
end if
end updatePath
on updateTags(maInput, theTags, returnList, stripTagsFromContent)
if class of maInput is list then
set maSections to maInput
else
set maSections to my getSections(maInput)
end if
set maFrontmatter to my getFrontmatterFromList(maSections)
set maInfo to my getInfoFromList(maSections)
set maContent to my getContentFromList(maSections)
set theTags to DT's uniqueList(theTags)
--set theTags to DT's simpleSort(theTags)
set theTags to lowercase from theTags
set theTagsString to join strings theTags using delimiter ","
if (regex search once maFrontmatter search pattern "^tags:\\s*\\[.*?\\]\\s*$") is not equal to "" then
set maFrontmatter to regex change maFrontmatter search pattern "^tags:\\s*\\[.*?\\]\\s*$" replace template "tags: [" & theTagsString & "]"
end if
if stripTagsFromContent is not false then
set maContent to my stripTagsFromContent(maContent)
end if
set maSections to my updateSections(maSections, maFrontmatter, missing value, maContent)
if returnList is true then
return maSections
else
return joinSections(maSections)
end if
end updateTags
on stripTagsFromContent(maContent)
return regex change maContent search pattern "\\s*#[a-zA-Z-0-9_-]*" replace template ""
end stripTagsFromContent
on getObsidianURL(theTitle)
return "obsidian://open?vault=" & obsidianVault & "&file=" & DT's urlencode(theTitle) & ".md"
end getObsidianURL
on getMacURL(thePath)
tell application "System Events" to set theFileURL to URL of (POSIX file thePath as alias)
return theFileURL
end getMacURL
on getIosURL(thePath)
set macURL to my getMacURL(thePath)
set folderPart to regex search once macURL search pattern "^.*/com~apple~CloudDocs/(.*)$" capture groups 1
return "shareddocuments:///private/var/mobile/Library/Mobile%20Documents/com~apple~CloudDocs/" & folderPart
end getIosURL
on shortPath(thePath)
set folderPart to regex search once thePath search pattern "^.*/com~apple~CloudDocs/(.*)$" capture groups 1
return folderPart
end shortPath