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..c8e254b09 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 } 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[..