Skip to main content

web_rpc/js/
writer.rs

1//! The two-pass const writer and the string helpers the renderers need.
2//!
3//! The renderers run twice: once as `Output<0>`, which only counts, and once as
4//! `Output<LENGTH>`, which writes.
5
6/// Const-evaluable output buffer.
7///
8/// With `CAPACITY == 0` nothing is stored and only [`Output::length`] advances (the measuring
9/// pass). With `CAPACITY > 0` the bytes are written into `bytes`, which must be exactly the
10/// length the measuring pass reported; anything else is an out-of-bounds const-eval error.
11pub struct Output<const CAPACITY: usize> {
12    /// The rendered bytes. Meaningful only when `CAPACITY` is the measured length.
13    pub bytes: [u8; CAPACITY],
14    /// The number of bytes rendered.
15    pub length: usize,
16}
17
18impl<const CAPACITY: usize> Default for Output<CAPACITY> {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl<const CAPACITY: usize> Output<CAPACITY> {
25    /// A fresh, empty writer.
26    pub const fn new() -> Self {
27        Self {
28            bytes: [0u8; CAPACITY],
29            length: 0,
30        }
31    }
32
33    const fn put_byte(&mut self, byte: u8) {
34        if CAPACITY > 0 {
35            self.bytes[self.length] = byte;
36        }
37        self.length += 1;
38    }
39
40    /// Append a string.
41    pub const fn put(&mut self, text: &str) {
42        let bytes = text.as_bytes();
43        let mut index = 0;
44        while index < bytes.len() {
45            self.put_byte(bytes[index]);
46            index += 1;
47        }
48    }
49
50    /// Append a decimal integer.
51    pub const fn put_usize(&mut self, value: usize) {
52        if value >= 10 {
53            self.put_usize(value / 10);
54        }
55        self.put_byte(b'0' + (value % 10) as u8);
56    }
57
58    /// Append a string as a double-quoted Javascript literal, escaping backslashes and
59    /// quotes.
60    pub const fn put_quoted(&mut self, text: &str) {
61        self.put_byte(b'"');
62        let bytes = text.as_bytes();
63        let mut index = 0;
64        while index < bytes.len() {
65            let byte = bytes[index];
66            if byte == b'"' || byte == b'\\' {
67                self.put_byte(b'\\');
68            }
69            self.put_byte(byte);
70            index += 1;
71        }
72        self.put_byte(b'"');
73    }
74
75    /// Append an identifier, adding a trailing underscore if it is a reserved word.
76    pub const fn put_ident(&mut self, name: &str) {
77        self.put(name);
78        if is_reserved(name) {
79            self.put("_");
80        }
81    }
82
83    /// Append an identifier as a quoted string, with the same underscore rule.
84    pub const fn put_quoted_ident(&mut self, name: &str) {
85        self.put_byte(b'"');
86        self.put_ident(name);
87        self.put_byte(b'"');
88    }
89
90    /// Append `count` levels of two-space indentation.
91    pub const fn indent(&mut self, count: usize) {
92        let mut level = 0;
93        while level < count {
94            self.put("  ");
95            level += 1;
96        }
97    }
98}
99
100/// Compare two strings for equality in a const context.
101pub const fn str_eq(left: &str, right: &str) -> bool {
102    let left = left.as_bytes();
103    let right = right.as_bytes();
104    if left.len() != right.len() {
105        return false;
106    }
107    let mut index = 0;
108    while index < left.len() {
109        if left[index] != right[index] {
110            return false;
111        }
112        index += 1;
113    }
114    true
115}
116
117/// True if `text` is a bare Javascript identifier, which is what decides whether a schema
118/// container is declared under its own name or rendered inline. Postcard-schema names std
119/// generics `"Vec<T>"`, `"Result<T, E>"` and so on, which this rejects.
120pub const fn is_ident(text: &str) -> bool {
121    let bytes = text.as_bytes();
122    if bytes.is_empty() {
123        return false;
124    }
125    let mut index = 0;
126    while index < bytes.len() {
127        let byte = bytes[index];
128        let alpha = byte.is_ascii_alphabetic() || byte == b'_' || byte == b'$';
129        let ok = if index == 0 {
130            alpha
131        } else {
132            alpha || byte.is_ascii_digit()
133        };
134        if !ok {
135            return false;
136        }
137        index += 1;
138    }
139    true
140}
141
142/// The Javascript reserved words, plus the two identifiers that are not reserved but cannot
143/// be bound in strict mode.
144const RESERVED: &[&str] = &[
145    "arguments",
146    "await",
147    "break",
148    "case",
149    "catch",
150    "class",
151    "const",
152    "continue",
153    "debugger",
154    "default",
155    "delete",
156    "do",
157    "else",
158    "enum",
159    "eval",
160    "export",
161    "extends",
162    "false",
163    "finally",
164    "for",
165    "function",
166    "if",
167    "implements",
168    "import",
169    "in",
170    "instanceof",
171    "interface",
172    "let",
173    "new",
174    "null",
175    "package",
176    "private",
177    "protected",
178    "public",
179    "return",
180    "static",
181    "super",
182    "switch",
183    "this",
184    "throw",
185    "true",
186    "try",
187    "typeof",
188    "var",
189    "void",
190    "while",
191    "with",
192    "yield",
193];
194
195/// True if `name` may not be used as an identifier and needs a trailing underscore.
196pub const fn is_reserved(name: &str) -> bool {
197    let mut index = 0;
198    while index < RESERVED.len() {
199        if str_eq(RESERVED[index], name) {
200            return true;
201        }
202        index += 1;
203    }
204    false
205}