//===----------------------------------------------------------------------===// // // This source file is part of the Swift.org open source project // // Copyright (c) 2020 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 import Darwin @_spi(Reflection) import Swift @_implementationOnly import os #endif extension String { @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) public init(_ characters: Slice) { let range = Range(uncheckedBounds: (characters.startIndex, characters.endIndex)) self.init(_from: characters.base._guts.string, in: range._bstringRange) } #if true // FIXME: Make this public. @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) internal init(_characters: AttributedString.CharacterView) { self.init(_from: _characters._guts.string, in: _characters._range._bstringRange) } #else @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) @_alwaysEmitIntoClient public init(_ characters: AttributedString.CharacterView) { if #available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) { self.init(_characters: characters) return } // Forward to the slice overload above, which somehow did end up shipping in // the original AttributedString release. self.init(characters[...]) } @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) @usableFromInline internal init(_characters: AttributedString.CharacterView) { self.init(_from: _characters._guts.string, in: _characters._range._bstringRange) } #endif } #if FOUNDATION_FRAMEWORK @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) public protocol ObjectiveCConvertibleAttributedStringKey : AttributedStringKey { associatedtype ObjectiveCValue : NSObject static func objectiveCValue(for value: Value) throws -> ObjectiveCValue static func value(for object: ObjectiveCValue) throws -> Value } @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) public extension ObjectiveCConvertibleAttributedStringKey where Value : RawRepresentable, Value.RawValue == Int, ObjectiveCValue == NSNumber { static func objectiveCValue(for value: Value) throws -> ObjectiveCValue { return NSNumber(value: value.rawValue) } static func value(for object: ObjectiveCValue) throws -> Value { if let val = Value(rawValue: object.intValue) { return val } throw CocoaError(.coderInvalidValue) } } @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) public extension ObjectiveCConvertibleAttributedStringKey where Value : RawRepresentable, Value.RawValue == String, ObjectiveCValue == NSString { static func objectiveCValue(for value: Value) throws -> ObjectiveCValue { return value.rawValue as NSString } static func value(for object: ObjectiveCValue) throws -> Value { if let val = Value(rawValue: object as String) { return val } throw CocoaError(.coderInvalidValue) } } internal struct _AttributeConversionOptions : OptionSet { let rawValue: Int // If an attribute's value(for: ObjectieCValue) or objectiveCValue(for: Value) function throws, ignore the error and drop the attribute static let dropThrowingAttributes = Self(rawValue: 1 << 0) } @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) public extension AttributeContainer { init(_ dictionary: [NSAttributedString.Key : Any]) { var attributeKeyTypes = _loadDefaultAttributes() // Passing .dropThrowingAttributes causes attributes that throw during conversion to be dropped, so it is safe to do try! here try! self.init(dictionary, including: AttributeScopes.FoundationAttributes.self, attributeKeyTypes: &attributeKeyTypes, options: .dropThrowingAttributes) } init(_ dictionary: [NSAttributedString.Key : Any], including scope: KeyPath) throws { try self.init(dictionary, including: S.self) } init(_ dictionary: [NSAttributedString.Key : Any], including scope: S.Type) throws { var attributeKeyTypes = [String : any AttributedStringKey.Type]() try self.init(dictionary, including: scope, attributeKeyTypes: &attributeKeyTypes) } fileprivate init(_ dictionary: [NSAttributedString.Key : Any], including scope: S.Type, attributeKeyTypes: inout [String : any AttributedStringKey.Type], options: _AttributeConversionOptions = []) throws { storage = .init() for (key, value) in dictionary { if let type = attributeKeyTypes[key.rawValue] ?? S.attributeKeyType(matching: key.rawValue) { attributeKeyTypes[key.rawValue] = type func project(_: K.Type) throws { storage[K.self] = try K._convertFromObjectiveCValue(value as AnyObject) } do { try project(type) } catch let conversionError { if !options.contains(.dropThrowingAttributes) { throw conversionError } } } // else, attribute is not in provided scope, so drop it } } } @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) public extension Dictionary where Key == NSAttributedString.Key, Value == Any { init(_ container: AttributeContainer) { var attributeKeyTypes = _loadDefaultAttributes() // Passing .dropThrowingAttributes causes attributes that throw during conversion to be dropped, so it is safe to do try! here try! self.init(container, including: AttributeScopes.FoundationAttributes.self, attributeKeyTypes: &attributeKeyTypes, options: .dropThrowingAttributes) } init(_ container: AttributeContainer, including scope: KeyPath) throws { try self.init(container, including: S.self) } init(_ container: AttributeContainer, including scope: S.Type) throws { var attributeKeyTypes = [String : any AttributedStringKey.Type]() try self.init(container, including: scope, attributeKeyTypes: &attributeKeyTypes) } // These includingOnly SPI initializers were provided originally when conversion boxed attributes outside of the given scope as an AnyObject // After rdar://80201634, these SPI initializers have the same behavior as the API initializers @_spi(AttributedString) init(_ container: AttributeContainer, includingOnly scope: KeyPath) throws { try self.init(container, including: S.self) } @_spi(AttributedString) init(_ container: AttributeContainer, includingOnly scope: S.Type) throws { try self.init(container, including: S.self) } fileprivate init(_ container: AttributeContainer, including scope: S.Type, attributeKeyTypes: inout [String : any AttributedStringKey.Type], options: _AttributeConversionOptions = []) throws { self.init() for key in container.storage.keys { if let type = attributeKeyTypes[key] ?? S.attributeKeyType(matching: key) { attributeKeyTypes[key] = type func project(_: K.Type) throws { self[NSAttributedString.Key(rawValue: key)] = try K._convertToObjectiveCValue(container.storage[K.self]!) } do { try project(type) } catch let conversionError { if !options.contains(.dropThrowingAttributes) { throw conversionError } } } } } } @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) public extension NSAttributedString { convenience init(_ attrStr: AttributedString) { // Passing .dropThrowingAttributes causes attributes that throw during conversion to be dropped, so it is safe to do try! here try! self.init(attrStr, scope: AttributeScopes.FoundationAttributes.self, otherAttributeTypes: _loadDefaultAttributes(), options: .dropThrowingAttributes) } convenience init(_ attrStr: AttributedString, including scope: KeyPath) throws { try self.init(attrStr, scope: S.self) } convenience init(_ attrStr: AttributedString, including scope: S.Type) throws { try self.init(attrStr, scope: scope) } @_spi(AttributedString) convenience init(_ attrStr: AttributedString, includingOnly scope: KeyPath) throws { try self.init(attrStr, scope: S.self) } @_spi(AttributedString) convenience init(_ attrStr: AttributedString, includingOnly scope: S.Type) throws { try self.init(attrStr, scope: scope) } internal convenience init( _ attrStr: AttributedString, scope: S.Type, otherAttributeTypes: [String : any AttributedStringKey.Type] = [:], options: _AttributeConversionOptions = [] ) throws { // FIXME: Consider making an NSString subclass backed by a _BString let result = NSMutableAttributedString(string: String(_from: attrStr._guts.string)) var attributeKeyTypes: [String : any AttributedStringKey.Type] = otherAttributeTypes // Iterate through each run of the source var nsStartIndex = 0 var stringStart = attrStr._guts.string.startIndex for run in attrStr._guts.runs { let stringEnd = attrStr._guts.string.utf8Index(stringStart, offsetBy: run.length) let utf16Length = attrStr._guts.string.utf16Distance(from: stringStart, to: stringEnd) let range = NSRange(location: nsStartIndex, length: utf16Length) let attributes = try Dictionary(AttributeContainer(run.attributes), including: scope, attributeKeyTypes: &attributeKeyTypes, options: options) result.setAttributes(attributes, range: range) nsStartIndex += utf16Length stringStart = stringEnd } self.init(attributedString: result) } } @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) public extension AttributedString { init(_ nsStr: NSAttributedString) { // Passing .dropThrowingAttributes causes attributes that throw during conversion to be dropped, so it is safe to do try! here try! self.init(nsStr, scope: AttributeScopes.FoundationAttributes.self, otherAttributeTypes: _loadDefaultAttributes(), options: .dropThrowingAttributes) } init(_ nsStr: NSAttributedString, including scope: KeyPath) throws { try self.init(nsStr, scope: S.self) } init(_ nsStr: NSAttributedString, including scope: S.Type) throws { try self.init(nsStr, scope: S.self) } private init(_ nsStr: NSAttributedString, scope: S.Type, otherAttributeTypes: [String : any AttributedStringKey.Type] = [:], options: _AttributeConversionOptions = []) throws { var string = nsStr.string // Eagerly bridge to a native string since AttributedString will do this anyways and this guarantees string's contents are well-formed (no unpaired surrogate characters) string.makeContiguousUTF8() var runs: [_InternalRun] = [] var attributeKeyTypes: [String : any AttributedStringKey.Type] = otherAttributeTypes var conversionError: Error? var endOfLastRange = string.utf16.startIndex var hasConstrainedAttributes = false // Enumerated ranges are guaranteed to be contiguous and have non-zero length nsStr.enumerateAttributes(in: NSMakeRange(0, nsStr.length), options: []) { (nsAttrs, range, stop) in let container: AttributeContainer do { container = try AttributeContainer(nsAttrs, including: scope, attributeKeyTypes: &attributeKeyTypes, options: options) } catch { conversionError = error stop.pointee = true return } var startOfCurrentRun = endOfLastRange var endOfCurrentRun = string.utf16.index(startOfCurrentRun, offsetBy: range.length) endOfLastRange = endOfCurrentRun // If the run ends in the middle of a surrogate pair, extend it to the full range of the pair if UTF16.isLeadSurrogate(string.utf16[string.utf16.index(before: endOfCurrentRun)]) { endOfCurrentRun = string.utf16.index(after: endOfCurrentRun) } // If the run begins in the middle of a surrogate pair, compress it forward to exclude the initial low character // Note: this can result in a zero-length run which is guarded against below if UTF16.isTrailSurrogate(string.utf16[startOfCurrentRun]) { startOfCurrentRun = string.utf16.index(after: startOfCurrentRun) } let runLength = string.utf8.distance(from: startOfCurrentRun, to: endOfCurrentRun) guard runLength > 0 else { return } let attrStorage = container.storage if let previous = runs.last, previous.attributes == attrStorage { runs[runs.endIndex - 1].length += runLength } else { runs.append(_InternalRun(length: runLength, attributes: attrStorage)) if !hasConstrainedAttributes { hasConstrainedAttributes = attrStorage.hasConstrainedAttributes } } } if let error = conversionError { throw error } self = AttributedString(Guts(string: string, runs: runs)) if hasConstrainedAttributes { self._guts.adjustConstrainedAttributesForUntrustedRuns() } } } internal func _loadDefaultAttributes() -> [String : any AttributedStringKey.Type] { #if !targetEnvironment(macCatalyst) // AppKit scope on macOS let macOSSymbol = ("$s10Foundation15AttributeScopesO6AppKitE0dE10AttributesVN", "/usr/lib/swift/libswiftAppKit.dylib") #else // UIKit scope on macOS let macOSSymbol = ("$s10Foundation15AttributeScopesO5UIKitE0D10AttributesVN", "/System/iOSSupport/System/Library/Frameworks/UIKit.framework/UIKit") #endif let loadedScopes = [ macOSSymbol, // UIKit scope on non-macOS ("$s10Foundation15AttributeScopesO5UIKitE0D10AttributesVN", "/System/Library/Frameworks/UIKit.framework/UIKit"), // SwiftUI scope ("$s10Foundation15AttributeScopesO7SwiftUIE0D12UIAttributesVN", "/System/Library/Frameworks/SwiftUI.framework/SwiftUI"), // Accessibility scope ("$s10Foundation15AttributeScopesO13AccessibilityE0D10AttributesVN", "/System/Library/Frameworks/Accessibility.framework/Accessibility") ].compactMap { _loadScopeAttributes(forSymbol: $0.0, from: $0.1) } return loadedScopes.reduce([:]) { result, item in result.merging(item) { current, new in new } } } fileprivate struct LoadedScopeCache : Sendable { var cache : [String : [String : any AttributedStringKey.Type]] } fileprivate let _loadedScopeCacheLock = OSAllocatedUnfairLock(initialState: LoadedScopeCache(cache: .init())) fileprivate func _loadScopeAttributes(forSymbol symbol: String, from path: String) -> [String : any AttributedStringKey.Type]? { return _loadedScopeCacheLock.withLock { cache in if let cachedResult = cache.cache[symbol] { return cachedResult } guard let handle = dlopen(path, RTLD_NOLOAD) else { return nil } guard let symbolPointer = dlsym(handle, symbol) else { return nil } guard let scopeType = unsafeBitCast(symbolPointer, to: Any.Type.self) as? any AttributeScope.Type else { return nil } let attributeTypes = _loadAttributeTypes(from: scopeType) cache.cache[symbol] = attributeTypes return attributeTypes } } fileprivate func _loadAttributeTypes(from scope: S.Type) -> [String : any AttributedStringKey.Type] { var result = [String : any AttributedStringKey.Type]() _forEachField(of: scope, options: [.ignoreUnknown]) { pointer, offset, type, kind -> Bool in switch type { case let attribute as any AttributedStringKey.Type: result[attribute.name] = attribute break case let scope as any AttributeScope.Type: result.merge(_loadAttributeTypes(from: scope), uniquingKeysWith: { current, new in new }) break default: break } return true } return result } #endif // FOUNDATION_FRAMEWORK @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) extension String.Index { // FIXME: Converting indices between different collection types does not make sense. // FIXME: (Indices are meaningless without the collection value to which they belong, // FIXME: and this entry point is not given that.) // FIXME: This API ought to be deprecated, with clients migrating to e.g. using UTF-8 offsets. public init?(_ sourcePosition: AttributedString.Index, within target: S) { let utf8Offset = sourcePosition._value._utf8Offset let isTrailingSurrogate = sourcePosition._value._isUTF16TrailingSurrogate let i = String.Index(_utf8Offset: utf8Offset, utf16TrailingSurrogate: isTrailingSurrogate) self.init(i, within: target) } } @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) extension AttributedString.Index { // FIXME: Converting indices between different collection types does not make sense. // FIXME: (Indices are meaningless without the collection value to which they belong, // FIXME: and this entry point is not given that.) // FIXME: This API ought to be deprecated, with clients migrating to e.g. using UTF-8 offsets. public init?(_ sourcePosition: String.Index, within target: S) { guard let i = target.__guts.string.index(from: sourcePosition), i >= target.startIndex._value, i <= target.endIndex._value else { return nil } let j = target.__guts.string.characterIndex(roundingDown: i) guard j == i else { return nil } self.init(j) } } #if FOUNDATION_FRAMEWORK @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) extension NSRange { public init( _ region: R, in target: S ) where R.Bound == AttributedString.Index { let range = region.relative(to: target.characters) precondition( range.lowerBound >= target.startIndex && range.upperBound <= target.endIndex, "Range out of bounds") let str = target.__guts.string let utf16Base = str.utf16Offset(of: target.startIndex._value) let utf16Start = str.utf16Offset(of: range.lowerBound._value) let utf16Length = str.utf16Distance( from: range.lowerBound._value, to: range.upperBound._value) self.init(location: utf16Start - utf16Base, length: utf16Length) } @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) public init?(_ markdownSourcePosition: AttributedString.MarkdownSourcePosition, in target: S) { let startOffsets: AttributedString.MarkdownSourcePosition.Offsets let endOffsets: AttributedString.MarkdownSourcePosition.Offsets if let start = markdownSourcePosition.startOffsets, let end = markdownSourcePosition.endOffsets { startOffsets = start endOffsets = end } else { guard let offsets = markdownSourcePosition.calculateOffsets(within: target) else { self.init(location: NSNotFound, length: NSNotFound) return } startOffsets = offsets.start endOffsets = offsets.end } // Since bounds are inclusive, we need to advance to the next UTF-16 scalar // If doing so will leave a hanging high surrogate value (i.e., the UTF-8 offset was within a code point), then don't advance var actualEndUTF16 = startOffsets.utf16 if endOffsets.utf8 + 1 == endOffsets.utf8NextCodePoint { actualEndUTF16 += endOffsets.utf16CurrentCodePointLength } self.init(location: startOffsets.utf16, length: actualEndUTF16 - startOffsets.utf16) } } #endif // FOUNDATION_FRAMEWORK extension AttributedString { /// A dummy collection type whose only purpose is to facilitate a `RangeExpression.relative(to:)` /// call that takes a range expression with string indices but needs to work on an /// attributed string. internal struct _IndexConverterFromString: Collection { typealias Index = String.Index typealias Element = Index let _string: _BString let _range: Range<_BString.Index> init(_ string: _BString, _ range: Range<_BString.Index>) { self._string = string self._range = range } subscript(position: String.Index) -> String.Index { position } var startIndex: String.Index { Index(_utf8Offset: _range.lowerBound._utf8Offset) } var endIndex: String.Index { Index(_utf8Offset: _range.upperBound._utf8Offset) } func index(after i: String.Index) -> Index { guard let j = _string.index(from: i) else { preconditionFailure("Index out of bounds") } let k = _string.characterIndex(after: j) return Index(_utf8Offset: k._utf8Offset) } } } extension _BString { func index(from stringIndex: String.Index) -> Index? { if stringIndex._canBeUTF8 { let utf8Offset = stringIndex._utf8Offset let utf16Delta = stringIndex._utf16Delta guard utf16Delta <= 1 else { return nil } let j = _BString.Index(_utf8Offset: utf8Offset, utf16TrailingSurrogate: utf16Delta > 0) guard j <= endIndex else { return nil } // Note: if utf16Delta > 0, ideally we should also check that the result // addresses a trailing surrogate. return j } let utf16Offset = stringIndex._abi_encodedOffset let utf8Delta = stringIndex._abi_transcodedOffset guard utf16Offset <= self.utf16Count else { return nil } let j = utf16Index(at: utf16Offset) guard utf8Delta > 0 else { return j } // Note: if utf8Delta > 0, ideally we should also check that the result // addresses a scalar that actually does have that many continuation bytes. return Index( baseUTF8Offset: j._utf8BaseOffset, rope: j._rope!, chunk: String.Index(_utf8Offset: j._utf8ChunkOffset + utf8Delta)) } func stringIndex(from index: Index) -> String.Index? { String.Index( _utf8Offset: index._utf8Offset, utf16TrailingSurrogate: index._isUTF16TrailingSurrogate) } } @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) extension Range where Bound == AttributedString.Index { #if FOUNDATION_FRAMEWORK public init?(_ range: NSRange, in string: S) { // FIXME: This can return indices addressing trailing surrogates, which isn't a thing // FIXME: AttributedString is normally prepared to handle. // FIXME: Consider rounding everything down to the nearest scalar boundary. guard range.location != NSNotFound else { return nil } guard range.location >= 0, range.length >= 0 else { return nil } let endOffset = range.location + range.length guard endOffset <= string.__guts.string.utf16Count else { return nil } let start = string.__guts.string.utf16Index(at: range.location) let end = string.__guts.string.utf16Index(start, offsetBy: range.length) guard start >= string.startIndex._value, end <= string.endIndex._value else { return nil } self.init(uncheckedBounds: (.init(start), .init(end))) } #endif // FOUNDATION_FRAMEWORK // FIXME: Converting indices between different collection types does not make sense. // FIXME: (Indices are meaningless without the collection value to which they belong, // FIXME: and this entry point is not given that.) // FIXME: This API ought to be deprecated, with clients migrating to using UTF-8 offsets. public init?( _ region: R, in attributedString: S ) where R.Bound == String.Index { if let range = region as? Range { self.init(_range: range, in: attributedString) return } // This is a frustrating API to implement -- we need to convert String indices to // AttributedString indices, but the only way for us to access the actual indices is to // go through `RangeExpression.relative(to:)`, which requires a collection value with a // matching index type. So we need to construct a dummy collection type for just that. let dummy = AttributedString._IndexConverterFromString( attributedString.__guts.string, attributedString.startIndex._value ..< attributedString.endIndex._value) let range = region.relative(to: dummy) self.init(_range: range, in: attributedString) } // The FIXME above also applies to this internal initializer. internal init?( _range: Range, in attributedString: some AttributedStringProtocol ) { guard let lower = attributedString.__guts.string.index(from: _range.lowerBound), let upper = attributedString.__guts.string.index(from: _range.upperBound), lower >= attributedString.startIndex._value, upper <= attributedString.endIndex._value else { return nil } self.init(uncheckedBounds: (.init(lower), .init(upper))) } } extension AttributedString { /// A dummy collection type whose only purpose is to facilitate a `RangeExpression.relative(to:)` /// call that takes a range expression with attributed string indices but needs to work on a /// regular string. internal struct _IndexConverterFromAttributedString: Collection { typealias Index = AttributedString.Index typealias Element = Index let string: Substring init(_ string: Substring) { self.string = string } subscript(position: Index) -> Index { position } var startIndex: Index { Index(_BString.Index(_utf8Offset: string.startIndex._utf8Offset)) } var endIndex: Index { Index(_BString.Index(_utf8Offset: string.endIndex._utf8Offset)) } func index(after i: Index) -> Index { let j = String.Index(_utf8Offset: i._value._utf8Offset) let k = string.index(after: j) return Index(_BString.Index(_utf8Offset: k._utf8Offset)) } } } @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) extension Range where Bound == String.Index { // FIXME: Converting indices between different collection types does not make sense. // FIXME: (Indices are meaningless without the collection value to which they belong, // FIXME: and this entry point is not given that.) // FIXME: This API ought to be deprecated, with clients migrating to using UTF-8 offsets. public init?( _ region: R, in string: S ) where R.Bound == AttributedString.Index { if let range = region as? Range { self.init(_range: range, in: Substring(string)) return } let str = Substring(string) let dummy = AttributedString._IndexConverterFromAttributedString(str) let range = region.relative(to: dummy) self.init(_range: range, in: str) } // The FIXME above also applies to this internal initializer. internal init?( _range: Range, in string: Substring ) { // Note: Attributed string indices are usually going to get implicitly round down to // (at least) the nearest scalar boundary, but NSRange conversions can still generate // indices addressing trailing surrogates, and we want to preserve those here. let start = String.Index( _utf8Offset: _range.lowerBound._value._utf8Offset, utf16TrailingSurrogate: _range.lowerBound._value._isUTF16TrailingSurrogate) let end = String.Index( _utf8Offset: _range.upperBound._value._utf8Offset, utf16TrailingSurrogate: _range.upperBound._value._isUTF16TrailingSurrogate) guard string.startIndex <= start, end <= string.endIndex else { return nil } self.init(uncheckedBounds: (start, end)) } #if FOUNDATION_FRAMEWORK // TODO: Support AttributedString markdown in FoundationPreview @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) public init?(_ markdownSourcePosition: AttributedString.MarkdownSourcePosition, in target: S) { if let start = markdownSourcePosition.startOffsets, let end = markdownSourcePosition.endOffsets { self = target.utf8.index(target.startIndex, offsetBy: start.utf8) ..< target.utf8.index(target.startIndex, offsetBy: end.utf8 + 1) } else { guard let offsets = markdownSourcePosition.calculateOffsets(within: target) else { return nil } self = target.utf8.index(target.startIndex, offsetBy: offsets.start.utf8) ..< target.utf8.index(target.startIndex, offsetBy: offsets.end.utf8 + 1) } } #endif // FOUNDATION_FRAMEWORK }