Skip to content

Commit b01f9c6

Browse files
authored
Implement AttributedString UTF8 and UTF16 views (#1066)
* (13984100) Implement AttributedString UTF8 and UTF16 views * Remove public initializers * Update index movement preconditions
1 parent 26fb3ba commit b01f9c6

File tree

5 files changed

+348
-0
lines changed

5 files changed

+348
-0
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
#if FOUNDATION_FRAMEWORK
14+
@_spi(Unstable) internal import CollectionsInternal
15+
#elseif canImport(_RopeModule)
16+
internal import _RopeModule
17+
#elseif canImport(_FoundationCollections)
18+
internal import _FoundationCollections
19+
#endif
20+
21+
@available(FoundationPreview 6.2, *)
22+
extension AttributedString {
23+
public struct UTF16View: Sendable {
24+
internal var _guts: Guts
25+
internal var _range: Range<BigString.Index>
26+
internal var _identity: Int = 0
27+
28+
internal init(_ guts: AttributedString.Guts) {
29+
self.init(guts, in: guts.stringBounds)
30+
}
31+
32+
internal init(_ guts: Guts, in range: Range<BigString.Index>) {
33+
_guts = guts
34+
_range = range
35+
}
36+
}
37+
38+
public var utf16: UTF16View {
39+
UTF16View(_guts)
40+
}
41+
}
42+
43+
@available(FoundationPreview 6.2, *)
44+
extension AttributedSubstring {
45+
public var utf16: AttributedString.UTF16View {
46+
AttributedString.UTF16View(_guts, in: _range)
47+
}
48+
}
49+
50+
@available(FoundationPreview 6.2, *)
51+
extension AttributedString.UTF16View {
52+
var _utf16: BigSubstring.UTF16View {
53+
BigSubstring.UTF16View(_unchecked: _guts.string, in: _range)
54+
}
55+
}
56+
57+
@available(FoundationPreview 6.2, *)
58+
extension AttributedString.UTF16View: BidirectionalCollection {
59+
public typealias Element = UTF16.CodeUnit
60+
public typealias Index = AttributedString.Index
61+
public typealias Subsequence = Self
62+
63+
public var startIndex: AttributedString.Index {
64+
.init(_range.lowerBound)
65+
}
66+
67+
public var endIndex: AttributedString.Index {
68+
.init(_range.upperBound)
69+
}
70+
71+
public var count: Int {
72+
_utf16.count
73+
}
74+
75+
public func index(before i: AttributedString.Index) -> AttributedString.Index {
76+
precondition(i > startIndex && i <= endIndex, "AttributedString index out of bounds")
77+
let j = Index(_guts.string.utf16.index(before: i._value))
78+
precondition(j >= startIndex, "Can't advance AttributedString index before start index")
79+
return j
80+
}
81+
82+
public func index(after i: AttributedString.Index) -> AttributedString.Index {
83+
precondition(i >= startIndex && i < endIndex, "AttributedString index out of bounds")
84+
let j = Index(_guts.string.utf16.index(after: i._value))
85+
precondition(j <= endIndex, "Can't advance AttributedString index after end index")
86+
return j
87+
}
88+
89+
public func index(_ i: AttributedString.Index, offsetBy distance: Int) -> AttributedString.Index {
90+
precondition(i >= startIndex && i <= endIndex, "AttributedString index out of bounds")
91+
let j = Index(_guts.string.utf16.index(i._value, offsetBy: distance))
92+
precondition(j >= startIndex && j <= endIndex, "AttributedString index out of bounds")
93+
return j
94+
}
95+
96+
public func index(
97+
_ i: AttributedString.Index,
98+
offsetBy distance: Int,
99+
limitedBy limit: AttributedString.Index
100+
) -> AttributedString.Index? {
101+
precondition(i >= startIndex && i <= endIndex, "AttributedString index out of bounds")
102+
precondition(limit >= startIndex && limit <= endIndex, "AttributedString index out of bounds")
103+
guard let j = _guts.string.utf16.index(
104+
i._value, offsetBy: distance, limitedBy: limit._value
105+
) else {
106+
return nil
107+
}
108+
precondition(j >= startIndex._value && j <= endIndex._value,
109+
"AttributedString index out of bounds")
110+
return Index(j)
111+
}
112+
113+
public func distance(
114+
from start: AttributedString.Index,
115+
to end: AttributedString.Index
116+
) -> Int {
117+
precondition(start >= startIndex && start <= endIndex, "AttributedString index out of bounds")
118+
precondition(end >= startIndex && end <= endIndex, "AttributedString index out of bounds")
119+
return _guts.string.utf16.distance(from: start._value, to: end._value)
120+
}
121+
122+
public subscript(index: AttributedString.Index) -> UTF16.CodeUnit {
123+
precondition(index >= startIndex && index < endIndex, "AttributedString index out of bounds")
124+
return _guts.string.utf16[index._value]
125+
}
126+
127+
public subscript(bounds: Range<AttributedString.Index>) -> Self {
128+
let bounds = bounds._bstringRange
129+
precondition(
130+
bounds.lowerBound >= _range.lowerBound && bounds.lowerBound <= _range.upperBound &&
131+
bounds.upperBound >= _range.lowerBound && bounds.upperBound <= _range.upperBound,
132+
"AttributedString index range out of bounds")
133+
return Self(_guts, in: bounds)
134+
}
135+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
#if FOUNDATION_FRAMEWORK
14+
@_spi(Unstable) internal import CollectionsInternal
15+
#elseif canImport(_RopeModule)
16+
internal import _RopeModule
17+
#elseif canImport(_FoundationCollections)
18+
internal import _FoundationCollections
19+
#endif
20+
21+
@available(FoundationPreview 6.2, *)
22+
extension AttributedString {
23+
public struct UTF8View: Sendable {
24+
internal var _guts: Guts
25+
internal var _range: Range<BigString.Index>
26+
internal var _identity: Int = 0
27+
28+
internal init(_ guts: AttributedString.Guts) {
29+
self.init(guts, in: guts.stringBounds)
30+
}
31+
32+
internal init(_ guts: Guts, in range: Range<BigString.Index>) {
33+
_guts = guts
34+
_range = range
35+
}
36+
}
37+
38+
public var utf8: UTF8View {
39+
UTF8View(_guts)
40+
}
41+
}
42+
43+
@available(FoundationPreview 6.2, *)
44+
extension AttributedSubstring {
45+
public var utf8: AttributedString.UTF8View {
46+
AttributedString.UTF8View(_guts, in: _range)
47+
}
48+
}
49+
50+
@available(FoundationPreview 6.2, *)
51+
extension AttributedString.UTF8View {
52+
var _utf8: BigSubstring.UTF8View {
53+
BigSubstring.UTF8View(_unchecked: _guts.string, in: _range)
54+
}
55+
}
56+
57+
@available(FoundationPreview 6.2, *)
58+
extension AttributedString.UTF8View: BidirectionalCollection {
59+
public typealias Element = UTF8.CodeUnit
60+
public typealias Index = AttributedString.Index
61+
public typealias Subsequence = Self
62+
63+
public var startIndex: AttributedString.Index {
64+
.init(_range.lowerBound)
65+
}
66+
67+
public var endIndex: AttributedString.Index {
68+
.init(_range.upperBound)
69+
}
70+
71+
public var count: Int {
72+
_utf8.count
73+
}
74+
75+
public func index(before i: AttributedString.Index) -> AttributedString.Index {
76+
precondition(i > startIndex && i <= endIndex, "AttributedString index out of bounds")
77+
let j = Index(_guts.string.utf8.index(before: i._value))
78+
precondition(j >= startIndex, "Can't advance AttributedString index before start index")
79+
return j
80+
}
81+
82+
public func index(after i: AttributedString.Index) -> AttributedString.Index {
83+
precondition(i >= startIndex && i < endIndex, "AttributedString index out of bounds")
84+
let j = Index(_guts.string.utf8.index(after: i._value))
85+
precondition(j <= endIndex, "Can't advance AttributedString index after end index")
86+
return j
87+
}
88+
89+
public func index(_ i: AttributedString.Index, offsetBy distance: Int) -> AttributedString.Index {
90+
precondition(i >= startIndex && i <= endIndex, "AttributedString index out of bounds")
91+
let j = Index(_guts.string.utf8.index(i._value, offsetBy: distance))
92+
precondition(j >= startIndex && j <= endIndex, "AttributedString index out of bounds")
93+
return j
94+
}
95+
96+
public func index(
97+
_ i: AttributedString.Index,
98+
offsetBy distance: Int,
99+
limitedBy limit: AttributedString.Index
100+
) -> AttributedString.Index? {
101+
precondition(i >= startIndex && i <= endIndex, "AttributedString index out of bounds")
102+
precondition(limit >= startIndex && limit <= endIndex, "AttributedString index out of bounds")
103+
guard let j = _guts.string.utf8.index(
104+
i._value, offsetBy: distance, limitedBy: limit._value
105+
) else {
106+
return nil
107+
}
108+
precondition(j >= startIndex._value && j <= endIndex._value,
109+
"AttributedString index out of bounds")
110+
return Index(j)
111+
}
112+
113+
public func distance(
114+
from start: AttributedString.Index,
115+
to end: AttributedString.Index
116+
) -> Int {
117+
precondition(start >= startIndex && start <= endIndex, "AttributedString index out of bounds")
118+
precondition(end >= startIndex && end <= endIndex, "AttributedString index out of bounds")
119+
return _guts.string.utf8.distance(from: start._value, to: end._value)
120+
}
121+
122+
public subscript(index: AttributedString.Index) -> UTF8.CodeUnit {
123+
precondition(index >= startIndex && index < endIndex, "AttributedString index out of bounds")
124+
return _guts.string.utf8[index._value]
125+
}
126+
127+
public subscript(bounds: Range<AttributedString.Index>) -> Self {
128+
let bounds = bounds._bstringRange
129+
precondition(
130+
bounds.lowerBound >= _range.lowerBound && bounds.lowerBound <= _range.upperBound &&
131+
bounds.upperBound >= _range.lowerBound && bounds.upperBound <= _range.upperBound,
132+
"AttributedString index range out of bounds")
133+
return Self(_guts, in: bounds)
134+
}
135+
}

Sources/FoundationEssentials/AttributedString/AttributedStringProtocol.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ public protocol AttributedStringProtocol
5151
var runs : AttributedString.Runs { get }
5252
var characters : AttributedString.CharacterView { get }
5353
var unicodeScalars : AttributedString.UnicodeScalarView { get }
54+
55+
@available(FoundationPreview 6.2, *)
56+
var utf8 : AttributedString.UTF8View { get }
57+
58+
@available(FoundationPreview 6.2, *)
59+
var utf16 : AttributedString.UTF16View { get }
5460

5561
@preconcurrency subscript<K: AttributedStringKey>(_: K.Type) -> K.Value? where K.Value : Sendable { get set }
5662
@preconcurrency subscript<K: AttributedStringKey>(dynamicMember keyPath: KeyPath<AttributeDynamicLookup, K>) -> K.Value? where K.Value : Sendable { get set }
@@ -59,6 +65,18 @@ public protocol AttributedStringProtocol
5965
subscript<R: RangeExpression>(bounds: R) -> AttributedSubstring where R.Bound == AttributedString.Index { get }
6066
}
6167

68+
69+
@available(FoundationPreview 6.2, *)
70+
extension AttributedStringProtocol {
71+
var utf8 : AttributedString.UTF8View {
72+
AttributedString.UTF8View(__guts, in: Range(uncheckedBounds: (startIndex._value, endIndex._value)))
73+
}
74+
75+
var utf16 : AttributedString.UTF16View {
76+
AttributedString.UTF16View(__guts, in: Range(uncheckedBounds: (startIndex._value, endIndex._value)))
77+
}
78+
}
79+
6280
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
6381
extension AttributedStringProtocol {
6482
public func settingAttributes(_ attributes: AttributeContainer) -> AttributedString {

Sources/FoundationEssentials/AttributedString/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ target_sources(FoundationEssentials PRIVATE
2121
AttributedString+Runs+Run.swift
2222
AttributedString+Runs.swift
2323
AttributedString+UnicodeScalarView.swift
24+
AttributedString+UTF8View.swift
25+
AttributedString+UTF16View.swift
2426
AttributedString+_InternalRun.swift
2527
AttributedString+_InternalRuns.swift
2628
AttributedString+_InternalRunsSlice.swift

Tests/FoundationEssentialsTests/AttributedString/AttributedStringTests.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2504,4 +2504,62 @@ E {
25042504

25052505
XCTAssertEqual(attrStr, AttributedString("XYZ", attributes: .init().testInt(1)))
25062506
}
2507+
2508+
func testUTF8View() {
2509+
let testStrings = [
2510+
"Hello, world",
2511+
"🎺😄abc🎶def",
2512+
"¡Hola! ¿Cómo estás?",
2513+
"שָׁלוֹם"
2514+
]
2515+
2516+
for string in testStrings {
2517+
let attrStr = AttributedString(string)
2518+
XCTAssertEqual(attrStr.utf8.count, string.utf8.count, "Counts are not equal for string \(string)")
2519+
XCTAssertTrue(attrStr.utf8.elementsEqual(string.utf8), "Full elements are not equal for string \(string)")
2520+
for offset in 0 ..< string.utf8.count {
2521+
let idxInString = string.utf8.index(string.startIndex, offsetBy: offset)
2522+
let idxInAttrStr = attrStr.utf8.index(attrStr.startIndex, offsetBy: offset)
2523+
XCTAssertEqual(
2524+
string.utf8.distance(from: string.startIndex, to: idxInString),
2525+
attrStr.utf8.distance(from: attrStr.startIndex, to: idxInAttrStr),
2526+
"Offsets to \(idxInString) are not equal for string \(string)"
2527+
)
2528+
XCTAssertEqual(string.utf8[idxInString], attrStr.utf8[idxInAttrStr], "Elements at offset \(offset) are not equal for string \(string)")
2529+
XCTAssertTrue(string.utf8[..<idxInString].elementsEqual(attrStr.utf8[..<idxInAttrStr]), "Slices up to \(offset) are not equal for string \(string)")
2530+
XCTAssertTrue(string.utf8[idxInString...].elementsEqual(attrStr.utf8[idxInAttrStr...]), "Slices from \(offset) are not equal for string \(string)")
2531+
XCTAssertTrue(string[..<idxInString].utf8.elementsEqual(attrStr[..<idxInAttrStr].utf8), "Slices up to \(offset) are not equal for string \(string)")
2532+
XCTAssertTrue(string[idxInString...].utf8.elementsEqual(attrStr[idxInAttrStr...].utf8), "Slices from \(offset) are not equal for string \(string)")
2533+
}
2534+
}
2535+
}
2536+
2537+
func testUTF16View() {
2538+
let testStrings = [
2539+
"Hello, world",
2540+
"🎺😄abc🎶def",
2541+
"¡Hola! ¿Cómo estás?",
2542+
"שָׁלוֹם"
2543+
]
2544+
2545+
for string in testStrings {
2546+
let attrStr = AttributedString(string)
2547+
XCTAssertEqual(attrStr.utf16.count, string.utf16.count, "Counts are not equal for string \(string)")
2548+
XCTAssertTrue(attrStr.utf16.elementsEqual(string.utf16), "Full elements are not equal for string \(string)")
2549+
for offset in 0 ..< string.utf16.count {
2550+
let idxInString = string.utf16.index(string.startIndex, offsetBy: offset)
2551+
let idxInAttrStr = attrStr.utf16.index(attrStr.startIndex, offsetBy: offset)
2552+
XCTAssertEqual(
2553+
string.utf16.distance(from: string.startIndex, to: idxInString),
2554+
attrStr.utf16.distance(from: attrStr.startIndex, to: idxInAttrStr),
2555+
"Offsets to \(idxInString) are not equal for string \(string)"
2556+
)
2557+
XCTAssertEqual(string.utf16[idxInString], attrStr.utf16[idxInAttrStr], "Elements at offset \(offset) are not equal for string \(string)")
2558+
XCTAssertTrue(string.utf16[..<idxInString].elementsEqual(attrStr.utf16[..<idxInAttrStr]), "Slices up to \(offset) are not equal for string \(string)")
2559+
XCTAssertTrue(string.utf16[idxInString...].elementsEqual(attrStr.utf16[idxInAttrStr...]), "Slices from \(offset) are not equal for string \(string)")
2560+
XCTAssertTrue(string[..<idxInString].utf16.elementsEqual(attrStr[..<idxInAttrStr].utf16), "Slices up to \(offset) are not equal for string \(string)")
2561+
XCTAssertTrue(string[idxInString...].utf16.elementsEqual(attrStr[idxInAttrStr...].utf16), "Slices from \(offset) are not equal for string \(string)")
2562+
}
2563+
}
2564+
}
25072565
}

0 commit comments

Comments
 (0)