|
-- 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, "&", "&") |
|
set t to my replaceText(t, "<", "<") |
|
set t to my replaceText(t, ">", ">") |
|
set t to my replaceText(t, "\"", """) |
|
set t to my replaceText(t, "'", "'") |
|
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 |