JavaScript's strings, in Rust.
A JavaScript string is a sequence of UTF-16 code units; a Rust `str` is UTF-8. Vue renders with
JavaScript's string methods, so wherever the two would differ the generated code calls a Rust twin
that counts and compares as JavaScript does. Each twin is held to vectors recorded from JavaScript.
# Counting in UTF-16 code units
`.length`, and every index and length a string method takes or returns, counts UTF-16 code units: a
character beyond U+FFFF, such as š¦, is two. So `"š¦".length` is 2 and `"š¦ crab".slice(3)` is
`"crab"`, where Rust's byte indices would make the first 4 and the second a panic.
```vue
<script setup lang="ts">
defineProps<{ name: string; id: number }>();
</script>
<template>
<p :title="name.toUpperCase()">{{ name.slice(0, 1) }}. #{{ String(id).padStart(4, "0") }} {{ name < "N" ? "AāM" : "NāZ" }}</p>
</template>
```
```rust
# mod initials {
# // @generated by ferrovue from client/components/Initials.vue. Do not edit: change the `.vue` file and run
# // `ferrovue`.
#
# use std::borrow::Cow;
#
# use ferrovue as fv;
#
# /// The component's name, as `data-island` carries it.
# pub const NAME: &str = "Initials";
#
# /// The props `Initials.vue` declares.
# #[derive(Debug, Clone, serde::Serialize)]
# #[cfg_attr(test, derive(serde::Deserialize))]
# pub struct Props<'a> {
# #[serde(rename = "name")]
# pub name: Cow<'a, str>,
# #[serde(rename = "id")]
# pub id: i64,
# }
#
# impl<'a> Props<'a> {
# /// Props with its required fields.
# pub fn new(name: impl Into<Cow<'a, str>>, id: i64) -> Self {
# Props { name: name.into(), id }
# }
#
# }
#
#
/// Write the component's server render into `out`.
pub fn render(out: &mut String, props: &Props<'_>) {
out.reserve(26 + props.name.len());
out.push_str("<p");
if props.name.to_uppercase().is_empty() {
out.push_str(" title");
} else {
out.push_str(" title=\"");
fv::escape_into(out, &props.name.to_uppercase());
out.push('"');
}
out.push('>');
fv::escape_into(out, &fv::js_slice(&props.name, 0.0f64, Some(1.0f64)));
out.push_str(". #");
fv::escape_into(out, &fv::js_pad_start(&fv::Js(props.id).to_string(), 4.0f64, "0"));
out.push(' ');
fv::escape_into(out, if fv::js_cmp(&props.name, "N").is_lt() { "AāM" } else { "NāZ" });
out.push_str("</p>");
}
#
# /// The component's markup, for a maud page that shows it without hydrating it.
# pub fn html<'p, 'a>(props: &'p Props<'a>) -> fv::Html<'p, Props<'a>> {
# fv::Html::markup(props, render)
# }
#
# /// The component as an island the client hydrates.
# pub fn island<'p, 'a>(props: &'p Props<'a>) -> fv::Html<'p, Props<'a>> {
# fv::Html::island(NAME, props, render)
# }
# }
let mut out = String::new();
initials::render(&mut out, &initials::Props::new("Ada", 7));
assert_eq!(out, r#"<p title="ADA">A. #0007 AāM</p>"#);
```
An index is an `f64`, because it is a JavaScript number: a fraction is truncated, `NaN` is 0, and
where JavaScript counts a negative index from the end, so does the twin.
| `s.length` | [`js_length`](crate::js_length) |
| `s.trim()`, `s.trimStart()`, `s.trimEnd()` | [`js_trim`](crate::js_trim), [`js_trim_start`](crate::js_trim_start), [`js_trim_end`](crate::js_trim_end): JavaScript's whitespace, which is not Rust's |
| `s.slice(start, end)` | [`js_slice`](crate::js_slice) |
| `s.substring(start, end)` | [`js_substring`](crate::js_substring) |
| `s.at(i)`, `s.charAt(i)` | [`js_at`](crate::js_at) (`None` for `undefined`), [`js_char_at`](crate::js_char_at) |
| `s.indexOf(x)`, `s.lastIndexOf(x)` | [`js_index_of`](crate::js_index_of), [`js_last_index_of`](crate::js_last_index_of) |
| `s.split(x)` | [`js_split`](crate::js_split) |
| `s.replace(x, y)`, `s.replaceAll(x, y)` | [`js_replace`](crate::js_replace), [`js_replace_all`](crate::js_replace_all), with the `$&`, `` $` ``, `$'` and `$$` patterns JavaScript reads in `y` |
| `s.padStart(n, x)`, `s.padEnd(n, x)` | [`js_pad_start`](crate::js_pad_start), [`js_pad_end`](crate::js_pad_end) |
| `s.repeat(n)` | [`js_repeat`](crate::js_repeat) |
| `s.includes(x)`, `s.startsWith(x)`, `s.endsWith(x)` | `str::contains`, `starts_with`, `ends_with`: they agree |
| `s.toUpperCase()`, `s.toLowerCase()` | `str::to_uppercase`, `to_lowercase`: they agree, `Ć` becoming `SS` included |
```rust
use ferrovue::{js_index_of, js_length, js_pad_start, js_slice, js_split};
assert_eq!(js_length("š¦ crab"), 7);
assert_eq!(js_slice("š¦ crab", 3.0, None), "crab");
assert_eq!(js_index_of("š¦ crab", "crab"), 3);
assert_eq!(js_split("a,b,", ","), ["a", "b", ""]);
assert_eq!(js_pad_start("7", 3.0, "0"), "007");
```
The patterns are strings: a regular expression (`.replace(/x/g, ā¦)`, `.split(/,/)`), a replacement
function, a search's starting position or a split's limit (`.includes(x, 3)`, `.split(",", 2)`) are
refused, as are `.toLocaleUpperCase()` and `.toLocaleLowerCase()`, since the server's locale is not
the browser's.
# Halves of surrogate pairs
A JavaScript string can hold half of a surrogate pair: `"š¦".slice(0, 1)`, `"š¦".charAt(1)`,
`"š¦".split("")`. A Rust string cannot. The twins write U+FFFD in its place, which is exactly what
the page carries anyway: a server sends Vue's string as UTF-8, and UTF-8 writes each half as U+FFFD
(`res.end`, `Buffer.from` and `TextEncoder` all do). The length in code units is the same.
```rust
assert_eq!(ferrovue::js_slice("š¦x", 1.0, None), "\u{FFFD}x");
assert_eq!(ferrovue::js_char_at("š¦", 0.0), "\u{FFFD}");
assert_eq!(ferrovue::js_split("š¦", ""), ["\u{FFFD}", "\u{FFFD}"]);
```
Where the half itself would decide the result, the compiler refuses: two strings that may each hold
a half compared or ordered, side by side (`a.slice(0, 1) + b.slice(1)`, a `.join("")`, a class
object's names), one searched for in another (`.includes(a.charAt(0))`), repeated or padded where
halves would join, ordered against anything but a literal below U+D800, or written by
`JSON.stringify` (which escapes it) or into a `<RouterLink>` (whose encoding throws). Three corners
remain, by design:
- a string holding U+FFFD in the props compares equal to a half, or finds one, where JavaScript
would tell them apart;
- a half handed to a child component or a helper is U+FFFD there, which matters only if that
component then compares or joins it as above;
- Vue itself cannot hydrate such text cleanly: the browser reads U+FFFD where its own render holds
the half, and reports a mismatch.
# Ordering
`<`, `>`, `<=` and `>=` between two strings order them by UTF-16 code unit, as JavaScript does, with
[`js_cmp`](crate::js_cmp). That is not the order of the characters: every character from U+E000 to
U+FFFF comes after one beyond U+FFFF, whose first code unit is a surrogate.
```rust
use std::cmp::Ordering;
assert_eq!(ferrovue::js_cmp("B", "a"), Ordering::Less); // by code unit, not alphabetically
assert_eq!(ferrovue::js_cmp("ļ½", "š¦"), Ordering::Greater); // U+FF5E, after 0xD83E
assert_eq!("ļ½".cmp("š¦"), Ordering::Less); // Rust's own order
```
Ordering a string against a number is refused: JavaScript converts the string to a number first.
# Numbers from strings, and strings from values
| `Number(s)` | [`js_number`](crate::js_number): JavaScript's whitespace trimmed, `""` is 0, `0x`, `0o` and `0b` read, anything else that is not a number `NaN` |
| `parseInt(s)`, `parseInt(s, 10)`, `parseInt(s, 16)` | [`js_parse_int`](crate::js_parse_int): the leading digits, or `NaN` |
| `parseFloat(s)` | [`js_parse_float`](crate::js_parse_float): the longest decimal literal at the start, or `NaN` |
| `JSON.stringify(s)` of a string | [`js_json_string`](crate::js_json_string) |
| `JSON.stringify(x)` of a number | [`js_json_number`](crate::js_json_number): `null` for `NaN` and the infinities |
| `String(x)`, `` `${x}` ``, `x.toString()` | [`push_number`](crate::push_number), [`Js`](crate::Js). See [`numbers`](crate::guide::numbers) |
Of a string, all three give a `Float`, since the result may be a fraction or `NaN`. `JSON.stringify`
also takes a boolean and a list of strings, numbers or booleans, which the generated code writes
between `[` and `]` with the twins above. `parseInt` with another radix, or of a number, is refused.
```rust
assert_eq!(ferrovue::js_number(" 0x1F "), 31.0);
assert_eq!(ferrovue::js_parse_int(" 42px", 10), 42.0);
assert_eq!(ferrovue::js_parse_float(" 1.5e3px"), 1500.0);
assert_eq!(ferrovue::js_json_string("say \"hi\""), r#""say \"hi\"""#);
assert_eq!(ferrovue::js_json_number(f64::NAN), "null");
```
# Slicing lists
A list's `.slice(start, end)` takes the same range as a string's, counted in items:
[`js_slice_items`](crate::js_slice_items) takes the items [`js_slice_range`](crate::js_slice_range)
gives. The other array methods with an arrow function, `.filter()`, `.map()`, `.some()`, `.every()`,
`.find()` and `.findIndex()`, become Rust iterator adaptors, as they have no JavaScript corners to
reproduce. Given `Boolean` in place of an arrow function, each takes the item's truthiness, as
`x => !!x` does: `.filter(Boolean)` drops empty strings, `0`, `NaN` and `false`, and, over a list or
an array literal whose items may be `null` or `undefined`, the absent ones too, leaving a list of the
values.
```rust
let words = ["a", "b", "c", "d"];
let last_two: Vec<&str> = ferrovue::js_slice_items(words.iter().copied(), -2.0, None).collect();
assert_eq!(last_two, ["c", "d"]);
```
# Panics
Where JavaScript throws, the generated code panics, as Vue's render fails: a negative or infinite
`.repeat()` count, and a `.repeat()`, `.padStart()` or `.padEnd()` longer than the longest string V8
makes, 2²⹠ā 24 code units. A `.repeat()` by a negative literal is refused at compile time.