Text Objects
When the use of text formatting involves more than the use of level styles, and includes the application of character-level styling, text objects provide a mechanism for associating formatting data with text strings.
A text object is a container that holds text and related style information, as well as optional references to attachments, and even other text objects.
Creating Text Objects
To create a text object, instantiate an instance of the Text class by providing the text string and the assigned style object. In the example shown below, the base style for the document is used:
new Text(string:String, style:Style) ==> Text Object • Returns a new Text instance with the given string contents and Style applied to the entire range of text.
When the following script example is run in the automation console, the result is an object reference to the created text object followed by a properties record displaying the default property values of the created text object.
Create New Text Object
textObj = new Text('How Now Brown Cow', document.outline.baseStyle)
//--> [object Text] {attachments: [], attributeRuns: [[object Text]], characters: [[object Text], [object Text], [object Text], [object Text], [object Text], [object Text], [object Text], [object Text], [object Text], [object Text], …], end: [object Text.Position], fileWrapper: null, paragraphs: [[object Text]], range: [object Text.Range], sentences: [[object Text]], start: [object Text.Position], string: "How Now Brown Cow", style: [object Style], words: [[object Text], [object Text], [object Text], [object Text]]}
NOTE: as you can see in the resulting properties record, text objects may contain other text objects.
Instance Properties
As with most scriptable objects, a text object has properties whose values can be accessed. However, in the case of text objects, editing the value of a property will not change the displayed text. To change the display of text objects, the Text.Range class will be used as demonstrated later in this section.
attachments (Array of Text Objects r/o) • Returns an array of copies of the blocks of Text in the receiver that represent Attachments. Note that editing these instances will not change the original.
attributeRuns (Array of Text Objects r/o) • Returns an array of copies of the contiguous blocks of Text in the receiver that have the same style. Note that editing these instances will not change the original.
characters (Array of Text Objects r/o) • Returns an array of copies of the characters in the Text. Note that editing these instances will not change the original.
fileWrapper (FileWrapper or nil r/o) • Returns the attached file wrapper for the Text (or rather, the first character of the text), if any.
paragraphs (Array of Text Objects r/o) • Returns an array of copies of the paragraphs in the Text. Note that editing these instances will not change the original. Paragraphs, if ended by a newline, will contain the newline character.
range (Text.Range r/o) • Returns a Text.Range that spans the entire Text.
sentences (Array of Text Objects r/o) • Returns an array of copies of the sentences in the Text. Note that editing these instances will not change the original.
start (Text.Position r/o) • Returns a Text.Position indicating the beginning of the Text.
string (String) • Returns a plain String version of the characters in the Text. Note that since JavaScript represents Strings as Unicode code points, the length of the returned string may be differnt from the number of characters in the Text object.
style (Style) • Returns a Style instance for this Text object.
words (Array of Text Objects r/o) • Returns an array of copies of the words in the Text. Note that editing these instances will not change the original.
Using the string property of a Text Object and the JavaScript map() function to convert the arrays of returned text objects to arrays of text strings:
Converting Text Objects to String Arrays
textObj = new Text('How Now Brown Cow.', document.outline.baseStyle)
textObj.characters.map(txtObj => txtObj.string)
//--> ["H",
"o", "w", " ", "N", "o", "w", " ", "B", "r", "o", "w", "n", " ", "C", "o", "w", "."]
textObj.words.map(txtObj => txtObj.string)
//--> ["How","Now","Brown","Cow"]
textObj.sentences.map(txtObj => txtObj.string)
//--> ["How Now Brown Cow."]
textObj.paragraphs.map(txtObj => txtObj.string)
//--> ["How Now Brown Cow."]
Instance Functions
Omni Automation support in OmniOutliner includes a set of functions (commands) for manipulating text objects.
textInRange(range:Text.Range) ==> Text Object • Returns a copy of the text in the specified range.
styleForRange(range:Text.Range) ==> Style • Returns a Style instance for the given range of the Text.
ranges(component:TextComponent, useEnclosingRange:Boolean or nil) ==> Array of Text.Ranges • Returns an array of Text.Ranges for the specified component. If useEnclosingRange is true, than any extra characters that separate follow a component will be included in its range. Any extra characters before the first found component will be included in the first range.
replace(range:Text.Range, with:Text) • Replaces the sub-range of the receiving Text with a copy of the passed in Text (which remains unchanged).
append(text:Text Object) • Appends the given Text Object to the receiver.
insert(position:Text.Position, text:Text) • Inserts a copy of the given Text at the specified position in the receiver.
remove(range:Text.Range) • Removes the indicated sub-range of the receiving Text.
find(string:String, options:Array of Text.FindOption or null, range:Text.Range or nil) ==> Text.Range or nil • Finds an occurrence of string within the Text and returns the enclosing Text.Range if there is a match. If range is passed, only that portion of the Text is searched. The supplied options, if any, change how the search is performed based on their definition.
Here’s a simple example of editing a text object by prepending it with another text object assigned with the same style as the target text object. Note the use of the start and style text object properties, and the insert() instance function:
Prepend to Text Object
textObj = new Text('How Now Brown Cow', document.outline.baseStyle)
textObj.insert(textObj.start, new Text("Well ", textObj.style))
textObj.string
//--> "Well How Now Brown Cow"
And performing an append by using the end property to indicate insertion position:
Append to Text Object
textObj = new Text('How Now Brown Cow', document.outline.baseStyle)
textObj.insert(textObj.end, new Text(" Bough", textObj.style))
textObj.string
//--> "How Now Brown Cow Bough"
To manipulate the styled content of rows in an outline document, you must first get the text object for the row content by using the TreeNode class of the Editor class:
Prepend Styled Row
editor = document.editors[0]
topicColumn = document.outline.outlineColumn
node = editor.selection.nodes[0]
textObj = node.valueForColumn(topicColumn)
textObj.insert(textObj.start, new Text("[12345]", textObj.style))
When the text object of the row is altered, the changes will be reflected in the document:
As an alternate to using the insert() method, you can use the append() method, as in this example that appends a styled date tag to the end of every row, using the style applied to that row:
Append Date to All Styled Rows
topicColumn = document.outline.outlineColumn
rootItem.apply(item => {
if (item != rootItem){
textObj = item.valueForColumn(topicColumn)
appendStr = ' [' + new Date().toDateString() + ']'
appendObj = new Text(appendStr, textObj.style)
textObj.append(appendObj)
}
})
(see the next page for an example of prepending/appending text objects with a specified style)
Iteration Script Templates
Here are script templates for processing the text object content of the main column of rows.
To edit all of the styled rows in an outline document, or all of the selected rows in an outline document, you can use the apply() or forEach() methods with either the rootItem or rootNode objects since both Items and TreeNodes support the valueForColumn() function that returns a text object:
Iterating All Items (rows)
topicColumn = document.outline.outlineColumn
rootItem.apply(item => {
if (item !== rootItem){
textObj = item.valueForColumn(topicColumn)
// processing statements go here
}
})
Iterating All Nodes (rows)
topicColumn = document.outline.outlineColumn
editor = document.editors[0]
editor.rootNode.apply(node => {
if (!node.isRootNode){
textObj = node.valueForColumn(topicColumn)
// processing statements go here
}
})
Process Selected Items (rows)
topicColumn = document.outline.outlineColumn
editor = document.editors[0]
editor.selection.items.forEach(item => {
textObj = item.valueForColumn(topicColumn)
// processing statements go here
})
Process Selected Nodes (rows)
topicColumn = document.outline.outlineColumn
editor = document.editors[0]
editor.selection.nodes.forEach(node => {
textObj = node.valueForColumn(topicColumn)
// processing statements go here
})
TextComponent
The TextComponent class is used with the ranges() function to define the delimiter type for the results of the command. For example TextComponent.Words will return the ranges of the words contained in the searched text object.
Ranges for Word Text Objects
wordRanges = textObjToSearch.ranges(TextComponent.Words)
TextComponent Class Properties
Attachments (TextComponent r/o) • The ranges of Text which represent Attachments.
AttributeRuns (TextComponent r/o) • The ranges of Text which have the same attributes.
Characters (TextComponent r/o) • The individual characters of the Text. Note that some characters (like emoji) consist of multiple Unicode code points.
Paragraphs (TextComponent r/o) • The paragraphs of Text. Unlike other options, the line breaking characters that end the paragraph are included.
Sentences (TextComponent r/o) • The sentences of the Text.
Words (TextComponent r/o) • The words in the Text. Whitespace or other word break characters are not included.
all (Array of TextComponent r/o) • All of the Text Component types. This property is used in creating plug-in forms.
Text.Range Class
When working with text objects, you use ranges to describe sections within the source text object. You can create new ranges from a source text object, in order to extract data from the originating object.
new Text.Range(start:Text.Position, end:Text.Position) ==> Text Object • Returns a new Text instance with the given string contents and Style applied to the entire range of text.
For example, the following example creates a new text range from the beginning of the third word of the source text object to the end of the source text object, and then converts the new range into a string:
Getting a Section of a Text Object
textObj = new Text('How Now Brown Cow', document.outline.baseStyle)
wordRanges = textObj.ranges(TextComponent.Words)
sectionRange = new Text.Range(wordRanges[2].start, textObj.end)
textObj.textInRange(sectionRange).string
//--> "Brown Cow"
Text.Range Properties
Here are the properties of a range:
end (Text.Position r/o) • Returns the Text.Position for the end of the Text.Range
isEmpty (Boolean r/o) • Returns true if the Text.Range contains no characters.
start (Text.Position r/o) • Returns the Text.Position for the beginning of the Text.Range
Changing Style Attributes of Text Objects
Here’s an example demonstrating how to use text components and ranges to change a style attribute of all occurrences of a word:
Change Style Attribute (FontWeight) for All Occurrences of Word
wordToMatch = 'Brown'
topicColumn = document.outline.outlineColumn
rootItem.apply(item => {
if (item !== rootItem){
textObj = item.valueForColumn(topicColumn)
wordRanges = textObj.ranges(TextComponent.Words)
wordRanges.forEach(range => {
if(textObj.textInRange(range).string === wordToMatch){
txtObjStyle = textObj.styleForRange(range)
txtObjStyle.set(Style.Attribute.FontWeight, 9)
}
})
}
})
And another example of altering a style attribute of a text object by set the value of the link style attribute:
Change Style Attribute (Link) for All Occurrences of Word
wordToMatch = 'iPad'
url = URL.fromString('https://www.apple.com/ipad/')
topicColumn = document.outline.outlineColumn
rootItem.apply(item => {
if (item !== rootItem){
textObj = item.valueForColumn(topicColumn)
wordRanges = textObj.ranges(TextComponent.Words)
wordRanges.forEach(range => {
if(textObj.textInRange(range).string === wordToMatch){
txtObjStyle = textObj.styleForRange(range)
txtObjStyle.set(Style.Attribute.Link, url)
}
})
}
})
Replacing Text Objects
Here's a script demonstrating how use text components and ranges to replace the occurence of specific words with a specified replacement.
Since the script uses ranges, which define a text object’s offset in a container, the array of ranges must be reordered using the the JavaScript reverse() method (line 8), thereby processing from the end to the beginning, avoiding conflicts with the stored ranges of matched word occurrences.
Also note (line 10) that the replacement text object is styled using the same style as the text object it replaces.
Replacing Matched Words
wordToMatch = 'Brown'
rWord = 'Red'
topicColumn = document.outline.outlineColumn
rootItem.apply(item => {
if (item !== rootItem){
textObj = item.valueForColumn(topicColumn)
wordRanges = textObj.ranges(TextComponent.Words)
wordRanges.reverse().forEach(function(range){
if(textObj.textInRange(range).string === wordToMatch){
rObj = new Text(rWord, textObj.styleForRange(range))
textObj.replace(range, rObj)
}
})
}
})
Finding Text Strings
As shown above, text object components are used identify and locate specific words in an outline. To identify and locate phrases of words or text strings, the find() method is used.
find(string:String, options:Array of Text.FindOption or null, range:Text.Range or nil) ==> Text.Range or nil • Finds an occurrence of string within the Text and returns the enclosing Text.Range if there is a match. If range is passed, only that portion of the Text is searched. The supplied options, if any, change how the search is performed based on their definition.
Text.FindOption
Parameters that determine the manner in which a string search is performed, can be adjusted by including an array of FindOption properties as the value for the optional range parameter of the find() method. Here are the available properties:
Anchored (Text.FindOption r/o) • Matches must be anchored to the beginning (or end if Backwards is include) of the string or search range.
Backwards (Text.FindOption r/o) • Search starting from the end of the string or range.
CaseInsensitive (Text.FindOption r/o) • Compare upper and lower case characters as equal.
DiacriticInsensitive (Text.FindOption r/o) • Ignore diacritics. For example, “ö” is considered the same as “o”.
ForcedOrdering (Text.FindOption r/o) • Force an ordering between strings that aren’t strictly equal.
Literal (Text.FindOption r/o) • Perform exact character-by-character matching.
Numeric (Text.FindOption r/o) • Order numbers by numeric value, not lexigraphically. Only applies to ordered comparisons, not find operations.
RegularExpression (Text.FindOption r/o) • For find operations, the string is treated as an ICU-compatible regular expression. If set, no other options can be used except for CaseInsensitive and Anchored.
WidthInsensitive (Text.FindOption r/o) • Ignore width differences. For example, “a” is considered the same as ‘FULLWIDTH LATIN SMALL LETTER A’ (U+FF41).
For example, a script for finding the occurence of a string that begins (anchored) a searched range, would have a syntax like this:
The find() method with optional parameters
findParams = [Text.FindOption.Anchored, Text.FindOption.CaseInsensitive]
range = textObjToSearch.find(stringToFind, findParams, rangeToSearch)
Finding All Occurrences of a String within a Text Object
By default, the find() method returns a reference to the first occurence (if any) of the specified string. A null value is returned when no matches are found. In order to locate all occurrences of a specified string in a text object, a while loop is used with in conjunction with script statements that adjust the range of the searched text to start from the end of the last occurence (below lines 9-10).
Apply Styling to All Occurrences of a String
searchString = 'iPad Pro'
topicColumn = document.outline.outlineColumn
rootItem.apply(item => {
if (item !== rootItem){
textObj = item.valueForColumn(topicColumn)
range = textObj.find(searchString)
while (range !== null){
textObjStyle = textObj.styleForRange(range)
textObjStyle.set(Style.Attribute.FontWeight, 9)
searchRange = new Text.Range(range.end, textObj.end)
range = textObj.find(searchString, null, searchRange)
}
}
})
Combining Finding Techniques
OutOutliner outlines can provide sources of additional information by attaching web links to their contents, so that readers can learn more through a simple “TAP|CLICK” that automatically loads supporting content in a browser view.
To find and apply styling (and links) to both words and text strings, using the previous techniques of word and string searching is a powerful approach to use.
The following script does just that by searching for both words and strings to apply corresponding web links to the found items. In this example, all occurrences of Apple computing devices in the outline, are converted into “TAP|CLICK” links to corresponding web pages.
Combine Search Techniques
var searchStrings = [
{"device":"iPad Pro","link":"https://www.apple.com/ipad-pro/"}, {"device":"iPad mini","link":"https://www.apple.com/ipad-mini-4/"}, {"device":"MacBook Air","link":"https://www.apple.com/macbook-air/"}, {"device":"MacBook Pro","link":"https://www.apple.com/macbook-pro/"}, {"device":"Mac Pro","link":"https://www.apple.com/mac-pro/"}, {"device":"iMac Pro","link":"https://www.apple.com/imac-pro/"}, {"device":"Mac mini","link":"https://www.apple.com/mac-mini/"}]
var searchWords = [
{"device":"iPad","link":"https://www.apple.com/ipad-9.7/"},
{"device":"MacBook","link":"https://www.apple.com/macbook/"},
{"device":"iMac","link":"https://www.apple.com/imac/"}
]
var topicColumn = document.outline.outlineColumn
findOptions = [Text.FindOption.Literal, Text.FindOption.WidthInsensitive]
var textObj
rootItem.apply(item => {
if (item != rootItem){
textObj = item.valueForColumn(topicColumn)
//apply link to search words
searchWords.forEach(obj =>{
wordToMatch = obj.device
url = URL.fromString(obj.link)
wordRanges = textObj.ranges(TextComponent.Words)
wordRanges.forEach(range => {
if(textObj.textInRange(range).string === wordToMatch){
textObjStyle = textObj.styleForRange(range)
textObjStyle.set(Style.Attribute.Link, url)
textObjStyle.set(Style.Attribute.FontWeight, 9)
}
})
})
//apply link to search strings
searchStrings.forEach(obj => {
searchString = obj.device
url = URL.fromString(obj.link)
range = textObj.find(searchString,findOptions)
while (range !== null){
textObjStyle = textObj.styleForRange(range)
textObjStyle.set(Style.Attribute.Link, url)
textObjStyle.set(Style.Attribute.FontWeight, 9)
searchRange = new Text.Range(range.end, textObj.end)
range = textObj.find(searchString, findOptions, searchRange)
}
})
}
})
Here is a link to the Apply Links to All Apple Device Names script saved as an OmniOutliner solitary action: