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}