Skip to content

Instantly share code, notes, and snippets.

@lcherone
Created September 5, 2025 08:54
Show Gist options
  • Save lcherone/392ec0cff05bb2bc6d6f5ff65441cfb4 to your computer and use it in GitHub Desktop.
Save lcherone/392ec0cff05bb2bc6d6f5ff65441cfb4 to your computer and use it in GitHub Desktop.
Export Apple Notes to Markdown
#!/bin/bash
# export Apple Notes to HTML files in current directory
# Usage: ./export_apple_notes.sh
set -e
EXPORT_DIR="$(pwd)"
echo "Exporting Apple Notes to: $EXPORT_DIR"
# Clean up existing exported files and folders
echo "Cleaning up existing exports..."
rm -rf "export" *.md *.html 2>/dev/null || true
# Create export directory
mkdir -p "export"
# Create the AppleScript to export notes
osascript <<'EOF'
tell application "Notes"
set exportDir to POSIX path of (path to desktop folder) & "Notes/"
-- Get the current directory from shell
set exportDir to do shell script "pwd"
set exportDir to exportDir & "/export/"
-- Create export directory if it doesn't exist
do shell script "mkdir -p " & quoted form of exportDir
set noteCount to 0
repeat with currentAccount in accounts
set accountName to name of currentAccount
log "Processing account: " & accountName
repeat with currentFolder in folders of currentAccount
set folderName to name of currentFolder
if folderName is not "Recently Deleted" then
log "Processing folder: " & folderName
repeat with currentNote in notes of currentFolder
set noteTitle to name of currentNote
set noteBody to body of currentNote
set noteDate to creation date of currentNote
set modDate to modification date of currentNote
-- Get additional note properties (with error handling)
set noteId to ""
set noteShared to ""
set noteLocked to ""
set noteAttachments to ""
try
set noteId to id of currentNote
end try
try
set noteShared to shared of currentNote
end try
try
set noteLocked to password protected of currentNote
end try
try
set attachmentCount to count of attachments of currentNote
if attachmentCount > 0 then
set noteAttachments to attachmentCount as string & " attachment(s)"
else
set noteAttachments to "No attachments"
end if
end try
-- Sanitize filename (no account/folder prefix)
set fileName to my sanitizeFileName(noteTitle)
set filePath to my getUniqueFilePath(exportDir, fileName, ".md")
-- Create Markdown content with comprehensive metadata
set markdownContent to "# " & noteTitle & "
**Created:** " & noteDate & "
**Modified:** " & modDate & "
**Account:** " & accountName & "
**Folder:** " & folderName & "
**Note ID:** " & noteId & "
**Shared:** " & noteShared & "
**Password Protected:** " & noteLocked & "
**Attachments:** " & noteAttachments & "
---
" & my convertHtmlToMarkdown(noteBody)
-- Write to file
try
do shell script "echo " & quoted form of markdownContent & " > " & quoted form of filePath
set noteCount to noteCount + 1
log "Exported: " & noteTitle
on error errMsg
log "Failed to export " & noteTitle & ": " & errMsg
end try
end repeat
end if
end repeat
end repeat
log "Export complete. " & noteCount & " notes exported."
return noteCount
end tell
-- Function to sanitize filename
on sanitizeFileName(fileName)
set fileName to my replaceText(fileName, "/", "-")
set fileName to my replaceText(fileName, ":", "-")
set fileName to my replaceText(fileName, "?", "")
set fileName to my replaceText(fileName, "*", "")
set fileName to my replaceText(fileName, "\"", "")
set fileName to my replaceText(fileName, "<", "")
set fileName to my replaceText(fileName, ">", "")
set fileName to my replaceText(fileName, "|", "")
return fileName
end sanitizeFileName
-- Function to get unique file path with incremental numbering
on getUniqueFilePath(dirPath, baseName, extension)
set testPath to dirPath & baseName & extension
-- Check if file exists using shell command
try
do shell script "test -f " & quoted form of testPath
-- File exists, need to find unique name
set counter to 1
repeat
set uniquePath to dirPath & baseName & " (" & counter & ")" & extension
try
do shell script "test -f " & quoted form of uniquePath
set counter to counter + 1
on error
-- File doesn't exist, use this path
return uniquePath
end try
end repeat
on error
-- File doesn't exist, use original path
return testPath
end try
end getUniqueFilePath
-- Function to convert basic HTML to Markdown
on convertHtmlToMarkdown(htmlText)
set markdownText to htmlText
-- Handle heading tags (convert to bold since they're often used for emphasis in Notes)
set markdownText to my replaceText(markdownText, "<h1>", "**")
set markdownText to my replaceText(markdownText, "</h1>", "**")
set markdownText to my replaceText(markdownText, "<h2>", "**")
set markdownText to my replaceText(markdownText, "</h2>", "**")
set markdownText to my replaceText(markdownText, "<h3>", "**")
set markdownText to my replaceText(markdownText, "</h3>", "**")
set markdownText to my replaceText(markdownText, "<h4>", "**")
set markdownText to my replaceText(markdownText, "</h4>", "**")
set markdownText to my replaceText(markdownText, "<h5>", "**")
set markdownText to my replaceText(markdownText, "</h5>", "**")
set markdownText to my replaceText(markdownText, "<h6>", "**")
set markdownText to my replaceText(markdownText, "</h6>", "**")
-- Remove Apple emoji font tags (keep the emoji content)
set markdownText to my replaceText(markdownText, "<font face=\".AppleColorEmojiUI\">", "")
set markdownText to my replaceText(markdownText, "</font>", "")
-- Handle lists
set markdownText to my replaceText(markdownText, "<ul>", return)
set markdownText to my replaceText(markdownText, "</ul>", return)
set markdownText to my replaceText(markdownText, "<ol>", return)
set markdownText to my replaceText(markdownText, "</ol>", return)
set markdownText to my replaceText(markdownText, "<li>", "- ")
set markdownText to my replaceText(markdownText, "</li>", return)
-- Convert common HTML tags to Markdown
set markdownText to my replaceText(markdownText, "<b>", "**")
set markdownText to my replaceText(markdownText, "</b>", "**")
set markdownText to my replaceText(markdownText, "<strong>", "**")
set markdownText to my replaceText(markdownText, "</strong>", "**")
set markdownText to my replaceText(markdownText, "<i>", "*")
set markdownText to my replaceText(markdownText, "</i>", "*")
set markdownText to my replaceText(markdownText, "<em>", "*")
set markdownText to my replaceText(markdownText, "</em>", "*")
set markdownText to my replaceText(markdownText, "<u>", "*")
set markdownText to my replaceText(markdownText, "</u>", "*")
set markdownText to my replaceText(markdownText, "<code>", "`")
set markdownText to my replaceText(markdownText, "</code>", "`")
-- Handle line breaks
set markdownText to my replaceText(markdownText, "<br>", " " & return)
set markdownText to my replaceText(markdownText, "<br/>", " " & return)
set markdownText to my replaceText(markdownText, "<br />", " " & return)
-- Handle paragraphs
set markdownText to my replaceText(markdownText, "<p>", "")
set markdownText to my replaceText(markdownText, "</p>", return & return)
-- Handle divs (common in Notes)
set markdownText to my replaceText(markdownText, "<div>", "")
set markdownText to my replaceText(markdownText, "</div>", return)
-- Clean up consecutive bold markers (like *****)
set markdownText to my replaceText(markdownText, "******", "**")
set markdownText to my replaceText(markdownText, "****", "**")
-- Clean up empty lines with just spaces
set markdownText to my replaceText(markdownText, " " & return & return, return & return)
set markdownText to my replaceText(markdownText, " " & return, return)
-- Clean up extra line breaks
set markdownText to my replaceText(markdownText, return & return & return, return & return)
return markdownText
end convertHtmlToMarkdown
-- Function to replace text
on replaceText(theText, searchString, replacementString)
set AppleScript's text item delimiters to searchString
set textItems to text items of theText
set AppleScript's text item delimiters to replacementString
set newText to textItems as string
set AppleScript's text item delimiters to ""
return newText
end replaceText
EOF
echo "Export completed!"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment