Skip to main content

topcoat_runtime/
js.rs

1use std::borrow::Cow;
2
3use serde::Serialize;
4use topcoat_core::context::Cx;
5use topcoat_view::{AttributeValueViewParts, PartsWriter};
6
7/// The JavaScript source of a runtime expression, captured at its `$(..)`
8/// site.
9///
10/// The `expr!` macro builds one of these next to the expression's Rust
11/// value: the source as it reaches the browser, with the values captured
12/// from the surrounding Rust scope serialized in place. Nothing is written
13/// until the expression is spliced into a view, where the source renders
14/// inside a marker comment.
15#[derive(Debug, Clone)]
16pub struct Js {
17    parts: Vec<JsPart>,
18}
19
20#[derive(Debug, Clone)]
21enum JsPart {
22    /// Source text, escaped for the position it renders in.
23    Source(Cow<'static, str>),
24    /// Trusted scaffolding written verbatim, like the `const [..] = [..]`
25    /// wrapper around captured values.
26    Raw(&'static str),
27    /// A captured value as JSON, hydrated on the client.
28    Surrogate(String),
29}
30
31impl Js {
32    /// Source without captured values.
33    #[must_use]
34    pub fn source(js: impl Into<Cow<'static, str>>) -> Self {
35        Self {
36            parts: vec![JsPart::Source(js.into())],
37        }
38    }
39
40    /// Starts source that interleaves captured values.
41    #[must_use]
42    pub fn builder() -> JsBuilder {
43        JsBuilder { parts: Vec::new() }
44    }
45
46    /// Writes the source through `parts`, sealed for the writer's current
47    /// context: a marker comment's body or a double-quoted attribute value.
48    pub(crate) fn write(&self, parts: &mut PartsWriter<'_>) {
49        for part in &self.parts {
50            match part {
51                JsPart::Source(Cow::Borrowed(js)) => {
52                    parts.push_static_str(js);
53                }
54                JsPart::Source(Cow::Owned(js)) => {
55                    parts.push_str(js);
56                }
57                JsPart::Raw(js) => {
58                    parts.push_static_str_unescaped(js);
59                }
60                JsPart::Surrogate(json) => {
61                    parts
62                        .push_promoted_str_unescaped(&"cx.hydrate(")
63                        .push_str(json)
64                        .push_promoted_str_unescaped(&")");
65                }
66            }
67        }
68    }
69}
70
71/// The source as the value of a `data-topcoat-*` attribute, like an event
72/// handler or a bind expression.
73impl AttributeValueViewParts for Js {
74    #[inline]
75    fn attribute_present(&self) -> bool {
76        true
77    }
78
79    #[inline]
80    fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
81        self.write(parts);
82    }
83}
84
85/// Builds a [`Js`] from source and captured values, in order.
86#[derive(Debug)]
87pub struct JsBuilder {
88    parts: Vec<JsPart>,
89}
90
91impl JsBuilder {
92    /// Appends source text.
93    #[must_use]
94    pub fn source(mut self, js: impl Into<Cow<'static, str>>) -> Self {
95        self.parts.push(JsPart::Source(js.into()));
96        self
97    }
98
99    /// Appends trusted scaffolding, written verbatim.
100    #[must_use]
101    pub fn raw(mut self, js: &'static str) -> Self {
102        self.parts.push(JsPart::Raw(js));
103        self
104    }
105
106    /// Appends a captured value, serialized now so the Rust expression can
107    /// consume it afterwards.
108    ///
109    /// # Panics
110    ///
111    /// Panics if the value fails to serialize.
112    #[must_use]
113    pub fn surrogate(mut self, value: &(impl Serialize + ?Sized)) -> Self {
114        let json = serde_json::to_string(value).expect("failed to serialize surrogate value");
115        self.parts.push(JsPart::Surrogate(json));
116        self
117    }
118
119    /// Finishes the builder into a [`Js`].
120    #[must_use]
121    pub fn build(self) -> Js {
122        Js { parts: self.parts }
123    }
124}