How to access comments and their associated text in iWork documents via AppleScript/ScriptingBridge?

Hi all,

I’m developing a Mac OS application with XCode that interacts with iWork documents (Pages, Numbers, Keynote) using ScriptingBridge (and maybe AppleScript). Right now I started with Pages, assuming if it works for Pages, it will likely be similar for Numbers and Keynote.

While I can successfully access and modify the main body text (e.g. the “body text” property in a Pages document), I’m having major difficulties accessing the comments (or annotations) within these documents.

There are many aspects, but right now what I’m trying to achieve:

  • For a Pages document, I need to scan the document and extract, for each comment:
    • The content of the comment (i.e. the comment’s text).
    • The text that is being commented on.
  • For Numbers, similarly, I need to retrieve the commented cell’s content and the associated comment.
  • For Keynote, the same as Pages, except I manage to get the Presenter Notes.

Once done, I could replace the content accordingly.

What I’ve tried:

  • Using AppleScript commands such as:
    • every comment of document "Test"
    • Accessing properties like content or range of a comment.
  • Attempting various syntaxes (including using class specifiers) to force AppleScript to recognize comments.
  • Using ScriptingBridge in my Swift code, but I couldn’t find any mapping for a “comment” object in the Pages dictionary.

However, all these attempts result in errors such as “cannot convert …” or “this class is not key value coding-compliant for the key …” which leads me to believe that the iWork scripting dictionaries may not expose comments (or annotations) in a scriptable way.

Questions:

  1. Is there a supported way to access the comments (and the associated commented text) in an iWork document via AppleScript or ScriptingBridge?
  2. If so, what is the proper syntax or property name to use? (For example, should I be looking for a class named “comment”, “annotation”, or perhaps something else?)
  3. If direct access via AppleScript/ScriptingBridge is not possible, what alternative approaches would you recommend for programmatically extracting comment data from iWork documents?

I apologize if my post isn't clear, it is translated from French. Any insights or examples would be greatly appreciated. Thank you!

Update on my findings:

I’ve come to the conclusion that comments in iWork documents are simply not scriptable or accessible via AppleScript or ScriptingBridge.

I’m now working with a different approach using placeholders. In Numbers, I can manipulate the content as expected, though placeholders are less convenient than comments.

For Pages, I also managed to use placeholders, but it caused me to lose all styles because I had to switch to plain text.

My strategy to keep all style is to perform a simple find/replace on my placeholders within the body text. For images, I use the "description" field intended for VoiceOver. Again, not the most elegant solution, but it works.

However, I’m stuck on one issue: when my placeholders span multiple paragraphs, I can’t find a way to remove the content between them without losing styles.

Does anyone have an idea on how to delete content placed between two placeholders, even across multiple paragraphs, while preserving styles? Any suggestions would be greatly appreciated!

Could be useful for someone, here's an example of script for Numbers to manipulate the content, cells and rows with placeholders:

