From 115ac2d8564b37cef0bc2eebd584b5f15ca3f31f Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Tue, 10 Dec 2024 14:22:27 -0800 Subject: [PATCH 1/3] (13984100) Implement AttributedString UTF8 and UTF16 views --- .../AttributedString+UTF16View.swift | 139 ++++++++++++++++++ .../AttributedString+UTF8View.swift | 139 ++++++++++++++++++ .../AttributedStringProtocol.swift | 18 +++ .../AttributedString/CMakeLists.txt | 2 + .../AttributedStringTests.swift | 58 ++++++++ 5 files changed, 356 insertions(+) create mode 100644 Sources/FoundationEssentials/AttributedString/AttributedString+UTF16View.swift create mode 100644 Sources/FoundationEssentials/AttributedString/AttributedString+UTF8View.swift diff --git a/Sources/FoundationEssentials/AttributedString/AttributedString+UTF16View.swift b/Sources/FoundationEssentials/AttributedString/AttributedString+UTF16View.swift new file mode 100644 index 000000000..27fbe0142 --- /dev/null +++ b/Sources/FoundationEssentials/AttributedString/AttributedString+UTF16View.swift @@ -0,0 +1,139 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if FOUNDATION_FRAMEWORK +@_spi(Unstable) internal import CollectionsInternal +#elseif canImport(_RopeModule) +internal import _RopeModule +#elseif canImport(_FoundationCollections) +internal import _FoundationCollections +#endif + +@available(FoundationPreview 6.2, *) +extension AttributedString { + public struct UTF16View: Sendable { + internal var _guts: Guts + internal var _range: Range + internal var _identity: Int = 0 + + internal init(_ guts: AttributedString.Guts) { + self.init(guts, in: guts.stringBounds) + } + + internal init(_ guts: Guts, in range: Range) { + _guts = guts + _range = range + } + + public init() { + self.init(Guts()) + } + } + + public var utf16: UTF16View { + UTF16View(_guts) + } +} + +@available(FoundationPreview 6.2, *) +extension AttributedSubstring { + public var utf16: AttributedString.UTF16View { + AttributedString.UTF16View(_guts, in: _range) + } +} + +@available(FoundationPreview 6.2, *) +extension AttributedString.UTF16View { + var _utf16: BigSubstring.UTF16View { + BigSubstring.UTF16View(_unchecked: _guts.string, in: _range) + } +} + +@available(FoundationPreview 6.2, *) +extension AttributedString.UTF16View: BidirectionalCollection { + public typealias Element = UTF16.CodeUnit + public typealias Index = AttributedString.Index + public typealias Subsequence = Self + + public var startIndex: AttributedString.Index { + .init(_range.lowerBound) + } + + public var endIndex: AttributedString.Index { + .init(_range.upperBound) + } + + public var count: Int { + _utf16.count + } + + public func index(before i: AttributedString.Index) -> AttributedString.Index { + precondition(i >= startIndex && i <= endIndex, "AttributedString index out of bounds") + let j = Index(_guts.string.utf16.index(before: i._value)) + precondition(j >= startIndex, "Can't advance AttributedString index before start index") + return j + } + + public func index(after i: AttributedString.Index) -> AttributedString.Index { + precondition(i >= startIndex && i <= endIndex, "AttributedString index out of bounds") + let j = Index(_guts.string.utf16.index(after: i._value)) + precondition(j <= endIndex, "Can't advance AttributedString index after end index") + return j + } + + public func index(_ i: AttributedString.Index, offsetBy distance: Int) -> AttributedString.Index { + precondition(i >= startIndex && i <= endIndex, "AttributedString index out of bounds") + let j = Index(_guts.string.utf16.index(i._value, offsetBy: distance)) + precondition(j >= startIndex && j <= endIndex, "AttributedString index out of bounds") + return j + } + + public func index( + _ i: AttributedString.Index, + offsetBy distance: Int, + limitedBy limit: AttributedString.Index + ) -> AttributedString.Index? { + precondition(i >= startIndex && i <= endIndex, "AttributedString index out of bounds") + precondition(limit >= startIndex && limit <= endIndex, "AttributedString index out of bounds") + guard let j = _guts.string.utf16.index( + i._value, offsetBy: distance, limitedBy: limit._value + ) else { + return nil + } + precondition(j >= startIndex._value && j <= endIndex._value, + "AttributedString index out of bounds") + return Index(j) + } + + public func distance( + from start: AttributedString.Index, + to end: AttributedString.Index + ) -> Int { + precondition(start >= startIndex && start <= endIndex, "AttributedString index out of bounds") + precondition(end >= startIndex && end <= endIndex, "AttributedString index out of bounds") + return _guts.string.utf16.distance(from: start._value, to: end._value) + } + + public subscript(index: AttributedString.Index) -> UTF16.CodeUnit { + precondition(index >= startIndex && index < endIndex, "AttributedString index out of bounds") + return _guts.string.utf16[index._value] + } + + public subscript(bounds: Range) -> Self { + let bounds = bounds._bstringRange + precondition( + bounds.lowerBound >= _range.lowerBound && bounds.lowerBound < _range.upperBound && + bounds.upperBound >= _range.lowerBound && bounds.upperBound <= _range.upperBound, + "AttributedString index range out of bounds") + return Self(_guts, in: bounds) + } +} diff --git a/Sources/FoundationEssentials/AttributedString/AttributedString+UTF8View.swift b/Sources/FoundationEssentials/AttributedString/AttributedString+UTF8View.swift new file mode 100644 index 000000000..b3d5b2b0f --- /dev/null +++ b/Sources/FoundationEssentials/AttributedString/AttributedString+UTF8View.swift @@ -0,0 +1,139 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if FOUNDATION_FRAMEWORK +@_spi(Unstable) internal import CollectionsInternal +#elseif canImport(_RopeModule) +internal import _RopeModule +#elseif canImport(_FoundationCollections) +internal import _FoundationCollections +#endif + +@available(FoundationPreview 6.2, *) +extension AttributedString { + public struct UTF8View: Sendable { + internal var _guts: Guts + internal var _range: Range + internal var _identity: Int = 0 + + internal init(_ guts: AttributedString.Guts) { + self.init(guts, in: guts.stringBounds) + } + + internal init(_ guts: Guts, in range: Range) { + _guts = guts + _range = range + } + + public init() { + self.init(Guts()) + } + } + + public var utf8: UTF8View { + UTF8View(_guts) + } +} + +@available(FoundationPreview 6.2, *) +extension AttributedSubstring { + public var utf8: AttributedString.UTF8View { + AttributedString.UTF8View(_guts, in: _range) + } +} + +@available(FoundationPreview 6.2, *) +extension AttributedString.UTF8View { + var _utf8: BigSubstring.UTF8View { + BigSubstring.UTF8View(_unchecked: _guts.string, in: _range) + } +} + +@available(FoundationPreview 6.2, *) +extension AttributedString.UTF8View: BidirectionalCollection { + public typealias Element = UTF8.CodeUnit + public typealias Index = AttributedString.Index + public typealias Subsequence = Self + + public var startIndex: AttributedString.Index { + .init(_range.lowerBound) + } + + public var endIndex: AttributedString.Index { + .init(_range.upperBound) + } + + public var count: Int { + _utf8.count + } + + public func index(before i: AttributedString.Index) -> AttributedString.Index { + precondition(i >= startIndex && i <= endIndex, "AttributedString index out of bounds") + let j = Index(_guts.string.utf8.index(before: i._value)) + precondition(j >= startIndex, "Can't advance AttributedString index before start index") + return j + } + + public func index(after i: AttributedString.Index) -> AttributedString.Index { + precondition(i >= startIndex && i <= endIndex, "AttributedString index out of bounds") + let j = Index(_guts.string.utf8.index(after: i._value)) + precondition(j <= endIndex, "Can't advance AttributedString index after end index") + return j + } + + public func index(_ i: AttributedString.Index, offsetBy distance: Int) -> AttributedString.Index { + precondition(i >= startIndex && i <= endIndex, "AttributedString index out of bounds") + let j = Index(_guts.string.utf8.index(i._value, offsetBy: distance)) + precondition(j >= startIndex && j <= endIndex, "AttributedString index out of bounds") + return j + } + + public func index( + _ i: AttributedString.Index, + offsetBy distance: Int, + limitedBy limit: AttributedString.Index + ) -> AttributedString.Index? { + precondition(i >= startIndex && i <= endIndex, "AttributedString index out of bounds") + precondition(limit >= startIndex && limit <= endIndex, "AttributedString index out of bounds") + guard let j = _guts.string.utf8.index( + i._value, offsetBy: distance, limitedBy: limit._value + ) else { + return nil + } + precondition(j >= startIndex._value && j <= endIndex._value, + "AttributedString index out of bounds") + return Index(j) + } + + public func distance( + from start: AttributedString.Index, + to end: AttributedString.Index + ) -> Int { + precondition(start >= startIndex && start <= endIndex, "AttributedString index out of bounds") + precondition(end >= startIndex && end <= endIndex, "AttributedString index out of bounds") + return _guts.string.utf8.distance(from: start._value, to: end._value) + } + + public subscript(index: AttributedString.Index) -> UTF8.CodeUnit { + precondition(index >= startIndex && index < endIndex, "AttributedString index out of bounds") + return _guts.string.utf8[index._value] + } + + public subscript(bounds: Range) -> Self { + let bounds = bounds._bstringRange + precondition( + bounds.lowerBound >= _range.lowerBound && bounds.lowerBound < _range.upperBound && + bounds.upperBound >= _range.lowerBound && bounds.upperBound <= _range.upperBound, + "AttributedString index range out of bounds") + return Self(_guts, in: bounds) + } +} diff --git a/Sources/FoundationEssentials/AttributedString/AttributedStringProtocol.swift b/Sources/FoundationEssentials/AttributedString/AttributedStringProtocol.swift index dd929cf23..1949be10a 100644 --- a/Sources/FoundationEssentials/AttributedString/AttributedStringProtocol.swift +++ b/Sources/FoundationEssentials/AttributedString/AttributedStringProtocol.swift @@ -51,6 +51,12 @@ public protocol AttributedStringProtocol var runs : AttributedString.Runs { get } var characters : AttributedString.CharacterView { get } var unicodeScalars : AttributedString.UnicodeScalarView { get } + + @available(FoundationPreview 6.2, *) + var utf8 : AttributedString.UTF8View { get } + + @available(FoundationPreview 6.2, *) + var utf16 : AttributedString.UTF16View { get } @preconcurrency subscript(_: K.Type) -> K.Value? where K.Value : Sendable { get set } @preconcurrency subscript(dynamicMember keyPath: KeyPath) -> K.Value? where K.Value : Sendable { get set } @@ -59,6 +65,18 @@ public protocol AttributedStringProtocol subscript(bounds: R) -> AttributedSubstring where R.Bound == AttributedString.Index { get } } + +@available(FoundationPreview 6.2, *) +extension AttributedStringProtocol { + var utf8 : AttributedString.UTF8View { + AttributedString.UTF8View(__guts, in: Range(uncheckedBounds: (startIndex._value, endIndex._value))) + } + + var utf16 : AttributedString.UTF16View { + AttributedString.UTF16View(__guts, in: Range(uncheckedBounds: (startIndex._value, endIndex._value))) + } +} + @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) extension AttributedStringProtocol { public func settingAttributes(_ attributes: AttributeContainer) -> AttributedString { diff --git a/Sources/FoundationEssentials/AttributedString/CMakeLists.txt b/Sources/FoundationEssentials/AttributedString/CMakeLists.txt index cce9ba263..aa77795a0 100644 --- a/Sources/FoundationEssentials/AttributedString/CMakeLists.txt +++ b/Sources/FoundationEssentials/AttributedString/CMakeLists.txt @@ -21,6 +21,8 @@ target_sources(FoundationEssentials PRIVATE AttributedString+Runs+Run.swift AttributedString+Runs.swift AttributedString+UnicodeScalarView.swift + AttributedString+UTF8View.swift + AttributedString+UTF16View.swift AttributedString+_InternalRun.swift AttributedString+_InternalRuns.swift AttributedString+_InternalRunsSlice.swift diff --git a/Tests/FoundationEssentialsTests/AttributedString/AttributedStringTests.swift b/Tests/FoundationEssentialsTests/AttributedString/AttributedStringTests.swift index c49567bb2..9e74498bb 100644 --- a/Tests/FoundationEssentialsTests/AttributedString/AttributedStringTests.swift +++ b/Tests/FoundationEssentialsTests/AttributedString/AttributedStringTests.swift @@ -2504,4 +2504,62 @@ E { XCTAssertEqual(attrStr, AttributedString("XYZ", attributes: .init().testInt(1))) } + + func testUTF8View() { + let testStrings = [ + "Hello, world", + "🎺😄abc🎶def", + "¡Hola! ¿Cómo estás?", + "שָׁלוֹם" + ] + + for string in testStrings { + let attrStr = AttributedString(string) + XCTAssertEqual(attrStr.utf8.count, string.utf8.count, "Counts are not equal for string \(string)") + XCTAssertTrue(attrStr.utf8.elementsEqual(string.utf8), "Full elements are not equal for string \(string)") + for offset in 0 ..< string.utf8.count { + let idxInString = string.utf8.index(string.startIndex, offsetBy: offset) + let idxInAttrStr = attrStr.utf8.index(attrStr.startIndex, offsetBy: offset) + XCTAssertEqual( + string.utf8.distance(from: string.startIndex, to: idxInString), + attrStr.utf8.distance(from: attrStr.startIndex, to: idxInAttrStr), + "Offsets to \(idxInString) are not equal for string \(string)" + ) + XCTAssertEqual(string.utf8[idxInString], attrStr.utf8[idxInAttrStr], "Elements at offset \(offset) are not equal for string \(string)") + XCTAssertTrue(string.utf8[.. Date: Thu, 12 Dec 2024 11:35:52 -0800 Subject: [PATCH 2/3] Remove public initializers --- .../AttributedString/AttributedString+UTF16View.swift | 4 ---- .../AttributedString/AttributedString+UTF8View.swift | 4 ---- 2 files changed, 8 deletions(-) diff --git a/Sources/FoundationEssentials/AttributedString/AttributedString+UTF16View.swift b/Sources/FoundationEssentials/AttributedString/AttributedString+UTF16View.swift index 27fbe0142..63d7c39a3 100644 --- a/Sources/FoundationEssentials/AttributedString/AttributedString+UTF16View.swift +++ b/Sources/FoundationEssentials/AttributedString/AttributedString+UTF16View.swift @@ -33,10 +33,6 @@ extension AttributedString { _guts = guts _range = range } - - public init() { - self.init(Guts()) - } } public var utf16: UTF16View { diff --git a/Sources/FoundationEssentials/AttributedString/AttributedString+UTF8View.swift b/Sources/FoundationEssentials/AttributedString/AttributedString+UTF8View.swift index b3d5b2b0f..cfd39582b 100644 --- a/Sources/FoundationEssentials/AttributedString/AttributedString+UTF8View.swift +++ b/Sources/FoundationEssentials/AttributedString/AttributedString+UTF8View.swift @@ -33,10 +33,6 @@ extension AttributedString { _guts = guts _range = range } - - public init() { - self.init(Guts()) - } } public var utf8: UTF8View { From eee79be9afeb061d1804c6561a32ec22e6d6a6b6 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Thu, 12 Dec 2024 11:41:48 -0800 Subject: [PATCH 3/3] Update index movement preconditions --- .../AttributedString/AttributedString+UTF16View.swift | 6 +++--- .../AttributedString/AttributedString+UTF8View.swift | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/FoundationEssentials/AttributedString/AttributedString+UTF16View.swift b/Sources/FoundationEssentials/AttributedString/AttributedString+UTF16View.swift index 63d7c39a3..b36d939b9 100644 --- a/Sources/FoundationEssentials/AttributedString/AttributedString+UTF16View.swift +++ b/Sources/FoundationEssentials/AttributedString/AttributedString+UTF16View.swift @@ -73,14 +73,14 @@ extension AttributedString.UTF16View: BidirectionalCollection { } public func index(before i: AttributedString.Index) -> AttributedString.Index { - precondition(i >= startIndex && i <= endIndex, "AttributedString index out of bounds") + precondition(i > startIndex && i <= endIndex, "AttributedString index out of bounds") let j = Index(_guts.string.utf16.index(before: i._value)) precondition(j >= startIndex, "Can't advance AttributedString index before start index") return j } public func index(after i: AttributedString.Index) -> AttributedString.Index { - precondition(i >= startIndex && i <= endIndex, "AttributedString index out of bounds") + precondition(i >= startIndex && i < endIndex, "AttributedString index out of bounds") let j = Index(_guts.string.utf16.index(after: i._value)) precondition(j <= endIndex, "Can't advance AttributedString index after end index") return j @@ -127,7 +127,7 @@ extension AttributedString.UTF16View: BidirectionalCollection { public subscript(bounds: Range) -> Self { let bounds = bounds._bstringRange precondition( - bounds.lowerBound >= _range.lowerBound && bounds.lowerBound < _range.upperBound && + bounds.lowerBound >= _range.lowerBound && bounds.lowerBound <= _range.upperBound && bounds.upperBound >= _range.lowerBound && bounds.upperBound <= _range.upperBound, "AttributedString index range out of bounds") return Self(_guts, in: bounds) diff --git a/Sources/FoundationEssentials/AttributedString/AttributedString+UTF8View.swift b/Sources/FoundationEssentials/AttributedString/AttributedString+UTF8View.swift index cfd39582b..7dd236617 100644 --- a/Sources/FoundationEssentials/AttributedString/AttributedString+UTF8View.swift +++ b/Sources/FoundationEssentials/AttributedString/AttributedString+UTF8View.swift @@ -73,14 +73,14 @@ extension AttributedString.UTF8View: BidirectionalCollection { } public func index(before i: AttributedString.Index) -> AttributedString.Index { - precondition(i >= startIndex && i <= endIndex, "AttributedString index out of bounds") + precondition(i > startIndex && i <= endIndex, "AttributedString index out of bounds") let j = Index(_guts.string.utf8.index(before: i._value)) precondition(j >= startIndex, "Can't advance AttributedString index before start index") return j } public func index(after i: AttributedString.Index) -> AttributedString.Index { - precondition(i >= startIndex && i <= endIndex, "AttributedString index out of bounds") + precondition(i >= startIndex && i < endIndex, "AttributedString index out of bounds") let j = Index(_guts.string.utf8.index(after: i._value)) precondition(j <= endIndex, "Can't advance AttributedString index after end index") return j @@ -127,7 +127,7 @@ extension AttributedString.UTF8View: BidirectionalCollection { public subscript(bounds: Range) -> Self { let bounds = bounds._bstringRange precondition( - bounds.lowerBound >= _range.lowerBound && bounds.lowerBound < _range.upperBound && + bounds.lowerBound >= _range.lowerBound && bounds.lowerBound <= _range.upperBound && bounds.upperBound >= _range.lowerBound && bounds.upperBound <= _range.upperBound, "AttributedString index range out of bounds") return Self(_guts, in: bounds)