Is it possible to reproduce rich text styles when splitting a file?

This script explodes text-like records into many separate records of the same type. It splits at a delimiter that you can set—here, it’s set to newline.

It works fine, but three hours ago I got greedy and wanted the content of created rich text files to match that of the original—font, bold, italics, and bold-italics are all I really care about.

So I screwed around with attribute runs—and searching this forum and everywhere else—in order to find a way to copy the rich text styling from the original to the created files.

Does anyone know how to preserve (and reapply) styling? Both text and rich text seem to strip all style info.

Here’s the working but style-destroying script:

property delim : "
"

tell application id "DNtp"
	try
		set theDatabase to current database
		set theseItems to the selection
		if selection is {} then error "Please select some contents"
		repeat with thisItem in theseItems
			set theLoc to location of thisItem
			set theType to type of thisItem
			set targetGroupName to theLoc & "/Exploded: " & (name of thisItem as string)
			set targetGroup to create location targetGroupName in theDatabase
			set richText to rich text of thisItem
			set {TIDs, AppleScript's text item delimiters} to {AppleScript's text item delimiters, delim}
			set richChunks to text items of richText
			set AppleScript's text item delimiters to TIDs
			
			repeat with thisChunk in richChunks
				if length of thisChunk ≠ 0 then
					set newName to paragraph 1 of thisChunk
					set newText to thisChunk
					set newRecord to create record with {name:newName, rich text:newText, type:theType} in targetGroup
					if theType is rtf then
						-- This is just me settling for my usual font choice.
						set font of text of newRecord to "Avenir-Book"
					end if
				end if
			end repeat
		end repeat
		
	on error errMsg number errNum
		if errNum is not -128 then display alert "DEVONthink" message errMsg as warning
	end try
end tell
1 Like

text items don’t contain style information so there’s nothing to preserve when you split the text.

Rich text is not a supported data type of AppleScript, therefore assigning it to a variable converts it to plain text. But it should be doable, see for example this example that performs merging instead of splitting. The trick is to directly copy the rich text from one record to the other.

I was trying to figure something like that out (with JXA) just yesterday. The scripting dictionary mentions texts (plural) in the “contains” section of record, same as children et al.

This implies (to me, at least) that a record contains a list of text “objects”, accessible as an Array by

const texts = record.texts();

