ferrovue 0.9.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.
The twins are `#[doc(hidden)]`: this page describes what they do, and their signatures follow the
version of generated code, outside the crate's stable API.

# 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. A position or a limit
that is `undefined`, or an optional number that is absent, is the default JavaScript takes for it
(the start, or the end for `.endsWith()` and `.lastIndexOf()`).

| JavaScript | Rust |
|---|---|
| `s.length` | `js_length` |
| `s.trim()`, `s.trimStart()`, `s.trimEnd()`, and `s.trimLeft()`, `s.trimRight()` | `js_trim`, `js_trim_start`, `js_trim_end`: JavaScript's whitespace, which is not Rust's |
| `s.slice(start, end)` | `js_slice` |
| `s.substring(start, end)` | `js_substring` |
| `s.substr(start, length)` | `js_substr`: `start` counts from the end when negative |
| `s.at(i)`, `s.charAt(i)` | `js_at` (`None` for `undefined`), `js_char_at` |
| `s.charCodeAt(i)`, `s.codePointAt(i)` | `js_char_code_at` (`NaN` outside the string), `js_code_point_at` (`None` for `undefined`; inside a pair, the second half alone) |
| `s.indexOf(x)`, `s.lastIndexOf(x)` | `js_index_of`, `js_last_index_of` |
| `s.indexOf(x, i)`, `s.lastIndexOf(x, i)` | `js_index_of_from`, `js_last_index_of_from`: `""` is found at `i` itself, and a `NaN` position for `.lastIndexOf()` is the end |
| `s.split(x)`, `s.split(x, n)` | `js_split`, `js_split_limit`: the first `n` pieces, `n` taken as `ToUint32` takes it (`-1` is 4294967295, 2³² is 0) |
| `s.concat(a, b)` | a `format!`, each argument written as `String()` writes it |
| `s.replace(x, y)`, `s.replaceAll(x, y)` | `js_replace`, `js_replace_all`, with the `$&`, `` $` ``, `$'` and `$$` patterns JavaScript reads in `y` |
| `s.padStart(n, x)`, `s.padEnd(n, x)` | `js_pad_start`, `js_pad_end` |
| `s.repeat(n)` | `js_repeat` |
| `s.includes(x)`, `s.startsWith(x)`, `s.endsWith(x)` | `str::contains`, `starts_with`, `ends_with`: they agree |
| `s.includes(x, i)`, `s.startsWith(x, i)`, `s.endsWith(x, i)` | `js_includes_from`, `js_starts_with_at`, `js_ends_with_at`: inside a pair only `""` starts or ends |
| `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");
```

A position inside a surrogate pair gives what JavaScript gives: a search string that holds no half
cannot start at the pair's second half, so `"šŸ¦€x".indexOf("x", 1)` is 2 and `"šŸ¦€x".startsWith("x", 1)`
is `false`, while `"šŸ¦€".indexOf("", 1)` is 1.

```rust
assert_eq!(ferrovue::js_index_of_from("šŸ¦€x", "x", 1.0), 2);
assert!(!ferrovue::js_starts_with_at("šŸ¦€x", "x", 1.0));
assert_eq!(ferrovue::js_split_limit("a,b,c", ",", 2.0), ["a", "b"]);
assert_eq!(ferrovue::js_substr("ferrovue", -3.0, None), "vue");
assert_eq!(ferrovue::js_code_point_at("šŸ¦€", 0.0), Some(0x1F980));
```

The patterns are strings: a regular expression (`.replace(/x/g, …)`, `.split(/,/)`) and a
replacement function are refused, as are `.toLocaleUpperCase()` and `.toLocaleLowerCase()`, since
the server's locale is not the browser's, and `.localeCompare()` and `.toLocaleString()`, which
format with `Intl` ([`dates_and_prices`](crate::guide::dates_and_prices)).

