Search & replace text in RTF document

I am trying to search and replace text in a RTF. I have it working fine in plain text, but can’t see how to retain the formatting etc? Can anyone help?

tell application id "com.culturedcode.ThingsMac"
	set sel to item 1 of selected to dos
	set theURL to notes of selected to dos as string
	set theURLlength to count theURL
	set the theUUID to characters 21 thru theURLlength of theURL as string
end tell

tell application id "DNtp"
	
	set thisItem to get record with uuid theUUID
	set tagList to tags of thisItem
	set itemText to get plain text of thisItem
	set plain text of thisItem to findAndReplaceInText(itemText, "#ToDo ", "#ToDoComplete ") of me
	
end tell

on findAndReplaceInText(theText, theSearchString, theReplacementString)
	set AppleScript's text item delimiters to theSearchString
	set theTextItems to every text item of theText
	set AppleScript's text item delimiters to theReplacementString
	set theText to theTextItems as string
	set AppleScript's text item delimiters to ""
	return theText
end findAndReplaceInText

You’ve already seen why this is not going to work the way you want it to. There are ways using Text Suite and the text property of paragraphs, but that’s a PITA. RTF is a pure text format on the surface, but in fact it uses formatting interspersed with text. Changing the underlying text by changing plain text probably messes up the whole structure since RTF is no plain text in the classical sense. There’s already been a similar discussion here

Use Markdown for that, nowadays it even has boxes that can be used for to-dos. That is a real text format, i.e. the mark up is very limited, very simple and very easy to parse. And replacing text in the plain text of a MD file does not break anything, since formatting is done externally, via CSS.

1 Like

@chrillek thanks for your reply. I am just interested to see if anyone else had any thoughts or workarounds. It’s strange that Textedit supports it and my understand is that the RTF in DT seems to use that format?

You’re not using TextEdit or DTs RTF editor but scripting. That’s completely different.

1 Like

It’s not that bad :grinning_face_with_smiling_eyes:

-- Replace text in RTF(D) documents
-- NOTE: This script can't replace text having mixed styles and is by default case-sensitive (see 'considering case')
-- WARNING: This can't be undone!

tell application id "DNtp"
	try
		if (count of selected records) is 0 then error "Please select some documents."
		repeat
			set search_string to display name editor "Replace Text" info "Enter text to find:"
			if search_string is not "" then exit repeat
		end repeat
		
		set replacement_string to display name editor "Replace Text" info "Enter replacement text:"
		
		repeat with theRecord in selected records
			if type of theRecord is rtf or type of theRecord is rtfd then
				tell text of theRecord
					repeat with theAttributeRun in attribute runs
						set theString to (theAttributeRun as string)
						considering case -- Alternative: ignoring case
							if theString contains search_string then
								set {od, text item delimiters of AppleScript} to {text item delimiters of AppleScript, search_string}
								set theString to text items of theString
								set text item delimiters of AppleScript to replacement_string
								set theString to "" & theString
								set text item delimiters of AppleScript to od
								set text of theAttributeRun to theString
							end if
						end considering
					end repeat
				end tell
			end if
		end repeat
	on error error_message number error_number
		if the error_number is not -128 then display alert "DEVONthink" message error_message as warning
	end try
end tell

Fantastic - thank you.

And here’s a version that supports both plain & rich text documents:

-- Replace text in plain and rich text documents
-- NOTE: This script can't replace text having mixed styles and is by default case-sensitive (see 'considering case')
-- WARNING: This can't be undone!

