Created
May 15, 2024 13:37
-
-
Save aciidgh/adbebcc8ca255c2c4030133753d36e31 to your computer and use it in GitHub Desktop.
HTML resultBuilder example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: """)) | |
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("&") | |
case "<": | |
append("<") | |
case ">": | |
append(">") | |
case "\'": | |
append("'") | |
case "\"": | |
append(""") | |
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) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage: