File: AttributeScope.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (204 lines) | stat: -rw-r--r-- 7,708 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2023 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
//
//===----------------------------------------------------------------------===//

// Developers can also add the attributes to pre-defined scopes of attributes, which are used to provide type information to the encoding and decoding of AttributedString values, as well as allow for dynamic member lookup in Runs of AttributedStrings.
// Example, where ForegroundColor is an existing AttributedStringKey:
// struct MyAttributes : AttributeScope {
//     var foregroundColor : ForegroundColor
// }
// An AttributeScope can contain other scopes as well.
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
public protocol AttributeScope : DecodingConfigurationProviding, EncodingConfigurationProviding {
    static var decodingConfiguration: AttributeScopeCodableConfiguration { get }
    static var encodingConfiguration: AttributeScopeCodableConfiguration { get }
}

@_nonSendable
@frozen
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
public enum AttributeScopes { }

#if FOUNDATION_FRAMEWORK

import Darwin
internal import MachO.dyld
internal import ReflectionInternal

fileprivate struct ScopeDescription : Sendable {
    var attributes: [String : any AttributedStringKey.Type] = [:]
    var markdownAttributes: [String : any MarkdownDecodableAttributedStringKey.Type] = [:]
    
    mutating func merge(_ other: Self) {
        attributes.merge(other.attributes, uniquingKeysWith: { current, new in new })
        markdownAttributes.merge(other.markdownAttributes, uniquingKeysWith: { current, new in new })
    }
}

fileprivate struct LoadedScopeCache : Sendable {
    private enum ScopeType : Equatable {
        case loaded(any AttributeScope.Type)
        case notLoaded
        
        static func == (lhs: LoadedScopeCache.ScopeType, rhs: LoadedScopeCache.ScopeType) -> Bool {
            switch (lhs, rhs) {
            case (.notLoaded, .notLoaded): true
            case (.loaded(let a), .loaded(let b)): a == b
            default: false
            }
        }
    }
    private var scopeMangledNames : [String : ScopeType]
    private var lastImageCount: UInt32
    private var scopeContents : [Type : ScopeDescription]
    
    init() {
        scopeMangledNames = [:]
        scopeContents = [:]
        lastImageCount = 0
    }
    
    mutating func scopeType(
        for name: String,
        in path: String
    ) -> (any AttributeScope.Type)? {
        if let cached = scopeMangledNames[name] {
            if case .loaded(let foundScope) = cached {
                // We have a cached result, provide it to the caller
                return foundScope
            }
            let currentImageCount = _dyld_image_count()
            if lastImageCount == currentImageCount {
                // We didn't find the scope last time we checked and no new images have been loaded
                return nil
            }
            // We didn't find the scope last time but new images have been loaded so remove all lookup misses from the cache
            lastImageCount = currentImageCount
            scopeMangledNames = scopeMangledNames.filter {
                $0.value != .notLoaded
            }
        }
        
        guard let handle = dlopen(path, RTLD_NOLOAD),
             let symbol = dlsym(handle, name) else {
            scopeMangledNames[name] = .notLoaded
            return nil
        }
        
        guard let type = unsafeBitCast(symbol, to: Any.Type.self) as? any AttributeScope.Type else {
            fatalError("Symbol \(name) is not an AttributeScope type")
        }
        scopeMangledNames[name] = .loaded(type)
        return type
    }
    
    subscript(_ type: any AttributeScope.Type) -> ScopeDescription? {
        get {
            scopeContents[Type(type)]
        }
        set {
            scopeContents[Type(type)] = newValue
        }
    }
}

fileprivate let _loadedScopeCache = LockedState(initialState: LoadedScopeCache())

internal func _loadDefaultAttributes() -> [String : any AttributedStringKey.Type] {
    // On native macOS, the UI framework that gets loaded is AppKit. On
    // macCatalyst however, we load a version of UIKit.
    #if !targetEnvironment(macCatalyst)
    // AppKit
    let macUIScope = (
        "$s10Foundation15AttributeScopesO6AppKitE0dE10AttributesVN",
        "/System/Library/Frameworks/AppKit.framework/AppKit"
    )
    #else
    // UIKit on macOS
    let macUIScope = (
        "$s10Foundation15AttributeScopesO5UIKitE0D10AttributesVN",
        "/System/iOSSupport/System/Library/Frameworks/UIKit.framework/UIKit"
    )
    #endif

    // Gather the metatypes for all scopes currently loaded into the process (may change over time)
    let defaultScopes = _loadedScopeCache.withLock { cache in
        [
            macUIScope,
            // UIKit
            (
                "$s10Foundation15AttributeScopesO5UIKitE0D10AttributesVN",
                "/System/Library/Frameworks/UIKit.framework/UIKit"
            ),
            // SwiftUI
            (
                "$s10Foundation15AttributeScopesO7SwiftUIE0D12UIAttributesVN",
                "/System/Library/Frameworks/SwiftUI.framework/SwiftUI"
            ),
            // Accessibility
            (
                "$s10Foundation15AttributeScopesO13AccessibilityE0D10AttributesVN",
                "/System/Library/Frameworks/Accessibility.framework/Accessibility"
            )
        ].compactMap {
            cache.scopeType(for: $0.0, in: $0.1)
        }
    }
    
    // Walk each scope (checking the cache) and gather each scope's attribute table
    let defaultAttributeTypes = (defaultScopes + [AttributeScopes.FoundationAttributes.self]).map {
        $0.attributeKeyTypes()
    }

    // Merge the attribute tables together into one large table
    return defaultAttributeTypes.reduce([:]) { result, item in
        result.merging(item) { current, new in new }
    }
}

// TODO: Support AttributeScope key finding in FoundationPreview
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
internal extension AttributeScope {
    private static var scopeDescription: ScopeDescription {
        if let cached = _loadedScopeCache.withLock({ $0[Self.self] }) {
            return cached
        }
        
        var desc = ScopeDescription()
        for field in Type(Self.self).fields {
            switch field.type.swiftType {
            case let attribute as any AttributedStringKey.Type:
                desc.attributes[attribute.name] = attribute
                if let markdownAttribute = attribute as? any MarkdownDecodableAttributedStringKey.Type {
                    desc.markdownAttributes[markdownAttribute.markdownName] = markdownAttribute
                }
            case let scope as any AttributeScope.Type:
                desc.merge(scope.scopeDescription)
            default: break
            }
        }
        let _desc = desc
        _loadedScopeCache.withLock {
            $0[Self.self] = _desc
        }
        return desc
    }
    
    static func attributeKeyTypes() -> [String : any AttributedStringKey.Type] {
        Self.scopeDescription.attributes
    }
    
    static func markdownKeyTypes() -> [String : any MarkdownDecodableAttributedStringKey.Type] {
        Self.scopeDescription.markdownAttributes
    }
}

#endif // FOUNDATION_FRAMEWORK