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!(
252    i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
253);
254
255// Implement for floats
256impl ToJsValue for f32 {
257    fn to_js_value(&self) -> String {
258        if self.is_nan() {
259            "NaN".to_string()
260        } else if self.is_infinite() {
261            if self.is_sign_positive() {
262                "Infinity".to_string()
263            } else {
264                "-Infinity".to_string()
265            }
266        } else {
267            self.to_string()
268        }
269    }
270}
271
272impl ToJsValue for f64 {
273    fn to_js_value(&self) -> String {
274        if self.is_nan() {
275            "NaN".to_string()
276        } else if self.is_infinite() {
277            if self.is_sign_positive() {
278                "Infinity".to_string()
279            } else {
280                "-Infinity".to_string()
281            }
282        } else {
283            self.to_string()
284        }
285    }
286}
287
288// Implement for bool
289impl ToJsValue for bool {
290    fn to_js_value(&self) -> String {
291        if *self { "true" } else { "false" }.to_string()
292    }
293}
294
295// Implement for strings
296impl ToJsValue for str {
297    fn to_js_value(&self) -> String {
298        escape_js_string(self)
299    }
300}
301
302impl ToJsValue for String {
303    fn to_js_value(&self) -> String {
304        escape_js_string(self)
305    }
306}
307
308// Implement for Option<T>
309impl<T: ToJsValue> ToJsValue for Option<T> {
310    fn to_js_value(&self) -> String {
311        match self {
312            Some(v) => v.to_js_value(),
313            None => "null".to_string(),
314        }
315    }
316}
317
318// Implement for references
319impl<T: ToJsValue + ?Sized> ToJsValue for &T {
320    fn to_js_value(&self) -> String {
321        (*self).to_js_value()
322    }
323}
324
325impl<T: ToJsValue + ?Sized> ToJsValue for &mut T {
326    fn to_js_value(&self) -> String {
327        (**self).to_js_value()
328    }
329}
330
331impl<T: ToJsValue + ?Sized> ToJsValue for Box<T> {
332    fn to_js_value(&self) -> String {
333        (**self).to_js_value()
334    }
335}
336
337// Implement for serde_json::Value when the feature is enabled
338#[cfg(feature = "json")]
339impl ToJsValue for serde_json::Value {
340    fn to_js_value(&self) -> String {
341        match self {
342            serde_json::Value::Null => "null".to_string(),
343            serde_json::Value::Bool(b) => b.to_js_value(),
344            serde_json::Value::Number(n) => n.to_string(),
345            serde_json::Value::String(s) => escape_js_string(s),
346            serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
347                // For complex types, serialize to JSON (which is valid JS)
348                self.to_string()
349            }
350        }
351    }
352}