Skip to content

Commit 07eedad

Browse files
escapeHTML macro now accepts an encoding parameter to escape correctly
- defaults to `string` and the parent encoding if contained in a #html macro
1 parent 747351e commit 07eedad

File tree

8 files changed

+239
-122
lines changed

8 files changed

+239
-122
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ Events.*
1818
EncodingTests.d
1919
EncodingTests.o
2020
EncodingTests.swiftdeps*
21+
EscapeHTMLTests.d
22+
EscapeHTMLTests.o
23+
EscapeHTMLTests.swiftdeps*
2124
HTMX.d
2225
HTMX.o
2326
HTMX.swiftdeps*

Sources/HTMLKit/HTMLKit.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ public extension StringProtocol {
1919
}
2020

2121
@freestanding(expression)
22-
public macro escapeHTML(_ innerHTML: CustomStringConvertible...) -> String = #externalMacro(module: "HTMLKitMacros", type: "EscapeHTML")
22+
public macro escapeHTML(
23+
encoding: HTMLEncoding = .string,
24+
_ innerHTML: CustomStringConvertible...
25+
) -> String = #externalMacro(module: "HTMLKitMacros", type: "EscapeHTML")
2326

2427
// MARK: HTML Representation
2528
@freestanding(expression)

Sources/HTMLKitUtilities/ParseData.swift

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,27 @@ import SwiftSyntaxMacros
1111

