1use std::borrow::Cow;
2
3use serde::Serialize;
4use topcoat_core::context::Cx;
5use topcoat_view::{AttributeValueViewParts, PartsWriter};
6
7#[derive(Debug, Clone)]
12pub struct Js {
13 parts: Vec<JsPart>,
14}
15
16#[derive(Debug, Clone)]
17enum JsPart {
18 Source(Cow<'static, str>),
20 Raw(&'static str),
23 Surrogate(String),
25}
26
27impl Js {
28 #[must_use]
30 pub fn source(js: impl Into<Cow<'static, str>>) -> Self {
31 Self {
32 parts: vec![JsPart::Source(js.into())],
33 }
34 }
35
36 #[must_use]
38 pub fn builder() -> JsBuilder {
39 JsBuilder { parts: Vec::new() }
40 }
41
42 #[must_use]
47 pub fn to_source(&self) -> String {
48 let mut source = String::new();
49 for part in &self.parts {
50 match part {
51 JsPart::Source(js) => source.push_str(js),
52 JsPart::Raw(js) => source.push_str(js),
53 JsPart::Surrogate(json) => {
54 source.push_str("cx.hydrate(");
55 source.push_str(json);
56 source.push(')');
57 }
58 }
59 }
60 source
61 }
62
63 pub(crate) fn write(&self, parts: &mut PartsWriter<'_>) {
66 for part in &self.parts {
67 match part {
68 JsPart::Source(Cow::Borrowed(js)) => {
69 parts.push_static_str(js);
70 }
71 JsPart::Source(Cow::Owned(js)) => {
72 parts.push_str(js);
73 }
74 JsPart::Raw(js) => {
75 parts.push_static_str_unescaped(js);
76 }
77 JsPart::Surrogate(json) => {
78 parts
79 .push_promoted_str_unescaped(&"cx.hydrate(")
80 .push_str(json)
81 .push_promoted_str_unescaped(&")");
82 }
83 }
84 }
85 }
86}
87
88impl AttributeValueViewParts for Js {
91 #[inline]
92 fn attribute_present(&self) -> bool {
93 true
94 }
95
96 #[inline]
97 fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
98 self.write(parts);
99 }
100}
101
102#[derive(Debug)]
104pub struct JsBuilder {
105 parts: Vec<JsPart>,
106}
107
108impl JsBuilder {
109 #[must_use]
111 pub fn expression<T>(mut self, expression: &crate::Expr<T>) -> Self {
112 self.parts.push(JsPart::Raw("("));
113 self.parts.extend(expression.js.parts.iter().cloned());
114 self.parts.push(JsPart::Raw(")"));
115 self
116 }
117
118 #[must_use]
120 pub fn source(mut self, js: impl Into<Cow<'static, str>>) -> Self {
121 self.parts.push(JsPart::Source(js.into()));
122 self
123 }
124
125 #[must_use]
127 pub fn raw(mut self, js: &'static str) -> Self {
128 self.parts.push(JsPart::Raw(js));
129 self
130 }
131
132 #[must_use]
139 pub fn surrogate(mut self, value: &(impl Serialize + ?Sized)) -> Self {
140 let json = serde_json::to_string(value).expect("failed to serialize surrogate value");
141 self.parts.push(JsPart::Surrogate(json));
142 self
143 }
144
145 #[must_use]
147 pub fn build(self) -> Js {
148 Js { parts: self.parts }
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn executable_source_preserves_javascript_and_capture_escaping() {
158 let js = Js::builder()
159 .raw("(() => { const value = ")
160 .surrogate(&"\"<>&\n")
161 .source("; return value; })()")
162 .build();
163
164 assert_eq!(
165 js.to_source(),
166 r#"(() => { const value = cx.hydrate("\"<>&\n"); return value; })()"#,
167 );
168 }
169}