Created
September 5, 2025 08:54
-
-
Save lcherone/392ec0cff05bb2bc6d6f5ff65441cfb4 to your computer and use it in GitHub Desktop.
Export Apple Notes to Markdown
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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