1212
public extension HTMLKitUtilities {
1313
// MARK: Escape HTML
14-
static func escapeHTML(expansion: MacroExpansionExprSyntax, context: some MacroExpansionContext) -> String {
15-
return expansion.arguments.children(viewMode: .all).compactMap({
16-
guard let child:LabeledExprSyntax = $0.labeled,
17-
// TODO: fix the below encoding?
18-
var c:CustomStringConvertible = HTMLKitUtilities.parseInnerHTML(context: context, encoding: .string, child: child, lookupFiles: []) else {
19-
return nil
20-
}
21-
if var element:HTMLElement = c as? HTMLElement {
22-
element.escaped = true
23-
c = element
14+
static func escapeHTML(expansion: MacroExpansionExprSyntax, encoding: HTMLEncoding = .string, context: some MacroExpansionContext) -> String {
15+
var encoding:HTMLEncoding = encoding
16+
let children:SyntaxChildren = expansion.arguments.children(viewMode: .all)
17+
var inner_html:String = ""
18+
inner_html.reserveCapacity(children.count)
19+
for e in children {
20+
if let child:LabeledExprSyntax = e.labeled {
21+
if let key:String = child.label?.text {
22+
if key == "encoding" {
23+
encoding = parseEncoding(expression: child.expression) ?? .string
24+
}
25+
} else if var c:CustomStringConvertible = HTMLKitUtilities.parseInnerHTML(context: context, encoding: encoding, child: child, lookupFiles: []) {
26+
if var element:HTMLElement = c as? HTMLElement {
27+
element.escaped = true
28+
c = element
29+
}
30+
inner_html += String(describing: c)
31+
}
2432
}
25-
return String(describing: c)
26-
}).joined()
33+
}
34+
return inner_html
2735
}
2836

2937
// MARK: Expand #html
@@ -82,16 +90,7 @@ public extension HTMLKitUtilities {
8290
if let child:LabeledExprSyntax = element.labeled {
8391
if let key:String = child.label?.text {
8492
if key == "encoding" {
85-
if let key:String = child.expression.memberAccess?.declName.baseName.text {
86-
encoding = HTMLEncoding(rawValue: key) ?? .string
87-
} else if let custom:FunctionCallExprSyntax = child.expression.functionCall {
88-
let logic:String = custom.arguments.first!.expression.stringLiteral!.string
89-
if custom.arguments.count == 1 {
90-
encoding = .custom(logic)
91-
} else {
92-
encoding = .custom(logic, stringDelimiter: custom.arguments.last!.expression.stringLiteral!.string)
93-
}
94-
}
93+
encoding = parseEncoding(expression: child.expression) ?? .string
9594
} else if key == "lookupFiles" {
9695
lookupFiles = Set(child.expression.array!.elements.compactMap({ $0.expression.stringLiteral?.string }))
9796
} else if key == "attributes" {
@@ -127,6 +126,21 @@ public extension HTMLKitUtilities {
127126
return ElementData(encoding, global_attributes, attributes, innerHTML, trailingSlash)
128127
}
129128

129+
// MARK: Parse Encoding
130+
static func parseEncoding(expression: ExprSyntax) -> HTMLEncoding? {
131+
if let key:String = expression.memberAccess?.declName.baseName.text {
132+
return HTMLEncoding(rawValue: key)
133+
} else if let custom:FunctionCallExprSyntax = expression.functionCall {
134+
guard let logic:String = custom.arguments.first?.expression.stringLiteral?.string else { return nil }
135+
if custom.arguments.count == 1 {
136+
return .custom(logic)
137+
} else {
138+
return .custom(logic, stringDelimiter: custom.arguments.last!.expression.stringLiteral!.string)
139+
}
140+
}
141+
return nil
142+
}
143+
130144
// MARK: Parse Global Attributes
131145
static func parseGlobalAttributes(
132146
context: some MacroExpansionContext,
@@ -170,7 +184,7 @@ public extension HTMLKitUtilities {
170184
) -> CustomStringConvertible? {
171185
if let expansion:MacroExpansionExprSyntax = child.expression.macroExpansion {
172186
if expansion.macroName.text == "escapeHTML" {
173-
return escapeHTML(expansion: expansion, context: context)
187+
return escapeHTML(expansion: expansion, encoding: encoding, context: context)
174188
}
175189
return "" // TODO: fix?
176190
} else if let element:HTMLElement = parse_element(context: context, encoding: encoding, expr: child.expression) {

Sources/HTMLKitUtilities/TranslateHTML.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
#if canImport(Foundation)
99
import Foundation
1010

11-
public enum TranslateHTML { // TODO: finish
11+
private enum TranslateHTML { // TODO: finish
1212
public static func translate(string: String) -> String {
1313
var result:String = ""
1414
result.reserveCapacity(string.count)
@@ -59,4 +59,5 @@ extension TranslateHTML {
5959
}
6060
}
6161
}
62+
6263
#endif

Tests/HTMLKitTests/AttributeTests.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ import Testing
99
import HTMLKit
1010

1111
struct AttributeTests {
12+
// MARK: ariarole
1213
@Test func ariarole() {
1314
//let array:String = HTMLElementType.allCases.map({ "case \"\($0)\": return \($0)(rawValue: rawValue)" }).joined(separator: "\n")
1415
//print(array)
1516
let string:StaticString = #html(div(attributes: [.role(.widget)]))
1617
#expect(string == "<div role=\"widget\"></div>")
1718
}
19+
20+
// MARK: ariaattribute
1821
@Test func ariaattribute() {
1922
var string:StaticString = #html(div(attributes: [.ariaattribute(.atomic(true))]))
2023
#expect(string == "<div aria-atomic=\"true\"></div>")
@@ -28,17 +31,29 @@ struct AttributeTests {
2831
string = #html(div(attributes: [.ariaattribute(.controls(["testing", "123", "yup"]))]))
2932
#expect(string == "<div aria-controls=\"testing 123 yup\"></div>")
3033
}
34+
35+
// MARK: attributionsrc
3136
@Test func attributionsrc() {
3237
var string:StaticString = #html(a(attributionsrc: []))
3338
#expect(string == "<a attributionsrc></a>")
3439

3540
string = #html(a(attributionsrc: ["https://github.com/RandomHashTags", "https://litleagues.com"]))
3641
#expect(string == "<a attributionsrc=\"https://github.com/RandomHashTags https://litleagues.com\"></a>")
3742
}
43+
44+
// MARK: class
45+
@Test func class_attribute() {
46+
let string:StaticString = #html(a(attributes: [.class(["womp", "donk", "g2-esports"])]))
47+
#expect(string == "<a class=\"womp donk g2-esports\"></a>")
48+
}
49+
50+
// MARK: data
3851
@Test func data() {
3952
let string:StaticString = #html(div(attributes: [.data("id", "5")]))
4053
#expect(string == "<div data-id=\"5\"></div>")
4154
}
55+
56+
// MARK: hidden
4257
@Test func hidden() {
4358
var string:StaticString = #html(div(attributes: [.hidden(.true)]))
4459
#expect(string == "<div hidden></div>")
@@ -47,7 +62,8 @@ struct AttributeTests {
4762
#expect(string == "<div hidden=\"until-found\"></div>")
4863
}
4964

50-
@Test func _custom() {
65+
// MARK: custom
66+
@Test func custom_attribute() {
5167
var string:StaticString = #html(div(attributes: [.custom("potofgold", "north")]))
5268
#expect(string == "<div potofgold=\"north\"></div>")
5369

@@ -58,11 +74,15 @@ struct AttributeTests {
5874
#expect(string == "<div potofgold1=\"1\" potofgold2=\"2\"></div>")
5975
}
6076

77+
// MARK: trailingSlash
6178
@Test func trailingSlash() {
6279
var string:StaticString = #html(meta(attributes: [.trailingSlash]))
6380
#expect(string == "<meta />")
6481

6582
string = #html(custom(tag: "slash", isVoid: true, attributes: [.trailingSlash]))
6683
#expect(string == "<slash />")
84+
85+
string = #html(custom(tag: "slash", isVoid: false, attributes: [.trailingSlash]))
86+
#expect(string == "<slash></slash>")
6787
}
6888
}

Tests/HTMLKitTests/ElementTests.swift

Lines changed: 32 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -9,51 +9,6 @@ import Testing
99
import HTMLKit
1010

1111
struct ElementTests {
12-
// MARK: Escape
13-
@Test func escape_html() {
14-
let unescaped:String = "<!DOCTYPE html><html>Test</html>"
15-
let escaped:String = "&lt;!DOCTYPE html&gt;&lt;html&gt;Test&lt;/html&gt;"
16-
var expected_result:String = "<p>\(escaped)</p>"
17-
18-
var string:String = #html(p("<!DOCTYPE html><html>Test</html>"))
19-
#expect(string == expected_result)
20-
21-
string = #escapeHTML("<!DOCTYPE html><html>Test</html>")
22-
#expect(string == escaped)
23-
24-
string = #escapeHTML(html("Test"))
25-
#expect(string == escaped)
26-
27-
string = #html(p(#escapeHTML(html("Test"))))
28-
#expect(string == expected_result)
29-
30-
string = #html(p("\(unescaped.escapingHTML(escapeAttributes: false))"))
31-
#expect(string == expected_result)
32-
33-
expected_result = "<div title=\"&lt;p&gt;\">&lt;p&gt;&lt;/p&gt;</div>"
34-
string = #html(div(attributes: [.title("<p>")], StaticString("<p></p>")))
35-
#expect(string == expected_result)
36-
37-
string = #html(div(attributes: [.title("<p>")], "<p></p>"))
38-
#expect(string == expected_result)
39-
40-
string = #html(p("What's 9 + 10? \"21\"!"))
41-
#expect(string == "<p>What&#39s 9 + 10? &quot;21&quot;!</p>")
42-
43-
string = #html(option(value: "bad boy <html>"))
44-
expected_result = "<option value=\"bad boy &lt;html&gt;\"></option>"
45-
#expect(string == expected_result)
46-
}
47-
}
48-
49-
50-
51-
// MARK: Elements
52-
53-
54-
55-
56-
extension ElementTests {
5712
// MARK: html
5813
@Test func _html() {
5914
var string:StaticString = #html(html())
@@ -63,12 +18,21 @@ extension ElementTests {
6318
#expect(string == "<!DOCTYPE html><html xmlns=\"test\"></html>")
6419
}
6520

66-
// MARK: HTMLKit.element
21+
// MARK: HTMLKit.<element>
6722
@Test func with_library_decl() {
6823
let string:StaticString = #html(html(HTMLKit.body()))
6924
#expect(string == "<!DOCTYPE html><html><body></body></html>")
7025
}
26+
}
27+
28+
29+
30+
// MARK: Elements
7131

32+
33+
34+
35+
extension ElementTests {
7236
// MARK: a
7337
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a
7438
@Test func _a() {
@@ -412,7 +376,7 @@ extension ElementTests {
412376
}
413377

414378
// MARK: custom
415-
@Test func _custom() {
379+
@Test func custom_element() {
416380
var bro:StaticString = #html(custom(tag: "bro", isVoid: false))
417381
#expect(bro == "<bro></bro>")
418382

@@ -475,27 +439,29 @@ extension ElementTests {
475439
}*/
476440

477441
/*@Test func not_allowed() {
478-
let _:StaticString = #div(attributes: [.id("1"), .id("2"), .id("3"), .id("4")])
479-
let _:StaticString = #a(
480-
attributes: [
481-
.class(["lets go"])
482-
],
483-
attributionSrc: ["lets go"],
484-
ping: ["lets go"]
442+
let _:StaticString = #html(div(attributes: [.id("1"), .id("2"), .id("3"), .id("4")]))
443+
let _:StaticString = #html(
444+
a(
445+
attributes: [
446+
.class(["lets go"])
447+
],
448+
attributionsrc: ["lets go"],
449+
ping: ["lets go"]
450+
)
485451
)
486-
let _:StaticString = #input(
487-
accept: ["lets,go"],
488-
autocomplete: ["lets go"]
452+
let _:StaticString = #html(
453+
input(
454+
accept: ["lets,go"],
455+
autocomplete: ["lets go"]
456+
)
489457
)
490-
let _:StaticString = #link(
491-
imagesizes: ["lets,go"],
492-
imagesrcset: ["lets,go"],
493-
rel: ["lets go"],
494-
sizes: ["lets,go"]
458+
let _:StaticString = #html(
459+
link(
460+
imagesizes: ["lets,go"],
461+
imagesrcset: ["lets,go"],
462+
rel: .stylesheet,
463+
size: "lets,go"
464+
)
495465
)
496-
let _:String = #div(attributes: [.custom("potof gold1", "\(1)"), .custom("potof gold2", "2")])
497-
498-
let _:StaticString = #div(attributes: [.trailingSlash])
499-
let _:StaticString = #html(custom(tag: "slash", isVoid: false, attributes: [.trailingSlash]))
500466
}*/
501467
}

0 commit comments

Comments
 (0)