Retrieving (Font) Attribute Information from NSAttributedString

foundation
asobjc

#1

ASObjC is completely new to me and i’m lost …

I want to get a list of the font styles (bold, italic) and their position/ranges from a “NSAttributedString”.
With “TextEdit.app” (NSTextStorage) i would use “attribute runs” to get the string segments and the font info.
How can i “extract” these attribute infos from a “NSAttributedString”?


(Jim Underwood) #2

i’m sure Shane or Mark will be along soon to answer your question.
But, in general, a great aid to learning ASObjC is:
Everyday AppleScriptObjC, Third Edition, by Shane Stanley

I didn’t find anything in it about “NSAttributedString”, but there is lots of other really great stuff.


#3

just bought it today, but still struggling :wink:


(Shane Stanley) #4

It’s a bit of work. If you’re after a particular attribute, say just the font, here’s an example using some code I just posted elsewhere to build the attributed string:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "AppKit"
use scripting additions

-- classes, constants, and enums used
property NSMutableArray : a reference to current application's NSMutableArray
property NSForegroundColorAttributeName : a reference to current application's NSForegroundColorAttributeName
property NSFontAttributeName : a reference to current application's NSFontAttributeName
property NSUTF8StringEncoding : a reference to 4
property NSString : a reference to current application's NSString
property NSAttributedString : a reference to current application's NSAttributedString

set theText to "<font color=\"#FFF800\">We'll kick their ass.</font>
<font color=\"#00F400\"><i>You see?</i></font>"
set theText to NSString's stringWithString:theText
set theData to theText's dataUsingEncoding:NSUTF8StringEncoding
if theData = missing value then error (theError's localizedDescription() as text)
set theATS to NSAttributedString's alloc()'s initWithHTML:theData documentAttributes:(missing value)
if theATS is missing value then error "Could not interpret HTML"
set theLen to theATS's |length|()
set theIndex to 0
set theFonts to NSMutableArray's array()
set theStrings to NSMutableArray's array()
repeat
	set {theValue, theRange} to theATS's |attribute|:NSFontAttributeName atIndex:theIndex longestEffectiveRange:(reference) inRange:{theIndex, theLen - theIndex}
	set theIndex to current application's NSMaxRange(theRange)
	theFonts's addObject:(theValue's fontName())
	theStrings's addObject:(theATS's |string|()'s substringWithRange:theRange)
	if theIndex ≥ theLen then exit repeat
end repeat

If you’re after multiple attributes, you use a similar approach with the method -attributesAtIndex:longestEffectiveRange:inRange:. That will break the string whenever any attribute changes, and will return a dictionary of all the attributes. (The strings here should match what you see for TextEdit’s attribute runs.)


#5

Thank you so much, Shane!

Your book and this specific snippet really helps me!


#6

Here’s my first working version. Code and performance is far from good but at least it is working. :wink:

As i turned out i only need to retrieve certain attributes (html: b, i, sub, sup, u) as well as their combinations.
Source is always the pasteboard with RTF and/or HTML.

I still need some time to get deeper into ASObjC to avoid the “switching” and hopefully speed up the script.

Thanks again!

run script getStyleRanges

script getStyleRanges
	use framework "Foundation"
	use framework "AppKit"
	set thePB to current application's NSPasteboard's generalPasteboard()
	set thePBFormats to thePB's pasteboardItems()'s firstObject()'s |types|() as list
	if "public.rtf" is in thePBFormats then
		set theData to thePB's dataForType:(current application's NSPasteboardTypeRTF)
		set theATS to current application's NSAttributedString's alloc()'s initWithRTF:theData documentAttributes:(missing value)
	else if "public.html" is in thePBFormats then
		set theData to thePB's dataForType:(current application's NSPasteboardTypeHTML)
		set theATS to current application's NSAttributedString's alloc()'s initWithHTML:theData documentAttributes:(missing value)
	else
		return {}
	end if
	if theATS is missing value then return {}
	set theLen to theATS's |length|()
	set theIndex to 0
	set theRanges to {}
	set theFontStyles to {b:{"Bold", "BoldMT", "Bd"}, i:{"Italic", "ItalicMT", "It", "Oblique", "Slanted"}}
	repeat
		set theStyleTags to {}
		set {theValue, theRange} to theATS's attributesAtIndex:theIndex longestEffectiveRange:(reference) inRange:{theIndex, theLen - theIndex}
		set theIndex to current application's NSMaxRange(theRange)
		set theFontName to theValue's NSFont's fontName
		if (count words of (theFontName as text)) is 2 then
			repeat with i in b of theFontStyles
				set aFontStyle to (current application's NSString's stringWithString:i)
				if (theFontName's containsString:aFontStyle) then
					copy "/b" to end of theStyleTags
					exit repeat
				end if
			end repeat
			repeat with i in i of theFontStyles
				set aFontStyle to (current application's NSString's stringWithString:i)
				if (theFontName's containsString:aFontStyle) then
					copy "/i" to end of theStyleTags
					exit repeat
				end if
			end repeat
		end if
		try
			set theSuperScript to theValue's NSSuperScript as integer
			if theSuperScript > 0 then
				copy "/sup" to end of theStyleTags
			else if theSuperScript < 0 then
				copy "/sub" to end of theStyleTags
			end if
		end try
		try
			set theUnderline to theValue's NSUnderline as integer
			if theUnderline = 1 then copy "/u" to end of theStyleTags
		end try
		if theStyleTags is not {} then copy {theStyleTags as text, theRange} to end of theRanges
		if theIndex ≥ theLen then exit repeat
	end repeat
	return {theText:theATS's |string|() as text, theLen:theLen, theRanges:theRanges}
end script

(Jim Underwood) #7

Thanks for sharing your script.
What is the advantage of using the run script in this use case?