From 7cf2bc2ae9b210307ec5c27f40ee2a81517010ba Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:45:31 +0100 Subject: [PATCH 1/2] add "isStaticallyKnownEmpty" to _AttributeStorage --- .../Elementary/Core/AttributeStorage.swift | 25 +++++--- Sources/Elementary/Core/CoreModel.swift | 2 +- Sources/Elementary/Core/Html+Attributes.swift | 14 ++++- Sources/Elementary/Core/Html+Elements.swift | 4 +- .../AttributeStorageTests.swift | 63 +++++++++++++++++++ 5 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 Tests/ElementaryTests/AttributeStorageTests.swift diff --git a/Sources/Elementary/Core/AttributeStorage.swift b/Sources/Elementary/Core/AttributeStorage.swift index 364cb60..b020481 100644 --- a/Sources/Elementary/Core/AttributeStorage.swift +++ b/Sources/Elementary/Core/AttributeStorage.swift @@ -5,13 +5,13 @@ /// The storage automatically optimizes for the number of attributes being stored, /// using the most efficient representation in each case. public enum _AttributeStorage: Sendable, Equatable { - case none + case none(isStaticallyKnownEmpty: Bool) case single(_StoredAttribute) case multiple([_StoredAttribute]) @inlinable - init() { - self = .none + init(isStaticallyKnownEmpty: Bool) { + self = .none(isStaticallyKnownEmpty: isStaticallyKnownEmpty) } @inlinable @@ -22,7 +22,7 @@ public enum _AttributeStorage: Sendable, Equatable { @inlinable init(_ attributes: [HTMLAttribute]) { switch attributes.count { - case 0: self = .none + case 0: self = .none(isStaticallyKnownEmpty: false) case 1: self = .single(attributes[0].htmlAttribute) default: self = .multiple(attributes.map { $0.htmlAttribute }) } @@ -30,18 +30,27 @@ public enum _AttributeStorage: Sendable, Equatable { public var isEmpty: Bool { switch self { - case .none: return true + case .none(_): return true case .single: return false case let .multiple(attributes): return attributes.isEmpty // just to be sure... } } + public var isStaticallyKnownEmpty: Bool { + switch self { + case let .none(isStaticallyKnownEmpty): return isStaticallyKnownEmpty + case .single, .multiple: return false + } + } + public mutating func append(_ attributes: consuming _AttributeStorage) { // maybe this was a bad idea.... switch (self, attributes) { - case (_, .none): + case let (.none(lhsStatic), .none(rhsStatic)): + self = .none(isStaticallyKnownEmpty: lhsStatic && rhsStatic) + case (_, .none(_)): break - case let (.none, other): + case let (.none(_), other): self = other case let (.single(existing), .single(other)): self = .multiple([existing, other]) @@ -86,7 +95,7 @@ public struct _MergedAttributes: Sequence, Sendable { init(_ storage: consuming _AttributeStorage) { switch storage { - case .none: state = .empty + case .none(_): state = .empty case let .single(attribute): state = .single(attribute) case let .multiple(attributes): state = .flattening(attributes, 0) } diff --git a/Sources/Elementary/Core/CoreModel.swift b/Sources/Elementary/Core/CoreModel.swift index 1c035b7..2951159 100644 --- a/Sources/Elementary/Core/CoreModel.swift +++ b/Sources/Elementary/Core/CoreModel.swift @@ -64,7 +64,7 @@ public struct _RenderingContext { @usableFromInline var attributes: _AttributeStorage - public static var emptyContext: Self { Self(attributes: .none) } + public static var emptyContext: Self { Self(attributes: .none(isStaticallyKnownEmpty: true)) } } // TODO: think about this interface... seems not ideal diff --git a/Sources/Elementary/Core/Html+Attributes.swift b/Sources/Elementary/Core/Html+Attributes.swift index 9a6cad8..f6cb365 100644 --- a/Sources/Elementary/Core/Html+Attributes.swift +++ b/Sources/Elementary/Core/Html+Attributes.swift @@ -107,7 +107,7 @@ public extension HTML where Tag: HTMLTrait.Attributes.Global { if condition { return _AttributedElement(content: self, attributes: .init(attribute)) } else { - return _AttributedElement(content: self, attributes: .init()) + return _AttributedElement(content: self, attributes: .init(isStaticallyKnownEmpty: false)) } } @@ -118,7 +118,11 @@ public extension HTML where Tag: HTMLTrait.Attributes.Global { /// - Returns: A new element with the specified attributes added. @inlinable func attributes(_ attributes: HTMLAttribute..., when condition: Bool = true) -> _AttributedElement { - _AttributedElement(content: self, attributes: .init(condition ? attributes : [])) + if condition { + return _AttributedElement(content: self, attributes: .init(attributes)) + } else { + return _AttributedElement(content: self, attributes: .none(isStaticallyKnownEmpty: false)) + } } /// Adds the specified attributes to the element. @@ -128,7 +132,11 @@ public extension HTML where Tag: HTMLTrait.Attributes.Global { /// - Returns: A new element with the specified attributes added. @inlinable func attributes(contentsOf attributes: [HTMLAttribute], when condition: Bool = true) -> _AttributedElement { - _AttributedElement(content: self, attributes: .init(condition ? attributes : [])) + if condition { + return _AttributedElement(content: self, attributes: .init(attributes)) + } else { + return _AttributedElement(content: self, attributes: .none(isStaticallyKnownEmpty: false)) + } } } diff --git a/Sources/Elementary/Core/Html+Elements.swift b/Sources/Elementary/Core/Html+Elements.swift index 8f1c98e..fa17b39 100644 --- a/Sources/Elementary/Core/Html+Elements.swift +++ b/Sources/Elementary/Core/Html+Elements.swift @@ -14,7 +14,7 @@ public struct HTMLElement: HTML where Tag /// - Parameter content: The content of the element. @inlinable public init(@HTMLBuilder content: () -> Content) { - _attributes = .init() + _attributes = .init(isStaticallyKnownEmpty: true) self.content = content() } @@ -87,7 +87,7 @@ public struct HTMLVoidElement: HTML where Tag: HTMLTrait /// Creates a new HTML void element. @inlinable public init() { - _attributes = .init() + _attributes = .init(isStaticallyKnownEmpty: true) } /// Creates a new HTML void element with the specified attribute. diff --git a/Tests/ElementaryTests/AttributeStorageTests.swift b/Tests/ElementaryTests/AttributeStorageTests.swift new file mode 100644 index 0000000..75c87c5 --- /dev/null +++ b/Tests/ElementaryTests/AttributeStorageTests.swift @@ -0,0 +1,63 @@ +import Testing + +@testable import Elementary + +struct AttributeStorageTests { + @Test func testArrayInitializerWithEmptyArrayCreatesDynamicNone() { + let emptyAttributes: [HTMLAttribute] = [] + let storage = _AttributeStorage(emptyAttributes) + + #expect(storage == .none(isStaticallyKnownEmpty: false)) + #expect(storage.isEmpty) + #expect(!storage.isStaticallyKnownEmpty) + } + + @Test func testArrayInitializerWithSingleAttributeCreatesSingleStorage() { + let attributes: [HTMLAttribute] = [ + .init(name: "id", value: "foo"), + ] + let storage = _AttributeStorage(attributes) + + #expect(storage == .single(.init(name: "id", value: "foo", mergeMode: .replaceValue))) + #expect(!storage.isEmpty) + #expect(!storage.isStaticallyKnownEmpty) + } + + @Test func testStaticEmptyStatusForNone() { + let staticEmpty = _AttributeStorage.none(isStaticallyKnownEmpty: true) + let dynamicEmpty = _AttributeStorage.none(isStaticallyKnownEmpty: false) + + #expect(staticEmpty.isEmpty) + #expect(dynamicEmpty.isEmpty) + #expect(staticEmpty.isStaticallyKnownEmpty) + #expect(!dynamicEmpty.isStaticallyKnownEmpty) + } + + @Test func testAppendPropagatesStaticEmptyStatusForNone() { + var allStatic = _AttributeStorage.none(isStaticallyKnownEmpty: true) + allStatic.append(.none(isStaticallyKnownEmpty: true)) + #expect(allStatic == .none(isStaticallyKnownEmpty: true)) + + var mixedStaticAndDynamic = _AttributeStorage.none(isStaticallyKnownEmpty: true) + mixedStaticAndDynamic.append(.none(isStaticallyKnownEmpty: false)) + #expect(mixedStaticAndDynamic == .none(isStaticallyKnownEmpty: false)) + + var allDynamic = _AttributeStorage.none(isStaticallyKnownEmpty: false) + allDynamic.append(.none(isStaticallyKnownEmpty: false)) + #expect(allDynamic == .none(isStaticallyKnownEmpty: false)) + } + + @Test func testAppendWithNonEmptyRemainsNonEmpty() { + let idAttribute = _StoredAttribute(name: "id", value: "foo", mergeMode: .replaceValue) + var storage = _AttributeStorage.none(isStaticallyKnownEmpty: true) + storage.append(.single(idAttribute)) + + #expect(!storage.isEmpty) + #expect(!storage.isStaticallyKnownEmpty) + #expect(storage == .single(idAttribute)) + + var nonEmpty = _AttributeStorage.single(idAttribute) + nonEmpty.append(.none(isStaticallyKnownEmpty: true)) + #expect(nonEmpty == .single(idAttribute)) + } +} From 2ef07391ec48247cf71ed4ef77712201413ac0b1 Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:49:49 +0100 Subject: [PATCH 2/2] format --- Tests/ElementaryTests/AttributeStorageTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ElementaryTests/AttributeStorageTests.swift b/Tests/ElementaryTests/AttributeStorageTests.swift index 75c87c5..b6c5fa5 100644 --- a/Tests/ElementaryTests/AttributeStorageTests.swift +++ b/Tests/ElementaryTests/AttributeStorageTests.swift @@ -14,7 +14,7 @@ struct AttributeStorageTests { @Test func testArrayInitializerWithSingleAttributeCreatesSingleStorage() { let attributes: [HTMLAttribute] = [ - .init(name: "id", value: "foo"), + .init(name: "id", value: "foo") ] let storage = _AttributeStorage(attributes)