|
| 1 | +// |
| 2 | +// CustomRecursiveStringConvertible.swift |
| 3 | +// OpenSwiftUICore |
| 4 | +// |
| 5 | +// Status: Complete |
| 6 | +// Audited for 6.5.4 |
| 7 | +// ID: 2DFA09903A864CB0F038E089ECDB7AF8 (SwiftUICore) |
| 8 | + |
| 9 | +import Foundation |
| 10 | + |
| 11 | +// MARK: - CustomRecursiveStringConvertible |
| 12 | + |
| 13 | +package protocol CustomRecursiveStringConvertible { |
| 14 | + var descriptionName: String { get } |
| 15 | + |
| 16 | + var descriptionAttributes: [(name: String, value: String)] { get } |
| 17 | + |
| 18 | + var defaultDescriptionAttributes: Set<DefaultDescriptionAttribute> { get } |
| 19 | + |
| 20 | + var descriptionChildren: [any CustomRecursiveStringConvertible] { get } |
| 21 | + |
| 22 | + var hideFromDescription: Bool { get } |
| 23 | +} |
| 24 | + |
| 25 | +extension CustomRecursiveStringConvertible { |
| 26 | + package var defaultDescriptionAttributes: Set<DefaultDescriptionAttribute> { |
| 27 | + DefaultDescriptionAttribute.all |
| 28 | + } |
| 29 | + |
| 30 | + package var descriptionChildren: [any CustomRecursiveStringConvertible] { |
| 31 | + [] |
| 32 | + } |
| 33 | + |
| 34 | + package var hideFromDescription: Bool { |
| 35 | + false |
| 36 | + } |
| 37 | +} |
| 38 | + |
| 39 | +extension CustomRecursiveStringConvertible { |
| 40 | + package var descriptionName: String { |
| 41 | + recursiveDescriptionName(Self.self) |
| 42 | + } |
| 43 | + |
| 44 | + package var descriptionAttributes: [(name: String, value: String)] { |
| 45 | + [] |
| 46 | + } |
| 47 | + |
| 48 | + package var recursiveDescription: String { |
| 49 | + _recursiveDescription(indent: 0, rounded: false) |
| 50 | + } |
| 51 | + |
| 52 | + package var roundedRecursiveDescription: String { |
| 53 | + _recursiveDescription(indent: 0, rounded: true) |
| 54 | + } |
| 55 | + |
| 56 | + package func _recursiveDescription( |
| 57 | + indent: Int, |
| 58 | + rounded: Bool |
| 59 | + ) -> String { |
| 60 | + let indentString = repeatElement(" ", count: indent).joined() |
| 61 | + var attributes = descriptionAttributes |
| 62 | + if rounded { |
| 63 | + attributes = attributes.roundedAttributes() |
| 64 | + } |
| 65 | + attributes.append(contentsOf: indent == 0 ? topLevelAttributes : []) |
| 66 | + let sortedAttributes = attributes.sorted(by: \.name) |
| 67 | + let attributeString = sortedAttributes.isEmpty ? "" : " " + sortedAttributes |
| 68 | + .map { |
| 69 | + let escapedName = $0.name |
| 70 | + .components(separatedBy: .whitespacesAndNewlines) |
| 71 | + .joined(separator: "_") |
| 72 | + .escapeXML() |
| 73 | + let escapedValue = $0.value.escapeXML() |
| 74 | + return #"\#(escapedName)="\#(escapedValue)""# |
| 75 | + } |
| 76 | + .joined(separator: " ") |
| 77 | + let escapedName = descriptionName |
| 78 | + .components(separatedBy: .whitespacesAndNewlines) |
| 79 | + .joined(separator: "_") |
| 80 | + .escapeXML() |
| 81 | + let mark = "\(indentString)<\(escapedName)\(attributeString)" |
| 82 | + if descriptionChildren.isEmpty { |
| 83 | + return "\(mark) />\n" |
| 84 | + } else { |
| 85 | + var result = "\(mark)>\n" |
| 86 | + for child in descriptionChildren { |
| 87 | + guard !child.hideFromDescription else { continue } |
| 88 | + result.append(child._recursiveDescription(indent: indent &+ 1, rounded: rounded)) |
| 89 | + } |
| 90 | + result.append("\(indentString)") |
| 91 | + result.append("</\(escapedName)>\n") |
| 92 | + return result |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + package var topLevelAttributes: [(name: String, value: String)] { |
| 97 | + guard _TestApp.isIntending(to: .includeStatusBar), |
| 98 | + let isHidden = CoreGlue2.shared.isStatusBarHidden() |
| 99 | + else { return [] } |
| 100 | + return [(name: "statusBar", value: isHidden ? "hidden" : "visible")] |
| 101 | + } |
| 102 | +} |
| 103 | + |
| 104 | +// MARK: - BridgeStringConvertible |
| 105 | + |
| 106 | +package protocol BridgeStringConvertible { |
| 107 | + var bridgeDescriptionChildren: [any CustomRecursiveStringConvertible] { get } |
| 108 | +} |
| 109 | + |
| 110 | +extension BridgeStringConvertible { |
| 111 | + package var bridgeDescriptionChildren: [any CustomRecursiveStringConvertible] { [] } |
| 112 | +} |
| 113 | + |
| 114 | +// MARK: - recursiveDescriptionName |
| 115 | + |
| 116 | +package func recursiveDescriptionName(_ type: any Any.Type) -> String { |
| 117 | + var name = "\(type)" |
| 118 | + if name.first == "(" { |
| 119 | + var substring = name.dropFirst() |
| 120 | + if let spaceIndex = substring.firstIndex(of: " ") { |
| 121 | + substring.removeSubrange(spaceIndex...) |
| 122 | + } |
| 123 | + name = String(substring) |
| 124 | + } |
| 125 | + if let angleIndex = name.firstIndex(of: "<") { |
| 126 | + name = String(name[..<angleIndex]) |
| 127 | + } |
| 128 | + return name |
| 129 | +} |
| 130 | + |
| 131 | +// MARK: - String + Extension |
| 132 | + |
| 133 | +extension String { |
| 134 | + package func tupleOfDoubles() -> [(label: String, value: Double)]? { |
| 135 | + guard let first, first == "(", |
| 136 | + let last, last == ")" |
| 137 | + else { return nil } |
| 138 | + |
| 139 | + func decomposeTuple() -> (labels: [String], values: [String]) { |
| 140 | + let inner = dropFirst().dropLast() |
| 141 | + let parts = inner.split(separator: ",", omittingEmptySubsequences: true) |
| 142 | + var labels: [String] = [] |
| 143 | + var values: [String] = [] |
| 144 | + for part in parts { |
| 145 | + if let colonIndex = part.firstIndex(of: ":") { |
| 146 | + let label = String(part[..<colonIndex]).trimmingCharacters(in: .whitespaces) |
| 147 | + let value = String(part[part.index(after: colonIndex)...]).trimmingCharacters(in: .whitespaces) |
| 148 | + labels.append(label) |
| 149 | + values.append(value) |
| 150 | + } else { |
| 151 | + labels.append("") |
| 152 | + values.append(part.trimmingCharacters(in: .whitespaces)) |
| 153 | + } |
| 154 | + } |
| 155 | + return (labels: labels, values: values) |
| 156 | + } |
| 157 | + |
| 158 | + let (labels, values) = decomposeTuple() |
| 159 | + var doubles: [Double] = [] |
| 160 | + for valueString in values { |
| 161 | + guard let value = Double(valueString) else { |
| 162 | + return nil |
| 163 | + } |
| 164 | + doubles.append(value) |
| 165 | + } |
| 166 | + guard labels.count == doubles.count else { return nil } |
| 167 | + return zip(labels, doubles).map { (label: $0, value: $1) } |
| 168 | + } |
| 169 | + |
| 170 | + fileprivate func escapeXML() -> String { |
| 171 | + var result = "" |
| 172 | + result.reserveCapacity(count) |
| 173 | + for char in self { |
| 174 | + switch char { |
| 175 | + case "\"": result.append(""") |
| 176 | + case "&": result.append("&") |
| 177 | + case "'": result.append("'") |
| 178 | + case "<": result.append("<") |
| 179 | + case ">": result.append(">") |
| 180 | + case "\n": result.append("\\n") |
| 181 | + case "\r": result.append("\\r") |
| 182 | + default: result.append(char) |
| 183 | + } |
| 184 | + } |
| 185 | + return result |
| 186 | + } |
| 187 | +} |
| 188 | + |
| 189 | +// MARK: - Sequence.roundedAttributes [?] |
| 190 | + |
| 191 | +extension Sequence where Element == (name: String, value: String) { |
| 192 | + package func roundedAttributes() -> [(name: String, value: String)] { |
| 193 | + map { (name, value) in |
| 194 | + if let doubleValue = Double(value) { |
| 195 | + let rounded = round(doubleValue * 256.0) / 256.0 |
| 196 | + return (name: name, value: rounded.description) |
| 197 | + } else if let tupleValues = value.tupleOfDoubles() { |
| 198 | + let roundedTuple = tupleValues.map { (label: $0.label, value: round($0.value * 256.0) / 256.0) } |
| 199 | + if roundedTuple.count == 4, |
| 200 | + name.range(of: "color", options: .caseInsensitive) != nil |
| 201 | + { |
| 202 | + let floats = roundedTuple.map { Float($0.value) } |
| 203 | + if let colorName = colorNameForColorComponents(floats[0], floats[1], floats[2], floats[3]) { |
| 204 | + return (name: name, value: colorName) |
| 205 | + } |
| 206 | + } |
| 207 | + let parts: [String] = roundedTuple.map { item in |
| 208 | + if item.label.isEmpty { |
| 209 | + return "\(item.value)" |
| 210 | + } else { |
| 211 | + return "\(item.label): \(item.value)" |
| 212 | + } |
| 213 | + } |
| 214 | + return (name: name, value: "(" + parts.joined(separator: ", ") + ")") |
| 215 | + } else { |
| 216 | + return (name, value) |
| 217 | + } |
| 218 | + } |
| 219 | + } |
| 220 | +} |
| 221 | + |
| 222 | +// MARK: - Color.Resolved.name |
| 223 | + |
| 224 | +extension Color.Resolved { |
| 225 | + package var name: String? { |
| 226 | + @inline(__always) |
| 227 | + func quantize(_ value: Float) -> Float { |
| 228 | + round(value * 256.0) / 256.0 |
| 229 | + } |
| 230 | + return colorNameForColorComponents( |
| 231 | + quantize(linearRed), |
| 232 | + quantize(linearGreen), |
| 233 | + quantize(linearBlue), |
| 234 | + quantize(opacity) |
| 235 | + ) |
| 236 | + } |
| 237 | +} |
| 238 | + |
| 239 | +private func colorNameForColorComponents(_ r: Float, _ g: Float, _ b: Float, _ a: Float) -> String? { |
| 240 | + if r == 0 && g == 0 && b == 0 { |
| 241 | + if a == 0 { |
| 242 | + return "clear" |
| 243 | + } else if a == 1 { |
| 244 | + return "black" |
| 245 | + } |
| 246 | + } |
| 247 | + if r == 1 && g == 1 && b == 1 && a == 1 { |
| 248 | + return "white" |
| 249 | + } else if r == 8.0 / 256.0 && g == 8.0 / 256.0 && b == 8.0 / 256.0 && a == 1 { |
| 250 | + return "gray" |
| 251 | + } else if r == 1 && g == 0 && b == 0 && a == 1 { |
| 252 | + return "red" |
| 253 | + } else if r == 1 && g == 11.0 / 256.0 && b == 11.0 / 256.0 && a == 1 { |
| 254 | + return "system-red" |
| 255 | + } else if r == 1 && g == 15.0 / 256.0 && b == 11.0 / 256.0 && a == 1 { |
| 256 | + return "system-red-dark" |
| 257 | + } else if r == 0 && g == 1 && b == 0 && a == 1 { |
| 258 | + return "green" |
| 259 | + } else if r == 0 && g == 0 && b == 1 && a == 1 { |
| 260 | + return "blue" |
| 261 | + } else if r == 1 && g == 1 && b == 0 && a == 1 { |
| 262 | + return "yellow" |
| 263 | + } else if r == 55.0 / 256.0 && g == 0 && b == 55.0 / 256.0 && a == 1 { |
| 264 | + return "purple" |
| 265 | + } else if r == 1 && g == 55.0 / 256.0 && b == 0 && a == 1 { |
| 266 | + return "orange" |
| 267 | + } else if r == 0 && g == 1 && b == 1 && a == 1 { |
| 268 | + return "teal" |
| 269 | + } else if r == 55.0 / 256.0 && g == 55.0 / 256.0 && b == 1 && a == 1 { |
| 270 | + return "indigo" |
| 271 | + } else if r == 1 && g == 0 && b == 55.0 / 256.0 && a == 1 { |
| 272 | + return "pink" |
| 273 | + } else if r == 12.0 / 256.0 && g == 12.0 / 256.0 && b == 14.0 / 256.0 && a == 64.0 / 256.0 { |
| 274 | + return "brown" |
| 275 | + } else if r == 12.0 / 256.0 && g == 12.0 / 256.0 && b == 14.0 / 256.0 && a == 76.0 / 256.0 { |
| 276 | + return "placeholder-text" |
| 277 | + } else { |
| 278 | + return nil |
| 279 | + } |
| 280 | +} |
0 commit comments