Additional resources - Automatically capture and annotate items: Markdown Annotation Helper

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
1 Like