From f6eaed91c344b21f1dd8f1b9a4304d1d76bbc11e Mon Sep 17 00:00:00 2001 From: Skwiggs <6209874+MrSkwiggs@users.noreply.github.com> Date: Mon, 11 May 2026 11:51:08 +0200 Subject: [PATCH 1/8] Refactor Markup to conform to StringInterpolation --- Sources/Ignite/Framework/Markup.swift | 84 ++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 7 deletions(-) diff --git a/Sources/Ignite/Framework/Markup.swift b/Sources/Ignite/Framework/Markup.swift index 96b2b3300..fd14a5978 100644 --- a/Sources/Ignite/Framework/Markup.swift +++ b/Sources/Ignite/Framework/Markup.swift @@ -6,7 +6,7 @@ // /// A structure that represents HTML markup content. -public struct Markup: Sendable { +public struct Markup: Sendable, ExpressibleByStringLiteral, ExpressibleByStringInterpolation { /// The raw HTML string content. var string: String @@ -15,10 +15,80 @@ public struct Markup: Sendable { return string.isEmpty } - /// Creates a markup instance with the given string. - /// - Parameter string: The string to use as HTML markup. - init(_ string: String = "") { - self.string = string + /// Creates an empty markup instance. + init() { + self.string = "" + } + + /// Creates a markup instance from another markup value. + /// - Parameter markup: The markup value to copy. + init(_ markup: Markup) { + self = markup + } + + /// Creates a markup instance from a raw string without interpolation handling. + /// - Parameter value: The string to use as HTML markup. + init(verbatim value: String) { + self.string = value + } + + /// Creates a markup instance from a static string literal. + /// - Parameter value: The string literal to use as HTML markup. + public init(stringLiteral value: String) { + self.string = value + } + + /// Creates a markup instance from an interpolated string literal. + /// - Parameter stringInterpolation: The accumulated interpolation state. + public init(stringInterpolation: StringInterpolation) { + if let context = PublishingContext.current { + for registration in stringInterpolation.pendingRegistrations { + registration.apply(to: context) + } + } + self.string = stringInterpolation.output + } + + /// Accumulator type used by Swift's string-interpolation machinery to build a `Markup`. + public struct StringInterpolation: StringInterpolationProtocol { + /// The interpolated HTML string assembled so far. + var output: String = "" + + /// Any pending publishing-time registrations to be applied. + var pendingRegistrations: [PublishingRegistration] = [] + + /// Creates a fresh interpolation accumulator with reserved capacity for the literal segments. + /// - Parameters: + /// - literalCapacity: The total length of the literal segments in the string. + /// - interpolationCount: The number of interpolations in the string. + public init(literalCapacity: Int, interpolationCount: Int) { + output.reserveCapacity(literalCapacity) + } + + /// Appends a literal segment of the interpolated string. + /// - Parameter literal: The literal text between interpolations. + public mutating func appendLiteral(_ literal: String) { + output += literal + } + + /// Appends a set of HTML attributes and queues their publishing-time registrations. + /// - Parameter attributes: The attributes to render and harvest from. + public mutating func appendInterpolation(_ attributes: CoreAttributes) { + pendingRegistrations.append(contentsOf: attributes.publishingRegistrations) + output += attributes.description + } + + /// Appends another markup value into the interpolation. + /// - Parameter markup: The markup whose contents should be appended. + public mutating func appendInterpolation(_ markup: Markup) { + output += markup.string + } + + /// Appends any other value by converting it to a string. + /// - Parameter value: The value to render via `String(describing:)`. + public mutating func appendInterpolation(_ value: T) { + output += String(describing: value) + } } } @@ -29,7 +99,7 @@ extension Array where Element == Markup { /// - Returns: A new Markup object with all strings joined func joined(separator: String = "") -> Markup { let joinedString = self.map { $0.string }.joined(separator: separator) - return Markup(joinedString) + return Markup(verbatim: joinedString) } } @@ -40,7 +110,7 @@ extension Markup { /// - rhs: Right-hand side Markup /// - Returns: A new Markup with the combined strings static func + (lhs: Markup, rhs: Markup) -> Markup { - return Markup(lhs.string + rhs.string) + return Markup(verbatim: lhs.string + rhs.string) } /// Allows for compound assignment (+=) with another Markup From c83a3382a0960ad18f821f01cf3302f0ada4f50b Mon Sep 17 00:00:00 2001 From: Skwiggs <6209874+MrSkwiggs@users.noreply.github.com> Date: Mon, 11 May 2026 11:51:36 +0200 Subject: [PATCH 2/8] Accommodate for Markup refactor --- Sources/Ignite/Elements/Image.swift | 4 ++-- Sources/Ignite/Elements/Include.swift | 2 +- Sources/Ignite/Elements/List.swift | 6 +++--- Sources/Ignite/Elements/PlainDocument.swift | 8 ++++---- Sources/Ignite/Elements/String.swift | 2 +- Sources/Ignite/Elements/Table.swift | 6 +++--- Sources/Ignite/Elements/Text.swift | 6 +----- Sources/Ignite/Elements/Video.swift | 4 ++-- Sources/Ignite/Framework/Analytics/Analytics.swift | 12 ++++++------ 9 files changed, 23 insertions(+), 27 deletions(-) diff --git a/Sources/Ignite/Elements/Image.swift b/Sources/Ignite/Elements/Image.swift index 670787cfb..69916a80f 100644 --- a/Sources/Ignite/Elements/Image.swift +++ b/Sources/Ignite/Elements/Image.swift @@ -120,7 +120,7 @@ public struct Image: InlineElement, LazyLoadable { return Markup("") } - var output = "" + var output = Markup("") if let darkSourceSet = generateSourceSet(darkVariants), let value = darkSourceSet.value { output += "" @@ -129,7 +129,7 @@ public struct Image: InlineElement, LazyLoadable { // Add the fallback img tag output += "" output += "" - return Markup(output) + return output } /// Renders this element using publishing context passed in. diff --git a/Sources/Ignite/Elements/Include.swift b/Sources/Ignite/Elements/Include.swift index 09c6899c3..7d5859100 100644 --- a/Sources/Ignite/Elements/Include.swift +++ b/Sources/Ignite/Elements/Include.swift @@ -34,7 +34,7 @@ public struct Include: HTML { do { let string = try String(contentsOf: fileURL) - return Markup(string) + return Markup(verbatim: string) } catch { publishingContext.addWarning(""" Failed to find \(filename) in Includes folder; \ diff --git a/Sources/Ignite/Elements/List.swift b/Sources/Ignite/Elements/List.swift index eeb4b43b9..a93266cc5 100644 --- a/Sources/Ignite/Elements/List.swift +++ b/Sources/Ignite/Elements/List.swift @@ -162,7 +162,7 @@ public struct List: HTML { public func markup() -> Markup { let listAttributes = getAttributes() - var output = "<\(listElementName)\(listAttributes)>" + var output: Markup = "<\(listElementName)\(listAttributes)>" for originalItem in items { var item = originalItem @@ -173,7 +173,7 @@ public struct List: HTML { if listStyle != .automatic { listableItem.attributes.append(classes: "list-group-item") } - output += listableItem.listMarkup().string + output += listableItem.listMarkup() } else { let styleClass = listStyle == .automatic ? "" : " class=\"list-group-item\"" item.attributes.append(classes: "m-0") @@ -183,6 +183,6 @@ public struct List: HTML { output += "" - return Markup(output) + return output } } diff --git a/Sources/Ignite/Elements/PlainDocument.swift b/Sources/Ignite/Elements/PlainDocument.swift index 4224097c3..682c38b88 100644 --- a/Sources/Ignite/Elements/PlainDocument.swift +++ b/Sources/Ignite/Elements/PlainDocument.swift @@ -42,11 +42,11 @@ public struct PlainDocument: Document, HTML { // Deferred head rendering to accommodate for context updates during body rendering let headMarkup = head.markup() - var output = "" + var output: Markup = "" output += "" - output += headMarkup.string - output += bodyMarkup.string + output += headMarkup + output += bodyMarkup output += "" - return Markup(output) + return output } } diff --git a/Sources/Ignite/Elements/String.swift b/Sources/Ignite/Elements/String.swift index a93dcc41e..df33a80bc 100644 --- a/Sources/Ignite/Elements/String.swift +++ b/Sources/Ignite/Elements/String.swift @@ -14,6 +14,6 @@ extension String: InlineElement, FormItem { /// Renders this element using publishing context passed in. /// - Returns: The HTML for this element. public func markup() -> Markup { - Markup(self) + Markup(verbatim: self) } } diff --git a/Sources/Ignite/Elements/Table.swift b/Sources/Ignite/Elements/Table.swift index 2a87a0cba..1f495bf87 100644 --- a/Sources/Ignite/Elements/Table.swift +++ b/Sources/Ignite/Elements/Table.swift @@ -164,7 +164,7 @@ public struct Table: HTML { tableAttributes.append(classes: ["table-striped-columns"]) } - var output = "" + var output = Markup() if let filterTitle { tableAttributes.id = "table-\(UUID().uuidString.truncatedHash)" @@ -190,9 +190,9 @@ public struct Table: HTML { } output += "" - output += rows.markupString() + output += Markup(verbatim: rows.markupString()) output += "" output += "" - return Markup(output) + return output } } diff --git a/Sources/Ignite/Elements/Text.swift b/Sources/Ignite/Elements/Text.swift index 8cd4d39e2..4dc904bfc 100644 --- a/Sources/Ignite/Elements/Text.swift +++ b/Sources/Ignite/Elements/Text.swift @@ -160,11 +160,7 @@ public struct Text: HTML, DropdownItem { .attributes(attributes) .markup() } else { - Markup( - "<\(font.rawValue)\(attributes)>" + - content.markupString() + - "" - ) + Markup("<\(font.rawValue)\(attributes)>\(content.markupString())") } } } diff --git a/Sources/Ignite/Elements/Video.swift b/Sources/Ignite/Elements/Video.swift index e41d24229..1cfbfc500 100644 --- a/Sources/Ignite/Elements/Video.swift +++ b/Sources/Ignite/Elements/Video.swift @@ -34,7 +34,7 @@ public struct Video: InlineElement, LazyLoadable { /// - files: The user videos to render. /// - Returns: The HTML for this element. private func render(files: [String]) -> Markup { - var output = "