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