diff --git a/Sources/Swim/Node.swift b/Sources/Swim/Node.swift index 5d0947c..dea8249 100644 --- a/Sources/Swim/Node.swift +++ b/Sources/Swim/Node.swift @@ -2,7 +2,7 @@ import Foundation public enum Node: Hashable { // The `Node`'s name, attribute and children. - indirect case element(String, [String: String], Node?) + indirect case element(String, [String: AnyHashable], Node?) // The `Node`'s text contents. case text(String) @@ -49,14 +49,26 @@ extension Node: TextOutputStreamable { target.write("<") target.write(name) - for (key, value) in attributes.sorted(by: { $0 < $1 }) { + for (key, value) in attributes.sorted(by: { $0.0 < $1.0 }) { target.write(" ") target.write(key) - guard value != "" else { continue } + let stringValue: String + + switch value.base { + case "" as String: + continue + case let string as String: + stringValue = string + case let streamable as TextOutputStreamable: + stringValue = String(describing: streamable) + default: + stringValue = String(describing: value.base) + } target.write("=\"") - target.write(value.replacingOccurrences(of: "\"", with: """)) + target.write(stringValue.replacingOccurrences(of: "\"", with: """, options: .literal)) + target.write("\"") } diff --git a/Sources/Swim/Visitor.swift b/Sources/Swim/Visitor.swift index e3cdcb7..23f4795 100644 --- a/Sources/Swim/Visitor.swift +++ b/Sources/Swim/Visitor.swift @@ -3,7 +3,7 @@ import Foundation public protocol Visitor { associatedtype Result - func visitElement(name: String, attributes: [String: String], child: Node?) -> Result + func visitElement(name: String, attributes: [String: AnyHashable], child: Node?) -> Result func visitText(text: String) -> Result @@ -42,7 +42,7 @@ extension Visitor { } public extension Visitor where Result == Node { - func visitElement(name: String, attributes: [String: String], child: Node?) -> Result { + func visitElement(name: String, attributes: [String: AnyHashable], child: Node?) -> Result { .element(name, attributes, child.map(visitNode)) } diff --git a/SwimBenchmark/Sources/SwimBenchmark/main.swift b/SwimBenchmark/Sources/SwimBenchmark/main.swift index b2bafa3..9cde6e0 100644 --- a/SwimBenchmark/Sources/SwimBenchmark/main.swift +++ b/SwimBenchmark/Sources/SwimBenchmark/main.swift @@ -9,176 +9,176 @@ benchmark("Build a basic page") { } } body { - article { + article(class: "article", id: "main") { section { - p { + p(class: "lorem-ipsum", id: "p-1") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } - p { + p(class: "lorem-ipsum", id: "p-2") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } - p { + p(class: "lorem-ipsum", id: "p-3") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } - p { + p(class: "lorem-ipsum", id: "p-4") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } - p { + p(class: "lorem-ipsum", id: "p-5") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } - p { + p(class: "lorem-ipsum", id: "p-6") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } - p { + p(class: "lorem-ipsum", id: "p-7") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } - p { + p(class: "lorem-ipsum", id: "p-8") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } } - section { - p { + section { + p(class: "lorem-ipsum", id: "p-9") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } - p { + p(class: "lorem-ipsum", id: "p-10") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } - p { + p(class: "lorem-ipsum", id: "p-11") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } - p { + p(class: "lorem-ipsum", id: "p-12") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } - p { + p(class: "lorem-ipsum", id: "p-13") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } - p { + p(class: "lorem-ipsum", id: "p-14") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } - p { + p(class: "lorem-ipsum", id: "p-15") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } - p { + p(class: "lorem-ipsum", id: "p-16") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } } section { - p { + p(class: "lorem-ipsum", id: "p-17") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } - p { + p(class: "lorem-ipsum", id: "p-18") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } - p { + p(class: "lorem-ipsum", id: "p-19") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } - p { + p(class: "lorem-ipsum", id: "p-20") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } - p { + p(class: "lorem-ipsum", id: "p-21") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } - p { + p(class: "lorem-ipsum", id: "p-22") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } - p { + p(class: "lorem-ipsum", id: "p-23") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } - p { + p(class: "lorem-ipsum", id: "p-24") { "Lorem ipsum, dolor sit amet." - a { + a(href: "https://example.org") { %"anchor"% } } diff --git a/Tests/HTMLTests/HTMLTests.swift b/Tests/HTMLTests/HTMLTests.swift index 674fff8..8246eef 100644 --- a/Tests/HTMLTests/HTMLTests.swift +++ b/Tests/HTMLTests/HTMLTests.swift @@ -189,7 +189,7 @@ final class HTMLTests: XCTestCase { struct TextExtractionVisitor: Visitor { typealias Result = [String] - func visitElement(name: String, attributes: [String : String], child: Node?) -> [String] { + func visitElement(name: String, attributes: [String: AnyHashable], child: Node?) -> [String] { child.map(visitNode) ?? [] } @@ -264,7 +264,7 @@ final class HTMLTests: XCTestCase { struct Sanitizer: Visitor { var denyList: [Tag] - func visitElement(name: String, attributes: [String : String], child: Node?) -> Node { + func visitElement(name: String, attributes: [String: AnyHashable], child: Node?) -> Node { if denyList.contains(where: { $0.elementName == name }) { let original = Node.element(name, attributes, child) diff --git a/Tests/SwimTests/SwimTests.swift b/Tests/SwimTests/SwimTests.swift index 5741cbc..1fc9971 100644 --- a/Tests/SwimTests/SwimTests.swift +++ b/Tests/SwimTests/SwimTests.swift @@ -27,4 +27,36 @@ final class SwimTests: XCTestCase { ], nil) XCTAssertEqual(n.rendered, "\n") } + + func testCustomAttributeValue() { + let color = Color(red: 1, green: 0.5, blue: 0.5) + + let n: Node = Node.element("custom", [ + "color": color + ], nil) + + XCTAssertEqual(n.rendered, "\n") + + if case let .element(_, attributes, _) = n { + XCTAssertEqual(attributes["color"]?.base as? Color, color) + } else { + XCTFail() + } + } +} + +struct Color: Hashable, TextOutputStreamable { + var red: Float + + var green: Float + + var blue: Float + + func write(to target: inout Target) where Target : TextOutputStream { + target.write(String(red)) + target.write(",") + target.write(String(green)) + target.write(",") + target.write(String(blue)) + } }