Skip to content

Instantly share code, notes, and snippets.

@dskecse
Created January 29, 2026 20:48
Show Gist options
  • Select an option

  • Save dskecse/57b9d6114ca968f076f6bc11adfa3a94 to your computer and use it in GitHub Desktop.

Select an option

Save dskecse/57b9d6114ca968f076f6bc11adfa3a94 to your computer and use it in GitHub Desktop.
YAML files to Apple Notes

Last working version summary

What it does

  • For each YAML file in a chosen folder, creates a new Apple Note.
  • Builds the note body as HTML:
    • Title in larger font
    • Body in <pre> to preserve formatting
  • Appends the syntax: line if present and not "None".
  • Escapes HTML and inserts U+2060 WORD JOINER around operators to prevent the Notes' Math Notes rendering from being applied, e.g. original text c = Cloud.find(3070) that looks like c = Cloud×find(3070) (note the weird × glyph).
  • Types hashtags at the bottom via UI scripting so Notes recognizes them as real tags.

Key behavior

  • Opens the newly created note in a new window (Window → Open Note in New Window) before typing tags, to ensure keystrokes land in the correct note and the hashtags get promoted automatically (become yellow and show up in the Tags section).
  • Closes the detached window after typing tags and waits for it to close before continuing.

Important notes

  • ⚠️ Requires Accessibility permission (System Settings → Privacy & Security → Accessibility) for UI scripting.
  • Uses a window-count wait loop (up to ~2 seconds).
  • Intermittent failures can occur due to UI focus/timing.
  • ⚠️ Non‑ASCII tag issue: tags are typed using System Events keystroke, which is not Unicode‑safe, so Cyrillic tags can become e.g. #aaaaaaaaaaa instead of #бухгалтерия (the issue still persists).

How to run

  • Open up a Script Editor app on a mac
  • Paste in the yaml_to_notes.applescript contents
  • Press the Run the Script button
  • Choose the directory containing YAML files that need to be imported to Apple Notes
    • The script expects each YAML file to contain title, body, syntax (None|Ruby|SQL|...) and tags (as an array) keys
  • Choose the account from the list
  • Choose the Apple Notes folder where new notes are going to be created (or create a new folder)
  • Wait for new notes to be created