set newList to {}
repeat with anItem in theList
set newList to {anItem} & newList
end repeat
return newList
end reverseList
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 text
set AppleScript's text item delimiters to ""
return newText
end replaceText
tell application "Numbers"
tell document "TestNum"
repeat with s in sheets
tell s
repeat with t in tables
-- Create a static list of row indices
set totalRows to count of rows of t
set rowIndices to {}
repeat with i from 1 to totalRows
copy i to end of rowIndices
end repeat
set revIndices to my reverseList(rowIndices)
repeat with iRef in revIndices
set rowNum to iRef as integer
-- Directly use "row rowNum of t" to access the row
set numCells to count of cells of (row rowNum of t)
set deleteRowFlag to false
repeat with j from 1 to numCells
set cellRef to cell j of (row rowNum of t)
set cellContent to value of cellRef
if cellContent is not missing value then
if cellContent contains "[DELETE_ROW]" then
set deleteRowFlag to true
exit repeat
end if
end if
end repeat
if deleteRowFlag then
delete (row rowNum of t)
log "Row " & rowNum & " : [DELETE_ROW] -> row deleted."
else
repeat with j from 1 to numCells
set cellRef to cell j of (row rowNum of t)
set cellContent to value of cellRef
if cellContent is not missing value then
if cellContent contains "[REPLACE]" and cellContent contains "[/REPLACE]" then
try
set startPos to offset of "[REPLACE]" in cellContent
set endPos to offset of "[/REPLACE]" in cellContent
set beforeTag to ""
if startPos > 1 then set beforeTag to text 1 thru (startPos - 1) of cellContent
set afterStart to endPos + (length of "[/REPLACE]")
set afterTag to ""
if afterStart (length of cellContent) then set afterTag to text afterStart thru -1 of cellContent
set cellContent to beforeTag & "REPLACED" & afterTag
set value of cellRef to cellContent
log "Row " & rowNum & ", Column " & j & " : [REPLACE][/REPLACE] replaced by 'REPLACED'."
on error errMsg
log "Error for [REPLACE][/REPLACE] at row " & rowNum & ", column " & j & " : " & errMsg
end try
end if
if cellContent contains "[ADD_ROW_1]" then
try
tell (row rowNum of t)
set newRow to add row below
end tell
set value of cell j of newRow to "ROW ADDED"
set cellContent to my replaceText(cellContent, "[ADD_ROW_1]", "")
set value of cellRef to cellContent
log "Row " & rowNum & ", Column " & j & " : [ADD_ROW_1] -> row added."
on error errMsg
log "Error for [ADD_ROW_1] at row " & rowNum & ", column " & j & " : " & errMsg
end try
end if
if cellContent contains "[ADD_ROW_2]" then
try
tell (row rowNum of t)
set newRow1 to add row below
-- Add the second row below the first newly added row
set newRow2 to add row below newRow1
end tell
set value of cell j of newRow1 to "ROW ADDED"
set value of cell j of newRow2 to "ROW ADDED"
set cellContent to my replaceText(cellContent, "[ADD_ROW_2]", "")
set value of cellRef to cellContent
log "Row " & rowNum & ", Column " & j & " : [ADD_ROW_2] -> two rows added."
on error errMsg
log "Error for [ADD_ROW_2] at row " & rowNum & ", column " & j & " : " & errMsg
end try
end if
if cellContent contains "[KEEP]" and cellContent contains "[/KEEP]" then
try
set startPos to offset of "[KEEP]" in cellContent
set endPos to offset of "[/KEEP]" in cellContent
set beforeTag to ""
if startPos > 1 then set beforeTag to text 1 thru (startPos - 1) of cellContent
-- Extract content between the tags
set textStart to startPos + (length of "[KEEP]")
set keptText to ""
if endPos > textStart then set keptText to text textStart thru (endPos - 1) of cellContent
set afterStart to endPos + (length of "[/KEEP]")
set afterTag to ""
if afterStart (length of cellContent) then set afterTag to text afterStart thru -1 of cellContent
set cellContent to beforeTag & keptText & afterTag
set value of cellRef to cellContent
log "Row " & rowNum & ", Column " & j & " : [KEEP][/KEEP] -> content kept, tags removed."
on error errMsg
log "Error for [KEEP][/KEEP] at row " & rowNum & ", column " & j & " : " & errMsg
end try
end if
if cellContent contains "[DELETE]" and cellContent contains "[/DELETE]" then
try
set startPos to offset of "[DELETE]" in cellContent
set endPos to offset of "[/DELETE]" in cellContent
set beforeTag to ""
if startPos > 1 then set beforeTag to text 1 thru (startPos - 1) of cellContent
set afterStart to endPos + (length of "[/DELETE]")
set afterTag to ""
if afterStart (length of cellContent) then set afterTag to text afterStart thru -1 of cellContent
set cellContent to beforeTag & afterTag
set value of cellRef to cellContent
log "Row " & rowNum & ", Column " & j & " : [DELETE][/DELETE] -> content deleted."
on error errMsg
log "Error for [DELETE][/DELETE] at row " & rowNum & ", column " & j & " : " & errMsg
end try
end if
end if
end repeat
end if
end repeat
end repeat
end tell
end repeat
end tell
end tell

For Pages, the script iterates over images, shapes, and body text. For images it checkes their description, for the shapes it checks in text, and deletes those that have a specific tag. For the body text, I face challenged to preserve the style and delete several paragraphs not necessarily containing the tag.

My solution was to work at the section level: if a particular tag is found in the body text of a section, the script clears all the content within that section. I then have to manually remove the section break later.

After processing the document’s content, I use GUI scripting to automate a find-and-replace operation. The script simulates pressing Command+F to open the search panel, clears the search field, enters the target search term, switches to the replacement field, clears it, and types in the new text before executing a “Replace All.”

For Keynote this is the same for the search/replace all. Instead of sections, I simple use a tag in the presenter notes to target a slide to delete it. For images I also use a tag in the description.

With Pages and Keynote my last little problem is the search & replace doesn't handle the line breaks. I'm stuck with this, but it's not such a huge problem for my project, I'll just have more constraints to deal with.

Finally with Keynote I failed to target specific shapes and text item to delete. I manage to get the text, to find the tag, but then the deletion process seems erratic. I either end with nothing deleted or almost everything (including shapes and text items not having the tag). Again this is a small constraint I can deal with.

Nonetheless, if some has a lead or a solution, I'd be happy to try it.

How to access comments and their associated text in iWork documents via AppleScript/ScriptingBridge?
 
 
Q