// vocab_node drives datetime.js's vocabulary resolution in plain Node // against a stub page: a document whose lang is en, no // self.fiestaStrings, and a field with no attributes, so the only // vocabulary left is the script's own English. It reads back one JSON // object: "words", the vocabulary the field resolved to; "parsed", // each phrase on stdin to the ISO instant the parser read it as (null // for no reading), at a fixed now of 2026-10-28 10:30 local; and // "malformed", the vocabulary a field with an attribute that is not // JSON resolves to on a Spanish page with the Spanish table loaded. // // echo '{"phrases":["tomorrow","next fri 9am","in 2 weeks"]}' | // node node/vocab_node.mjs // // check/datetime_test.go is the only caller. It holds the words to // i18n/en.json's date-words and wants every phrase read, which is what // the README promises of an English page that loads no table. import { readFileSync } from "node:fs"; import vm from "node:vm"; const src = readFileSync(new URL("../../js/datetime.js", import.meta.url), "utf8"); const chunks = []; for await (const chunk of process.stdin) chunks.push(chunk); const { phrases = [] } = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"); // load evaluates the script in a fresh context: the least DOM it // touches on load, a loading document whose root carries lang, and a // self holding strings as its registry. It returns what the script // exported. function load(lang, strings) { const root = { lang, getAttribute: (k) => (k === "lang" ? lang : null) }; const document = { readyState: "loading", documentElement: root, addEventListener() {}, querySelectorAll: () => [], }; const self = { fiestaStrings: strings }; const module = { exports: {} }; vm.runInNewContext(src, { document, self, window: self, module, exports: module.exports, setTimeout, clearTimeout }, { filename: "datetime.js" }); return module.exports; } // An element with attributes and nothing else. const element = (attrs) => ({ getAttribute: (k) => (Object.prototype.hasOwnProperty.call(attrs, k) ? attrs[k] : null) }); const { vocabulary, parse, tables } = load("en", {}); if (typeof vocabulary !== "function") { process.stderr.write("datetime.js does not export vocabulary\n"); process.exit(1); } // A field with no attributes at all, on an English page with no table. const words = vocabulary(element({})); // A Spanish page whose table is the shipped i18n/es.json, and a field // whose attribute is not JSON: the attribute is as good as absent, so // the vocabulary must be the table's, not English. const es = JSON.parse(readFileSync(new URL("../../i18n/es.json", import.meta.url), "utf8")); const malformed = load("es", { es }).vocabulary(element({ "data-rst-date-words": "{not json" })); const now = new Date(2026, 9, 28, 10, 30); const tbl = tables("en"); const parsed = {}; for (const phrase of phrases) { let read = null; try { read = parse(phrase, words, tbl, now, {}); } catch (e) { read = null; } parsed[phrase] = read && read.date ? read.date.toISOString() : null; } process.stdout.write(JSON.stringify({ words, parsed, malformed }));