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
How to access comments and their associated text in iWork documents via AppleScript/ScriptingBridge?
 
 
Q