Skip to content

Commit 391b2e9

Browse files
authored
Fix DatastarEvent.PatchElements rendering for multiline elements (#3833)
1 parent 4e06fe8 commit 391b2e9

File tree

2 files changed

+155
-1
lines changed

2 files changed

+155
-1
lines changed

zio-http-datastar-sdk/src/main/scala/zio/http/datastar/DatastarEvent.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ object DatastarEvent {
4545
sb.append("useViewTransition true\n")
4646
}
4747

48-
sb.append("elements ").append(elements.renderMinified).append('\n')
48+
val rendered = elements.renderMinified
49+
if (rendered.contains('\n'))
50+
rendered.split('\n').foreach(line => sb.append("elements ").append(line).append('\n'))
51+
else
52+
sb.append("elements ").append(rendered).append('\n')
4953

5054
val retry = if (retryDuration != DefaultRetryDelay) Some(retryDuration) else None
5155
ServerSentEvent(sb.toString(), Some(eventType.render), eventId, retry)

zio-http-datastar-sdk/src/test/scala/zio/http/datastar/DatastarEventSpec.scala

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,5 +412,155 @@ object DatastarEventSpec extends ZIOSpecDefault {
412412
)
413413
},
414414
),
415+
suite("PatchElements with multi-line content")(
416+
test("handles script tags with multi-line JavaScript") {
417+
val scriptContent = """console.log('line 1');
418+
|console.log('line 2');
419+
|console.log('line 3');""".stripMargin
420+
val event = DatastarEvent.patchElements(
421+
Dom.script(scriptContent),
422+
)
423+
424+
val sse = event.toServerSentEvent
425+
426+
assertTrue(
427+
sse.data == """elements <script>console.log('line 1');
428+
|elements console.log('line 2');
429+
|elements console.log('line 3');</script>
430+
|""".stripMargin,
431+
)
432+
},
433+
test("handles style tags with multi-line CSS") {
434+
val cssContent = """.button {
435+
| color: red;
436+
| background: blue;
437+
|}""".stripMargin
438+
val event = DatastarEvent.patchElements(
439+
Dom.style(cssContent),
440+
)
441+
442+
val sse = event.toServerSentEvent
443+
444+
assertTrue(
445+
sse.data == """elements <style>.button {
446+
|elements color: red;
447+
|elements background: blue;
448+
|elements }</style>
449+
|""".stripMargin,
450+
)
451+
},
452+
test("handles inline style attribute with newlines") {
453+
val styleContent = """color: red;
454+
|background: blue;
455+
|padding: 10px;""".stripMargin
456+
val event = DatastarEvent.patchElements(
457+
div(Dom.attr("style", styleContent))("Content"),
458+
)
459+
460+
val sse = event.toServerSentEvent
461+
462+
// The minified output should handle newlines in attribute values
463+
assertTrue(
464+
sse.data.startsWith("elements ") &&
465+
sse.data.contains("style=") &&
466+
sse.data.contains("Content"),
467+
)
468+
},
469+
test("handles complex HTML with embedded script and style") {
470+
val event = DatastarEvent.patchElements(
471+
div(
472+
Dom.style(""".test {
473+
| color: red;
474+
|}""".stripMargin),
475+
Dom.script("""console.log('test');
476+
|alert('hello');""".stripMargin),
477+
p("Content"),
478+
),
479+
)
480+
481+
val sse = event.toServerSentEvent
482+
483+
// Should split on newlines and prefix each line with "elements "
484+
val lines = sse.data.split('\n').filter(_.nonEmpty)
485+
assertTrue(
486+
lines.forall(_.startsWith("elements ")),
487+
lines.length > 1, // Multi-line content
488+
sse.data.contains(".test"),
489+
sse.data.contains("console.log"),
490+
sse.data.contains("Content"),
491+
)
492+
},
493+
test("handles single-line content without extra splitting") {
494+
val event = DatastarEvent.patchElements(
495+
div("Simple content"),
496+
)
497+
498+
val sse = event.toServerSentEvent
499+
500+
assertTrue(
501+
sse.data == "elements <div>Simple content</div>\n",
502+
)
503+
},
504+
test("handles script with selector and mode options") {
505+
val scriptContent = """function init() {
506+
| console.log('initialized');
507+
|}""".stripMargin
508+
val event = DatastarEvent.patchElements(
509+
Dom.script(scriptContent),
510+
Some(selector"#app"),
511+
ElementPatchMode.Append,
512+
)
513+
514+
val sse = event.toServerSentEvent
515+
516+
assertTrue(
517+
sse.data.contains("selector #app\n"),
518+
sse.data.contains("mode append\n"),
519+
sse.data.contains("elements <script>function init() {\n"),
520+
sse.data.contains("elements console.log('initialized');\n"),
521+
sse.data.contains("elements }</script>\n"),
522+
)
523+
},
524+
test("handles CSS with minification and newlines") {
525+
val cssContent = """.container {
526+
| display: flex;
527+
| justify-content: center;
528+
|}
529+
|.item {
530+
| margin: 5px;
531+
|}""".stripMargin
532+
val event = DatastarEvent.patchElements(
533+
Dom.style(cssContent),
534+
)
535+
536+
val sse = event.toServerSentEvent
537+
538+
// Each line should be prefixed with "elements "
539+
val lines = sse.data.split('\n').filter(_.nonEmpty)
540+
assertTrue(
541+
lines.forall(_.startsWith("elements ")),
542+
lines.length >= 5, // Multiple lines from CSS
543+
sse.data.contains("container"),
544+
sse.data.contains("display"),
545+
sse.data.contains("item"),
546+
)
547+
},
548+
test("ExecuteScript should also handle multi-line correctly") {
549+
val scriptContent = """const x = 1;
550+
|const y = 2;
551+
|console.log(x + y);""".stripMargin
552+
val event = DatastarEvent.executeScript(scriptContent)
553+
554+
val sse = event.toServerSentEvent
555+
556+
assertTrue(
557+
sse.data.contains("selector <body></body>\n"),
558+
sse.data.contains("mode append\n"),
559+
sse.data.contains("elements <script data-effect=\"el.remove\">const x = 1;\n"),
560+
sse.data.contains("elements const y = 2;\n"),
561+
sse.data.contains("elements console.log(x + y);</script>\n"),
562+
)
563+
},
564+
),
415565
)
416566
}

0 commit comments

Comments
 (0)