@barefootjs/rust 0.21.4 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -189190,18 +189190,18 @@ var minijinjaAdapter = new MinijinjaAdapter;
189190
189190
  // src/conformance-pins.ts
189191
189191
  var conformancePins = {
189192
189192
  "static-array-from-props": [
189193
- { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2087" }
189193
+ { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2321" }
189194
189194
  ],
189195
189195
  "static-array-from-props-with-component": [
189196
- { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2087" }
189196
+ { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2321" }
189197
189197
  ],
189198
189198
  "filter-nested-callback-predicate": [
189199
- { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
189199
+ { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2320" }
189200
189200
  ],
189201
189201
  "filter-nested-find-predicate": [
189202
- { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
189202
+ { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2320" }
189203
189203
  ],
189204
- "dangerous-inner-html-dynamic": [{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2215" }],
189204
+ "dangerous-inner-html-dynamic": [{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2319" }],
189205
189205
  "date-method-uncatalogued": [{ code: "BF021", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2274" }]
189206
189206
  };
189207
189207
  // src/render-divergences.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/rust",
3
- "version": "0.21.4",
3
+ "version": "0.24.1",
4
4
  "description": "minijinja (Rust) adapter for BarefootJS — compiles IR to .j2 templates and ships a Rust rendering runtime (packages/adapter-rust/runtime/); runs under any Rust web framework (axum, etc.)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -54,14 +54,14 @@
54
54
  "directory": "packages/adapter-rust"
55
55
  },
56
56
  "dependencies": {
57
- "@barefootjs/shared": "0.21.4"
57
+ "@barefootjs/shared": "0.24.1"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "@barefootjs/jsx": ">=0.2.0"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@barefootjs/adapter-tests": "0.1.0",
64
- "@barefootjs/jsx": "0.21.4",
64
+ "@barefootjs/jsx": "0.24.1",
65
65
  "typescript": "^5.0.0"
66
66
  }
67
67
  }
@@ -161,6 +161,152 @@ pub fn date(recv: &JsValue, op: &str) -> JsValue {
161
161
  }
162
162
  }
163
163
 
164
+ /// Parse a fixed UTC offset `tz` matching `^([+-])(\d{2}):(\d{2})$` into
165
+ /// signed minutes -- mirrors `OFFSET_RE` in
166
+ /// `packages/client/src/format-date.ts` and `tzOffsetRE` in the Go
167
+ /// runtime's `bf.go`. ANY other shape (`"UTC"`, an IANA zone name, a
168
+ /// malformed offset like `"+9:00"`) is not this exact 6-byte
169
+ /// sign/digit/digit/colon/digit/digit layout and normalizes to `0`
170
+ /// minutes (UTC) -- byte/ascii-digit checks here, not a `regex` crate
171
+ /// dependency (this crate's fixed dependency list excludes `regex`, same
172
+ /// as this module's docstring notes for `chrono`).
173
+ fn parse_tz_offset(tz: &str) -> i64 {
174
+ let b = tz.as_bytes();
175
+ if b.len() != 6 || b[3] != b':' {
176
+ return 0;
177
+ }
178
+ let sign = match b[0] {
179
+ b'+' => 1i64,
180
+ b'-' => -1i64,
181
+ _ => return 0,
182
+ };
183
+ if !b[1].is_ascii_digit() || !b[2].is_ascii_digit() || !b[4].is_ascii_digit() || !b[5].is_ascii_digit() {
184
+ return 0;
185
+ }
186
+ let hh = (b[1] - b'0') as i64 * 10 + (b[2] - b'0') as i64;
187
+ let mm = (b[4] - b'0') as i64 * 10 + (b[5] - b'0') as i64;
188
+ sign * (hh * 60 + mm)
189
+ }
190
+
191
+ /// `names`-table section offsets (#2334, mirrors `MONTHS_WIDE` /
192
+ /// `MONTHS_ABBR` / `WEEKDAYS_WIDE` / `WEEKDAYS_ABBR` in
193
+ /// `packages/client/src/format-date.ts` and the Go runtime's `bf.go`
194
+ /// `monthsWide` / `monthsAbbr` / `weekdaysWide` / `weekdaysAbbr` constants).
195
+ const MONTHS_WIDE: usize = 0;
196
+ const MONTHS_ABBR: usize = 12;
197
+ const WEEKDAYS_WIDE: usize = 24;
198
+ const WEEKDAYS_ABBR: usize = 31;
199
+
200
+ /// Read one `names` table entry (#2334): `names` is the runtime's own
201
+ /// [`JsValue`] array value (whatever array shape `bf.arr`-equivalent
202
+ /// template output materializes into -- see [`crate::runtime::mj_to_js`]'s
203
+ /// `Seq`/`Iterable` arm, which is what a compiled minijinja template's
204
+ /// 4th `format_date` argument becomes by the time it reaches here). A
205
+ /// missing/out-of-range index, a non-array `names`, or a non-string
206
+ /// element all render `""` -- the same total, zero-value discipline as an
207
+ /// unparseable date, mirroring `names[index] ?? ''` in the JS reference
208
+ /// (`packages/client/src/format-date.ts`'s `nameAt`) and `formatDateName`
209
+ /// in the Go runtime's `bf.go`.
210
+ fn name_at(names: &JsValue, index: usize) -> &str {
211
+ match names.as_array().and_then(|arr| arr.get(index)) {
212
+ Some(JsValue::String(s)) => s.as_str(),
213
+ _ => "",
214
+ }
215
+ }
216
+
217
+ /// `format_date(recv, pattern, tz, names)` -- the lowering target for a
218
+ /// `formatDate(date, pattern, timeZone, names)` call
219
+ /// (spec/template-helpers.md "format_date", #2324, #2334): a total,
220
+ /// deterministic date-pattern formatter -- no locale, no host timezone, no
221
+ /// `SystemTime::now`. Mirrors `packages/client/src/format-date.ts` (the
222
+ /// JS-normative reference) and the Go runtime's `FormatDate` byte-for-byte.
223
+ ///
224
+ /// `recv` follows the `date` helper's receiver contract exactly (see
225
+ /// [`epoch_ms_of`]): this runtime's own [`JsValue::Date`] or an ISO-8601
226
+ /// string, both normalized to a single epoch-ms instant. A `None`
227
+ /// (nil/unset or unparseable) receiver renders `""` -- unlike [`date`],
228
+ /// there is no accessor/`getTime` numeric fallback here, since
229
+ /// `format_date` always returns a string.
230
+ ///
231
+ /// `tz` resolves via [`parse_tz_offset`]; the instant is shifted by
232
+ /// `offset_minutes * 60_000` ms and the shifted instant's UTC calendar
233
+ /// fields (not the original instant's) are what the pattern tokens read --
234
+ /// the shifted UTC clock face IS the local clock face at that offset.
235
+ ///
236
+ /// `names` (#2334) is a flat table in fixed layout: `[0..11]` wide month
237
+ /// names, `[12..23]` abbreviated month names, `[24..30]` wide weekday names
238
+ /// (Sunday-first), `[31..37]` abbreviated weekday names -- see [`name_at`].
239
+ /// The caller owns the values (locale selection is not this function's
240
+ /// job); this only indexes the table.
241
+ ///
242
+ /// `pattern` is scanned left-to-right, longest-match, for the token set
243
+ /// `YYYY|MMMM|MMM|MM|DD|dddd|ddd|M|D` (checking longer tokens before
244
+ /// shorter ones at each position, the same alternation-order discipline the
245
+ /// Go/JS ports use); every other character -- including multi-byte ones
246
+ /// like 年/月/日 or 月-name table values -- passes through/renders
247
+ /// literally. The scan advances by whole characters only (ASCII token
248
+ /// matches consume exactly 1/2/4 ASCII bytes; the fallback branch consumes
249
+ /// one full `char` via `len_utf8`), so slicing never lands mid-codepoint.
250
+ /// `YYYY` is `abs(year)` zero-padded to 4 digits, `-`-prefixed for a
251
+ /// negative year; `MM`/`DD` zero-pad to 2; `M`/`D` are bare. `MMMM`/`MMM`
252
+ /// read `names[month-1]` / `names[12+month-1]`; `dddd`/`ddd` read
253
+ /// `names[24+weekday]` / `names[31+weekday]`, where `weekday` (0 = Sunday)
254
+ /// is derived from the shifted epoch day count via
255
+ /// `(days + 4).rem_euclid(7)` -- 1970-01-01 (`days == 0`) is a Thursday,
256
+ /// and `rem_euclid` (not `%`, which truncates toward zero in Rust) keeps
257
+ /// the result in `[0, 6]` for a negative (pre-1970) `days` too.
258
+ pub fn format_date(recv: &JsValue, pattern: &str, tz: &str, names: &JsValue) -> String {
259
+ let ms = match epoch_ms_of(recv) {
260
+ Some(ms) => ms,
261
+ None => return String::new(),
262
+ };
263
+ let offset_minutes = parse_tz_offset(tz);
264
+ let shifted = ms + offset_minutes * 60_000;
265
+ let days = shifted.div_euclid(MS_PER_DAY);
266
+ let (year, month, day) = civil_from_days(days);
267
+ let yyyy = if year < 0 { format!("-{:04}", -year) } else { format!("{year:04}") };
268
+ let weekday = (days + 4).rem_euclid(7) as usize; // 0 = Sunday; epoch (days=0) is Thursday
269
+
270
+ let mut out = String::with_capacity(pattern.len());
271
+ let mut i = 0;
272
+ while i < pattern.len() {
273
+ let rest = &pattern[i..];
274
+ if rest.starts_with("YYYY") {
275
+ out.push_str(&yyyy);
276
+ i += 4;
277
+ } else if rest.starts_with("MMMM") {
278
+ out.push_str(name_at(names, MONTHS_WIDE + (month - 1) as usize));
279
+ i += 4;
280
+ } else if rest.starts_with("MMM") {
281
+ out.push_str(name_at(names, MONTHS_ABBR + (month - 1) as usize));
282
+ i += 3;
283
+ } else if rest.starts_with("MM") {
284
+ out.push_str(&format!("{month:02}"));
285
+ i += 2;
286
+ } else if rest.starts_with("DD") {
287
+ out.push_str(&format!("{day:02}"));
288
+ i += 2;
289
+ } else if rest.starts_with("dddd") {
290
+ out.push_str(name_at(names, WEEKDAYS_WIDE + weekday));
291
+ i += 4;
292
+ } else if rest.starts_with("ddd") {
293
+ out.push_str(name_at(names, WEEKDAYS_ABBR + weekday));
294
+ i += 3;
295
+ } else if rest.starts_with('M') {
296
+ out.push_str(&month.to_string());
297
+ i += 1;
298
+ } else if rest.starts_with('D') {
299
+ out.push_str(&day.to_string());
300
+ i += 1;
301
+ } else {
302
+ let ch = rest.chars().next().expect("i < pattern.len() guarantees a char remains");
303
+ out.push(ch);
304
+ i += ch.len_utf8();
305
+ }
306
+ }
307
+ out
308
+ }
309
+
164
310
  #[cfg(test)]
165
311
  mod tests {
166
312
  use super::*;
@@ -1338,6 +1338,16 @@ impl Object for BfInstance {
1338
1338
  // string; `date::date` normalizes and dispatches both. `op`
1339
1339
  // stays a plain string arg, never routed through `js_number`.
1340
1340
  "date" => Ok(js_to_mj(&date::date(a(0), a(1).as_str().unwrap_or("")))),
1341
+ // `format_date(recv, pattern, tz, names)` (spec/template-helpers.md
1342
+ // "format_date", #2324, #2334) -- total, locale-free date-pattern
1343
+ // formatting layered on the same `recv` normalization as
1344
+ // `date` above. `names` stays the array-shaped `JsValue` that
1345
+ // `mj_to_js`'s `Seq`/`Iterable` arm already produced above (the
1346
+ // compiler's lowering always passes 4 args); a non-array or
1347
+ // missing `names` degrades to `""` for every name token, per
1348
+ // `date::name_at`. See `date::format_date`'s docstring for the
1349
+ // full contract.
1350
+ "format_date" => Ok(MjValue::from(date::format_date(a(0), a(1).as_str().unwrap_or(""), a(2).as_str().unwrap_or(""), a(3)))),
1341
1351
 
1342
1352
  // -- Array / string method helpers (#1448 Tier A) ------------------
1343
1353
  "includes" => Ok(MjValue::from(includes(a(0), a(1)))),
@@ -150,6 +150,7 @@ fn call_binding(fn_name: &str, args: &[JsValue]) -> Option<JsValue> {
150
150
  JsValue::String(num::to_fixed(runtime::js_number(&a(0)), digits))
151
151
  }
152
152
  "date" => date::date(&a(0), a(1).as_str().unwrap_or("")),
153
+ "format_date" => JsValue::String(date::format_date(&a(0), a(1).as_str().unwrap_or(""), a(2).as_str().unwrap_or(""), &a(3))),
153
154
  "lower" => JsValue::String(runtime::js_string(&a(0)).to_lowercase()),
154
155
  "upper" => JsValue::String(runtime::js_string(&a(0)).to_uppercase()),
155
156
  "trim" => JsValue::String(runtime::trim(&a(0))),
@@ -42,15 +42,15 @@ export const conformancePins: ConformancePins = {
42
42
  // diagnostic (mirrors adapter-jinja's identical check). Fixing the
43
43
  // underlying gap (computed-array-from-props as a loop source) is out of
44
44
  // scope for #2087; tracked as a follow-up at
45
- // https://github.com/piconic-ai/barefootjs/issues/2087.
45
+ // https://github.com/piconic-ai/barefootjs/issues/2321.
46
46
  'static-array-from-props': [
47
- { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2087' },
47
+ { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2321' },
48
48
  ],
49
49
  // BF101 (unresolvable computed loop array, see above) fires; BF103
50
50
  // (imported child in the loop body) no longer does now that the
51
51
  // conformance harness passes `siblingTemplatesRegistered: true` (#2205).
52
52
  'static-array-from-props-with-component': [
53
- { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2087' },
53
+ { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2321' },
54
54
  ],
55
55
  // Rest-destructure `.map()` callbacks (#2087 Phase B): every shape now
56
56
  // lowers natively — fixed bindings at any depth/shape via a chained
@@ -80,12 +80,12 @@ export const conformancePins: ConformancePins = {
80
80
  // lossy, same as xslate. The `/* @client */` twin
81
81
  // (`filter-nested-callback-predicate-client`) has no pin here: it must
82
82
  // render clean on every adapter, which asserts the suppression contract.
83
- // https://github.com/piconic-ai/barefootjs/issues/2038
83
+ // Faithful lowering tracked: https://github.com/piconic-ai/barefootjs/issues/2320 (successor to #2038)
84
84
  'filter-nested-callback-predicate': [
85
- { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2038' },
85
+ { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2320' },
86
86
  ],
87
87
  'filter-nested-find-predicate': [
88
- { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2038' },
88
+ { code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2320' },
89
89
  ],
90
90
  // NB: TOP-LEVEL `.find` / `.findIndex` / `.findLast` / `.findLastIndex`
91
91
  // (text position) are NOT pinned here — like xslate (unlike mojo, which
@@ -101,8 +101,8 @@ export const conformancePins: ConformancePins = {
101
101
  // `dangerouslySetInnerHTML={{ __html: '...' }}` is spliced directly into
102
102
  // the template as trusted raw text (`resolveDangerousInnerHtml`, #2207).
103
103
  // A dynamic/signal-derived value still refuses with BF101 — see the
104
- // `dangerous-inner-html-dynamic` fixture/pin below (tracked: #2215).
105
- 'dangerous-inner-html-dynamic': [{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2215' }],
104
+ // `dangerous-inner-html-dynamic` fixture/pin below (tracked: #2319, successor to #2215).
105
+ 'dangerous-inner-html-dynamic': [{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2319' }],
106
106
  // #2273: a method call on a prop typed as a built-in host rich type
107
107
  // (Date, Map, …) has no catalogued lowering in any adapter — this is a
108
108
  // compiler-level refusal (`checkRichTypeMethodCalls`, wired ahead of