Skip to content

Instantly share code, notes, and snippets.

@aciidgh
Created May 15, 2024 13:37
Show Gist options
  • Save aciidgh/adbebcc8ca255c2c4030133753d36e31 to your computer and use it in GitHub Desktop.
Save aciidgh/adbebcc8ca255c2c4030133753d36e31 to your computer and use it in GitHub Desktop.
HTML resultBuilder example
import Foundation
/// Helper for creating HTML document in Swift using resultbuilders.
package struct HTML: HTML.Tag {
package protocol Tag {
var elementName: String { get }
}
package let elementName: String = "html"
package func callAsFunction(
lang: String? = nil,
@NodeBuilder children: () -> NodeConvertible = { Node.fragment([]) }
) -> Node {
var attributes: [String: String] = [:]
if let lang = lang {
attributes["lang"] = lang
}
let element = Node.Element(
name: elementName,
attributes: attributes,
children: children().asNode()
)
return .element(element)
}
public init() {
}
}
package enum Node: Hashable {
public struct Element: Hashable {
let name: String
let attributes: [String: String]
let children: Node?
}
indirect case element(Element)
case text(String)
case fragment([Node])
case trim
public var string: String {
var output = ""
self.write(to: &output)
return (output)
}
}
@resultBuilder
struct NodeBuilder {
static func buildBlock(_ components: Node...) -> Node {
return .fragment(components)
}
}
package protocol NodeConvertible {
func asNode() -> Node
}
extension Node: NodeConvertible {
package func asNode() -> Node {
return self
}
}
extension Node: TextOutputStreamable {
package func write<Target>(
to target: inout Target
) where Target: TextOutputStream {
switch self {
case .element(let element):
target.write("<")
target.write(element.name)
for (key, value) in element.attributes.sorted(by: { $0 < $1 }) {
target.write(" ")
target.write(key)
guard value != "" else { continue }
target.write("=\"")
target.write(value.replacingOccurrences(of: "\"", with: "&quot;"))
target.write("\"")
}
if let _ = element.children {
target.write(">")
target.write("</")
target.write(element.name)
target.write(">")
} else {
target.write("/>")
}
case .text(let value):
print("value", value)
case .fragment(let children):
print("fragment", children)
case .trim:
break
}
}
}
// MARK: - Private extensions
extension TextOutputStream {
fileprivate mutating func writeWhitespace(indent: Int) {
write(String(repeating: " ", count: indent))
}
}
extension String {
fileprivate var xml: String {
guard unicodeScalars.contains(where: \.needsEscaping) else {
return self
}
return unicodeScalars.reduce(into: "", { $0.appendEscaped($1) })
}
fileprivate mutating func appendEscaped(_ unicodeScalar: Unicode.Scalar) {
switch unicodeScalar {
case "&":
append("&amp;")
case "<":
append("&lt;")
case ">":
append("&gt;")
case "\'":
append("&apos;")
case "\"":
append("&quot;")
default:
append(Character(unicodeScalar))
}
}
}
extension UnicodeScalar {
fileprivate var needsEscaping: Bool {
switch self {
case "&", "<", ">", "\'", "\"":
return true
default:
return false
}
}
}
// MARK: - Public extensions
extension Node: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self = .text(value)
}
}
extension Node: ExpressibleByArrayLiteral {
public init(arrayLiteral elements: Node...) {
if elements.count == 1 {
self = elements[0]
} else {
self = .fragment(elements)
}
}
}
@aciidgh
Copy link
Author

aciidgh commented May 15, 2024

Usage:

let html = HTML()
let node = html(lang: "en-US")
print(node.string)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment