Skip to content

Commit ebbb515

Browse files
HTMLEncoding updates, which allowed...
- removal of `swift-nio` dependency - addition of `custom` HTMLEncoding case (read documentation to learn how to use)
1 parent 18a47b8 commit ebbb515

File tree

7 files changed

+76
-54
lines changed

7 files changed

+76
-54
lines changed

Package.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ let package = Package(
1616
)
1717
],
1818
dependencies: [
19-
.package(url: "https://github.com/swiftlang/swift-syntax", from: "600.0.1"),
20-
.package(url: "https://github.com/apple/swift-nio", from: "2.75.0")
19+
.package(url: "https://github.com/swiftlang/swift-syntax", from: "600.0.1")
2120
],
2221
targets: [
2322
.macro(
@@ -35,8 +34,7 @@ let package = Package(
3534
"HTMLKitUtilityMacros",
3635
.product(name: "SwiftDiagnostics", package: "swift-syntax"),
3736
.product(name: "SwiftSyntax", package: "swift-syntax"),
38-
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
39-
.product(name: "NIOCore", package: "swift-nio")
37+
.product(name: "SwiftSyntaxMacros", package: "swift-syntax")
4038
]
4139
),
4240
.macro(

Sources/HTMLKit/HTMLKit.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
import struct Foundation.Data
1212
#endif
1313

14-
import struct NIOCore.ByteBuffer
15-
1614
// MARK: StaticString equality
1715
public extension StaticString {
1816
static func == (left: Self, right: Self) -> Bool { left.description == right.description }
@@ -29,8 +27,8 @@ public macro escapeHTML<T: ExpressibleByStringLiteral>(_ innerHTML: T...) -> T =
2927

3028
// MARK: HTML Representation
3129
@freestanding(expression)
32-
public macro html<T: ExpressibleByStringLiteral>(
30+
public macro html<T: CustomStringConvertible>(
3331
encoding: HTMLEncoding = .string,
3432
lookupFiles: [StaticString] = [],
35-
_ innerHTML: CustomStringConvertible...
33+
_ innerHTML: HTMLElement...
3634
) -> T = #externalMacro(module: "HTMLKitMacros", type: "HTMLElementMacro")

Sources/HTMLKitMacros/HTMLElement.swift

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,9 @@ import SwiftSyntaxMacros
1414
import struct Foundation.Data
1515
#endif
1616

17-
import struct NIOCore.ByteBuffer
18-
1917
enum HTMLElementMacro : ExpressionMacro {
2018
static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> ExprSyntax {
21-
let string:String = expand_macro(context: context, macro: node.macroExpansion!)
22-
let encoding:HTMLEncoding = HTMLEncoding.string
23-
19+
let (string, encoding):(String, HTMLEncoding) = expand_macro(context: context, macro: node.macroExpansion!)
2420
func has_no_interpolation() -> Bool {
2521
let has_interpolation:Bool = !string.ranges(of: try! Regex("\\((.*)\\)")).isEmpty
2622
guard !has_interpolation else {
@@ -52,15 +48,17 @@ enum HTMLElementMacro : ExpressionMacro {
5248

5349
case .string:
5450
return "\"\(raw: string)\""
51+
case .custom(let encoded):
52+
return "\(raw: encoded.replacingOccurrences(of: "$0", with: string))"
5553
}
5654
}
5755
}
5856

5957
private extension HTMLElementMacro {
6058
// MARK: Expand Macro
61-
static func expand_macro(context: some MacroExpansionContext, macro: MacroExpansionExprSyntax) -> String {
59+
static func expand_macro(context: some MacroExpansionContext, macro: MacroExpansionExprSyntax) -> (String, HTMLEncoding) {
6260
guard let elementType:HTMLElementType = HTMLElementType(rawValue: macro.macroName.text) else {
63-
return "\(macro)"
61+
return ("\(macro)", .string)
6462
}
6563
let children:SyntaxChildren = macro.arguments.children(viewMode: .all)
6664
/*if elementType == .escapeHTML {
@@ -71,6 +69,6 @@ private extension HTMLElementMacro {
7169
return array.map({ String(describing: $0) }).joined()
7270
}*/
7371
let data:HTMLKitUtilities.ElementData = HTMLKitUtilities.parse_arguments(context: context, children: children)
74-
return data.innerHTML.map({ String(describing: $0) }).joined()
72+
return (data.innerHTML.map({ String(describing: $0) }).joined(), data.encoding)
7573
}
7674
}

Sources/HTMLKitUtilities/HTMLEncoding.swift

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,29 @@
77

88
/// The value type the data should be encoded to when returned from the macro.
99
///
10-
/// ## Interpolation Promotion
11-
/// Swift HTMLKit tries to [promote](https://github.com/RandomHashTags/swift-htmlkit/blob/94793984763308ef5275dd9f71ea0b5e83fea417/Sources/HTMLKitMacros/HTMLElement.swift#L423) known interpolation at compile time with an equivalent `StaticString` for the best performance.
10+
/// ### Interpolation Promotion
11+
/// Swift HTMLKit tries to [promote](https://github.com/RandomHashTags/swift-htmlkit/blob/94793984763308ef5275dd9f71ea0b5e83fea417/Sources/HTMLKitMacros/HTMLElement.swift#L423) known interpolation at compile time with an equivalent string literal for the best performance, regardless of encoding.
1212
/// It is currently limited due to macro expansions being sandboxed and lexical contexts/AST not being available for macro arguments.
13-
/// This means referencing content known at compile time in a html macro won't get replaced by its literal contents.
13+
/// This means referencing content known at compile time in a html macro won't get promoted to its expected value.
1414
/// [Read more about this limitation](https://forums.swift.org/t/swift-lexical-lookup-for-referenced-stuff-located-outside-scope-current-file/75776/6).
1515
///
16-
/// ### Promotion Example
16+
/// #### Promotion Example
1717
///
1818
/// ```swift
1919
/// let _:StaticString = #html(div("\("string")")) // ✅ promotion makes this "<div>string</div>"
20-
/// let _:StaticString = #html("\(5)") // ✅ promotion makes this "<div>5</div>"
21-
/// let _:StaticString = #html(5) // ✅ promotion makes this "<div>5</div>"
20+
/// let _:StaticString = #html(div("\(5)")) // ✅ promotion makes this "<div>5</div>"
21+
/// let _:StaticString = #html(div(5)) // ✅ promotion makes this "<div>5</div>"
2222
/// ````
2323
///
24-
/// ### Promotion Limitation
24+
/// #### Promotion Limitation
2525
///
2626
/// ```swift
2727
/// let string:StaticString = "Test"
2828
/// let _:StaticString = #html(div(string)) // ❌ promotion cannot be applied; StaticString not allowed
2929
/// let _:String = #html(div(string)) // ⚠️ promotion cannot be applied; compiled as "<div>\(string)</div>"
30-
/// ````
30+
/// ```
3131
///
32-
public enum HTMLEncoding : String {
32+
public enum HTMLEncoding {
3333
/// `String`/`StaticString`
3434
case string
3535

@@ -47,6 +47,18 @@ public enum HTMLEncoding : String {
4747
case foundationData
4848

4949
/// `ByteBuffer`
50-
/// - Warning: Swift HTMLKit does not depend on `swift-nio`. You need to import `NIOCore` to use this!
50+
/// - Warning: You need to import `NIOCore` to use this! Swift HTMLKit does not depend on `swift-nio`!
5151
case byteBuffer
52+
53+
/// Encode the HTML into a custom type.
54+
///
55+
/// Use `$0` for the compiled HTML.
56+
/// - Parameters:
57+
/// - logic: The encoding logic, represented as a string.
58+
/// ### Example Usage
59+
/// ```swift
60+
/// let _:String = #html(encoding: .custom(#"String("$0")"#), p(5)) // String("<p>5</p>")
61+
/// ```
62+
///
63+
case custom(_ logic: String)
5264
}

Sources/HTMLKitUtilities/HTMLKitUtilities.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,20 @@
88
// MARK: HTMLKitUtilities
99
public enum HTMLKitUtilities {
1010
public struct ElementData {
11+
public let encoding:HTMLEncoding
1112
public let globalAttributes:[HTMLElementAttribute]
1213
public let attributes:[String:Any]
1314
public let innerHTML:[CustomStringConvertible]
1415
public let trailingSlash:Bool
1516

1617
init(
18+
_ encoding: HTMLEncoding,
1719
_ globalAttributes: [HTMLElementAttribute],
1820
_ attributes: [String:Any],
1921
_ innerHTML: [CustomStringConvertible],
2022
_ trailingSlash: Bool
2123
) {
24+
self.encoding = encoding
2225
self.globalAttributes = globalAttributes
2326
self.attributes = attributes
2427
self.innerHTML = innerHTML

Sources/HTMLKitUtilities/ParseData.swift

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public extension HTMLKitUtilities {
1515
context: some MacroExpansionContext,
1616
children: SyntaxChildren
1717
) -> ElementData {
18+
var encoding:HTMLEncoding = HTMLEncoding.string
1819
var global_attributes:[HTMLElementAttribute] = []
1920
var attributes:[String:Any] = [:]
2021
var innerHTML:[CustomStringConvertible] = []
@@ -23,7 +24,20 @@ public extension HTMLKitUtilities {
2324
for element in children {
2425
if let child:LabeledExprSyntax = element.labeled {
2526
if var key:String = child.label?.text {
26-
if key == "lookupFiles" {
27+
if key == "encoding" {
28+
if let key:String = child.expression.memberAccess?.declName.baseName.text {
29+
switch key {
30+
case "string": encoding = .string
31+
case "utf8Bytes": encoding = .utf8Bytes
32+
case "utf16Bytes": encoding = .utf16Bytes
33+
case "foundationData": encoding = .foundationData
34+
case "byteBuffer": encoding = .byteBuffer
35+
default: break
36+
}
37+
} else if let custom:FunctionCallExprSyntax = child.expression.functionCall {
38+
encoding = .custom(custom.arguments.first!.expression.stringLiteral!.string)
39+
}
40+
} else if key == "lookupFiles" {
2741
lookupFiles = Set(child.expression.array!.elements.compactMap({ $0.expression.stringLiteral?.string }))
2842
} else if key == "attributes" {
2943
(global_attributes, trailingSlash) = parse_global_attributes(context: context, array: child.expression.array!.elements, lookupFiles: lookupFiles)
@@ -43,7 +57,7 @@ public extension HTMLKitUtilities {
4357
}
4458
}
4559
}
46-
return ElementData(global_attributes, attributes, innerHTML, trailingSlash)
60+
return ElementData(encoding, global_attributes, attributes, innerHTML, trailingSlash)
4761
}
4862
// MARK: Parse Global Attributes
4963
static func parse_global_attributes(
@@ -144,7 +158,7 @@ public extension HTMLKitUtilities {
144158
default: break
145159
}
146160
for expr in interpolation {
147-
string.replace("\(expr)", with: flatten_interpolation(context: context, remaining_interpolation: &remaining_interpolation, expr: expr, lookupFiles: lookupFiles))
161+
string.replace("\(expr)", with: promote_interpolation(context: context, remaining_interpolation: &remaining_interpolation, expr: expr, lookupFiles: lookupFiles))
148162
}
149163
if remaining_interpolation > 0 {
150164
warn_interpolation(context: context, node: expression, string: &string, remaining_interpolation: &remaining_interpolation, lookupFiles: lookupFiles)
@@ -232,15 +246,15 @@ public extension HTMLKitUtilities {
232246
}
233247
return nil
234248
}
235-
// MARK: Flatten Interpolation
236-
static func flatten_interpolation(
249+
// MARK: Promote Interpolation
250+
static func promote_interpolation(
237251
context: some MacroExpansionContext,
238252
remaining_interpolation: inout Int,
239253
expr: ExpressionSegmentSyntax,
240254
lookupFiles: Set<String>
241255
) -> String {
242-
let expression:ExprSyntax = expr.expressions.first!.expression
243256
var string:String = "\(expr)"
257+
guard let expression:ExprSyntax = expr.expressions.first?.expression else { return string }
244258
if let stringLiteral:StringLiteralExprSyntax = expression.stringLiteral {
245259
let segments:StringLiteralSegmentListSyntax = stringLiteral.segments
246260
if segments.count(where: { $0.is(StringSegmentSyntax.self) }) == segments.count {
@@ -252,13 +266,13 @@ public extension HTMLKitUtilities {
252266
if let literal:String = segment.as(StringSegmentSyntax.self)?.content.text {
253267
string += literal
254268
} else if let interpolation:ExpressionSegmentSyntax = segment.as(ExpressionSegmentSyntax.self) {
255-
let flattened:String = flatten_interpolation(context: context, remaining_interpolation: &remaining_interpolation, expr: interpolation, lookupFiles: lookupFiles)
256-
if "\(interpolation)" == flattened {
257-
//string += "\\(\"\(flattened)\".escapingHTML(escapeAttributes: true))"
258-
string += "\(flattened)"
269+
let promoted:String = promote_interpolation(context: context, remaining_interpolation: &remaining_interpolation, expr: interpolation, lookupFiles: lookupFiles)
270+
if "\(interpolation)" == promoted {
271+
//string += "\\(\"\(promoted)\".escapingHTML(escapeAttributes: true))"
272+
string += "\(promoted)"
259273
warn_interpolation(context: context, node: interpolation, string: &string, remaining_interpolation: &remaining_interpolation, lookupFiles: lookupFiles)
260274
} else {
261-
string += flattened
275+
string += promoted
262276
}
263277
} else {
264278
//string += "\\(\"\(segment)\".escapingHTML(escapeAttributes: true))"

Tests/HTMLKitTests/HTMLKitTests.swift

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,47 +12,46 @@ import HTMLKit
1212
import struct Foundation.Data
1313
#endif
1414

15-
import struct NIOCore.ByteBuffer
16-
1715
// MARK: Representations
1816
struct HTMLKitTests {
1917
func representations() {
2018
let _:StaticString = #html()
19+
let _:StaticString = #html(encoding: .string)
2120
let _:String = #html()
22-
let _:[UInt8] = #htmlUTF8Bytes("")
23-
let _:[UInt16] = #htmlUTF16Bytes("")
24-
let _:ContiguousArray<CChar> = #htmlUTF8CString("")
21+
let _:String = #html(encoding: .string)
22+
let _:[UInt8] = #html(encoding: .utf8Bytes, p())
23+
let _:[UInt16] = #html(encoding: .utf16Bytes, p())
24+
let _:ContiguousArray<CChar> = #html(encoding: .utf8CString, p())
2525
#if canImport(Foundation)
26-
let _:Data = #htmlData("")
26+
let _:Data = #html(encoding: .foundationData, p())
2727
#endif
28-
let _:ByteBuffer = #htmlByteBuffer("")
29-
30-
//let bro:String = ""
31-
//let _:[UInt8] = #htmlUTF8Bytes("\(bro)")
28+
//let _:ByteBuffer = #html(encoding: .byteBuffer, "")
29+
let _:String = #html(encoding: .custom(#"String("$0")"#), p(5))
3230
}
3331
func representation1() -> StaticString {
34-
#html()
32+
#html(p(123))
3533
}
3634
func representation2() -> String {
37-
#html()
35+
#html(p(123))
3836
}
3937
func representation3() -> [UInt8] {
40-
#htmlUTF8Bytes("")
38+
#html(encoding: .utf8Bytes, p(123))
4139
}
4240
func representation4() -> [UInt16] {
43-
#htmlUTF16Bytes("")
41+
#html(encoding: .utf16Bytes, p(123))
4442
}
4543
func representation5() -> ContiguousArray<CChar> {
46-
#htmlUTF8CString("")
44+
#html(encoding: .utf8CString, p(123))
4745
}
4846
#if canImport(Foundation)
4947
func representation5() -> Data {
50-
#htmlData("")
48+
#html(encoding: .foundationData, p(123))
5149
}
5250
#endif
51+
/*
5352
func representation6() -> ByteBuffer {
5453
#htmlByteBuffer("")
55-
}
54+
}*/
5655
}
5756

5857
// MARK: StaticString Example

0 commit comments

Comments
 (0)