Skip to content

Commit d565e35

Browse files
fix: handle newlines in SSE events to prevent content truncation
When HTML content contains newline characters (\n), the SSE event formatting was not escaping them properly. This caused SSE events to be prematurely terminated, resulting in truncated content on the client side during live-reload updates. Root cause: format-datastar-fragment was outputting HTML directly in a single 'data: elements' line without handling newlines. Any \n\n in the HTML would prematurely end the SSE event according to the SSE spec. Fix: Split HTML by \n and emit multiple 'data: elements' lines, which Datastar concatenates. This follows Datastar's documented multi-line SSE format and prevents \n\n from breaking the event stream. Tests added: - Unit tests for textarea and pre elements with newline content - E2E test for content with newlines during route redefinition
1 parent fe7e9e8 commit d565e35

File tree

3 files changed

+44
-6
lines changed

3 files changed

+44
-6
lines changed

src/hyper/render.clj

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"Rendering pipeline.
33
44
Handles rendering hiccup to HTML and formatting Datastar SSE events."
5-
(:require [dev.onionpancakes.chassis.core :as c]
5+
(:require [clojure.string :as string]
6+
[dev.onionpancakes.chassis.core :as c]
67
[hyper.context :as context]
78
[hyper.routes :as routes]
89
[hyper.state :as state]
@@ -27,10 +28,16 @@
2728
event: datastar-patch-elements
2829
data: elements <html content>
2930
30-
(blank line to end event)"
31+
For multi-line HTML content, emits multiple 'data: elements' lines
32+
that Datastar will concatenate. This prevents \n in HTML from
33+
prematurely terminating the SSE event."
3134
[html]
32-
(str "event: datastar-patch-elements\n"
33-
"data: elements " html "\n\n"))
35+
(let [lines (string/split-lines html)]
36+
(str "event: datastar-patch-elements\n"
37+
(->> lines
38+
(map (fn [line] (str "data: elements " line "\n")))
39+
(apply str))
40+
"\n")))
3441

3542
(defn mark-head-elements
3643
"Add `{:data-hyper-head true}` to each top-level hiccup element in a

test/hyper/e2e_test.clj

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,8 +578,22 @@
578578

579579
(is (= "Live Reloaded!" (w/text-content "h1")))
580580
(is (= "This content was hot-swapped"
581-
(w/text-content "#reloaded-marker")))))
581+
(w/text-content "#reloaded-marker"))))
582582

583+
;; Test that content with newlines renders correctly
584+
(testing "Content with newlines preserved in route handler"
585+
(alter-var-root #'*test-routes*
586+
(constantly
587+
[["/" {:name :home
588+
:title "Newlines Test"
589+
:get (fn [_]
590+
[:div
591+
[:textarea#newline-content "line1\nline2"]
592+
[:pre#pre-content "code\nwith\n\nnew\n\nlines\n\n"]])}]]))
593+
(w/navigate (str base-url "/"))
594+
(wait-for-sse)
595+
(is (= "line1\nline2" (w/text-content "#newline-content")))
596+
(is (= "code\nwith\n\nnew\n\nlines\n\n" (w/text-content "#pre-content")))))
583597
(finally
584598
(close-browser! browser-info)))))
585599

test/hyper/render_test.clj

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,24 @@
3838
(let [html "<span>test</span>"
3939
fragment (render/format-datastar-fragment html)]
4040
(is (.contains fragment html))
41-
(is (.startsWith fragment "event: datastar-patch-elements\n")))))
41+
(is (.startsWith fragment "event: datastar-patch-elements\n"))))
42+
43+
(testing "HTML with 2 newlines emits multiple data lines"
44+
(let [html "<pre>code\nwith\nnewline</pre>"
45+
fragment (render/format-datastar-fragment html)]
46+
(is (= 3 (count (re-seq #"data: elements" fragment))))
47+
(is (.contains fragment "<pre>code"))
48+
(is (.contains fragment "with"))
49+
(is (.contains fragment "newline</pre>"))
50+
(is (.endsWith fragment "\n\n"))))
51+
52+
(testing "HTML with double newlines emits multiple data lines"
53+
(let [html "<textarea>line1\n\nline2</textarea>"
54+
fragment (render/format-datastar-fragment html)]
55+
(is (= 3 (count (re-seq #"data: elements" fragment))))
56+
(is (.contains fragment "<textarea>line1"))
57+
(is (.contains fragment "line2</textarea>"))
58+
(is (.endsWith fragment "\n\n")))))
4259

4360
(deftest test-render-tab
4461
(testing "render-tab returns nil when no render-fn is registered"

0 commit comments

Comments
 (0)