Skip to content

@Builder<FlowContent> / @Builder<PhrasingContent>` constraints break component composition #29

@mynona

Description

@mynona

@Builder<FlowContent> / @Builder<PhrasingContent> constraints break component composition

Problem

Many HTML tags enforce content model constraints at compile time by using typed builders:

// LiTag.swift, PTag.swift, TdTag.swift, SectionTag.swift, NavTag.swift, etc.
public init(
    @Builder<FlowContent> _ block: () -> [any FlowContent]
)

This makes it impossible to embed reusable components that return [any SGML.Element] inside these tags.

Minimal Reproduction

// A typical reusable component
struct MyBadge: TemplateRepresentable {
    @Builder<Element>
    func render(_ request: Request) -> [Element] {
        Span { Text("badge") }
    }
}

// ❌ Compile error: "Argument type 'any Element' does not conform to expected type 'FlowContent'"
Li {
    MyBadge().render(request)  // returns [any SGML.Element]
}

Affected Tags

All tags with @Builder<FlowContent> or @Builder<PhrasingContent> constraints:

FlowContent: Li, P, Td, Th, Section, Nav, Main, A, Body, Article, Aside, Blockquote, Dd, Del, Dialog, Figcaption, Ins

PhrasingContent: H1, H2, H3, H4, H5, H6, Span, B, I, Em, Strong, Cite, Data, Kbd, Mark, Output, Pre, Q, Samp, Small, Sub, Sup, U, Var

Current Workaround (user-side, fragile)

extension Builder where Element == any FlowContent {
    public static func buildExpression(
        _ expression: [SGML.Element]
    ) -> [any FlowContent] {
        expression.compactMap { $0 as? any FlowContent }
    }
}

extension Builder where Element == any PhrasingContent {
    public static func buildExpression(
        _ expression: [SGML.Element]
    ) -> [any PhrasingContent] {
        expression.compactMap { $0 as? any PhrasingContent }
    }
}

This works but silently drops elements at runtime that don't conform to FlowContent — no compile-time or runtime warning.

Suggested Fixes

Option 1 (lowest risk) — Add an unconstrained @Builder<Element> overload alongside the typed one on all affected tags:

public init(@Builder<Element> _ block: () -> [Element]) {
    self.init(children: block())
}

Option 2 — Make SGML.Element (or concrete tag types) conform to FlowContent and PhrasingContent, so arrays of Element are accepted without casting.

Option 3 — Add the buildExpression bridge in the library itself, so user code doesn't need the workaround.

Option 1 is the lowest-risk change — it adds an unconstrained overload while keeping the typed one for users who want compile-time content model checking.


Related Issue

AttributeStore.swift fails to compile under Swift 6 strict import visibility:

// Current (fails under Swift 6 #MemberImportVisibility):
import Collections

// Fix:
import OrderedCollections

Issue created together with AI :-)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions