Problem upcasing text, while preserving styles


#1

In an OmniOutliner Plug-In, I’ve coded this in an attempt to upcase text while preserving styling:

selected = selection.items
topicColumn = document.outline.outlineColumn
selected.forEach(function(item){
    textObj = item.valueForColumn(topicColumn)
    attrRanges = textObj.ranges(TextComponent.AttributeRuns)
    attrRanges.forEach(function(range){
        currStyle = textObj.styleForRange(range)
        oldText = textObj.textInRange(range).string
        console.info(oldText)
        newText = oldText.toUpperCase()
        newTextObj = new Text(newText,currStyle)
        textObj.replace(range,newTextObj)
    })
})

(I realize I’m inefficiently declaring a bunch of temporary variables I don’t need, but this version is written for utter clarity, over efficiency.)

The code does successfully upcase everything, but given this input, where the italic text is locally-applied styling…

  • This is a simple test.

…the result is…

  • THIS IS A SIMPLE TEST.

…with the italic styling of “simple” missing.

I can see in the console output that it processed 'This is a ', then 'simple', then ' test.', so it seems that using TextComponent.AttributeRuns is the right way to go, but I don’t understand why I’m not managing to “carry along” the existing styling using styleForRange().

Any thoughts?


#2

You’ll see the same behavior in Pages and TextEdit for the transformations. This isn’t a problem with your script, but it is the nature of text transformations in general. The text attributes applied to the first character of the string are used for text transformations.

If you need the local style attributes, I’m not sure there is an easy way to do that while using any of the transformations. You could try getting the styles before transformation and reapplying them at the end.


#3

Thanks very much for your response!

I don’t quite understand, however. Are you saying that in my currStyle = textObj.styleForRange(range) line, I’m not actually retrieving all the styling that exists in the range?

Or is the problem that my newTextObj = new Text(newText,currStyle) line isn’t effectively applying that styling to the new Text object?

And can you please explain what you mean when you say:

The text attributes applied to the first character of the string are used for text transformations.

My understanding is that I’m getting the current style that exists in each range, and applying it to every character of my replacement text.

But we may just be talking about different things :)


#4

I mean that if you select text in the application and apply a text transformation manually in any application, the text itself is replaced without retaining the locally applied text attributes. That is expected for how the feature works (in multiple applications on macOS).

Steps:

  1. Select your text that says “This is a simple test” and apply a transformation (in TextEdit, Pages, or OmniOutliner).
  2. Transform the selected text to UpperCase.
  3. Notice the italic is not kept. If you change the first character (the T in This) to italic, all of the characters will be in italic.

What I mean is UpperCase, and other text transformation features, do not carry along attributes in general. This is not a problem with your script or a bug, but a general limitation of text transformations.


#5

EDIT: I had a very wordy response posted, but I think I’ve found the root of my problem, so reworking this post.

I understand things, my code is grabbing the styled Text object that is the topic:

textObj = item.valueForColumn(topicColumn)

Then slicing it up not word-by-word, but by runs of attributes (“styling blocks”, effectively):

attrRanges = textObj.ranges(TextComponent.AttributeRuns)

Then grabbing the style for each of those ranges:

currStyle = textObj.styleForRange(range)

And I think my problem is here–I expect currStyle to be the style of a given block of text, but in every case, currStyle.locallyDefinedAtrributes is undefined.

I suspect my expectations are wrong here–can you (or anyone) help?


#6

I figured out a solution, but don’t understand why it is necessary.

Above, when I create my new Text object, I provide a style (currStyle)…

newTextObj = new Text(newText,currStyle)

…and then replace the old text in the range…

 textObj.replace(range,newTextObj)

…which, surprisingly (to me, anyway), results in unstyled replacement text.

Here’s my fix: if I then follow that up by re-applying that same currStyle style to that same range…

textObj.styleForRange(range).setStyle(currStyle)

…then everything is perfect.

For the record, the sample “Replace Matched Words” code near the bottom of https://omni-automation.com/omnioutliner/text-object.html has the same problem–it’s specifically noted there that in the sample code “…the replacement text object is styled using the same style as the text object it replaces.”. However, that code doesn’t preserve styling either, and is also fixed by the line of code I added above.

This sure seems like a bug in Text.replace().


#7

Based on what I’ve learned from this adventure, here’s sample code for modifying text (it currently upcases text, but is easily modified) in an outline without losing styling–I hope it helps someone!

targetItems = selection.items
targetItems.forEach(function(item){
	textObj = item.valueForColumn(document.outline.outlineColumn)
	textObj.ranges(TextComponent.AttributeRuns).reverse().forEach(function(range){
		origRangeStyle = textObj.styleForRange(range)
		textObj.replace(range,new Text(textObj.textInRange(range).string.toUpperCase(),origRangeStyle))
		// Next line reasserts same styling already asserted in line above; odd, but needed in OO 5.4.2
		textObj.styleForRange(range).setStyle(origRangeStyle)
	})
})

Note that the .reverse() isn’t actually needed in this case, since the length of replaced text never changes, but I’m leaving it in there for safety, since it isn’t very expensive to execute and may prevent frustration if someone (maybe me!) modifies this code for other use.

Though I still might be misunderstanding something, I’ve reported the failure of Text.replace() to successfully insert styled text in this case as a potential bug, so that last line may not be necessary in the future.