Script: Split RTF(D) at Delimiter

This script splits RTF(D) records at a delimiter.

It optionally changes the font and font size.

Setup

You can change every property.

  • property changeFont: If true then the resulting records’ font and font size is changed.

  • property theFontSize: The desired font size of the body text, e.g. 14.0
    If set to 0 then DEVONthink’s rich text font size is used (see Preferences > Edit )

  • property theFontName: The desired font’s family name, e.g. "Lucida Grande"
    If set to "" then DEVONthink’s rich text font is used (see Preferences > Edit )

  • property theMonoFontName: The desired mono spaced font’s family name, e.g. "Courier"
    If set to "" then only the font size of mono spaced fonts is changed
    (There’s no DEVONthink preference that could be used, so if you want to change mono spaced fonts you’ll have to set this property)

Usage

  • Insert the delimiter §§§ wherever you want to split a record.
    It’s not necessary to insert the delimiter at the beginning or end.
    Delimiters can be placed between lines and also inside.

  • Run script

-- Split RTF(D) at Delimiter

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

property theDelimiter : "§§§" -- insert this delimiter anywhere in a record
property changeFont : true -- set this to false if you don't want to change the font
property theFontSize : 0 -- if 0 DEVONthink's rich text font size is used (see preferences > edit). To use another size set this property to e.g. 14.0
property theFontName : "" -- if "" DEVONthink's rich text font is used (see preferences > edit). To use another font set this property to e.g. "Lucida Grande" 
property theMonoFontName : "" -- if "" only the mono spaced font's size is changed. To use another mono spaced font set this property to e.g. "Courier" 

tell application id "DNtp"
	try
		set theRecords to selected records whose type = rtfd or type = rtf
		if theRecords = {} then my displayReminder()
		
		repeat with thisRecord in theRecords
			set thisRecord_Path to path of thisRecord
			set thisRecord_Name to name without extension of thisRecord
			my splitRTFDatDelimiter(thisRecord_Path, thisRecord_Name, thisRecord)
		end repeat
		
		hide progress indicator
	on error error_message number error_number
		hide progress indicator
		activate
		if the error_number is not -128 then display alert "DEVONthink" message error_message as warning
		return
	end try
end tell

on splitRTFDatDelimiter(theRecord_Path, theRecord_Name, theRecord)
	try
		if changeFont then
			set theProgressSteps to 4
		else
			set theProgressSteps to 2
		end if
		
		set theAttributedString to my readAttributedString(theRecord_Path)
		set theAttributedString_Length to theAttributedString's |length|()
		if theAttributedString_Length = 0 then return
		
		--------------------------------------------------------------- Change Font ---------------------------------------------------------------------
		
		if changeFont then
			---------------------------------------------------------- Get Font Attributes ---------------------------------------------------------------
			
			tell application id "DNtp" to show progress indicator "Analyzing... " & theRecord_Name steps theProgressSteps with cancel button
			tell application id "DNtp" to step progress indicator
			set theLocation to 0
			set theAttribute to current application's NSFontAttributeName
			set theAttributesArray to current application's NSMutableArray's new()
			repeat while (theLocation < theAttributedString_Length)
				set {thisFont, thisFontRange} to theAttributedString's |attribute|:theAttribute atIndex:(theLocation) longestEffectiveRange:(reference) inRange:{location:theLocation, |length|:(theAttributedString_Length - theLocation)}
				theAttributesArray's addObject:{|Font|:thisFont, FontRange:thisFontRange, FontSize:(thisFont's pointSize())}
				set theLocation to theLocation + (thisFontRange's |length|)
			end repeat
			
			---------------------------------------------------------- Analyze Font Sizes  ---------------------------------------------------------------
			
			set theFontSizes to ((theAttributesArray's valueForKey:"FontSize")'s valueForKeyPath:"@distinctUnionOfObjects.self")'s sortedArrayUsingDescriptors:{(current application's NSSortDescriptor's sortDescriptorWithKey:"self" ascending:false selector:"compare:")}
			set theFontSizes_SortedTotalLengthsArray to current application's NSMutableArray's new()
			repeat with i from 0 to ((theFontSizes's |count|()) - 1)
				set thisFontSize to (theFontSizes's objectAtIndex:i)
				set thisFontSizeTotalLength to (((theAttributesArray's filteredArrayUsingPredicate:(current application's NSPredicate's predicateWithFormat:("self.FontSize = " & (thisFontSize))))'s valueForKeyPath:"FontRange.length")'s valueForKeyPath:"@sum.self")
				(theFontSizes_SortedTotalLengthsArray's addObject:{FontSize:thisFontSize, TotalLength:thisFontSizeTotalLength})
			end repeat
			theFontSizes_SortedTotalLengthsArray's sortUsingDescriptors:{(current application's NSSortDescriptor's sortDescriptorWithKey:"TotalLength" ascending:false selector:"compare:")}
			set theFontSize_Body to (((theFontSizes_SortedTotalLengthsArray)'s firstObject())'s valueForKey:"FontSize") as real
			
			------------------------------------------------------------- Change Font  --------------------------------------------------------------------
			
			tell application id "DNtp" to show progress indicator "Changing Font... " & theRecord_Name steps theProgressSteps with cancel button
			tell application id "DNtp" to step progress indicator
			
			if theFontName = "" or theFontSize = 0 then
				if current application's id = "com.devon-technologies.think3" then -- https://forum.latenightsw.com/t/storing-persistent-values-in-preferences/864
					set theDefaults to current application's NSUserDefaults's standardUserDefaults()
				else
					set theDefaults to current application's NSUserDefaults's alloc()'s initWithSuiteName:"com.devon-technologies.think3"
				end if
				if theFontName = "" then set theFontName to (theDefaults's dictionaryRepresentation())'s stringForKey:"RichFontName"
				if theFontSize = 0 then set theFontSize to (theDefaults's dictionaryRepresentation())'s doubleForKey:"RichPointSize"
			end if
			set theFont to current application's NSFont's fontWithName:(theFontName) |size|:theFontSize
			if theFont = missing value then error "Please provide a valid Font Name"
			set theFont_FamilyName to theFont's familyName()
			if theMonoFontName ≠ "" then
				set theMonoFont to current application's NSFont's fontWithName:(theMonoFontName) |size|:theFontSize
				if (theMonoFont = missing value) or not ((theMonoFont's isFixedPitch()) as boolean) then error "Please provide a valid Mono Font Name"
				set theMonoFont_FamilyName to theMonoFont's familyName()
			else
				set theMonoFont_FamilyName to missing value
			end if
			
			set theFontSize_Ratio to (theFontSize / theFontSize_Body)
			set theFontManager to current application's NSFontManager's sharedFontManager()
			set theAttribute to current application's NSFontAttributeName
			set theMutableAttributedString to theAttributedString's mutableCopy()
			
			repeat with i from 0 to ((theAttributesArray's |count|()) - 1)
				set thisItem to (theAttributesArray's objectAtIndex:i)
				set thisFont to (thisItem's valueForKey:"Font")
				set thisFontRange to (thisItem's valueForKey:"FontRange")
				set thisFontSize to (thisItem's valueForKey:"FontSize") as real
				set thisNewFontSize to thisFontSize * theFontSize_Ratio
				set thisNewFont to (theFontManager's convertFont:(thisFont) toSize:thisNewFontSize)
				if not ((thisFont's isFixedPitch()) as boolean) then
					set thisNewFont to (theFontManager's convertFont:thisNewFont toFamily:theFont_FamilyName)
				else
					if theMonoFont_FamilyName ≠ missing value then
						set thisNewFont to (theFontManager's convertFont:thisNewFont toFamily:theMonoFont_FamilyName)
					end if
				end if
				(theMutableAttributedString's removeAttribute:theAttribute range:thisFontRange)
				(theMutableAttributedString's addAttribute:theAttribute value:thisNewFont range:thisFontRange)
			end repeat
			set theAttributedString to theMutableAttributedString
		end if
		
		---------------------------------------------------------- Get Substring Ranges ----------------------------------------------------------------
		
		if theDelimiter = "§§§" then
			set theDelimiter_escaped to theDelimiter
		else
			set theDelimiter_String to current application's NSString's stringWithString:theDelimiter
			set theDelimiter_escaped to (theDelimiter_String's stringByReplacingOccurrencesOfString:("\\,|\\!|\\?|\\.|\\(|\\)|\\[|\\]|\\{|\\}|\\*|\\\\|\\^|\\+|\\<|\\>|\\||\\$|\\=") withString:("\\\\$0") options:(current application's NSRegularExpressionSearch) range:{location:0, |length|:theDelimiter_String's |length|()}) as string
		end if
		set {theRegex, theError} to current application's NSRegularExpression's regularExpressionWithPattern:("(?ms:(?<=" & theDelimiter_escaped & "|\\A)(.*?)(?=" & theDelimiter_escaped & "|\\Z))") options:0 |error|:(reference)
		if theError ≠ missing value then error (theError's localizedDescription() as string)
		set theAttributedString_String to theAttributedString's |string|()
		set theMatches to (theRegex's matchesInString:theAttributedString_String options:0 range:{0, theAttributedString_String's |length|()})
		if theMatches's |count|() < 2 then
			tell application id "DNtp" to log message info "Script \"Split RTFD at Delimiter\": No Delimiter" record theRecord
			return
		end if
		
		------------------------------------------------------ Write Temp Files & Import  --------------------------------------------------------------
		
		tell application id "DNtp"
			show progress indicator "Splitting... " & theRecord_Name steps theProgressSteps as string with cancel button
			step progress indicator
			set theGroup to create record with {name:(theRecord_Name & " [Split at Delimiter]"), type:group} in parent 1 of theRecord
		end tell
		
		set theTempDirectoryURL to my createTempDirectory()
		set theCharacterSet to (current application's NSCharacterSet's alphanumericCharacterSet())'s invertedSet()
		
		repeat with i from 0 to ((theMatches's |count|()) - 1)
			set {thisAttributedString, thisTempURL} to my writeAttributedStringToTempFile(theAttributedString, (((theMatches's objectAtIndex:i)'s rangeAtIndex:0)), theTempDirectoryURL)
			set thisTempPath to (thisTempURL's |path|()) as string
			set thisName to ((((((thisAttributedString's |string|())'s stringByTrimmingCharactersInSet:theCharacterSet))'s componentsSeparatedByString:linefeed)'s firstObject())'s stringByTrimmingCharactersInSet:theCharacterSet) as string
			if thisName = "" then set thisName to "Untitled"
			tell application id "DNtp" to import thisTempPath name thisName to theGroup
		end repeat
		
		set {successDeleteDir, theError} to (current application's NSFileManager's defaultManager()'s removeItemAtURL:(theTempDirectoryURL) |error|:(reference))
		if theError ≠ missing value then error (theError's localizedDescription() as string)
		tell application id "DNtp" to step progress indicator
		tell application id "DNtp" to hide progress indicator
		
	on error error_message number error_number
		activate
		if the error_number is not -128 then display alert "Error: Handler \"splitRTFDatDelimiter\"" message error_message as warning
		try
			current application's NSFileManager's defaultManager()'s removeItemAtURL:(theTempDirectoryURL) |error|:(missing value)
		end try
		error number -128
	end try
end splitRTFDatDelimiter

on displayReminder()
	activate
	display alert "Split RTF(D)" message linefeed & "Insert delimiter " & theDelimiter & " anywhere in a record, e.g.:" & linefeed & linefeed & "Chapter 1" & linefeed & theDelimiter & linefeed & "Chapter 2" & linefeed ¬
		& linefeed & "or:" & linefeed & linefeed & "This is " & theDelimiter & " a line." & linefeed as informational
	error number -128
end displayReminder

on readAttributedString(thePath)
	try
		set {theAttributedString, theError} to current application's NSAttributedString's alloc()'s initWithURL:(current application's |NSURL|'s fileURLWithPath:thePath) options:(missing value) documentAttributes:({NSDocumentTypeDocumentAttribute:(current application's NSRTFDTextDocumentType)}) |error|:(reference)
		if theError ≠ missing value then error (theError's localizedDescription() as string)
		return theAttributedString
	on error error_message number error_number
		activate
		if the error_number is not -128 then display alert "Error: Handler \"readAttributedString\"" message error_message as warning
		error number -128
	end try
end readAttributedString

on createTempDirectory()
	try
		set theTempDirectoryURL to current application's |NSURL|'s fileURLWithPath:((current application's NSTemporaryDirectory())'s stringByAppendingPathComponent:("Script Split RTFD at Delimiter" & space & (current application's NSProcessInfo's processInfo()'s globallyUniqueString())))
		set {successCreateDir, theError} to current application's NSFileManager's defaultManager's createDirectoryAtURL:theTempDirectoryURL withIntermediateDirectories:false attributes:(missing value) |error|:(reference)
		if theError ≠ missing value then error (theError's localizedDescription() as string)
		return theTempDirectoryURL
	on error error_message number error_number
		activate
		if the error_number is not -128 then display alert "Error: Handler \"createTempDirectory\"" message error_message as warning
		error number -128
	end try
end createTempDirectory

on writeAttributedStringToTempFile(theAttributedString, theRange, theTempDirectoryURL)
	try
		set thisAttributedString to (my regexReplaceAttributedString(((theAttributedString's attributedSubstringFromRange:theRange))'s mutableCopy(), "(^\\s+|\\s+$)", ""))
		set thisAttributedString_Range to {location:0, |length|:thisAttributedString's |length|()}
		if (thisAttributedString's containsAttachmentsInRange:thisAttributedString_Range) then
			set thisFileWrapper to (thisAttributedString's RTFDFileWrapperFromRange:thisAttributedString_Range documentAttributes:{NSDocumentTypeDocumentAttribute:(current application's NSRTFDTextDocumentType)})
			set thisTempURL to ((theTempDirectoryURL's URLByAppendingPathComponent:(current application's NSProcessInfo's processInfo()'s globallyUniqueString()))'s URLByAppendingPathExtension:"rtfd")
			set {successWrite, theError} to (thisFileWrapper's writeToURL:thisTempURL options:(current application's NSFileWrapperWritingAtomic) originalContentsURL:(missing value) |error|:(reference))
		else
			set thisData to (thisAttributedString's RTFFromRange:(thisAttributedString_Range) documentAttributes:{NSDocumentTypeDocumentAttribute:(current application's NSRTFTextDocumentType)})
			set thisTempURL to ((theTempDirectoryURL's URLByAppendingPathComponent:(current application's NSProcessInfo's processInfo()'s globallyUniqueString()))'s URLByAppendingPathExtension:"rtf")
			set {successWrite, theError} to (thisData's writeToURL:thisTempURL options:(current application's NSDataWritingAtomic) |error|:(reference))
		end if
		if theError ≠ missing value then error (theError's localizedDescription() as string)
		return {thisAttributedString, thisTempURL}
	on error error_message number error_number
		activate
		if the error_number is not -128 then display alert "Error: Handler \"writeAttributedStringToTempFile\"" message error_message as warning
		error number -128
	end try
end writeAttributedStringToTempFile

on regexReplaceAttributedString(theMutableAttributedString, thePattern, theReplacementPattern) -- based on Shane Stanley's code, https://forum.latenightsw.com/t/read-and-write-rtf-files/1200/5
	try
		set {theRegex, theError} to current application's NSRegularExpression's regularExpressionWithPattern:thePattern options:0 |error|:(reference)
		if theRegex = missing value then error theError's localizedDescription() as text
		set theMutableAttributedString_string to theMutableAttributedString's |string|()
		set theMatches to (theRegex's matchesInString:theMutableAttributedString_string options:0 range:{0, theMutableAttributedString_string's |length|()}) as list
		set theMatches to reverse of theMatches
		repeat with thisMatch in theMatches
			(theMutableAttributedString's replaceCharactersInRange:(thisMatch's range()) withString:theReplacementPattern)
		end repeat
		return theMutableAttributedString
	on error error_message number error_number
		activate
		if the error_number is not -128 then display alert "Error: Handler \"regexReplaceAttributedString\"" message error_message as warning
		error number -128
	end try
end regexReplaceAttributedString

3 Likes

Helpful script! Would it be possible to modify the script that splittet documents stay in original format? Because now when running this command on markdown files, they become rtf.

I’m wondering why you’d run a script that has explicitly been written for RTF(D) records on MD files. Splitting MDs is in fact a no-brainer, even in AppleScript. And I’m sure there’s a script available here in the forum for that.

Right: Automating DT with JavaScript: Splitting Markdown

1 Like

Thank you for the link. Will adapt into my workflow.

Reason to why I asked here is probably because I had this script on my machine, but haven’t used it in a while. Since I write in Markdown, it would make more sense to have markdown oriented script.