ferrovue 0.2.0

Vue templates compiled to Rust render functions: server-side rendering with no JavaScript at run time
Documentation
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
<!-- client/components/Initials.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.

| JavaScript | Rust |
|---|---|
| `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

| JavaScript | Rust |
|---|---|
| `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.

```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.