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 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 containing the data for a particular style. The one parameter is a record specifying the features to include which aren’t the defaults. So it may be anything from an empty record to a morass of nested records and lists.
styleAndAppend() – Apply styling to some text and append the result to the output. The parameter is the AppleScript text to be styled. This handler’s inherited by the “paragraph style” objects and should only be called as belonging to one of them, not to the library itself. eg.:

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

Through the magic of inheritance, the handler uses the properties of the script object used to call it.
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.
And 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 whose value’s a boolean. Where |replacing| isn’t specified, it’s ‘false’.
styledTextResult() – Return the styled text (an NSMutableAttributedString) to the calling script. No parameters.
textResult() – Return plain AppleScript text to the calling script. No parameters.

So the modus operandi is:
Initialise the output at some point before the first section of text is appended to it.
Create each “paragraph style” at some point before its first use.
Append text to the output in sequence, using the appropriate style(s).
Use one or more of the other options when the text is complete.

The only other thing one has to know is what to put in makeParagraphStyle()'s record parameter. The demo script at the bottom of this post creates a styled document in TextEdit which explains all. :slight_smile:

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.
Uses ASObjC.

In use:
	Tell the library to startNewStyledText().
	For each paragraph style required, tell the library to makeParagraphStyle(), passing a record with the details. The handler will return a script object set up for the application of that style. This can be done at any point before the style's first used.
	Append each style run [block of text in a particular style] by telling the relevant script object (not the library directly) to styleAndAppend() the relevant AppleScript text or NSString.
	Options when the styled text's complete:
		Put it on the clipboard by telling the library to putResultOnClipboard(). The previous clipboard contents are stored in a property.
		Paste from the clipboard into a document by telling the library to paste(). The clipboard contents are restored from the property afterwards. It's the calling script's responsibility to ensure that the receiving document's ready. The receiving application must paste in response to command-v and must understand and implement the pasted RTF data.
		Save the styled text as an RTF file by telling the library to saveResultAsRTF(), passing the destination details.
		Get the library's styledTextResult() (an NSMutableAttributedString).
		Get the library's textResult() (plain 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.3"

property |⌘| : current application
property styledText : missing value -- The styled output will be assembled here.
property storedClipboardContents : {} -- The current clipboard contents will be stored here.

(* Public handlers. *)

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

(* Apply a paragraph style's attributes to a block of text and append the result to 'styledText'.
		Input parameter: AS text or NSString.
		'my attributes' and 'my spotFX' refer through inheritance to properties of the particular 'paragraph style' script object owning the handler at the time. *)