just as I can do with children etc.
However, that doesn’t work. Instead I have to use record.text (in analogy to the AppleScript in Merge Document with file Name - #2 by cgrunenberg)

Is this a glitch in the documentation of the scripting dictionary, an anomaly in the “contains” relationship, or am I just too thick to get it? Here’s what works in JXA:

const text = record.text; /* gives me a single text "object" */
const paragraphs = text.paragraphs(); /* gives me an Array of paragraphs */
const attributeRuns = text.attributeRuns(); /* gives me an Array of AttributeRuns */

So, paragraphs and attributeRuns in the text “object” behave exactly as they’re supposed to.

There still is a richText property in a record object. Of which the documentation says

The rich text of a record (see text suite). Use the ‘text’ relationship introduced by version 3.0 instead, especially for changing the contents/styles of RTF(D) documents.

Which I find confusing, since I can’t find a definition of “relationship” in a scripting dictionary, nor where I would find text. There’s only texts in a record, which (as described) doesn’t work as expected.

I found that in AppleScript, texts of theRecord compiles to text of theRecord (without the plural) in Script Editor. Maybe this is related to the JXA behavior.

1 Like

And text has a property text of type Text. So you have a class Text, and a data type Text (aka string), and the two are not the same.

This stuff is a mess. From what I found on the net, it’s not even possible to do what the OP asked for in AppleScript. Or rather: It might be possible, but only with a major effort. Perhaps NSAttributedString is more useful.

1 Like

Creates a temporary file on desktop that you need to remove. I’m sure you can automate removal, but this does what you want. I tried on a sample, but you should test on longer text.

use framework "Foundation"
use scripting additions

property delim : return -- Newline character for splitting

tell application id "DNtp"
	try
		-- Ensure a selection is made
		set theseItems to the selection
		if theseItems is {} then error "Please select a document."
		
		-- Process each selected item
		repeat with thisItem in theseItems
			-- Get item name and location
			set itemName to name of thisItem
			set itemLocation to location of thisItem
			set targetGroupName to itemLocation & "/Exploded: " & itemName
			set targetGroup to create location targetGroupName in current database
			
			-- Export to Desktop instead of temp folder
			set desktopPath to POSIX path of (path to desktop as text)
			set tempRTFDPath to desktopPath & "temp_exploded.rtfd"
			
			-- Export the document as an RTFD file
			export record thisItem to tempRTFDPath
			
			-- Ensure the exported folder exists
			set fileManager to current application's NSFileManager's defaultManager()
			if not (fileManager's fileExistsAtPath:tempRTFDPath) then
				error "RTFD export failed: Folder was not created."
			end if
			
			-- Find the actual `.rtf` file inside the `.rtfd` package
			set rtfFilePath to tempRTFDPath & "/New Rich Text 1.rtf"
			if not (fileManager's fileExistsAtPath:rtfFilePath) then
				error "RTF file missing inside the exported RTFD package."
			end if
			
			-- Load the correct `.rtf` file as `NSData`
			set fileURL to (current application's NSURL's fileURLWithPath:rtfFilePath)
			set rtfNSData to (current application's NSData's alloc()'s initWithContentsOfURL:fileURL)
			
			-- Convert `NSData` into `NSAttributedString` to keep formatting
			set attrString to (current application's NSAttributedString's alloc()'s initWithRTF:rtfNSData documentAttributes:(missing value))
			if attrString is missing value then
				error "Failed to create NSAttributedString from RTF file."
			end if
			
			-- Extract plain text version to determine paragraph locations
			set nsPlainText to attrString's |string|()
			
			-- Normalize newlines (Convert `\r` and `\r\n` to `\n`)
			set nsPlainText to (nsPlainText's stringByReplacingOccurrencesOfString:("
") withString:("
"))
			set nsPlainText to (nsPlainText's stringByReplacingOccurrencesOfString:("
") withString:("
"))
			
			-- Split into paragraphs
			set parStrings to (nsPlainText's componentsSeparatedByString:("
"))
			
			-- Iterate over paragraphs and extract styled content
			set startIndex to 0
			repeat with i from 0 to ((parStrings's |count|()) - 1)
				set onePara to (parStrings's objectAtIndex:i)
				if (onePara's |length|() > 0) then
					-- Locate paragraph range
					set paraRange to ((attrString's |string|())'s rangeOfString:(onePara))
					if paraRange is missing value then
						error "Could not find paragraph range."
					end if
					
					-- Extract attributed substring for paragraph
					set subAttrStr to (attrString's attributedSubstringFromRange:paraRange)
					
					-- Convert attributed substring to RTF (NO EXTERNAL FILES)
					set subRTFData to (subAttrStr's RTFFromRange:(current application's NSMakeRange(0, subAttrStr's |length|())) documentAttributes:(current application's NSDictionary's dictionary()))
					
					-- Convert RTF `NSData` to AppleScript text
					set newRTFString to (current application's NSString's alloc()'s initWithData:subRTFData encoding:(current application's NSMacOSRomanStringEncoding))
					
					-- Create a new DEVONthink record with full formatting
					set newName to onePara as text
					set newRecord to create record with {name:newName, rich text:(newRTFString as text), type:rtf} in targetGroup
					
					-- Update `startIndex` to prevent overlap
					set startIndex to startIndex + (onePara's |length|()) + 1
				end if
			end repeat
			
			--
			
		end repeat
	on error errMsg
		display dialog "Error: " & errMsg
	end try
end tell

Thanks so much for all this effort. I tried running it but it threw

"Error: RTF file missing inside the exported RTFD package.”

You’ve also inspired me to get back to basics. I used to write command-line obj-c programs that used all those NS libraries. Those were golden days.

And I remember seeing another AppleScript script years ago that removed images from RTFDs. It made extensive use of NS frameworks and attribute runs, and …

Oh, it was in this forum! I just located it. Here’s the link: If DevonThink could find images inside RTF files … - #3 by pete31.

Since this latter script manages to copy-through all the rich properties, we know that rich-text reproduction is possible. All I need to do is figure out how to combine cutting at a delimiter with rich-text preservation.

Thanks again.

style
I’m not getting that error. Try this

use framework "Foundation"
use scripting additions

property delim : return -- Newline character for splitting

tell application id "DNtp"
	try
		-- Ensure a selection is made
		set theseItems to the selection
		if theseItems is {} then error "Please select a document."
		
		-- Process each selected item
		repeat with thisItem in theseItems
			-- Get item name and location
			set itemName to name of thisItem
			set itemLocation to location of thisItem
			set targetGroupName to itemLocation & "/Exploded: " & itemName
			set targetGroup to create location targetGroupName in current database
			
			-- Export to Desktop instead of temp folder
			set desktopPath to POSIX path of (path to desktop as text)
			set tempRTFDPath to desktopPath & "temp_exploded.rtfd"
			
			-- Export the document as an RTFD file
			export record thisItem to tempRTFDPath
			
			-- Ensure the exported folder exists
			set fileManager to current application's NSFileManager's defaultManager()
			if not (fileManager's fileExistsAtPath:tempRTFDPath) then
				error "RTFD export failed: Folder was not created."
			end if
			
			-- Find the RTF file inside the .rtfd package - try both common names
			set rtfFilePath to tempRTFDPath & "/TXT.rtf"
			set altRtfFilePath to tempRTFDPath & "/New Rich Text 1.rtf"
			
			if (fileManager's fileExistsAtPath:rtfFilePath) then
				set rtfFilePath to rtfFilePath
			else if (fileManager's fileExistsAtPath:altRtfFilePath) then
				set rtfFilePath to altRtfFilePath
			else
				-- List contents of RTFD package to find RTF file
				set rtfdContents to (fileManager's contentsOfDirectoryAtPath:tempRTFDPath |error|:(missing value))
				set foundRTF to false
				repeat with fileName in rtfdContents
					if (fileName as text) ends with ".rtf" then
						set rtfFilePath to tempRTFDPath & "/" & (fileName as text)
						set foundRTF to true
						exit repeat
					end if
				end repeat
				
				if not foundRTF then
					error "RTF file missing inside the exported RTFD package. Contents: " & (rtfdContents as text)
				end if
			end if
			
			-- Load the RTF file as NSData
			set fileURL to (current application's NSURL's fileURLWithPath:rtfFilePath)
			set rtfNSData to (current application's NSData's alloc()'s initWithContentsOfURL:fileURL)
			if rtfNSData is missing value then
				error "Failed to load RTF file data from " & rtfFilePath
			end if
			
			-- Convert NSData into NSAttributedString to keep formatting
			set attrString to (current application's NSAttributedString's alloc()'s initWithRTF:rtfNSData documentAttributes:(missing value))
			if attrString is missing value then
				error "Failed to create NSAttributedString from RTF file."
			end if
			
			-- Extract plain text version to determine paragraph locations
			set nsPlainText to attrString's |string|()
			
			-- Normalize newlines (Convert \r and \r\n to \n)
			set nsPlainText to (nsPlainText's stringByReplacingOccurrencesOfString:("
") withString:("
"))
			set nsPlainText to (nsPlainText's stringByReplacingOccurrencesOfString:("
") withString:("
"))
			
			-- Split into paragraphs while preserving empty lines
			set parStrings to (nsPlainText's componentsSeparatedByString:("
"))
			
			-- Iterate over paragraphs and extract styled content
			set startIndex to 0
			repeat with i from 0 to ((parStrings's |count|()) - 1)
				set onePara to (parStrings's objectAtIndex:i)
				
				-- Process only non-empty paragraphs
				set paraLength to (onePara's |length|())
				if paraLength > 0 then
					-- Locate paragraph range
					set paraRange to ((attrString's |string|())'s rangeOfString:(onePara))
					
					if paraRange is missing value then
						error "Could not find paragraph range for text: " & (onePara as text)
					end if
					
					-- Extract attributed substring for paragraph
					set subAttrStr to (attrString's attributedSubstringFromRange:paraRange)
					
					-- Convert attributed substring to RTF
					set subRTFData to (subAttrStr's RTFFromRange:(current application's NSMakeRange(0, subAttrStr's |length|())) documentAttributes:(current application's NSDictionary's dictionary()))
					
					-- Convert RTF NSData to AppleScript text
					set newRTFString to (current application's NSString's alloc()'s initWithData:subRTFData encoding:(current application's NSMacOSRomanStringEncoding))
					
					-- Create a new DEVONthink record with full formatting
					set newRecord to create record with {name:(onePara as text), rich text:(newRTFString as text), type:rtf} in targetGroup
					
					-- Update startIndex to prevent overlap
					set startIndex to startIndex + paraLength + 1
				else
					-- Just update startIndex for empty lines
					set startIndex to startIndex + 1
				end if
			end repeat
			
			-- Clean up: Delete the temporary RTFD package using AppleScript
			tell application "Finder"
				if exists POSIX file tempRTFDPath then
					delete POSIX file tempRTFDPath
				end if
			end tell
			
		end repeat
	on error errMsg
		-- Clean up on error using AppleScript
		tell application "Finder"
			if exists POSIX file tempRTFDPath then
				delete POSIX file tempRTFDPath
			end if
		end tell
		display dialog "Error: " & errMsg
	end try
end tell

or you can try this one:

use framework "Foundation"
use scripting additions

tell application id "DNtp"
	try
		-- Ensure a selection is made
		set theseItems to the selection
		if theseItems is {} then error "Please select a document."
		
		-- Get Foundation classes we'll use
		set NSFileManager to current application's NSFileManager
		set NSString to current application's NSString
		set NSAttributedString to current application's NSAttributedString
		set NSMutableAttributedString to current application's NSMutableAttributedString
		set NSData to current application's NSData
		set NSUTF8StringEncoding to current application's NSUTF8StringEncoding
		set NSMacOSRomanStringEncoding to current application's NSMacOSRomanStringEncoding
		set NSDocumentTypeDocumentAttribute to current application's NSDocumentTypeDocumentAttribute
		set NSRTFTextDocumentType to current application's NSRTFTextDocumentType
		set NSNotFound to current application's NSNotFound
		
		-- Process each selected item
		repeat with thisItem in theseItems
			set itemName to name of thisItem
			set itemLocation to location of thisItem
			set targetGroupName to itemLocation & "/Exploded: " & itemName
			set targetGroup to create location targetGroupName in current database
			
			-- Create a temporary path for RTFD
			set desktopPath to POSIX path of (path to desktop as text)
			set tempPath to desktopPath & "temp_exploded.rtfd"
			
			-- Export the document as RTFD
			export record thisItem to tempPath
			
			-- Initialize file manager
			set fm to NSFileManager's defaultManager()
			
			-- Verify RTFD package exists and find RTF file
			if not (fm's fileExistsAtPath:tempPath) then
				error "Failed to create RTFD package"
			end if
			
			-- Try to find the RTF file
			set rtfPath to tempPath & "/New Rich Text 1.rtf"
			if not (fm's fileExistsAtPath:rtfPath) then
				set rtfPath to tempPath & "/TXT.rtf"
				if not (fm's fileExistsAtPath:rtfPath) then
					error "Could not find RTF file in package"
				end if
			end if
			
			-- Read RTF data
			set rtfData to NSData's dataWithContentsOfFile:rtfPath
			if rtfData is missing value then
				error "Failed to read RTF file data"
			end if
			
			-- Create attributed string from RTF
			set options to current application's NSDictionary's dictionaryWithObject:NSRTFTextDocumentType forKey:NSDocumentTypeDocumentAttribute
			set errorRef to missing value
			set attrString to (NSAttributedString's alloc()'s initWithData:rtfData options:options documentAttributes:(missing value) |error|:(reference to errorRef))
			
			if errorRef is not missing value then
				error "Failed to create attributed string: " & ((errorRef's localizedDescription()) as text)
			end if
			
			-- Get plain text and split into paragraphs
			set plainText to attrString's |string|() as text
			
			-- Split text into paragraphs using AppleScript's text handling
			set paragraphList to {}
			set tempText to plainText
			set oldDelimiters to AppleScript's text item delimiters
			set AppleScript's text item delimiters to {return, linefeed}
			set paragraphList to text items of tempText
			set AppleScript's text item delimiters to oldDelimiters
			
			-- Process each paragraph
			set startLocation to 0
			repeat with i from 1 to count of paragraphList
				set paraText to item i of paragraphList
				if length of paraText > 0 then
					try
						-- Find the range of this paragraph in the original attributed string
						set paraLength to length of paraText
						set paraRange to current application's NSMakeRange(startLocation, paraLength)
						
						-- Extract the styled paragraph
						set paraAttrStr to (attrString's attributedSubstringFromRange:paraRange)
						
						-- Convert to RTF data
						set docAttrs to current application's NSDictionary's dictionary()
						set paraRTFData to (paraAttrStr's RTFFromRange:(current application's NSMakeRange(0, paraAttrStr's |length|())) documentAttributes:docAttrs)
						
						-- Convert to string
						set rtfString to (NSString's alloc()'s initWithData:paraRTFData encoding:NSMacOSRomanStringEncoding) as text
						
						-- Create record
						create record with {name:paraText, rich text:rtfString, type:rtf} in targetGroup
					on error errText
						log "Failed to process paragraph: " & errText
					end try
				end if
				-- Update start location for next paragraph (add 1 for the line ending)
				set startLocation to startLocation + (length of paraText) + 1
			end repeat
			
			-- Clean up
			tell application "Finder"
				if exists POSIX file tempPath then
					delete POSIX file tempPath
				end if
			end tell
			
		end repeat
		
	on error errMsg
		-- Clean up on error
		try
			tell application "Finder"
				if exists POSIX file tempPath then
					delete POSIX file tempPath
				end if
			end tell
		end try
		display dialog "Error: " & errMsg
	end try
end tell
1 Like

Fabuloso! It works perfectly. All stylings are reproduced.

Thank you so much for taking the time to do this. May all your dreams come true.

I was referring to AppleScript on its own, not DEVONthink. It’s the same e.g. in TextEdit:

# Fails
tell application "TextEdit"
	set theText to text of document 1
	set text of document 2 to theText
end tell

#  Works
tell application "TextEdit"
	set text of document 2 to text of document 1
end tell

The Script Editor.app calls them elements, the script suite technically one-to-one or one-to-many relationships.

I’m aware of that. What I find irritating is that the “contains” sections says texts, implying a one-to-many relationship, while there is only a single text element (one-to-one). And apparently Script Editor is aware of the discrepancy, if it changes texts to text at compile-time for AppleScript.

That’s an issue of the Script Editor.app, not one of the script suite. And just in case that it’s useful for anyone here’s a simple AppleScript-only version to split RTF(D) documents into paragraphs:

tell application id "DNtp"
	repeat with theRecord in selected records
		set theName to name of theRecord
		set theLocation to location of theRecord
		set theLocation to theLocation & "/Exploded: " & (theName as string)
		set theGroup to create location theLocation in (database of theRecord)
		set theNum to count of paragraphs of text of theRecord
		repeat with i from 1 to theNum
			set theText to paragraph i of text of theRecord as string
			if length of theText is greater than 1 then set paraRecord to create record with {name:theText, type:rtf, content:paragraph i of text of theRecord} in theGroup
		end repeat
	end repeat
end tell
1 Like

Modified yours a bit

tell application id "DNtp"
	repeat with theRecord in selected records
		set theName to name of theRecord
		set theLocation to location of theRecord
		set theDatabase to database of theRecord
		
		-- Create "Exploded" group inside the same location
		set theGroup to create location (theLocation & "/Exploded: " & theName) in theDatabase
		
		-- Get the total number of paragraphs
		set theNum to count of paragraphs of text of theRecord
		
		-- Loop through each paragraph, keeping RTF formatting
		repeat with i from 1 to theNum
			set theText to paragraph i of text of theRecord
			if length of theText is greater than 1 then
				create record with {name:("Part " & i), type:rtf, rich text:(paragraph i of text of theRecord)} in theGroup
			end if
		end repeat
	end repeat
end tell

I guess since the delimiter proposed by the OP isn’t important, but only paragraphs are, here’s my offering…

tell application id "DNtp"
	if (selected records) is {} then return
	repeat with theRecord in ((selected records) whose (type is rtf) or (type is rtfd))
		set {loc, recName} to {location, name without extension} of theRecord
		set dest to create location (loc & "/Exploded: " & recName as string) in (database of theRecord)
		repeat with theParagraph in (paragraphs of (text of theRecord))
			set paraText to theParagraph as string
			if (words of paraText) is not {} then
				set newDoc to create record with {name:paraText, content:theParagraph, type:rtf} in dest
			end if
		end repeat
	end repeat
end tell
1 Like