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
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
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.
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.
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 …
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.
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
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 singletext 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
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