on styleAndAppend(txt)
	-- Make an NSAttributedString with the passed text and the attributes of the executing style.
	set thisStyleRun to |⌘|'s class "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 thisStyleRun to thisStyleRun's mutableCopy()
		set txt to |⌘|'s class "NSString"'s stringWithString:(txt)
		set searchRange to {location:0, |length|:txt's |length|()}
		
		repeat with thisEffect in (my spotFX)
			set {regex:effectRegex, attributes:effectPrompts} to thisEffect
			-- Get the ranges in the NSMutableAttributedString where this effect is to be applied.
			set effectRanges to ((effectRegex's matchesInString:(txt) options:(0) range:(searchRange))'s valueForKey:("range"))
			-- Apply the effect to those ranges.
			if (effectPrompts's class is record) then -- If still a record, this is a capitalization effect.
				set capitalizing to effectPrompts's capitalization
				set theLocale to |⌘|'s class "NSLocale"'s currentLocale()
				repeat with i from (count effectRanges) to 1 by -1
					set thisRange to item i of effectRanges
					set thisSubstring to (txt's substringWithRange:(thisRange))
					if (capitalizing) then
						set capitalizedSubstring to (thisSubstring's uppercaseStringWithLocale:(theLocale))
					else
						set capitalizedSubstring to (thisSubstring's lowercaseStringWithLocale:(theLocale))
					end if
					tell thisStyleRun to replaceCharactersInRange:(thisRange) withString:(capitalizedSubstring)
				end repeat
			else -- effectPrompts is a list containing attribute names and/or an attribute dictionary.
				-- In each range, delete attributes with the loose names and/or add the attribute(s) in the dictionary.
				repeat with thisRange in effectRanges
					repeat with thisEntry in effectPrompts
						if ((thisEntry's isKindOfClass:(|⌘|'s class "NSString")) as boolean) then
							tell thisStyleRun to removeAttribute:(thisEntry) range:(thisRange)
						else
							tell thisStyleRun to addAttributes:(thisEntry) range:(thisRange)
						end if
					end repeat
				end repeat
			end if
		end repeat
	end if
	
	-- Append the result to the styled text so far.
	tell my styledText to appendAttributedString:(thisStyleRun)
end styleAndAppend

(* 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 types()
		repeat with j from 1 to (count theTypes)
			set thisType to item j of theTypes
			set theData to (thisPasteboardItem's dataForType:(thisType)) -- 's mutableCopy()
			if (theData is not missing value) then tell newPasteboardItem to setData:(theData) forType:(thisType)
		end repeat
		tell aMutableArray to addObject:(newPasteboardItem)
	end repeat
	set my storedClipboardContents to aMutableArray
	----------
	set newContents to |⌘|'s class "NSArray"'s arrayWithObject:(my styledText)
	tell thePasteboard
		its clearContents()
		its writeObjects:(newContents)
	end tell
end putResultOnClipboard

(* Issue 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 class "NSPasteboard"'s generalPasteboard()
		its clearContents()
		its writeObjects:(my storedClipboardContents)
	end tell
end paste

(* Return the styled text to the calling script in its NSMutableAttributedString form. (Introduced in version 1.0.3.) *)
on styledTextResult()
	return my styledText
end styledTextResult

(* Return the styled text to the calling script as plain AppleScript text. (Introduced in version 1.0.3.) *)
on textResult()
	return my styledText's |string|() as text
end textResult

(* Save the styled text as an RTF file. (Introduced in version 1.0.3.)
		Destination parameter: 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(fileOrPathOrNSURLOrRecord)
	-- The destination parameter may be a bookmark, alias, HFS path, POSIX path, or 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.
	set classOfDestinationParameter to class of fileOrPathOrNSURLOrRecord
	try
		-- Get the |file| and |replacing| values according to whether or not in record form.
		if (classOfDestinationParameter is record) then
			set {|file|:fileOrPathOrNSURL, replacing:replacing} to fileOrPathOrNSURLOrRecord & {|file|:missing value, replacing:false}
			if (class of replacing is not boolean) then error "Invalid replacing parameter"
			set classOfDestinationParameter to class of fileOrPathOrNSURL
		else
			set fileOrPathOrNSURL to fileOrPathOrNSURLOrRecord
			set replacing to false
		end if
		-- Try to derive an NSURL for the save destination.
		if (classOfDestinationParameter is text) then
			if (fileOrPathOrNSURL begins with "/") then
				set destinationURL to |⌘|'s class "NSURL"'s fileURLWithPath:(fileOrPathOrNSURL)
			else if (fileOrPathOrNSURL begins with "~/") then
				set destinationURL to |⌘|'s class "NSURL"'s fileURLWithPath:((|⌘|'s class "NSString"'s stringWithString:(fileOrPathOrNSURL))'s stringByExpandingTildeInPath())
			else if (fileOrPathOrNSURL contains ":") then
				set destinationURL to |⌘|'s class "NSURL"'s fileURLWithPath:(POSIX path of fileOrPathOrNSURL)
			else
				error "Path parameter format not recognised."
			end if
		else if ((classOfDestinationParameter is alias) or (classOfDestinationParameter is «class furl»)) then
			set destinationURL to |⌘|'s class "NSURL"'s fileURLWithPath:(POSIX path of fileOrPathOrNSURL)
		else
			try
				if ((fileOrPathOrNSURL's isKindOfClass:(|⌘|'s class "NSURL")) as boolean) then
					set destinationURL to fileOrPathOrNSURL
				else
					error
				end if
			on error
				error "Invalid file parameter."
			end try
		end if
		-- Check the URL's validity.
		if ((destinationURL's pathExtension()'s isEqualToString:("rtf")) as boolean) then -- Correct extension?
			set {itemExists, isFolder} to destinationURL's getResourceValue:(reference) forKey:(|⌘|'s NSURLIsDirectoryKey) |error|:(missing value)
			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 ((destinationURL's URLByDeletingLastPathComponent()'s checkResourceIsReachableAndReturnError:(missing value)) 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
	
	-- Convert the styled text to RTF data and write to the specified destination.
	tell my styledText to set RTFData to its RTFFromRange:({location:0, |length|:its |length|()}) documentAttributes:(missing value)
	tell RTFData to writeToURL:(destinationURL) atomically:(true)
end saveResultAsRTF

(* Initialise and return a script object implementing a particular 'paragraph style'.
		Specification parameter: AS record 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 makeTextTabArray() handler), |underline| (boolean/text as for |strikethrough|), |underline color| (AS color/missing value)} *)
on makeParagraphStyle(parameterRecord)
	-- Set variables to the individual property values, using defaults for any not specified.
	set defaults to {alignment:"natural", |bold|:false, capitalization:false, |character background color|:missing value, |color|:missing value, |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|:missing value, |strikethrough|:false, |tab stops|:{}, |underline color|:missing value, |underline|:false}
	set {alignment:textAlignment, |bold|:styleBold, capitalization:styleCapitalization, |character background color|:styleBackgroundColor, |color|:styleTextColor, |default tab interval|:defaultTabInterval, |first line indent|:firstLineIndent, |font name|:styleFontFamily, |font size|:styleFontSize, |indent units|:indentUnits, |italic|:styleItalic, |left indent|:leftIndent, |line spacing|:lineSpacing, |line spacing type|:lineSpacingType, |outline|:styleOutline, |right indent|:rightIndent, |right to left|:rightToLeft, |space after|:spaceAfter, |space before|:spaceBefore, |spot FX|:spotFX, |strikethrough color|:styleStrikethroughColor, |strikethrough|:styleStrikethrough, |tab stops|:tabStops, |underline color|:styleUnderlineColor, |underline|:styleUnderline} to parameterRecord & defaults
	
	-- Set a multiplier for converting tab or indent parameters to points.
	if ((indentUnits is "cm") or (indentUnits is "centimeters") or (indentUnits is "centimetres")) then
		set ppu to 28.346456692913
	else if ((indentUnits is "pt") or (indentUnits is "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
	
	-- Initialise the script object to be returned.
	script paragraphStyle
		property attributes : missing value
		property spotFX : missing value
	end script
	
	-- Initialise a dictionary which will become the script object's 'attributes' value.
	set styleAttributes to |⌘|'s class "NSMutableDictionary"'s new()
	
	-- Add an entry to the dictionary for an NSFont with the required attributes.
	set theNSFont to makeFontObject(styleBold, styleItalic, styleFontSize, styleFontFamily)
	tell styleAttributes to setValue:(theNSFont) forKey:(|⌘|'s NSFontAttributeName)
	
	-- Set up and add an entry to the dictionary for an NSMutableParagraphStyle.
	set theNSParagraphStyle to |⌘|'s class "NSMutableParagraphStyle"'s new()
	tell theNSParagraphStyle
		-- Tab stops.
		its setTabStops:(my makeTextTabArray(tabStops, defaultTabInterval, ppu))
		-- Text alignment.
		if (textAlignment is "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 is "relative") then
			-- Relative line spacing can be specified as "double" or a multiplier. Anything else is treated as "single".
			if (lineSpacing is "double") then set lineSpacing to 2.0
			if not ((lineSpacing's class is real) or (lineSpacing's class is integer)) then set lineSpacing to 1.0
			if (lineSpacing is not 1.0) then its setLineHeightMultiple:(lineSpacing as real)
		else if (lineSpacing > 0.0) then
			if ((lineSpacingType is "at least") or (lineSpacingType begins with "min")) then
				its setMinimumLineHeight:(lineSpacing as real)
			else if ((lineSpacingType is "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) -- Positive in Pages ’09, 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
	tell styleAttributes to setValue:(theNSParagraphStyle) forKey:(|⌘|'s NSParagraphStyleAttributeName)
	
	-- Other attributes for the dictionary:
	-- Underline style.
	set styleUnderlineAttributes to getUnderlineAttributes(styleUnderline)
	if (styleUnderlineAttributes > 0) then tell styleAttributes to setValue:(styleUnderlineAttributes) forKey:(|⌘|'s NSUnderlineStyleAttributeName)
	-- Strikethrough style.
	set styleStrikethoughAttributes to getUnderlineAttributes(styleStrikethrough)
	if (styleStrikethoughAttributes > 0) then tell styleAttributes to setValue:(styleStrikethoughAttributes) forKey:(|⌘|'s NSStrikethroughStyleAttributeName)
	-- Outline.
	if (styleOutline's class is boolean) then set styleOutline to styleOutline as integer
	if (styleOutline is not 0.0) then tell styleAttributes to setValue:(styleOutline as real) forKey:(|⌘|'s NSStrokeWidthAttributeName)
	-- Text colour.
	if (styleTextColor's class is list) then tell styleAttributes to setValue:(my makeColor(styleTextColor)) forKey:(|⌘|'s NSForegroundColorAttributeName)
	-- Background colour.
	if (styleBackgroundColor's class is list) then tell styleAttributes to setValue:(my makeColor(styleBackgroundColor)) forKey:(|⌘|'s NSBackgroundColorAttributeName)
	-- Underline colour.
	if (styleUnderlineColor's class is list) then tell styleAttributes to setValue:(my makeColor(styleUnderlineColor)) forKey:(|⌘|'s NSUnderlineColorAttributeName)
	-- Strikethrough colour.
	if (styleStrikethroughColor's class is list) then tell styleAttributes to setValue:(my makeColor(styleStrikethroughColor)) forKey:(|⌘|'s NSStrikethroughColorAttributeName)
	
	-- Set the script object's attributes property to the dictionary.
	set paragraphStyle's attributes to styleAttributes
	
	-- Capitalisation changes the text and is set up here as an effect so that the regexes for any spot effects can be run against the original text later. It has to be the _first_ effect in order not to interfere with other effects.
	if ((styleCapitalization is true) or (styleCapitalization is "all caps")) then
		-- The whole text except for any eszetts will be capitalised.
		set beginning of spotFX to {regex:"[^ß]++", attributes:{capitalization:true}}
	else if (styleCapitalization is "small caps") then
		-- All originally lower-case letters and all spaces will be rendered 1/5th smaller …
		set beginning of spotFX to {regex:"[[:lower:] ]++", attributes:{|font size|:styleFontSize * 0.8}}
		-- … after the whole text except for any eszetts is capitalised.
		set beginning of spotFX to {regex:"[^ß]++", attributes:{capitalization:true}}
	else if (styleCapitalization is "title") then
		-- The initial letter at each word boundary will be capitalised.
		set beginning of spotFX to {regex:"\\b[[:alpha:]]", attributes:{capitalization:true}}
	else if ((styleCapitalization contains "small caps") and (styleCapitalization contains "title")) then
		-- All originally lower-case letters not at intitial word boundaries and all spaces will be rendered 1/5th smaller …
		set beginning of spotFX to {regex:"(\\B[[:lower:]]| )++", attributes:{|font size|:styleFontSize * 0.8}}
		-- … after the whole text except for any eszetts is capitalised.
		set beginning of spotFX to {regex:"[^ß]++", attributes:{capitalization:true}}
	else if (styleCapitalization is "all lower") then
		-- The entire text will be lower-cased.
		set beginning of spotFX to {regex:"\\A.++\\Z", attributes:{capitalization:false}}
	end if
	
	-- Now for the spotFX value, which is a list of records, one for each, possibly compound effect. Each record has a 'regex' property with an ICU regex string and an 'attributes' property with a record similar to the main parameter for this handler. The possible attributes are a subset of the main ones plus |baseline shift| (real), |subscript| (boolean), and |superscript| (boolean). The first one or two effects may be capitalisation effects set above. Only their regexes are processed here.
	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|:styleBold, |character background color|:styleBackgroundColor, |color|:styleTextColor, |font name|:styleFontFamily, |font size|:styleFontSize, |italic|:styleItalic, |outline|:styleOutline, |strikethrough color|:styleStrikethroughColor, |strikethrough|:styleStrikethrough, |subscript|:false, |superscript|:false, |underline color|:styleUnderlineColor, |underline|:styleUnderline}
		
		-- Replace the values in each record with forms convenient for styleAndAppend().
		repeat with thisEffect in spotFX
			-- Replace the regex string with an NSRegularExpression.
			set thisEffect's regex to (|⌘|'s class "NSRegularExpression"'s regularExpressionWithPattern:(thisEffect's regex) options:(|⌘|'s NSRegularExpressionAnchorsMatchLines) |error|:(missing value))
			
			-- If this isn't 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 (capitalization of ((thisEffect's attributes) & {capitalization:missing value}) is missing value) then
				-- Set variables to the effect's specified values or to the defaults.
				set {|baseline shift|:effectBaselineShift, |bold|:effectBold, |character background color|:effectBackgroundColor, |color|:effectTextColor, |font name|:effectFontFamily, |font size|:effectFontSize, |italic|:effectItalic, |outline|:effectOutline, |strikethrough color|:effectStrikethroughColor, |strikethrough|:effectStrikethrough, |subscript|:effectSubscript, |superscript|:effectSuperscript, |underline color|:effectUnderlineColor, |underline|:effectUnderline} to (thisEffect's attributes) & defaults
				
				-- Initialise the list and the dictionary.
				set effectPrompts to {}
				set effectAttributes to |⌘|'s class "NSMutableDictionary"'s new()
				
				-- If any of the effect's attributes are different from the style's, or are effect-only, append entries for them:
				-- Font.
				if not ((effectBold = styleBold) and (effectItalic = styleItalic) and (effectFontSize = styleFontSize) and (effectFontFamily = styleFontFamily)) then tell effectAttributes to setValue:(my makeFontObject(effectBold, effectItalic, effectFontSize, effectFontFamily)) forKey:(|⌘|'s NSFontAttributeName)
				-- Underline.
				if (effectUnderline is not styleUnderline) then
					if (effectUnderline is false) then
						set end of effectPrompts to |⌘|'s NSUnderlineStyleAttributeName
					else
						tell effectAttributes to setValue:(my getUnderlineAttributes(effectUnderline)) forKey:(|⌘|'s NSUnderlineStyleAttributeName)
					end if
				end if
				-- Strikethrough.
				if (effectStrikethrough is not styleStrikethrough) then
					if (effectStrikethrough is false) then
						set end of effectPrompts to |⌘|'s NSStrikethroughStyleAttributeName
					else
						tell effectAttributes to setValue:(my getUnderlineAttributes(effectStrikethrough)) forKey:(|⌘|'s NSStrikethroughStyleAttributeName)
					end if
				end if
				-- Outline.
				if (effectOutline's class is boolean) then set effectOutline to effectOutline as integer
				if (effectOutline is not styleOutline) then
					if (effectOutline is 0) then
						set end of effectPrompts to |⌘|'s NSStrokeWidthAttributeName
					else
						tell effectAttributes to setValue:(effectOutline as real) forKey:(|⌘|'s NSStrokeWidthAttributeName)
					end if
				end if
				-- Text colour.
				if (effectTextColor is not styleTextColor) then
					if (effectTextColor is missing value) then
						set end of effectPrompts to |⌘|'s NSForegroundColorAttributeName
					else
						tell effectAttributes to setValue:(my makeColor(effectTextColor)) forKey:(|⌘|'s NSForegroundColorAttributeName)
					end if
				end if
				-- Background colour.
				if (effectBackgroundColor is not styleBackgroundColor) then
					if (effectBackgroundColor is missing value) then
						set end of effectPrompts to |⌘|'s NSBackgroundColorAttributeName
					else
						tell effectAttributes to setValue:(my makeColor(effectBackgroundColor)) forKey:(|⌘|'s NSBackroundColorAttributeName)
					end if
				end if
				-- Underline colour.
				if (effectUnderlineColor is not styleUnderlineColor) then
					if (effectUnderlineColor is missing value) then
						set end of effectPrompts to |⌘|'s NSUnderlineColorAttributeName
					else
						tell effectAttributes to setValue:(my makeColor(effectUnderlineColor)) forKey:(|⌘|'s NSUnderlineColorAttributeName)
					end if
				end if
				-- Strikethrough colour.
				if (effectStrikethroughColor is not styleStrikethroughColor) then
					if (effectStrikethroughColor is missing value) then
						set end of effectPrompts to |⌘|'s NSStrikethroughColorAttributeName
					else
						tell effectAttributes to setValue:(my makeColor(effectStrikethroughColor)) forKey:(|⌘|'s NSStrikethroughColorAttributeName)
					end if
				end if
				
				-- Superscript or subscript (effect only), giving priority to superscript if both!
				if (effectSuperscript) then
					tell effectAttributes to setValue:(1) forKey:(|⌘|'s NSSuperscriptAttributeName)
				else if (effectSubscript) then
					tell effectAttributes to setValue:(-1) forKey:(|⌘|'s NSSuperscriptAttributeName)
				end if
				-- Baseline shift (effect only).
				if (effectBaselineShift is not 0.0) then tell effectAttributes to setValue:(effectBaselineShift) forKey:(|⌘|'s NSBaselineOffsetAttributeName)
				
				-- Append the attribute dictionary to the list if its not empty (although it doesn't really matter if it is).
				if (effectAttributes's |count|() > 0) then set end of effectPrompts to effectAttributes
				-- Replace the effect record's original 'attibutes' value with the list just created.
				set thisEffect's attributes to effectPrompts
			end if
		end repeat
	end if
	-- Assign the spotFX list (even if it's empty) to the paragraph style script's 'spotFX' property.
	set paragraphStyle's spotFX to spotFX
	
	return paragraphStyle
end makeParagraphStyle

(* PRIVATE HANDLERS *)

(* Make a font object with given attributes. *)
on makeFontObject(|bold|, |italic|, fontSize, fontFamily)
	set fontAttributes to {NSFontFamilyAttribute:fontFamily}
	if (|bold|) then
		set fontFace to "bold"
		if (|italic|) then set fontFace to "bold italic"
		set fontAttributes to fontAttributes & {NSFontFaceAttribute:fontFace}
	else if (|italic|) then
		set fontAttributes to fontAttributes & {NSFontFaceAttribute:"Italic"}
	end if
	set fontDescriptor to |⌘|'s class "NSFontDescriptor"'s fontDescriptorWithFontAttributes:(fontAttributes)
	set theFont to |⌘|'s class "NSFont"'s fontWithDescriptor:(fontDescriptor) |size|:(fontSize as real)
	-- If the specified font can't be found, use Helvetica Neue instead.
	if (theFont is missing value) then set theFont to makeFontObject(|bold|, |italic|, fontSize, "Helvetica Neue")
	
	return theFont
end makeFontObject

(* Convert a |tab stops| list to an array of NSTextTabs.
		Items in the list can be either records or integers. A record specifies a tab stop using optional properties: {indent (real, in 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 makeTextTabArray(tabStops, defaultTabInterval, ppu)
	-- Initial "previous" settings.
	set previousTabIndent to 0.0
	set previousTabAlignment to "natural"
	-- Empty dictionary for most types of tab stop.
	set blankDictionary to |⌘|'s class "NSDictionary"'s new()
	-- Terminator dictionary for decimal tab stops in the current locale.
	set currentLocale to |⌘|'s class "NSLocale"'s currentLocale()
	set columnTerminators to (|⌘|'s class "NSTextTab"'s columnTerminatorsForLocale:(currentLocale))
	set terminatorDictionary to (|⌘|'s class "NSDictionary"'s dictionaryWithObject:(columnTerminators) forKey:(|⌘|'s NSTabColumnTerminatorsAttributeName))
	-- The output array.
	set tabStopArray to |⌘|'s class "NSMutableArray"'s new()
	
	repeat with thisEntry in tabStops
		set classOfEntry to thisEntry's class
		if ((classOfEntry is record) or (thisEntry's contents is {})) then
			-- Record parameter. Set one tab stop with the specified or default properties.
			set n to 1
		else if ((classOfEntry is integer) or (classOfEntry is real)) then
			-- Number parameter. Make that number of stops at the default interval with the same properties as the previous tab stop (defaults if none).
			set n to thisEntry as integer
			set thisEntry to {}
		else -- Bad parameter.
			set n to 0
		end if
		repeat n times
			-- If the indent's not specified, use the previous indent + the default interval. If the alignment's not specified, use the previous alignment.
			set defaults to {indent:(previousTabIndent + defaultTabInterval), alignment:previousTabAlignment}
			set {indent:tabIndent, alignment:tabAlignment} to thisEntry & defaults
			
			set previousTabIndent to tabIndent
			set previousTabAlignment to tabAlignment
			
			set tabIndent to tabIndent * ppu
			if (tabAlignment is "decimal") then
				-- The Xcode documentation says to use right alignment with the terminators, but natural alignment's what works.
				set tabAlignment to |⌘|'s NSTextAlignmentNatural
				set appropriateDictionary to terminatorDictionary
			else -- No terminators needed.
				set tabAlignment to getAlignmentKey(tabAlignment)
				set appropriateDictionary to blankDictionary
			end if
			tell tabStopArray to addObject:(|⌘|'s class "NSTextTab"'s alloc()'s initWithTextAlignment:(tabAlignment) location:(tabIndent) options:(appropriateDictionary))
		end repeat
	end repeat
	
	return tabStopArray
end makeTextTabArray

(* Return the system value corresponding to an alignment parameter. *)
on getAlignmentKey(alignmentParameter)
	-- "justified" and "decimal" alignments are specific to paragraph style and tabs respectively and are dealt with in those sections.
	if (alignmentParameter is "left") then
		set alignmentKey to |⌘|'s NSTextAlignmentLeft
	else if (alignmentParameter is "right") then
		set alignmentKey to |⌘|'s NSTextAlignmentRight
	else if ((alignmentParameter is "center") or (alignmentParameter is "centre")) then
		set alignmentKey to |⌘|'s NSTextAlignmentCenter
	else -- Assume "natural" (left or right according to locale). The default.
		set alignmentKey to |⌘|'s NSTextAlignmentNatural
	end if
	
	return alignmentKey
end getAlignmentKey

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

(* Make an NSColor from an AS color. *)
on makeColor({r, g, b})
	return |⌘|'s class "NSColor"'s colorWithCalibratedRed:(r / 65535) green:(g / 65535) blue:(b / 65535) alpha:(1.0)
end makeColor

Edits: Now uses Shane’s clipboard storage method (see following post), which is better.
saveResultAsRTF(), styledTextResult(), and textResult() handlers added and comments revised.

Demo script:

(* 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 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 are optional and covered by defaults. All the labels are shown barred here, but one or two of them may not need to be, depending on what else is installed.)" & linefeed)
	
	set parameterDescription to "|alignment| (\"left\"/\"center\"/\"centre\"/\"justify\"/\"right\"/\"natural\") : The horizontal alignment. Default: \"natural\" (left or right according to the writing direction on the machine).
|bold| (boolean) : Use a bold font. Default: false. If true and no bold version of the specified font is found, Helvetica Neue is used.
|capitalization| (boolean/\"all caps\"/\"small caps\"/\"title\"/\"all lower\") : Capitalise the 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 eszetts are not capitalised for practical and aesthetic reasons.
|character background color| (color or missing value) : The background colour. Default: missing value (for white).
|color| (color or missing value) : The foreground text colour. Default: missing value (for black).
|default tab interval| (real) : The default distance between consecutively set tab stops in the units set with |indent units|. Default: 1.0.
|first line indent| (real) : The indent of each paragraph's first line from the document's leading margin in the units set with |indent units|. Default: 0.0.
|font name| (text) : The name of the font family. Default: \"Helvetica Neue\".
|font size| (real) : The font size in points. Default: 12.0.
|indent units| (\"in\"/\"inches\"/\"cm\"/\"centimeters\"/\"centimetres\"/\"pt\"/\"points\") : The units in which paragraph and tab indent settings are specified.  Default: \"inches\".
|italic| (boolean) : Use an italic font. Default: false. If true and no italic version of the specified font is found, Helvetica Neue is used.
|left indent| (real) : The indent of all but the first line of each paragraph from the document's left margin in the 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) : The line spacing within paragraphs. The value is interpreted according to the |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 the |line spacing| parameter 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 the minimum line height in points.
    \"at most\"/\"max\"/\"maximum\" : |line spacing| is the maximum line height in points. |line spacing| = 0.0 means no limit.
    \"exact…\" : |line spacing| indicates 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 an 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) : The indent of each paragraph from the document's right margin in the 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) : Whether or not 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 a strikethrough font style. 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) : The strikethrough colour. Default: missing value (for the same colour as the struck-through text).
|tab stops| (list of tab stop) : The tab stops to be applied within the style. Default: {}.
|underline| (boolean/text) : Use an underline font style. Description as for |strikethrough|. 
|underline color| (color or missing value) : The underline colour. Default: missing value (for the same colour as the underlined text)."
	tell parameterDescriptionStyle to styleAndAppend(parameterDescription & linefeed)
	
	tell typeDefHeaderStyle to styleAndAppend("Type definitions:" & linefeed)
	set typeDefinitions to "color : A list of three integers from 0 to 65535 representing the 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 257 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 : A record with the following properties, both required:
    |regex| (text) : An ICU-standard regular expression ('^' & '$' matching the starts and ends of lines) describing the text to be effected.
    |attributes| (record) : The 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 the font size and lower the baseline of the text. Default: false.
        |superscript| (boolean) : Decrease the font size and raise the baseline of the text. (Has priority if subscript is 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 the units set with |indent units|. Default: |default tab interval| units after the previously set tab stop indent.
    |alignment| (\"left\"/\"right\"/\"center\"/\"centre\"/\"decimal\"/\"natural\") : How text is aligned to the tab stop. Default: the same alignment as 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.

1 Like

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