tell application id "DNtp"
	try
		if (count of selected records) is 0 then error "Please select some documents."
		repeat
			set search_string to display name editor "Replace Text" info "Enter text to find:"
			if search_string is not "" then exit repeat
		end repeat
		
		set replacement_string to display name editor "Replace Text" info "Enter replacement text:"
		
		considering case -- Alternative: ignoring case
			repeat with theRecord in selected records
				if type of theRecord is rtf or type of theRecord is rtfd then
					tell text of theRecord
						repeat with theAttributeRun in attribute runs
							set theString to (theAttributeRun as string)
							if theString contains search_string then
								set theString to my replaceText(theString, search_string, replacement_string)
								set text of theAttributeRun to theString
							end if
						end repeat
					end tell
				else if type of theRecord is txt then
					set theString to plain text of theRecord
					if theString contains search_string then
						set theString to my replaceText(theString, search_string, replacement_string)
						set plain text of theRecord to theString
					end if
				end if
			end repeat
		end considering
	on error error_message number error_number
		if the error_number is not -128 then display alert "DEVONthink" message error_message as warning
	end try
end tell

on replaceText(theString, find, replace)
	local od
	set {od, text item delimiters of AppleScript} to {text item delimiters of AppleScript, find}
	set theString to text items of theString
	set text item delimiters of AppleScript to replace
	set theString to "" & theString
	set text item delimiters of AppleScript to od
	return theString
end replaceText

Ok, it’s attribute runs, not paragraphs where you have to change the text attribute.

If you have a minute (ok, bad joke), could you explain why one would use one and not the other? Or maybe just post a link to a relevant document.
Thanks a lot . I still think it’s convoluted :wink:

Setting the text of a paragraph changes the style (“attribute run”) of the complete paragraph to the style at the beginning, therefore this is not suitable in case of paragraphs having mixed styles.

Here’s a case insensitive version which is in case of RTF(D) also noticeably faster:

-- Replace text in plain and rich text documents (case insensitive)
-- NOTES: 
--	1. This script can't replace text having mixed styles 
--	2. The replacement text can't contain the search text
--	3. But it's about 5 times faster in case of RTF(D) than the case sensitive version
-- WARNING: This can't be undone!
-- Created by Christian Grunenberg Thu Jul 27 2021.
-- Copyright (c) 2021. All rights reserved.

tell application id "DNtp"
	try
		set theNum to count of selected records
		if theNum is 0 then error "Please select some documents."
		repeat
			set search_string to display name editor "Replace Text" info "Enter text to find:"
			if search_string is not "" then exit repeat
		end repeat
		
		set replacement_string to display name editor "Replace Text" info "Enter replacement text:"
		ignoring case
			if replacement_string contains search_string then error "Replacement text can't contain search string"
			
			show progress indicator "Replacing Text" steps theNum
			repeat with theRecord in selected records
				step progress indicator (name of theRecord) as string
				if type of theRecord is rtf or type of theRecord is rtfd then
					try
						tell text of theRecord
							repeat while true
								-- We can't retrieve all attribute ones at once as changing the first one invalidates the
								-- retrieved values. In addition, the results are always case insensitive and 
								-- don't honor considering/ignoring case
								tell (attribute run 0 whose text 1 contains search_string)
									set theString to my replaceText(text 1 as string, search_string, replacement_string)
									set text 1 to theString
								end tell
							end repeat
						end tell
					end try
				else if type of theRecord is txt then
					set theString to plain text of theRecord
					if theString contains search_string then
						set theString to my replaceText(theString, search_string, replacement_string)
						set plain text of theRecord to theString
					end if
				end if
			end repeat
			hide progress indicator
		end ignoring
	on error error_message number error_number
		hide progress indicator
		if the error_number is not -128 then display alert "DEVONthink" message error_message as warning
	end try
end tell

on replaceText(theString, find, replace)
	local od
	set {od, text item delimiters of AppleScript} to {text item delimiters of AppleScript, find}
	set theString to text items of theString
	set text item delimiters of AppleScript to replace
	set theString to "" & theString
	set text item delimiters of AppleScript to od
	return theString
end replaceText

Thank you @cgrunenberg - all working :slight_smile:

Those of us who don’t like scripting would just export to Nisus Writer Pro, which reads and writes almost “native” RTF and has a superb search and replace. Too bad if you missed it in the SummerFest Sale.
Edgar