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}