Skip to content

Commit 9705a00

Browse files
streaming HTML should now work with interpolation/dynamic content
1 parent effede9 commit 9705a00

File tree

2 files changed

+127
-12
lines changed

2 files changed

+127
-12
lines changed

Sources/HTMLKitParse/ExpandHTMLMacro.swift

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,7 @@ extension HTMLKitUtilities {
184184
let left = encodedResult[index..<interp.range.lowerBound]
185185
values.append("StaticString(\(left))")
186186

187-
var interpolationValue = encodedResult[interp.range]
188-
interpolationValue.removeFirst(22)
189-
interpolationValue.removeLast(3)
190-
interpolationValue.removeAll(where: { $0.isNewline })
191-
values.append(String("\"\\(" + interpolationValue + "\""))
187+
values.append(normalizeInterpolation(encodedResult[interp.range]))
192188
index = interp.range.upperBound
193189

194190
reserveCapacity += left.count + 32
@@ -200,6 +196,13 @@ extension HTMLKitUtilities {
200196
}
201197
return "HTMLOptimizedLiteral(reserveCapacity: \(reserveCapacity)).render((\n\(values.joined(separator: ",\n"))\n))"
202198
}
199+
static func normalizeInterpolation(_ value: Substring) -> String {
200+
var value = value
201+
value.removeFirst(22) // ` + String(describing: `.count
202+
value.removeLast(3) // ` + `.count
203+
value.removeAll(where: { $0.isNewline })
204+
return String("\"\\(" + value + "\"")
205+
}
203206
}
204207

205208
// MARK: Chunks
@@ -211,28 +214,57 @@ extension HTMLKitUtilities {
211214
optimized: Bool,
212215
chunkSize: Int,
213216
) -> [String] {
217+
var interpolationMatches = encodedResult.matches(of: interpolationRegex)
214218
var chunks = [String]()
215219
let delimiter:(Character) -> String? = encoding == .string ? { $0 != "\"" ? "\"" : nil } : { _ in nil }
216220
let count = encodedResult.count
217221
var i = 0
218222
while i < count {
219223
var endingIndex = i + chunkSize
224+
var offset = 0
220225
if i == 0 && encoding == .string {
221226
endingIndex += 1
227+
offset = 1
222228
}
223-
let endIndex = encodedResult.index(encodedResult.startIndex, offsetBy: endingIndex, limitedBy: encodedResult.endIndex) ?? encodedResult.endIndex
224-
let slice = encodedResult[encodedResult.index(encodedResult.startIndex, offsetBy: i)..<endIndex]
225-
i += chunkSize + (i == 0 && encoding == .string ? 1 : 0)
226-
if slice.isEmpty || encoding == .string && slice.count == 1 && slice.first == "\"" {
229+
var endIndex = encodedResult.index(encodedResult.startIndex, offsetBy: endingIndex, limitedBy: encodedResult.endIndex) ?? encodedResult.endIndex
230+
let range = encodedResult.index(encodedResult.startIndex, offsetBy: i)..<endIndex
231+
var slice = encodedResult[range]
232+
var interpolation:String?
233+
if let interp = interpolationMatches.first, range.contains(interp.range.lowerBound) { // chunk contains interpolation
234+
var normalized = normalizeInterpolation(encodedResult[interp.range])
235+
normalized.removeFirst()
236+
interpolation = normalized
237+
if !range.contains(interp.range.upperBound) {
238+
endIndex = encodedResult.index(before: interp.range.lowerBound)
239+
slice = slice[slice.startIndex..<endIndex]
240+
i += encodedResult.distance(from: range.upperBound, to: interp.range.upperBound)
241+
} else {
242+
interpolation = nil
243+
normalized.removeLast()
244+
slice.remove(at: interp.range.upperBound) // "
245+
slice.replaceSubrange(interp.range, with: normalized)
246+
slice.remove(at: slice.index(before: interp.range.lowerBound)) // "
247+
}
248+
interpolationMatches.removeFirst()
249+
} else {
250+
interpolation = nil
251+
}
252+
i += chunkSize + offset
253+
if slice.isEmpty || encoding == .string && slice.count == 1 && slice[slice.startIndex] == "\"" {
227254
continue
228255
}
229256
var string = ""
230257
if let f = slice.first, let d = delimiter(f) {
231258
string += d
232259
}
233-
string += slice
234-
if let l = slice.last, let d = delimiter(l) {
235-
string += d
260+
if let interpolation {
261+
string += slice
262+
string += interpolation
263+
} else {
264+
string += slice
265+
if let l = slice.last, let d = delimiter(l) {
266+
string += d
267+
}
236268
}
237269
chunks.append(string)
238270
}

Tests/HTMLKitTests/StreamTests.swift

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,87 @@ struct StreamTests {
8383
}
8484
}
8585

86+
// MARK: Interpolation
87+
extension StreamTests {
88+
@Test
89+
func streamInterpolation() async {
90+
let rawHTMLInterpolationTest = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567689"
91+
let expected:String = #html(
92+
html {
93+
body {
94+
div()
95+
div()
96+
div()
97+
div()
98+
div()
99+
rawHTMLInterpolationTest
100+
div()
101+
div()
102+
div()
103+
div()
104+
div()
105+
}
106+
}
107+
)
108+
109+
var test:AsyncStream<String> = #html(
110+
representation: .streamedAsync(chunkSize: 50, { _ in
111+
try await Task.sleep(for: .milliseconds(5))
112+
})) {
113+
html {
114+
body {
115+
div()
116+
div()
117+
div()
118+
div()
119+
div()
120+
rawHTMLInterpolationTest
121+
div()
122+
div()
123+
div()
124+
div()
125+
div()
126+
}
127+
}
128+
}
129+
var receivedHTML = ""
130+
var now = ContinuousClock.now
131+
for await test in test {
132+
receivedHTML += test
133+
}
134+
var took = ContinuousClock.now - now
135+
#expect(took < .milliseconds(25))
136+
#expect(receivedHTML == expected)
137+
138+
test = #html(
139+
representation: .streamedAsync(chunkSize: 200, { _ in
140+
try await Task.sleep(for: .milliseconds(5))
141+
})) {
142+
html {
143+
body {
144+
div()
145+
div()
146+
div()
147+
div()
148+
div()
149+
rawHTMLInterpolationTest
150+
div()
151+
div()
152+
div()
153+
div()
154+
div()
155+
}
156+
}
157+
}
158+
receivedHTML = ""
159+
now = ContinuousClock.now
160+
for await test in test {
161+
receivedHTML += test
162+
}
163+
took = ContinuousClock.now - now
164+
#expect(took < .milliseconds(25))
165+
#expect(receivedHTML == expected)
166+
}
167+
}
168+
86169
#endif

0 commit comments

Comments
 (0)