# 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 (beside one another, or with only a string that may be empty between them),
ordered against anything but a literal below U+D800, read by `.charCodeAt()` or `.codePointAt()`
(which would read U+FFFD's code), or written by `JSON.stringify` (which escapes it), by
`encodeURIComponent()` and `encodeURI()` 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`. 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`: 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`: the leading digits, or `NaN` |
| `parseFloat(s)` | `js_parse_float`: the longest decimal literal at the start, or `NaN` |
| `JSON.stringify(s)` of a string | `js_json_string` |
| `JSON.stringify(x)` of a number | `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 |
| `String(x)`, `` `${x}` ``, `s.concat(x)` of a value that may be absent | `undefined`, or `null` for a value typed `T \| null` |
| `String(xs)`, `` `${xs}` `` of a list | the items joined by `,`, as `xs.join()` joins them: an item that is `null` or `undefined` as nothing |

`{{ x }}` is not `String(x)`: Vue writes an absent value as nothing and a list as JSON, two spaces
deep (`toDisplayString`), so `{{ label }}` of an absent label writes nothing where
`` {{ `${label}` }} `` writes `undefined`, and `{{ tags }}` of a list is refused where
`` {{ `${tags}` }} `` writes `a,b`. A value that may be either `null` or `undefined`, which its Rust
`Option` cannot tell apart, is refused ([FV0721](crate::guide::error_codes#fv0721)): give it a
fallback with `??`.

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");
```

# URIs

`encodeURIComponent(s)` and `encodeURI(s)` write each byte of a character's UTF-8 as `%XX`, the
hexadecimal digits in upper case, except the characters JavaScript keeps:
[`js_encode_uri_component`](crate::js_encode_uri_component) keeps the ASCII letters and digits and
`-_.!~*'()`, and [`js_encode_uri`](crate::js_encode_uri) the characters a URI reserves as well,
`;/?:@&=+$,#`. Both are held to `uri.json`, every ASCII character and a few hundred strings drawn at
random, recorded from Node and read alike by Chromium and Firefox. Their argument is a string, or
anything `String()` writes (`encodeURIComponent(page)` of a number). JavaScript throws a `URIError`
on half of a surrogate pair, which ferrovue holds as U+FFFD, so a string that may hold one is
refused ([FV0706](crate::guide::error_codes#fv0706)). `decodeURI()` and `decodeURIComponent()`,
which throw on a malformed escape, are refused too.

```rust
assert_eq!(ferrovue::js_encode_uri_component("rust & vue/Ć©"), "rust%20%26%20vue%2F%C3%A9");
assert_eq!(ferrovue::js_encode_uri("/a b?q=1&r=[2]#top"), "/a%20b?q=1&r=%5B2%5D#top");
```

# Slicing lists

A list's `.slice(start, end)` takes the same range as a string's, counted in items:
`js_slice_items` takes the items `js_slice_range` gives. `.at(i)` is the item `js_item_at` picks,
counted from the end when `i` is negative and `undefined` outside the list, and `list[i]` with an
integer `i` is `list.get(i)`, `undefined` for a negative `i` too. The other array methods with an
arrow function, `.filter()`, `.map()`, `.some()`, `.every()`, `.find()`, `.findIndex()`,
`.findLast()` and `.findLastIndex()`, 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. `.indexOf()` and `.lastIndexOf()` compare with `===`: `NaN` is found
nowhere, though `.includes()` finds it, and `-0` is found as `0`. `.concat()` chains the items of
lists of one item type, numbers whether `number` or `Float`, and so does a spread in an array
literal, `[...a, x, ...b]`.

# Building lists

`.reduce(f, init)` and `.reduceRight(f, init)` fold the items with `js_reduce`, from the end for
`.reduceRight()`, keeping the index each item has in the list. The accumulator's type is fixed when
compiling, from the initial value and the function's body: an integer start whose body gives a
fraction, or may give `-0` (`acc * -x`), accumulates a `Float`, and a string grows by `+`, under the
rules `+` follows for halves of surrogate pairs. Without an initial value JavaScript throws on an
empty list, so both are refused without one.

`.toSorted()` without a comparator orders the items by `String(x)`, compared by UTF-16 code unit
as `js_cmp` compares them: `js_sorted_by_string` sorts `[10, 9, 1]` to `[1, 10, 9]`, puts `NaN`
after `Infinity` and leaves `-0` and `0`, both `"0"`, in their order. With a comparator, the
compiler accepts the shapes whose order it can reproduce for every input: `a - b` and `b - a` of
integers or of integer fields (`a.n - b.n`), whose sign `js_order` takes, and `?:` over `<`, `>`,
`<=`, `>=`, `===` and `!==` of integers or strings, giving literals whose signs make a consistent
order (`a < b ? -1 : a > b ? 1 : 0`). `js_sorted` sorts by it, and, as V8 and SpiderMonkey do,
keeps the items it finds equal in their order. A `Float` key is refused: once a `NaN` is among the
keys, the comparator is inconsistent and each engine orders the items its own way. So is
`localeCompare`, which depends on the locale. `.toReversed()` is the items from the end, and
`[...list].sort(…)` and `[...list].reverse()` are the same as `.toSorted(…)` and `.toReversed()`;
`.sort()` and `.reverse()` of a prop or state would change it in place, and are refused.

`Math.max(...xs)` and `Math.min(...xs)` fold the items with `js_max` and `js_min` from `-Infinity`
and `Infinity`, which they give for an empty list, `NaN` when any item is, and `0` above `-0`.

```rust
use std::cmp::Ordering;

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"]);
assert_eq!(ferrovue::js_item_at(words.iter().copied(), -1.0), Some("d"));
assert_eq!(ferrovue::js_item_at(words.iter().copied(), 4.0), None);

let sorted: Vec<i64> = ferrovue::js_sorted_by_string([10, 9, 1].into_iter(), |v| ferrovue::Js(*v).to_string()).collect();
assert_eq!(sorted, [1, 10, 9]);
let by_value: Vec<i64> = ferrovue::js_sorted([10, 9, 1].into_iter(), |a, b| ferrovue::js_order(*a as f64, *b as f64)).collect();
assert_eq!(by_value, [1, 9, 10]);
assert_eq!(ferrovue::js_order(f64::NAN, 1.0), Ordering::Equal);
assert_eq!(ferrovue::js_reduce(words.iter().rev(), String::new(), |acc, w| acc + w), "dcba");
assert!([0.0, -0.0].into_iter().fold(f64::INFINITY, ferrovue::js_min).is_sign_negative());
```

# 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. A
`computed` that calls one of them is worked out where the render first reads it, once, as Vue
works it out, so it panics only when the render reads it.