Notes to markdown: rename file and replace text

I have this script that someone helped me write that creates .md files from my Notes app:

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

tell application "Notes"
	tell folder "Notes" of account "iCloud"
		set theCount to (count notes)
		set invalid to ((theCount = 0) or ((theCount = 1) and (note 1's body = "")))
	end tell
end tell

if (invalid) then error number -128 -- "User canceled" error.

set rootFolder to "Macintosh HD:Users:dannywyatt:Library:Mobile Documents:iCloud~md~obsidian:Documents:Tiago:Inbox"

tell application "Finder"
	set exportFolder to make new folder at folder rootFolder with properties {name:"Converted notes from NOTES app"}
	set exportFolder to exportFolder as text
end tell
tell application "Notes" to activate

-- Simple text replacing

on replaceText(find, replace, subject)
	set prevTIDs to text item delimiters of AppleScript
	set text item delimiters of AppleScript to find
	set subject to text items of subject
	
	set text item delimiters of AppleScript to replace
	set subject to "" & subject
	set text item delimiters of AppleScript to prevTIDs
	
	return subject
end replaceText

-- Get an .md file to save the note in.  We have to escape
-- the colons or AppleScript gets upset.
on noteNameToFilePath(noteName)
	global exportFolder
	set strLength to the length of noteName
	
	if strLength > 250 then
		set noteName to text 1 thru 250 of noteName
	end if
	
	set fileName to (exportFolder & replaceText(":", "_", noteName) & ".md")
	return fileName
end noteNameToFilePath

tell application "Notes"
	
	repeat with theNote in notes of folder "Notes"
		
		set oldDelimiters to AppleScript's text item delimiters
		set AppleScript's text item delimiters to "/"
		set AppleScript's text item delimiters to oldDelimiters
		
		set fileName to name of theNote as string
		set filepath to noteNameToFilePath(fileName) of me
		set noteFile to open for access filepath with write permission
		set theText to body of theNote as string
		write theText to noteFile as «class utf8»
		
		close access noteFile
		
	end repeat
	
end tell

I use Keyboard Maestro and I am able to make some changes to the file name as well as replacing some stuff in the file itself:
1 - Remove ... from the file name
2 - Replace the string &quot with " (file’s content)
3 - Using RegEx, remove <.{1,4}> (file’s content). This is HTML “garbage” that I don’t want to keep, such as <div>, etc.

So yes, I can achieve this with some extra Keyboard Maestro actions, but if I can achieve this just with the script, even better.
I’m still learning AS, so this is a bit complex for me.

When it comes to the ... issue I assume it’s something similar to this?

set fileName to (exportFolder & replaceText("...", "", noteName) &  ".md")

But when I tried this, it didn't work, so I'm obviously doing something wrong:
set fileName to (exportFolder & replaceText(":", "_", noteName) & replaceText("...", "", noteName)  ".md")

alltiagocom. The handler included below will accomplish your first and third requests. I don’t understand your second request, although I suspect the handler can do this as well.

use framework "Foundation"
use scripting additions

set fileName to "Test...File.txt"
set cleanedFileName to getCleanedString(fileName, "\\.{3}")

set theString to "<div><h1>Travel Data</h1></div>
<div>  </div>
<div>Burnby Hall &amp Gardens, The Balk, Pocklington, York, YO42 2QF</div>
<div>57 minutes</div>"
set cleanedString to getCleanedString(theString, "<.{1,4}>")

on getCleanedString(theString, thePattern)
	set theString to current application's NSMutableString's stringWithString:theString
	(theString's replaceOccurrencesOfString:thePattern withString:"" options:1024 range:{0, theString's |length|()})
	return theString as text
end getCleanedString

BTW, the same task can be accomplished with basic AppleScript using text item delimiters and with the shell. I’m more comfortable using ASObjC for its speed and simplicity.

When the file’s content includes something like this: This is my "best" year ever, it becomes This is my &quotbest&quot year ever, so I want to replace all instances of &quot with "

I tried your script with Script Debugger and I can see the output, which indeed removes the HTML stuff, as well as the file name without ...

As I mentioned, I’m still learning AS so I can’t really understand how that would be added to my script? Is that possible? If so, where would I put that?
You also mention ASObjC, which I have no idea what it is… :confused:

If it’s too complicated to merge the two, I understand. No worries. I can still use Keyboard Maestro to make all of those changes.
I’m in the very beginning of learning AS and I’ve been learning a lot (at least it’s not 100% foreign to me), but still not on a level where I can fully play with it, comfortably to merge them.

alltiagocom. Sorry for not explaining more–it’s always a bit difficult to know if I’m explaining too much or not enough.

As far as my script’s operation goes, you copy the handler to the end of your script and call it wherever you need to remove unwanted characters from a string. The parameters when you call the handler are the source string (which is simple text) and the regex pattern that matches the characters that will be deleted. For example:

use framework "Foundation"
use scripting additions

-- first part of the script

-- cleaned text is required here
set fileName to "Test...File.txt"
set cleanedFileName to getCleanedString(fileName, "\\.{3}") --> "TestFile.txt"

-- more of the script

-- cleaned text is required here
set theString to "This is my &quotbest&quot year ever"
set cleanedString to getCleanedString(theString, "&quot") --> "This is my best year ever"

-- more of the script

-- put this handler at end of script
on getCleanedString(theString, thePattern) -- thePattern is the regex pattern
	set theString to current application's NSMutableString's stringWithString:theString
	(theString's replaceOccurrencesOfString:thePattern withString:"" options:1024 range:{0, theString's |length|()})
	return theString as text
end getCleanedString

The only thing to note is that regex metacharacters have to be escaped with two instead of just one backslash. If you aren’t familiar with regex, just ask and a forum member will supply the required pattern.

ASObjC refers to the AppleScript implementation of Objective-C. It’s not something you need to learn, but it can be useful in situations like this, where you don’t really need to understand it to use it.

Since your script works with Keyboard Maestro, I would just stick with that for now. You can then play around with my suggestion when you have the time and inclination.

1 Like

Thanks for clarifying!

Yes I will use Keyboard Maestro for now and will test your script along with mine to see how to merge them.

Thanks again for sharing this :raised_hands:

Hi.

Unless I’ve misunderstood what you’re trying to do, the simplest way to get a note’s text without the HTML is to use its plaintext instead of its body.

1 Like

Hey! It seems to work :slight_smile: You mean this line, right?
set theText to plaintext of theNote as string

I changed it and I got the files without all the HMTL “garbage”.

Would you also know how to fix the file’s name, by removing the ...?
This is the section:

on noteNameToFilePath(noteName)
	global exportFolder
	set strLength to the length of noteName
	
	if strLength > 250 then
		set noteName to text 1 thru 250 of noteName
	end if
	
	set fileName to (exportFolder & replaceText(":", "_", noteName) & ".md")
	return fileName
end noteNameToFilePath

You can see that it replaces : with _, but I can’t figure out how to completely remove the ...

Yes. That’s right. You shouldn’t need the as string, but it doesn’t hurt.

The code you tried in your first post above nominally appends the result of deleting group(s) of three dots in the original note name to the result of replacing colons(s) with underscore(s) in that same original note name. So you get one after the other in the file name. You need instead to perform one of the actions on the original name and then perform the other action on the result of that.

Another thing that occurs to me is that your three dots may actually be the ellipsis character (character id 8230), so ideally you need to cover both possibilites.

set editedNoteName to replaceText(":", "_", noteName)
set editedNoteName to replaceText({"...", "…"}, "", editedNoteName)
set fileName to (exportFolder & editedNoteName & ".md")
1 Like

Nigel’s plaintext suggestion greatly simplifies matters. I tested the following script on my Sonoma computer without issue. It replaces colons with underscores and deletes three periods in file names. I set the maximum length of file names to 20 characters, although this can be changed to whatever is desired:

use framework "Foundation"
use scripting additions

set theFolder to "/Users/Robert/Desktop/Note Copies/" -- user set to desired value

tell application "Notes"
	activate
	set theNotes to notes of folder "Notes"
	set theCount to count theNotes
	if theCount is 0 then display dialog "No notes were found" buttons {"OK"} cancel button 1 default button 1
end tell

set fileManager to current application's NSFileManager's defaultManager()
fileManager's createDirectoryAtPath:theFolder withIntermediateDirectories:false attributes:(missing value) |error|:(missing value)

tell application "Notes"
	repeat with aNote in theNotes
		set noteText to plaintext of aNote
		set noteName to name of aNote
		set noteNameCount to (count noteName)
		if noteNameCount is greater than 20 then set noteName to characters 1 thru 20 of noteName as text
		set noteName to searchAndReplace(noteName, ":", "_") of me -- replace colon with underscore
		set noteName to searchAndReplace(noteName, "...", "") of me -- delete 3 periods
		writeFile(noteText, theFolder, noteName) of me -- existing files are overwritten
	end repeat
end tell

on searchAndReplace(theString, searchString, replaceString)
	set theString to (current application's NSString's stringWithString:theString)
	return (theString's stringByReplacingOccurrencesOfString:searchString withString:replaceString)
end searchAndReplace

on writeFile(theString, theFolder, fileName) -- overwrites existing files
	set theString to current application's NSString's stringWithString:theString
	set theFolder to current application's |NSURL|'s fileURLWithPath:theFolder
	set theFile to (theFolder's URLByAppendingPathComponent:fileName)'s URLByAppendingPathExtension:"md"
	theString's writeToURL:theFile atomically:true encoding:(current application's NSUTF8StringEncoding) |error|:(missing value)
end writeFile
1 Like

I’ve included a shortcut solution below. As written, it saves the notes in TXT format but is easily modified to save them as RTF or HTML. The folder that will contain the saved notes is set in the shortcut’s first action.

Save Notes.shortcut (23.4 KB)

1 Like

My macOS is still Catalina and I don’t have Shortcuts. I also use Keyboard Maestro, which seems to be more advanced overall, even though Shortcuts is good when it comes to having things that work on both Mac and iPhone/iPad (at least that’s what it seems from what I read online).

The format of the files needs to be .md, because I use Obsidian as my app for notes.

Thanks for sharing this!

Today I was playing around with ChatGPT (haven’t tried it before) and I actually asked about this script and it gave me a few extra tips, such as adding the current date and time to the end of the folder, which I was adding with Keyboard Maestro.
It seems that ChatGPT is gonna be a good tool for me to learn a few things here and there as well, along with this forum, as long as I am able to create a good prompt, which seems to be the case so far.

Here’s the final script I got and now I don’t have to rely on anything but the AppleScript action in Keyboard Maestro (by the way, the “ellipsis” was actually the Horizontal Ellipsis, which seems to be what you just described as well):

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

tell application "Notes"
	tell folder "Notes" of account "iCloud"
		set theCount to (count notes)
		set invalid to ((theCount = 0) or ((theCount = 1) and (note 1's body = "")))
	end tell
end tell

if (invalid) then error number -128 -- "User canceled" error.

-- Get current date and time
set currentDate to current date
set dateFormatter to current application's NSDateFormatter's new()
-- Set your desired date format here
dateFormatter's setDateFormat:"EEEE, MMM d, yyyy 'at' h'h' mm'm' ss's' a"
set formattedDate to (dateFormatter's stringFromDate:currentDate) as text

set rootFolder to "Macintosh HD:Users:dannywyatt:Library:Mobile Documents:iCloud~md~obsidian:Documents:Tiago:Inbox"

-- Append current date and time to the folder name
set folderName to "Converted notes from NOTES app " & formattedDate

tell application "Finder"
	set exportFolder to make new folder at folder rootFolder with properties {name:folderName}
	set exportFolder to exportFolder as text
end tell

tell application "Notes" to activate

-- Simple text replacing

on replaceText(find, replace, subject)
	set prevTIDs to text item delimiters of AppleScript
	set text item delimiters of AppleScript to find
	set subject to text items of subject
	
	set text item delimiters of AppleScript to replace
	set subject to "" & subject
	set text item delimiters of AppleScript to prevTIDs
	
	return subject
end replaceText

-- Get an .md file to save the note in.  We have to escape
-- the colons or AppleScript gets upset.
on noteNameToFilePath(noteName)
	global exportFolder
	set strLength to the length of noteName
	
	if strLength > 250 then
		set noteName to text 1 thru 250 of noteName
	end if
	
	-- Construct file path
	set fileName to (exportFolder & replaceText(":", "_", noteName) & ".md")
	
	-- Remove Horizontal Ellipsis (U+2026) from filename
	set fileName to my replaceText("…", "", fileName)
	
	return fileName
end noteNameToFilePath

tell application "Notes"
	repeat with theNote in notes of folder "Notes"
		
		set oldDelimiters to AppleScript's text item delimiters
		set AppleScript's text item delimiters to "/"
		set AppleScript's text item delimiters to oldDelimiters
		
		set fileName to name of theNote as string
		set filepath to noteNameToFilePath(fileName) of me
		set noteFile to open for access filepath with write permission
		set theText to plaintext of theNote as string
		write theText to noteFile as «class utf8»
		
		close access noteFile
		
	end repeat
	
end tell

When you use this line:

set editedNoteName to replaceText({"...", "…"}, "", editedNoteName)

The {"...", "…"}, "" section works kind of like an OR operator, right? The content inside the curly brackets works as a single element that gets replaced, but it’s “this” or “that”, correct? Hope I’m making sense…

The solutions provided in threads like this will be consulted by users for years to come. Scripts or shortcuts that do not meet your needs will likely be of use to others in the future. That has always been my thinking when posting multiple solutions to a request.

1 Like

Agree 100%. I was just stating that for my current case, Shortcuts won’t be a good alternative, not only because of my OS, but also because I use Keyboard Maestro, and because I was looking for a simplified version using only AS.
These solutions also help me understand AS a bit more as well.

peavine answered this, but I see he’s now deleted his post for some reason. Maybe he’s reformulating it. But yes. Without going into detail myself about how AppleScript’s text item delimiters work, by passing a list of strings to be replaced to the script’s replaceText() handler, you’re specifying that you want every instance in the main text of any of these substrings to be replaced with the given replacement string. In the example above, you get back a copy of the main text which doesn’t contain any of the substrings.

If you’re interested and haven’t already done it, there are a few ways your script might be tidied up:

  1. The two handlers (the subroutine blocks beginning with ‘on’) should ideally be placed right at the end of the script or just below the use statements at the top. This is because the rest of the code is notionally in something called an “implicit run handler” and it’s considered good form to keep this as a block itself, not interspersed with code for other handlers. If the run handler were explicit, the script wouldn’t compile unless all the handlers were in their own space.
  2. Your ‘currentDate’ variable isn’t used, so the line setting it is redundant.
  3. Similarly, in the tell block currently at the bottom, the three lines involving AppleScript’s text item delimiters simply change the delimiter value and immediately change it back again, so they’re superfluous too.
  4. With regard to the statements in that block which write the text to the file, it’s a good idea to protect them with a try statement to ensure that, should an error occur while the file’s open for access, the script won’t just crash and leave the file open. I personally would put the writing to file in its own handler.

I’ve implemented these suggestions below, but haven’t run the script to test it. Of course it’s entirely up to you whether or not you wish to adopt them. :smile:

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

(* Implicit run handler starts here *)

tell application "Notes"
	set notesFolder to folder "Notes" of account "iCloud"
	tell notesFolder
		set theCount to (count notes)
		set invalid to ((theCount = 0) or ((theCount = 1) and (note 1's body = "")))
	end tell
end tell

if (invalid) then error number -128 -- "User canceled" error.

-- Get current date and time
set dateFormatter to current application's NSDateFormatter's new()
-- Set your desired date format here
dateFormatter's setDateFormat:"EEEE, MMM d, yyyy 'at' h'h' mm'm' ss's' a"
set formattedDate to (dateFormatter's stringFromDate:currentDate) as text

set rootFolder to "Macintosh HD:Users:dannywyatt:Library:Mobile Documents:iCloud~md~obsidian:Documents:Tiago:Inbox"

-- Append current date and time to the folder name
set folderName to "Converted notes from NOTES app " & formattedDate

tell application "Finder"
	set exportFolder to make new folder at folder rootFolder with properties {name:folderName}
	set exportFolder to exportFolder as text
end tell

tell application "Notes"
	activate
	repeat with theNote in notes of notesFolder
		
		set fileName to name of theNote
		set filePath to my noteNameToFilePath(fileName)
		set theText to plaintext of theNote
		my writeToFile(theText, filePath, «class utf8»)
		
	end repeat
end tell


(* Other handlers *)

-- Simple text replacing
on replaceText(find, replace, subject)
	set prevTIDs to text item delimiters of AppleScript
	set text item delimiters of AppleScript to find
	set subject to text items of subject
	
	set text item delimiters of AppleScript to replace
	set subject to "" & subject
	set text item delimiters of AppleScript to prevTIDs
	
	return subject
end replaceText

-- Get an .md file to save the note in.  We have to escape
-- the colons or AppleScript gets upset.
on noteNameToFilePath(noteName)
	global exportFolder
	set strLength to the length of noteName
	
	if strLength > 250 then
		set noteName to text 1 thru 250 of noteName
	end if
	
	-- Construct file path
	set fileName to (exportFolder & replaceText(":", "_", noteName) & ".md")
	
	-- Remove Horizontal Ellipsis (U+2026) from filename
	set fileName to my replaceText("…", "", fileName)
	
	return fileName
end noteNameToFilePath

on writeToFile(theData, filePath, saveFormat)
	set fileRef to (open for access file filePath with write permission)
	try
		set eof fileRef to 0 -- Delete existing content, if any.
		write theData to fileRef as saveFormat
		close access fileRef
	on error errMsg -- If anything goes wrong, close the file, display a message, and continue.
		close access fileRef
		tell application (path to frontmost application as text) to display dialog errMsg
	end try
end writeToFile

Nigel. After responding to alltiagocom’s question, I noticed that he had directed it to you. So, just as a matter of forum etiquette, I deleted my post.

1 Like

Thanks, peavine. Your fielding of the question wasn’t an issue for me (it saved me having to do it!) but I very much appreciate your thoughtfulness. :+1: