| 1 | // vocab_node drives datetime.js's vocabulary resolution in plain Node |
| 2 | // against a stub page: a document whose lang is en, no |
| 3 | // self.fiestaStrings, and a field with no attributes, so the only |
| 4 | // vocabulary left is the script's own English. It reads back one JSON |
| 5 | // object: "words", the vocabulary the field resolved to; "parsed", |
| 6 | // each phrase on stdin to the ISO instant the parser read it as (null |
| 7 | // for no reading), at a fixed now of 2026-10-28 10:30 local; and |
| 8 | // "malformed", the vocabulary a field with an attribute that is not |
| 9 | // JSON resolves to on a Spanish page with the Spanish table loaded. |
| 10 | // |
| 11 | // echo '{"phrases":["tomorrow","next fri 9am","in 2 weeks"]}' | |
| 12 | // node node/vocab_node.mjs |
| 13 | // |
| 14 | // check/datetime_test.go is the only caller. It holds the words to |
| 15 | // i18n/en.json's date-words and wants every phrase read, which is what |
| 16 | // the README promises of an English page that loads no table. |
| 17 | import { readFileSync } from "node:fs"; |
| 18 | import vm from "node:vm"; |
| 19 | |
| 20 | const src = readFileSync(new URL("../../js/datetime.js", import.meta.url), "utf8"); |
| 21 | |
| 22 | const chunks = []; |
| 23 | for await (const chunk of process.stdin) chunks.push(chunk); |
| 24 | const { phrases = [] } = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"); |
| 25 | |
| 26 | // load evaluates the script in a fresh context: the least DOM it |
| 27 | // touches on load, a loading document whose root carries lang, and a |
| 28 | // self holding strings as its registry. It returns what the script |
| 29 | // exported. |
| 30 | function load(lang, strings) { |
| 31 | const root = { lang, getAttribute: (k) => (k === "lang" ? lang : null) }; |
| 32 | const document = { |
| 33 | readyState: "loading", |
| 34 | documentElement: root, |
| 35 | addEventListener() {}, |
| 36 | querySelectorAll: () => [], |
| 37 | }; |
| 38 | const self = { fiestaStrings: strings }; |
| 39 | const module = { exports: {} }; |
| 40 | vm.runInNewContext(src, { document, self, window: self, module, exports: module.exports, setTimeout, clearTimeout }, { filename: "datetime.js" }); |
| 41 | return module.exports; |
| 42 | } |
| 43 | |
| 44 | // An element with attributes and nothing else. |
| 45 | const element = (attrs) => ({ getAttribute: (k) => (Object.prototype.hasOwnProperty.call(attrs, k) ? attrs[k] : null) }); |
| 46 | |
| 47 | const { vocabulary, parse, tables } = load("en", {}); |
| 48 | if (typeof vocabulary !== "function") { |
| 49 | process.stderr.write("datetime.js does not export vocabulary\n"); |
| 50 | process.exit(1); |
| 51 | } |
| 52 | // A field with no attributes at all, on an English page with no table. |
| 53 | const words = vocabulary(element({})); |
| 54 | |
| 55 | // A Spanish page whose table is the shipped i18n/es.json, and a field |
| 56 | // whose attribute is not JSON: the attribute is as good as absent, so |
| 57 | // the vocabulary must be the table's, not English. |
| 58 | const es = JSON.parse(readFileSync(new URL("../../i18n/es.json", import.meta.url), "utf8")); |
| 59 | const malformed = load("es", { es }).vocabulary(element({ "data-rst-date-words": "{not json" })); |
| 60 | const now = new Date(2026, 9, 28, 10, 30); |
| 61 | const tbl = tables("en"); |
| 62 | const parsed = {}; |
| 63 | for (const phrase of phrases) { |
| 64 | let read = null; |
| 65 | try { |
| 66 | read = parse(phrase, words, tbl, now, {}); |
| 67 | } catch (e) { |
| 68 | read = null; |
| 69 | } |
| 70 | parsed[phrase] = read && read.date ? read.date.toISOString() : null; |
| 71 | } |
| 72 | process.stdout.write(JSON.stringify({ words, parsed, malformed })); |
| 73 | |