Notes is another of Apple’s applications with half-baked
scripting support. There are about a gazillion questions
about weird behavior and errors…
So there are only two examples in this section.
The first one shows how to save all selected notes as HTML and plain
text in a pre-defined folder, the second how
to copy the attachments of the selected
notes to a pre-defined folder. It should be fairly easy to
combine both of them into one that saves the text and
attachments in the same folder.
The script goes over all notes and saves their content as
plain text and HTML in a subfolder of the predefined
baseFolder. The name of this subfolder is derived by the
note’s name, with special characters replaced by
underscores.
Note that the text is not written with the JXA
method write() because this one does not allow to define
Unicode encoding. Without that, using write() would break
any text outside the ASCII code range. So the script uses a
part of the
JXA-ObjectiveC-Bridge, described here.
ObjC.import("Foundation");
(() => {
const curApp = Application.currentApplication();
curApp.includeStandardAdditions = true;
const baseFolder = `${curApp.pathTo("desktop")}/Notes`;
const notesApp = Application("Notes");
const notes = notesApp.selection();
notes.forEach((n) => {
/*
* Get plain text and HTML for every selected note
*/
const text = n.plaintext();
const html = n.body();
/*
* Build folder name for this note
*/
const dirName = n.name().replaceAll(/[:/&<>|]/g, "_");
let targetFolder = `${baseFolder}/${dirName}`;
if (text || html) {
/*
* Create folder for note data, ignoring already
* existing folder
*/
curApp.doShellScript(`mkdir -p "${targetFolder}"`);
/*
* Save plain text and HTML using ObjC-Bride
*/
if (text) {
writeUTF8(`${targetFolder}/note.html`, text);
}
if (html) {
writeUTF8(`${targetFolder}/note.html`, html);
}
}
});
})();
function writeUTF8(fileName, text) {
/*
* ObjC-Bridge methods to write text
* with UTF8 encoding to file
*/
const str = $.NSString.alloc.initWithUTF8String(text);
str.writeToFileAtomicallyEncodingError(
fileName, true, $.NSUTF8StringEncoding, null
);
}
There’s no direct way to export notes from Apple’s program.
You can only save notes as PDF, and there’s no provision to
save attached files. This makes it difficult to move your
documents to another program. The following script saves all
attachments for the currently selected notes in sub-folders
under the baseFolder, named after the notes.
The saving operation is a bit tricky, since simply using
the Notes’s save() method can fail because of missing
access rights. In that case, the script inspects the error
message and extracts the path to the attachment from it. It
then uses the shell’s cp command to copy the attachment
file to the new folder.
Note that the shell command can only work if you have
granted full disk access to sh in the privacy section of the macOS system
preferences.
(() => {
const curApp = Application.currentApplication();
curApp.includeStandardAdditions = true;
const baseFolder = `${curApp.pathTo("desktop")}/Notes`;
/*
* Get currently selected notes
*/
const notesApp = Application("Notes");
const notes = notesApp.selection();
notes.forEach((n) => {
/*
* Get attachments for every selected notes
*/
const attachments = n.attachments();
if (attachments.length > 0) {
/*
* Build the name of the folder to save these attachments
* in by taking the note's name and replacing replacing
* all special characters by underscores.
*/
const dirName = n.name().replaceAll(/[:/&<>|]/g, "_");
let targetFolder = `${baseFolder}/${dirName}`;
/*
* Create the folder for this note, do nothing
* if it exists already
*/
curApp.doShellScript(`mkdir -p "${targetFolder}"`);
attachments.forEach((a) => {
/* Get the name of every attachment and try to
* save it with the Notes method save. This can
* fail due to issues with the access rights.
* In that case, use the shell's cp command
* to copy the data, getting the original file name
* of the attachment from the error message
*/
const targetFile = `${targetFolder}/${a.name()}`;
try {
notesApp.save(a, { in: targetFile });
} catch (error) {
/* If the error "Operation not permitted" occurs,
* the offending filename is contained in the error message
* after "NSSourceFilePathErrorKey=". Extract it and
* use shell command to copy the file to the traget anyway
*/
if (/Code=1 "Operation not permitted"/.test(error.message)) {
const match = error.message.match(
/NSSourceFilePathErrorKey=(.*),\s/m
);
curApp.doShellScript(`cp "${match[1]}" "${targetFile}"`);
} else {
curapp.displayAlert(error.message);
}
}
});
}
});
})();