-- Create Apple Notes from all YAML files in chosen folder
-- Uses YAML keys: title, body, syntax, tags
-- Preserves content exactly (HTML-escaped + <pre>)
-- Uses Finder to list files (no sorting)
-- 1) Choose directory
set chosenFolder to choose folder with prompt "Choose a directory containing files:"
set folderPOSIX to POSIX path of chosenFolder
-- Get file names in directory (non-folders only), using Finder
tell application "Finder"
set fileNames to name of every file of folder chosenFolder
end tell
if fileNames is {} then
display alert "No files found in the chosen directory."
return
end if
-- Process all files
set maxToProcess to count of fileNames
-- 2) Choose account (always prompt)
tell application "Notes"
set accountList to name of accounts
end tell
if accountList is {} then
display alert "No Notes accounts found."
return
end if
set chosenAccountName to choose from list accountList with title "Choose Account" with prompt "Select the Notes account:" default items {item 1 of accountList}
if chosenAccountName is false then return
set chosenAccountName to item 1 of chosenAccountName
-- 3) Choose folder (allow new)
tell application "Notes"
set folderNames to name of folders of account chosenAccountName
end tell
set folderChoices to {"➕ New Folder…"} & folderNames
set chosenFolderName to choose from list folderChoices with title "Choose Folder" with prompt "Select a Notes folder (or create a new one):" default items {item 1 of folderChoices}
if chosenFolderName is false then return
set chosenFolderName to item 1 of chosenFolderName
if chosenFolderName is "➕ New Folder…" then
set newFolderName to text returned of (display dialog "Enter new folder name:" default answer "")
if newFolderName is "" then return
tell application "Notes"
set chosenNotesFolder to make new folder at account chosenAccountName with properties {name:newFolderName}
end tell
else
tell application "Notes"
set chosenNotesFolder to folder chosenFolderName of account chosenAccountName
end tell
end if
-- 4) Prepare YAML parser (Ruby / Psych)
set sep to character id 31 -- Unit Separator
set tsep to character id 30 -- Record Separator
set rubyCmd to "/usr/bin/env ruby -E UTF-8 -r psych -e " & quoted form of ¬
"sep=\"\\u001F\"; tsep=\"\\u001E\"; path=ARGV[0]; data=Psych.safe_load(File.read(path), permitted_classes: [], aliases: true); data = {} unless data.is_a?(Hash); title=data[\"title\"]; body=data[\"body\"]; syntax=data[\"syntax\"]; tags=data[\"tags\"]; if tags.is_a?(String); tags=[tags]; end; tags=Array(tags).compact.map(&:to_s); out=[title, body, syntax, tags.join(tsep)].map{|v| v.nil? ? \"\" : v.to_s}; STDOUT.write(out.join(sep))"
-- 5) Process all files
repeat with i from 1 to maxToProcess
set fileName to item i of fileNames
set filePath to folderPOSIX & fileName
set baseName to my stripExtension(fileName)
-- Parse YAML
set parsed to ""
set hadError to false
try
set parsed to do shell script rubyCmd & " " & quoted form of filePath
on error errMsg number errNum
set hadError to true
display alert "YAML parse failed for " & fileName message errMsg
end try
if hadError is false then
-- Split parsed fields
set AppleScript's text item delimiters to sep
set parts to text items of parsed
set AppleScript's text item delimiters to ""
-- Ensure 4 parts
set yamlTitle to ""
set yamlBody to ""
set yamlSyntax to ""
set yamlTagsStr to ""
if (count of parts) ≥ 1 then set yamlTitle to item 1 of parts
if (count of parts) ≥ 2 then set yamlBody to item 2 of parts
if (count of parts) ≥ 3 then set yamlSyntax to item 3 of parts
if (count of parts) ≥ 4 then set yamlTagsStr to item 4 of parts
-- Build final title: "basename - yamlTitle" (or just basename if yamlTitle missing)
if yamlTitle is "" then
set finalTitle to baseName
else
set finalTitle to baseName & " - " & yamlTitle
end if
-- Tags list
set tagList to {}
if yamlTagsStr is not "" then
set AppleScript's text item delimiters to tsep
set tagList to text items of yamlTagsStr
set AppleScript's text item delimiters to ""
end if
-- Compose note text (body + syntax)
set composedBody to yamlBody
if yamlSyntax is not "" and yamlSyntax is not "None" then
if composedBody is not "" then set composedBody to composedBody & return & return
set composedBody to composedBody & "syntax: " & yamlSyntax
end if
-- Insert invisible WORD JOINER around math operators (body/syntax only)
set composedBody to my insertWordJoiners(composedBody)
-- Build hashtags line (no word-joiners)
set tagLine to ""
if (count of tagList) > 0 then
set tagLine to my tagsToHashtags(tagList)
end if
-- Escape HTML
set escapedContents to my htmlEscape(composedBody)
set escapedTitle to my htmlEscape(finalTitle)
-- Body: title (20px) + blank line + content in <pre>
set htmlBody to ""
set htmlBody to htmlBody & "<div style=\"font-size:20px; font-weight:600; line-height:1.2;\">" & escapedTitle & "</div>"
set htmlBody to htmlBody & "<div><br></div>"
set htmlBody to htmlBody & "<pre style=\"font-family: -apple-system, Menlo, SFMono-Regular, monospace; white-space: pre-wrap; -webkit-text-size-adjust:100%;\">" & escapedContents & "</pre>"
-- Create and show the note
tell application "Notes"
set newNote to make new note at chosenNotesFolder with properties {body:htmlBody}
show newNote
set selection to newNote
activate
end tell
-- UI-type hashtags ONLY if tags exist
if tagLine is not "" then
my typeTagsLineInDetachedWindow(tagLine)
--delay 0.3 -- settle before next note
end if
end if
end repeat
-- ===== Helpers =====
-- Strip the last file extension (e.g., "a.b.txt" -> "a.b")
on stripExtension(fileName)
if fileName does not contain "." then return fileName
set AppleScript's text item delimiters to "."
set parts to text items of fileName
set AppleScript's text item delimiters to ""
if (count of parts) ≤ 1 then return fileName
-- Rejoin all but the last part
set AppleScript's text item delimiters to "."
set baseName to (items 1 thru -2 of parts) as text
set AppleScript's text item delimiters to ""
return baseName
end stripExtension
-- Inserts U+2060 WORD JOINER around operators that trigger Math Results
on insertWordJoiners(t)
if t is "" then return t
set wj to character id 8288 -- U+2060 WORD JOINER (invisible)
set specials to {"=", "+", "-", "*", "/", ".", "(", ")", "[", "]", "{", "}", "<", ">", ":", ";", ",", "|", "&", "%", "!", "?"}
repeat with s in specials
set t to my replaceText(t, s as text, wj & (s as text) & wj)
end repeat
return t
end insertWordJoiners
on tagsToHashtags(tagList)
set outTags to {}
repeat with t in tagList
set clean to my sanitizeTag(t as text)
if clean is not "" then set end of outTags to "#" & clean
end repeat
if (count of outTags) is 0 then return ""
set AppleScript's text item delimiters to " "
set resultText to outTags as text
set AppleScript's text item delimiters to ""
return resultText
end tagsToHashtags
on sanitizeTag(t)
set t to my trimText(t)
if t begins with "#" then
if (length of t) > 1 then
set t to text 2 thru -1 of t
else
return ""
end if
end if
set t to my replaceText(t, " ", "_")
set t to my replaceText(t, tab, "_")
set t to my replaceText(t, return, "_")
set t to my replaceText(t, linefeed, "_")
return t
end sanitizeTag
on trimText(t)
repeat while t begins with " " or t begins with tab or t begins with return or t begins with linefeed
set t to text 2 thru -1 of t
end repeat
repeat while t ends with " " or t ends with tab or t ends with return or t ends with linefeed
set t to text 1 thru -2 of t
end repeat
return t
end trimText
on htmlEscape(t)
set t to my replaceText(t, "&", "&amp;")
set t to my replaceText(t, "<", "&lt;")
set t to my replaceText(t, ">", "&gt;")
set t to my replaceText(t, "\"", "&quot;")
set t to my replaceText(t, "'", "&#39;")
return t
end htmlEscape
on replaceText(theText, searchString, replacementString)
set AppleScript's text item delimiters to searchString
set theItems to text items of theText
set AppleScript's text item delimiters to replacementString
set theText to theItems as text
set AppleScript's text item delimiters to ""
return theText
end replaceText
-- UI typing of hashtags line in a detached note window (requires Accessibility)
-- Blocks until the detached window is closed, to avoid overlap with next note creation
on typeTagsLineInDetachedWindow(tagLine)
--delay 0.5
tell application "System Events"
if not (UI elements enabled) then
display alert "UI Scripting is not enabled" message "Enable Accessibility for Script Editor or osascript in System Settings → Privacy & Security → Accessibility."
return
end if
tell process "Notes"
set frontmost to true
--delay 0.2
-- Record current window count
set initialWindowCount to (count of windows)
-- Open selected note in its own window (Window menu)
try
click menu item "Open Note in New Window" of menu "Window" of menu bar 1
on error
-- Fallback shortcut (if available)
keystroke "o" using {shift down, command down}
end try
-- Wait for new window to appear (up to 2 seconds)
set t0 to (current date)
repeat while ((count of windows) < (initialWindowCount + 1))
--delay 0.05
if ((current date) - t0) > 2 then exit repeat
end repeat
--delay 0.2
-- Now the front window should be the note window
try
if (exists scroll area 1 of front window) then
set sa to scroll area 1 of front window
if (exists text area 1 of sa) then
click text area 1 of sa
else
click sa
end if
else if (count of text areas of front window) > 0 then
click item 1 of text areas of front window
else
click front window
end if
end try
--delay 0.1
-- Go to end of note
key code 125 using command down -- Cmd+Down
--delay 0.1
-- Insert two newlines then tags, with trailing space
keystroke return
keystroke return
keystroke tagLine
keystroke " "
--delay 0.1
-- Close detached window to avoid clutter
keystroke "w" using command down
-- Wait for window count to return to initial (up to 2 seconds)
set t1 to (current date)
repeat while ((count of windows) > initialWindowCount)
--delay 0.05
if ((current date) - t1) > 2 then exit repeat
end repeat
end tell
end tell
end typeTagsLineInDetachedWindow
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment