Script: Convert WikiLinks to Markdown links

This script converts WikiLinks to Markdown links.

WikiLinks in any form

  • Hello World

  • Hello World.md

  • [[Hello World]]

  • [[Hello World.md]]

are converted to item links, depending on property useSuffix

  • [Hello World.md](x-devonthink-item://1EC569F5-5086-49C0-8D5A-F7CB4523F76D)

  • [Hello World](x-devonthink-item://1EC569F5-5086-49C0-8D5A-F7CB4523F76D)

Yes, you can access your wiki in DEVONthink To Go

If you don’t want to convert your original records use duplicates, sync, :footprints:

Result with useSuffix set to true

Result with useSuffix set to false

:warning: Note:

Although I assume that the way the script uses regex there’s no chance of replacing wrong strings I might have missed something.

Before usage with real data:

  • :face_with_monocle: Test with duplicates in a new database.

After testing with duplicates:

  • :face_with_raised_eyebrow: If you first want to see the result of a conversion

    • duplicate the record you want to convert,

    • run the script on the duplicate,

    • verify the result,

    • paste the result into the original record.

-- Convert WikiLinks to Markdown links

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

property useSuffix : true -- include suffix in markdown link name

tell application id "DNtp"
	try
		set theRecords to selected records
		if theRecords = {} then error "Nothing selected"
		set theEscapePattern to "\\,|\\!|\\?|\\.|\\(|\\)|\\[|\\]|\\{|\\}|\\*|\\\\|\\^|\\+|\\<|\\>|\\||\\$|\\="
		
		repeat with thisRecord in theRecords
			set theType to (type of thisRecord) as string
			if theType is in {"markdown", "«constant ****mkdn»"} then
				
				set the_record to {}
				
				set theWikiLinkRecords to outgoing Wiki references of thisRecord
				if theWikiLinkRecords ≠ {} then
					repeat with thisWikiRecord in theWikiLinkRecords
						set thisWikiRecord_RefURL to reference URL of thisWikiRecord
						set thisWikiRecord_Type to (type of thisWikiRecord) as string
						set thisWikiRecord_Kind to kind of thisWikiRecord
						set {thisWikiRecord_NameWithoutSuffix, thisWikiRecord_Suffix} to {item 1, item 2} of my recordName(name of thisWikiRecord, filename of thisWikiRecord)
						set end of the_record to {namewithoutsuffix_:thisWikiRecord_NameWithoutSuffix, suffix_:thisWikiRecord_Suffix, rurl_:thisWikiRecord_RefURL, neglookbehind_:{"\\t", "\\["}, neglookahead_:{("(\\." & thisWikiRecord_Suffix & ")?" & "\\]\\(") as string}, pattern_:"", type_:thisWikiRecord_Type, kind_:thisWikiRecord_Kind}
					end repeat
					
					set theLinkTexts to {}
					
					set theSource to source of thisRecord
					set theSource_Body to item 2 of my tid(theSource, ("</head>" & linefeed & "<body>") as string)
					set theURLs to get links of theSource_Body
					if theURLs ≠ {} then
						repeat with thisURL in theURLs
							set thisURL to thisURL as string
							set thisURL_escaped to my regexReplace(thisURL, theEscapePattern, "\\\\$0")
							set thisURL_LinkTexts to my regexFind(theSource_Body, (("(?<=\\<a href=\"" & thisURL_escaped & "\">)(.*?)(?=\\</a\\>)") as string))
							repeat with thisLinkText in thisURL_LinkTexts
								set thisLinkText to thisLinkText as string
								if thisLinkText starts with "[[" and thisLinkText ends with "]]" then set thisLinkText to (characters 3 thru -3 in thisLinkText) as string
								if theLinkTexts does not contain thisLinkText then set end of theLinkTexts to thisLinkText
								set thisLinkText_decoded to my decodeHTML(thisLinkText)
								if thisLinkText_decoded ≠ thisLinkText then set end of theLinkTexts to thisLinkText_decoded
							end repeat
						end repeat
					end if
					
					repeat with this_record in the_record
						set thisWikiRecord_NameWithoutSuffix to namewithoutsuffix_ of this_record
						set thisWikiRecord_NameWithoutSuffix_Length to length of thisWikiRecord_NameWithoutSuffix
						set thisSuffix to ("." & (suffix_ of this_record)) as string
						considering case
							repeat with thisLinkText in theLinkTexts
								set thisLinkText to thisLinkText as string
								if thisWikiRecord_NameWithoutSuffix is in thisLinkText then
									if thisWikiRecord_NameWithoutSuffix ≠ thisLinkText then
										set thisSubStringOffsets to my getSubStringOffsets(thisWikiRecord_NameWithoutSuffix, thisLinkText)
										set thisLinkText_Length to length of thisLinkText
										repeat with thisOffset in thisSubStringOffsets
											if thisOffset > 1 then
												set thisNegLookbehind to characters 1 thru (thisOffset - 1) in thisLinkText as string
												set thisNegLookbehind_escaped to ("(\\[)?" & my regexReplace(thisNegLookbehind, theEscapePattern, "\\\\$0")) as string
												if neglookbehind_ of this_record does not contain thisNegLookbehind_escaped then
													set end of neglookbehind_ of this_record to thisNegLookbehind_escaped
												end if
											end if
											if ((thisOffset + thisWikiRecord_NameWithoutSuffix_Length)) < thisLinkText_Length then
												set thisNegLookahead to characters (thisOffset + thisWikiRecord_NameWithoutSuffix_Length) thru -1 in thisLinkText as string
												if thisNegLookahead ≠ thisSuffix then
													set thisNegLookahead_escaped to (my regexReplace(thisNegLookahead, theEscapePattern, "\\\\$0") & "(\\]\\()?") as string
													if neglookahead_ of this_record does not contain thisNegLookahead_escaped then
														set end of neglookahead_ of this_record to thisNegLookahead_escaped
													end if
												end if
											end if
										end repeat
									end if
								end if
							end repeat
						end considering
					end repeat
					
					set newText to plain text of thisRecord
					
					repeat with this_record in the_record
						set thisWikiRecord_NameWithoutSuffix to namewithoutsuffix_ of this_record
						set thisWikiRecord_NameWithoutSuffix_escaped to my regexReplace(thisWikiRecord_NameWithoutSuffix, theEscapePattern, "\\\\$0")
						set thisWikiRecord_RefURL to rurl_ of this_record
						set thisWikiRecord_Suffix to suffix_ of this_record
						if useSuffix = true then
							set thisWikiRecord_Type to type_ of this_record
							set thisWikiRecord_Kind to kind_ of this_record
							if thisWikiRecord_Type is in {"group", "«constant ****DTgr»", "smart group", "«constant ****DTsg»"} or thisWikiRecord_Kind = "Tag" then
								set thisMarkdownLink to ("[" & thisWikiRecord_NameWithoutSuffix_escaped & "](" & thisWikiRecord_RefURL & ")") as string
							else
								set thisMarkdownLink to ("[" & thisWikiRecord_NameWithoutSuffix_escaped & "." & thisWikiRecord_Suffix & "](" & thisWikiRecord_RefURL & ")") as string
							end if
						else
							set thisMarkdownLink to ("[" & thisWikiRecord_NameWithoutSuffix_escaped & "](" & thisWikiRecord_RefURL & ")") as string
						end if
						set thisPattern to ("(?<!" & my tid((neglookbehind_ of this_record), "|") & ")" & "(\\[\\[)?" & thisWikiRecord_NameWithoutSuffix_escaped) as string
						set thisPattern to (thisPattern & "(?!" & my tid((neglookahead_ of this_record), "|") & ")") as string
						set thisPattern to (thisPattern & "(\\." & thisWikiRecord_Suffix & ")?" & "(\\]\\])?" & "(\\." & thisWikiRecord_Suffix & ")?") as string
						set pattern_ of this_record to thisPattern
						set newText to my regexReplace(newText, thisPattern, thisMarkdownLink)
					end repeat
					
					set plain text of thisRecord to newText
				end if
			end if
		end repeat
		
	on error error_message number error_number
		if the error_number is not -128 then display alert "DEVONthink" message error_message as warning
		return
	end try
end tell

on recordName(theName, theFilename)
	set theSuffix to my getSuffix(theFilename)
	if theName ends with theSuffix and theName ≠ theSuffix then set theName to characters 1 thru -((length of theSuffix) + 2) in theName as string
	return {theName, theSuffix}
end recordName

on getSuffix(thePath)
	set revPath to reverse of characters in thePath as string
	set theSuffix to reverse of characters 1 thru ((offset of "." in revPath) - 1) in revPath as string
end getSuffix

on tid(theInput, theDelimiter)
	set d to AppleScript's text item delimiters
	set AppleScript's text item delimiters to theDelimiter
	if class of theInput = text then
		set theOutput to text items of theInput
	else if class of theInput = list then
		set theOutput to theInput as text
	end if
	set AppleScript's text item delimiters to d
	return theOutput
end tid

on regexFind(theText, thePattern)
	try
		set aString to current application's NSString's stringWithString:theText
		set {theExpr, theError} to current application's NSRegularExpression's regularExpressionWithPattern:(thePattern) options:0 |error|:(reference)
		set theMatches to theExpr's matchesInString:aString options:0 range:{0, aString's |length|()}
		set theResults to {}
		repeat with aMatch in theMatches
			set theRange to (aMatch's rangeAtIndex:0)
			set theString to (aString's substringWithRange:theRange) as text
			if theString is not in theResults then
				set end of theResults to theString
			end if
		end repeat
		return theResults
	on error error_message number error_number
		activate
		display alert "Error: Handler \"regexFind\"" message error_message as warning
		error number -128
	end try
end regexFind

on regexReplace(theText, thePattern, theRepacement)
	try
		set theString to current application's NSString's stringWithString:theText
		set newString to theString's stringByReplacingOccurrencesOfString:(thePattern) withString:(theRepacement) options:(current application's NSRegularExpressionSearch) range:{location:0, |length|:length of theText}
		set newText to newString as string
	on error error_message number error_number
		activate
		display alert "Error: Handler \"regexReplace\"" message error_message as warning
		error number -128
	end try
end regexReplace

on decodeHTML(theText)
	try
		-- https://macscripter.net/viewtopic.php?pid=190404#p190404
		set ca to current application
		set str to ca's class "NSMutableString"'s stringWithString:(theText)
		set HTMLData to str's dataUsingEncoding:(ca's NSUTF8StringEncoding)
		set attributedStr to ca's class "NSAttributedString"'s alloc()'s initWithHTML:(HTMLData) documentAttributes:(missing value)
		set decodedString to attributedStr's |string|()
		return decodedString as text
	on error error_message number error_number
		activate
		display alert "Error: Handler \"decodeHTML\"" message error_message as warning
		error number -128
	end try
end decodeHTML

on getSubStringOffsets(theSubstring, theText)
	try
		set theSubstringOffsets to {}
		set x to 1
		repeat (count (characters in theText)) times
			set thisOffset to (offset of theSubstring in (characters x thru -1 in theText) as string)
			if thisOffset > 0 then
				set end of theSubstringOffsets to (thisOffset + x - 1)
				set x to x + thisOffset
			else
				exit repeat
			end if
		end repeat
		return theSubstringOffsets
	on error error_message number error_number
		activate
		display alert "Error: Handler \"getSubStringOffsets\"" message error_message as warning
		error number -128
	end try
end getSubStringOffsets

5 Likes

Pete31 this is awesome thanks very much. I would love all links to behave like item links with all their advantages, but this is a useful script thank you for sharing it, I will definitely use this. I tried it and it works as described, thanks for sharing your work.

Steve

1 Like

Awesome Idea, thank you very much for that.

I also run in the Problem, and like the Idea of converting the Links. Sadly, the Script Runs in an Error for me:

unable to set argument 5 - the AppleScript value <NSAppleEventDescriptor: { 'DTlo':0, 'usrf':[ 'utxt'("length"), 56 ] }> could not be coerced to type {_NSRange=QQ}.

The Input is quite simple:

# Lernen

- [[Mehr Wissen in den Kopf?]]
- [[4h Lernen]]
- [[Vokabeln Lernen]]

Without having really understood the script: did you try to omit the question mark in the first link? It is part of the escape pattern. @pete31 should know more about this, of course

Couldn’t reproduce this. Over here the script works with your input text.

Is this the whole error message? A capture of the error dialog could be helpful.

Wired, may I do something wrong? I copied the Script to the Menu Folder, select a markdown file and run it.

Maybe a Copy and Paste thing, are the code indents important? I copy it to a VIM with paste mode on.

Please copy it again and use Script Editor.app

Thank you very much, then it was my fault. Didn’t know that the script needed a converting.
Sorry for the question then, now it works like a charm.

1 Like

Fantastic, thanks a lot for sharing!

1 Like

Hi, really fantastic! However, I try to make it execute automatically in smart rules as

-- Convert WikiLinks to Markdown links

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

property useSuffix : true -- include suffix in markdown link name

on performSmartRule(theRecords)
	tell application id "DNtp"
		try
			set theRecords to selected records
			if theRecords = {} then error "Nothing selected"
			set theEscapePattern to "\\,|\\!|\\?|\\.|\\(|\\)|\\[|\\]|\\{|\\}|\\*|\\\\|\\^|\\+|\\<|\\>|\\||\\$|\\="
			
			repeat with thisRecord in theRecords
				set theType to (type of thisRecord) as string
				if theType is in {"markdown", "«constant ****mkdn»"} then
					
					set the_record to {}
					
					set theWikiLinkRecords to outgoing Wiki references of thisRecord
					if theWikiLinkRecords ≠ {} then
						repeat with thisWikiRecord in theWikiLinkRecords
							set thisWikiRecord_RefURL to reference URL of thisWikiRecord
							set thisWikiRecord_Type to (type of thisWikiRecord) as string
							set thisWikiRecord_Kind to kind of thisWikiRecord
							set {thisWikiRecord_NameWithoutSuffix, thisWikiRecord_Suffix} to {item 1, item 2} of my recordName(name of thisWikiRecord, filename of thisWikiRecord)
							set end of the_record to {namewithoutsuffix_:thisWikiRecord_NameWithoutSuffix, suffix_:thisWikiRecord_Suffix, rurl_:thisWikiRecord_RefURL, neglookbehind_:{"\\t", "\\["}, neglookahead_:{("(\\." & thisWikiRecord_Suffix & ")?" & "\\]\\(") as string}, pattern_:"", type_:thisWikiRecord_Type, kind_:thisWikiRecord_Kind}
						end repeat
						
						set theLinkTexts to {}
						
						set theSource to source of thisRecord
						set theSource_Body to item 2 of my tid(theSource, ("</head>" & linefeed & "<body>") as string)
						set theURLs to get links of theSource_Body
						if theURLs ≠ {} then
							repeat with thisURL in theURLs
								set thisURL to thisURL as string
								set thisURL_escaped to my regexReplace(thisURL, theEscapePattern, "\\\\$0")
								set thisURL_LinkTexts to my regexFind(theSource_Body, (("(?<=\\<a href=\"" & thisURL_escaped & "\">)(.*?)(?=\\</a\\>)") as string))
								repeat with thisLinkText in thisURL_LinkTexts
									set thisLinkText to thisLinkText as string
									if thisLinkText starts with "[[" and thisLinkText ends with "]]" then set thisLinkText to (characters 3 thru -3 in thisLinkText) as string
									if theLinkTexts does not contain thisLinkText then set end of theLinkTexts to thisLinkText
									set thisLinkText_decoded to my decodeHTML(thisLinkText)
									if thisLinkText_decoded ≠ thisLinkText then set end of theLinkTexts to thisLinkText_decoded
								end repeat
							end repeat
						end if
						
						repeat with this_record in the_record
							set thisWikiRecord_NameWithoutSuffix to namewithoutsuffix_ of this_record
							set thisWikiRecord_NameWithoutSuffix_Length to length of thisWikiRecord_NameWithoutSuffix
							set thisSuffix to ("." & (suffix_ of this_record)) as string
							considering case
								repeat with thisLinkText in theLinkTexts
									set thisLinkText to thisLinkText as string
									if thisWikiRecord_NameWithoutSuffix is in thisLinkText then
										if thisWikiRecord_NameWithoutSuffix ≠ thisLinkText then
											set thisSubStringOffsets to my getSubStringOffsets(thisWikiRecord_NameWithoutSuffix, thisLinkText)
											set thisLinkText_Length to length of thisLinkText
											repeat with thisOffset in thisSubStringOffsets
												if thisOffset > 1 then
													set thisNegLookbehind to characters 1 thru (thisOffset - 1) in thisLinkText as string
													set thisNegLookbehind_escaped to ("(\\[)?" & my regexReplace(thisNegLookbehind, theEscapePattern, "\\\\$0")) as string
													if neglookbehind_ of this_record does not contain thisNegLookbehind_escaped then
														set end of neglookbehind_ of this_record to thisNegLookbehind_escaped
													end if
												end if
												if ((thisOffset + thisWikiRecord_NameWithoutSuffix_Length)) < thisLinkText_Length then
													set thisNegLookahead to characters (thisOffset + thisWikiRecord_NameWithoutSuffix_Length) thru -1 in thisLinkText as string
													if thisNegLookahead ≠ thisSuffix then
														set thisNegLookahead_escaped to (my regexReplace(thisNegLookahead, theEscapePattern, "\\\\$0") & "(\\]\\()?") as string
														if neglookahead_ of this_record does not contain thisNegLookahead_escaped then
															set end of neglookahead_ of this_record to thisNegLookahead_escaped
														end if
													end if
												end if
											end repeat
										end if
									end if
								end repeat
							end considering
						end repeat
						
						set newText to plain text of thisRecord
						
						repeat with this_record in the_record
							set thisWikiRecord_NameWithoutSuffix to namewithoutsuffix_ of this_record
							set thisWikiRecord_NameWithoutSuffix_escaped to my regexReplace(thisWikiRecord_NameWithoutSuffix, theEscapePattern, "\\\\$0")
							set thisWikiRecord_RefURL to rurl_ of this_record
							set thisWikiRecord_Suffix to suffix_ of this_record
							if useSuffix = true then
								set thisWikiRecord_Type to type_ of this_record
								set thisWikiRecord_Kind to kind_ of this_record
								if thisWikiRecord_Type is in {"group", "«constant ****DTgr»", "smart group", "«constant ****DTsg»"} or thisWikiRecord_Kind = "Tag" then
									set thisMarkdownLink to ("[" & thisWikiRecord_NameWithoutSuffix_escaped & "](" & thisWikiRecord_RefURL & ")") as string
								else
									set thisMarkdownLink to ("[" & thisWikiRecord_NameWithoutSuffix_escaped & "." & thisWikiRecord_Suffix & "](" & thisWikiRecord_RefURL & ")") as string
								end if
							else
								set thisMarkdownLink to ("[" & thisWikiRecord_NameWithoutSuffix_escaped & "](" & thisWikiRecord_RefURL & ")") as string
							end if
							set thisPattern to ("(?<!" & my tid((neglookbehind_ of this_record), "|") & ")" & "(\\[\\[)?" & thisWikiRecord_NameWithoutSuffix_escaped) as string
							set thisPattern to (thisPattern & "(?!" & my tid((neglookahead_ of this_record), "|") & ")") as string
							set thisPattern to (thisPattern & "(\\." & thisWikiRecord_Suffix & ")?" & "(\\]\\])?" & "(\\." & thisWikiRecord_Suffix & ")?") as string
							set pattern_ of this_record to thisPattern
							set newText to my regexReplace(newText, thisPattern, thisMarkdownLink)
						end repeat
						
						set plain text of thisRecord to newText
					end if
				end if
			end repeat
			
		on error error_message number error_number
			if the error_number is not -128 then display alert "DEVONthink" message error_message as warning
			return
		end try
	end tell
end performSmartRule

on recordName(theName, theFilename)
	set theSuffix to my getSuffix(theFilename)
	if theName ends with theSuffix and theName ≠ theSuffix then set theName to characters 1 thru -((length of theSuffix) + 2) in theName as string
	return {theName, theSuffix}
end recordName

on getSuffix(thePath)
	set revPath to reverse of characters in thePath as string
	set theSuffix to reverse of characters 1 thru ((offset of "." in revPath) - 1) in revPath as string
end getSuffix

on tid(theInput, theDelimiter)
	set d to AppleScript's text item delimiters
	set AppleScript's text item delimiters to theDelimiter
	if class of theInput = text then
		set theOutput to text items of theInput
	else if class of theInput = list then
		set theOutput to theInput as text
	end if
	set AppleScript's text item delimiters to d
	return theOutput
end tid

on regexFind(theText, thePattern)
	try
		set aString to current application's NSString's stringWithString:theText
		set {theExpr, theError} to current application's NSRegularExpression's regularExpressionWithPattern:(thePattern) options:0 |error|:(reference)
		set theMatches to theExpr's matchesInString:aString options:0 range:{0, aString's |length|()}
		set theResults to {}
		repeat with aMatch in theMatches
			set theRange to (aMatch's rangeAtIndex:0)
			set theString to (aString's substringWithRange:theRange) as text
			if theString is not in theResults then
				set end of theResults to theString
			end if
		end repeat
		return theResults
	on error error_message number error_number
		activate
		display alert "Error: Handler \"regexFind\"" message error_message as warning
		error number -128
	end try
end regexFind

on regexReplace(theText, thePattern, theRepacement)
	try
		set theString to current application's NSString's stringWithString:theText
		set newString to theString's stringByReplacingOccurrencesOfString:(thePattern) withString:(theRepacement) options:(current application's NSRegularExpressionSearch) range:{location:0, |length|:length of theText}
		set newText to newString as string
	on error error_message number error_number
		activate
		display alert "Error: Handler \"regexReplace\"" message error_message as warning
		error number -128
	end try
end regexReplace

on decodeHTML(theText)
	try
		-- https://macscripter.net/viewtopic.php?pid=190404#p190404
		set ca to current application
		set str to ca's class "NSMutableString"'s stringWithString:(theText)
		set HTMLData to str's dataUsingEncoding:(ca's NSUTF8StringEncoding)
		set attributedStr to ca's class "NSAttributedString"'s alloc()'s initWithHTML:(HTMLData) documentAttributes:(missing value)
		set decodedString to attributedStr's |string|()
		return decodedString as text
	on error error_message number error_number
		activate
		display alert "Error: Handler \"decodeHTML\"" message error_message as warning
		error number -128
	end try
end decodeHTML

on getSubStringOffsets(theSubstring, theText)
	try
		set theSubstringOffsets to {}
		set x to 1
		repeat (count (characters in theText)) times
			set thisOffset to (offset of theSubstring in (characters x thru -1 in theText) as string)
			if thisOffset > 0 then
				set end of theSubstringOffsets to (thisOffset + x - 1)
				set x to x + thisOffset
			else
				exit repeat
			end if
		end repeat
		return theSubstringOffsets
	on error error_message number error_number
		activate
		display alert "Error: Handler \"getSubStringOffsets\"" message error_message as warning
		error number -128
	end try
end getSubStringOffsets

by just put the tell application id “DNtp” part into the performSmartRule(theRecords) function. However, there’s a error show as
CleanShot 2022-09-09 at 21.51.12.
Is there something wrong with my way? Thanks a lot!

See this post.

1 Like

There’s no selected records in a smart rule script. The records are passed in as a parameter to the perform smart rule handler.
Just remove the line re-defining theRecords

Thanks a lot!

1 Like