viewpoint_js_core/
lib.rs

1//! Core types for the `viewpoint-js` macro.
2//!
3//! This crate provides the [`ToJsValue`] trait used for interpolating Rust values
4//! into JavaScript code via the `js!` macro, as well as utilities for escaping
5//! strings for use in JavaScript.
6//!
7//! # Example
8//!
9//! ```rust
10//! use viewpoint_js_core::{ToJsValue, escape_js_string, escape_for_css_attr};
11//!
12//! // ToJsValue for interpolation
13//! assert_eq!(42.to_js_value(), "42");
14//! assert_eq!(true.to_js_value(), "true");
15//! assert_eq!("hello".to_js_value(), r#""hello""#);
16//!
17//! // Direct string escaping
18//! assert_eq!(escape_js_string("hello"), r#""hello""#);
19//!
20//! // CSS attribute selector escaping (for use inside JS strings)
21//! assert_eq!(escape_for_css_attr("my-id"), r#"\"my-id\""#);
22//! ```
23
24/// Escape a string for use in a JavaScript string literal (double-quoted).
25///
26/// Returns a double-quoted JavaScript string with proper escaping.
27///
28/// This handles:
29/// - Backslashes
30/// - Double quotes
31/// - Newlines, carriage returns, tabs
32/// - Unicode characters that need escaping
33///
34/// # Example
35///
36/// ```rust
37/// use viewpoint_js_core::escape_js_string;
38///
39/// assert_eq!(escape_js_string("hello"), r#""hello""#);
40/// assert_eq!(escape_js_string("it's fine"), r#""it's fine""#);
41/// assert_eq!(escape_js_string(r#"say "hi""#), r#""say \"hi\"""#);
42/// ```
43pub fn escape_js_string(s: &str) -> String {
44    let mut result = String::with_capacity(s.len() + 2);
45    result.push('"');
46    escape_js_string_contents_into(s, &mut result);
47    result.push('"');
48    result
49}
50
51/// Escape a string for use in a CSS attribute selector within JavaScript.
52///
53/// This is for building selectors like `document.querySelector('[data-id="value"]')`.
54/// The returned string includes escaped double quotes that work inside a JS string.
55///
56/// # Example
57///
58/// ```rust
59/// use viewpoint_js_core::escape_for_css_attr;
60///
61/// // For: document.querySelector('[data-testid="submit-button"]')
62/// let attr_value = escape_for_css_attr("submit-button");
63/// assert_eq!(attr_value, r#"\"submit-button\""#);
64///
65/// // Use in a format string:
66/// let selector = format!(r#"document.querySelector('[data-testid={}]')"#, attr_value);
67/// assert_eq!(selector, r#"document.querySelector('[data-testid=\"submit-button\"]')"#);
68/// ```
69pub fn escape_for_css_attr(s: &str) -> String {
70    let mut result = String::with_capacity(s.len() + 4);
71    // We need escaped double quotes that work inside a JS string
72    // The outer JS string uses single quotes, and CSS attr uses double quotes
73    result.push_str(r#"\""#);
74    // Escape the content for both JS string and CSS attribute
75    for c in s.chars() {
76        match c {
77            '\\' => result.push_str("\\\\"),
78            '"' => result.push_str("\\\""),
79            '\n' => result.push_str("\\n"),
80            '\r' => result.push_str("\\r"),
81            '\t' => result.push_str("\\t"),
82            c => result.push(c),
83        }
84    }
85    result.push_str(r#"\""#);
86    result
87}
88
89/// Escape a string for use in a single-quoted JavaScript string literal.
90///
91/// Returns a single-quoted JavaScript string with proper escaping.
92/// This is useful when you need to embed a string in JavaScript that uses
93/// single quotes (e.g., in template literals or when double quotes are used
94/// for HTML attributes).
95///
96/// # Example
97///
98/// ```rust
99/// use viewpoint_js_core::escape_js_string_single;
100///
101/// assert_eq!(escape_js_string_single("hello"), "'hello'");
102/// assert_eq!(escape_js_string_single("it's fine"), r"'it\'s fine'");
103/// assert_eq!(escape_js_string_single(r#"say "hi""#), r#"'say "hi"'"#);
104/// ```
105pub fn escape_js_string_single(s: &str) -> String {
106    let mut result = String::with_capacity(s.len() + 2);
107    result.push('\'');
108    for c in s.chars() {
109        match c {
110            '\\' => result.push_str("\\\\"),
111            '\'' => result.push_str("\\'"),
112            '\n' => result.push_str("\\n"),
113            '\r' => result.push_str("\\r"),
114            '\t' => result.push_str("\\t"),
115            '\x08' => result.push_str("\\b"),
116            '\x0C' => result.push_str("\\f"),
117            '\u{2028}' => result.push_str("\\u2028"),
118            '\u{2029}' => result.push_str("\\u2029"),
119            c if c.is_control() => {
120                use std::fmt::Write;
121                let _ = write!(result, "\\u{:04x}", c as u32);
122            }
123            c => result.push(c),
124        }
125    }
126    result.push('\'');
127    result
128}
129
130/// Escape string contents for JavaScript without adding surrounding quotes.
131///
132/// This escapes for double-quoted JS strings. Use `escape_js_contents_single`
133/// for single-quoted strings.
134///
135/// # Example
136///
137/// ```rust
138/// use viewpoint_js_core::escape_js_contents;
139///
140/// assert_eq!(escape_js_contents("hello"), "hello");
141/// assert_eq!(escape_js_contents("line1\nline2"), r"line1\nline2");
142/// assert_eq!(escape_js_contents(r#"say "hi""#), r#"say \"hi\""#);
143/// ```
144pub fn escape_js_contents(s: &str) -> String {
145    let mut result = String::with_capacity(s.len());
146    escape_js_string_contents_into(s, &mut result);
147    result
148}
149
150/// Escape string contents for a single-quoted JavaScript string.
151///
152/// This is useful when building JS strings that use single quotes,
153/// such as `querySelectorAll('...')`. Single quotes are escaped,
154/// double quotes are left as-is.
155///
156/// # Example
157///
158/// ```rust
159/// use viewpoint_js_core::escape_js_contents_single;
160///
161/// assert_eq!(escape_js_contents_single("hello"), "hello");
162/// assert_eq!(escape_js_contents_single("it's"), r"it\'s");
163/// assert_eq!(escape_js_contents_single(r#"say "hi""#), r#"say "hi""#);
164/// assert_eq!(escape_js_contents_single("input[type='text']"), r"input[type=\'text\']");
165/// ```
166pub fn escape_js_contents_single(s: &str) -> String {
167    let mut result = String::with_capacity(s.len());
168    for c in s.chars() {
169        match c {
170            '\\' => result.push_str("\\\\"),
171            '\'' => result.push_str("\\'"),
172            '\n' => result.push_str("\\n"),
173            '\r' => result.push_str("\\r"),
174            '\t' => result.push_str("\\t"),
175            '\x08' => result.push_str("\\b"),
176            '\x0C' => result.push_str("\\f"),
177            '\u{2028}' => result.push_str("\\u2028"),
178            '\u{2029}' => result.push_str("\\u2029"),
179            c if c.is_control() => {
180                use std::fmt::Write;
181                let _ = write!(result, "\\u{:04x}", c as u32);
182            }
183            c => result.push(c),
184        }
185    }
186    result
187}
188
189/// Internal helper to escape JS string contents into a buffer.
190fn escape_js_string_contents_into(s: &str, result: &mut String) {
191    for c in s.chars() {
192        match c {
193            '\\' => result.push_str("\\\\"),
194            '"' => result.push_str("\\\""),
195            '\n' => result.push_str("\\n"),
196            '\r' => result.push_str("\\r"),
197            '\t' => result.push_str("\\t"),
198            '\x08' => result.push_str("\\b"),
199            '\x0C' => result.push_str("\\f"),
200            // Line separator and paragraph separator need escaping
201            '\u{2028}' => result.push_str("\\u2028"),
202            '\u{2029}' => result.push_str("\\u2029"),
203            c if c.is_control() => {
204                // Escape control characters as \uXXXX
205                use std::fmt::Write;
206                let _ = write!(result, "\\u{:04x}", c as u32);
207            }
208            c => result.push(c),
209        }
210    }
211}
212
213/// A trait for converting Rust types to JavaScript value representations.
214///
215/// Types that implement this trait can be used in `js!` macro interpolation.
216///
217/// # Examples
218///
219/// ```rust
220/// use viewpoint_js_core::ToJsValue;
221///
222/// assert_eq!(42.to_js_value(), "42");
223/// assert_eq!(true.to_js_value(), "true");
224/// assert_eq!("hello".to_js_value(), r#""hello""#);
225/// ```
226pub trait ToJsValue {
227    /// Convert this value to a JavaScript representation.
228    ///
229    /// The returned string should be valid JavaScript that represents
230    /// this value. For example:
231    /// - Integers: `"42"`
232    /// - Strings: `"\"hello\""`
233    /// - Booleans: `"true"` or `"false"`
234    /// - null: `"null"`
235    fn to_js_value(&self) -> String;
236}
237
238// Implement for integers
239macro_rules! impl_to_js_value_int {
240    ($($t:ty),*) => {
241        $(
242            impl ToJsValue for $t {
243                fn to_js_value(&self) -> String {
244                    self.to_string()
245                }
246            }
247        )*
248    };
249}
250
251impl_to_js_value_int!(i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize);
252
253// Implement for floats
254impl ToJsValue for f32 {
255    fn to_js_value(&self) -> String {
256        if self.is_nan() {
257            "NaN".to_string()
258        } else if self.is_infinite() {
259            if self.is_sign_positive() {
260                "Infinity".to_string()
261            } else {
262                "-Infinity".to_string()
263            }
264        } else {
265            self.to_string()
266        }
267    }
268}
269
270impl ToJsValue for f64 {
271    fn to_js_value(&self) -> String {
272        if self.is_nan() {
273            "NaN".to_string()
274        } else if self.is_infinite() {
275            if self.is_sign_positive() {
276                "Infinity".to_string()
277            } else {
278                "-Infinity".to_string()
279            }
280        } else {
281            self.to_string()
282        }
283    }
284}
285
286// Implement for bool
287impl ToJsValue for bool {
288    fn to_js_value(&self) -> String {
289        if *self { "true" } else { "false" }.to_string()
290    }
291}
292
293// Implement for strings
294impl ToJsValue for str {
295    fn to_js_value(&self) -> String {
296        escape_js_string(self)
297    }
298}
299
300impl ToJsValue for String {
301    fn to_js_value(&self) -> String {
302        escape_js_string(self)
303    }
304}
305
306// Implement for Option<T>
307impl<T: ToJsValue> ToJsValue for Option<T> {
308    fn to_js_value(&self) -> String {
309        match self {
310            Some(v) => v.to_js_value(),
311            None => "null".to_string(),
312        }
313    }
314}
315
316// Implement for references
317impl<T: ToJsValue + ?Sized> ToJsValue for &T {
318    fn to_js_value(&self) -> String {
319        (*self).to_js_value()
320    }
321}
322
323impl<T: ToJsValue + ?Sized> ToJsValue for &mut T {
324    fn to_js_value(&self) -> String {
325        (**self).to_js_value()
326    }
327}
328
329impl<T: ToJsValue + ?Sized> ToJsValue for Box<T> {
330    fn to_js_value(&self) -> String {
331        (**self).to_js_value()
332    }
333}
334
335// Implement for serde_json::Value when the feature is enabled
336#[cfg(feature = "json")]
337impl ToJsValue for serde_json::Value {
338    fn to_js_value(&self) -> String {
339        match self {
340            serde_json::Value::Null => "null".to_string(),
341            serde_json::Value::Bool(b) => b.to_js_value(),
342            serde_json::Value::Number(n) => n.to_string(),
343            serde_json::Value::String(s) => escape_js_string(s),
344            serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
345                // For complex types, serialize to JSON (which is valid JS)
346                self.to_string()
347            }
348        }
349    }
350}
351
352