Styled Text Lib

This is an expansion of a library I developed last month to make a script that was originally written for Pages ’09 usable with the practically unscriptable Pages 5. It allows styled text to be composed without the aid of a styled-text application — which has obvious implications for unscriptable applications and speed.

It’s intended simply for the composition of body text which can be pasted into a document. It’s not intended for editing text already in a document and doesn’t do exotic stuff like text boxes, images, lists, and paragraph groupings. Not all applications observe all of its features

It’s inspired by Pages ’09’s “paragraph styles” and uses some of the same terminology. While lacking some of the features of the Pages styles, it does offer some of its own — such as the ability to specify the measurement units used in indent settings, the ability to set tab stops, and what I’ve called “spot effects”, which are style differences always applied to regex-identifiable text within a particular style. So for instance, numbers followed by dots at the beginnings of lines may always be bold, or the word “banana” always yellow, or a certain style feature may not be applied to certain text, etc.

There are eight public handlers:
startNewStyledText() – Initialise a new styled output text. No parameters.
makeParagraphStyle() – Create a “paragraph style” script object implementing the data for a particular style. The one parameter is a record specifying the features to include which aren’t the defaults. So it can be anything from an empty record to a morass of nested records and lists! The demo script in a later post (#19) creates a styled document in TextEdit which hopefully explains all. :slightly_smiling_face:
styleAndAppend() – Apply a paragraph style to some text and append the result to the output. The parameter is the AppleScript text or NSString to be styled. This handler must be called through the script object for the relevant “paragraph style”, not directly through the library. eg.:

tell myStyle to styleAndAppend("I think there's something wrong with this banana.")
-- or:
myStyle's styleAndAppend("I think there's something wrong with this banana.")

putResultOnClipboard() – No parameters. Does what it says, storing the current clipboard contents first. No parameters.
paste() – Keystroke Command-“v”, wait a second, then restore the clipboard contents. No parameters. It’s the calling script’s responsibility to see that the receiving application’s frontmost.
New with version 1.0.3:
saveResultAsRTF() – Save the styled text as an RTF file. The parameter can be a bookmark, alias, HFS or POSIX path, or NSURL. Alternatively, it can be a record with one of these as the value of a |file| property. The record can also have a |replacing| property with a boolean value. Where |replacing| isn’t specified, it’s false.
styledTextResult() – Return the styled text to the calling script as an NSMutableAttributedString. No parameters.
textResult() – Return the assembled text to the calling script as plain AppleScript text. No parameters.

The modus operandi is thus:
Initialise an output object.
Create each required “paragraph style” at some point before its first use.
Use the created styles to append successive pieces of text to the output.
Paste, save, and/or return the completed text.

Library script:

(* Library of handlers for building styled text, putting it on the clipboard, and pasting it into a document.
From version 1.0.3, the text can also (or instead) be returned to the calling script and/or saved as an RTF file.
By Nigel Garvey July-October 2016.
Better clipboard content storage method provided by Shane Stanley 31st August 2016.

In use:
	Tell the library to startNewStyledText().
	For each paragraph style needed, tell the library to makeParagraphStyle(), passing a record with the details. This can be done at any point before the style's first used. The result is a script object set up to apply that style.
	To append a block of text in a particular style, tell the relevant style script object (not the library) to styleAndAppend(), passing an AppleScript text or NSString.
	Options when the styled text's complete:
		Put it on the clipboard: tell the library to putResultOnClipboard(). The previous clipboard contents are stored in a property.
		Paste from the clipboard into a document: tell the library to paste(). The clipboard contents are restored from the property afterwards. It's the calling script's responsibility to ensure the receiving document's ready. The receiving application must paste in response to command-v and must understand and implement pasted RTF data.
		Save the styled text as an RTF file: tell the library to saveResultAsRTF(), passing the destination details.
		Get the library's styledTextResult() (an NSMutableAttributedString).
		Get the library's textResult() (AppleScript text).
*)

use AppleScript version "2.3.1"
use framework "Foundation"
use framework "AppKit"

property name : "Styled Text Lib"
property author : "Nigel Garvey"
property version : "1.0.4"

property |⌘| : current application
property mv : missing value
property styledText : mv -- Output assembled here.
property clipboardStore : {} -- Clipboard contents stored here.

(* PUBLIC HANDLERS *)

(* Start a new styled text. *)
on startNewStyledText()
	set my styledText to |⌘|'s NSMutableAttributedString's new()
end startNewStyledText

(* Return a script object set up to implement a user-defined 'paragraph style'.
params: Record or NSDictionary with optional properties: {alignment ("left"/"center"/"centre"/"justify"/"right"/"natural"), |bold| (boolean), capitalization (boolean/"all caps"/"small caps"/"title"/"small caps title"/"title small caps"/"all lower"), |character background color| (AS color/missing value), |color| (AS color/missing value), |default tab interval| (real, in indent units), |first line indent| (real, in indent units), |font name| (text), |font size| (real, in points), |indent units| ("in"/"inches"/"cm"/"centimeters"/"centimetres"/"pt"/"points"), |italic| (boolean), |left indent| (real, in indent units), |line spacing| (real, in points or lines depending on line spacing type), |line spacing type| ("relative"/"at least"/"min"/"minimum"/"at most"/"max"/"maximum"/anything beginning with "exact"/anything ending with "between"), |outline| (boolean/real), |right indent| (positive real, in indent units), |right to left| (boolean), |space after| (real, in points), |space before| (real, in points), |spot FX| (list of spot effect, see below), |strikethrough| (boolean/text ("single"/"double"/"thick" AND/OR "dash"/"dot"/"dash dot"/"dash dot dot" AND/OR "by word")), |strikethrough color| (AS color/missing value), |tab stops| (list of tab stop, see getTabArray() handler), |underline| (boolean/text as for |strikethrough|), |underline color| (AS color/missing value)} *)
on makeParagraphStyle(params)
	-- Set variables to the passed or to default values.
	set defaults to {alignment:"natural", |bold|:false, capitalization:false, |character background color|:mv, |color|:mv, |default tab interval|:1.0, |first line indent|:0.0, |font name|:"Helvetica Neue", |font size|:12.0, |indent units|:"inches", |italic|:false, |left indent|:0.0, |line spacing|:1.0, |line spacing type|:"relative", |outline|:0.0, |right indent|:0.0, |right to left|:false, |space after|:0.0, |space before|:0.0, |spot FX|:{}, |strikethrough color|:mv, |strikethrough|:false, |tab stops|:{}, |underline color|:mv, |underline|:false}
	set {alignment:textAlignment, |bold|:|bold|, capitalization:capitalization, |character background color|:backgroundColor, |color|:textColor, |default tab interval|:defaultTabInterval, |first line indent|:firstLineIndent, |font name|:fontFamily, |font size|:fontSize, |indent units|:indentUnits, |italic|:|italic|, |left indent|:leftIndent, |line spacing|:lineSpacing, |line spacing type|:lineSpacingType, |outline|:|outline|, |right indent|:rightIndent, |right to left|:rightToLeft, |space after|:spaceAfter, |space before|:spaceBefore, |spot FX|:spotFX, |strikethrough color|:strikethroughColor, |strikethrough|:|strikethrough|, |tab stops|:tabStops, |underline color|:underlineColor, |underline|:|underline|} to (params as record) & defaults
	
	-- Multiplier to convert tab or indent parameters to points.
	if ((indentUnits = "cm") or (indentUnits = "centimeters") or (indentUnits = "centimetres")) then
		set ppu to 28.346456692913
	else if ((indentUnits = "pt") or (indentUnits = "points")) then
		set ppu to 1.0
	else -- Inches. Assume this also if the |indent units| parameter's rubbish!
		set ppu to 72.0
	end if
	
	-- Script object to be returned. Inherits styleAndAppend() below.
	script |style|
		property attributes : |⌘|'s NSMutableDictionary's new()
		property spotFX : mv
	end script
	
	-- Add a font attributes entry to |style|'s attributes dictionary.
	|style|'s attributes's setValue:(my getNSFont(|bold|, |italic|, fontSize, fontFamily)) forKey:(|⌘|'s NSFontAttributeName)
	
	-- Set up and add a paragraph style attribute entry.
	set paragraphStyle to |⌘|'s NSMutableParagraphStyle's new()
	tell paragraphStyle
		-- Tab stops.
		its setTabStops:(my getTabArray(tabStops, defaultTabInterval, ppu))
		-- Text alignment.
		if (textAlignment = "justify") then
			its setAlignment:(|⌘|'s NSTextAlignmentJustified)
		else
			its setAlignment:(my getAlignmentKey(textAlignment))
		end if
		-- Line spacing. The meaning of the figure depends on the 'line spacing type' setting.
		if (lineSpacingType = "relative") then
			-- Relative line spacing can be specified as "double" or a multiplier. Anything else is treated as "single".
			if (lineSpacing = "double") then set lineSpacing to 2.0
			if not ((lineSpacing's class = real) or (lineSpacing's class = integer)) then set lineSpacing to 1.0
			if (lineSpacing ≠ 1.0) then its setLineHeightMultiple:(lineSpacing as real)
		else if (lineSpacing > 0.0) then
			if ((lineSpacingType = "at least") or (lineSpacingType begins with "min")) then
				its setMinimumLineHeight:(lineSpacing as real)
			else if ((lineSpacingType = "at most") or (lineSpacingType begins with "max")) then
				its setMaximumLineHeight:(lineSpacing as real)
			else if (lineSpacingType begins with "exact") then -- eg. "exact" or "exactly"
				its setMinimumLineHeight:(lineSpacing as real)
				its setMaximumLineHeight:(lineSpacing as real)
			else if (lineSpacingType ends with "between") then -- eg. "between" or "in between" or "inbetween".
				its setLineSpacing:(lineSpacing as real)
			end if
		end if
		-- Paragraph indents.
		if (firstLineIndent > 0.0) then its setFirstLineHeadIndent:(firstLineIndent * ppu)
		-- If intended for a machine configured for right-to-left text, swap the left and right indent settings to change their "head" and "tail" associations.
		if (rightToLeft) then set {leftIndent, rightIndent} to {rightIndent, leftIndent}
		if (leftIndent > 0.0) then its setHeadIndent:(leftIndent * ppu)
		if (rightIndent > 0.0) then its setTailIndent:(rightIndent * -ppu) -- Negative in NSParagraphStyle.
		-- Spaces before and after paragraphs.
		if (spaceBefore > 0.0) then its setParagraphSpacingBefore:(spaceBefore as real)
		if (spaceAfter > 0.0) then its setParagraphSpacing:(spaceAfter as real)
	end tell
	|style|'s attributes's setValue:(paragraphStyle) forKey:(|⌘|'s NSParagraphStyleAttributeName)
	
	-- Other attributes:
	tell |style|'s attributes
		set underlineAttributes to my getAttributeMask(|underline|)
		if (underlineAttributes > 0) then its setValue:(underlineAttributes) forKey:(|⌘|'s NSUnderlineStyleAttributeName)
		set strikethroughAttributes to my getAttributeMask(|strikethrough|)
		if (strikethroughAttributes > 0) then its setValue:(strikethroughAttributes) forKey:(|⌘|'s NSStrikethroughStyleAttributeName)
		if (|outline|'s class = boolean) then set |outline| to |outline| as integer
		if (|outline| ≠ 0.0) then its setValue:(|outline| as real) forKey:(|⌘|'s NSStrokeWidthAttributeName)
		if (textColor's class = list) then its setValue:(my getColor(textColor)) forKey:(|⌘|'s NSForegroundColorAttributeName)
		if (backgroundColor's class = list) then its setValue:(my getColor(backgroundColor)) forKey:(|⌘|'s NSBackgroundColorAttributeName)
		if (underlineColor's class = list) then its setValue:(my getColor(underlineColor)) forKey:(|⌘|'s NSUnderlineColorAttributeName)
		if (strikethroughColor's class = list) then its setValue:(my getColor(strikethroughColor)) forKey:(|⌘|'s NSStrikethroughColorAttributeName)
	end tell
	
	-- The spotFX value is a list of records, one for each, possibly compound effect. Each record has a 'regex' property (ICU regex pattern) and an 'attributes' property (record similar to params above). The possible attributes are a subset of those in params plus |baseline shift| (real), |subscript| (boolean), and |superscript| (boolean). The first one or two records may be for capitalization, bundled here for convenient handling of its effect on the text and the regexes for other effects.
	if ((capitalization = true) or (capitalization = "all caps")) then
		-- Text capitalized except for any Eszetts.
		set spotFX's beginning to {regex:"[^ß]++", attributes:{capitalization:true}}
	else if (capitalization = "small caps") then
		-- Originally lower-case letters and all spaces 1/5th smaller …
		set spotFX's beginning to {regex:"[[:lower:] ]++", attributes:{|font size|:fontSize * 0.8}}
		-- … after text (except for Eszetts) capitalised.
		set spotFX's beginning to {regex:"[^ß]++", attributes:{capitalization:true}}
	else if (capitalization = "title") then
		-- Initials at word boundaries capitalised.
		set spotFX's beginning to {regex:"\\b[[:alpha:]]", attributes:{capitalization:true}}
	else if ((capitalization contains "small caps") and (capitalization contains "title")) then
		-- Originally lower-case letters not at initial word boundaries and all spaces 1/5th smaller …
		set spotFX's beginning to {regex:"(\\B[[:lower:]]| )++", attributes:{|font size|:fontSize * 0.8}}
		-- … after text (except for Eszetts) capitalised.
		set spotFX's beginning to {regex:"[^ß]++", attributes:{capitalization:true}}
	else if (capitalization = "all lower") then
		-- Text lower-cased.
		set spotFX's beginning to {regex:"\\A.++\\Z", attributes:{capitalization:false}}
	end if
	
	if ((count spotFX) > 0) then
		-- Except for effect-only properties, the default attribute values are those of the paragraph style.
		set defaults to {|baseline shift|:0.0, |bold|:|bold|, |character background color|:backgroundColor, |color|:textColor, |font name|:fontFamily, |font size|:fontSize, |italic|:|italic|, |outline|:|outline|, |strikethrough color|:strikethroughColor, |strikethrough|:|strikethrough|, |subscript|:false, |superscript|:false, |underline color|:underlineColor, |underline|:|underline|}
		
		-- Replace the values in each record with forms convenient for styleAndAppend().
		repeat with effect in spotFX
			-- Replace the regex string with an NSRegularExpression.
			set effect's regex to (|⌘|'s NSRegularExpression's regularExpressionWithPattern:(effect's regex) options:(|⌘|'s NSRegularExpressionAnchorsMatchLines) |error|:(mv))
			
			-- If not a capitalization effect, replace its attributes record with a list containing the NSString names of attributes to delete and/or a dictionary of attributes to add.
			if (((effect's attributes) & {capitalization:mv})'s capitalization = mv) then
				-- Set variables to the effect's specified values or to the defaults.
				set {|baseline shift|:FXBaselineShift, |bold|:FXBold, |character background color|:FXBackgroundColor, |color|:FXTextColor, |font name|:FXFontFamily, |font size|:FXFontSize, |italic|:FXItalic, |outline|:FXOutline, |strikethrough color|:FXStrikethroughColor, |strikethrough|:FXStrikethrough, |subscript|:FXSubscript, |superscript|:FXSuperscript, |underline color|:FXUnderlineColor, |underline|:FXUnderline} to (effect's attributes) & defaults
				
				-- Initialise the list and the dictionary.
				set FXPrompts to {}
				set FXAttributes to |⌘|'s NSMutableDictionary's new()
				
				-- If any of the effect's attributes are different from the style's, or are effect-only, append entries for them:
				if not ((FXBold = |bold|) and (FXItalic = |italic|) and (FXFontSize = fontSize) and (FXFontFamily = fontFamily)) then (FXAttributes's setValue:(my getNSFont(FXBold, FXItalic, FXFontSize, FXFontFamily)) forKey:(|⌘|'s NSFontAttributeName))
				if (FXUnderline ≠ |underline|) then
					if (FXUnderline = false) then
						set FXPrompts's end to |⌘|'s NSUnderlineStyleAttributeName
					else
						(FXAttributes's setValue:(my getAttributeMask(FXUnderline)) forKey:(|⌘|'s NSUnderlineStyleAttributeName))
					end if
				end if
				if (FXStrikethrough ≠ |strikethrough|) then
					if (FXStrikethrough = false) then
						set FXPrompts's end to |⌘|'s NSStrikethroughStyleAttributeName
					else
						(FXAttributes's setValue:(my getAttributeMask(FXStrikethrough)) forKey:(|⌘|'s NSStrikethroughStyleAttributeName))
					end if
				end if
				if (FXOutline's class = boolean) then set FXOutline to FXOutline as integer
				if (FXOutline ≠ |outline|) then
					if (FXOutline = 0) then
						set FXPrompts's end to |⌘|'s NSStrokeWidthAttributeName
					else
						(FXAttributes's setValue:(FXOutline as real) forKey:(|⌘|'s NSStrokeWidthAttributeName))
					end if
				end if
				if (FXTextColor ≠ textColor) then
					if (FXTextColor = mv) then
						set FXPrompts's end to |⌘|'s NSForegroundColorAttributeName
					else
						(FXAttributes's setValue:(my getColor(FXTextColor)) forKey:(|⌘|'s NSForegroundColorAttributeName))
					end if
				end if
				if (FXBackgroundColor ≠ backgroundColor) then
					if (FXBackgroundColor = mv) then
						set FXPrompts's end to |⌘|'s NSBackgroundColorAttributeName
					else
						(FXAttributes's setValue:(my getColor(FXBackgroundColor)) forKey:(|⌘|'s NSBackroundColorAttributeName))
					end if
				end if
				if (FXUnderlineColor ≠ underlineColor) then
					if (FXUnderlineColor = mv) then
						set FXPrompts's end to |⌘|'s NSUnderlineColorAttributeName
					else
						(FXAttributes's setValue:(my getColor(FXUnderlineColor)) forKey:(|⌘|'s NSUnderlineColorAttributeName))
					end if
				end if
				if (FXStrikethroughColor ≠ strikethroughColor) then
					if (FXStrikethroughColor = mv) then
						set FXPrompts's end to |⌘|'s NSStrikethroughColorAttributeName
					else
						(FXAttributes's setValue:(my getColor(FXStrikethroughColor)) forKey:(|⌘|'s NSStrikethroughColorAttributeName))
					end if
				end if
				
				if (FXSuperscript) then
					(FXAttributes's setValue:(1) forKey:(|⌘|'s NSSuperscriptAttributeName))
				else if (FXSubscript) then
					(FXAttributes's setValue:(-1) forKey:(|⌘|'s NSSuperscriptAttributeName))
				end if
				if (FXBaselineShift ≠ 0.0) then (FXAttributes's setValue:(FXBaselineShift) forKey:(|⌘|'s NSBaselineOffsetAttributeName))
				
				-- Append the attribute dictionary to the list if it's not empty (although it doesn't really matter if it is).
				if (FXAttributes's |count|() > 0) then set FXPrompts's end to FXAttributes
				-- Replace the effect record's original 'attibutes' value with the list just created.
				set effect's attributes to FXPrompts
			end if
		end repeat
	end if
	-- Assign the spotFX list (even if it's empty) to the paragraph style script's 'spotFX' property.
	set |style|'s spotFX to spotFX
	
	return |style|
end makeParagraphStyle

(* Apply a paragraph style to some text and append the result to 'styledText'.
txt: AS text or NSString. *)
on styleAndAppend(txt)
	-- Make an NSAttributedString with the passed text and the attributes of the executing style.
	-- ('my attributes' and 'my spotFX' belong to the 'paragraph style' script object calling this handler.)
	set styleRun to |⌘|'s NSAttributedString's alloc()'s initWithString:(txt) attributes:(my attributes)
	
	-- If the style includes spot effects, apply them to the result.
	if ((count my spotFX) > 0) then
		set styleRun to styleRun's mutableCopy()
		set txt to |⌘|'s NSString's stringWithString:(txt)
		set searchRange to {location:0, |length|:txt's |length|()}
		repeat with effect in (my spotFX)
			set {regex:FXRegex, attributes:FXPrompts} to effect
			-- Get the ranges in the NSMutableAttributedString where this effect is to be applied.
			set FXRanges to ((FXRegex's matchesInString:(txt) options:(0) range:(searchRange))'s valueForKey:("range"))
			-- Apply the effect to those ranges.
			if (FXPrompts's class = record) then -- If still a record, this is a capitalization effect.
				set capitalizing to FXPrompts's capitalization
				set theLocale to |⌘|'s NSLocale's currentLocale()
				repeat with i from (count FXRanges) to 1 by -1
					set range to FXRanges's item i
					set substring to (txt's substringWithRange:(range))
					if (capitalizing) then
						set substring to (substring's uppercaseStringWithLocale:(theLocale))
					else
						set substring to (substring's lowercaseStringWithLocale:(theLocale))
					end if
					(styleRun's replaceCharactersInRange:(range) withString:(substring))
				end repeat
			else -- FXPrompts is a list containing attribute names and/or an attribute dictionary.
				-- In each range, delete attributes with the names and/or add attribute(s) from the dictionary.
				repeat with range in FXRanges
					repeat with entry in FXPrompts
						if ((entry's isKindOfClass:(|⌘|'s NSString)) as boolean) then
							(styleRun's removeAttribute:(entry) range:(range))
						else
							(styleRun's addAttributes:(entry) range:(range))
						end if
					end repeat
				end repeat
			end if
		end repeat
	end if
	
	-- Append the result to the styled text obtained so far.
	my (styledText's appendAttributedString:(styleRun))
end styleAndAppend

(* Store the current clipboard contents and put the styled text on the clipboard. *)
on putResultOnClipboard()
	----------
	-- Clipboard storage method supplied by Shane Stanley.
	set my clipboardStore to |⌘|'s NSMutableArray's new()
	set pasteboard to |⌘|'s NSPasteboard's generalPasteboard()
	set pasteboardItems to pasteboard's pasteboardItems()
	repeat with i from 1 to (count pasteboardItems)
		set pbItem to pasteboardItems's item i
		set newItem to |⌘|'s NSPasteboardItem's new()
		set theTypes to pbItem's |types|()
		repeat with j from 1 to (count theTypes)
			set thisType to theTypes's item j
			set theData to (pbItem's dataForType:(thisType))
			if (theData ≠ mv) then (newItem's setData:(theData) forType:(thisType))
		end repeat
		my (clipboardStore's addObject:(newItem))
	end repeat
	----------
	pasteboard's clearContents()
	pasteboard's writeObjects:(|⌘|'s NSArray's arrayWithObject:(my styledText))
end putResultOnClipboard

(* Send a Command-"v" keystroke to the frontmost application, wait 1 second, then restore the old clipboard contents. *)
on paste()
	tell application "System Events" to keystroke "v" using {command down}
	using terms from scripting additions
		delay 1
	end using terms from
	tell |⌘|'s NSPasteboard's generalPasteboard()
		its clearContents()
		its writeObjects:(my clipboardStore)
	end tell
end paste

(* Return the styled text as an NSMutableAttributedString. (v1.0.3 or later.) *)
on styledTextResult()
	return my styledText
end styledTextResult

(* Return the styled text as plain AppleScript text. (v1.0.3 or later.) *)
on textResult()
	return my styledText's |string|() as text
end textResult

(* Save the styled text as an RTF file. (v1.0.3 or later.)
dest: destination as bookmark, alias, HFS path, POSIX path, NSURL, or a record with one of these as a |file| property value and an optional |replacing| property with a boolean value. Replacing is, by default, false.
*)
on saveResultAsRTF(dest)
	set destClass to dest's class
	try
		-- Get the |file| and |replacing| values according to whether or not in record form.
		if (destClass = record) then
			set {|file|:dest, replacing:replacing} to dest & {|file|:mv, replacing:false}
			if (replacing's class ≠ boolean) then error "Invalid replacing parameter"
			set destClass to dest's class
		else
			set replacing to false
		end if
		-- Try to derive an NSURL for the save destination.
		if (destClass = text) then
			if (dest begins with "/") then
				set destURL to |⌘|'s NSURL's fileURLWithPath:(dest)
			else if (dest begins with "~/") then
				set destURL to |⌘|'s NSURL's fileURLWithPath:((|⌘|'s NSString's stringWithString:(dest))'s stringByExpandingTildeInPath())
			else if (dest contains ":") then
				set destURL to |⌘|'s NSURL's fileURLWithPath:(dest's POSIX path)
			else
				error "Path parameter format not recognised."
			end if
		else if ((destClass = alias) or (destClass = «class furl»)) then
			set destURL to |⌘|'s NSURL's fileURLWithPath:(dest's POSIX path)
		else
			try
				if ((dest's isKindOfClass:(|⌘|'s NSURL)) as boolean) then
					set destURL to dest
				else
					error
				end if
			on error
				error "Invalid file parameter."
			end try
		end if
		-- Check the URL's validity.
		if ((destURL's pathExtension()'s isEqualToString:("rtf")) as boolean) then -- Correct extension?
			set {itemExists, isFolder} to destURL's getResourceValue:(reference) forKey:(|⌘|'s NSURLIsDirectoryKey) |error|:(mv)
			if (itemExists as boolean) then -- File/folder already exists.
				if (isFolder as boolean) then
					error "Save destination already exists as a folder." -- Unlikely, but hey.
				else if (not replacing) then
					error "File already exists."
				end if
			else if (not ((destURL's URLByDeletingLastPathComponent()'s checkResourceIsReachableAndReturnError:(mv)) as boolean)) then -- Container folder doesn't exist.
				error "Destination folder doesn't exist."
			end if
		else
			error "Not an .rtf file extension."
		end if
	on error errMsg
		error "Styled Text Lib: saveResultAsRTF(): " & errMsg
	end try
	
	-- Write RTF data derived from the styled text to the specified file.
	tell my styledText to set RTFData to its RTFFromRange:({location:0, |length|:its |length|()}) documentAttributes:(mv)
	RTFData's writeToURL:(destURL) atomically:(true)
end saveResultAsRTF

(* PRIVATE HANDLERS *)

(* Return an NSFont with given attributes. *)
on getNSFont(|bold|, |italic|, fontSize, fontFamily)
	set attributes to {NSFontFamilyAttribute:fontFamily}
	if (|bold|) then
		set fontFace to "bold"
		if (|italic|) then set fontFace to "bold italic"
		set attributes to attributes & {NSFontFaceAttribute:fontFace}
	else if (|italic|) then
		set attributes to attributes & {NSFontFaceAttribute:"Italic"}
	end if
	set fontDescriptor to |⌘|'s NSFontDescriptor's fontDescriptorWithFontAttributes:(attributes)
	set theFont to |⌘|'s NSFont's fontWithDescriptor:(fontDescriptor) |size|:(fontSize as real)
	-- If the specified font isn't found, use Helvetica Neue instead.
	if (theFont = mv) then set theFont to getNSFont(|bold|, |italic|, fontSize, "Helvetica Neue")
	
	return theFont
end getNSFont

(* Return an array of NSTextTabs.
tabStops: list of records and/or integers. A record specifies a tab stop. Its optional properties are: {indent (real) indent units from the leading margin (default: |default tab interval| units after the previously set tab), alignment ("left"/"right"/"center"/"centre"/"decimal"/"natural") (default: "natural", then the same as the previously set tab)}. An integer indicates a number of repeats of the previously set tab stop at |default tab interval| intervals. *)
on getTabArray(tabStops, defaultInterval, ppu)
	-- Initial "previous" settings.
	set prevIndent to 0.0
	set prevAlignment to "natural"
	-- Empty dictionary for most types of tab stop.
	set blankDict to |⌘|'s NSDictionary's new()
	-- Terminator dictionary for decimal tab stops in the current locale.
	set terminatorDict to (|⌘|'s NSDictionary's dictionaryWithObject:(|⌘|'s NSTextTab's columnTerminatorsForLocale:(|⌘|'s NSLocale's currentLocale())) forKey:(|⌘|'s NSTabColumnTerminatorsAttributeName))
	-- Output array.
	set tabArray to |⌘|'s NSMutableArray's new()
	
	repeat with entry in tabStops
		set entryClass to entry's class
		if ((entryClass = record) or (entry's contents = {})) then
			-- Set one tab stop with the specified or default properties.
			set n to 1
		else if ((entryClass = integer) or (entryClass = real)) then
			-- Make this number of stops at the default interval with the same properties as the previous tab stop (defaults if none).
			set n to entry as integer
			set entry to {}
		else -- Bad parameter.
			set n to 0
		end if
		repeat n times
			-- If no indent specified, use previous indent + default interval. If no alignment specified, use previous alignment.
			set defaults to {indent:(prevIndent + defaultInterval), alignment:prevAlignment}
			set {indent:indent, alignment:alignment} to entry & defaults
			
			set prevIndent to indent
			set prevAlignment to alignment
			
			set indent to indent * ppu
			if (alignment = "decimal") then
				-- Xcode documentation did say use right alignment with the terminators, but natural's what works.
				set alignment to |⌘|'s NSTextAlignmentNatural
				set dict to terminatorDict
			else -- No terminators needed.
				set alignment to getAlignmentKey(alignment)
				set dict to blankDict
			end if
			(tabArray's addObject:(|⌘|'s NSTextTab's alloc()'s initWithTextAlignment:(alignment) location:(indent) options:(dict)))
		end repeat
	end repeat
	
	return tabArray
end getTabArray

(* Return an Obj-C key text alignment key. *)
on getAlignmentKey(alignment)
	-- "justified" and "decimal" are specific to paragraph style and tabs respectively and are dealt with in those sections.
	if (alignment = "left") then return |⌘|'s NSTextAlignmentLeft
	if (alignment = "right") then return |⌘|'s NSTextAlignmentRight
	if ((alignment = "center") or (alignment = "centre")) then return |⌘|'s NSTextAlignmentCenter
	return |⌘|'s NSTextAlignmentNatural -- Default is "natural" (left or right according to locale).
end getAlignmentKey

(* Return an attribute mask for an |underline| or |strikethrough| parameter. *)
on getAttributeMask(param)
	set mask to 0
	if ((param = true) or (param contains "single")) then
		set mask to (|⌘|'s NSUnderlineStyleSingle)
	else if (param contains "double") then
		set mask to (|⌘|'s NSUnderlineStyleDouble)
	else if (param contains "thick") then
		set mask to (|⌘|'s NSUnderlineStyleThick)
	end if
	if (param contains "dash dot dot") then
		set mask to (|⌘|'s NSUnderlinePatternDashDotDot) + mask
	else if (param contains "dash dot") then
		set mask to (|⌘|'s NSUnderlinePatternDashDot) + mask
	else if (param contains "dash") then
		set mask to (|⌘|'s NSUnderlinePatternDash) + mask
	else if (param contains "dot") then
		set mask to (|⌘|'s NSUnderlinePatternDot) + mask
	end if
	if (param contains "by word") then set mask to (|⌘|'s NSUnderlineByWord) + mask
	-- If a pattern's specified but no stroke style, let that imply "single".
	if ((mask > 0) and (mask mod 256 = 0)) then set mask to (|⌘|'s NSUnderlineStyleSingle) + mask
	
	return mask
end getAttributeMask

(* Return an NSColor for an AS RGB list. *)
on getColor({r, g, b})
	return |⌘|'s NSColor's colorWithCalibratedRed:(r / 65535) green:(g / 65535) blue:(b / 65535) alpha:(1.0)
end getColor

Edits: Now uses Shane’s clipboard storage method (see following post), which is better.
saveResultAsRTF(), styledTextResult(), and textResult() handlers added and comments revised.
November 2024: Code debloated and post split (demo script now in post #19) to allow what was otherwise a two-character edit for Script Debugger compatibility to be posted under MacScripter’s current Discourse software!

2 Likes

Somebody has been having fun :cool:

Let me offer an alternative for storing the clipboard – this should cope with all eventualities, including multiple items.

use AppleScript version "2.3.1"
use scripting additions
use framework "Foundation"
use framework "AppKit"

on fetchStorableClipboard()
	set aMutableArray to current application's NSMutableArray's array() -- used to store contents
	-- get the pasteboard and then its pasteboard items
	set thePasteboard to current application's NSPasteboard's generalPasteboard()
	set theItems to thePasteboard's pasteboardItems()
	-- loop through pasteboard items
	repeat with i from 1 to count of theItems
		-- make a new pasteboard item to store existing item's stuff
		set newPBItem to current application's NSPasteboardItem's alloc()'s init()
		-- get the types of data stored on the pasteboard item
		set theTypes to (item i of theItems)'s |types|()
		-- for each type, get the corresponding data and store it all in the new pasteboard item
		repeat with j from 1 to count of theTypes
			set theData to ((item i of theItems)'s dataForType:(item j of theTypes))'s mutableCopy()
			if theData is not missing value then
				(newPBItem's setData:theData forType:(item j of theTypes))
			end if
		end repeat
		-- add new pasteboard item to array
		(aMutableArray's addObject:newPBItem)
	end repeat
	return aMutableArray
end fetchStorableClipboard

on putOnClipboard:theArray
	-- get pasteboard
	set thePasteboard to current application's NSPasteboard's generalPasteboard()
	-- clear it, then write new contents
	thePasteboard's clearContents()
	thePasteboard's writeObjects:theArray
end putOnClipboard:

set theClip to my fetchStorableClipboard()
-- put your stuff on clipboard, for example...
my putOnClipboard:{my styledText}
-- do paste here
-- restore...
my putOnClipboard:theClip
1 Like

Hi Shane.

Thanks for your interest ” and for the pasteboard suggestion. :slight_smile:

I’m studying the latter now. I’m not so sure about mutableCopy making deep copies:

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

-- Get an array containing a mutable object.
set aMutableObject to current application's class "NSMutableArray"'s arrayWithArray:({1, 2, 3})
set anArray to current application's class "NSArray"'s arrayWithArray:({aMutableObject, "etc."})

-- Make a mutable copy of the array.
set aMutableCopy to anArray's mutableCopy()

-- Modify the mutable object in the copy.
tell aMutableCopy's firstObject() to addObject:("aardvark")
{anArray as list, aMutableCopy as list}
--> {{{1, 2, 3, "aardvark"}, "etc."}, {{1, 2, 3, "aardvark"}, "etc."}}

-- Ditto the mutable object in the original array.
tell anArray's firstObject() to addObject:("banana")
{anArray as list, aMutableCopy as list}
--> {{{1, 2, 3, "aardvark", "banana"}, "etc."}, {{1, 2, 3, "aardvark", "banana"}, "etc."}}

Later: Yes. Thanks Shane. Your method does a better job of preserving the clipboard contents than mine did. I’ve now incorporated into the library code above.

You’re right. I copied that from the Objective-C original (some time ago…).

Let me try to clarify the mutableCopy issue. If you call copy on an immutable object, like an NSArray, what gets returned is the same pointer. There’s no need to duplicate anything; all that happens is that the object’s retain count is incremented (and that’s used to calculate when the memory used can be freed).

But mutableCopy actually makes a new object. However, it’s just a new array containing the same pointers as the original. So it’s deeper than a copy, but certainly not deep. (To complicate things more, in Objective-C the compiler might optimize things by not actually duplicating the array until the use tries to mutate it.)

But the issue here is that when an app puts stuff on the pasteboard, it doesn’t always pass all the data. For example, an app might offer just the first and best representation of an image, and generate others only if requested. This is quicker and more memory efficient.

Making a fuller copy using mutableCopy attempts to deal with that issue by triggering a request for the data, which may no longer be available once something new has been put on the clipboard.

Ah. The “promised data” mentioned in the documentation. And dataForType: doesn’t itself trigger such a request?

I’m not really sure whether the mutableCopy was required to avoid over-optimization in Objective-C, or what, to be honest. But data is often handled lazily. It’s possible that the “data” returned by dataForType: is effectively just a reference to a chunk of memory, not held strongly enough to survive clearContents().

Hmmm. An error I’m getting at the moment says “missing value doesn’t understand the “mutableCopy” message.” It went away, of course, after I copied it to paste into this post. :wink:

Have a look at what the docs say for -dataForType: – a timeout, perhaps.

My point was meant to be that the following line checks to see if the result is missing value. But if it is, the script errors on mutableCopy before reaching there.

set theData to ((item i of theItems)'s dataForType:(item j of theTypes))'s mutableCopy()
if theData is not missing value then

Ah, right – best split the line and check after the first bit, I guess. In Objective-C, that wouldn’t happen: calling a method on nil simply returns nil.

Updated to version 1.0.3 with additional functions:

saveResultAsRTF(destinationParameter) – Saves the styled text as an RTF file.
styledTextResult() – Returns the styled text (an NSMutableAttributedString) to the calling script.
textResult() – Returns a plain AppleScript version of the text to the calling script.

On Sonoma the demo sadly gives me:
„type {} of «class ocid» id «data optr000000001078DB0FE77F0000»“ can not be read.
No time to chase the bug right now.

Hi @pjh.

Thanks for your interest in my library script. It’s a few years since I’ve looked at it!

I’m afraid I’ve not been able to reproduce the error you’re getting. The demo script works fine on my own Sonoma and Ventura systems with the library installed.

The word ‘type’ in the error message suggests the problem may be occurring in the code which handles the clipboard (aka. pasteboard) contents. If so, your clipboard may be empty or may have something on it that I didn’t foresee when I wrote the code. These are my only guesses at the moment. :thinking:

Hm - I am back quite late, but:
Too me it seems that the last line of this code in the library fails, but I am unable to fix it, whatever I try:

(* Store the current clipboard contents and put the styled text on the clipboard. *)
on putResultOnClipboard()
	----------
	-- Clipboard storage method supplied by Shane Stanley, but rerendered here in my house style. :)
	-- Transfer or copy the contents to new, unbound pasteboard items and store those.
	set aMutableArray to |⌘|'s class "NSMutableArray"'s new()
	set thePasteboard to |⌘|'s class "NSPasteboard"'s generalPasteboard()
	set currentPasteboardItems to thePasteboard's pasteboardItems()
	repeat with i from 1 to (count currentPasteboardItems)
		set thisPasteboardItem to item i of currentPasteboardItems
		set newPasteboardItem to |⌘|'s class "NSPasteboardItem"'s new()
		set theTypes to thisPasteboardItem's type {}

Hi @pjh.

Funnily enough, I encountered this exact problem a couple of days ago when adapting the code for another script. type {} should in fact be types(). Looking more deeply into the cause of the spontaneous change this morning, it seems that while types() compiles as intended in Script Editor, it’s rendered as type {} when compiled in Script Debugger. Clearly there’s a clash with something in SD’s own scripting dictionary. If instead you type |types|(), the code should work. It’ll appear as |types|() in Script Debugger and as types() in Script Editor, but the underlying compiled code will be the same.

Unfortunately, the forum software MacScripter now uses won’t let me edit the post at the top of this thread as it considers it to be too long! :face_with_raised_eyebrow:

Nigel,
great! Many thanks for your instantaneous reply!
I experimented with so many variations of type, |, and various brackets - but not once I tried to use the plural - of course :laughing:.
So, again, many thanks for making this work!
Peter

Thanks for your continued interest and perseverance after all this time! I hope the library turns out to be useful.

Demo script: (Originally part of post #1.)

(* Demo: Compose a styled "scripting dictionary" for makeParagraphStyle()'s record parameter and paste it into a new TextEdit document. *)

use styledTextLib : script "Styled Text Lib" -- Assuming this is what the library file's been called.

on main()
	-- Make "paragraph style" script objects for the various headline styles to be used.
	tell styledTextLib
		set headlineStyle to makeParagraphStyle({capitalization:"title small caps", |font name|:"Verdana", |bold|:true, |font size|:19.4, alignment:"center", |space before|:12.0, |space after|:12.0, |color|:{65535, 0, 0}, |underline|:"double"})
		set headlineInsertStyle to makeParagraphStyle({|font name|:"Verdana", |bold|:true, |italic|:true, |font size|:19.4, |underline|:"double", |underline color|:{65535, 0, 0}})
		set subheadlineStyle to makeParagraphStyle({|font name|:"Verdana", |italic|:true, |font size|:12.0, alignment:"centre", |space after|:24.0})
		set typeDefHeaderStyle to makeParagraphStyle({|font name|:"Verdana", |bold|:true, |font size|:14.0, |space before|:12.0, |space after|:4.0})
	end tell
	
	-- The remaining style — for the actual descriptions — has more (and more complex) settings, which are pulled out here for dissection:    
	-- The default interval between consecutively set tab stops (in inches, since that's the default when |indent units| isn't specified).
	set defaultTabInterval to 0.4
	-- Three default-alignment ("natural") tab stops at the interval just set. Ways of setting this range from as brief as {3} to as full as {{indent:0.4, alignment:"natural"}, {indent:0.8, alignment:"natural"},{indent:1.2, alignment:"natural"}}.
	set tabStopList to {{indent:0.4}, 2} -- Here the first tab column's set explicitly and two more follow at the interval set previously.
	-- A spot effect to embolden property names (between and including vertical bars).
	set boldPropertyLabels to {regex:"\\|[^\\|]++\\|", attributes:{|bold|:true}}
	-- A spot effect to colour the phrases "color", "spot effect", and "tab stop" blue where they appear as value types.
	set bluePseudolinks to {regex:"((?<=\\()color|(?<=\\(list of )(?:spot effect|tab stop))", attributes:{|color|:{0, 0, 65535}}}
	-- A similar effect to colour them blue and embolden them in type definitions.
	set bluePseudolinkDestinations to {regex:"^(?:color|spot effect|tab stop)", attributes:{|color|:{0, 0, 65535}, |bold|:true}}
	-- A spot effect to italicise the Latin abbreviations "eg.", "ie.", and "etc."
	set italicLatinAbbreviations to {regex:"\\b(?:eg|ie|etc)\\.", attributes:{|italic|:true}}
	
	-- Tell the library to create this last paragraph style and to intitalise a styled text for output.
	tell styledTextLib
		set parameterDescriptionStyle to makeParagraphStyle({|font name|:"Verdana", |font size|:12.0, |left indent|:0.4, |space before|:3.0, |space after|:3.0, |default tab interval|:defaultTabInterval, |tab stops|:tabStopList, |spot FX|:{boldPropertyLabels, bluePseudolinks, bluePseudolinkDestinations, italicLatinAbbreviations}})
		
		startNewStyledText()
	end tell
	
	-- Tell the various paragraph styles to apply themselves to the appropriate texts and to append the results to the output.
	tell headlineStyle to styleAndAppend(linefeed & "Properties that can be set in ") -- linefeed in front so that the style's |space before| setting gets used.
	tell headlineInsertStyle to styleAndAppend("styleAndAppend()'s")
	tell headlineStyle to styleAndAppend(" record parameter" & linefeed)
	tell subheadlineStyle to styleAndAppend("(All optional and covered by defaults. All labels are shown barred, but one or two may not need to be, depending on the compiling environment.)" & linefeed)
	
	set parameterDescription to "|alignment| (\"left\"/\"center\"/\"centre\"/\"justify\"/\"right\"/\"natural\") : Horizontal alignment. Default: \"natural\" (left or right according to set writing direction set on machine).
|bold| (boolean) : Use bold font. Default: false. If true and no bold version of the specified font, Helvetica Neue is used.
|capitalization| (boolean/\"all caps\"/\"small caps\"/\"title\"/\"all lower\") : Capitalise text. true = \"all caps\". false = no active capitalisation. \"small caps\" and \"title\" can be used in the same string for a combined effect. Default: false. Lower-case Eeszetts are not capitalised.
|character background color| (color or missing value) : Background colour. Default: missing value (for white).
|color| (color or missing value) : Text colour. Default: missing value (for black).
|default tab interval| (real) : Default distance between consecutive tab stops in units set with |indent units|. Default: 1.0.
|first line indent| (real) : Indent of each paragraph's first line from the document's leading margin in units set with |indent units|. Default: 0.0.
|font name| (text) : Font family. Default: \"Helvetica Neue\".
|font size| (real) : Font size in points. Default: 12.0.
|indent units| (\"in\"/\"inches\"/\"cm\"/\"centimeters\"/\"centimetres\"/\"pt\"/\"points\") : Units in which paragraph and tab indent settings are specified.  Default: \"inches\".
|italic| (boolean) : Use italic font. Default: false. If true and no italic version of the specified font, Helvetica Neue is used.
|left indent| (real) : Indent of all but the first line of each paragraph from document left margin in units set with |indent units|. Default: 0.0. If the receiving machine's configured for right-to-left text, |right to left| must be set to true.
|line spacing| (real) : Line spacing within paragraphs. Value interpreted according to |line spacing type| setting. Defaults: 1.0 with \"relative\", 0.0 otherwise.
|line spacing type| (\"relative\"/\"at least\"/\"min\"/\"minimum\"/\"at most\"/\"max\"/\"maximum\"/anything beginning with \"exact\"/anything ending with \"between\") : How |line spacing| is interpreted:
    \"relative\" (the default type) : |line spacing| is a multiplier to be applied to the normal line height for the font. For double-spacing, the |line spacing| value may be \"double\" instead of 2.0 if preferred.
    \"at least\"/\"min\"/\"minimum\" : |line spacing| is minimum line height in points.
    \"at most\"/\"max\"/\"maximum\" : |line spacing| is maximum line height in points. |line spacing| = 0.0 means no limit.
    \"exact…\" : |line spacing| is a fixed line height in points. |line spacing| = 0.0 means no limit.
    \"…between\" : |line spacing| is a gap to be inserted between lines in points.
|outline| (boolean/real) : Use outline font style. true and false work as in Pages ’09, the equivalent reals being 1.0 and 0.0. But a typical setting is 3.0 according to the Xcode documentation. |outline| is actually a stroke-width setting and negative values can be used to thicken the text without outlining it. Default: false. 
|right indent| (positive real) : Indent of each paragraph from document right margin in units set with |indent units|. Default: 0.0.  If the receiving machine's configured for right-to-left text, |right to left| must be set to true.
|right to left| (boolean) : Is the output is intended for display on a machine configured for right-to-left text? Default: false. This setting maps |left indent| and |right indent| to the appropriate head and tail indents used by the system.
|space after| (real) : Additional space below each paragraph in points. Default: 0.0.
|space before| (real) : Additional space above each paragraph in points. Default: 0.0.
|spot FX| (list of spot effect) : Character style variations to be applied within the paragraph style. Default: {}.
|strikethrough| (boolean/text) : Use strikethrough. true = \"single\". false = no strikethrough. Default: false.
    A text parameter can contain up to three components:
        A stroke style: \"single\", \"double\", or \"thick\"
        A stroke pattern: \"dash\", \"dot\", \"dash dot\", or \"dash dot dot\"
        A 'skip white space' indicator: \"by word\"
    eg. \"double\", \"single, dot, by word\", \"thick dash\", etc.
    A stroke style without a pattern implies continuous strikethrough. A pattern or \"by word\" without a style implies \"single\".
|strikethrough color| (color or missing value) : Strikethrough colour. Default: missing value (for same colour as struck-through text).
|tab stops| (list of tab stop) : Tab stops to be applied within the style. Default: {}.
|underline| (boolean/text) : Use underline. Description as for |strikethrough|. 
|underline color| (color or missing value) : Underline colour. Default: missing value (for same colour as underlined text)."
	tell parameterDescriptionStyle to styleAndAppend(parameterDescription & linefeed)
	
	tell typeDefHeaderStyle to styleAndAppend("Type definitions:" & linefeed)
	set typeDefinitions to "color : List of three integers from 0 to 65535 representing RGB component values. Since the styled text is output as RTF data, which only grades colour components into 255 different shades, the AS numbers will have been rounded off to multiples of 256 by the time they're checked in the receiving document. But this happens anyway when an application saves an RTF document and then reopens it. The difference is barely noticeable.
spot effect : Record with the following properties, both required:
    |regex| (text) : ICU-standard regular expression ('^' & '$' matching the starts and ends of lines) describing the text to be effected.
    |attributes| (record) : Effect properties which differ from the style properties. They can be any of:
         |bold|, |character background color|, |color|, |font name|, |font size|, |italic|, |outline|, |strikethrough|, |strikethrough color|, |underline|, |underline color|.
    All are defined in the same way as the main style equivalents, but the defaults are the values set for the main style. Additionally, there may be these effect-only settings:
        |subscript| (boolean) : Decrease font size and lower baseline. Default: false.
        |superscript| (boolean) : Decrease font size and raise the baseline. (Takes priority if subscript also set!) Default: false.
        |baseline shift| (real) : Raise (positive) or lower (negative) the text by so many points. Default: 0.0.
tab stop : Either a record or an integer. A record may have the following properties, both optional:
    |indent| (real) : The tab stop's offset from the document's leading margin (not necessarily its position on the ruler) in units set with |indent units|. Default: |default tab interval| units after previously set tab stop indent.
    |alignment| (\"left\"/\"right\"/\"center\"/\"centre\"/\"decimal\"/\"natural\") : How text is aligned to the tab stop. Default: same as for previously set tab stop (\"natural\" if none).
    An empty record repeats the previously set tab stop (or the default tab stop if none) at the |default tab interval| distance.
    An integer repeats the previously set tab stop the specified number of times at |default tab interval| intervals."
	tell parameterDescriptionStyle to styleAndAppend(typeDefinitions)
	
	-- Create a TextEdit document and set it for rich text wrapped to window.
	tell application "TextEdit"
		activate
		make new document
		set {l, t, w, h} to bounds of window 1
		set bounds of window 1 to {10, t, 816, h}
	end tell
	using terms from scripting additions
		set TEDefaults to (do shell script "defaults read com.apple.TextEdit")
	end using terms from
	tell application "System Events"
		set frontmost of application process "TextEdit" to true
		if (TEDefaults contains "RichText = 0;") then keystroke "t" using {command down, shift down}
		if (TEDefaults contains "ShowPageBreaks = 1;") then keystroke "w" using {command down, shift down}
	end tell
	
	-- Put the styled text onto the clipboard and paste it into the document.
	tell styledTextLib
		putResultOnClipboard()
		paste()
		(* Other options:
        using terms from scripting additions
            saveResultAsRTF({|file|:(path to desktop as text) & "Styled Text Lib makeParagraphStyle() parameter description.rtf", replacing:true})
        end using terms from
        set theStyledText to its styledTextResult()
        set theText to its textResult()
        *)
	end tell
end main

main()

Edits: Demo script rearranged and now pastes the styled result into a TextEdit document.
Reposted 17th March 2019 to correct text encoding issues introduced into the post by a MacScripter BBS software update.
29th November 2024: Split off into a separate post to get round a size limit imposed by MacScripter’s switch to Discourse software.