diff --git a/Sources/HTML/Tags.swift b/Sources/HTML/Tags.swift
index b4c51e8..a4f1d90 100644
--- a/Sources/HTML/Tags.swift
+++ b/Sources/HTML/Tags.swift
@@ -3,6 +3,13 @@
@_exported import Swim
+extension Node {
+ static func element(_ name: String, _ attributes: [String:String], _ node: Node?) -> Node {
+ Node.element(name, Attributes(attributes: attributes.sorted(by: { $0.key < $1.key }).map(Attribute.init)), node)
+ }
+}
+
+
public struct ATag: Tag {
public let elementName: String = "a"
diff --git a/Sources/Swim/Node.swift b/Sources/Swim/Node.swift
index 5d0947c..c63d506 100644
--- a/Sources/Swim/Node.swift
+++ b/Sources/Swim/Node.swift
@@ -1,8 +1,29 @@
import Foundation
+public struct Attribute: Hashable {
+ public var key: String
+ public var value: String
+
+ public init(_ pair: (String, String)) {
+ (self.key, self.value) = pair
+ }
+}
+
+public struct Attributes: ExpressibleByDictionaryLiteral, Hashable {
+ public var attributes: [Attribute]
+
+ public init(attributes: [Attribute]) {
+ self.attributes = attributes
+ }
+
+ public init(dictionaryLiteral elements: (String, String)...) {
+ self.attributes = elements.map(Attribute.init)
+ }
+}
+
public enum Node: Hashable {
// The `Node`'s name, attribute and children.
- indirect case element(String, [String: String], Node?)
+ indirect case element(String, Attributes, Node?)
// The `Node`'s text contents.
case text(String)
@@ -49,14 +70,14 @@ extension Node: TextOutputStreamable {
target.write("<")
target.write(name)
- for (key, value) in attributes.sorted(by: { $0 < $1 }) {
+ for attribute in attributes.attributes {
target.write(" ")
- target.write(key)
+ target.write(attribute.key)
- guard value != "" else { continue }
+ guard attribute.value != "" else { continue }
target.write("=\"")
- target.write(value.replacingOccurrences(of: "\"", with: """))
+ target.write(attribute.value.replacingOccurrences(of: "\"", with: """))
target.write("\"")
}
diff --git a/Sources/Swim/Visitor.swift b/Sources/Swim/Visitor.swift
index e3cdcb7..1ba0339 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: Attributes, 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: Attributes, child: Node?) -> Result {
.element(name, attributes, child.map(visitNode))
}
diff --git a/Tests/HTMLTests/HTMLTests.swift b/Tests/HTMLTests/HTMLTests.swift
index 674fff8..4240d4f 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: Attributes, 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: Attributes, child: Node?) -> Node {
if denyList.contains(where: { $0.elementName == name }) {
let original = Node.element(name, attributes, child)