viewpoint_js_core/
lib.rs

1//! # Viewpoint JS Core - JavaScript Value Conversion
2//!
3//! Core types for the `viewpoint-js` macro, providing the [`ToJsValue`] trait
4//! for converting Rust values to JavaScript representations, and utilities
5//! for escaping strings for use in JavaScript.
6//!
7//! ## Features
8//!
9//! - **[`ToJsValue`] trait**: Convert Rust types to JavaScript value strings
10//! - **String escaping**: Properly escape strings for JavaScript contexts
11//! - **CSS attribute escaping**: Escape values for CSS attribute selectors
12//!
13//! ## Quick Start
14//!
15//! ```rust
16//! use viewpoint_js_core::{ToJsValue, escape_js_string};
17//!
18//! // Convert Rust values to JavaScript representation
19//! assert_eq!(42.to_js_value(), "42");
20//! assert_eq!(true.to_js_value(), "true");
21//! assert_eq!("hello".to_js_value(), r#""hello""#);
22//!
23//! // Escape a string for JavaScript
24//! assert_eq!(escape_js_string("line1\nline2"), r#""line1\nline2""#);
25//! ```
26//!
27//! ## The `ToJsValue` Trait
28//!
29//! The [`ToJsValue`] trait converts Rust values to their JavaScript representations.
30//! It's used by the `js!` macro for value interpolation (`#{expr}`):
31//!
32//! ```rust
33//! use viewpoint_js_core::ToJsValue;
34//!
35//! // Integers
36//! assert_eq!(42i32.to_js_value(), "42");
37//! assert_eq!((-17i64).to_js_value(), "-17");
38//!
39//! // Floats
40//! assert_eq!(3.14f64.to_js_value(), "3.14");
41//! assert_eq!(f64::INFINITY.to_js_value(), "Infinity");
42//! assert_eq!(f64::NAN.to_js_value(), "NaN");
43//!
44//! // Booleans
45//! assert_eq!(true.to_js_value(), "true");
46//! assert_eq!(false.to_js_value(), "false");
47//!
48//! // Strings (properly quoted and escaped)
49//! assert_eq!("hello".to_js_value(), r#""hello""#);
50//! assert_eq!("say \"hi\"".to_js_value(), r#""say \"hi\"""#);
51//! assert_eq!("line1\nline2".to_js_value(), r#""line1\nline2""#);
52//!
53//! // Option types
54//! assert_eq!(Some(42).to_js_value(), "42");
55//! assert_eq!(None::<i32>.to_js_value(), "null");
56//! ```
57//!
58//! ## String Escaping Functions
59//!
60//! ### `escape_js_string` - Double-Quoted Strings
61//!
62//! Escapes a string for use in a double-quoted JavaScript string literal:
63//!
64//! ```rust
65//! use viewpoint_js_core::escape_js_string;
66//!
67//! assert_eq!(escape_js_string("hello"), r#""hello""#);
68//! assert_eq!(escape_js_string("it's fine"), r#""it's fine""#);  // Single quotes preserved
69//! assert_eq!(escape_js_string(r#"say "hi""#), r#""say \"hi\"""#);  // Double quotes escaped
70//! assert_eq!(escape_js_string("tab\there"), r#""tab\there""#);
71//! ```
72//!
73//! ### `escape_js_string_single` - Single-Quoted Strings
74//!
75//! Escapes a string for use in a single-quoted JavaScript string literal:
76//!
77//! ```rust
78//! use viewpoint_js_core::escape_js_string_single;
79//!
80//! assert_eq!(escape_js_string_single("hello"), "'hello'");
81//! assert_eq!(escape_js_string_single("it's"), r"'it\'s'");  // Single quotes escaped
82//! assert_eq!(escape_js_string_single(r#"say "hi""#), r#"'say "hi"'"#);  // Double quotes preserved
83//! ```
84//!
85//! ### `escape_js_contents` - Without Quotes
86//!
87//! Escapes string contents without adding surrounding quotes:
88//!
89//! ```rust
90//! use viewpoint_js_core::escape_js_contents;
91//!
92//! assert_eq!(escape_js_contents("hello"), "hello");
93//! assert_eq!(escape_js_contents("line1\nline2"), r"line1\nline2");
94//! assert_eq!(escape_js_contents(r#"say "hi""#), r#"say \"hi\""#);
95//! ```
96//!
97//! ### `escape_for_css_attr` - CSS Attribute Selectors
98//!
99//! Escapes a string for use in CSS attribute selectors within JavaScript:
100//!
101//! ```rust
102//! use viewpoint_js_core::escape_for_css_attr;
103//!
104//! // For: document.querySelector('[data-testid="submit-button"]')
105//! let attr_value = escape_for_css_attr("submit-button");
106//! assert_eq!(attr_value, r#"\"submit-button\""#);
107//!
108//! // Use in a format string:
109//! let selector = format!(r#"document.querySelector('[data-testid={}]')"#, attr_value);
110//! assert_eq!(selector, r#"document.querySelector('[data-testid=\"submit-button\"]')"#);
111//! ```
112//!
113//! ## Implementing `ToJsValue` for Custom Types
114//!
115//! You can implement `ToJsValue` for your own types:
116//!
117//! ```rust
118//! use viewpoint_js_core::{ToJsValue, escape_js_string};
119//!
120//! struct User {
121//!     name: String,
122//!     age: u32,
123//! }
124//!
125//! impl ToJsValue for User {
126//!     fn to_js_value(&self) -> String {
127//!         format!(
128//!             r#"{{ name: {}, age: {} }}"#,
129//!             escape_js_string(&self.name),
130//!             self.age
131//!         )
132//!     }
133//! }
134//!
135//! let user = User { name: "John".to_string(), age: 30 };
136//! assert_eq!(user.to_js_value(), r#"{ name: "John", age: 30 }"#);
137//! ```
138//!
139//! ## JSON Support
140//!
141//! When the `json` feature is enabled, `serde_json::Value` implements `ToJsValue`:
142//!
143//! ```ignore
144//! use viewpoint_js_core::ToJsValue;
145//! use serde_json::json;
146//!
147//! let value = json!({ "key": "value", "number": 42 });
148//! let js = value.to_js_value();
149//! // Produces valid JavaScript object literal
150//! ```
151
152/// Escape a string for use in a JavaScript string literal (double-quoted).
153///
154/// Returns a double-quoted JavaScript string with proper escaping.
155///
156/// This handles:
157/// - Backslashes
158/// - Double quotes
159/// - Newlines, carriage returns, tabs
160/// - Unicode characters that need escaping
161///
162/// # Example
163///
164/// ```rust
165/// use viewpoint_js_core::escape_js_string;
166///
167/// assert_eq!(escape_js_string("hello"), r#""hello""#);
168/// assert_eq!(escape_js_string("it's fine"), r#""it's fine""#);
169/// assert_eq!(escape_js_string(r#"say "hi""#), r#""say \"hi\"""#);
170/// ```
171pub fn escape_js_string(s: &str) -> String {
172    let mut result = String::with_capacity(s.len() + 2);
173    result.push('"');
174    escape_js_string_contents_into(s, &mut result);
175    result.push('"');
176    result
177}
178
179/// Escape a string for use in a CSS attribute selector within JavaScript.
180///
181/// This is for building selectors like `document.querySelector('[data-id="value"]')`.
182/// The returned string includes escaped double quotes that work inside a JS string.
183///
184/// # Example
185///
186/// ```rust
187/// use viewpoint_js_core::escape_for_css_attr;
188///
189/// // For: document.querySelector('[data-testid="submit-button"]')
190/// let attr_value = escape_for_css_attr("submit-button");
191/// assert_eq!(attr_value, r#"\"submit-button\""#);
192///
193/// // Use in a format string:
194/// let selector = format!(r#"document.querySelector('[data-testid={}]')"#, attr_value);
195/// assert_eq!(selector, r#"document.querySelector('[data-testid=\"submit-button\"]')"#);
196/// ```
197pub fn escape_for_css_attr(s: &str) -> String {
198    let mut result = String::with_capacity(s.len() + 4);
199    // We need escaped double quotes that work inside a JS string
200    // The outer JS string uses single quotes, and CSS attr uses double quotes
201    result.push_str(r#"\""#);
202    // Escape the content for both JS string and CSS attribute
203    for c in s.chars() {
204        match c {
205            '\\' => result.push_str("\\\\"),
206            '"' => result.push_str("\\\""),
207            '\n' => result.push_str("\\n"),
208            '\r' => result.push_str("\\r"),
209            '\t' => result.push_str("\\t"),
210            c => result.push(c),
211        }
212    }
213    result.push_str(r#"\""#);
214    result
215}
216
217/// Escape a string for use in a single-quoted JavaScript string literal.
218///
219/// Returns a single-quoted JavaScript string with proper escaping.
220/// This is useful when you need to embed a string in JavaScript that uses
221/// single quotes (e.g., in template literals or when double quotes are used
222/// for HTML attributes).
223///
224/// # Example
225///
226/// ```rust
227/// use viewpoint_js_core::escape_js_string_single;
228///
229/// assert_eq!(escape_js_string_single("hello"), "'hello'");
230/// assert_eq!(escape_js_string_single("it's fine"), r"'it\'s fine'");
231/// assert_eq!(escape_js_string_single(r#"say "hi""#), r#"'say "hi"'"#);
232/// ```
233pub fn escape_js_string_single(s: &str) -> String {
234    let mut result = String::with_capacity(s.len() + 2);
235    result.push('\'');
236    for c in s.chars() {
237        match c {
238            '\\' => result.push_str("\\\\"),
239            '\'' => result.push_str("\\'"),
240            '\n' => result.push_str("\\n"),
241            '\r' => result.push_str("\\r"),
242            '\t' => result.push_str("\\t"),
243            '\x08' => result.push_str("\\b"),
244            '\x0C' => result.push_str("\\f"),
245            '\u{2028}' => result.push_str("\\u2028"),
246            '\u{2029}' => result.push_str("\\u2029"),
247            c if c.is_control() => {
248                use std::fmt::Write;
249                let _ = write!(result, "\\u{:04x}", c as u32);
250            }
251            c => result.push(c),
252        }
253    }
254    result.push('\'');
255    result
256}
257
258/// Escape string contents for JavaScript without adding surrounding quotes.
259///
260/// This escapes for double-quoted JS strings. Use `escape_js_contents_single`
261/// for single-quoted strings.
262///
263/// # Example
264///
265/// ```rust
266/// use viewpoint_js_core::escape_js_contents;
267///
268/// assert_eq!(escape_js_contents("hello"), "hello");
269/// assert_eq!(escape_js_contents("line1\nline2"), r"line1\nline2");
270/// assert_eq!(escape_js_contents(r#"say "hi""#), r#"say \"hi\""#);
271/// ```
272pub fn escape_js_contents(s: &str) -> String {
273    let mut result = String::with_capacity(s.len());
274    escape_js_string_contents_into(s, &mut result);
275    result
276}
277
278/// Escape string contents for a single-quoted JavaScript string.
279///
280/// This is useful when building JS strings that use single quotes,
281/// such as `querySelectorAll('...')`. Single quotes are escaped,
282/// double quotes are left as-is.
283///
284/// # Example
285///
286/// ```rust
287/// use viewpoint_js_core::escape_js_contents_single;
288///
289/// assert_eq!(escape_js_contents_single("hello"), "hello");
290/// assert_eq!(escape_js_contents_single("it's"), r"it\'s");
291/// assert_eq!(escape_js_contents_single(r#"say "hi""#), r#"say "hi""#);
292/// assert_eq!(escape_js_contents_single("input[type='text']"), r"input[type=\'text\']");
293/// ```
294pub fn escape_js_contents_single(s: &str) -> String {
295    let mut result = String::with_capacity(s.len());
296    for c in s.chars() {
297        match c {
298            '\\' => result.push_str("\\\\"),
299            '\'' => result.push_str("\\'"),
300            '\n' => result.push_str("\\n"),
301            '\r' => result.push_str("\\r"),
302            '\t' => result.push_str("\\t"),
303            '\x08' => result.push_str("\\b"),
304            '\x0C' => result.push_str("\\f"),
305            '\u{2028}' => result.push_str("\\u2028"),
306            '\u{2029}' => result.push_str("\\u2029"),
307            c if c.is_control() => {
308                use std::fmt::Write;
309                let _ = write!(result, "\\u{:04x}", c as u32);
310            }
311            c => result.push(c),
312        }
313    }
314    result
315}
316
317/// Internal helper to escape JS string contents into a buffer.
318fn escape_js_string_contents_into(s: &str, result: &mut String) {
319    for c in s.chars() {
320        match c {
321            '\\' => result.push_str("\\\\"),
322            '"' => result.push_str("\\\""),
323            '\n' => result.push_str("\\n"),
324            '\r' => result.push_str("\\r"),
325            '\t' => result.push_str("\\t"),
326            '\x08' => result.push_str("\\b"),
327            '\x0C' => result.push_str("\\f"),
328            // Line separator and paragraph separator need escaping
329            '\u{2028}' => result.push_str("\\u2028"),
330            '\u{2029}' => result.push_str("\\u2029"),
331            c if c.is_control() => {
332                // Escape control characters as \uXXXX
333                use std::fmt::Write;
334                let _ = write!(result, "\\u{:04x}", c as u32);
335            }
336            c => result.push(c),
337        }
338    }
339}
340
341/// A trait for converting Rust types to JavaScript value representations.
342///
343/// Types that implement this trait can be used in `js!` macro interpolation
344/// with the `#{expr}` syntax.
345///
346/// # Built-in Implementations
347///
348/// - **Integers** (`i8`, `i16`, `i32`, `i64`, `i128`, `isize`, `u8`, `u16`, `u32`, `u64`, `u128`, `usize`): Converted to number strings
349/// - **Floats** (`f32`, `f64`): Converted to number strings, with special handling for `Infinity`, `-Infinity`, and `NaN`
350/// - **Booleans**: Converted to `"true"` or `"false"`
351/// - **Strings** (`str`, `String`): Properly quoted and escaped
352/// - **`Option<T>`**: `Some(v)` delegates to `v.to_js_value()`, `None` becomes `"null"`
353/// - **References**: Delegates to the inner type
354///
355/// # Examples
356///
357/// ```rust
358/// use viewpoint_js_core::ToJsValue;
359///
360/// assert_eq!(42.to_js_value(), "42");
361/// assert_eq!(true.to_js_value(), "true");
362/// assert_eq!("hello".to_js_value(), r#""hello""#);
363/// assert_eq!(Some(42).to_js_value(), "42");
364/// assert_eq!(None::<i32>.to_js_value(), "null");
365/// ```
366pub trait ToJsValue {
367    /// Convert this value to a JavaScript representation.
368    ///
369    /// The returned string should be valid JavaScript that represents
370    /// this value. For example:
371    /// - Integers: `"42"`
372    /// - Strings: `"\"hello\""`
373    /// - Booleans: `"true"` or `"false"`
374    /// - null: `"null"`
375    fn to_js_value(&self) -> String;
376}
377
378// Implement for integers
379macro_rules! impl_to_js_value_int {
380    ($($t:ty),*) => {
381        $(
382            impl ToJsValue for $t {
383                fn to_js_value(&self) -> String {
384                    self.to_string()
385                }
386            }
387        )*
388    };
389}
390
391impl_to_js_value_int!(
392    i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
393);
394
395// Implement for floats
396impl ToJsValue for f32 {
397    fn to_js_value(&self) -> String {
398        if self.is_nan() {
399            "NaN".to_string()
400        } else if self.is_infinite() {
401            if self.is_sign_positive() {
402                "Infinity".to_string()
403            } else {
404                "-Infinity".to_string()
405            }
406        } else {
407            self.to_string()
408        }
409    }
410}
411
412impl ToJsValue for f64 {
413    fn to_js_value(&self) -> String {
414        if self.is_nan() {
415            "NaN".to_string()
416        } else if self.is_infinite() {
417            if self.is_sign_positive() {
418                "Infinity".to_string()
419            } else {
420                "-Infinity".to_string()
421            }
422        } else {
423            self.to_string()
424        }
425    }
426}
427
428// Implement for bool
429impl ToJsValue for bool {
430    fn to_js_value(&self) -> String {
431        if *self { "true" } else { "false" }.to_string()
432    }
433}
434
435// Implement for strings
436impl ToJsValue for str {
437    fn to_js_value(&self) -> String {
438        escape_js_string(self)
439    }
440}
441
442impl ToJsValue for String {
443    fn to_js_value(&self) -> String {
444        escape_js_string(self)
445    }
446}
447
448// Implement for Option<T>
449impl<T: ToJsValue> ToJsValue for Option<T> {
450    fn to_js_value(&self) -> String {
451        match self {
452            Some(v) => v.to_js_value(),
453            None => "null".to_string(),
454        }
455    }
456}
457
458// Implement for references
459impl<T: ToJsValue + ?Sized> ToJsValue for &T {
460    fn to_js_value(&self) -> String {
461        (*self).to_js_value()
462    }
463}
464
465impl<T: ToJsValue + ?Sized> ToJsValue for &mut T {
466    fn to_js_value(&self) -> String {
467        (**self).to_js_value()
468    }
469}
470
471impl<T: ToJsValue + ?Sized> ToJsValue for Box<T> {
472    fn to_js_value(&self) -> String {
473        (**self).to_js_value()
474    }
475}
476
477// Implement for serde_json::Value when the feature is enabled
478#[cfg(feature = "json")]
479impl ToJsValue for serde_json::Value {
480    fn to_js_value(&self) -> String {
481        match self {
482            serde_json::Value::Null => "null".to_string(),
483            serde_json::Value::Bool(b) => b.to_js_value(),
484            serde_json::Value::Number(n) => n.to_string(),
485            serde_json::Value::String(s) => escape_js_string(s),
486            serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
487                // For complex types, serialize to JSON (which is valid JS)
488                self.to_string()
489            }
490        }
491    }
492}