From 76c332b428be6d33fc80af36983e45769232b4d9 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 28 Dec 2025 19:15:24 +0800 Subject: [PATCH 1/9] Implement DefaultDescriptionAttribute --- .../Util/DefaultDescriptionAttribute.swift | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 Sources/OpenSwiftUICore/Util/DefaultDescriptionAttribute.swift diff --git a/Sources/OpenSwiftUICore/Util/DefaultDescriptionAttribute.swift b/Sources/OpenSwiftUICore/Util/DefaultDescriptionAttribute.swift new file mode 100644 index 000000000..b548d0f11 --- /dev/null +++ b/Sources/OpenSwiftUICore/Util/DefaultDescriptionAttribute.swift @@ -0,0 +1,77 @@ +// +// DefaultDescriptionAttribute.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +// MARK: - DefaultDescriptionAttribute + +package enum DefaultDescriptionAttribute: String, CaseIterable { + case rect + case origin + case startPoint + case endPoint + case transform + case clips + case cornerRadius + case continuousCorners + case opacity + case borderWidth + case borderColor + case backgroundColor + case compositingFilter + case disableUpdates + case shadowOpacity + case shadowRadius + case shadowColor + case shadowOffset + case shadowPath + case shadowPathIsBounds + case contentsCenter + case contentsScaling + case contentsMultiplyColor + case colorScheme + case filters + case gradientType + case gradientColors + case gradientLocations + case gradientInterpolations + + package static var all: Set { + var cases = Set(Self.allCases) + if _TestApp.isIntending(to: .ignoreGeometry) { + cases.subtract(Self.geometry) + } + if _TestApp.isIntending(to: .ignoreCornerRadius) { + cases.subtract(Self.relatedToCornerRadius) + } + if !_TestApp.isIntending(to: .includeContinuousCorners) { + cases.remove(.continuousCorners) + } + if !_TestApp.isIntending(to: .includeExtendedContents) { + cases.remove(.contentsMultiplyColor) + } + if !_TestApp.isIntending(to: .includeExtendedGradients) { + cases.remove(.gradientType) + cases.remove(.gradientColors) + cases.remove(.gradientLocations) + cases.remove(.gradientInterpolations) + } + if _TestApp.isIntending(to: .ignoreOpacity) { + cases.remove(.opacity) + } + if _TestApp.isIntending(to: .ignoreCompositingFilters) { + cases.subtract([.filters, .compositingFilter]) + } + return Set(cases) + } + + package static var geometry: Set { + [.rect, .origin, .startPoint, .endPoint, .transform] + } + + package static var relatedToCornerRadius: Set { + [.cornerRadius, .continuousCorners] + } +} From 2e77ce0ef97c634332231c0e6e96cff74aa31e42 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 28 Dec 2025 19:55:49 +0800 Subject: [PATCH 2/9] Add CustomRecursiveStringConvertible --- .../CustomRecursiveStringConvertible.swift | 136 ++++++++++++++++++ ...ustomRecursiveStringConvertibleTests.swift | 64 +++++++++ 2 files changed, 200 insertions(+) create mode 100644 Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift create mode 100644 Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift diff --git a/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift b/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift new file mode 100644 index 000000000..26d051f7d --- /dev/null +++ b/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift @@ -0,0 +1,136 @@ +// +// CustomRecursiveStringConvertible.swift +// OpenSwiftUICore +// +// Status: WIP +// Audited for 6.5.4 +// ID: 2DFA09903A864CB0F038E089ECDB7AF8 (SwiftUICore) + +import Foundation + +// MARK: - CustomRecursiveStringConvertible [WIP] + +package protocol CustomRecursiveStringConvertible { + var descriptionName: String { get } + + var descriptionAttributes: [(name: String, value: String)] { get } + + var defaultDescriptionAttributes: Set { get } + + var descriptionChildren: [any CustomRecursiveStringConvertible] { get } + + var hideFromDescription: Bool { get } +} + +extension CustomRecursiveStringConvertible { + package var defaultDescriptionAttributes: Set { + DefaultDescriptionAttribute.all + } + + package var descriptionChildren: [any CustomRecursiveStringConvertible] { + [] + } + + package var hideFromDescription: Bool { + false + } +} + +extension CustomRecursiveStringConvertible { + package var descriptionName: String { + recursiveDescriptionName(Self.self) + } + + package var descriptionAttributes: [(name: String, value: String)] { + [] + } + + package var recursiveDescription: String { + _recursiveDescription(indent: 0, rounded: false) + } + + package var roundedRecursiveDescription: String { + _recursiveDescription(indent: 0, rounded: true) + } + + package func _recursiveDescription( + indent: Int, + rounded: Bool + ) -> String { + _openSwiftUIUnimplementedFailure() + } + + package var topLevelAttributes: [(name: String, value: String)] { + _openSwiftUIUnimplementedFailure() + } +} + +// MARK: - BridgeStringConvertible + +package protocol BridgeStringConvertible { + var bridgeDescriptionChildren: [any CustomRecursiveStringConvertible] { get } +} + +extension BridgeStringConvertible { + package var bridgeDescriptionChildren: [any CustomRecursiveStringConvertible] { [] } +} + +// MARK: - CustomRecursiveStringConvertible Helpers [WIP] + +package func recursiveDescriptionName(_ type: any Any.Type) -> String { + _openSwiftUIUnimplementedFailure() +} + +extension String { + package func tupleOfDoubles() -> [(label: String, value: Double)]? { + guard let first, first == "(", + let last, last == ")" + else { return nil } + + func decomposeTuple() -> (labels: [String], values: [String]) { + let inner = dropFirst().dropLast() + let parts = inner.split(separator: ",", omittingEmptySubsequences: true) + var labels: [String] = [] + var values: [String] = [] + for part in parts { + if let colonIndex = part.firstIndex(of: ":") { + let label = String(part[.. [(name: String, value: String)] { + preconditionFailure("TODO") + } +} + +extension Color.Resolved { + package var name: String? { + _openSwiftUIUnimplementedFailure() + } +} + +private func colorNameForColorComponents(_ r: Float, _ g: Float, _ b: Float, _ a: Float) -> String? { + nil +} diff --git a/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift b/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift new file mode 100644 index 000000000..c160e3efc --- /dev/null +++ b/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift @@ -0,0 +1,64 @@ +// +// CustomRecursiveStringConvertibleTests.swift +// OpenSwiftUICoreTests + +import Testing +@testable import OpenSwiftUICore + +struct CustomRecursiveStringConvertibleTests { + // MARK: - String.tupleOfDoubles Tests + + @Test(arguments: [ + // Valid tuples with labeled elements + ("(x: 1.0, y: 2.0)", [("x", 1.0), ("y", 2.0)]), + ("(width: 100, height: 200)", [("width", 100.0), ("height", 200.0)]), + ("(a: 0.5)", [("a", 0.5)]), + // With negative values + ("(x: -1.0, y: -2.5)", [("x", -1.0), ("y", -2.5)]), + // With scientific notation + ("(value: 1e10)", [("value", 1e10)]), + // Multiple elements + ("(a: 1, b: 2, c: 3)", [("a", 1.0), ("b", 2.0), ("c", 3.0)]), + // With whitespace variations + ("( x: 1.0 , y: 2.0 )", [("x", 1.0), ("y", 2.0)]), + ]) + func tupleOfDoublesValid(input: String, expected: [(String, Double)]) { + let result = input.tupleOfDoubles() + #expect(result != nil) + guard let result else { return } + #expect(result.count == expected.count) + for (index, element) in result.enumerated() { + #expect(element.label == expected[index].0) + #expect(element.value.isApproximatelyEqual(to: expected[index].1)) + } + } + + @Test(arguments: [ + // Missing opening parenthesis + "x: 1.0, y: 2.0)", + // Missing closing parenthesis + "(x: 1.0, y: 2.0", + // No parentheses + "x: 1.0, y: 2.0", + // Invalid double value + "(x: abc, y: 2.0)", + // Empty string + "", + // Just parentheses with no content - though this might parse to empty array + // Wrong bracket types + "[x: 1.0, y: 2.0]", + ]) + func tupleOfDoublesInvalid(input: String) { + let result = input.tupleOfDoubles() + #expect(result == nil) + } + + @Test + func tupleOfDoublesEmptyParens() { + // Empty parens should return empty array (not nil) + let result = "()".tupleOfDoubles() + #expect(result != nil) + #expect(result?.isEmpty == true) + } +} + From f1e66492f40f25d828362565532007b139885f18 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 28 Dec 2025 20:37:10 +0800 Subject: [PATCH 3/9] Add Color.Resolved.name --- .../CustomRecursiveStringConvertible.swift | 52 ++++++++++++++++++- ...ustomRecursiveStringConvertibleTests.swift | 37 +++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift b/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift index 26d051f7d..010c6d6ea 100644 --- a/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift +++ b/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift @@ -127,10 +127,58 @@ extension Sequence where Element == (name: String, value: String) { extension Color.Resolved { package var name: String? { - _openSwiftUIUnimplementedFailure() + @inline(__always) + func quantize(_ value: Float) -> Float { + round(value * 256.0) / 256.0 + } + return colorNameForColorComponents( + quantize(linearRed), + quantize(linearGreen), + quantize(linearBlue), + quantize(opacity) + ) } } private func colorNameForColorComponents(_ r: Float, _ g: Float, _ b: Float, _ a: Float) -> String? { - nil + if r == 0 && g == 0 && b == 0 { + if a == 0 { + return "clear" + } else if a == 1 { + return "black" + } + } + if r == 1 && g == 1 && b == 1 && a == 1 { + return "white" + } else if r == 8.0 / 256.0 && g == 8.0 / 256.0 && b == 8.0 / 256.0 && a == 1 { + return "gray" + } else if r == 1 && g == 0 && b == 0 && a == 1 { + return "red" + } else if r == 1 && g == 11.0 / 256.0 && b == 11.0 / 256.0 && a == 1 { + return "system-red" + } else if r == 1 && g == 15.0 / 256.0 && b == 11.0 / 256.0 && a == 1 { + return "system-red-dark" + } else if r == 0 && g == 1 && b == 0 && a == 1 { + return "green" + } else if r == 0 && g == 0 && b == 1 && a == 1 { + return "blue" + } else if r == 1 && g == 1 && b == 0 && a == 1 { + return "yellow" + } else if r == 55.0 / 256.0 && g == 0 && b == 55.0 / 256.0 && a == 1 { + return "purple" + } else if r == 1 && g == 55.0 / 256.0 && b == 0 && a == 1 { + return "orange" + } else if r == 0 && g == 1 && b == 1 && a == 1 { + return "teal" + } else if r == 55.0 / 256.0 && g == 55.0 / 256.0 && b == 1 && a == 1 { + return "indigo" + } else if r == 1 && g == 0 && b == 55.0 / 256.0 && a == 1 { + return "pink" + } else if r == 12.0 / 256.0 && g == 12.0 / 256.0 && b == 14.0 / 256.0 && a == 64.0 / 256.0 { + return "brown" + } else if r == 12.0 / 256.0 && g == 12.0 / 256.0 && b == 14.0 / 256.0 && a == 76.0 / 256.0 { + return "placeholder-text" + } else { + return nil + } } diff --git a/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift b/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift index c160e3efc..5dcf43861 100644 --- a/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift +++ b/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift @@ -60,5 +60,42 @@ struct CustomRecursiveStringConvertibleTests { #expect(result != nil) #expect(result?.isEmpty == true) } + + // MARK: - Color.Resolved.name Tests + + @Test(arguments: [ + // Basic colors + (Float(0), Float(0), Float(0), Float(0), "clear"), + (Float(0), Float(0), Float(0), Float(1), "black"), + (Float(1), Float(1), Float(1), Float(1), "white"), + (Float(8.0 / 256.0), Float(8.0 / 256.0), Float(8.0 / 256.0), Float(1), "gray"), + (Float(1), Float(0), Float(0), Float(1), "red"), + (Float(0), Float(1), Float(0), Float(1), "green"), + (Float(0), Float(0), Float(1), Float(1), "blue"), + (Float(1), Float(1), Float(0), Float(1), "yellow"), + (Float(0), Float(1), Float(1), Float(1), "teal"), + // System colors + (Float(1), Float(11.0 / 256.0), Float(11.0 / 256.0), Float(1), "system-red"), + (Float(1), Float(15.0 / 256.0), Float(11.0 / 256.0), Float(1), "system-red-dark"), + // Extended colors + (Float(55.0 / 256.0), Float(0), Float(55.0 / 256.0), Float(1), "purple"), + (Float(1), Float(55.0 / 256.0), Float(0), Float(1), "orange"), + (Float(55.0 / 256.0), Float(55.0 / 256.0), Float(1), Float(1), "indigo"), + (Float(1), Float(0), Float(55.0 / 256.0), Float(1), "pink"), + (Float(12.0 / 256.0), Float(12.0 / 256.0), Float(14.0 / 256.0), Float(64.0 / 256.0), "brown"), + (Float(12.0 / 256.0), Float(12.0 / 256.0), Float(14.0 / 256.0), Float(76.0 / 256.0), "placeholder-text"), + // Quantization: slight variations round to named color + (Float(0.999), Float(0.001), Float(0.001), Float(0.999), "red"), + ] as [(Float, Float, Float, Float, String)]) + func colorResolvedName(r: Float, g: Float, b: Float, a: Float, expected: String) { + let color = Color.Resolved(linearRed: r, linearGreen: g, linearBlue: b, opacity: a) + #expect(color.name == expected) + } + + @Test + func colorResolvedNameUnknown() { + let color = Color.Resolved(linearRed: 0.5, linearGreen: 0.3, linearBlue: 0.7, opacity: 1) + #expect(color.name == nil) + } } From 58de127529d848decef88ecbe9e4813fb7745e76 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 28 Dec 2025 20:56:44 +0800 Subject: [PATCH 4/9] Implement recursiveDescriptionName --- .../CustomRecursiveStringConvertible.swift | 21 +++++++++++++++++-- ...ustomRecursiveStringConvertibleTests.swift | 20 ++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift b/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift index 010c6d6ea..308ef89b9 100644 --- a/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift +++ b/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift @@ -75,12 +75,25 @@ extension BridgeStringConvertible { package var bridgeDescriptionChildren: [any CustomRecursiveStringConvertible] { [] } } -// MARK: - CustomRecursiveStringConvertible Helpers [WIP] +// MARK: - recursiveDescriptionName package func recursiveDescriptionName(_ type: any Any.Type) -> String { - _openSwiftUIUnimplementedFailure() + var name = "\(type)" + if name.first == "(" { + var substring = name.dropFirst() + if let spaceIndex = substring.firstIndex(of: " ") { + substring.removeSubrange(spaceIndex...) + } + name = String(substring) + } + if let angleIndex = name.firstIndex(of: "<") { + name = String(name[.. [(label: String, value: Double)]? { guard let first, first == "(", @@ -119,12 +132,16 @@ extension String { } } +// MARK: - Sequence.roundedAttributes + extension Sequence where Element == (name: String, value: String) { package func roundedAttributes() -> [(name: String, value: String)] { preconditionFailure("TODO") } } +// MARK: - Color.Resolved.name + extension Color.Resolved { package var name: String? { @inline(__always) diff --git a/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift b/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift index 5dcf43861..30c5e5e1a 100644 --- a/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift +++ b/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift @@ -6,6 +6,26 @@ import Testing @testable import OpenSwiftUICore struct CustomRecursiveStringConvertibleTests { + // MARK: - recursiveDescriptionName Tests + + private struct PrivateType {} + struct SimpleType {} + struct GenericType {} + + @Test(arguments: [ + (PrivateType.self, "PrivateType"), + (SimpleType.self, "SimpleType"), + (GenericType.self, "GenericType"), + (Int.self, "Int"), + (String.self, "String"), + (Array.self, "Array"), + (Dictionary.self, "Dictionary"), + ((Int, String).self, "Int,"), + ] as [(Any.Type, String)]) + func recursiveDescriptionNameTests(type: Any.Type, expected: String) { + #expect(recursiveDescriptionName(type) == expected) + } + // MARK: - String.tupleOfDoubles Tests @Test(arguments: [ From 4f1d1b055948c89144782da0178a0c46219b8c4b Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 28 Dec 2025 21:27:43 +0800 Subject: [PATCH 5/9] Add roundedAttributes --- .../CustomRecursiveStringConvertible.swift | 29 +++++++- ...ustomRecursiveStringConvertibleTests.swift | 71 ++++++++++++++++++- 2 files changed, 97 insertions(+), 3 deletions(-) diff --git a/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift b/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift index 308ef89b9..1b8278fc4 100644 --- a/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift +++ b/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift @@ -132,11 +132,36 @@ extension String { } } -// MARK: - Sequence.roundedAttributes +// MARK: - Sequence.roundedAttributes [?] extension Sequence where Element == (name: String, value: String) { package func roundedAttributes() -> [(name: String, value: String)] { - preconditionFailure("TODO") + map { (name, value) in + if let doubleValue = Double(value) { + let rounded = round(doubleValue * 256.0) / 256.0 + return (name: name, value: rounded.description) + } else if let tupleValues = value.tupleOfDoubles() { + let roundedTuple = tupleValues.map { (label: $0.label, value: round($0.value * 256.0) / 256.0) } + if roundedTuple.count == 4, + name.range(of: "color", options: .caseInsensitive) != nil + { + let floats = roundedTuple.map { Float($0.value) } + if let colorName = colorNameForColorComponents(floats[0], floats[1], floats[2], floats[3]) { + return (name: name, value: colorName) + } + } + let parts: [String] = roundedTuple.map { item in + if item.label.isEmpty { + return "\(item.value)" + } else { + return "\(item.label): \(item.value)" + } + } + return (name: name, value: "(" + parts.joined(separator: ", ") + ")") + } else { + return (name, value) + } + } } } diff --git a/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift b/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift index 30c5e5e1a..8abd37070 100644 --- a/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift +++ b/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift @@ -117,5 +117,74 @@ struct CustomRecursiveStringConvertibleTests { let color = Color.Resolved(linearRed: 0.5, linearGreen: 0.3, linearBlue: 0.7, opacity: 1) #expect(color.name == nil) } -} + // MARK: - Sequence.roundedAttributes Tests + + @Test + func roundedAttributesSimpleDouble() { + let attrs: [(name: String, value: String)] = [ + (name: "width", value: "100.123456789"), + (name: "height", value: "200.0"), + ] + let result = attrs.roundedAttributes() + #expect(result.count == 2) + // 100.123456789 * 256 = 25631.60493... → rounds to 25632 → 25632/256 = 100.125 + #expect(result[0].name == "width") + #expect(result[0].value == "100.125") + #expect(result[1].name == "height") + #expect(result[1].value == "200.0") + } + + @Test + func roundedAttributesTupleOfDoubles() { + let attrs: [(name: String, value: String)] = [ + (name: "position", value: "(x: 10.123456, y: 20.987654)"), + ] + let result = attrs.roundedAttributes() + #expect(result.count == 1) + #expect(result[0].name == "position") + // 10.123456 * 256 = 2591.60 → 2592 → 10.125 + // 20.987654 * 256 = 5372.84 → 5373 → 20.98828125 + #expect(result[0].value == "(x: 10.125, y: 20.98828125)") + } + + @Test + func roundedAttributesColorDetection() { + // Color with RGBA values that match "red" + let attrs: [(name: String, value: String)] = [ + (name: "foregroundColor", value: "(1.0, 0.0, 0.0, 1.0)"), + ] + let result = attrs.roundedAttributes() + #expect(result.count == 1) + #expect(result[0].name == "foregroundColor") + #expect(result[0].value == "red") + } + + @Test + func roundedAttributesColorNotMatched() { + // Color values that don't match any known color + let attrs: [(name: String, value: String)] = [ + (name: "someColor", value: "(0.5, 0.3, 0.7, 1.0)"), + ] + let result = attrs.roundedAttributes() + #expect(result.count == 1) + #expect(result[0].name == "someColor") + // Should fall back to tuple format with rounded values + #expect(result[0].value == "(0.5, 0.30078125, 0.69921875, 1.0)") + } + + @Test + func roundedAttributesNonNumeric() { + // Non-numeric values should be returned unchanged + let attrs: [(name: String, value: String)] = [ + (name: "title", value: "Hello World"), + (name: "isEnabled", value: "true"), + ] + let result = attrs.roundedAttributes() + #expect(result.count == 2) + #expect(result[0].name == "title") + #expect(result[0].value == "Hello World") + #expect(result[1].name == "isEnabled") + #expect(result[1].value == "true") + } +} From da8163015265e213115afc08dfa8c62a974b9674 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 28 Dec 2025 21:32:10 +0800 Subject: [PATCH 6/9] Update .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2622fa6b4..0ffb55b0f 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ Example/ReferenceImages/**/*.png .docs gh-pages .swift_pd_guess -.claude/commands/guess_filename.md \ No newline at end of file +.claude/commands/guess_filename.md +/.augments \ No newline at end of file From da1dafe3f14bcf2304f282f8bdc6a125ec97c69a Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 28 Dec 2025 21:47:50 +0800 Subject: [PATCH 7/9] Add escapeXML --- .../CustomRecursiveStringConvertible.swift | 20 +++++++++++++- ...ustomRecursiveStringConvertibleTests.swift | 27 +++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift b/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift index 1b8278fc4..efea7a5c9 100644 --- a/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift +++ b/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift @@ -92,7 +92,7 @@ package func recursiveDescriptionName(_ type: any Any.Type) -> String { return name } -// MARK: - String.tupleOfDoubles +// MARK: - String + Extension extension String { package func tupleOfDoubles() -> [(label: String, value: Double)]? { @@ -130,6 +130,24 @@ extension String { guard labels.count == doubles.count else { return nil } return zip(labels, doubles).map { (label: $0, value: $1) } } + + fileprivate func escapeXML() -> String { + var result = "" + result.reserveCapacity(count) + for char in self { + switch char { + case "\"": result.append(""") + case "&": result.append("&") + case "'": result.append("'") + case "<": result.append("<") + case ">": result.append(">") + case "\n": result.append("\\n") + case "\r": result.append("\\r") + default: result.append(char) + } + } + return result + } } // MARK: - Sequence.roundedAttributes [?] diff --git a/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift b/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift index 8abd37070..1233ce93d 100644 --- a/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift +++ b/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift @@ -2,8 +2,12 @@ // CustomRecursiveStringConvertibleTests.swift // OpenSwiftUICoreTests +@testable +#if OPENSWIFTUI_ENABLE_PRIVATE_IMPORTS +@_private(sourceFile: "CustomRecursiveStringConvertible.swift") +#endif +import OpenSwiftUICore import Testing -@testable import OpenSwiftUICore struct CustomRecursiveStringConvertibleTests { // MARK: - recursiveDescriptionName Tests @@ -72,7 +76,26 @@ struct CustomRecursiveStringConvertibleTests { let result = input.tupleOfDoubles() #expect(result == nil) } - + + // MARK: - String.escapeXML Tests + + #if OPENSWIFTUI_ENABLE_PRIVATE_IMPORTS + @Test(arguments: [ + ("hello", "hello"), + (#""quote""#, ""quote""), + ("a & b", "a & b"), + ("it's", "it's"), + ("", "<tag>"), + ("line1\nline2", #"line1\nline2"#), + ("line1\rline2", #"line1\rline2"#), + (#"<"'&>"#, "<"'&>"), + ("", ""), + ] as [(String, String)]) + func escapeXMLTests(input: String, expected: String) { + #expect(input.escapeXML() == expected) + } + #endif + @Test func tupleOfDoublesEmptyParens() { // Empty parens should return empty array (not nil) From 192c20e86da54ef1ce29ff26b8adffe6875fa2b0 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 28 Dec 2025 22:46:55 +0800 Subject: [PATCH 8/9] Update CoreGlue2 usage --- Sources/OpenSwiftUI/Util/OpenSwiftUIGlue.swift | 8 ++++---- Sources/OpenSwiftUICore/Test/TestApp.swift | 2 +- Sources/OpenSwiftUICore/Util/CoreGlue.swift | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/OpenSwiftUI/Util/OpenSwiftUIGlue.swift b/Sources/OpenSwiftUI/Util/OpenSwiftUIGlue.swift index 5dd9e91fd..f54eef06c 100644 --- a/Sources/OpenSwiftUI/Util/OpenSwiftUIGlue.swift +++ b/Sources/OpenSwiftUI/Util/OpenSwiftUIGlue.swift @@ -104,10 +104,6 @@ final public class OpenSwiftUIGlue2: CoreGlue2 { #endif } - override public func configureEmptyEnvironment(_ environment: inout EnvironmentValues) { - environment.configureForPlatform(traitCollection: nil) - } - override public final func configureDefaultEnvironment(_: inout EnvironmentValues) { #if os(iOS) || os(visionOS) #else @@ -115,6 +111,10 @@ final public class OpenSwiftUIGlue2: CoreGlue2 { #endif } + override public func configureEmptyEnvironment(_ environment: inout EnvironmentValues) { + environment.configureForPlatform(traitCollection: nil) + } + override public final func makeRootView(base: AnyView, rootFocusScope: Namespace.ID) -> AnyView { AnyView(base.safeAreaInsets(.zero, next: nil)) } diff --git a/Sources/OpenSwiftUICore/Test/TestApp.swift b/Sources/OpenSwiftUICore/Test/TestApp.swift index 74cd2e470..afdbc43cb 100644 --- a/Sources/OpenSwiftUICore/Test/TestApp.swift +++ b/Sources/OpenSwiftUICore/Test/TestApp.swift @@ -47,7 +47,7 @@ public struct _TestApp { package static let defaultEnvironment: EnvironmentValues = { var environment = EnvironmentValues() - CoreGlue2.shared.configureDefaultEnvironment(&environment) + CoreGlue2.shared.configureEmptyEnvironment(&environment) // TODO: Font: "HelveticaNeue" environment.displayScale = 2.0 environment.setTestSystemColorDefinition() diff --git a/Sources/OpenSwiftUICore/Util/CoreGlue.swift b/Sources/OpenSwiftUICore/Util/CoreGlue.swift index ceb25a896..75fac2204 100644 --- a/Sources/OpenSwiftUICore/Util/CoreGlue.swift +++ b/Sources/OpenSwiftUICore/Util/CoreGlue.swift @@ -226,11 +226,11 @@ open class CoreGlue2: NSObject { _openSwiftUIBaseClassAbstractMethod() } - open func configureEmptyEnvironment(_: inout EnvironmentValues) { + open func configureDefaultEnvironment(_: inout EnvironmentValues) { _openSwiftUIBaseClassAbstractMethod() } - open func configureDefaultEnvironment(_: inout EnvironmentValues) { + open func configureEmptyEnvironment(_: inout EnvironmentValues) { _openSwiftUIBaseClassAbstractMethod() } From c643000d3fbd3f7561cc001a5163337cc854282d Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 28 Dec 2025 23:34:28 +0800 Subject: [PATCH 9/9] Implement _recursiveDescription --- .../CustomRecursiveStringConvertible.swift | 44 +++- ...ustomRecursiveStringConvertibleTests.swift | 192 +++++++++++++----- 2 files changed, 186 insertions(+), 50 deletions(-) diff --git a/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift b/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift index efea7a5c9..2592c849e 100644 --- a/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift +++ b/Sources/OpenSwiftUICore/Util/CustomRecursiveStringConvertible.swift @@ -2,13 +2,13 @@ // CustomRecursiveStringConvertible.swift // OpenSwiftUICore // -// Status: WIP +// Status: Complete // Audited for 6.5.4 // ID: 2DFA09903A864CB0F038E089ECDB7AF8 (SwiftUICore) import Foundation -// MARK: - CustomRecursiveStringConvertible [WIP] +// MARK: - CustomRecursiveStringConvertible package protocol CustomRecursiveStringConvertible { var descriptionName: String { get } @@ -57,11 +57,47 @@ extension CustomRecursiveStringConvertible { indent: Int, rounded: Bool ) -> String { - _openSwiftUIUnimplementedFailure() + let indentString = repeatElement(" ", count: indent).joined() + var attributes = descriptionAttributes + if rounded { + attributes = attributes.roundedAttributes() + } + attributes.append(contentsOf: indent == 0 ? topLevelAttributes : []) + let sortedAttributes = attributes.sorted(by: \.name) + let attributeString = sortedAttributes.isEmpty ? "" : " " + sortedAttributes + .map { + let escapedName = $0.name + .components(separatedBy: .whitespacesAndNewlines) + .joined(separator: "_") + .escapeXML() + let escapedValue = $0.value.escapeXML() + return #"\#(escapedName)="\#(escapedValue)""# + } + .joined(separator: " ") + let escapedName = descriptionName + .components(separatedBy: .whitespacesAndNewlines) + .joined(separator: "_") + .escapeXML() + let mark = "\(indentString)<\(escapedName)\(attributeString)" + if descriptionChildren.isEmpty { + return "\(mark) />\n" + } else { + var result = "\(mark)>\n" + for child in descriptionChildren { + guard !child.hideFromDescription else { continue } + result.append(child._recursiveDescription(indent: indent &+ 1, rounded: rounded)) + } + result.append("\(indentString)") + result.append("\n") + return result + } } package var topLevelAttributes: [(name: String, value: String)] { - _openSwiftUIUnimplementedFailure() + guard _TestApp.isIntending(to: .includeStatusBar), + let isHidden = CoreGlue2.shared.isStatusBarHidden() + else { return [] } + return [(name: "statusBar", value: isHidden ? "hidden" : "visible")] } } diff --git a/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift b/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift index 1233ce93d..07ec7b575 100644 --- a/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift +++ b/Tests/OpenSwiftUICoreTests/Util/CustomRecursiveStringConvertibleTests.swift @@ -10,6 +10,107 @@ import OpenSwiftUICore import Testing struct CustomRecursiveStringConvertibleTests { + + // MARK: - recursiveDescription Tests + + struct TestView: CustomRecursiveStringConvertible { + var name: String = "View" + var attributes: [(name: String, value: String)] = [] + var children: [any CustomRecursiveStringConvertible] = [] + var hidden: Bool = false + + var descriptionName: String { name } + var descriptionAttributes: [(name: String, value: String)] { attributes } + var descriptionChildren: [any CustomRecursiveStringConvertible] { children } + var hideFromDescription: Bool { hidden } + } + + @Test( + arguments: [ + // recursiveDescriptionBasic + ( + TestView(), + "\n" + ), + ( + TestView( + name: "SimpleView", + attributes: [(name: "title", value: "Hello")] + ), + #""# + "\n" + ), + // recursiveDescriptionSortsAttributes + ( + TestView( + name: "View", + attributes: [ + (name: "zIndex", value: "1"), + (name: "alpha", value: "0.5"), + (name: "beta", value: "test"), + ] + ), + #""# + "\n" + ), + // recursiveDescriptionEscapesXML + ( + TestView( + name: "My View", + attributes: [(name: "text", value: #""#)] + ), + #""# + "\n" + ), + // recursiveDescriptionWithChildren + ( + TestView( + name: "Parent", + children: [ + TestView(name: "Child"), + TestView(name: "Child"), + ] + ), + "\n \n \n\n" + ), + // recursiveDescriptionHidesChildren + ( + TestView( + name: "Parent", + children: [ + TestView(name: "Hidden", hidden: true), + TestView(name: "Visible"), + ] + ), + "\n \n\n" + ), + ] as [(TestView, String)] + ) + func recursiveDescriptionTests(view: TestView, expected: String) { + #expect(view.recursiveDescription == expected) + } + + @Test(arguments: [ + // Simple double rounding + (TestView(name: "View", attributes: [(name: "value", value: "1.23456789")]), #""# + "\n"), + // Tuple of doubles rounding + (TestView(name: "View", attributes: [(name: "pos", value: "(x: 10.123456, y: 20.987654)")]), #""# + "\n"), + // Color detection (red) + (TestView(name: "View", attributes: [(name: "color", value: "(1.0, 0.0, 0.0, 1.0)")]), #""# + "\n"), + // Integer values become doubles + (TestView(name: "View", attributes: [(name: "count", value: "42")]), #""# + "\n"), + // Non-numeric values unchanged + (TestView(name: "View", attributes: [(name: "title", value: "Hello")]), #""# + "\n"), + ] as [(TestView, String)]) + func roundedRecursiveDescriptionTests(view: TestView, expected: String) { + #expect(view.roundedRecursiveDescription == expected) + } + + // MARK: - topLevelAttributes Tests + + @Test + func topLevelAttributesWithoutIntent() { + _TestApp.setIntents([]) + #expect(TestView().topLevelAttributes.isEmpty) + } + // MARK: - recursiveDescriptionName Tests private struct PrivateType {} @@ -77,6 +178,14 @@ struct CustomRecursiveStringConvertibleTests { #expect(result == nil) } + @Test + func tupleOfDoublesEmptyParens() { + // Empty parens should return empty array (not nil) + let result = "()".tupleOfDoubles() + #expect(result != nil) + #expect(result?.isEmpty == true) + } + // MARK: - String.escapeXML Tests #if OPENSWIFTUI_ENABLE_PRIVATE_IMPORTS @@ -96,51 +205,6 @@ struct CustomRecursiveStringConvertibleTests { } #endif - @Test - func tupleOfDoublesEmptyParens() { - // Empty parens should return empty array (not nil) - let result = "()".tupleOfDoubles() - #expect(result != nil) - #expect(result?.isEmpty == true) - } - - // MARK: - Color.Resolved.name Tests - - @Test(arguments: [ - // Basic colors - (Float(0), Float(0), Float(0), Float(0), "clear"), - (Float(0), Float(0), Float(0), Float(1), "black"), - (Float(1), Float(1), Float(1), Float(1), "white"), - (Float(8.0 / 256.0), Float(8.0 / 256.0), Float(8.0 / 256.0), Float(1), "gray"), - (Float(1), Float(0), Float(0), Float(1), "red"), - (Float(0), Float(1), Float(0), Float(1), "green"), - (Float(0), Float(0), Float(1), Float(1), "blue"), - (Float(1), Float(1), Float(0), Float(1), "yellow"), - (Float(0), Float(1), Float(1), Float(1), "teal"), - // System colors - (Float(1), Float(11.0 / 256.0), Float(11.0 / 256.0), Float(1), "system-red"), - (Float(1), Float(15.0 / 256.0), Float(11.0 / 256.0), Float(1), "system-red-dark"), - // Extended colors - (Float(55.0 / 256.0), Float(0), Float(55.0 / 256.0), Float(1), "purple"), - (Float(1), Float(55.0 / 256.0), Float(0), Float(1), "orange"), - (Float(55.0 / 256.0), Float(55.0 / 256.0), Float(1), Float(1), "indigo"), - (Float(1), Float(0), Float(55.0 / 256.0), Float(1), "pink"), - (Float(12.0 / 256.0), Float(12.0 / 256.0), Float(14.0 / 256.0), Float(64.0 / 256.0), "brown"), - (Float(12.0 / 256.0), Float(12.0 / 256.0), Float(14.0 / 256.0), Float(76.0 / 256.0), "placeholder-text"), - // Quantization: slight variations round to named color - (Float(0.999), Float(0.001), Float(0.001), Float(0.999), "red"), - ] as [(Float, Float, Float, Float, String)]) - func colorResolvedName(r: Float, g: Float, b: Float, a: Float, expected: String) { - let color = Color.Resolved(linearRed: r, linearGreen: g, linearBlue: b, opacity: a) - #expect(color.name == expected) - } - - @Test - func colorResolvedNameUnknown() { - let color = Color.Resolved(linearRed: 0.5, linearGreen: 0.3, linearBlue: 0.7, opacity: 1) - #expect(color.name == nil) - } - // MARK: - Sequence.roundedAttributes Tests @Test @@ -198,7 +262,6 @@ struct CustomRecursiveStringConvertibleTests { @Test func roundedAttributesNonNumeric() { - // Non-numeric values should be returned unchanged let attrs: [(name: String, value: String)] = [ (name: "title", value: "Hello World"), (name: "isEnabled", value: "true"), @@ -210,4 +273,41 @@ struct CustomRecursiveStringConvertibleTests { #expect(result[1].name == "isEnabled") #expect(result[1].value == "true") } + + // MARK: - Color.Resolved.name Tests + + @Test(arguments: [ + // Basic colors + (Float(0), Float(0), Float(0), Float(0), "clear"), + (Float(0), Float(0), Float(0), Float(1), "black"), + (Float(1), Float(1), Float(1), Float(1), "white"), + (Float(8.0 / 256.0), Float(8.0 / 256.0), Float(8.0 / 256.0), Float(1), "gray"), + (Float(1), Float(0), Float(0), Float(1), "red"), + (Float(0), Float(1), Float(0), Float(1), "green"), + (Float(0), Float(0), Float(1), Float(1), "blue"), + (Float(1), Float(1), Float(0), Float(1), "yellow"), + (Float(0), Float(1), Float(1), Float(1), "teal"), + // System colors + (Float(1), Float(11.0 / 256.0), Float(11.0 / 256.0), Float(1), "system-red"), + (Float(1), Float(15.0 / 256.0), Float(11.0 / 256.0), Float(1), "system-red-dark"), + // Extended colors + (Float(55.0 / 256.0), Float(0), Float(55.0 / 256.0), Float(1), "purple"), + (Float(1), Float(55.0 / 256.0), Float(0), Float(1), "orange"), + (Float(55.0 / 256.0), Float(55.0 / 256.0), Float(1), Float(1), "indigo"), + (Float(1), Float(0), Float(55.0 / 256.0), Float(1), "pink"), + (Float(12.0 / 256.0), Float(12.0 / 256.0), Float(14.0 / 256.0), Float(64.0 / 256.0), "brown"), + (Float(12.0 / 256.0), Float(12.0 / 256.0), Float(14.0 / 256.0), Float(76.0 / 256.0), "placeholder-text"), + // Quantization: slight variations round to named color + (Float(0.999), Float(0.001), Float(0.001), Float(0.999), "red"), + ] as [(Float, Float, Float, Float, String)]) + func colorResolvedName(r: Float, g: Float, b: Float, a: Float, expected: String) { + let color = Color.Resolved(linearRed: r, linearGreen: g, linearBlue: b, opacity: a) + #expect(color.name == expected) + } + + @Test + func colorResolvedNameUnknown() { + let color = Color.Resolved(linearRed: 0.5, linearGreen: 0.3, linearBlue: 0.7, opacity: 1) + #expect(color.name == nil) + } }