Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions Sources/Elementary/Core/AttributeStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,26 +22,35 @@ public enum _AttributeStorage: Sendable, Equatable {
@inlinable
init(_ attributes: [HTMLAttribute<some HTMLTagDefinition>]) {
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 })
}
}

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])
Expand Down Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Elementary/Core/CoreModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 11 additions & 3 deletions Sources/Elementary/Core/Html+Attributes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

Expand All @@ -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<Tag>..., when condition: Bool = true) -> _AttributedElement<Self> {
_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.
Expand All @@ -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<Tag>], when condition: Bool = true) -> _AttributedElement<Self> {
_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))
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/Elementary/Core/Html+Elements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public struct HTMLElement<Tag: HTMLTagDefinition, Content: HTML>: 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()
}

Expand Down Expand Up @@ -87,7 +87,7 @@ public struct HTMLVoidElement<Tag: HTMLTagDefinition>: 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.
Expand Down
63 changes: 63 additions & 0 deletions Tests/ElementaryTests/AttributeStorageTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Testing

@testable import Elementary

struct AttributeStorageTests {
@Test func testArrayInitializerWithEmptyArrayCreatesDynamicNone() {
let emptyAttributes: [HTMLAttribute<HTMLTag.p>] = []
let storage = _AttributeStorage(emptyAttributes)

#expect(storage == .none(isStaticallyKnownEmpty: false))
#expect(storage.isEmpty)
#expect(!storage.isStaticallyKnownEmpty)
}

@Test func testArrayInitializerWithSingleAttributeCreatesSingleStorage() {
let attributes: [HTMLAttribute<HTMLTag.p>] = [
.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))
}
}
Loading