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
4 changes: 2 additions & 2 deletions Sources/Ignite/Elements/Image.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public struct Image: InlineElement, LazyLoadable {
return Markup("<img\(attributes) />")
}

var output = "<picture>"
var output = Markup("<picture>")

if let darkSourceSet = generateSourceSet(darkVariants), let value = darkSourceSet.value {
output += "<source media=\"(prefers-color-scheme: dark)\" srcset=\"\(value)\">"
Expand All @@ -129,7 +129,7 @@ public struct Image: InlineElement, LazyLoadable {
// Add the fallback img tag
output += "<img\(attributes) />"
output += "</picture>"
return Markup(output)
return output
}

/// Renders this element using publishing context passed in.
Expand Down
2 changes: 1 addition & 1 deletion Sources/Ignite/Elements/Include.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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; \
Expand Down
6 changes: 3 additions & 3 deletions Sources/Ignite/Elements/List.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand All @@ -183,6 +183,6 @@ public struct List: HTML {

output += "</\(listElementName)>"

return Markup(output)
return output
}
}
8 changes: 4 additions & 4 deletions Sources/Ignite/Elements/PlainDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<!doctype html>"
var output: Markup = "<!doctype html>"
output += "<html\(attributes)>"
output += headMarkup.string
output += bodyMarkup.string
output += headMarkup
output += bodyMarkup
output += "</html>"
return Markup(output)
return output
}
}
2 changes: 1 addition & 1 deletion Sources/Ignite/Elements/String.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
6 changes: 3 additions & 3 deletions Sources/Ignite/Elements/Table.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand All @@ -190,9 +190,9 @@ public struct Table: HTML {
}

