Skip to content

Commit 9415f36

Browse files
README fixes, and...
- *experimental* can now declare what value type you want the compiled html output to be
1 parent 38d479d commit 9415f36

File tree

5 files changed

+144
-10
lines changed

5 files changed

+144
-10
lines changed

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ Write HTML using Swift Macros.
1111
- [Dynamic](#dynamic)
1212
- [Conclusion](#conclusion)
1313
- [Contributing](#contributing)
14-
- [Funding](#funding)
1514

1615
## Why
1716
- Swift Macros are powerful, efficient and essentially removes any runtime overhead
@@ -89,9 +88,9 @@ If you're working with **runtime** data:
8988
- `<string>.escapeHTMLAttributes()`
9089
- mutates `self` escaping only attribute characters
9190
- `<string>.escapingHTML(escapeAttributes:)`
92-
- creates a copy of `self` escaping HTML and, optionally, attribute characters
91+
- returns a copy of `self` escaping HTML and, optionally, attribute characters
9392
- `<string>.escapingHTMLAttributes()`
94-
- creates a copy of `self` escaping only attribute characters
93+
- returns a copy of `self` escaping only attribute characters
9594

9695
</details>
9796

Sources/HTMLKit/HTMLKit.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,21 @@ import HTMLKitUtilities
99

1010
// MARK: StaticString equality
1111
public extension StaticString {
12-
static func == (left: Self, right: String) -> Bool { left.description == right }
13-
1412
static func == (left: Self, right: Self) -> Bool { left.description == right.description }
1513
static func != (left: Self, right: Self) -> Bool { left.description != right.description }
1614
}
15+
// MARK: StaticString == String equality
1716
public extension String {
1817
static func == (left: Self, right: StaticString) -> Bool { left == right.description }
18+
static func == (left: StaticString, right: Self) -> Bool { left.description == right }
1919
}
2020

2121
@freestanding(expression)
2222
public macro escapeHTML<T: ExpressibleByStringLiteral>(_ innerHTML: T...) -> T = #externalMacro(module: "HTMLKitMacros", type: "HTMLElement")
2323

2424
// MARK: Elements
2525
@freestanding(expression)
26-
public macro html<T: ExpressibleByStringLiteral>(attributes: [HTMLElementAttribute] = [], xmlns: T? = nil, _ innerHTML: T...) -> T = #externalMacro(module: "HTMLKitMacros", type: "HTMLElement")
26+
public macro html<V>(representation: HTMLDataRepresentation = .string, attributes: [HTMLElementAttribute] = [], xmlns: (any ExpressibleByStringLiteral)? = nil, _ innerHTML: any ExpressibleByStringLiteral...) -> V = #externalMacro(module: "HTMLKitMacros", type: "HTMLElement")
2727

2828
@freestanding(expression)
2929
public macro custom<T: ExpressibleByStringLiteral>(tag: String, isVoid: Bool, attributes: [HTMLElementAttribute] = [], _ innerHTML: T...) -> T = #externalMacro(module: "HTMLKitMacros", type: "HTMLElement")

Sources/HTMLKitMacros/HTMLElement.swift

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,81 @@ import SwiftSyntaxMacros
1010
import SwiftDiagnostics
1111
import HTMLKitUtilities
1212

13+
#if canImport(Foundation)
14+
import struct Foundation.Data
15+
#endif
16+
17+
#if canImport(NIOCore)
18+
import struct NIOCore.ByteBuffer
19+
#endif
20+
1321
struct HTMLElement : ExpressionMacro {
1422
static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> ExprSyntax {
15-
return "\"\(raw: parse_macro(context: context, expression: node.as(MacroExpansionExprSyntax.self)!))\""
23+
var representation:HTMLDataRepresentation
24+
if let declared:String = node.arguments.children(viewMode: .all).first(where: { $0.labeled?.label?.text == "representation" })?.labeled?.expression.as(MemberAccessExprSyntax.self)?.declName.baseName.text {
25+
//context.diagnose(Diagnostic(node: node, message: DiagnosticMsg(id: "test", message: context.lexicalContext.last!.debugDescription)))
26+
representation = HTMLDataRepresentation(rawValue: declared) ?? .string
27+
} else {
28+
representation = .string
29+
//context.diagnose(Diagnostic(node: node, message: DiagnosticMsg(id: "test", message: context.lexicalContext.last!.debugDescription)))
30+
if let returnClause:ReturnClauseSyntax = context.lexicalContext.first?.as(FunctionDeclSyntax.self)?.signature.returnClause {
31+
if let array_type:String = returnClause.type.as(ArrayTypeSyntax.self)?.element.as(IdentifierTypeSyntax.self)?.name.text {
32+
switch array_type {
33+
case "UInt8":
34+
representation = .uint8Array
35+
break
36+
case "UInt16":
37+
representation = .uint16Array
38+
break
39+
default:
40+
break
41+
}
42+
} else if let id:String = returnClause.type.as(IdentifierTypeSyntax.self)?.name.text {
43+
switch id {
44+
case "Data":
45+
#if canImport(Foundation)
46+
representation = .data
47+
#endif
48+
break
49+
case "ByteBuffer":
50+
#if canImport(NIOCore)
51+
representation = .byteBuffer
52+
#endif
53+
break
54+
default:
55+
break
56+
}
57+
}
58+
} else {
59+
//context.diagnose(Diagnostic(node: node, message: DiagnosticMsg(id: "test", message: context.lexicalContext.first!.debugDescription)))
60+
}
61+
}
62+
let string:String = parse_macro(context: context, expression: node.as(MacroExpansionExprSyntax.self)!)
63+
// TODO: check for interpolation
64+
func bytes<T: FixedWidthInteger>(_ bytes: [T]) -> String {
65+
return "[" + bytes.map({ "\($0)" }).joined(separator: ",") + "]"
66+
}
67+
switch representation {
68+
case .uint8Array: return "\(raw: bytes([UInt8](string.utf8)))"
69+
case .uint16Array: return "\(raw: bytes([UInt16](string.utf16)))"
70+
71+
#if canImport(Foundation)
72+
case .data: return "Data(\(raw: bytes([UInt8](string.utf8))))"
73+
#endif
74+
75+
#if canImport(NIOCore)
76+
case .byteBuffer: return "ByteBuffer(string: \(raw: string))"
77+
#endif
78+
79+
default: return "\"\(raw: string)\""
80+
}
81+
}
82+
}
83+
84+
extension HTMLDataRepresentation {
85+
enum Result {
86+
case string(String)
87+
case uint8Array([UInt8])
1688
}
1789
}
1890

@@ -50,7 +122,8 @@ private extension HTMLElement {
50122
for element in children {
51123
if let child:LabeledExprSyntax = element.labeled {
52124
if var key:String = child.label?.text {
53-
if key == "attributes" {
125+
if key == "representation" { // we don't care
126+
} else if key == "attributes" {
54127
attributes.append(contentsOf: parse_global_attributes(context: context, elementType: elementType, array: child.expression.array!))
55128
} else {
56129
if key == "acceptCharset" {

Sources/HTMLKitUtilities/HTMLKitUtilities.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,30 @@
55
// Created by Evan Anderson on 9/19/24.
66
//
77

8+
// MARK: Data Representation
9+
/// Determines what value type the HTML is compiled to.
10+
public enum HTMLDataRepresentation : String {
11+
// native Swift
12+
/// the raw compiled `StaticString`/`String`
13+
case string
14+
/// converts the compiled output to `[UInt8]` using utf8 encoding
15+
case uint8Array
16+
/// converts the compiled output to `[UInt16]` using utf16 encoding
17+
case uint16Array
18+
19+
// Foundation
20+
#if canImport(Foundation)
21+
/// converts the compiled output to `Data` using utf8 encoding
22+
case data
23+
#endif
24+
25+
// NIOCore
26+
#if canImport(NIOCore)
27+
/// converts the compiled output to `ByteBuffer` using utf8 encoding
28+
case byteBuffer
29+
#endif
30+
}
31+
832
// MARK: Escape HTML
933
public extension String {
1034
/// Escapes all occurrences of source-breaking HTML characters

Tests/HTMLKitTests/HTMLKitTests.swift

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
import Testing
99
import HTMLKit
1010

11+
#if canImport(Foundation)
12+
import Foundation
13+
#endif
14+
1115
struct HTMLKitTests {
1216
@Test func escape_html() {
1317
let unescaped:String = "<!DOCTYPE html><html>Test</html>"
@@ -38,6 +42,40 @@ struct HTMLKitTests {
3842
}
3943
}
4044

45+
extension HTMLKitTests {
46+
func representation1() {
47+
let _:StaticString = #html(representation: .string)
48+
let _:String = #html(representation: .string)
49+
let _:[UInt8] = #html(representation: .uint8Array)
50+
let _:[UInt16] = #html(representation: .uint16Array)
51+
#if canImport(Foundation)
52+
let _:Data = #html(representation: .data)
53+
#endif
54+
#if canImport(NIOCore)
55+
let _:[ByteBuffer] = #html(representation: .byteBuffer)
56+
#endif
57+
}
58+
func representation2() -> StaticString {
59+
#html()
60+
}
61+
func representation3() -> String {
62+
#html()
63+
}
64+
func representation4() -> [UInt8] {
65+
#html()
66+
}
67+
#if canImport(Foundation)
68+
func representation5() -> Data {
69+
#html()
70+
}
71+
#endif
72+
#if canImport(NIOCore)
73+
func representation6() -> ByteBuffer {
74+
#html()
75+
}
76+
#endif
77+
}
78+
4179
extension HTMLKitTests {
4280
@Test func element_html() {
4381
var string:StaticString = #html()
@@ -195,10 +233,10 @@ extension HTMLKitTests {
195233

196234
@Test func no_value_type() {
197235
let expected_string:String = "<!DOCTYPE html><html><body><h1>HTMLKitTests</h1></body></html>"
198-
let test1 = #html(#body(#h1("HTMLKitTests")))
236+
let test1:String = #html(#body(#h1("HTMLKitTests")))
199237
#expect(type(of: test1) == String.self)
200238
#expect(test1 == expected_string)
201-
let test2 = #html(#body(#h1(StaticString("HTMLKitTests"))))
239+
let test2:StaticString = #html(#body(#h1(StaticString("HTMLKitTests"))))
202240
#expect(type(of: test2) == StaticString.self)
203241
#expect(test2 == expected_string)
204242
}

0 commit comments

Comments
 (0)