Skip to content

Commit

Permalink
escapeHTML macro now accepts an encoding parameter to escape correctly
Browse files Browse the repository at this point in the history
- defaults to `string` and the parent encoding if contained in a #html macro
  • Loading branch information
RandomHashTags committed Nov 29, 2024
1 parent 747351e commit 07eedad
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 122 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ Events.*
EncodingTests.d
EncodingTests.o
EncodingTests.swiftdeps*
EscapeHTMLTests.d
EscapeHTMLTests.o
EscapeHTMLTests.swiftdeps*
HTMX.d
HTMX.o
HTMX.swiftdeps*
Expand Down
5 changes: 4 additions & 1 deletion Sources/HTMLKit/HTMLKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ public extension StringProtocol {
}

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

// MARK: HTML Representation
@freestanding(expression)
Expand Down
60 changes: 37 additions & 23 deletions Sources/HTMLKitUtilities/ParseData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,27 @@ import SwiftSyntaxMacros

public extension HTMLKitUtilities {
// MARK: Escape HTML
static func escapeHTML(expansion: MacroExpansionExprSyntax, context: some MacroExpansionContext) -> String {
return expansion.arguments.children(viewMode: .all).compactMap({
guard let child:LabeledExprSyntax = $0.labeled,
// TODO: fix the below encoding?
var c:CustomStringConvertible = HTMLKitUtilities.parseInnerHTML(context: context, encoding: .string, child: child, lookupFiles: []) else {
return nil
}
if var element:HTMLElement = c as? HTMLElement {
element.escaped = true
c = element
static func escapeHTML(expansion: MacroExpansionExprSyntax, encoding: HTMLEncoding = .string, context: some MacroExpansionContext) -> String {
var encoding:HTMLEncoding = encoding
let children:SyntaxChildren = expansion.arguments.children(viewMode: .all)
var inner_html:String = ""
inner_html.reserveCapacity(children.count)
for e in children {
if let child:LabeledExprSyntax = e.labeled {
if let key:String = child.label?.text {
if key == "encoding" {
encoding = parseEncoding(expression: child.expression) ?? .string
}
} else if var c:CustomStringConvertible = HTMLKitUtilities.parseInnerHTML(context: context, encoding: encoding, child: child, lookupFiles: []) {
if var element:HTMLElement = c as? HTMLElement {
element.escaped = true
c = element
}
inner_html += String(describing: c)
}
}
return String(describing: c)
}).joined()
}
return inner_html
}

// MARK: Expand #html
Expand Down Expand Up @@ -82,16 +90,7 @@ public extension HTMLKitUtilities {
if let child:LabeledExprSyntax = element.labeled {
if let key:String = child.label?.text {
if key == "encoding" {
if let key:String = child.expression.memberAccess?.declName.baseName.text {
encoding = HTMLEncoding(rawValue: key) ?? .string
} else if let custom:FunctionCallExprSyntax = child.expression.functionCall {
let logic:String = custom.arguments.first!.expression.stringLiteral!.string
if custom.arguments.count == 1 {
encoding = .custom(logic)
} else {
encoding = .custom(logic, stringDelimiter: custom.arguments.last!.expression.stringLiteral!.string)
}
}
encoding = parseEncoding(expression: child.expression) ?? .string
} else if key == "lookupFiles" {
lookupFiles = Set(child.expression.array!.elements.compactMap({ $0.expression.stringLiteral?.string }))
} else if key == "attributes" {
Expand Down Expand Up @@ -127,6 +126,21 @@ public extension HTMLKitUtilities {
return ElementData(encoding, global_attributes, attributes, innerHTML, trailingSlash)
}

// MARK: Parse Encoding
static func parseEncoding(expression: ExprSyntax) -> HTMLEncoding? {
if let key:String = expression.memberAccess?.declName.baseName.text {
return HTMLEncoding(rawValue: key)
} else if let custom:FunctionCallExprSyntax = expression.functionCall {
guard let logic:String = custom.arguments.first?.expression.stringLiteral?.string else { return nil }
if custom.arguments.count == 1 {
return .custom(logic)
} else {
return .custom(logic, stringDelimiter: custom.arguments.last!.expression.stringLiteral!.string)
}
}
return nil
}

// MARK: Parse Global Attributes
static func parseGlobalAttributes(
context: some MacroExpansionContext,
Expand Down Expand Up @@ -170,7 +184,7 @@ public extension HTMLKitUtilities {
) -> CustomStringConvertible? {
if let expansion:MacroExpansionExprSyntax = child.expression.macroExpansion {
if expansion.macroName.text == "escapeHTML" {
return escapeHTML(expansion: expansion, context: context)
return escapeHTML(expansion: expansion, encoding: encoding, context: context)
}
return "" // TODO: fix?
} else if let element:HTMLElement = parse_element(context: context, encoding: encoding, expr: child.expression) {
Expand Down
3 changes: 2 additions & 1 deletion Sources/HTMLKitUtilities/TranslateHTML.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
#if canImport(Foundation)
import Foundation

public enum TranslateHTML { // TODO: finish
private enum TranslateHTML { // TODO: finish
public static func translate(string: String) -> String {
var result:String = ""
result.reserveCapacity(string.count)
Expand Down Expand Up @@ -59,4 +59,5 @@ extension TranslateHTML {
}
}
}

#endif
22 changes: 21 additions & 1 deletion Tests/HTMLKitTests/AttributeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ import Testing
import HTMLKit

struct AttributeTests {
// MARK: ariarole
@Test func ariarole() {
//let array:String = HTMLElementType.allCases.map({ "case \"\($0)\": return \($0)(rawValue: rawValue)" }).joined(separator: "\n")
//print(array)
let string:StaticString = #html(div(attributes: [.role(.widget)]))
#expect(string == "<div role=\"widget\"></div>")
}

// MARK: ariaattribute
@Test func ariaattribute() {
var string:StaticString = #html(div(attributes: [.ariaattribute(.atomic(true))]))
#expect(string == "<div aria-atomic=\"true\"></div>")
Expand All @@ -28,17 +31,29 @@ struct AttributeTests {
string = #html(div(attributes: [.ariaattribute(.controls(["testing", "123", "yup"]))]))
#expect(string == "<div aria-controls=\"testing 123 yup\"></div>")
}

// MARK: attributionsrc
@Test func attributionsrc() {
var string:StaticString = #html(a(attributionsrc: []))
#expect(string == "<a attributionsrc></a>")

string = #html(a(attributionsrc: ["https://github.com/RandomHashTags", "https://litleagues.com"]))
#expect(string == "<a attributionsrc=\"https://github.com/RandomHashTags https://litleagues.com\"></a>")
}

// MARK: class
@Test func class_attribute() {
let string:StaticString = #html(a(attributes: [.class(["womp", "donk", "g2-esports"])]))
#expect(string == "<a class=\"womp donk g2-esports\"></a>")
}

// MARK: data
@Test func data() {
let string:StaticString = #html(div(attributes: [.data("id", "5")]))
#expect(string == "<div data-id=\"5\"></div>")
}

// MARK: hidden
@Test func hidden() {
var string:StaticString = #html(div(attributes: [.hidden(.true)]))
#expect(string == "<div hidden></div>")
Expand All @@ -47,7 +62,8 @@ struct AttributeTests {
#expect(string == "<div hidden=\"until-found\"></div>")
}

@Test func _custom() {
// MARK: custom
@Test func custom_attribute() {
var string:StaticString = #html(div(attributes: [.custom("potofgold", "north")]))
#expect(string == "<div potofgold=\"north\"></div>")

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

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

string = #html(custom(tag: "slash", isVoid: true, attributes: [.trailingSlash]))
#expect(string == "<slash />")

string = #html(custom(tag: "slash", isVoid: false, attributes: [.trailingSlash]))
#expect(string == "<slash></slash>")
}
}
98 changes: 32 additions & 66 deletions Tests/HTMLKitTests/ElementTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,51 +9,6 @@ import Testing
import HTMLKit

struct ElementTests {
// MARK: Escape
@Test func escape_html() {
let unescaped:String = "<!DOCTYPE html><html>Test</html>"
let escaped:String = "&lt;!DOCTYPE html&gt;&lt;html&gt;Test&lt;/html&gt;"
var expected_result:String = "<p>\(escaped)</p>"

var string:String = #html(p("<!DOCTYPE html><html>Test</html>"))
#expect(string == expected_result)

string = #escapeHTML("<!DOCTYPE html><html>Test</html>")
#expect(string == escaped)

string = #escapeHTML(html("Test"))
#expect(string == escaped)

string = #html(p(#escapeHTML(html("Test"))))
#expect(string == expected_result)

string = #html(p("\(unescaped.escapingHTML(escapeAttributes: false))"))
#expect(string == expected_result)

expected_result = "<div title=\"&lt;p&gt;\">&lt;p&gt;&lt;/p&gt;</div>"
string = #html(div(attributes: [.title("<p>")], StaticString("<p></p>")))
#expect(string == expected_result)

string = #html(div(attributes: [.title("<p>")], "<p></p>"))
#expect(string == expected_result)

string = #html(p("What's 9 + 10? \"21\"!"))
#expect(string == "<p>What&#39s 9 + 10? &quot;21&quot;!</p>")

string = #html(option(value: "bad boy <html>"))
expected_result = "<option value=\"bad boy &lt;html&gt;\"></option>"
#expect(string == expected_result)
}
}



// MARK: Elements




extension ElementTests {
// MARK: html
@Test func _html() {
var string:StaticString = #html(html())
Expand All @@ -63,12 +18,21 @@ extension ElementTests {
#expect(string == "<!DOCTYPE html><html xmlns=\"test\"></html>")
}

// MARK: HTMLKit.element
// MARK: HTMLKit.<element>
@Test func with_library_decl() {
let string:StaticString = #html(html(HTMLKit.body()))
#expect(string == "<!DOCTYPE html><html><body></body></html>")
}
}



// MARK: Elements




extension ElementTests {
// MARK: a
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a
@Test func _a() {
Expand Down Expand Up @@ -412,7 +376,7 @@ extension ElementTests {
}

// MARK: custom
@Test func _custom() {
@Test func custom_element() {
var bro:StaticString = #html(custom(tag: "bro", isVoid: false))
#expect(bro == "<bro></bro>")

Expand Down Expand Up @@ -475,27 +439,29 @@ extension ElementTests {
}*/

/*@Test func not_allowed() {
let _:StaticString = #div(attributes: [.id("1"), .id("2"), .id("3"), .id("4")])
let _:StaticString = #a(
attributes: [
.class(["lets go"])
],
attributionSrc: ["lets go"],
ping: ["lets go"]
let _:StaticString = #html(div(attributes: [.id("1"), .id("2"), .id("3"), .id("4")]))
let _:StaticString = #html(
a(
attributes: [
.class(["lets go"])
],
attributionsrc: ["lets go"],
ping: ["lets go"]
)
)
let _:StaticString = #input(
accept: ["lets,go"],
autocomplete: ["lets go"]
let _:StaticString = #html(
input(
accept: ["lets,go"],
autocomplete: ["lets go"]
)
)
let _:StaticString = #link(
imagesizes: ["lets,go"],
imagesrcset: ["lets,go"],
rel: ["lets go"],
sizes: ["lets,go"]
let _:StaticString = #html(
link(
imagesizes: ["lets,go"],
imagesrcset: ["lets,go"],
rel: .stylesheet,
size: "lets,go"
)
)
let _:String = #div(attributes: [.custom("potof gold1", "\(1)"), .custom("potof gold2", "2")])

let _:StaticString = #div(attributes: [.trailingSlash])
let _:StaticString = #html(custom(tag: "slash", isVoid: false, attributes: [.trailingSlash]))
}*/
}
Loading

0 comments on commit 07eedad

Please sign in to comment.