From 43e8b774690b24d470bcfb579a0e8f0582f4790f Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Wed, 19 Nov 2025 08:55:44 +0100 Subject: [PATCH 1/2] rename content to body --- .swift-version | 1 + Examples/HummingbirdDemo/.swift-version | 1 + .../HummingbirdDemo/Sources/App/Pages.swift | 4 +- Sources/Elementary/Core/AsyncContent.swift | 4 +- Sources/Elementary/Core/AsyncForEach.swift | 2 + Sources/Elementary/Core/CoreModel.swift | 30 ++++++++-- Sources/Elementary/Core/Html+Attributes.swift | 3 + Sources/Elementary/Core/Html+Elements.swift | 3 + Sources/Elementary/Core/HtmlBuilder.swift | 6 +- Sources/Elementary/HtmlDocument.swift | 57 ++++++++++++++----- .../ElementaryTests/AsyncRenderingTests.swift | 2 +- .../CompositionRenderingTest.swift | 4 +- .../EnvironmentRenderingTests.swift | 4 +- .../ElementaryTests/SendableAnyHTMLBox.swift | 2 +- .../ElementaryTests/TextRenderingTests.swift | 2 +- 15 files changed, 93 insertions(+), 32 deletions(-) create mode 100644 .swift-version create mode 100644 Examples/HummingbirdDemo/.swift-version diff --git a/.swift-version b/.swift-version new file mode 100644 index 0000000..0df17dd --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +6.2.1 \ No newline at end of file diff --git a/Examples/HummingbirdDemo/.swift-version b/Examples/HummingbirdDemo/.swift-version new file mode 100644 index 0000000..0df17dd --- /dev/null +++ b/Examples/HummingbirdDemo/.swift-version @@ -0,0 +1 @@ +6.2.1 \ No newline at end of file diff --git a/Examples/HummingbirdDemo/Sources/App/Pages.swift b/Examples/HummingbirdDemo/Sources/App/Pages.swift index 80393a8..57c86a5 100644 --- a/Examples/HummingbirdDemo/Sources/App/Pages.swift +++ b/Examples/HummingbirdDemo/Sources/App/Pages.swift @@ -25,7 +25,7 @@ struct MainLayout: HTMLDocument { } struct WelcomePage: HTML { - var content: some HTML { + var body: some HTML { div(.class("flex flex-col gap-4")) { p { "This is a simple example of using " @@ -52,7 +52,7 @@ struct GreetingPage: HTML { @Environment(requiring: EnvironmentValues.$name) var name var greetingCount: Int - var content: some HTML { + var body: some HTML { if greetingCount < 1 { p(.class("text-red-500")) { "No greetings to show." diff --git a/Sources/Elementary/Core/AsyncContent.swift b/Sources/Elementary/Core/AsyncContent.swift index 60cfd43..8de7722 100644 --- a/Sources/Elementary/Core/AsyncContent.swift +++ b/Sources/Elementary/Core/AsyncContent.swift @@ -3,9 +3,11 @@ /// The this element can only be rendered in an async context (ie: by calling ``HTML/render(into:chunkSize:)`` or ``HTML/renderAsync()``). /// All HTML tag types (``HTMLElement``) support async content closures in their initializers, so you don't need to use this element directly in most cases. public struct AsyncContent: HTML, Sendable { + public typealias Body = Never + public typealias Tag = Content.Tag + @usableFromInline var content: @Sendable () async throws -> Content - public typealias Tag = Content.Tag /// Creates a new async HTML element with the specified content. /// diff --git a/Sources/Elementary/Core/AsyncForEach.swift b/Sources/Elementary/Core/AsyncForEach.swift index 50d52d7..e8ea46a 100644 --- a/Sources/Elementary/Core/AsyncForEach.swift +++ b/Sources/Elementary/Core/AsyncForEach.swift @@ -12,6 +12,8 @@ /// } /// ``` public struct AsyncForEach: HTML { + public typealias Body = Never + @usableFromInline var sequence: Source @usableFromInline diff --git a/Sources/Elementary/Core/CoreModel.swift b/Sources/Elementary/Core/CoreModel.swift index 064e4df..a39bb0d 100644 --- a/Sources/Elementary/Core/CoreModel.swift +++ b/Sources/Elementary/Core/CoreModel.swift @@ -22,13 +22,18 @@ public protocol HTML { /// The Tag type defines which attributes can be attached to an HTML element. /// If an element does not represent a specific HTML tag, the Tag type will /// be ``Swift/Never`` and the element cannot be attributed. - associatedtype Tag: HTMLTagDefinition = Content.Tag + associatedtype Tag: HTMLTagDefinition = Body.Tag /// The type of the HTML content this component represents. - associatedtype Content: HTML = Never + associatedtype Body: HTML /// The HTML content of this component. - @HTMLBuilder var content: Content { get } + @HTMLBuilder var body: Body { get } + + /// The HTML content of this component. + @HTMLBuilder + @available(*, deprecated, message: "`var content` is deprecated, use `var body` instead") + var content: Body { get } static func _render( _ html: consuming Self, @@ -42,6 +47,21 @@ public protocol HTML { ) async throws } +extension HTML { + @available(*, deprecated, message: "`var content` is deprecated, use `var body` instead") + public var body: Body { + // NOTE: sorry for the change + content + } + + @available(*, deprecated, message: "Content was renamed, use Body instead") + public typealias Content = Body + + public var content: Body { + fatalError("content was renamed to body") + } +} + /// A type that represents an HTML tag. public protocol HTMLTagDefinition: Sendable { /// The name of the HTML tag as it is rendered in an HTML document. @@ -93,7 +113,7 @@ public extension HTML { into renderer: inout Renderer, with context: consuming _RenderingContext ) { - Content._render(html.content, into: &renderer, with: context) + Body._render(html.body, into: &renderer, with: context) } @inlinable @@ -102,7 +122,7 @@ public extension HTML { into renderer: inout Renderer, with context: consuming _RenderingContext ) async throws { - try await Content._render(html.content, into: &renderer, with: context) + try await Body._render(html.body, into: &renderer, with: context) } } diff --git a/Sources/Elementary/Core/Html+Attributes.swift b/Sources/Elementary/Core/Html+Attributes.swift index 6dc8c4e..4cb4081 100644 --- a/Sources/Elementary/Core/Html+Attributes.swift +++ b/Sources/Elementary/Core/Html+Attributes.swift @@ -64,6 +64,9 @@ extension HTMLAttribute { } public struct _AttributedElement: HTML { + public typealias Body = Never + public typealias Tag = Content.Tag + public var content: Content public var attributes: _AttributeStorage diff --git a/Sources/Elementary/Core/Html+Elements.swift b/Sources/Elementary/Core/Html+Elements.swift index 485d3cb..9b8f08d 100644 --- a/Sources/Elementary/Core/Html+Elements.swift +++ b/Sources/Elementary/Core/Html+Elements.swift @@ -2,6 +2,9 @@ public struct HTMLElement: HTML where Tag: HTMLTrait.Paired { /// The type of the HTML tag this element represents. public typealias Tag = Tag + public typealias Body = Never + public typealias Content = Content + public var _attributes: _AttributeStorage // The content of the element. diff --git a/Sources/Elementary/Core/HtmlBuilder.swift b/Sources/Elementary/Core/HtmlBuilder.swift index 37c3cbf..2ca32b6 100644 --- a/Sources/Elementary/Core/HtmlBuilder.swift +++ b/Sources/Elementary/Core/HtmlBuilder.swift @@ -49,8 +49,8 @@ } } -public extension HTML where Content == Never { - var content: Never { +public extension HTML where Body == Never { + var body: Never { #if hasFeature(Embedded) fatalError("content was called on an unsupported type") #else @@ -61,7 +61,7 @@ public extension HTML where Content == Never { extension Never: HTML { public typealias Tag = Never - public typealias Content = Never + public typealias Body = Never } extension Optional: HTML where Wrapped: HTML { diff --git a/Sources/Elementary/HtmlDocument.swift b/Sources/Elementary/HtmlDocument.swift index 7570a34..c9ce91d 100644 --- a/Sources/Elementary/HtmlDocument.swift +++ b/Sources/Elementary/HtmlDocument.swift @@ -41,8 +41,50 @@ public protocol HTMLDocument: HTML { @HTMLBuilder var body: HTMLBody { get } } +// NOTE: The default implementation uses an empty string as the "magic value" for undefined. +// This is to avoid the need for an optional `lang` or `dir`` property on the protocol, +// which would cause confusing issues when adopters provide a property of a non-optional type. +private let defaultUndefinedLanguage = "" +private let defaultUndefinedDirection = "" + public extension HTMLDocument { - @HTMLBuilder var content: some HTML { + /// The default value for the `lang` property is an empty string and will not be rendered in the HTML. + var lang: String { defaultUndefinedLanguage } + /// The default value for the `dir` property is an empty string and will not be rendered in the HTML. + var dir: HTMLAttributeValue.Direction { .init(value: defaultUndefinedDirection) } +} + +// NOTE: this is a bit messy after the renaming of var content to var body +public extension HTMLDocument { + static func _render( + _ html: consuming Self, + into renderer: inout Renderer, + with context: consuming _RenderingContext + ) { + func render(_ html: H, into renderer: inout some _HTMLRendering, with context: consuming _RenderingContext) { + H._render(html, into: &renderer, with: context) + } + + render(html.__body, into: &renderer, with: context) + } + + static func _render( + _ html: consuming Self, + into renderer: inout Renderer, + with context: consuming _RenderingContext + ) async throws { + func render( + _ html: H, + into renderer: inout some _AsyncHTMLRendering, + with context: consuming _RenderingContext + ) async throws { + try await H._render(html, into: &renderer, with: context) + } + + try await render(html.__body, into: &renderer, with: context) + } + + @HTMLBuilder var __body: some HTML { HTMLRaw("") html { Elementary.head { @@ -55,16 +97,3 @@ public extension HTMLDocument { .attributes(.dir(dir), when: dir.value != defaultUndefinedDirection) } } - -// NOTE: The default implementation uses an empty string as the "magic value" for undefined. -// This is to avoid the need for an optional `lang` or `dir`` property on the protocol, -// which would cause confusing issues when adopters provide a property of a non-optional type. -private let defaultUndefinedLanguage = "" -private let defaultUndefinedDirection = "" - -public extension HTMLDocument { - /// The default value for the `lang` property is an empty string and will not be rendered in the HTML. - var lang: String { defaultUndefinedLanguage } - /// The default value for the `dir` property is an empty string and will not be rendered in the HTML. - var dir: HTMLAttributeValue.Direction { .init(value: defaultUndefinedDirection) } -} diff --git a/Tests/ElementaryTests/AsyncRenderingTests.swift b/Tests/ElementaryTests/AsyncRenderingTests.swift index f21fff6..0e58fda 100644 --- a/Tests/ElementaryTests/AsyncRenderingTests.swift +++ b/Tests/ElementaryTests/AsyncRenderingTests.swift @@ -92,7 +92,7 @@ final class AsyncRenderingTests: XCTestCase { private struct AwaitedP: HTML { var number: Int - var content: some HTML { + var body: some HTML { AsyncContent { let _ = try await Task.sleep(for: .milliseconds(1)) p { "\(number)" } diff --git a/Tests/ElementaryTests/CompositionRenderingTest.swift b/Tests/ElementaryTests/CompositionRenderingTest.swift index faa8bdb..7e38301 100644 --- a/Tests/ElementaryTests/CompositionRenderingTest.swift +++ b/Tests/ElementaryTests/CompositionRenderingTest.swift @@ -65,7 +65,7 @@ struct MyList: HTML { var items: [String] var selectedIndex: Int - var content: some HTML { + var body: some HTML { ul { for (index, item) in items.enumerated() { MyListItem(text: item, isSelected: index == selectedIndex) @@ -79,7 +79,7 @@ struct MyListItem: HTML { var text: String var isSelected: Bool = false - var content: some HTML { + var body: some HTML { li { text } .attributes(.class("selected"), when: isSelected) } diff --git a/Tests/ElementaryTests/EnvironmentRenderingTests.swift b/Tests/ElementaryTests/EnvironmentRenderingTests.swift index fe7612d..6d11c45 100644 --- a/Tests/ElementaryTests/EnvironmentRenderingTests.swift +++ b/Tests/ElementaryTests/EnvironmentRenderingTests.swift @@ -28,14 +28,14 @@ final class EnvironmentRenderingTests: XCTestCase { struct MyNumber: HTML { @Environment(Values.$number) var number - var content: some HTML { + var body: some HTML { "\(number)" } } struct MyDatabaseValue: HTML { @Environment(requiring: Values.$database) var database - var content: some HTML { + var body: some HTML { p { await database.value } diff --git a/Tests/ElementaryTests/SendableAnyHTMLBox.swift b/Tests/ElementaryTests/SendableAnyHTMLBox.swift index 8da1944..eac69eb 100644 --- a/Tests/ElementaryTests/SendableAnyHTMLBox.swift +++ b/Tests/ElementaryTests/SendableAnyHTMLBox.swift @@ -29,7 +29,7 @@ class NonSendable { struct MyComponent: HTML { let ns = NonSendable() - var content: some HTML { + var body: some HTML { div { "\(ns.x)" } } } diff --git a/Tests/ElementaryTests/TextRenderingTests.swift b/Tests/ElementaryTests/TextRenderingTests.swift index d6341e9..bd9c1fb 100644 --- a/Tests/ElementaryTests/TextRenderingTests.swift +++ b/Tests/ElementaryTests/TextRenderingTests.swift @@ -71,7 +71,7 @@ class Bar { class JO: HTML { var bar: Bar = .init() - var content: some HTML { + var body: some HTML { "Hello, World!" } } From f8d192bc64a47324a819328925d0ee930eb40516 Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Wed, 19 Nov 2025 09:04:56 +0100 Subject: [PATCH 2/2] readme, comments, benchmarks --- .../Benchmarks/ElementaryBenchmarks/Benchmarks.swift | 4 ++-- README.md | 12 ++++++------ Sources/Elementary/Core/CoreModel.swift | 4 ++-- Sources/Elementary/Core/Environment.swift | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Benchmarks/Benchmarks/ElementaryBenchmarks/Benchmarks.swift b/Benchmarks/Benchmarks/ElementaryBenchmarks/Benchmarks.swift index bd7c14d..62ce6ec 100644 --- a/Benchmarks/Benchmarks/ElementaryBenchmarks/Benchmarks.swift +++ b/Benchmarks/Benchmarks/ElementaryBenchmarks/Benchmarks.swift @@ -252,7 +252,7 @@ struct MyCustomElement: HTML { myContent = content() } - var content: some HTML { + var body: some HTML { myContent } } @@ -260,7 +260,7 @@ struct MyCustomElement: HTML { struct MyListItem: HTML { let number: Int - var content: some HTML { + var body: some HTML { let isEven = number.isMultiple(of: 2) li(.id("\(number)")) { diff --git a/README.md b/README.md index b1dc6c4..e3a2ae9 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ struct MainPage: HTMLDocument { struct FeatureList: HTML { var features: [String] - var content: some HTML { + var body: some HTML { ul { for feature in features { li { feature } @@ -122,7 +122,7 @@ struct List: HTML { var items: [String] var importantIndex: Int - var content: some HTML { + var body: some HTML { // conditional rendering if items.isEmpty { p { "No items" } @@ -142,7 +142,7 @@ struct ListItem: HTML { var text: String var isImportant: Bool = false - var content: some HTML { + var body: some HTML { // conditional attributes li { text } .attributes(.class("important"), when: isImportant) @@ -183,7 +183,7 @@ struct Button: HTML { var text: String // by exposing the HTMLTag type information... - var content: some HTML { + var body: some HTML { input(.type(.button), .value(text)) } } @@ -209,7 +209,7 @@ div { } struct MyComponent: HTML { - var content: some HTML { + var body: some HTML { AsyncContent { "So does this: \(await getMoreData())" } @@ -245,7 +245,7 @@ struct MyComponent: HTML { // ... their values can be accessed ... @Environment(MyValues.$userName) var userName - var content: some HTML { + var body: some HTML { p { "Hello, \(userName)!" } } } diff --git a/Sources/Elementary/Core/CoreModel.swift b/Sources/Elementary/Core/CoreModel.swift index a39bb0d..708205a 100644 --- a/Sources/Elementary/Core/CoreModel.swift +++ b/Sources/Elementary/Core/CoreModel.swift @@ -7,7 +7,7 @@ /// struct FeatureList: HTML { /// var features: [String] /// -/// var content: some HTML { +/// var body: some HTML { /// ul { /// for feature in features { /// li { feature } @@ -58,7 +58,7 @@ extension HTML { public typealias Content = Body public var content: Body { - fatalError("content was renamed to body") + fatalError("Please make sure to add a `var body` implementation to your HTML type.") } } diff --git a/Sources/Elementary/Core/Environment.swift b/Sources/Elementary/Core/Environment.swift index 6b02bd5..ee5e72d 100644 --- a/Sources/Elementary/Core/Environment.swift +++ b/Sources/Elementary/Core/Environment.swift @@ -11,7 +11,7 @@ /// struct MyNumber: HTML { /// @Environment(Values.$myNumber) var number /// -/// var content: some HTML { +/// var body: some HTML { /// p { "\(number)" } /// } /// }