output += "<tbody>"
output += rows.markupString()
output += Markup(verbatim: rows.markupString())
output += "</tbody>"
output += "</table>"
return Markup(output)
return output
}
}
6 changes: 1 addition & 5 deletions Sources/Ignite/Elements/Text.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,7 @@ public struct Text: HTML, DropdownItem {
.attributes(attributes)
.markup()
} else {
Markup(
"<\(font.rawValue)\(attributes)>" +
content.markupString() +
"</\(font.rawValue)>"
)
Markup("<\(font.rawValue)\(attributes)>\(content.markupString())</\(font.rawValue)>")
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/Ignite/Elements/Video.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<video controls\(attributes)>"
var output: Markup = "<video controls\(attributes)>"

for filename in files {
if let fileType = videoType(for: filename) {
Expand All @@ -44,7 +44,7 @@ public struct Video: InlineElement, LazyLoadable {

output += "Your browser does not support the video tag."
output += "</video>"
return Markup(output)
return output
}

/// Renders this element using publishing context passed in.
Expand Down
12 changes: 6 additions & 6 deletions Sources/Ignite/Framework/Analytics/Analytics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,22 @@ public struct Analytics: HeadElement {
public func markup() -> Markup {
switch service {
case .googleAnalytics(let measurementID):
Markup(googleAnalyticsCode(for: measurementID))
Markup(verbatim: googleAnalyticsCode(for: measurementID))

case .plausible(let domain, let measurements):
Markup(plausibleCode(for: domain, using: measurements))
Markup(verbatim: plausibleCode(for: domain, using: measurements))

case .fathom(let siteID):
Markup(fathomCode(for: siteID))
Markup(verbatim: fathomCode(for: siteID))

case .clicky(let siteID):
Markup(clickyCode(for: siteID))
Markup(verbatim: clickyCode(for: siteID))

case .telemetryDeck(let siteID):
Markup(telemetryDeckCode(for: siteID))
Markup(verbatim: telemetryDeckCode(for: siteID))

case .custom(let code):
Markup(code)
Markup(verbatim: code)
}
}

Expand Down
58 changes: 10 additions & 48 deletions Sources/Ignite/Framework/CoreAttributes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,10 @@
//

import Foundation
import OrderedCollections

// A typealias that allows us to use `OrderedSet` without importing OrderedCollections
public typealias OrderedSet<Element: Hashable> = OrderedCollections.OrderedSet<Element>

// A typealias that allows us to use `OrderedDictionary` without importing OrderedCollections
public typealias OrderedDictionary<Key: Hashable, Value> = OrderedCollections.OrderedDictionary<Key, Value>

// A typealias that allows us to use `UUID` without importing Foundation
public typealias UUID = Foundation.UUID

// A typealias that allows us to use `URL` without importing Foundation
public typealias URL = Foundation.URL

// A typealias that allows us to use `Data` without importing Foundation
public typealias Data = Foundation.Data

// A typealias that allows us to use `Date` without importing Foundation
public typealias Date = Foundation.Date

/// A publishing-time registration required by rendered attributes.
enum PublishingRegistration: Hashable, Equatable, Sendable {
case fontFamily(Font)
case responsiveFont(ResponsiveValues<LengthUnit>)
case responsiveVisibility(ResponsiveValues<Bool>)

func apply(to context: PublishingContext) {
switch self {
case .fontFamily(let font):
context.cssManager.registerFontFamily(font)
case .responsiveFont(let values):
_ = context.cssManager.registerFont(values)
case .responsiveVisibility(let values):
_ = context.cssManager.registerStyles(values)
}
}
}

/// A handful of attributes that all HTML types must support, either for
/// rendering or for publishing purposes.
public struct CoreAttributes: Equatable, Sendable, CustomStringConvertible {
public struct CoreAttributes: Equatable, Sendable {
/// A unique identifier. Can be empty.
var id = ""

Expand Down Expand Up @@ -75,10 +38,10 @@ public struct CoreAttributes: Equatable, Sendable, CustomStringConvertible {
/// Whether this set of attributes is empty.
var isEmpty: Bool { self == CoreAttributes() }

/// All core attributes collapsed down to a single string for easy application.
public var description: String {
registerPublishingRequirements()
return "\(idString)\(customAttributeString)\(classString)\(styleString)\(dataString)\(ariaString)\(eventString)"
/// All core attributes collapsed down to a single string for inclusion
/// in HTML markup output.
var markupAttributeString: String {
"\(idString)\(customAttributeString)\(classString)\(styleString)\(dataString)\(ariaString)\(eventString)"
}

/// The ID of this element, if set.
Expand Down Expand Up @@ -337,12 +300,11 @@ public struct CoreAttributes: Equatable, Sendable, CustomStringConvertible {
publishingRegistrations.formUnion(other.publishingRegistrations)
}

/// Applies publishing-time registrations when a publishing context is active.
private func registerPublishingRequirements() {
guard let context = PublishingContext.current else { return }
}

for registration in publishingRegistrations {
registration.apply(to: context)
}
extension String.StringInterpolation {
@available(*, deprecated, message: "Interpolate CoreAttributes into Markup, not String — String interpolation drops publishing-time registrations.")
public mutating func appendInterpolation(_ attributes: CoreAttributes) {
appendLiteral(attributes.markupAttributeString)
}
}
84 changes: 77 additions & 7 deletions Sources/Ignite/Framework/Markup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.markupAttributeString
}

/// 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<T>(_ value: T) {
output += String(describing: value)
}
}
}

Expand All @@ -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)
}
}

Expand All @@ -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
Expand Down
26 changes: 26 additions & 0 deletions Sources/Ignite/Framework/PublishingRegistration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// PublishingRegistration.swift
// Ignite
// https://www.github.com/twostraws/Ignite
// See LICENSE for license information.
//

import Foundation

/// A publishing-time registration required by rendered attributes.
enum PublishingRegistration: Hashable, Equatable, Sendable {
case fontFamily(Font)
case responsiveFont(ResponsiveValues<LengthUnit>)
case responsiveVisibility(ResponsiveValues<Bool>)

func apply(to context: PublishingContext) {
switch self {
case .fontFamily(let font):
context.cssManager.registerFontFamily(font)
case .responsiveFont(let values):
_ = context.cssManager.registerFont(values)
case .responsiveVisibility(let values):
_ = context.cssManager.registerStyles(values)
}
}
}
Loading
Loading