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