diff --git a/Source/CFAttributedString.c b/Source/CFAttributedString.c index 699b334..89cd4b8 100644 --- a/Source/CFAttributedString.c +++ b/Source/CFAttributedString.c @@ -125,7 +125,14 @@ CFAttributedStringCacheAttribute (CFDictionaryRef attribs) cachedAttr = insert; CFRelease (insert); } - + else + { + /* Another run now references this cached attribute set; bump its bag + * count so it survives until every referencing run is uncached. + */ + CFBagAddValue (_kCFAttributedStringCache, cachedAttr); + } + GSMutexUnlock (&_kCFAttributedStringCacheLock); return cachedAttr; @@ -284,8 +291,8 @@ CFAttributedStringGetTypeID (void) return _kCFAttributedStringTypeID; } -#define CFATTRIBUTESTRING_SIZE sizeof(CFRuntimeClass) \ - - sizeof(struct __CFAttributedString) +#define CFATTRIBUTESTRING_SIZE (sizeof(struct __CFAttributedString) \ + - sizeof(CFRuntimeBase)) static CFAttributedStringRef CFAttributedStringCreateInlined (CFAllocatorRef alloc, CFStringRef str, @@ -301,12 +308,12 @@ CFAttributedStringCreateInlined (CFAllocatorRef alloc, CFStringRef str, CFIndex idx; new->_string = CFStringCreateCopy (alloc, str); - new->_attribCount = 1; + new->_attribCount = count; new->_attribs = (Attr*)&new[1]; - + for (idx = 0 ; idx < count ; ++idx) { - new->_attribs[idx].index = idx; + new->_attribs[idx].index = attribs[idx].index; new->_attribs[idx].attrib = CFAttributedStringCacheAttribute (attribs[idx].attrib); } @@ -348,32 +355,48 @@ CFAttributedStringCreateWithSubstring (CFAllocatorRef alloc, { CFAttributedStringRef new; CFMutableAttributedStringRef tmp; + CFStringRef substr; CFRange r; CFIndex cur; - + tmp = CFAttributedStringCreateMutable (alloc, range.length); - - /* We do not need to protect this with beging and end editing because + + /* We do not need to protect this with begin and end editing because * we will be adding attributes in the order they appear. Using - * the being/end editing functions would actually slow things down. + * the begin/end editing functions would actually slow things down. */ - CFAttributedStringReplaceString (tmp, range, - CFAttributedStringGetString (str)); - + substr = CFStringCreateWithSubstring (alloc, + CFAttributedStringGetString (str), + range); + CFAttributedStringReplaceString (tmp, CFRangeMake (0, 0), substr); + CFRelease (substr); + cur = range.location; do { CFDictionaryRef attribs; - + CFIndex start; + CFIndex end; + attribs = CFAttributedStringGetAttributes (str, cur, &r); - CFAttributedStringSetAttributes (tmp, r, attribs, true); - + + /* The effective range is in the source's coordinates and may reach + * past the requested range at either end. Clip it to range and shift + * it into the substring's coordinates. + */ + start = r.location < range.location ? range.location : r.location; + end = r.location + r.length; + if (end > range.location + range.length) + end = range.location + range.length; + CFAttributedStringSetAttributes (tmp, + CFRangeMake (start - range.location, end - start), attribs, true); + cur = r.location + r.length; - } while (cur < range.length); - + } while (cur < range.location + range.length); + new = CFAttributedStringCreateCopy (alloc, tmp); CFRelease (tmp); - + return new; } @@ -423,7 +446,48 @@ CFAttributedStringGetAttributeAndLongestEffectiveRange ( CFAttributedStringRef str, CFIndex loc, CFStringRef attrName, CFRange inRange, CFRange *longestEffRange) { - return NULL; /* FIXME */ + CFRange r; + CFTypeRef value; + CFIndex start; + CFIndex end; + CFIndex inEnd; + + value = CFAttributedStringGetAttribute (str, loc, attrName, &r); + start = r.location; + end = r.location + r.length; + inEnd = inRange.location + inRange.length; + + while (start > inRange.location) + { + CFRange pr; + CFTypeRef pv = CFAttributedStringGetAttribute (str, start - 1, attrName, + &pr); + if ((pv == NULL) != (value == NULL) + || (pv != NULL && !CFEqual (pv, value))) + break; + start = pr.location; + } + while (end < inEnd) + { + CFRange nr; + CFTypeRef nv = CFAttributedStringGetAttribute (str, end, attrName, &nr); + if ((nv == NULL) != (value == NULL) + || (nv != NULL && !CFEqual (nv, value))) + break; + end = nr.location + nr.length; + } + + if (start < inRange.location) + start = inRange.location; + if (end > inEnd) + end = inEnd; + if (longestEffRange) + { + longestEffRange->location = start; + longestEffRange->length = end - start; + } + + return value; } CFDictionaryRef @@ -431,7 +495,46 @@ CFAttributedStringGetAttributesAndLongestEffectiveRange ( CFAttributedStringRef str, CFIndex loc, CFRange inRange, CFRange *longestEffRange) { - return NULL; /* FIXME */ + CFRange r; + CFDictionaryRef attribs; + CFIndex start; + CFIndex end; + CFIndex inEnd; + + attribs = CFAttributedStringGetAttributes (str, loc, &r); + start = r.location; + end = r.location + r.length; + inEnd = inRange.location + inRange.length; + + while (start > inRange.location) + { + CFRange pr; + CFDictionaryRef pa = CFAttributedStringGetAttributes (str, start - 1, + &pr); + if (!CFEqual (pa, attribs)) + break; + start = pr.location; + } + while (end < inEnd) + { + CFRange nr; + CFDictionaryRef na = CFAttributedStringGetAttributes (str, end, &nr); + if (!CFEqual (na, attribs)) + break; + end = nr.location + nr.length; + } + + if (start < inRange.location) + start = inRange.location; + if (end > inEnd) + end = inEnd; + if (longestEffRange) + { + longestEffRange->location = start; + longestEffRange->length = end - start; + } + + return attribs; } @@ -451,12 +554,13 @@ InsertAttributesAtIndex (CFMutableAttributedStringRef str, CFIndex idx, if (working->_attribCount == working->_attribCap) { /* Grow */ + working->_attribCap <<= 1; working->_attribs = CFAllocatorReallocate (alloc, working->_attribs, - (working->_attribCap << 1), + sizeof(Attr) * working->_attribCap, 0); } - + /* Move things to the right */ stop = &working->_attribs[idx]; cur = &working->_attribs[working->_attribCount]; @@ -475,8 +579,7 @@ ReplaceAttributesAtIndex (CFMutableAttributedStringRef str, CFIndex idx, CFDictionaryRef repl) { CFAttributedStringUncacheAttribute (str->_attribs[idx].attrib); - str->_attribs[idx].attrib = - CFAttributedStringCacheAttribute (str->_attribs[idx].attrib); + str->_attribs[idx].attrib = CFAttributedStringCacheAttribute (repl); } static void @@ -528,18 +631,19 @@ RemoveAttributesAtIndex (CFMutableAttributedStringRef str, CFRange range) cur = &working->_attribs[range.location]; next = cur + range.length; - stop = cur + (working->_attribCount - (range.location + range.length) - 1); + stop = cur + (working->_attribCount - (range.location + range.length)); while (cur < stop) *cur++ = *next++; working->_attribCount -= range.length; - + if (working->_attribCount < (working->_attribCap >> 2) && working->_attribCount > 9) { /* Shrink */ + working->_attribCap >>= 1; working->_attribs = CFAllocatorReallocate (alloc, working->_attribs, - (working->_attribCap >> 1), + sizeof(Attr) * working->_attribCap, 0); } } @@ -556,35 +660,35 @@ CFAttributedStringCoalesce (CFMutableAttributedStringRef str, CFRange range) { CFIndex cur; CFIndex end; - Attr *array; - - array = working->_attribs; - if (range.location > 0) - { - if (array[range.location - 1].attrib == array[range.location].attrib) - { - RemoveAttributesAtIndex (str, CFRangeMake (range.location, 1)); - range.length -= 1; - } - } - - cur = range.location; - end = range.location + range.length; - + Attr *array = working->_attribs; + + /* Run 0 has no predecessor to merge into, so start at 1. Also fold in + * the run that immediately follows the affected range, since removing + * attributes can make it equal to its new predecessor. + */ + cur = range.location < 1 ? 1 : range.location; + end = range.location + range.length + 1; + if (end > working->_attribCount) + end = working->_attribCount; + while (cur < end) { - if (array[cur - 1].attrib == array[cur].attrib) + if (array[cur].attrib == array[cur - 1].attrib) { + /* This run repeats its predecessor: drop it (the predecessor's + * range absorbs it) and re-check the run now shifted into cur. + */ RemoveAttributesAtIndex (str, CFRangeMake (cur, 1)); end -= 1; } - cur++; + else + cur += 1; } } } -#define CFMUTABLEATTRIBUTESTRING_SIZE sizeof(CFRuntimeClass) \ - - sizeof(struct __CFMutableAttributedString) +#define CFMUTABLEATTRIBUTESTRING_SIZE (sizeof(struct __CFMutableAttributedString) \ + - sizeof(CFRuntimeBase)) CFMutableAttributedStringRef CFAttributedStringCreateMutable (CFAllocatorRef alloc, CFIndex maxLength) @@ -602,7 +706,8 @@ CFAttributedStringCreateMutable (CFAllocatorRef alloc, CFIndex maxLength) new->_attribCount = 1; new->_attribs[0].index = 0; new->_attribs[0].attrib = CFAttributedStringGetBlankAttribute (); - + new->_isEditing = 0; + CFAttributedStringSetMutable ((CFAttributedStringRef)new); } @@ -674,12 +779,39 @@ void CFAttributedStringRemoveAttribute (CFMutableAttributedStringRef str, CFRange range, CFStringRef attrName) { + CFIndex cur; + CFIndex end; + CF_OBJC_FUNCDISPATCHV (_kCFAttributedStringTypeID, void, str, "removeAttribute:range:", attrName, range); - + if (!CFAttributedStringIsMutable(str)) return; - /* FIXME */ + + cur = range.location; + end = range.location + range.length; + while (cur < end) + { + CFRange r; + CFDictionaryRef attribs; + CFIndex stop; + + attribs = CFAttributedStringGetAttributes (str, cur, &r); + stop = r.location + r.length; + if (stop > end) + stop = end; + if (attribs != NULL && CFDictionaryContainsKey (attribs, attrName)) + { + CFMutableDictionaryRef reduced; + + reduced = CFDictionaryCreateMutableCopy (NULL, 0, attribs); + CFDictionaryRemoveValue (reduced, attrName); + CFAttributedStringSetAttributes (str, + CFRangeMake (cur, stop - cur), reduced, true); + CFRelease (reduced); + } + cur = r.location + r.length; + } } void @@ -718,12 +850,34 @@ CFAttributedStringReplaceAttributedString (CFMutableAttributedStringRef str, CFRange range, CFAttributedStringRef repl) { + CFIndex replLen; + CFIndex cur; + CF_OBJC_FUNCDISPATCHV (_kCFAttributedStringTypeID, void, str, "replaceCharactersInRange:withAttributeString:", range, repl); - + if (!CFAttributedStringIsMutable(str)) return; + + CFAttributedStringReplaceString (str, range, + CFAttributedStringGetString (repl)); + + /* The characters are in place; now copy the replacement's own attributes + * over the region they were inserted into. + */ + replLen = CFAttributedStringGetLength (repl); + cur = 0; + while (cur < replLen) + { + CFRange r; + CFDictionaryRef attribs; + + attribs = CFAttributedStringGetAttributes (repl, cur, &r); + CFAttributedStringSetAttributes (str, + CFRangeMake (range.location + r.location, r.length), attribs, true); + cur = r.location + r.length; + } } void diff --git a/Source/CFBag.c b/Source/CFBag.c index d94da9b..3e855b6 100644 --- a/Source/CFBag.c +++ b/Source/CFBag.c @@ -198,7 +198,7 @@ CFBagCreateMutableCopy (CFAllocatorRef allocator, CFIndex capacity, void CFBagAddValue (CFMutableBagRef bag, const void *value) { - GSHashTableAddValue ((GSHashTableRef)bag, value, value); + GSHashTableAddValueCounted ((GSHashTableRef)bag, value, value); } void diff --git a/Source/GSFunctions.c b/Source/GSFunctions.c index b2a23aa..80b1e49 100644 --- a/Source/GSFunctions.c +++ b/Source/GSFunctions.c @@ -47,7 +47,7 @@ GSBSearch (const void *array, const void *key, CFRange range, CFIndex size, r = comp (key, cur, ctxt); if (r == kCFCompareLessThan) { - max = mid - 1; + max = mid; } else if (r == kCFCompareGreaterThan) { diff --git a/Source/GSHashTable.c b/Source/GSHashTable.c index 6dc1802..d989c3f 100644 --- a/Source/GSHashTable.c +++ b/Source/GSHashTable.c @@ -655,6 +655,32 @@ GSHashTableAddValue (GSHashTableRef table, const void *key, const void *value) } } +void +GSHashTableAddValueCounted (GSHashTableRef table, const void *key, + const void *value) +{ + GSHashTableBucket *bucket; + + GSHashTableGrowIfNeeded (table); + + bucket = GSHashTableFindBucket (table, key, _kGSHashTableRetrieve); + if (!bucket) + bucket = GSHashTableFindBucket (table, key, _kGSHashTableInsert); + + if (bucket->count <= 0) + { + GSHashTableAddKeyValuePair (table, bucket, key, value); + table->_count += 1; + } + else + { + /* Increment the multiplicity so a bag counts repeated additions; + * GSHashTableRemoveValue decrements it symmetrically. + */ + bucket->count += 1; + } +} + void GSHashTableReplaceValue (GSHashTableRef table, const void *key, const void *value) diff --git a/Source/GSHashTable.h b/Source/GSHashTable.h index a6fb057..e4bb2b1 100644 --- a/Source/GSHashTable.h +++ b/Source/GSHashTable.h @@ -140,6 +140,10 @@ GSHashTableCreateMutableCopy (CFAllocatorRef alloc, GSHashTableRef table, GS_PRIVATE void GSHashTableAddValue (GSHashTableRef table, const void *key, const void *value); +GS_PRIVATE void +GSHashTableAddValueCounted (GSHashTableRef table, const void *key, + const void *value); + GS_PRIVATE void GSHashTableReplaceValue (GSHashTableRef table, const void *key, const void *value); diff --git a/Tests/CFAttributedString/backlog.m b/Tests/CFAttributedString/backlog.m new file mode 100755 index 0000000..34fa54a --- /dev/null +++ b/Tests/CFAttributedString/backlog.m @@ -0,0 +1,97 @@ +#include "CoreFoundation/CFAttributedString.h" +#include "CoreFoundation/CFString.h" + +#include "../CFTesting.h" + +static Boolean +attrIs (CFAttributedStringRef s, CFIndex i, CFStringRef key, CFStringRef want) +{ + CFTypeRef v = CFAttributedStringGetAttribute (s, i, key, NULL); + if (want == NULL) + return v == NULL; + return v != NULL && CFEqual (v, want); +} + +int +main (void) +{ + CFMutableAttributedStringRef m; + CFAttributedStringRef sub; + + /* CreateWithSubstring: "Hello World" with (0,5)=color and (6,5)=weight; + * the substring (3,5) is "lo Wo" with color over its first two characters, + * a bare space, then weight over its last two. + */ + m = CFAttributedStringCreateMutable (NULL, 0); + CFAttributedStringReplaceString (m, CFRangeMake (0, 0), CFSTR ("Hello World")); + CFAttributedStringSetAttribute (m, CFRangeMake (0, 5), CFSTR ("color"), + CFSTR ("red")); + CFAttributedStringSetAttribute (m, CFRangeMake (6, 5), CFSTR ("weight"), + CFSTR ("bold")); + sub = CFAttributedStringCreateWithSubstring (NULL, m, CFRangeMake (3, 5)); + PASS_CFEQ(CFAttributedStringGetString (sub), CFSTR ("lo Wo"), + "The substring holds the requested characters."); + PASS_CF(attrIs (sub, 0, CFSTR ("color"), CFSTR ("red")) + && attrIs (sub, 1, CFSTR ("color"), CFSTR ("red")), + "The source color is remapped over the substring's first run."); + PASS_CF(attrIs (sub, 2, CFSTR ("color"), NULL) + && attrIs (sub, 2, CFSTR ("weight"), NULL), + "The gap between the source runs stays bare."); + PASS_CF(attrIs (sub, 3, CFSTR ("weight"), CFSTR ("bold")) + && attrIs (sub, 4, CFSTR ("weight"), CFSTR ("bold")), + "The source weight is remapped over the substring's last run."); + PASS_CF(attrIs (sub, 3, CFSTR ("color"), NULL), + "The substring does not carry color past the source run."); + CFRelease (sub); + CFRelease (m); + + /* RemoveAttribute clears a single key across the range and leaves the + * others in place. + */ + m = CFAttributedStringCreateMutable (NULL, 0); + CFAttributedStringReplaceString (m, CFRangeMake (0, 0), CFSTR ("Hello World")); + CFAttributedStringSetAttribute (m, CFRangeMake (0, 11), CFSTR ("color"), + CFSTR ("red")); + CFAttributedStringSetAttribute (m, CFRangeMake (0, 5), CFSTR ("weight"), + CFSTR ("bold")); + CFAttributedStringRemoveAttribute (m, CFRangeMake (0, 11), CFSTR ("color")); + PASS_CF(attrIs (m, 0, CFSTR ("color"), NULL) + && attrIs (m, 5, CFSTR ("color"), NULL) + && attrIs (m, 10, CFSTR ("color"), NULL), + "color is removed across the whole range."); + PASS_CF(attrIs (m, 0, CFSTR ("weight"), CFSTR ("bold")) + && attrIs (m, 4, CFSTR ("weight"), CFSTR ("bold")), + "weight is left untouched by the color removal."); + PASS_CF(attrIs (m, 5, CFSTR ("weight"), NULL), + "The run that only carried color is now bare."); + CFRelease (m); + + /* ReplaceAttributedString swaps in the replacement's characters and its + * attributes, and keeps the surrounding attributes. + */ + m = CFAttributedStringCreateMutable (NULL, 0); + CFAttributedStringReplaceString (m, CFRangeMake (0, 0), CFSTR ("Hello World")); + CFAttributedStringSetAttribute (m, CFRangeMake (0, 11), CFSTR ("color"), + CFSTR ("red")); + { + CFMutableAttributedStringRef repl = CFAttributedStringCreateMutable (NULL, 0); + CFAttributedStringReplaceString (repl, CFRangeMake (0, 0), CFSTR ("HI")); + CFAttributedStringSetAttribute (repl, CFRangeMake (0, 2), CFSTR ("weight"), + CFSTR ("bold")); + CFAttributedStringReplaceAttributedString (m, CFRangeMake (0, 5), repl); + CFRelease (repl); + } + PASS_CFEQ(CFAttributedStringGetString (m), CFSTR ("HI World"), + "The replacement characters are spliced in."); + PASS_CF(attrIs (m, 0, CFSTR ("weight"), CFSTR ("bold")) + && attrIs (m, 1, CFSTR ("weight"), CFSTR ("bold")), + "The replacement's own attributes cover the inserted text."); + PASS_CF(attrIs (m, 0, CFSTR ("color"), NULL), + "The inserted text does not inherit the surrounding color."); + PASS_CF(attrIs (m, 2, CFSTR ("color"), CFSTR ("red")) + && attrIs (m, 7, CFSTR ("color"), CFSTR ("red")), + "The surviving text keeps its original color."); + CFRelease (m); + + return 0; +} diff --git a/Tests/CFAttributedString/longest_range.m b/Tests/CFAttributedString/longest_range.m new file mode 100755 index 0000000..d9457cb --- /dev/null +++ b/Tests/CFAttributedString/longest_range.m @@ -0,0 +1,52 @@ +#include "CoreFoundation/CFAttributedString.h" +#include "CoreFoundation/CFString.h" +#include "CoreFoundation/CFDictionary.h" + +#include "../CFTesting.h" + +int +main (void) +{ + CFRange lr; + CFTypeRef v; + CFDictionaryRef d; + CFMutableAttributedStringRef m; + + m = CFAttributedStringCreateMutable (NULL, 0); + CFAttributedStringReplaceString (m, CFRangeMake (0, 0), CFSTR ("Hello World")); + /* (0,5) = {color, shared}, (5,11) = {weight, shared}. */ + CFAttributedStringSetAttribute (m, CFRangeMake (0, 5), CFSTR ("color"), + CFSTR ("red")); + CFAttributedStringSetAttribute (m, CFRangeMake (0, 11), CFSTR ("shared"), + CFSTR ("x")); + CFAttributedStringSetAttribute (m, CFRangeMake (5, 6), CFSTR ("weight"), + CFSTR ("bold")); + + v = CFAttributedStringGetAttributeAndLongestEffectiveRange ( + m, 2, CFSTR ("shared"), CFRangeMake (0, 11), &lr); + PASS_CFEQ(v, CFSTR ("x"), "The longest-range shared value is returned."); + PASS_CF(lr.location == 0 && lr.length == 11, + "shared has the same value across both runs."); + + v = CFAttributedStringGetAttributeAndLongestEffectiveRange ( + m, 2, CFSTR ("color"), CFRangeMake (0, 11), &lr); + PASS_CFEQ(v, CFSTR ("red"), "The longest-range color value is returned."); + PASS_CF(lr.location == 0 && lr.length == 5, + "color has the same value only over (0,5)."); + + d = CFAttributedStringGetAttributesAndLongestEffectiveRange ( + m, 8, CFRangeMake (0, 11), &lr); + PASS_CF(d != NULL && CFDictionaryGetCount (d) == 2, + "The longest-range attributes at 8 are the second run's two."); + PASS_CF(lr.location == 5 && lr.length == 6, + "The full attribute set stays equal only over (5,6)."); + + v = CFAttributedStringGetAttributeAndLongestEffectiveRange ( + m, 2, CFSTR ("shared"), CFRangeMake (0, 3), &lr); + PASS_CF(lr.location == 0 && lr.length == 3, + "The longest range is clamped to the search range."); + + CFRelease (m); + + return 0; +} diff --git a/Tests/CFAttributedString/multirun.m b/Tests/CFAttributedString/multirun.m new file mode 100644 index 0000000..9054a89 --- /dev/null +++ b/Tests/CFAttributedString/multirun.m @@ -0,0 +1,61 @@ +#include "CoreFoundation/CFAttributedString.h" +#include "CoreFoundation/CFString.h" +#include "CoreFoundation/CFDictionary.h" + +#include "../CFTesting.h" + +int +main (void) +{ + CFRange er; + CFDictionaryRef d; + CFMutableAttributedStringRef m; + + m = CFAttributedStringCreateMutable (NULL, 0); + CFAttributedStringReplaceString (m, CFRangeMake (0, 0), CFSTR ("Hello World")); + + /* Two runs: (0,5) = {color}, (5,11) = {weight}. */ + CFAttributedStringSetAttribute (m, CFRangeMake (0, 5), CFSTR ("color"), + CFSTR ("red")); + CFAttributedStringSetAttribute (m, CFRangeMake (5, 6), CFSTR ("weight"), + CFSTR ("bold")); + + d = CFAttributedStringGetAttributes (m, 2, &er); + PASS_CF(d != NULL && CFDictionaryGetCount (d) == 1 + && er.location == 0 && er.length == 5, + "The first run's attributes cover (0,5)."); + PASS_CFEQ(CFAttributedStringGetAttribute (m, 2, CFSTR ("color"), NULL), + CFSTR ("red"), "The first run has the color attribute."); + d = CFAttributedStringGetAttributes (m, 8, &er); + PASS_CF(d != NULL && CFDictionaryGetCount (d) == 1 + && er.location == 5 && er.length == 6, + "The second run's attributes cover (5,6)."); + PASS_CFEQ(CFAttributedStringGetAttribute (m, 8, CFSTR ("weight"), NULL), + CFSTR ("bold"), "The second run has the weight attribute."); + PASS_CF(CFAttributedStringGetAttribute (m, 2, CFSTR ("weight"), NULL) == NULL, + "weight is absent from the first run."); + PASS_CF(CFAttributedStringGetAttribute (m, 8, CFSTR ("color"), NULL) == NULL, + "color is absent from the second run."); + d = CFAttributedStringGetAttributes (m, 5, &er); + PASS_CF(er.location == 5 && er.length == 6, + "A boundary index resolves to the second run."); + + /* Setting an attribute over both runs keeps each run's own attributes. */ + CFAttributedStringSetAttribute (m, CFRangeMake (0, 11), CFSTR ("lang"), + CFSTR ("en")); + d = CFAttributedStringGetAttributes (m, 2, &er); + PASS_CF(CFDictionaryGetCount (d) == 2 + && CFDictionaryGetValue (d, CFSTR ("color")) != NULL + && CFDictionaryGetValue (d, CFSTR ("lang")) != NULL + && er.location == 0 && er.length == 5, + "A spanning attribute merges into the first run."); + d = CFAttributedStringGetAttributes (m, 8, &er); + PASS_CF(CFDictionaryGetCount (d) == 2 + && CFDictionaryGetValue (d, CFSTR ("weight")) != NULL + && CFDictionaryGetValue (d, CFSTR ("lang")) != NULL + && er.location == 5 && er.length == 6, + "A spanning attribute merges into the second run."); + CFRelease (m); + + return 0; +}