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..b6c5fa5 --- /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)) + } +}