Skip to content

Commit 0172302

Browse files
development improvements & breaking changes
- moved some logic to their own files - replaced instances of `CustomStringConvertible & Sendable` with `Sendable` - minor documentation fixes - added `arrayOfLiterals` case to `LiteralReturnType` - progress towards supporting `HTMLResultRepresentation.literalOptimized`
1 parent e5627ea commit 0172302

File tree

17 files changed

+591
-501
lines changed

17 files changed

+591
-501
lines changed

Sources/HTMLAttributes/HTMLAttribute.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,14 +227,14 @@ extension HTMLKitUtilities {
227227
public let encoding:HTMLEncoding
228228
public let globalAttributes:[HTMLAttribute]
229229
public let attributes:[String:Sendable]
230-
public let innerHTML:[CustomStringConvertible & Sendable]
230+
public let innerHTML:[Sendable]
231231
public let trailingSlash:Bool
232232

233233
package init(
234234
_ encoding: HTMLEncoding,
235235
_ globalAttributes: [HTMLAttribute],
236236
_ attributes: [String:Sendable],
237-
_ innerHTML: [CustomStringConvertible & Sendable],
237+
_ innerHTML: [Sendable],
238238
_ trailingSlash: Bool
239239
) {
240240
self.encoding = encoding

Sources/HTMLElements/CustomElement.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public struct custom: HTMLElement {
99

1010
public let tag:String
1111
public var attributes:[HTMLAttribute]
12-
public var innerHTML:[CustomStringConvertible & Sendable]
12+
public var innerHTML:[Sendable]
1313

1414
public private(set) var encoding = HTMLEncoding.string
1515
public var isVoid:Bool
@@ -31,7 +31,7 @@ public struct custom: HTMLElement {
3131
tag: String,
3232
isVoid: Bool,
3333
attributes: [HTMLAttribute] = [],
34-
_ innerHTML: CustomStringConvertible & Sendable...
34+
_ innerHTML: Sendable...
3535
) {
3636
self.tag = tag
3737
self.isVoid = isVoid

Sources/HTMLElements/HTMLElement.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public protocol HTMLElement: CustomStringConvertible, Sendable {
2626
var attributes: [HTMLAttribute] { get }
2727

2828
/// The inner HTML content of this element.
29-
var innerHTML: [CustomStringConvertible & Sendable] { get }
29+
var innerHTML: [Sendable] { get }
3030

3131
init(_ encoding: HTMLEncoding, _ data: HTMLKitUtilities.ElementData)
3232
}

Sources/HTMLElements/svg/svg.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ struct svg: HTMLElement {
1010

1111
public let tag:String = "svg"
1212
public var attributes:[HTMLAttribute]
13-
public var innerHTML:[CustomStringConvertible & Sendable]
13+
public var innerHTML:[Sendable]
1414
public var height:String?
1515
public var preserveAspectRatio:Attributes.PreserveAspectRatio?
1616
public var viewBox:String?
@@ -34,7 +34,7 @@ struct svg: HTMLElement {
3434
}
3535
public init(
3636
attributes: [HTMLAttribute] = [],
37-
_ innerHTML: CustomStringConvertible & Sendable...
37+
_ innerHTML: Sendable...
3838
) {
3939
trailingSlash = attributes.contains(.trailingSlash)
4040
self.attributes = attributes

Sources/HTMLKit/HTMLKit.swift

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,78 +11,78 @@
1111
public macro escapeHTML(
1212
encoding: HTMLEncoding = .string,
1313
representation: HTMLResultRepresentation = .literalOptimized,
14-
_ innerHTML: CustomStringConvertible & Sendable...
14+
_ innerHTML: Sendable...
1515
) -> String = #externalMacro(module: "HTMLKitMacros", type: "EscapeHTML")
1616

1717
// MARK: HTML
18-
/// - Returns: The inferred concrete type that conforms to `CustomStringConvertible & Sendable`.
18+
/// - Returns: The inferred concrete type.
1919
@freestanding(expression)
2020
//@available(*, deprecated, message: "innerHTML is now initialized using brackets instead of parentheses")
2121
public macro html<T>(
2222
encoding: HTMLEncoding = .string,
2323
representation: HTMLResultRepresentation = .literalOptimized,
2424
lookupFiles: [StaticString] = [],
25-
_ innerHTML: CustomStringConvertible & Sendable...
25+
_ innerHTML: Sendable...
2626
) -> T = #externalMacro(module: "HTMLKitMacros", type: "HTMLElementMacro")
2727

2828
// MARK: HTML
29-
/// - Returns: The inferred concrete type that conforms to `CustomStringConvertible & Sendable`.
29+
/// - Returns: The inferred concrete type.
3030
@freestanding(expression)
3131
public macro html<T>(
3232
encoding: HTMLEncoding = .string,
3333
representation: HTMLResultRepresentation = .literalOptimized,
3434
lookupFiles: [StaticString] = [],
35-
_ innerHTML: () -> CustomStringConvertible & Sendable...
35+
_ innerHTML: () -> Sendable...
3636
) -> T = #externalMacro(module: "HTMLKitMacros", type: "HTMLElementMacro")
3737

38-
/// - Returns: An existential conforming to `CustomStringConvertible & Sendable`.
38+
/// - Returns: `any Sendable`.
3939
@freestanding(expression)
4040
public macro anyHTML(
4141
encoding: HTMLEncoding = .string,
4242
representation: HTMLResultRepresentation = .literalOptimized,
4343
lookupFiles: [StaticString] = [],
44-
_ innerHTML: CustomStringConvertible & Sendable...
45-
) -> any CustomStringConvertible & Sendable = #externalMacro(module: "HTMLKitMacros", type: "HTMLElementMacro")
44+
_ innerHTML: Sendable...
45+
) -> any Sendable = #externalMacro(module: "HTMLKitMacros", type: "HTMLElementMacro")
4646

4747
// MARK: Unchecked
4848
/// Same as `#html` but ignoring compiler warnings.
4949
///
50-
/// - Returns: The inferred concrete type that conforms to `CustomStringConvertible & Sendable`.
50+
/// - Returns: The inferred concrete type.
5151
@freestanding(expression)
52-
public macro uncheckedHTML<T: CustomStringConvertible & Sendable>(
52+
public macro uncheckedHTML<T>(
5353
encoding: HTMLEncoding = .string,
5454
representation: HTMLResultRepresentation = .literalOptimized,
5555
lookupFiles: [StaticString] = [],
56-
_ innerHTML: CustomStringConvertible & Sendable...
56+
_ innerHTML: Sendable...
5757
) -> T = #externalMacro(module: "HTMLKitMacros", type: "HTMLElementMacro")
5858

5959
// MARK: Raw
6060
/// Does not escape the `innerHTML`.
6161
///
62-
/// - Returns: The inferred concrete type that conforms to `CustomStringConvertible & Sendable`.
62+
/// - Returns: The inferred concrete type.
6363
@freestanding(expression)
64-
public macro rawHTML<T: CustomStringConvertible & Sendable>(
64+
public macro rawHTML<T>(
6565
encoding: HTMLEncoding = .string,
6666
representation: HTMLResultRepresentation = .literalOptimized,
6767
lookupFiles: [StaticString] = [],
6868
minify: Bool = false,
69-
_ innerHTML: CustomStringConvertible & Sendable...
69+
_ innerHTML: Sendable...
7070
) -> T = #externalMacro(module: "HTMLKitMacros", type: "RawHTML")
7171

7272
/// Does not escape the `innerHTML`.
7373
///
74-
/// - Returns: An existential conforming to `CustomStringConvertible & Sendable`.
74+
/// - Returns: `any Sendable`.
7575
@freestanding(expression)
7676
public macro anyRawHTML(
7777
encoding: HTMLEncoding = .string,
7878
representation: HTMLResultRepresentation = .literalOptimized,
7979
lookupFiles: [StaticString] = [],
8080
minify: Bool = false,
81-
_ innerHTML: CustomStringConvertible & Sendable...
82-
) -> any CustomStringConvertible & Sendable = #externalMacro(module: "HTMLKitMacros", type: "RawHTML")
81+
_ innerHTML: Sendable...
82+
) -> any Sendable = #externalMacro(module: "HTMLKitMacros", type: "RawHTML")
8383

8484
// MARK: HTML Context
8585
@freestanding(expression)
86-
macro htmlContext<T: CustomStringConvertible & Sendable>(
86+
macro htmlContext<T: Sendable>(
8787
_ value: () -> T
8888
) -> T = #externalMacro(module: "HTMLKitMacros", type: "HTMLContext")
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
2+
import HTMLKitUtilities
3+
import SwiftDiagnostics
4+
import SwiftSyntax
5+
6+
extension HTMLKitUtilities {
7+
public static func expandHTMLMacro(context: HTMLExpansionContext) throws -> ExprSyntax {
8+
var context = context
9+
return try expandHTMLMacro(context: &context)
10+
}
11+
public static func expandHTMLMacro(context: inout HTMLExpansionContext) throws -> ExprSyntax {
12+
let (string, encoding) = expandMacro(context: &context)
13+
let encodingResult = encodingResult(context: context, node: context.expansion, string: string, for: encoding)
14+
let expandedResult = representationResult(encoding: encoding, encodedResult: encodingResult, representation: context.representation)
15+
return "\(raw: expandedResult)"
16+
}
17+
18+
static func expandMacro(context: inout HTMLExpansionContext) -> (String, HTMLEncoding) {
19+
let data = HTMLKitUtilities.parseArguments(context: &context)
20+
var string = ""
21+
for v in data.innerHTML {
22+
string += String(describing: v)
23+
}
24+
string.replace(HTMLKitUtilities.lineFeedPlaceholder, with: "\\n")
25+
return (string, data.encoding)
26+
}
27+
}
28+
29+
// MARK: Encoding result
30+
extension HTMLKitUtilities {
31+
static func encodingResult(
32+
context: HTMLExpansionContext,
33+
node: MacroExpansionExprSyntax,
34+
string: String,
35+
for encoding: HTMLEncoding
36+
) -> String {
37+
switch encoding {
38+
case .utf8Bytes:
39+
guard hasNoInterpolation(context, node, string) else { return "" }
40+
return bytes([UInt8](string.utf8))
41+
case .utf16Bytes:
42+
guard hasNoInterpolation(context, node, string) else { return "" }
43+
return bytes([UInt16](string.utf16))
44+
case .utf8CString:
45+
guard hasNoInterpolation(context, node, string) else { return "" }
46+
return "\(string.utf8CString)"
47+
48+
case .foundationData:
49+
guard hasNoInterpolation(context, node, string) else { return "" }
50+
return "Data(\(bytes([UInt8](string.utf8))))"
51+
52+
case .byteBuffer:
53+
guard hasNoInterpolation(context, node, string) else { return "" }
54+
return "ByteBuffer(bytes: \(bytes([UInt8](string.utf8))))"
55+
56+
case .string:
57+
return "\"\(string)\""
58+
case .custom(let encoded, _):
59+
return encoded.replacingOccurrences(of: "$0", with: string)
60+
}
61+
}
62+
private static func bytes<T: FixedWidthInteger>(_ bytes: [T]) -> String {
63+
var string = "["
64+
for b in bytes {
65+
string += "\(b),"
66+
}
67+
string.removeLast()
68+
return string.isEmpty ? "[]" : string + "]"
69+
}
70+
private static func hasNoInterpolation(_ context: HTMLExpansionContext, _ node: MacroExpansionExprSyntax, _ string: String) -> Bool {
71+
guard string.firstRange(of: try! Regex("\\((.*)\\)")) == nil else {
72+
if !context.ignoresCompilerWarnings {
73+
context.context.diagnose(Diagnostic(node: node, message: DiagnosticMsg(id: "interpolationNotAllowedForDataType", message: "String Interpolation is not allowed for this data type. Runtime values get converted to raw text, which is not the intended result.")))
74+
}
75+
return false
76+
}
77+
return true
78+
}
79+
}
80+
81+
// MARK: Representation results
82+
extension HTMLKitUtilities {
83+
static func representationResult(
84+
encoding: HTMLEncoding,
85+
encodedResult: String,
86+
representation: HTMLResultRepresentation
87+
) -> String {
88+
switch representation {
89+
case .literal:
90+
break
91+
case .literalOptimized:
92+
if encoding == .string {
93+
// TODO: implement
94+
} else {
95+
// TODO: show compiler diagnostic
96+
}
97+
case .chunked(let optimized, let chunkSize):
98+
return "[" + chunks(encoding: encoding, encodedResult: encodedResult, async: false, optimized: optimized, chunkSize: chunkSize).joined(separator: ", ") + "]"
99+
#if compiler(>=6.2)
100+
case .chunkedInline(let optimized, let chunkSize):
101+
let typeAnnotation:String = "String" // TODO: fix
102+
let chunks = chunks(encoding: encoding, encodedResult: encodedResult, async: false, optimized: optimized, chunkSize: chunkSize).joined(separator: ", ")
103+
return "InlineArray<\(chunks.count), \(typeAnnotation)>([\(chunks)])"
104+
#endif
105+
case .streamed(let optimized, let chunkSize):
106+
return streamedRepresentation(encoding: encoding, encodedResult: encodedResult, async: false, optimized: optimized, chunkSize: chunkSize, suspendDuration: nil)
107+
case .streamedAsync(let optimized, let chunkSize, let suspendDuration):
108+
return streamedRepresentation(encoding: encoding, encodedResult: encodedResult, async: true, optimized: optimized, chunkSize: chunkSize, suspendDuration: suspendDuration)
109+
default:
110+
break
111+
}
112+
return encodedResult
113+
}
114+
115+
static func chunks(
116+
encoding: HTMLEncoding,
117+
encodedResult: String,
118+
async: Bool,
119+
optimized: Bool,
120+
chunkSize: Int,
121+
) -> [String] {
122+
var chunks = [String]()
123+
let delimiter:(Character) -> String? = encoding == .string ? { $0 != "\"" ? "\"" : nil } : { _ in nil }
124+
let count = encodedResult.count
125+
var i = 0
126+
while i < count {
127+
var endingIndex = i + chunkSize
128+
if i == 0 && encoding == .string {
129+
endingIndex += 1
130+
}
131+
let endIndex = encodedResult.index(encodedResult.startIndex, offsetBy: endingIndex, limitedBy: encodedResult.endIndex) ?? encodedResult.endIndex
132+
let slice = encodedResult[encodedResult.index(encodedResult.startIndex, offsetBy: i)..<endIndex]
133+
i += chunkSize + (i == 0 && encoding == .string ? 1 : 0)
134+
if slice.isEmpty || encoding == .string && slice.count == 1 && slice.first == "\"" {
135+
continue
136+
}
137+
var string = ""
138+
if let f = slice.first, let d = delimiter(f) {
139+
string += d
140+
}
141+
string += slice
142+
if let l = slice.last, let d = delimiter(l) {
143+
string += d
144+
}
145+
chunks.append(string)
146+
}
147+
return chunks
148+
}
149+
150+
static func streamedRepresentation(
151+
encoding: HTMLEncoding,
152+
encodedResult: String,
153+
async: Bool,
154+
optimized: Bool,
155+
chunkSize: Int,
156+
suspendDuration: Duration?
157+
) -> String {
158+
var string = "AsyncStream { continuation in\n"
159+
if async {
160+
string += "Task {\n"
161+
}
162+
let chunks = chunks(encoding: encoding, encodedResult: encodedResult, async: async, optimized: optimized, chunkSize: chunkSize)
163+
for chunk in chunks {
164+
string += "continuation.yield(" + chunk + ")\n"
165+
if let suspendDuration {
166+
string += "try await Task.sleep(for: \(suspendDuration))\n"
167+
}
168+
}
169+
string += "continuation.finish()\n}"
170+
if async {
171+
string += "\n}"
172+
}
173+
return string
174+
}
175+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
public struct HTMLExpansionResult {
3+
public let literals:[LiteralReturnType]
4+
}

0 commit comments

Comments
 (0)