diff --git a/.gitignore b/.gitignore index aa6f0f0..6f3aad1 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,9 @@ jspm_packages # Optional REPL history .node_repl_history + +# Playground build output (copied from index.js + languages/) +site/lib/ + +# Netlify local state +.netlify/ diff --git a/.npmignore b/.npmignore index 055e5e8..e038fe6 100644 --- a/.npmignore +++ b/.npmignore @@ -1,5 +1,8 @@ -# Server (API) - repo only, not published to npm -server/ +# Archived code and the playground site - repo only, not published to npm +archive/ +site/ +scripts/ +netlify.toml # Tests test/ @@ -39,3 +42,6 @@ Thumbs.db # Dependencies node_modules/ + +# Netlify local link state +.netlify/ diff --git a/AGENTS.md b/AGENTS.md index 3d8d1a4..65c2e56 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,18 +2,28 @@ ## Overview -`numberstring` is a JavaScript library that converts numbers to their English word representation. It supports numbers from 0 to `Number.MAX_SAFE_INTEGER` (~9 quadrillion). +`numberstring` is a zero-dependency JavaScript library that converts numbers to words in 22 languages, with ordinals, decimals, currency, fractions, Roman numerals, and BigInt support up to 10^36. A static playground in `site/` is deployed to Netlify. ## Key Functions ### Main Export: `numberstring(n, options)` -- Converts a number to English words +- Converts a number to words; forgiving: accepts integers, negatives, decimals, numeric strings, BigInt - Returns `string` on success, `false` on invalid input -- Options: `{ cap: 'title'|'upper'|'lower', punc: '!'|'?'|'.' }` +- Options: `cap` (title, upper, lower, sentence, camel, pascal, snake, kebab, constant, dot), `punc` ('!' '?' '.'), `lang`, `point`, `and` (British), `formal` (zh/ja) ### Named Exports -- `comma(n)` - Format number with comma separators -- `group(n)` - Get magnitude group (0=ones, 1=thousands, 2=millions, etc.) + +English words: `ordinal`, `nth`, `decimal`, `fraction`, `percent`, `currency`, `year`, `telephone`, `negative`, `parse`, `toWords` + +Spoken and coded: `nato` (aliases `icao`, `military`), `morse` + +Notation: `compact`, `scientific`, `comma`, `group`, `binary`, `octal`, `hex`, `radix`, `bytes`, `bits` + +Other numeral systems (`numerals.js`): `roman`, `greek`, `egyptian`, `babylonian`, `fancy`, `clock` + +Languages (cardinals, non-negative integers): `spanish`, `french`, `german`, `danish`, `chinese`, `hindi`, `russian`, `portuguese`, `japanese`, `korean`, `arabic`, `italian`, `dutch`, `turkish`, `polish`, `swedish`, `indonesian`, `thai`, `norwegian`, `finnish`, `icelandic`. `chinese` and `japanese` accept `{ formal: true }`; `spanish` and `portuguese` accept `{ cap }`. + +Constants: `CAP_STYLES`, `FANCY_STYLE_NAMES` ## Usage Examples @@ -21,7 +31,9 @@ import numberstring, { comma, group } from 'numberstring'; numberstring(42); // 'forty-two' -numberstring(1000000); // 'one million' +numberstring(-3.14); // 'negative three point one four' +numberstring('1000000'); // 'one million' +numberstring(42, { lang: 'es' }); // 'cuarenta y dos' numberstring(100, { cap: 'title' }); // 'One Hundred' numberstring(50, { punc: '!' }); // 'fifty!' @@ -31,8 +43,8 @@ group(1000000); // 2 (millions) ## Important Notes -- Only accepts non-negative integers within safe integer range -- Returns `false` for: NaN, Infinity, negative numbers, strings, objects +- Returns `false` for: NaN, Infinity, non-numeric strings, objects, values beyond 10^36 +- Language modules handle non-negative integers only; negatives/decimals with a non-English `lang` return `false` - Hyphenates compound numbers (e.g., "forty-two", "ninety-nine") - This is an ESM-only package (use `import`, not `require`) @@ -46,7 +58,15 @@ npm run test:coverage # Run with coverage ## Code Architecture -- `index.js` - Single file containing all logic +- `index.js` - English core and all public helpers (ordinal, nato, scientific, bytes, ...) +- `numerals.js` - roman-adjacent systems (egyptian, babylonian, greek), `fancy()` digit styles, `clock()` +- `languages/*.js` - one module per language; `test/languages.test.js` is the per-language spot-check table +- `test/extras.test.js` - tests for everything added in 1.2.0 +- `index.d.ts` - hand-written types; check with `npx tsc --noEmit --strict index.d.ts` +- `site/` - playground; `scripts/build-site.js` stages the library into `site/lib/` +- `archive/` - unmaintained code (old Express server), excluded from tests and lint - Pure functions, no side effects +- **Zero runtime dependencies.** Never add a package to `dependencies`. +- Only use Unicode blocks that macOS renders with system fonts (Mayan numerals and tally marks were dropped for this reason) - Frozen arrays for immutable word lists - Full JSDoc type documentation diff --git a/CHANGELOG.md b/CHANGELOG.md index 808bf9c..5de0d48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,68 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.0] - 2026-10-06 + +### Added + +- **Casing styles** - `cap` now also accepts `sentence`, `camel`, `pascal`, `snake`, `kebab` (alias `hyphen`), `constant` (alias `screaming`), and `dot`: `numberstring(123, { cap: 'snake' })` → `one_hundred_twenty_three`. Exported as `CAP_STYLES`. +- **British `and` option** - `numberstring(123, { and: true })` → "one hundred and twenty-three", `numberstring(1001, { and: true })` → "one thousand and one". Also honored by `ordinal()`. +- **`nth(n)`** - Numeric ordinal suffix: `1st`, `22nd`, `113th`. +- **`compact(n, opt)`** - `1.5K`, `2.3B`, `1Sx`, with `digits` and `long` ("1.5 million") options. +- **`fancy(n, style)`** - Digits in Unicode styles: circled ④②, superscript ⁴², subscript, fullwidth, bold, doublestruck 𝟜𝟚, sans, monospace, keycap 4️⃣2️⃣, braille ⠼⠙⠃. +- **Alternative numeral systems** in `numerals.js`: `egyptian()` hieroglyphs (to 9,999,999), `babylonian()` base-60 cuneiform, `greek()` Ionic letters (to 9999). Mayan numerals and tally marks were tried and dropped: no system font on macOS. +- **Financial numerals** - `chinese(n, { formal: true })` → 壹仟零壹 (大写), `japanese(n, { formal: true })` → 壱千壱 (大字). Also via `toWords(n, { lang: 'zh', formal: true })`. +- **`nato(n)`** (aliases `icao`, `military`) - ICAO radiotelephony numerals: `1984` → "wun niner ait fower", `2500` → "too tousand fife hundred", `121.5` → "wun too wun decimal fife". +- **`morse(n)`** - International Morse code digits. +- `telephone(n, { oh: true })` says "oh" for zero. +- **`scientific(n)`** - Exact-mantissa scientific notation: `1984` → "1.984 × 10³", with `caret`, `e`, and `words` formats and a `digits` option. +- **`binary()`, `octal()`, `hex()`, `radix(n, base)`** - Other bases with `prefix`, `upper`, `pad` options. +- **`bytes(n)`** and **`bits(n)`** - `1.5 KB`, `1.5 KiB`, `1.5 Mb`, or "one point five kilobytes". +- **`clock(time)`** - Clock-face emoji for an hour or `H:MM`. +- `fancy()` accepts `emoji` as an alias for `keycap`, and a `clock` style that turns each digit into a clock face (814 → 🕗🕐🕓). +- `roman()` now supports vinculum notation above 3999 (a bar multiplies by 1000, two bars by a million), up to 3,999,999,999. +- Open Graph and Twitter card tags on the playground, with a PNG preview image (`site/og.png`, rendered from `site/og.svg`). +- `comma()` keeps decimals (`1,234,567.89`) and accepts numeric strings. +- Fuzz test (`test/fuzz.test.js`) proves every public function returns a value, never throws, for hostile inputs and options. +- `year()` accepts years beyond 9999 (read as cardinals) and BigInt. +- `bahasa` accepted as an alias for Indonesian. + +### Fixed + +- Playground: ASCII art header restored to the exact index.js block and no longer skewed by per-line centering; a malformed URL hash no longer breaks the page. +- Type declarations: per-language converters no longer advertise a `cap` option they ignore (use `toWords()` for that); Spanish and Portuguese keep it, Chinese and Japanese gain `formal`. + +### Changed + +- Still zero runtime dependencies. + +## [1.1.0] - 2026-10-02 + +### Added + +- **Forgiving default export** - `numberstring()` now accepts negatives, decimals, numeric strings, and a `lang` option, delegating to `negative()`, `decimal()`, and `toWords()`. Invalid input still returns `false`. +- **Playground** - Static site in `site/` deployed to Netlify. Type a number, see it in 22 languages plus ordinal, Roman, year, currency, and more. +- **Per-language spot-check tests** - `test/languages.test.js` locks in tricky numbers (21, 71, 80, 91, 100, 101, 1000, 1001, 2000, 21000, 1M, 2M, 21M) for all 22 languages. +- **TypeScript declarations** - `index.d.ts` covering the default export, every helper, options, and the language functions. +- Numbers above `Number.MAX_SAFE_INTEGER` (e.g. `1e21`) are widened to BigInt and converted instead of returning `false`. +- `npm run site` and `npm run site:build` scripts. + +### Fixed + +- Spanish: `100` is now `cien` (was `ciento`); `uno` apocopates before scale words (`un millón`, `veintiún mil`, `ciento un millones`). +- French: `million`/`milliard` pluralize (`deux millions`); `cents` and `quatre-vingts` drop the `s` before `mille` (`deux cent mille`). +- Italian: `-uno` elides before `mila` (`ventunmila`). +- Japanese: `一千` is kept after a higher scale word (`二万一千`, was `二万千`). +- Icelandic: gender agreement for 1–4 before `hundrað`/`þúsund` (neuter: `tvö þúsund`), `milljón` (feminine: `ein milljón`, `tvær milljónir`), and singular after numbers ending in 1 (`tuttugu og ein milljón`). +- `decimal(1e21)` returned `"one"`; exponent-form integers are now expanded, exponent-form fractions return `false`. +- `decimal()` kept only 15 digits of precision for long integer parts in strings. +- `parse('nineteen eighty-four')` returned `103`; adjacent simple words other than tens + ones now return `false`. + +### Changed + +- The Express REST server moved to `archive/server/` and is no longer tested, linted, or maintained. The playground supersedes it. +- Removed `express` and `supertest` from devDependencies. + ## [1.0.1] - 2026-02-08 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 18ec6d2..9e23735 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,6 +11,7 @@ npm install # Install dev dependencies npm test # Run Vitest tests npm run lint # Run ESLint npm run test:coverage # Run tests with coverage report +npm run site # Build + serve the playground at http://localhost:8080 ``` ## Code Style @@ -18,7 +19,7 @@ npm run test:coverage # Run tests with coverage report - ES2022+ syntax (const/let, arrow functions, template literals) - ESM modules only (`import`/`export`) - Full JSDoc documentation -- 100% test coverage target +- Keep coverage at or above 90% (current level); 100% on functions ## Architecture @@ -33,7 +34,7 @@ Key constants: ## When Making Changes 1. **ALWAYS run lint and tests before committing**: `npm run lint && npm test` -2. Maintain 100% coverage: `npm run test:coverage` +2. Keep coverage from dropping: `npm run test:coverage` 3. Update CHANGELOG.md for any user-facing changes 4. Preserve the fun flair (ASCII art header, tagline) @@ -44,6 +45,15 @@ Key constants: - Fix valid issues before pushing new commits - Use `gh pr view --comments` to fetch PR comments +## Layout + +- `index.js` - core English conversion plus all public helpers; `numberstring()` is forgiving and delegates to `negative()`, `decimal()`, `toWords()` +- `numerals.js` - alternative numeral systems (egyptian, babylonian, greek) and `fancy()` Unicode digit styles; table-driven, re-exported from index.js. Only add Unicode blocks that macOS renders out of the box (Mayan numerals and tally marks did not) +- `languages/` - one module per language, cardinals only, non-negative integers only +- `test/languages.test.js` - per-language spot-check table; update expectations when fixing a language +- `site/` - static playground deployed to Netlify (`netlify.toml`); `scripts/build-site.js` copies the library into `site/lib/`. `og.png` is the social preview; after editing `og.svg` run `npm run site:og` to re-render it +- `archive/server/` - old Express API, unmaintained, excluded from tests and lint; do not extend it + ## Supported Languages - English (default) @@ -55,17 +65,20 @@ Key constants: - Hindi (`hi`, `hindi`, `हिन्दी`) - Russian (`ru`, `russian`, `русский`) - Portuguese (`pt`, `portuguese`, `português`) +- Plus Japanese, Korean, Arabic, Italian, Dutch, Turkish, Polish, Swedish, Indonesian, Thai, Norwegian, Finnish, Icelandic (22 total) ## Features -- Number to words (cardinal) -- Ordinals (1st, 2nd, 3rd) -- Decimals (3.14 → "three point one four") -- Currency ($1.23 → "one dollar and twenty-three cents") -- Fractions (1/2 → "one half") -- Roman numerals (42 → "XLII") -- Negative numbers -- BigInt support up to 10^36 +Everything is exported from `index.js`; every function returns `string | false`. + +- English words: `numberstring` (cardinal, forgiving input), `ordinal`, `nth`, `decimal`, `fraction`, `percent`, `currency`, `year`, `telephone` (`oh` option), `negative`, `parse` (words → number) +- 22 languages via `toWords(n, { lang })` or the named exports; `chinese`/`japanese` take `formal` for 大写/大字 +- Spoken and coded: `nato` (`icao`, `military`), `morse` +- Notation: `compact` (1.5K), `scientific` (1.984 × 10³), `comma`, `binary`/`octal`/`hex`/`radix`, `bytes`/`bits` +- Other numeral systems: `roman` (vinculum above 3999), `greek`, `egyptian`, `babylonian`, `fancy` (circled, superscript, doublestruck, keycap/emoji, braille, ...), `clock` +- Options: `cap` casing styles (title, upper, lower, sentence, camel, pascal, snake, kebab, constant, dot), `punc`, `and` (British), `lang`, `point`, `formal` +- BigInt support up to 10^36; invalid input returns `false` +- **Zero runtime dependencies, always.** Never add a package to `dependencies`. --- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b04d92d..65b49e9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,15 +21,15 @@ We'd love help adding more languages! Here's how: 1. Create a new file in `languages/` (e.g., `pt.js` for Portuguese) 2. Follow the pattern in `languages/en.js` -3. Export a default function that converts numbers to words -4. Add your language to `languages/index.js` -5. Add tests in `test/index.test.js` -6. Update README.md with the new language -7. Submit a PR! +3. Export a default function that converts non-negative integers to words +4. Add your language and its aliases to `languages/index.js`, re-export it from `index.js`, and add it to the `toWords` switch +5. Add a row to the spot-check table in `test/languages.test.js` and a named export in `index.d.ts` +6. Update README.md (feature list, language table, direct exports) and CHANGELOG.md +7. Submit a PR against `dev` ### Pull Request Process -1. Fork the repo and create your branch from `main` +1. Fork the repo and create your branch from `dev` (PRs target `dev`; `master` is the release branch) 2. Run `npm install` to install dependencies 3. Make your changes 4. Run `npm test` to ensure tests pass @@ -37,12 +37,22 @@ We'd love help adding more languages! Here's how: 6. Update documentation if needed 7. Submit your PR! +### Adding a New Conversion + +1. Add the function to `index.js` (or `numerals.js` for glyph-based systems) with JSDoc and an `@example` +2. Return `false` for input you cannot convert; never throw +3. Add it to the export block, `index.d.ts`, the README at-a-glance table and API section, and CHANGELOG.md +4. Add tests in `test/extras.test.js` +5. Add a row to the playground in `site/app.js` if it is worth seeing + ### Code Style -- ES2022+ syntax (const/let, arrow functions, async/await) +- ES2022+ syntax (const/let, arrow functions, template literals) - ESM modules only +- **Zero runtime dependencies.** Dev dependencies only. - Add JSDoc comments for public functions -- Maintain test coverage +- Maintain test coverage (`npm run test:coverage`) +- Unicode output must render with macOS system fonts out of the box ## Code of Conduct diff --git a/README.md b/README.md index 63b6dac..a150dcb 100644 --- a/README.md +++ b/README.md @@ -11,17 +11,22 @@ > Number One Way to Makes Words from Numbers -Transform any number into beautiful words. From `42` to `"forty-two"`, from `1000000` to `"one million"`. Supports **22 languages**, ordinals, currency, Roman numerals, and more! +Transform any number into beautiful words. From `42` to `"forty-two"`, from `1000000` to `"one million"`. Supports **22 languages**, ordinals, currency, Roman numerals, Egyptian hieroglyphs, Babylonian cuneiform, circled digits, and more! + +**Try it:** [numberstring.netlify.app](https://numberstring.netlify.app) — type a number, see it in 22 languages. Runs entirely in your browser. ## Why numberstring? - **Zero dependencies** - Lightweight and fast - **22 languages** - English, Spanish, French, German, Danish, Chinese, Hindi, Russian, Portuguese, Japanese, Korean, Arabic, Italian, Dutch, Turkish, Polish, Swedish, Indonesian, Thai, Norwegian, Finnish, Icelandic - **Huge range** - Supports 0 to decillions (10^36) with BigInt -- **Feature-rich** - Ordinals, decimals, currency, fractions, years, phone numbers -- **Roman numerals** - Convert to and from Roman numerals -- **Well tested** - 265 tests with 90%+ coverage -- **Modern ES modules** - Tree-shakeable, TypeScript-friendly +- **Feature-rich** - Ordinals, decimals, currency, fractions, years, phone numbers, NATO/ICAO radio numerals, Morse code, scientific notation, binary/hex, byte sizes, clock faces +- **Roman numerals** - Classic and vinculum notation to 3,999,999,999 +- **Ancient and alternative numerals** - Egyptian hieroglyphs, Babylonian cuneiform, Greek letters, Chinese/Japanese financial forms +- **Unicode digit styles** - ④② ⁴² 42 𝟜𝟚 4️⃣2️⃣ ⠼⠙⠃ +- **Forgiving input** - Integers, negatives, decimals, numeric strings, BigInt. It just works +- **Well tested** - 780+ tests with 90%+ coverage, per-language spot checks, and a fuzz suite proving nothing ever throws +- **Modern ES modules** - Tree-shakeable, with bundled TypeScript declarations ## Installation @@ -37,23 +42,79 @@ import numberstring from 'numberstring'; numberstring(42); // 'forty-two' numberstring(1000000); // 'one million' numberstring(10n ** 18n); // 'one quintillion' (BigInt!) +numberstring(-3.14); // 'negative three point one four' +numberstring('1000'); // 'one thousand' +numberstring(42, { lang: 'es' }); // 'cuarenta y dos' +numberstring(123, { and: true }); // 'one hundred and twenty-three' numberstring(123, { cap: 'title' }); // 'One Hundred Twenty-Three' ``` ## API Reference +Everything at a glance. Every function returns a `string`, or `false` when the input cannot be converted. + +| Function | Example | Result | +|----------|---------|--------| +| `numberstring(n, opt)` | `numberstring(-3.14)` | `negative three point one four` | +| `toWords(n, { lang })` | `toWords(42, { lang: 'es' })` | `cuarenta y dos` | +| `ordinal(n)` | `ordinal(21)` | `twenty-first` | +| `nth(n)` | `nth(22)` | `22nd` | +| `decimal(n)` | `decimal(3.14)` | `three point one four` | +| `fraction(a, b)` | `fraction(3, 4)` | `three quarters` | +| `percent(n)` | `percent(50)` | `fifty percent` | +| `currency(s)` | `currency('$1.50')` | `one dollar and fifty cents` | +| `year(n)` | `year(1984)` | `nineteen eighty-four` | +| `telephone(s, { oh })` | `telephone(8675309, { oh: true })` | `eight six seven five three oh nine` | +| `nato(n)` / `icao` / `military` | `nato(1984)` | `wun niner ait fower` | +| `morse(n)` | `morse(42)` | `....- ..---` | +| `compact(n)` | `compact(1500000)` | `1.5M` | +| `scientific(n)` | `scientific(1984)` | `1.984 × 10³` | +| `comma(n)` | `comma(1234567)` | `1,234,567` | +| `binary(n)` / `octal` / `hex` / `radix(n, base)` | `hex(255, { prefix: true })` | `0xff` | +| `bytes(n)` / `bits(n)` | `bytes(1536)` | `1.5 KB` | +| `roman(n)` | `roman(1999)` | `MCMXCIX` | +| `greek(n)` | `greek(42)` | `μβʹ` | +| `egyptian(n)` | `egyptian(42)` | `𓎆𓎆𓎆𓎆𓏺𓏺` | +| `babylonian(n)` | `babylonian(42)` | `𒌋𒌋𒌋𒌋𒁹𒁹` | +| `fancy(n, style)` | `fancy(42, 'doublestruck')` | `𝟜𝟚` | +| `clock(time)` | `clock('3:30')` | `🕞` | +| `parse(words)` | `parse('forty-two')` | `42` | +| `negative(n)` | `negative(-42)` | `negative forty-two` | + +Constants: `CAP_STYLES` (casing names for `cap`), `FANCY_STYLE_NAMES` (styles for `fancy`). Each language is also a named export (`spanish`, `french`, ... see below). + + ### Core Functions #### `numberstring(n, [options])` -Convert a number to English words. +Convert a number to words. Forgiving by design: accepts integers, negatives, decimals, numeric strings, and BigInt, and honors `lang`. Returns `false` for anything it cannot say. ```javascript numberstring(42); // 'forty-two' +numberstring(-7); // 'negative seven' +numberstring(2.5); // 'two point five' +numberstring('1000000'); // 'one million' +numberstring(42, { lang: 'fr' }); // 'quarante-deux' numberstring(100, { cap: 'title' }); // 'One Hundred' numberstring(100, { punc: '!' }); // 'one hundred!' +numberstring('abc'); // false ``` +Every word-producing function takes `cap`, and it does more than capitalize: + +```javascript +numberstring(123, { cap: 'camel' }); // 'oneHundredTwentyThree' +numberstring(123, { cap: 'pascal' }); // 'OneHundredTwentyThree' +numberstring(123, { cap: 'snake' }); // 'one_hundred_twenty_three' +numberstring(123, { cap: 'kebab' }); // 'one-hundred-twenty-three' +numberstring(123, { cap: 'constant' }); // 'ONE_HUNDRED_TWENTY_THREE' +numberstring(123, { cap: 'dot' }); // 'one.hundred.twenty.three' +numberstring(123, { cap: 'sentence' }); // 'One hundred twenty-three' +``` + +Negatives and decimals are English-only; with another `lang` they return `false` rather than falling back to English. Pass `and: true` for British style ("one hundred and one", "one thousand and one"). + #### `ordinal(n, [options])` Convert to ordinal words (first, second, etc.). @@ -97,7 +158,7 @@ Supported currencies: `$` `€` `£` `¥` `₹` `元` (USD, EUR, GBP, JPY, INR, #### `roman(n, [options])` -Convert to Roman numerals. +Convert to Roman numerals. Above 3999, vinculum notation puts a bar over a group to multiply it by 1000 (two bars for a million), reaching 3,999,999,999. ```javascript import { roman } from 'numberstring'; @@ -105,6 +166,8 @@ import { roman } from 'numberstring'; roman(42); // 'XLII' roman(1999); // 'MCMXCIX' roman(4, { lower: true }); // 'iv' +roman(4000); // 'I̅V̅' +roman(8675309); // 'V̿I̿I̿I̿D̅C̅L̅X̅X̅V̅CCCIX' ``` #### `parse(str)` @@ -119,6 +182,70 @@ parse('one thousand'); // 1000 parse('one quintillion'); // 1000000000000000000n (BigInt) ``` +#### `nth(n)` + +Numeric ordinal suffix. + +```javascript +import { nth } from 'numberstring'; + +nth(1); // '1st' +nth(22); // '22nd' +nth(113); // '113th' +``` + +#### `compact(n, [options])` + +Compact notation. + +```javascript +import { compact } from 'numberstring'; + +compact(1500); // '1.5K' +compact(2300000000); // '2.3B' +compact(999950); // '1M' +compact(1234567, { digits: 2 }); // '1.23M' +compact(1500000, { long: true }); // '1.5 million' +``` + +#### `fancy(n, [style])` + +Digits in a Unicode style: `circled` (default), `superscript`, `subscript`, `fullwidth`, `bold`, `doublestruck`, `sans`, `monospace`, `keycap` (alias `emoji`), `clock`, `braille`. + +```javascript +import { fancy } from 'numberstring'; + +fancy(42); // '④②' +fancy(42, 'superscript'); // '⁴²' +fancy(42, 'doublestruck'); // '𝟜𝟚' +fancy(42, 'keycap'); // '4️⃣2️⃣' +fancy(814, 'clock'); // '🕗🕐🕓' +fancy(-3.5, 'braille'); // '⠼⠤⠉⠨⠑' +``` + +### Ancient and Alternative Numerals + +All render with Unicode glyphs, so they need a font that covers the block (most modern systems do). + +```javascript +import { egyptian, babylonian, greek } from 'numberstring'; + +egyptian(42); // '𓎆𓎆𓎆𓎆𓏺𓏺' additive, 1 to 9,999,999 +babylonian(42); // '𒌋𒌋𒌋𒌋𒁹𒁹' base 60, places separated by spaces +babylonian(3600); // '𒁹 𒑊 𒑊' +greek(42); // 'μβʹ' Ionic letters, 1 to 9999 +greek(1999); // '͵αϡϟθʹ' +``` + +Chinese and Japanese also have the anti-fraud financial forms used on cheques: + +```javascript +import { chinese, japanese } from 'numberstring'; + +chinese(1001, { formal: true }); // '壹仟零壹' (大写) +japanese(1001, { formal: true }); // '壱千壱' (大字) +``` + ### Utility Functions #### `negative(n, [options])` @@ -151,9 +278,10 @@ Convert years to spoken form. ```javascript import { year } from 'numberstring'; -year(1984); // 'nineteen eighty-four' -year(2000); // 'two thousand' -year(2024); // 'twenty twenty-four' +year(1984); // 'nineteen eighty-four' +year(2000); // 'two thousand' +year(2024); // 'twenty twenty-four' +year(8675309); // 'eight million six hundred seventy-five thousand three hundred nine' ``` #### `telephone(phone, [options])` @@ -163,8 +291,86 @@ Convert phone numbers to words. ```javascript import { telephone } from 'numberstring'; -telephone('555-1234'); // 'five five five one two three four' -telephone(8675309); // 'eight six seven five three zero nine' +telephone('555-1234'); // 'five five five one two three four' +telephone(8675309); // 'eight six seven five three zero nine' +telephone(8675309, { oh: true }); // 'eight six seven five three oh nine' +``` + +#### `nato(n, [options])` + +ICAO / NATO radiotelephony numerals, the way pilots and air traffic control read numbers. Also exported as `icao` and `military`. + +```javascript +import { nato } from 'numberstring'; + +nato(1984); // 'wun niner ait fower' +nato(2500); // 'too tousand fife hundred' +nato('121.5'); // 'wun too wun decimal fife' +nato(2500, { digits: true }); // 'too fife zero zero' +``` + +#### `morse(n)` + +International Morse code for the digits. + +```javascript +import { morse } from 'numberstring'; + +morse(42); // '....- ..---' +morse(3.1); // '...-- .-.-.- .----' +``` + +#### `scientific(n, [options])` + +Scientific notation with an exact decimal mantissa. Formats: `unicode` (default), `caret`, `e`, `words`. + +```javascript +import { scientific } from 'numberstring'; + +scientific(1984); // '1.984 × 10³' +scientific(0.00042); // '4.2 × 10⁻⁴' +scientific(1984, { format: 'e' }); // '1.984e3' +scientific(1984, { digits: 3 }); // '1.98 × 10³' +scientific(1984, { format: 'words' }); // 'one point nine eight four times ten to the third' +``` + +#### `binary(n)`, `octal(n)`, `hex(n)`, `radix(n, base)` + +Integers in other bases, 2 to 36. + +```javascript +import { binary, hex, radix } from 'numberstring'; + +binary(42); // '101010' +hex(255, { prefix: true, upper: true }); // '0xFF' +binary(5, { pad: 8 }); // '00000101' +radix(42, 36); // '16' +``` + +#### `bytes(n, [options])` and `bits(n, [options])` + +Human-readable data sizes. Bytes use `KB`/`KiB`, bits use bandwidth-style `kb`/`Mb`. + +```javascript +import { bytes, bits } from 'numberstring'; + +bytes(1536); // '1.5 KB' +bytes(1536, { binary: true }); // '1.5 KiB' +bytes(1536, { long: true }); // 'one point five kilobytes' +bits(1500000); // '1.5 Mb' +bits(1500000, { long: true }); // 'one point five megabits' +``` + +#### `clock(time)` + +Clock-face emoji for an hour or an `H:MM` time, rounded to the half hour. + +```javascript +import { clock } from 'numberstring'; + +clock(3); // '🕒' +clock('3:30'); // '🕞' +clock(15); // '🕒' ``` #### `percent(pct, [options])` @@ -186,7 +392,8 @@ Format a number with comma separators. ```javascript import { comma } from 'numberstring'; -comma(1234567); // '1,234,567' +comma(1234567); // '1,234,567' +comma(1234567.89); // '1,234,567.89' ``` ## Multi-Language Support @@ -247,23 +454,37 @@ toWords(42, { lang: 'is' }); // 'fjörutíu og tveir' | `fi` | Finnish | neljäkymmentäkaksi | | `is` | Icelandic | fjörutíu og tveir | +### Direct language exports + +Each language is also exported by name for tree-shaking: `spanish`, `french`, `german`, `danish`, `chinese`, `hindi`, `russian`, `portuguese`, `japanese`, `korean`, `arabic`, `italian`, `dutch`, `turkish`, `polish`, `swedish`, `indonesian`, `thai`, `norwegian`, `finnish`, `icelandic`. They take a non-negative integer and return the cardinal words. Use `toWords()` or `numberstring()` with `lang` when you want `cap` and the other options. + +```javascript +import { french, japanese } from 'numberstring'; + +french(1984); // 'mille neuf cent quatre-vingt-quatre' +japanese(1984, { formal: true }); // '壱千九百八拾四' +``` + ### Adding a New Language Languages are modular! To add a new language: 1. Create `languages/xx.js` following the pattern in `languages/en.js` 2. Export your conversion function -3. Add to `languages/index.js` -4. Submit a PR! +3. Add to `languages/index.js`, re-export it from `index.js`, and add it to the `toWords` switch +4. Add a row to the table in `test/languages.test.js` and a named export in `index.d.ts` +5. Submit a PR! ## Options | Option | Type | Description | |--------|------|-------------| -| `cap` | `string` | Capitalization: `'title'`, `'upper'`, or `'lower'` | +| `cap` | `string` | Casing: `'title'`, `'upper'`, `'lower'`, `'sentence'`, `'camel'`, `'pascal'`, `'snake'`, `'kebab'`, `'constant'`, `'dot'` | | `punc` | `string` | Punctuation: `'!'`, `'?'`, or `'.'` | -| `lang` | `string` | Language code for `toWords()` | +| `lang` | `string` | Language code for `numberstring()` and `toWords()` | | `point` | `string` | Word for decimal point (default: `'point'`) | +| `and` | `boolean` | British style: `one hundred and one` | +| `formal` | `boolean` | Chinese/Japanese financial numerals | | `lower` | `boolean` | Lowercase Roman numerals | ## Supported Scales @@ -283,40 +504,15 @@ Languages are modular! To add a new language: | **Nonillions** | 10^30 | `five nonillion` *(BigInt)* | | **Decillions** | 10^33 | `five decillion` *(BigInt)* | -## REST API Server +## Playground -numberstring includes a ready-to-use REST API server! +The playground at [numberstring.netlify.app](https://numberstring.netlify.app) is a single static page in `site/` that imports the library directly. No framework, no backend. ```bash -cd server -npm install -npm start -# Server running at http://localhost:3456 +npm run site # stage the library into site/lib and serve at http://localhost:8080 ``` -### Endpoints - -| Endpoint | Description | Example | -|----------|-------------|---------| -| `GET /convert/:n` | Number to words | `/convert/42` → `"forty-two"` | -| `GET /convert/:n?lang=es` | Multi-language | `/convert/42?lang=es` → `"cuarenta y dos"` | -| `GET /ordinal/:n` | Ordinal words | `/ordinal/3` → `"third"` | -| `GET /decimal/:n` | Decimal words | `/decimal/3.14` → `"three point one four"` | -| `GET /currency/:amt` | Currency words | `/currency/$99.99` → `"ninety-nine dollars..."` | -| `GET /roman/:n` | Roman numerals | `/roman/2024` → `"MMXXIV"` | -| `GET /parse/:words` | Words to number | `/parse/forty-two` → `42` | -| `GET /comma/:n` | Format with commas | `/comma/1000000` → `"1,000,000"` | -| `GET /languages` | List languages | Returns supported language codes | - -### Example - -```bash -curl http://localhost:3456/convert/1000000000000000 -# {"input":"1000000000000000","output":"one quadrillion","lang":"en"} - -curl "http://localhost:3456/convert/42?lang=fr" -# {"input":"42","output":"quarante-deux","lang":"fr"} -``` +The former Express REST server lives in `archive/server/`, unmaintained. ## Development @@ -325,6 +521,7 @@ npm install # Install dependencies npm test # Run tests npm run lint # Run linter npm run test:coverage # Test with coverage +npm run site # Run the playground locally ``` ## Contributing diff --git a/archive/README.md b/archive/README.md new file mode 100644 index 0000000..b8138ae --- /dev/null +++ b/archive/README.md @@ -0,0 +1,10 @@ +# Archive + +Code kept for reference but no longer maintained, tested, linted, or published. + +## server/ + +An Express REST API wrapping numberstring. Never deployed. Superseded by the +static playground in `site/`, which runs the library directly in the browser +with no backend. If a hosted API is ever wanted, these handlers port directly +to Netlify Functions. Slated for deletion. diff --git a/server/index.js b/archive/server/index.js similarity index 100% rename from server/index.js rename to archive/server/index.js diff --git a/server/index.test.js b/archive/server/index.test.js similarity index 100% rename from server/index.test.js rename to archive/server/index.test.js diff --git a/server/package.json b/archive/server/package.json similarity index 100% rename from server/package.json rename to archive/server/package.json diff --git a/eslint.config.js b/eslint.config.js index 759fb14..65772b4 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -17,6 +17,10 @@ export default [ } }, { - ignores: ['node_modules/', 'coverage/'] + files: ['site/**/*.js'], + languageOptions: { globals: { document: 'readonly', window: 'readonly', location: 'readonly', history: 'readonly' } } + }, + { + ignores: ['node_modules/', 'coverage/', 'archive/', 'site/lib/'] } ]; diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..62aae43 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,256 @@ +/** + * Type declarations for numberstring + */ + +/** Capitalization styles */ +export type CapStyle = + | 'title' | 'upper' | 'lower' | 'sentence' + | 'camel' | 'pascal' | 'snake' | 'kebab' | 'hyphen' | 'constant' | 'screaming' | 'dot'; + +/** The casing styles the `cap` option accepts */ +export const CAP_STYLES: readonly CapStyle[]; + +/** Trailing punctuation */ +export type Punc = '!' | '?' | '.'; + +/** Language codes and aliases accepted by `lang` */ +export type Lang = + | 'en' | 'english' + | 'es' | 'spanish' | 'español' + | 'fr' | 'french' | 'français' + | 'de' | 'german' | 'deutsch' + | 'da' | 'danish' | 'dansk' + | 'zh' | 'chinese' | 'mandarin' | '中文' + | 'hi' | 'hindi' | 'हिन्दी' + | 'ru' | 'russian' | 'русский' + | 'pt' | 'portuguese' | 'português' + | 'ja' | 'japanese' | '日本語' + | 'ko' | 'korean' | '한국어' + | 'ar' | 'arabic' | 'العربية' + | 'it' | 'italian' | 'italiano' + | 'nl' | 'dutch' | 'nederlands' + | 'tr' | 'turkish' | 'türkçe' + | 'pl' | 'polish' | 'polski' + | 'sv' | 'swedish' | 'svenska' + | 'id' | 'indonesian' | 'bahasa' | 'bahasa indonesia' + | 'th' | 'thai' | 'ไทย' + | 'no' | 'norwegian' | 'norsk' + | 'fi' | 'finnish' | 'suomi' + | 'is' | 'icelandic' | 'íslenska' + | (string & {}); + +export interface Options { + /** Casing: title, upper, lower, sentence, camel, pascal, snake, kebab, constant, dot */ + cap?: CapStyle; + /** Trailing punctuation: '!', '?', or '.' */ + punc?: Punc; + /** Language code (default 'en'). Non-English languages cover non-negative integers only. */ + lang?: Lang; + /** Word for the decimal point (default 'point') */ + point?: string; + /** British style: 'one hundred and twenty-three', 'one thousand and one' */ + and?: boolean; + /** Chinese/Japanese only: financial 大写 / 大字 numerals (壹贰叁, 壱弐参) */ + formal?: boolean; +} + +export interface CompactOptions { + /** Maximum decimal places (default 1, max 6) */ + digits?: number; + /** Spell the scale word: '1.5 million' instead of '1.5M' */ + long?: boolean; +} + + +/** Unicode digit styles accepted by fancy() */ +export type FancyStyle = + | 'circled' | 'superscript' | 'subscript' | 'fullwidth' | 'bold' + | 'doublestruck' | 'sans' | 'monospace' | 'keycap' | 'emoji' | 'clock' | 'braille'; + +export interface ScientificOptions extends Pick { + /** Maximum significant digits, rounds half up (default 12) */ + digits?: number; + /** 'unicode' (1.984 × 10³), 'caret' (1.984 × 10^3), 'e' (1.984e3), or 'words' */ + format?: 'unicode' | 'caret' | 'e' | 'words'; +} + +export interface RadixOptions { + /** Add 0b / 0o / 0x for bases 2, 8, 16 */ + prefix?: boolean; + /** Uppercase letter digits */ + upper?: boolean; + /** Left-pad with zeros to this many digits */ + pad?: number; +} + +export interface BytesOptions { + /** Use 1024 steps and KiB/MiB units */ + binary?: boolean; + /** Maximum decimal places (default 1) */ + digits?: number; + /** Spell it out: 'one point five kilobytes' / 'one point five megabits' */ + long?: boolean; +} + +export interface CurrencyOptions extends Pick { + /** Currency symbol or ISO code when the amount has none: '$', 'USD', '€', 'EUR', '£', 'GBP', '¥', 'JPY', '₹', 'INR', '元', 'CNY' */ + currency?: string; +} + +export interface RomanOptions { + /** Return lowercase numerals */ + lower?: boolean; +} + +/** A conversion result, or `false` when the input cannot be converted */ +export type Result = string | false; + +/** Any value numberstring will try to convert */ +export type Numeric = number | bigint | string; + +/** + * Convert a number to words. Accepts integers, negatives, decimals, + * numeric strings, and BigInt up to 10^36 - 1. Honors `lang`. + * + * @example + * numberstring(42) // 'forty-two' + * numberstring(-3.14) // 'negative three point one four' + * numberstring('1000') // 'one thousand' + * numberstring(42, { lang: 'es' }) // 'cuarenta y dos' + */ +declare function numberstring(n: Numeric, opt?: Options): Result; +export default numberstring; + +/** Convert to words in any supported language (non-negative integers) */ +export function toWords(n: number | bigint, opt?: Pick): Result; + +/** Ordinal words: 1 → 'first', 21 → 'twenty-first' */ +export function ordinal(n: number | bigint, opt?: Pick): Result; + +/** Numeric ordinal suffix: 1 → '1st', 22 → '22nd', 113 → '113th' */ +export function nth(n: Numeric): string | false; + +/** Compact notation: 1500 → '1.5K', 2300000000 → '2.3B' */ +export function compact(n: Numeric, opt?: CompactOptions): string | false; + +/** Digits in a Unicode style: fancy(42) → '④②', fancy(42, 'superscript') → '⁴²' */ +export function fancy(n: Numeric, style?: FancyStyle): string | false; + +/** The style names fancy() accepts */ +export const FANCY_STYLE_NAMES: readonly FancyStyle[]; + +/** Egyptian hieroglyphic numerals, 1 to 9,999,999 */ +export function egyptian(n: Numeric): string | false; + +/** Babylonian base-60 cuneiform numerals */ +export function babylonian(n: Numeric): string | false; + +/** Greek Ionic alphabetic numerals, 1 to 9999 */ +export function greek(n: Numeric): string | false; + + + +/** Decimal words: 3.14 → 'three point one four' */ +export function decimal(n: number | string, opt?: Pick): Result; + +/** Currency words: '$123.45' → 'one hundred twenty-three dollars and forty-five cents' */ +export function currency(amount: number | string, opt?: CurrencyOptions): Result; + +/** Roman numerals for 1–3,999,999,999: 42 → 'XLII'; above 3999 uses vinculum bars (4000 → 'I̅V̅') */ +export function roman(n: number, opt?: RomanOptions): Result; + +/** Parse English words back to a number: 'forty-two' → 42. Returns BigInt above the safe integer range. */ +export function parse(str: string): number | bigint | false; + +/** Signed words: -42 → 'negative forty-two' */ +export function negative(n: number | bigint, opt?: Pick): Result; + +/** Fraction words: (1, 2) → 'one half', (3, 4) → 'three quarters' */ +export function fraction(numerator: number, denominator: number, opt?: Pick): Result; + +/** Year as spoken: 1984 → 'nineteen eighty-four'; beyond 9999 reads as a cardinal */ +export function year(y: number | bigint, opt?: Pick): Result; + +export interface TelephoneOptions extends Pick { + /** Say 'oh' instead of 'zero' */ + oh?: boolean; +} + +/** Digits read individually: '555-1234' → 'five five five one two three four' */ +export function telephone(phone: number | string, opt?: TelephoneOptions): Result; + +export interface NatoOptions extends Pick { + /** Always read digit by digit, even round hundreds and thousands */ + digits?: boolean; +} + +/** ICAO / NATO radiotelephony numerals: 1984 → 'wun niner ait fower', 2500 → 'too tousand fife hundred' */ +export function nato(n: Numeric, opt?: NatoOptions): Result; +/** Alias of nato() */ +export function icao(n: Numeric, opt?: NatoOptions): Result; +/** Alias of nato() */ +export function military(n: Numeric, opt?: NatoOptions): Result; + +/** International Morse code digits: 42 → '....- ..---' */ +export function morse(n: Numeric): string | false; + +/** Scientific notation with an exact mantissa: 1984 → '1.984 × 10³' */ +export function scientific(n: Numeric, opt?: ScientificOptions): string | false; + +/** Integer in another base, 2 to 36: radix(42, 16) → '2a' */ +export function radix(n: Numeric, base?: number, opt?: RadixOptions): string | false; +/** Binary: 42 → '101010' */ +export function binary(n: Numeric, opt?: RadixOptions): string | false; +/** Octal: 42 → '52' */ +export function octal(n: Numeric, opt?: RadixOptions): string | false; +/** Hexadecimal: 42 → '2a' */ +export function hex(n: Numeric, opt?: RadixOptions): string | false; + +/** Human-readable byte sizes: 1536 → '1.5 KB' */ +export function bytes(n: Numeric, opt?: BytesOptions): string | false; + +/** Human-readable bit counts: 1500000 → '1.5 Mb' */ +export function bits(n: Numeric, opt?: BytesOptions): string | false; + +/** Clock-face emoji for an hour (0-24) or 'H:MM': clock('3:30') → '🕞' */ +export function clock(time: number | string): string | false; + +/** Percent words: 50 → 'fifty percent' */ +export function percent(pct: number | string, opt?: Pick): Result; + +/** Thousands separators: 1234567 → '1,234,567' */ +export function comma(n: number | bigint): string | false; + +/** Magnitude group: 0 = ones, 1 = thousands, 2 = millions, ... */ +export function group(n: number | bigint): number; + +/** A single-language converter for non-negative integers. Use toWords() for `cap`. */ +export type LanguageConverter = (n: number | bigint) => Result; + +/** Spanish and Portuguese also accept `cap` directly */ +export type LanguageConverterWithCap = (n: number | bigint, opt?: Pick) => Result; + +/** Chinese and Japanese accept `formal` for 大写 / 大字 numerals */ +export type LanguageConverterWithFormal = (n: number | bigint, opt?: Pick) => Result; + +export const spanish: LanguageConverterWithCap; +export const french: LanguageConverter; +export const german: LanguageConverter; +export const danish: LanguageConverter; +export const chinese: LanguageConverterWithFormal; +export const hindi: LanguageConverter; +export const russian: LanguageConverter; +export const portuguese: LanguageConverterWithCap; +export const japanese: LanguageConverterWithFormal; +export const korean: LanguageConverter; +export const arabic: LanguageConverter; +export const italian: LanguageConverter; +export const dutch: LanguageConverter; +export const turkish: LanguageConverter; +export const polish: LanguageConverter; +export const swedish: LanguageConverter; +export const indonesian: LanguageConverter; +export const thai: LanguageConverter; +export const norwegian: LanguageConverter; +export const finnish: LanguageConverter; +export const icelandic: LanguageConverter; diff --git a/index.js b/index.js index e07ac47..6536f5b 100644 --- a/index.js +++ b/index.js @@ -21,8 +21,12 @@ // Import language functions import { english, spanish, french, german, danish, chinese, hindi, russian, portuguese, japanese, korean, arabic, italian, dutch, turkish, polish, swedish, indonesian, thai, norwegian, finnish, icelandic, LANGUAGES } from './languages/index.js'; -// Re-export language functions +import { fancy, FANCY_STYLE_NAMES, egyptian, babylonian, greek, clock } from './numerals.js'; + +// Re-export language functions and alternative numeral systems export { spanish, french, german, danish, chinese, hindi, russian, portuguese, japanese, korean, arabic, italian, dutch, turkish, polish, swedish, indonesian, thai, norwegian, finnish, icelandic }; +export { fancy, FANCY_STYLE_NAMES, egyptian, babylonian, greek, clock }; +export { CAP_STYLES }; // ============================================================================ // CONSTANTS @@ -93,7 +97,11 @@ const MAX_VALUE = 10n ** 36n - 1n; // HELPER FUNCTIONS // ============================================================================ -const group = (n) => Math.ceil(n.toString().length / 3) - 1; +const group = (n) => { + if (typeof n !== 'number' && typeof n !== 'bigint') return false; + if (typeof n === 'number' && !Number.isFinite(n)) return false; + return Math.ceil((n < 0 ? -n : n).toString().length / 3) - 1; +}; const power = (g) => 10n ** BigInt(g * 3); const segment = (n, g) => n % power(g + 1); const hundment = (n, g) => Number(segment(n, g) / power(g)); @@ -113,7 +121,18 @@ const ten = (n) => { return `${TENS[Math.floor(n / 10)]} `; }; +/** Casing styles accepted by the `cap` option */ +const CAP_STYLES = Object.freeze(['title', 'upper', 'lower', 'sentence', 'camel', 'pascal', 'snake', 'kebab', 'hyphen', 'constant', 'screaming', 'dot']); + +const capFirst = (w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase(); + +/** + * Apply a casing style. Word-joining styles split on spaces and hyphens: + * 'one hundred twenty-three' → camel 'oneHundredTwentyThree', snake + * 'one_hundred_twenty_three', kebab 'one-hundred-twenty-three'. + */ const cap = (str, style) => { + const words = () => str.split(/[\s-]+/).filter(Boolean); switch (style) { case 'title': return str.replace(/\w([^-\s]*)/g, (txt) => @@ -123,6 +142,22 @@ const cap = (str, style) => { return str.toUpperCase(); case 'lower': return str.toLowerCase(); + case 'sentence': + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); + case 'camel': + return words().map((w, i) => (i === 0 ? w.toLowerCase() : capFirst(w))).join(''); + case 'pascal': + return words().map(capFirst).join(''); + case 'snake': + return words().map((w) => w.toLowerCase()).join('_'); + case 'kebab': + case 'hyphen': + return words().map((w) => w.toLowerCase()).join('-'); + case 'constant': + case 'screaming': + return words().map((w) => w.toUpperCase()).join('_'); + case 'dot': + return words().map((w) => w.toLowerCase()).join('.'); default: return str; } @@ -138,8 +173,18 @@ const comma = (n) => { if (typeof n === 'bigint') { return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); } - if (isNaN(n)) return false; - return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + if (typeof n === 'number') { + if (!Number.isFinite(n)) return false; + const [intPart, fracPart] = n.toString().split('.'); + const grouped = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ','); + return fracPart === undefined ? grouped : `${grouped}.${fracPart}`; + } + if (typeof n === 'string' && /^-?\d+(\.\d+)?$/.test(n.trim())) { + const [intPart, fracPart] = n.trim().split('.'); + const grouped = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ','); + return fracPart === undefined ? grouped : `${grouped}.${fracPart}`; + } + return false; }; // ============================================================================ @@ -147,14 +192,14 @@ const comma = (n) => { // ============================================================================ /** - * Convert a number or BigInt to its English word representation + * Convert a non-negative integer (number or BigInt) to English words. + * Internal: the public `string()` normalizes input and delegates here. * @param {number|bigint} n - The number to convert (0 to 10^36-1) * @param {Object} [opt] - Options object - * @param {string} [opt.cap] - Capitalization: 'title', 'upper', or 'lower' - * @param {string} [opt.punc] - Punctuation: '!', '?', or '.' + * @param {boolean} [opt.and] - British "and" after hundreds / before a final group under 100 * @returns {string|false} The word representation or false if invalid */ -const string = (n, opt) => { +const cardinal = (n, opt) => { let num; if (typeof n === 'bigint') { @@ -170,15 +215,20 @@ const string = (n, opt) => { let s = ''; + const useAnd = opt?.and === true; + if (num === 0n) { s = 'zero'; } else { for (let i = group(num); i >= 0; i--) { - s += hundred(hundment(num, i)); - s += ten(tenment(num, i)); - if (hundment(num, i) > 0) { - s += `${ILLIONS[i]} `; - } + const h = hundment(num, i); + if (h === 0) continue; + const t = tenment(num, i); + s += hundred(h); + // British style: "one hundred and one", "one thousand and one" + if (useAnd && t > 0 && (h >= 100 || (i === 0 && s))) s += 'and '; + s += ten(t); + s += `${ILLIONS[i]} `; } } @@ -192,6 +242,72 @@ const string = (n, opt) => { return s; }; +/** + * Convert a number to its word representation. + * + * Forgiving by design: accepts integers, negatives, decimals, numeric strings, + * and BigInt, and honors `opt.lang` for any of the 22 supported languages. + * Invalid input returns `false`. + * + * @param {number|bigint|string} n - The number to convert + * @param {Object} [opt] - Options object + * @param {string} [opt.cap] - Casing: 'title', 'upper', 'lower', 'sentence', 'camel', 'pascal', 'snake', 'kebab', 'constant', 'dot' + * @param {string} [opt.punc] - Punctuation: '!', '?', or '.' + * @param {string} [opt.lang] - Language code (default 'en') + * @param {string} [opt.point] - Word for the decimal point (default 'point') + * @param {boolean} [opt.and] - British style: 'one hundred and twenty-three' + * @returns {string|false} The word representation or false if invalid + * + * @example + * numberstring(42) // 'forty-two' + * numberstring(123, { and: true }) // 'one hundred and twenty-three' + * numberstring(-5) // 'negative five' + * numberstring(3.14) // 'three point one four' + * numberstring('1000') // 'one thousand' + * numberstring(42, { lang: 'es' }) // 'cuarenta y dos' + */ +const string = (n, opt) => { + let value = n; + // Delegates apply `cap` themselves; `punc` is applied once in finish() + const inner = opt ? { ...opt, punc: undefined } : opt; + const lang = typeof opt?.lang === 'string' ? opt.lang.toLowerCase() : undefined; + const foreign = Boolean(lang && LANGUAGES[lang] && LANGUAGES[lang] !== 'english'); + + if (typeof value === 'string') { + const str = value.trim(); + if (!/^-?\d+(\.\d+)?$/.test(str)) return false; + // Other languages only cover non-negative integers; don't fall back to English + if (str.includes('.')) return foreign ? false : finish(decimal(str, inner), opt); + const digits = str.replace('-', ''); + value = digits.length <= 15 ? Number(str) : BigInt(str); + } + + if (typeof value === 'number') { + if (!Number.isFinite(value)) return false; + if (!Number.isInteger(value)) return foreign ? false : finish(decimal(value, inner), opt); + // 1e21 is an integer too; widen to BigInt rather than rejecting it + if (Math.abs(value) > Number.MAX_SAFE_INTEGER) value = BigInt(value); + } + + if (typeof value === 'number') { + if (value < 0) return foreign ? false : finish(negative(value, inner), opt); + } else if (typeof value === 'bigint') { + if (value < 0n) return foreign ? false : finish(negative(value, inner), opt); + } else { + return false; + } + + if (foreign) return finish(toWords(value, inner), opt); + return cardinal(value, opt); +}; + +/** Apply `punc` to a delegated result (helpers already apply `cap`). */ +const finish = (result, opt) => { + if (result === false) return false; + if (opt?.punc !== undefined) return punc(result, opt.punc); + return result; +}; + // ============================================================================ // ORDINAL FUNCTION // ============================================================================ @@ -226,10 +342,10 @@ const ordinal = (n, opt) => { s = `${TENS[tensDigit]}-${ORDINAL_ONES[onesDigit]}`; } } else { - const cardinal = string(num); - if (!cardinal) return false; + const base = cardinal(num, { and: opt?.and }); + if (!base) return false; - const parts = cardinal.split(' '); + const parts = base.split(' '); const lastPart = parts[parts.length - 1]; const hyphenParts = lastPart.split('-'); const lastWord = hyphenParts[hyphenParts.length - 1]; @@ -272,8 +388,12 @@ const decimal = (n, opt) => { let numStr; if (typeof n === 'number') { - if (isNaN(n)) return false; + if (!Number.isFinite(n)) return false; numStr = n.toString(); + if (numStr.includes('e')) { + if (!Number.isInteger(n)) return false; + numStr = BigInt(n).toString(); + } } else if (typeof n === 'string') { numStr = n.trim(); if (!/^-?\d+\.?\d*$/.test(numStr) && !/^-?\d*\.?\d+$/.test(numStr)) { @@ -289,8 +409,9 @@ const decimal = (n, opt) => { const pointWord = opt?.point || 'point'; const [intPart, decPart] = numStr.split('.'); - const intNum = parseInt(intPart || '0', 10); - const intWords = string(intNum); + const intDigits = intPart || '0'; + const intNum = intDigits.length <= 15 ? parseInt(intDigits, 10) : BigInt(intDigits); + const intWords = cardinal(intNum, { and: opt?.and }); if (intWords === false) return false; let result = isNegative ? 'negative ' : ''; @@ -378,19 +499,52 @@ const currency = (amount, opt) => { // ROMAN NUMERAL FUNCTION // ============================================================================ -const roman = (n, opt) => { - if (typeof n !== 'number' || isNaN(n) || !Number.isInteger(n)) return false; - if (n < 1 || n > 3999) return false; - +/** Classic Roman numerals for 1-3999 */ +const romanBase = (n) => { let result = ''; let remaining = n; - for (const [value, numeral] of ROMAN_VALUES) { while (remaining >= value) { result += numeral; remaining -= value; } } + return result; +}; + +/** Combining marks for vinculum notation: one bar = x1000, two bars = x1000000 */ +const ROMAN_BARS = Object.freeze(['', '\u0305', '\u033F']); + +/** Maximum value expressible with a double vinculum (3,999,999,999) */ +const ROMAN_MAX = 3999999999; + +/** + * Convert to Roman numerals. Classic numerals cover 1-3999; above that, + * vinculum notation places a bar over a group to multiply it by 1000, so + * 4000 is I̅V̅ and 8675309 is V̿I̿I̿I̿D̅C̅L̅X̅X̅V̅CCCIX. + * @param {number} n - Integer from 1 to 3,999,999,999 + * @param {Object} [opt] - Options object + * @param {boolean} [opt.lower] - Return lowercase numerals + * @returns {string|false} The Roman numeral or false if out of range + */ +const roman = (n, opt) => { + if (typeof n !== 'number' || isNaN(n) || !Number.isInteger(n)) return false; + if (n < 1 || n > ROMAN_MAX) return false; + + let result = ''; + let remaining = n; + let level = 0; + + while (remaining > 0) { + // The topmost group keeps the classic form up to 3999; lower groups are 0-999 + const groupValue = remaining < 4000 ? remaining : remaining % 1000; + const bar = ROMAN_BARS[level]; + const letters = romanBase(groupValue); + const barred = bar ? [...letters].map((ch) => ch + bar).join('') : letters; + result = barred + result; + remaining = (remaining - groupValue) / 1000; + level++; + } return opt?.lower ? result.toLowerCase() : result; }; @@ -419,22 +573,30 @@ const parse = (str) => { let result = useBigInt ? 0n : 0; let current = useBigInt ? 0n : 0; + let lastSimple = null; for (const word of words) { if (Object.hasOwn(WORD_VALUES, word)) { const val = WORD_VALUES[word]; + // Only "tens + ones" (twenty two) may follow a simple word directly; + // "nineteen eighty" is a year, not a cardinal + const tensThenOnes = lastSimple >= 20 && lastSimple % 10 === 0 && val >= 1 && val <= 9; + if (lastSimple !== null && !tensThenOnes) return false; + lastSimple = val; if (useBigInt) { current += BigInt(val); } else { current += val; } } else if (word === 'hundred') { + lastSimple = null; if (useBigInt) { current *= 100n; } else { current *= 100; } } else if (Object.hasOwn(SCALE_VALUES, word)) { + lastSimple = null; const scale = SCALE_VALUES[word]; if (useBigInt) { const bigScale = typeof scale === 'bigint' ? scale : BigInt(scale); @@ -458,27 +620,436 @@ const parse = (str) => { }; // ============================================================================ -// ADDITIONAL UTILITY FUNCTIONS +// NTH (numeric ordinal suffix) // ============================================================================ -const negative = (n, opt) => { - if (typeof n === 'bigint') { - if (n >= 0n) return string(n, opt); - const result = string(-n, opt); - if (result === false) return false; - let s = `negative ${result}`; - if (opt?.cap) s = cap(s, opt.cap); - return s; +/** + * Append the English ordinal suffix to a number: 1st, 2nd, 3rd, 4th, 11th, 112th. + * @param {number|bigint|string} n - Integer (negatives keep their sign) + * @returns {string|false} + * + * @example + * nth(1) // '1st' + * nth(22) // '22nd' + * nth(113) // '113th' + */ +const nth = (n) => { + let value; + if (typeof n === 'bigint') value = n; + else if (typeof n === 'number' && Number.isInteger(n) && Math.abs(n) <= Number.MAX_SAFE_INTEGER) value = BigInt(n); + else if (typeof n === 'string' && /^-?\d+$/.test(n.trim())) value = BigInt(n.trim()); + else return false; + + const abs = value < 0n ? -value : value; + const mod100 = Number(abs % 100n); + const mod10 = Number(abs % 10n); + let suffix = 'th'; + if (mod100 < 11 || mod100 > 13) { + if (mod10 === 1) suffix = 'st'; + else if (mod10 === 2) suffix = 'nd'; + else if (mod10 === 3) suffix = 'rd'; } + return `${value}${suffix}`; +}; + +// ============================================================================ +// COMPACT (1.5K, 2.3M) +// ============================================================================ + +/** Short suffixes aligned with ILLIONS: thousand, million, billion, ... */ +const COMPACT_SUFFIXES = Object.freeze(['', 'K', 'M', 'B', 'T', 'Qa', 'Qi', 'Sx', 'Sp', 'Oc', 'No', 'Dc']); - if (typeof n !== 'number' || isNaN(n)) return false; +/** + * Compact notation: 1500 → '1.5K', 2300000 → '2.3M'. + * @param {number|bigint|string} n - The number + * @param {Object} [opt] - Options object + * @param {number} [opt.digits=1] - Maximum decimal places + * @param {boolean} [opt.long] - Spell the scale: '1.5 thousand' + * @returns {string|false} + * + * @example + * compact(1500) // '1.5K' + * compact(2300000000) // '2.3B' + * compact(999950) // '1M' + * compact(1500000, { long: true }) // '1.5 million' + */ +const compact = (n, opt) => { + let str; + if (typeof n === 'bigint') str = n.toString(); + else if (typeof n === 'number') { + if (!Number.isFinite(n)) return false; + str = Number.isInteger(n) ? BigInt(n).toString() : n.toString(); + if (str.includes('e')) return false; + } else if (typeof n === 'string' && /^-?\d+(\.\d+)?$/.test(n.trim())) str = n.trim(); + else return false; + + const negative = str.startsWith('-'); + if (negative) str = str.slice(1); + const [rawInt, fracPart = ''] = str.split('.'); + // Leading zeros carry no magnitude: '0001000' is 1000 + const intPart = rawInt.replace(/^0+(?=\d)/, ''); + const digits = Number.isFinite(opt?.digits) ? Math.max(0, Math.min(Math.trunc(opt.digits), 6)) : 1; + + if (intPart.length < 4) { + const small = Number(`${intPart}.${fracPart || '0'}`); + const rounded = Number(small.toFixed(digits)); + // 999.99 rounds up to 1000, which belongs in the next scale + if (rounded < 1000) return `${negative ? '-' : ''}${rounded}`; + return `${negative ? '-' : ''}1${opt?.long ? ` ${ILLIONS[1]}` : COMPACT_SUFFIXES[1]}`; + } - if (n >= 0) return string(n, opt); + let g = Math.floor((intPart.length - 1) / 3); + if (g >= COMPACT_SUFFIXES.length) return false; + // Value / 10^(3g) as a float; precision loss is irrelevant at <= 6 decimals + const scaled = Number(`${intPart.slice(0, intPart.length - 3 * g)}.${intPart.slice(intPart.length - 3 * g)}${fracPart}`); + let value = Number(scaled.toFixed(digits)); + // Rounding can carry into the next scale; at the last scale keep 1000Dc + if (value >= 1000 && g + 1 < COMPACT_SUFFIXES.length) { + g++; + value = Number((value / 1000).toFixed(digits)); + } - const result = string(Math.abs(n), opt); + const scale = opt?.long ? ` ${ILLIONS[g]}` : COMPACT_SUFFIXES[g]; + return `${negative ? '-' : ''}${value}${scale}`; +}; + +// ============================================================================ +// NATO / ICAO PHONETIC NUMERALS +// ============================================================================ + +/** ICAO radiotelephony pronunciations for the digits */ +const NATO_DIGITS = Object.freeze(['zero', 'wun', 'too', 'tree', 'fower', 'fife', 'six', 'seven', 'ait', 'niner']); + +/** + * NATO / ICAO radiotelephony numerals. Digits are read one at a time + * (1984 → 'wun niner ait fower'); per ICAO, whole hundreds and thousands + * are read with 'hundred' and 'tousand' (2500 → 'too tousand fife hundred'). + * A decimal point is 'decimal', a negative sign 'minus'. + * @param {number|bigint|string} n - The number + * @param {Object} [opt] - Options object + * @param {boolean} [opt.digits] - Always read digit by digit, even round numbers + * @param {string} [opt.cap] - Capitalization: 'title', 'upper', or 'lower' + * @returns {string|false} + * + * @example + * nato(1984) // 'wun niner ait fower' + * nato(2500) // 'too tousand fife hundred' + * nato(3.14) // 'tree decimal wun fower' + */ +const nato = (n, opt) => { + let str; + if (typeof n === 'bigint') str = n.toString(); + else if (typeof n === 'number') { + if (!Number.isFinite(n)) return false; + str = Number.isInteger(n) ? BigInt(n).toString() : n.toString(); + if (str.includes('e')) return false; + } else if (typeof n === 'string' && /^-?\d+(\.\d+)?$/.test(n.trim())) str = n.trim(); + else return false; + + const words = []; + if (str.startsWith('-')) { + words.push('minus'); + str = str.slice(1); + } + const [intPart, fracPart] = str.split('.'); + const spell = (digits) => [...digits].map((d) => NATO_DIGITS[Number(d)]); + + // Round hundreds / thousands: "fife hundred", "wun tousand", "too fife tousand" + const roundMatch = !opt?.digits && !fracPart && intPart.match(/^(\d{1,2})(\d?)(00)$/); + if (roundMatch && !intPart.startsWith('0') && intPart.length >= 3 && intPart.length <= 5) { + const thousands = intPart.slice(0, -3); + const hundredsDigit = intPart.slice(-3, -2); + if (thousands) words.push(...spell(thousands), 'tousand'); + if (hundredsDigit !== '0') words.push(NATO_DIGITS[Number(hundredsDigit)], 'hundred'); + } else { + words.push(...spell(intPart)); + if (fracPart) words.push('decimal', ...spell(fracPart)); + } + + let result = words.join(' '); + if (opt?.cap) result = cap(result, opt.cap); + return result; +}; + +// ============================================================================ +// SCIENTIFIC NOTATION +// ============================================================================ + +/** Expand a float's exponent form ('1.5e-7') into a plain decimal string */ +const expandExponent = (str) => { + const negative = str.startsWith('-'); + const [m, e] = (negative ? str.slice(1) : str).split('e'); + const exp = Number(e); + const mDigits = m.replace('.', ''); + const point = m.split('.')[0].length + exp; + let out; + if (point <= 0) out = `0.${'0'.repeat(-point)}${mDigits}`; + else if (point >= mDigits.length) out = `${mDigits}${'0'.repeat(point - mDigits.length)}`; + else out = `${mDigits.slice(0, point)}.${mDigits.slice(point)}`; + return (negative ? '-' : '') + out; +}; + +/** Normalize number | bigint | numeric string to a plain decimal string, or null */ +const toPlainDecimal = (n) => { + if (typeof n === 'bigint') return n.toString(); + if (typeof n === 'number') { + if (!Number.isFinite(n)) return null; + const str = n.toString(); + return str.includes('e') ? expandExponent(str) : str; + } + if (typeof n === 'string' && /^-?\d+(\.\d+)?$/.test(n.trim())) return n.trim(); + return null; +}; + +/** + * Scientific notation with an exact decimal mantissa (no float drift). + * @param {number|bigint|string} n - The number + * @param {Object} [opt] - Options object + * @param {number} [opt.digits=12] - Maximum significant digits (rounds half up) + * @param {string} [opt.format='unicode'] - 'unicode' (1.984 × 10³), 'caret' (1.984 × 10^3), + * 'e' (1.984e3), or 'words' (one point nine eight four times ten to the third) + * @param {string} [opt.cap] - Capitalization for the words format + * @returns {string|false} + * + * @example + * scientific(1984) // '1.984 × 10³' + * scientific(0.00042) // '4.2 × 10⁻⁴' + * scientific(1984, { format: 'e' }) // '1.984e3' + * scientific(1984, { format: 'words' }) // 'one point nine eight four times ten to the third' + */ +const scientific = (n, opt) => { + let str = toPlainDecimal(n); + if (str === null) return false; + + const negative = str.startsWith('-'); + if (negative) str = str.slice(1); + const [rawInt, fracPart = ''] = str.split('.'); + const intPart = rawInt.replace(/^0+/, ''); + const all = intPart + fracPart; + const firstNonZero = all.search(/[1-9]/); + + let exponent = 0; + let sig = '0'; + if (firstNonZero !== -1) { + exponent = intPart ? intPart.length - 1 : -(firstNonZero + 1); + sig = all.slice(firstNonZero).replace(/0+$/, '') || '0'; + } + + const maxDigits = Number.isFinite(opt?.digits) ? Math.max(1, Math.min(Math.trunc(opt.digits), 36)) : 12; + if (sig.length > maxDigits) { + const rounded = BigInt(sig.slice(0, maxDigits)) + (Number(sig[maxDigits]) >= 5 ? 1n : 0n); + let roundedStr = rounded.toString(); + if (roundedStr.length > maxDigits) { + // 999 → 1000 carries into the exponent + exponent++; + roundedStr = roundedStr.slice(0, -1); + } + sig = roundedStr.replace(/0+$/, '') || '0'; + } + + const mantissa = sig.length > 1 ? `${sig[0]}.${sig.slice(1)}` : sig; + const sign = negative ? '-' : ''; + const format = opt?.format || 'unicode'; + + if (format === 'e') return `${sign}${mantissa}e${exponent}`; + if (format === 'caret') return `${sign}${mantissa} × 10^${exponent}`; + if (format === 'words') { + const mantissaWords = decimal(`${sign}${mantissa}`); + if (mantissaWords === false) return false; + let result = mantissaWords; + if (exponent !== 0) { + const power = exponent < 0 ? `negative ${ordinal(-exponent)}` : ordinal(exponent); + result += ` times ten to the ${power}`; + } + return opt?.cap ? cap(result, opt.cap) : result; + } + if (format !== 'unicode') return false; + return `${sign}${mantissa} × 10${fancy(exponent, 'superscript')}`; +}; + +// ============================================================================ +// RADIX (binary, octal, hex) +// ============================================================================ + +const RADIX_PREFIXES = Object.freeze({ 2: '0b', 8: '0o', 16: '0x' }); + +/** + * Integer in another base, 2 to 36. + * @param {number|bigint|string} n - Integer + * @param {number} [base=2] - Radix, 2 to 36 + * @param {Object} [opt] - Options object + * @param {boolean} [opt.prefix] - Add 0b / 0o / 0x for bases 2, 8, 16 + * @param {boolean} [opt.upper] - Uppercase letter digits + * @param {number} [opt.pad] - Left-pad with zeros to this many digits + * @returns {string|false} + * + * @example + * radix(42) // '101010' + * radix(42, 16) // '2a' + * radix(255, 16, { prefix: true, upper: true }) // '0xFF' + */ +const radix = (n, base = 2, opt) => { + if (!Number.isInteger(base) || base < 2 || base > 36) return false; + let value; + if (typeof n === 'bigint') value = n; + else if (typeof n === 'number' && Number.isInteger(n) && Math.abs(n) <= Number.MAX_SAFE_INTEGER) value = BigInt(n); + else if (typeof n === 'string' && /^-?\d+$/.test(n.trim())) value = BigInt(n.trim()); + else return false; + + const negative = value < 0n; + let digits = (negative ? -value : value).toString(base); + if (opt?.upper) digits = digits.toUpperCase(); + if (opt?.pad) digits = digits.padStart(opt.pad, '0'); + const prefix = opt?.prefix ? (RADIX_PREFIXES[base] || '') : ''; + return `${negative ? '-' : ''}${prefix}${digits}`; +}; + +/** Binary: 42 → '101010' */ +const binary = (n, opt) => radix(n, 2, opt); +/** Octal: 42 → '52' */ +const octal = (n, opt) => radix(n, 8, opt); +/** Hexadecimal: 42 → '2a' */ +const hex = (n, opt) => radix(n, 16, opt); + +// ============================================================================ +// BYTES +// ============================================================================ + +const BYTE_UNITS = Object.freeze({ + decimal: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], + binary: ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'], + decimalWords: ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte', 'petabyte', 'exabyte', 'zettabyte', 'yottabyte'], + binaryWords: ['byte', 'kibibyte', 'mebibyte', 'gibibyte', 'tebibyte', 'pebibyte', 'exbibyte', 'zebibyte', 'yobibyte'] +}); + +const BIT_UNITS = Object.freeze({ + decimal: ['b', 'kb', 'Mb', 'Gb', 'Tb', 'Pb', 'Eb', 'Zb', 'Yb'], + binary: ['b', 'Kib', 'Mib', 'Gib', 'Tib', 'Pib', 'Eib', 'Zib', 'Yib'], + decimalWords: ['bit', 'kilobit', 'megabit', 'gigabit', 'terabit', 'petabit', 'exabit', 'zettabit', 'yottabit'], + binaryWords: ['bit', 'kibibit', 'mebibit', 'gibibit', 'tebibit', 'pebibit', 'exbibit', 'zebibit', 'yobibit'] +}); + +/** Shared engine for bytes() and bits(): all rounding in BigInt, so precision holds at any size */ +const dataSize = (n, opt, table) => { + let value; + if (typeof n === 'bigint') value = n; + else if (typeof n === 'number' && Number.isInteger(n) && n <= Number.MAX_SAFE_INTEGER) value = BigInt(n); + else if (typeof n === 'string' && /^\d+$/.test(n.trim())) value = BigInt(n.trim()); + else return false; + if (value < 0n) return false; + + const step = opt?.binary ? 1024n : 1000n; + const units = opt?.binary ? table.binary : table.decimal; + const words = opt?.binary ? table.binaryWords : table.decimalWords; + // Clamp to a finite integer 0-6 so BigInt() cannot throw on 1.5, NaN, or Infinity + const digits = Number.isFinite(opt?.digits) ? Math.max(0, Math.min(Math.trunc(opt.digits), 6)) : 1; + const precision = 10n ** BigInt(digits); + + let unit = 0; + let scale = 1n; + while (unit < units.length - 1 && value >= scale * step) { + scale *= step; + unit++; + } + // amount in units of 10^-digits, rounded half up + let fixed = (value * precision * 2n + scale) / (scale * 2n); + if (fixed >= step * precision && unit < units.length - 1) { + unit++; + scale *= step; + fixed = (value * precision * 2n + scale) / (scale * 2n); + } + + const whole = (fixed / precision).toString(); + const frac = digits ? (fixed % precision).toString().padStart(digits, '0').replace(/0+$/, '') : ''; + const amountStr = frac ? `${whole}.${frac}` : whole; + + if (opt?.long) { + const amountWords = frac ? decimal(amountStr) : cardinal(BigInt(whole)); + if (amountWords === false) return false; + const noun = amountStr === '1' ? words[unit] : `${words[unit]}s`; + return `${amountWords} ${noun}`; + } + return `${amountStr} ${units[unit]}`; +}; + +/** + * Human-readable byte sizes. + * @param {number|bigint|string} n - Non-negative integer count of bytes + * @param {Object} [opt] - Options object + * @param {boolean} [opt.binary] - Use 1024 steps and KiB/MiB units + * @param {number} [opt.digits=1] - Maximum decimal places + * @param {boolean} [opt.long] - Spell it out: 'one point five kilobytes' + * @returns {string|false} + * + * @example + * bytes(1536) // '1.5 KB' + * bytes(1536, { binary: true }) // '1.5 KiB' + * bytes(1536, { long: true }) // 'one point five kilobytes' + */ +const bytes = (n, opt) => dataSize(n, opt, BYTE_UNITS); + +/** + * Human-readable bit counts (bandwidth style: kb, Mb, Gb). + * @param {number|bigint|string} n - Non-negative integer count of bits + * @param {Object} [opt] - Same options as bytes() + * @returns {string|false} + * + * @example + * bits(1500000) // '1.5 Mb' + * bits(1536, { binary: true }) // '1.5 Kib' + * bits(1500000, { long: true }) // 'one point five megabits' + */ +const bits = (n, opt) => dataSize(n, opt, BIT_UNITS); + +// ============================================================================ +// MORSE CODE +// ============================================================================ + +const MORSE_DIGITS = Object.freeze(['-----', '.----', '..---', '...--', '....-', '.....', '-....', '--...', '---..', '----.']); + +/** + * International Morse code for the digits. Digits are separated by a space, + * a decimal point is '.-.-.-' and a minus sign '-....-'. + * @param {number|bigint|string} n - The number + * @returns {string|false} + * + * @example + * morse(42) // '....- ..---' + * morse(3.1) // '...-- .-.-.- .----' + */ +const morse = (n) => { + let str; + if (typeof n === 'bigint') str = n.toString(); + else if (typeof n === 'number') { + if (!Number.isFinite(n)) return false; + str = Number.isInteger(n) ? BigInt(n).toString() : n.toString(); + if (str.includes('e')) return false; + } else if (typeof n === 'string' && /^-?\d+(\.\d+)?$/.test(n.trim())) str = n.trim(); + else return false; + + return [...str].map((ch) => { + if (ch === '-') return '-....-'; + if (ch === '.') return '.-.-.-'; + return MORSE_DIGITS[Number(ch)]; + }).join(' '); +}; + + +const negative = (n, opt) => { + let value; + if (typeof n === 'bigint') value = n; + else if (typeof n === 'number' && !isNaN(n)) value = n; + else return false; + + if (value >= 0) return cardinal(value, opt); + + // Case the whole phrase once; casing the inner words first would break camel/pascal + const inner = opt ? { ...opt, cap: undefined, punc: undefined } : opt; + const result = cardinal(typeof value === 'bigint' ? -value : Math.abs(value), inner); if (result === false) return false; let s = `negative ${result}`; if (opt?.cap) s = cap(s, opt.cap); + if (opt?.punc !== undefined) s = punc(s, opt.punc); return s; }; @@ -522,8 +1093,15 @@ const fraction = (numerator, denominator, opt) => { }; const year = (y, opt) => { + // Far-future years are read as plain cardinals: "the year ten thousand" + if (typeof y === 'bigint') { + if (y < 0n) return false; + if (y > 9999n) return cardinal(y, opt); + y = Number(y); + } if (typeof y !== 'number' || isNaN(y) || !Number.isInteger(y)) return false; - if (y < 0 || y > 9999) return false; + if (y < 0) return false; + if (y > 9999) return cardinal(y, opt); let result; @@ -561,7 +1139,8 @@ const telephone = (phone, opt) => { const phoneStr = String(phone).replace(/\D/g, ''); if (!phoneStr) return false; - const digitWords = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']; + // 'oh' for zero is how English speakers usually read phone numbers aloud + const digitWords = [opt?.oh ? 'oh' : 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']; const words = phoneStr.split('').map(d => digitWords[parseInt(d, 10)]); let result = words.join(' '); @@ -601,7 +1180,7 @@ const percent = (pct, opt) => { * @returns {string|false} The word representation */ const toWords = (n, opt) => { - const lang = opt?.lang?.toLowerCase() || 'en'; + const lang = typeof opt?.lang === 'string' ? opt.lang.toLowerCase() : 'en'; const langKey = LANGUAGES[lang] || 'english'; let result; @@ -622,7 +1201,7 @@ const toWords = (n, opt) => { result = danish(n); break; case 'chinese': - result = chinese(n); + result = chinese(n, opt); break; case 'hindi': result = hindi(n); @@ -634,7 +1213,7 @@ const toWords = (n, opt) => { result = portuguese(n); break; case 'japanese': - result = japanese(n); + result = japanese(n, opt); break; case 'korean': result = korean(n); @@ -696,5 +1275,18 @@ export { year, telephone, percent, + nth, + compact, + nato, + nato as icao, + nato as military, + morse, + scientific, + radix, + binary, + octal, + hex, + bytes, + bits, toWords }; diff --git a/languages/es.js b/languages/es.js index 5b27e91..3b99bb9 100644 --- a/languages/es.js +++ b/languages/es.js @@ -21,7 +21,7 @@ const tenment = (n, g) => hundment(n, g) % 100; const hundredEs = (n) => { if (n < 100 || n >= 1000) return ''; const h = Math.floor(n / 100); - if (h === 1) return 'ciento '; + if (h === 1) return n === 100 ? 'cien ' : 'ciento '; if (h === 5) return 'quinientos '; if (h === 7) return 'setecientos '; if (h === 9) return 'novecientos '; @@ -88,8 +88,11 @@ const spanish = (n, opt) => { if (h === 1 && i === 1) { s += 'mil '; } else { - s += hundredEs(h); - s += tenEs(tenment(num, i)); + // "uno" apocopates to "un" before a scale word: un millón, veintiún mil + const groupWords = (hundredEs(h) + tenEs(tenment(num, i))) + .replace(/veintiuno $/, 'veintiún ') + .replace(/uno $/, 'un '); + s += groupWords; const illionWord = h === 1 ? ES_ILLIONS[i] : ES_ILLIONS_PLURAL[i]; s += `${illionWord} `; } diff --git a/languages/fr.js b/languages/fr.js index 9fd7893..806f40f 100644 --- a/languages/fr.js +++ b/languages/fr.js @@ -84,10 +84,15 @@ const french = (n) => { s += tenFr(tenment(num, i)); } else if (i === 1 && h === 1) { s += 'mille '; + } else if (i === 1) { + // "cents" and "quatre-vingts" lose their plural s before "mille" + const groupWords = (hundredFr(h) + tenFr(tenment(num, i))).replace(/(cent|vingt)s $/, '$1 '); + s += `${groupWords}mille `; } else { s += hundredFr(h); s += tenFr(tenment(num, i)); - s += `${FR_ILLIONS[i]} `; + // million, milliard, etc. are nouns and take a plural s + s += h > 1 ? `${FR_ILLIONS[i]}s ` : `${FR_ILLIONS[i]} `; } } } diff --git a/languages/index.js b/languages/index.js index de73e7b..cf44cd7 100644 --- a/languages/index.js +++ b/languages/index.js @@ -90,6 +90,7 @@ const LANGUAGES = Object.freeze({ id: 'indonesian', indonesian: 'indonesian', 'bahasa indonesia': 'indonesian', + bahasa: 'indonesian', th: 'thai', thai: 'thai', 'ไทย': 'thai', diff --git a/languages/is.js b/languages/is.js index 4ce9a39..6409d88 100644 --- a/languages/is.js +++ b/languages/is.js @@ -18,23 +18,38 @@ const segment = (n, g) => n % power(g + 1); const hundment = (n, g) => Number(segment(n, g) / power(g)); const tenment = (n, g) => hundment(n, g) % 100; -const tenIs = (n) => { +// 1-4 decline for gender: masculine (default), feminine (milljón), neuter (hundrað, þúsund) +const IS_ONES_F = Object.freeze(['', 'ein', 'tvær', 'þrjár', 'fjórar']); +const IS_ONES_N = Object.freeze(['', 'eitt', 'tvö', 'þrjú', 'fjögur']); + +const onesIs = (d, gender) => { + if (d >= 1 && d <= 4) { + if (gender === 'f') return IS_ONES_F[d]; + if (gender === 'n') return IS_ONES_N[d]; + } + return IS_ONES[d]; +}; + +const tenIs = (n, gender) => { if (n === 0) return ''; - if (n < 10) return IS_ONES[n]; + if (n < 10) return onesIs(n, gender); if (n < 20) return IS_TEENS[n - 10]; const onesDigit = n % 10; const tensDigit = Math.floor(n / 10); if (onesDigit === 0) return IS_TENS[tensDigit]; - return `${IS_TENS[tensDigit]} og ${IS_ONES[onesDigit]}`; + return `${IS_TENS[tensDigit]} og ${onesIs(onesDigit, gender)}`; }; const hundredIs = (n) => { if (n < 100 || n >= 1000) return ''; const h = Math.floor(n / 100); if (h === 1) return 'eitt hundrað'; - return `${IS_ONES[h]} hundruð`; + return `${onesIs(h, 'n')} hundruð`; }; +/** Grammatical gender of each scale word: -jón feminine, -jarður masculine, þúsund neuter */ +const illionGender = (i) => (i === 1 ? 'n' : i % 2 === 0 ? 'f' : 'm'); + /** * Convert a number to Icelandic words * @param {number|bigint} n - The number to convert @@ -69,28 +84,14 @@ const icelandic = (n) => { if (h >= 100) s += hundredIs(h) + ' '; const t = tenment(num, i); if (t > 0) s += tenIs(t); - } else if (i === 1) { - if (h === 1) { - s += 'eitt þúsund '; - } else { - if (h >= 100) s += hundredIs(h) + ' '; - const t = tenment(num, i); - if (t > 0) s += tenIs(t) + ' '; - s += 'þúsund '; - } } else { + const gender = illionGender(i); if (h >= 100) s += hundredIs(h) + ' '; const t = tenment(num, i); - if (t > 0) { - if (t === 1) { - s += 'einn '; - } else { - s += tenIs(t) + ' '; - } - } else if (h < 100 && h >= 1 && h === 1) { - s += 'einn '; - } - const illionWord = h === 1 ? IS_ILLIONS[i] : IS_ILLIONS_PLURAL[i]; + if (t > 0) s += tenIs(t, gender) + ' '; + // Numbers ending in 1 (but not 11) take the singular: tuttugu og ein milljón + const singular = h % 10 === 1 && h % 100 !== 11; + const illionWord = singular ? IS_ILLIONS[i] : IS_ILLIONS_PLURAL[i]; s += `${illionWord} `; } } diff --git a/languages/it.js b/languages/it.js index 1aeda77..6f2a109 100644 --- a/languages/it.js +++ b/languages/it.js @@ -105,6 +105,8 @@ const italian = (n) => { if (h >= 100) s += hundredIt(h); const t = tenment(num, i); if (t > 0) s += tenIt(t); + // "-uno" elides before mila: ventunmila, trentunmila + s = s.replace(/(ant|ent)uno$/, '$1un'); s += 'mila'; } } else { diff --git a/languages/ja.js b/languages/ja.js index 4b7ce4b..3025421 100644 --- a/languages/ja.js +++ b/languages/ja.js @@ -5,6 +5,8 @@ */ const JA_DIGITS = Object.freeze(['', '一', '二', '三', '四', '五', '六', '七', '八', '九']); +/** Daiji (大字) anti-fraud forms used on banknotes and legal documents */ +const JA_DAIJI = Object.freeze(['', '壱', '弐', '参', '四', '五', '六', '七', '八', '九']); const JA_SCALES = Object.freeze(['', '万', '億', '兆', '京', '垓', '𥝱', '穣', '溝']); /** Maximum supported value (10^36 - 1, up to 溝) */ @@ -16,8 +18,10 @@ const MAX_VALUE = 10n ** 36n - 1n; * @param {number} grp - The group value (0-9999) * @returns {string} The Japanese representation */ -const groupToJa = (grp) => { +const groupToJa = (grp, afterScale = false, formal = false) => { if (grp === 0) return ''; + const digits = formal ? JA_DAIJI : JA_DIGITS; + const ten = formal ? '拾' : '十'; const thousands = Math.floor(grp / 1000); const hundreds = Math.floor((grp % 1000) / 100); @@ -26,36 +30,38 @@ const groupToJa = (grp) => { let result = ''; - // Thousands: 1 before 千 is omitted + // Thousands: 1 before 千 is omitted at the start (千) but kept after a + // higher scale word (二万一千) + // Daiji always writes the 壱 (壱千, 壱百, 壱拾) if (thousands > 0) { - if (thousands === 1) { + if (thousands === 1 && !afterScale && !formal) { result += '千'; } else { - result += JA_DIGITS[thousands] + '千'; + result += digits[thousands] + '千'; } } // Hundreds: 1 before 百 is omitted if (hundreds > 0) { - if (hundreds === 1) { + if (hundreds === 1 && !formal) { result += '百'; } else { - result += JA_DIGITS[hundreds] + '百'; + result += digits[hundreds] + '百'; } } // Tens: 1 before 十 is omitted if (tens > 0) { - if (tens === 1) { - result += '十'; + if (tens === 1 && !formal) { + result += ten; } else { - result += JA_DIGITS[tens] + '十'; + result += digits[tens] + ten; } } // Ones if (ones > 0) { - result += JA_DIGITS[ones]; + result += digits[ones]; } return result; @@ -64,6 +70,8 @@ const groupToJa = (grp) => { /** * Convert a number to Japanese words * @param {number|bigint} n - The number to convert + * @param {Object} [opt] - Options object + * @param {boolean} [opt.formal] - Use daiji 大字 numerals (壱弐参, 拾) * @returns {string|false} The Japanese word representation * * @example @@ -71,7 +79,8 @@ const groupToJa = (grp) => { * japanese(1000) // '千' * japanese(10000) // '一万' */ -const japanese = (n) => { +const japanese = (n, opt) => { + const formal = opt?.formal === true; let num; if (typeof n === 'bigint') { @@ -85,7 +94,7 @@ const japanese = (n) => { return false; } - if (num === 0n) return 'ゼロ'; + if (num === 0n) return formal ? '零' : 'ゼロ'; const str = num.toString(); const len = str.length; @@ -105,7 +114,7 @@ const japanese = (n) => { if (grp === 0) continue; - const grpStr = groupToJa(grp); + const grpStr = groupToJa(grp, i > 0, formal); // 1 before 万 and above IS included (handled naturally by groupToJa // since grp=1 produces '一' for the ones digit in the group) diff --git a/languages/zh.js b/languages/zh.js index 95c89e7..6c3a28c 100644 --- a/languages/zh.js +++ b/languages/zh.js @@ -6,6 +6,9 @@ const ZH_ONES = Object.freeze(['零', '一', '二', '三', '四', '五', '六', '七', '八', '九']); const ZH_UNITS = Object.freeze(['', '十', '百', '千']); +/** Financial (大写) anti-fraud forms used on cheques and contracts */ +const ZH_FORMAL_ONES = Object.freeze(['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']); +const ZH_FORMAL_UNITS = Object.freeze(['', '拾', '佰', '仟']); const ZH_ILLIONS = Object.freeze(['', '万', '亿', '兆', '京', '垓', '秭', '穰', '沟', '涧', '正', '载']); const MAX_VALUE = 10n ** 36n - 1n; @@ -13,14 +16,20 @@ const MAX_VALUE = 10n ** 36n - 1n; /** * Convert a number to Mandarin Chinese words * @param {number|bigint} n - The number to convert + * @param {Object} [opt] - Options object + * @param {boolean} [opt.formal] - Use financial 大写 numerals (壹贰叁, 拾佰仟) * @returns {string|false} The Mandarin word representation * * @example * chinese(42) // '四十二' * chinese(1000) // '一千' * chinese(10000) // '一万' + * chinese(42, { formal: true }) // '肆拾贰' */ -const chinese = (n) => { +const chinese = (n, opt) => { + const formal = opt?.formal === true; + const digitWords = formal ? ZH_FORMAL_ONES : ZH_ONES; + const unitWords = formal ? ZH_FORMAL_UNITS : ZH_UNITS; let num; if (typeof n === 'bigint') { @@ -71,27 +80,27 @@ const chinese = (n) => { let innerZero = false; if (thousands > 0) { - grpStr += ZH_ONES[thousands] + ZH_UNITS[3]; + grpStr += digitWords[thousands] + unitWords[3]; innerZero = false; } else if (result || i > 0) { innerZero = true; } if (hundreds > 0) { - if (innerZero && grpStr) grpStr += '零'; - grpStr += ZH_ONES[hundreds] + ZH_UNITS[2]; + if (innerZero && (grpStr || (result && !result.endsWith('零')))) grpStr += '零'; + grpStr += digitWords[hundreds] + unitWords[2]; innerZero = false; } else if (thousands > 0) { innerZero = true; } if (tens > 0) { - if (innerZero && grpStr) grpStr += '零'; + if (innerZero && (grpStr || (result && !result.endsWith('零')))) grpStr += '零'; // Special: 10-19 at start is just 十X, not 一十X - if (tens === 1 && !result && thousands === 0 && hundreds === 0) { - grpStr += ZH_UNITS[1]; + if (tens === 1 && !formal && !result && thousands === 0 && hundreds === 0) { + grpStr += unitWords[1]; } else { - grpStr += ZH_ONES[tens] + ZH_UNITS[1]; + grpStr += digitWords[tens] + unitWords[1]; } innerZero = false; } else if (hundreds > 0 || thousands > 0) { @@ -99,8 +108,8 @@ const chinese = (n) => { } if (ones > 0) { - if (innerZero && grpStr) grpStr += '零'; - grpStr += ZH_ONES[ones]; + if (innerZero && (grpStr || (result && !result.endsWith('零')))) grpStr += '零'; + grpStr += digitWords[ones]; } result += grpStr; diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..e64c833 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,12 @@ +[build] + command = "node scripts/build-site.js" + publish = "site" + +[build.environment] + NODE_VERSION = "22" + +[[headers]] + for = "/*" + [headers.values] + X-Content-Type-Options = "nosniff" + Referrer-Policy = "strict-origin-when-cross-origin" diff --git a/numerals.js b/numerals.js new file mode 100644 index 0000000..a09a226 --- /dev/null +++ b/numerals.js @@ -0,0 +1,239 @@ +/** + * Alternative numeral systems and Unicode digit styles. + * Everything here is pure, table-driven, and renders with Unicode glyphs. + * Only blocks with broad system-font coverage are included (Mayan numerals + * and tally marks were dropped for lack of fonts on macOS). + * @module numerals + */ + +// ============================================================================ +// SHARED +// ============================================================================ + +/** Normalize a non-negative integer input to BigInt, or null if invalid */ +const toCount = (n) => { + if (typeof n === 'bigint') return n >= 0n ? n : null; + if (typeof n === 'number') return Number.isInteger(n) && n >= 0 ? BigInt(n) : null; + if (typeof n === 'string' && /^\d+$/.test(n.trim())) return BigInt(n.trim()); + return null; +}; + +/** Normalize any numeric input (sign, decimal point) to a digit string, or null */ +const toDigitString = (n) => { + if (typeof n === 'bigint') return n.toString(); + if (typeof n === 'number') { + if (!Number.isFinite(n)) return null; + const str = n.toString(); + if (str.includes('e')) return Number.isInteger(n) ? BigInt(n).toString() : null; + return str; + } + if (typeof n === 'string') { + const str = n.trim(); + return /^-?\d+(\.\d+)?$/.test(str) ? str : null; + } + return null; +}; + +// ============================================================================ +// UNICODE DIGIT STYLES +// ============================================================================ + +/** Digit tables: ten glyphs for 0-9, plus minus and point */ +const FANCY_STYLES = Object.freeze({ + circled: { digits: '⓪①②③④⑤⑥⑦⑧⑨', minus: '−', point: '·' }, + superscript: { digits: '⁰¹²³⁴⁵⁶⁷⁸⁹', minus: '⁻', point: '˙' }, + subscript: { digits: '₀₁₂₃₄₅₆₇₈₉', minus: '₋', point: '.' }, + fullwidth: { digits: '0123456789', minus: '-', point: '.' }, + bold: { digits: '𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗', minus: '−', point: '.' }, + doublestruck: { digits: '𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡', minus: '−', point: '.' }, + sans: { digits: '𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫', minus: '−', point: '.' }, + monospace: { digits: '𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿', minus: '−', point: '.' }, + keycap: { digits: ['0️⃣', '1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣'], minus: '➖', point: '.' }, + emoji: { digits: ['0️⃣', '1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣'], minus: '➖', point: '.' }, + // Clock faces: 1-9 o'clock, with 12 o'clock standing in for 0 + clock: { digits: '🕛🕐🕑🕒🕓🕔🕕🕖🕗🕘', minus: '−', point: '·' }, + // Braille: numeric indicator ⠼ then a-j, decimal point ⠨, minus ⠤ + braille: { digits: '⠚⠁⠃⠉⠙⠑⠋⠛⠓⠊', minus: '⠤', point: '⠨', prefix: '⠼' } +}); + +/** Style names accepted by fancy() */ +const FANCY_STYLE_NAMES = Object.freeze(Object.keys(FANCY_STYLES)); + +/** + * Render a number's digits in a Unicode style. + * @param {number|bigint|string} n - The number + * @param {string} [style='circled'] - One of circled, superscript, subscript, + * fullwidth, bold, doublestruck, sans, monospace, keycap (alias emoji), clock, braille + * @returns {string|false} Styled digits or false if invalid + * + * @example + * fancy(42) // '④②' + * fancy(42, 'superscript') // '⁴²' + * fancy(-3.5, 'fullwidth') // '-3.5' + */ +const fancy = (n, style = 'circled') => { + const table = FANCY_STYLES[style]; + if (!table) return false; + const str = toDigitString(n); + if (str === null) return false; + + const glyphs = Array.isArray(table.digits) ? table.digits : [...table.digits]; + let out = table.prefix || ''; + for (const ch of str) { + if (ch === '-') out += table.minus; + else if (ch === '.') out += table.point; + else out += glyphs[Number(ch)]; + } + return out; +}; + +// ============================================================================ +// EGYPTIAN HIEROGLYPHS +// ============================================================================ + +/** Hieroglyphs for 1, 10, 100, ... 1,000,000 (stroke, heel bone, coil, lotus, finger, tadpole, Heh) */ +const EGYPTIAN_SYMBOLS = Object.freeze(['𓏺', '𓎆', '𓍢', '𓆼', '𓂭', '𓆐', '𓁨']); + +/** Largest value expressible with repeated hieroglyphs (9,999,999) */ +const EGYPTIAN_MAX = 9999999n; + +/** + * Egyptian hieroglyphic numerals, additive: each power of ten is a symbol + * repeated up to nine times, largest first. + * @param {number|bigint|string} n - Integer from 1 to 9,999,999 + * @returns {string|false} + * + * @example + * egyptian(42) // '𓎆𓎆𓎆𓎆𓏺𓏺' + * egyptian(1000) // '𓆼' + */ +const egyptian = (n) => { + const count = toCount(n); + if (count === null || count < 1n || count > EGYPTIAN_MAX) return false; + + const digits = count.toString(); + let out = ''; + for (let i = 0; i < digits.length; i++) { + const power = digits.length - 1 - i; + out += EGYPTIAN_SYMBOLS[power].repeat(Number(digits[i])); + } + return out; +}; + +// ============================================================================ +// BABYLONIAN CUNEIFORM +// ============================================================================ + +/** DIŠ (U+12079), the positional unit wedge; GESH2 (U+12415) looks alike but means sixty */ +const CUNEIFORM_ONE = '𒁹'; +const CUNEIFORM_TEN = '𒌋'; +/** Late Babylonian placeholder for an empty sexagesimal position */ +const CUNEIFORM_ZERO = '𒑊'; + +/** + * Babylonian sexagesimal (base 60) numerals. Each position is written with + * tens wedges then unit wedges; positions are separated by a space and an + * empty inner position uses the Late Babylonian placeholder sign. + * @param {number|bigint|string} n - Non-negative integer + * @returns {string|false} + * + * @example + * babylonian(42) // '𒌋𒌋𒌋𒌋𒁹𒁹' + * babylonian(3600) // '𒁹 𒑊 𒑊' + */ +const babylonian = (n) => { + const count = toCount(n); + if (count === null) return false; + if (count === 0n) return CUNEIFORM_ZERO; + + const places = []; + let rest = count; + while (rest > 0n) { + places.unshift(Number(rest % 60n)); + rest /= 60n; + } + return places + .map((p) => (p === 0 ? CUNEIFORM_ZERO : CUNEIFORM_TEN.repeat(Math.floor(p / 10)) + CUNEIFORM_ONE.repeat(p % 10))) + .join(' '); +}; + +// ============================================================================ +// CLOCK FACES +// ============================================================================ + +/** 🕐..🕛 for 1..12 o'clock (U+1F550..), 🕜..🕧 for the half hours (U+1F55C..) */ +const CLOCK_HOURS = Object.freeze([...'🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚🕛']); +const CLOCK_HALVES = Object.freeze([...'🕜🕝🕞🕟🕠🕡🕢🕣🕤🕥🕦🕧']); + +/** + * Clock-face emoji for an hour (0-24, 24-hour values wrap) or an "H:MM" time. + * Minutes round to the nearest half hour; 0 and 24 are 🕛. + * @param {number|string} time - Hour, or 'H:MM' + * @returns {string|false} + * + * @example + * clock(3) // '🕒' + * clock('3:30') // '🕞' + * clock(15) // '🕒' + * clock('23:50') // '🕛' + */ +const clock = (time) => { + let hour; + let minute = 0; + if (typeof time === 'number') { + if (!Number.isInteger(time) || time < 0 || time > 24) return false; + hour = time; + } else if (typeof time === 'string') { + const m = time.trim().match(/^(\d{1,2})(?::(\d{2}))?$/); + if (!m) return false; + hour = Number(m[1]); + minute = Number(m[2] || 0); + if (hour > 24 || minute > 59) return false; + } else { + return false; + } + + // Round to the nearest half hour; :45 and later roll to the next hour + if (minute >= 45) { + hour += 1; + minute = 0; + } + const half = minute >= 15; + const index = (hour + 11) % 12; + return half ? CLOCK_HALVES[index] : CLOCK_HOURS[index]; +}; + +// ============================================================================ +// GREEK (IONIC / MILESIAN) +// ============================================================================ + +const GREEK_ONES = Object.freeze(['', 'α', 'β', 'γ', 'δ', 'ε', 'ϛ', 'ζ', 'η', 'θ']); +const GREEK_TENS = Object.freeze(['', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', 'ϟ']); +const GREEK_HUNDREDS = Object.freeze(['', 'ρ', 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω', 'ϡ']); +/** Keraia marks a number; the lower keraia marks thousands */ +const GREEK_KERAIA = 'ʹ'; +const GREEK_THOUSANDS = '͵'; + +/** + * Greek alphabetic (Ionic) numerals for 1-9999, with stigma, koppa and sampi + * for 6, 90 and 900 and the keraia marks. + * @param {number|bigint|string} n - Integer from 1 to 9999 + * @returns {string|false} + * + * @example + * greek(42) // 'μβʹ' + * greek(1999) // '͵αϡϟθʹ' + */ +const greek = (n) => { + const count = toCount(n); + if (count === null || count < 1n || count > 9999n) return false; + + const v = Number(count); + const thousands = Math.floor(v / 1000); + const rest = v % 1000; + let out = thousands ? GREEK_THOUSANDS + GREEK_ONES[thousands] : ''; + out += GREEK_HUNDREDS[Math.floor(rest / 100)] + GREEK_TENS[Math.floor((rest % 100) / 10)] + GREEK_ONES[rest % 10]; + return out + GREEK_KERAIA; +}; + +export { fancy, FANCY_STYLE_NAMES, egyptian, babylonian, greek, clock }; diff --git a/package-lock.json b/package-lock.json index 10d0a6c..980a1bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,16 @@ { "name": "numberstring", - "version": "1.0.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "numberstring", - "version": "1.0.0", + "version": "1.2.0", "license": "MIT", "devDependencies": { "@vitest/coverage-v8": "^4.0.18", "eslint": "^9.17.0", - "express": "^4.21.2", - "supertest": "^7.0.0", "vitest": "^4.0.18" }, "engines": { @@ -745,29 +743,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@paralleldrive/cuid2": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", - "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@noble/hashes": "^1.1.5" - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.57.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", @@ -1299,20 +1274,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1376,20 +1337,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true, - "license": "MIT" - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1412,13 +1359,6 @@ "js-tokens": "^10.0.0" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1426,48 +1366,6 @@ "dev": true, "license": "MIT" }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -1479,47 +1377,6 @@ "concat-map": "0.0.1" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1577,29 +1434,6 @@ "dev": true, "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1607,53 +1441,6 @@ "dev": true, "license": "MIT" }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true, - "license": "MIT" - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1694,100 +1481,6 @@ "dev": true, "license": "MIT" }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dezalgo": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "dev": true, - "license": "ISC", - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -1795,35 +1488,6 @@ "dev": true, "license": "MIT" }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/esbuild": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", @@ -1866,13 +1530,6 @@ "@esbuild/win32-x64": "0.27.2" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2050,16 +1707,6 @@ "node": ">=0.10.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -2070,70 +1717,6 @@ "node": ">=12.0.0" } }, - "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.14.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2155,13 +1738,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, - "license": "MIT" - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2193,42 +1769,6 @@ "node": ">=16.0.0" } }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2267,216 +1807,57 @@ "dev": true, "license": "ISC" }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 6" + "node": ">=10.13.0" } }, - "node_modules/formidable": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", - "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", - "dependencies": { - "@paralleldrive/cuid2": "^2.2.2", - "dezalgo": "^1.0.4", - "once": "^1.4.0" - }, "engines": { - "node": ">=14.0.0" + "node": ">=18" }, "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -2484,40 +1865,6 @@ "dev": true, "license": "MIT" }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2555,23 +1902,6 @@ "node": ">=0.8.19" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2767,82 +2097,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2889,29 +2143,6 @@ "dev": true, "license": "MIT" }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -2923,29 +2154,6 @@ ], "license": "MIT" }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3009,16 +2217,6 @@ "node": ">=6" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3039,13 +2237,6 @@ "node": ">=8" } }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "dev": true, - "license": "MIT" - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -3112,20 +2303,6 @@ "node": ">= 0.8.0" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3136,48 +2313,6 @@ "node": ">=6" } }, - "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3233,34 +2368,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -3274,71 +2381,6 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3362,82 +2404,6 @@ "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -3462,16 +2428,6 @@ "dev": true, "license": "MIT" }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", @@ -3492,65 +2448,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/superagent": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", - "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "component-emitter": "^1.3.1", - "cookiejar": "^2.1.4", - "debug": "^4.3.7", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.5", - "formidable": "^3.5.4", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.14.1" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/superagent/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/supertest": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", - "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cookie-signature": "^1.2.2", - "methods": "^1.1.2", - "superagent": "^10.3.0" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/supertest/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3608,16 +2505,6 @@ "node": ">=14.0.0" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3631,30 +2518,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -3665,26 +2528,6 @@ "punycode": "^2.1.0" } }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", @@ -3881,13 +2724,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 8654a0a..a0bf815 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "numberstring", - "version": "1.0.1", - "description": "Number One Way to Makes Words from Numbers", + "version": "1.2.0", + "description": "Numbers to words in 22 languages, plus ordinals, Roman, NATO, Morse, scientific, hex, hieroglyphs and more. Zero dependencies.", "type": "module", "main": "index.js", "exports": { ".": { + "types": "./index.d.ts", "import": "./index.js" } }, @@ -15,39 +16,61 @@ "test:watch": "vitest", "test:coverage": "vitest run --coverage", "lint": "eslint . --format stylish", - "lint:report": "eslint . --format html -o coverage/lint-report.html" + "lint:report": "eslint . --format html -o coverage/lint-report.html", + "site:build": "node scripts/build-site.js", + "site": "node scripts/build-site.js && node scripts/serve-site.js", + "site:og": "sh scripts/build-og.sh" }, "repository": { "type": "git", "url": "git+https://github.com/brianfunk/numberstring.git" }, "keywords": [ - "number", - "string", - "word", - "words", - "integer", "bigint", - "text", + "binary", + "braille", + "bytes", + "camelcase", + "compact", "convert", + "cuneiform", + "decillion", + "emoji", "english", + "hex", + "hieroglyphs", + "i18n", + "icao", + "integer", + "mayan", + "morse", + "multilingual", + "nato", + "number", + "number-to-words", + "ordinal", "quintillion", - "decillion" + "roman", + "scientific-notation", + "snake-case", + "string", + "text", + "word", + "words" ], "author": "Brian Funk", "license": "MIT", "bugs": { "url": "https://github.com/brianfunk/numberstring/issues" }, - "homepage": "https://github.com/brianfunk/numberstring#readme", + "homepage": "https://numberstring.netlify.app", "engines": { "node": ">=18.0.0" }, "devDependencies": { "@vitest/coverage-v8": "^4.0.18", "eslint": "^9.17.0", - "express": "^4.21.2", - "supertest": "^7.0.0", "vitest": "^4.0.18" - } + }, + "types": "index.d.ts" } diff --git a/scripts/build-og.sh b/scripts/build-og.sh new file mode 100755 index 0000000..d143020 --- /dev/null +++ b/scripts/build-og.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh +# Rasterize site/og.svg to site/og.png (1200x630) with headless Chrome. +# Run after editing og.svg: npm run site:og +set -e +cd "$(dirname "$0")/.." +CHROME="${CHROME:-/Applications/Google Chrome.app/Contents/MacOS/Google Chrome}" +[ -x "$CHROME" ] || CHROME="$(command -v google-chrome || command -v chromium || command -v chrome)" +"$CHROME" --headless=new --disable-gpu --hide-scrollbars --force-device-scale-factor=1 \ + --window-size=1200,630 --screenshot="$PWD/site/og.png" "file://$PWD/site/og.svg" 2>/dev/null +echo "site/og.png rendered from site/og.svg" diff --git a/scripts/build-site.js b/scripts/build-site.js new file mode 100644 index 0000000..b23922b --- /dev/null +++ b/scripts/build-site.js @@ -0,0 +1,22 @@ +#!/usr/bin/env node +/** + * Stage the library into site/lib/ so the playground can import it with a + * relative URL. Netlify runs this as the build command; no bundler needed. + */ + +/* global process */ + +import { cpSync, mkdirSync, rmSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); +const lib = join(root, 'site', 'lib'); + +rmSync(lib, { recursive: true, force: true }); +mkdirSync(lib, { recursive: true }); +cpSync(join(root, 'index.js'), join(lib, 'index.js')); +cpSync(join(root, 'numerals.js'), join(lib, 'numerals.js')); +cpSync(join(root, 'languages'), join(lib, 'languages'), { recursive: true }); + +process.stdout.write('site/lib/ staged from index.js + numerals.js + languages/\n'); diff --git a/scripts/serve-site.js b/scripts/serve-site.js new file mode 100644 index 0000000..f9e980a --- /dev/null +++ b/scripts/serve-site.js @@ -0,0 +1,42 @@ +#!/usr/bin/env node +/** + * Minimal static server for local playground development. + * Run: npm run site (then open http://localhost:8080) + */ + +/* global process, URL */ + +import { createServer } from 'node:http'; +import { createReadStream, existsSync, statSync } from 'node:fs'; +import { dirname, extname, join, normalize } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const site = join(dirname(fileURLToPath(import.meta.url)), '..', 'site'); +const PORT = process.env.PORT || 8080; + +const TYPES = { + '.html': 'text/html; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.svg': 'image/svg+xml', + '.png': 'image/png', + '.ico': 'image/x-icon' +}; + +createServer((req, res) => { + const url = new URL(req.url, 'http://localhost'); + let file = join(site, normalize(decodeURIComponent(url.pathname))); + if (!file.startsWith(site)) { + res.writeHead(403).end(); + return; + } + if (existsSync(file) && statSync(file).isDirectory()) file = join(file, 'index.html'); + if (!existsSync(file)) { + res.writeHead(404, { 'Content-Type': 'text/plain' }).end('not found'); + return; + } + res.writeHead(200, { 'Content-Type': TYPES[extname(file)] || 'application/octet-stream' }); + createReadStream(file).pipe(res); +}).listen(PORT, () => { + process.stdout.write(`playground at http://localhost:${PORT}\n`); +}); diff --git a/server/package-lock.json b/server/package-lock.json deleted file mode 100644 index 473afe6..0000000 --- a/server/package-lock.json +++ /dev/null @@ -1,2786 +0,0 @@ -{ - "name": "numberstring-api", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "numberstring-api", - "version": "1.0.0", - "dependencies": { - "express": "^4.21.2" - }, - "devDependencies": { - "@vitest/coverage-v8": "^4.0.0", - "supertest": "^7.0.0", - "vitest": "^4.0.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@paralleldrive/cuid2": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", - "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@noble/hashes": "^1.1.5" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/coverage-v8": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", - "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.0.18", - "ast-v8-to-istanbul": "^0.3.10", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.2.0", - "magicast": "^0.5.1", - "obug": "^2.1.1", - "std-env": "^3.10.0", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "4.0.18", - "vitest": "4.0.18" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } - }, - "node_modules/@vitest/expect": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", - "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "chai": "^6.2.1", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", - "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.0.18", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", - "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", - "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.0.18", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", - "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.0.18", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", - "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", - "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.0.18", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/ast-v8-to-istanbul": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz", - "integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^10.0.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "node_modules/cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dezalgo": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "dev": true, - "license": "ISC", - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.14.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/formidable": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", - "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@paralleldrive/cuid2": "^2.2.2", - "dezalgo": "^1.0.4", - "once": "^1.4.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/magicast": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", - "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "source-map-js": "^1.2.1" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT" - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" - }, - "node_modules/superagent": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", - "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "component-emitter": "^1.3.1", - "cookiejar": "^2.1.4", - "debug": "^4.3.7", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.5", - "formidable": "^3.5.4", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.14.1" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/superagent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/superagent/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/superagent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/supertest": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", - "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cookie-signature": "^1.2.2", - "methods": "^1.1.2", - "superagent": "^10.3.0" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/supertest/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vitest": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", - "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.0.18", - "@vitest/mocker": "4.0.18", - "@vitest/pretty-format": "4.0.18", - "@vitest/runner": "4.0.18", - "@vitest/snapshot": "4.0.18", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^3.10.0", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.18", - "@vitest/browser-preview": "4.0.18", - "@vitest/browser-webdriverio": "4.0.18", - "@vitest/ui": "4.0.18", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - } - } -} diff --git a/site/app.js b/site/app.js new file mode 100644 index 0000000..f2f69ac --- /dev/null +++ b/site/app.js @@ -0,0 +1,171 @@ +import numberstring, { + comma, ordinal, roman, year, currency, telephone, fraction, + nth, compact, fancy, egyptian, babylonian, greek, chinese, japanese, nato, morse, + scientific, binary, octal, hex, bytes, bits +} from './lib/index.js'; + +const LANGS = [ + ['en', 'English'], ['es', 'Español'], ['fr', 'Français'], ['de', 'Deutsch'], + ['pt', 'Português'], ['it', 'Italiano'], ['nl', 'Nederlands'], ['da', 'Dansk'], + ['sv', 'Svenska'], ['no', 'Norsk'], ['fi', 'Suomi'], ['is', 'Íslenska'], + ['pl', 'Polski'], ['ru', 'Русский'], ['tr', 'Türkçe'], ['ar', 'العربية'], + ['hi', 'हिन्दी'], ['zh', '中文'], ['ja', '日本語'], ['ko', '한국어'], + ['id', 'Indonesia'], ['th', 'ไทย'] +]; +const RTL = new Set(['ar']); + +const $ = (id) => document.getElementById(id); +const input = $('n'); +const english = $('english'); +const error = $('error'); +const facts = $('facts'); +const langs = $('langs'); + +/** Turn the raw string into a number/BigInt plus flags, or null. */ +const interpret = (raw) => { + const str = raw.trim().replace(/[,_\s]/g, ''); + if (!/^-?\d+(\.\d+)?$/.test(str)) return null; + const negative = str.startsWith('-'); + const isDecimal = str.includes('.'); + const digits = str.replace('-', '').split('.')[0]; + let value; + if (isDecimal) value = Number(str); + else value = digits.length <= 15 ? Number(str) : BigInt(str); + return { str, value, negative, isDecimal, magnitude: digits.length }; +}; + +const row = (label, text, cls) => { + const dt = document.createElement('dt'); + dt.textContent = label; + const dd = document.createElement('dd'); + if (cls) dd.classList.add(cls); + if (text === false || text == null) { + dd.textContent = '—'; + dd.className = 'na'; + } else { + dd.textContent = text; + } + facts.append(dt, dd); +}; + +/** Extra row for the Chinese 大写 / Japanese 大字 financial numerals */ +const formalRow = (code, value) => { + const tr = document.createElement('tr'); + const c = document.createElement('td'); + c.className = 'code'; + c.textContent = code; + const n = document.createElement('td'); + n.className = 'name'; + n.textContent = code === 'zh' ? '大写' : '大字'; + const w = document.createElement('td'); + w.className = 'words'; + const fn = code === 'zh' ? chinese : japanese; + const out = value === null ? false : fn(value, { formal: true }); + w.textContent = out === false ? '—' : out; + tr.append(c, n, w); + return tr; +}; + +const render = (raw) => { + const parsed = interpret(raw); + facts.replaceChildren(); + langs.replaceChildren(); + + if (!parsed) { + english.textContent = ''; + error.hidden = raw.trim() === ''; + error.textContent = 'That is not a number I know how to say.'; + return; + } + + const words = numberstring(parsed.str); + if (words === false) { + english.textContent = ''; + error.hidden = false; + error.textContent = 'Too big. I stop at 10^36 − 1 (nine hundred ninety-nine decillion…).'; + return; + } + error.hidden = true; + english.textContent = words; + + const { value, negative, isDecimal } = parsed; + const wholeInt = !isDecimal && !negative; + const smallInt = wholeInt && typeof value === 'number'; + + row('comma', isDecimal ? false : comma(value)); + row('ordinal', wholeInt && value !== 0 && value !== 0n ? ordinal(value) : false); + row('roman', smallInt && value >= 1 && value <= 3999999999 ? roman(value) : false, 'roman'); + row('year', wholeInt && value >= 1 ? year(value) : false); + row('currency', !negative && typeof value === 'number' && value < 1e15 ? currency(`$${parsed.str}`) : false); + row('telephone', wholeInt && parsed.magnitude <= 15 ? telephone(parsed.str, { oh: true }) : false); + row('pilot', nato(parsed.str)); + row('scientific', scientific(parsed.str)); + row('binary', !isDecimal ? binary(value, { prefix: true }) : false, 'roman'); + row('octal', !isDecimal ? octal(value, { prefix: true }) : false, 'roman'); + row('hex', !isDecimal ? hex(value, { prefix: true }) : false, 'roman'); + row('bytes', wholeInt ? bytes(value) : false); + row('bits', wholeInt ? bits(value) : false); + row('morse', morse(parsed.str), 'roman'); + row('fraction', smallInt && value >= 2 ? fraction(1, value) : false); + row('british', wholeInt ? numberstring(value, { and: true }) : false); + row('nth', wholeInt ? nth(value) : false); + row('compact', compact(parsed.str)); + row('title', numberstring(parsed.str, { cap: 'title' })); + row('sentence', numberstring(parsed.str, { cap: 'sentence', punc: '.' })); + row('shout', numberstring(parsed.str, { cap: 'upper', punc: '!' })); + row('camelCase', numberstring(parsed.str, { cap: 'camel' }), 'roman'); + row('PascalCase', numberstring(parsed.str, { cap: 'pascal' }), 'roman'); + row('snake_case', numberstring(parsed.str, { cap: 'snake' }), 'roman'); + row('kebab-case', numberstring(parsed.str, { cap: 'kebab' }), 'roman'); + row('CONSTANT', numberstring(parsed.str, { cap: 'constant' }), 'roman'); + row('dot.case', numberstring(parsed.str, { cap: 'dot' }), 'roman'); + row('circled', fancy(parsed.str, 'circled')); + row('superscript', fancy(parsed.str, 'superscript')); + row('fullwidth', fancy(parsed.str, 'fullwidth')); + row('doublestruck', fancy(parsed.str, 'doublestruck')); + row('emoji', fancy(parsed.str, 'emoji')); + row('clocks', fancy(parsed.str, 'clock'), 'glyphs'); + row('braille', fancy(parsed.str, 'braille')); + row('egyptian', wholeInt ? egyptian(value) : false, 'glyphs'); + row('babylonian', wholeInt ? babylonian(value) : false, 'glyphs'); + row('greek', wholeInt ? greek(value) : false, 'glyphs'); + + for (const [code, name] of LANGS) { + const tr = document.createElement('tr'); + const c = document.createElement('td'); + c.className = 'code'; + c.textContent = code; + const n = document.createElement('td'); + n.className = 'name'; + n.textContent = name; + const w = document.createElement('td'); + w.className = 'words'; + if (RTL.has(code)) w.dir = 'rtl'; + const out = numberstring(parsed.str, { lang: code }); + w.textContent = out === false ? '—' : out; + tr.append(c, n, w); + langs.append(tr); + if (code === 'zh' || code === 'ja') langs.append(formalRow(code, wholeInt ? value : null)); + } +}; + +input.addEventListener('input', () => render(input.value)); +document.querySelectorAll('.chips button').forEach((b) => { + b.addEventListener('click', () => { + input.value = b.dataset.n; + render(input.value); + input.focus(); + }); +}); + +let fromHash = ''; +try { + fromHash = decodeURIComponent(location.hash.slice(1)); +} catch { + fromHash = ''; +} +if (fromHash) input.value = fromHash; +render(input.value); +input.addEventListener('change', () => { + history.replaceState(null, '', input.value ? `#${encodeURIComponent(input.value)}` : ' '); +}); diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..c51d1c0 --- /dev/null +++ b/site/index.html @@ -0,0 +1,90 @@ + + + + + + numberstring + + + + + + + + + + + + + + + + + + + + +
+
+ + + +

numberstring

+

Number One Way to Makes Words from Numbers

+
+ +
+ + +
+ + + + + + +
+
+ +
+

forty-two

+ +
+ +
+

Also

+
+
+ +
+

In 22 languages

+ + +
+
+ +
+

+ npm install numberstring + · npm + · GitHub + · zero dependencies, runs entirely in your browser +

+

### # " "

+
+
+ + + diff --git a/site/og.png b/site/og.png new file mode 100644 index 0000000..7d0e309 Binary files /dev/null and b/site/og.png differ diff --git a/site/og.svg b/site/og.svg new file mode 100644 index 0000000..44825a2 --- /dev/null +++ b/site/og.svg @@ -0,0 +1,10 @@ + + + numberstring + Number One Way to Makes Words from Numbers + 42 + forty-two + cuarenta y dos · quarante-deux · zweiundvierzig + 四十二 · сорок два · बयालीस · اثنان وأربعون + 22 languages · ordinals · NATO · Morse · scientific · hex · Roman · hieroglyphs · emoji · zero dependencies + diff --git a/site/style.css b/site/style.css new file mode 100644 index 0000000..ed13e4a --- /dev/null +++ b/site/style.css @@ -0,0 +1,152 @@ +:root { + --bg: #0f1115; + --panel: #171a21; + --fg: #e8e8e3; + --muted: #8b93a1; + --accent: #7ee787; + --accent-2: #f2cc60; + --border: #2a2f3a; + --error: #ff7b72; + --mono: ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; + --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; +} + +@media (prefers-color-scheme: light) { + :root { + --bg: #fbfbf8; + --panel: #ffffff; + --fg: #1b1f27; + --muted: #5d6571; + --accent: #1a7f37; + --accent-2: #9a6700; + --border: #e1e4e8; + --error: #cf222e; + } +} + +* { box-sizing: border-box; } + +body { + margin: 0; + background: var(--bg); + color: var(--fg); + font-family: var(--sans); + line-height: 1.5; +} + +main { + max-width: 960px; + margin: 0 auto; + padding: 24px 16px 64px; +} + +header { text-align: center; margin-bottom: 24px; } + +.ascii { + font-family: var(--mono); + font-size: clamp(4px, 1.1vw, 10px); + line-height: 1.15; + color: var(--accent); + margin: 0 auto 8px; + overflow: hidden; + white-space: pre; + /* header is centered; a pre would center each line separately and skew the art */ + text-align: left; + width: max-content; + max-width: 100%; +} + +h1 { + font-family: var(--mono); + font-size: 2rem; + margin: 0; + letter-spacing: -0.02em; +} + +.tagline { color: var(--muted); margin: 4px 0 0; font-style: italic; } + +.input-row { margin: 24px 0; } + +#n { + width: 100%; + font-family: var(--mono); + font-size: clamp(1.4rem, 4vw, 2.4rem); + padding: 14px 18px; + background: var(--panel); + color: var(--fg); + border: 2px solid var(--border); + border-radius: 12px; + outline: none; +} +#n:focus { border-color: var(--accent); } + +.chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; } +.chips button { + font-family: var(--mono); + font-size: 0.9rem; + padding: 6px 12px; + background: transparent; + color: var(--muted); + border: 1px solid var(--border); + border-radius: 999px; + cursor: pointer; +} +.chips button:hover { color: var(--fg); border-color: var(--accent); } +.chips sup { font-size: 0.7em; } + +.hero { min-height: 4rem; margin: 8px 0 32px; } +.english { + font-size: clamp(1.4rem, 3.5vw, 2.2rem); + font-weight: 600; + margin: 0; + color: var(--accent); + overflow-wrap: anywhere; +} +.error { color: var(--error); font-family: var(--mono); margin: 0; } + +h2 { + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--muted); + margin: 0 0 12px; +} + +.facts dl { + display: grid; + grid-template-columns: max-content 1fr; + gap: 6px 16px; + margin: 0 0 32px; +} +.facts dt { color: var(--muted); font-family: var(--mono); font-size: 0.9rem; } +.facts dd { margin: 0; overflow-wrap: anywhere; } +.facts dd.na { color: var(--muted); } +/* vinculum bars need room so adjacent overlines don't merge */ +.facts dd.roman { font-family: var(--mono); letter-spacing: 0.12em; } +/* historical glyph blocks render small in most fonts */ +.facts dd.glyphs { font-size: 1.35em; line-height: 1.2; letter-spacing: 0.05em; } +header a.home { color: inherit; text-decoration: none; display: inline-block; } +header a.home:hover .ascii { color: var(--accent-2); } + +table { width: 100%; border-collapse: collapse; } +td { padding: 8px 8px; border-top: 1px solid var(--border); vertical-align: top; } +td.code { font-family: var(--mono); color: var(--accent-2); width: 3ch; } +td.name { color: var(--muted); width: 11ch; white-space: nowrap; } +td.words { overflow-wrap: anywhere; } +td.words[dir="rtl"] { text-align: left; } + +footer { margin-top: 48px; color: var(--muted); font-size: 0.9rem; text-align: center; } +footer a { color: var(--fg); } +footer code { font-family: var(--mono); background: var(--panel); padding: 2px 6px; border-radius: 4px; } +.footnote { font-family: var(--mono); opacity: 0.5; } + +.visually-hidden { + position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; +} + +@media (max-width: 520px) { + .facts dl { grid-template-columns: 1fr; gap: 2px; } + .facts dd { margin-bottom: 8px; } + td.name { display: none; } +} diff --git a/test/extras.test.js b/test/extras.test.js new file mode 100644 index 0000000..071d1d3 --- /dev/null +++ b/test/extras.test.js @@ -0,0 +1,585 @@ +import { describe, it, expect } from 'vitest'; +import numberstring, { + ordinal, nth, compact, fancy, FANCY_STYLE_NAMES, nato, icao, military, morse, telephone, + scientific, radix, binary, octal, hex, bytes, bits, clock, CAP_STYLES, roman, decimal, currency, year, + egyptian, babylonian, greek, + chinese, japanese, toWords +} from '../index.js'; + +describe('and option (British style)', () => { + it('inserts and after hundreds', () => { + expect(numberstring(123, { and: true })).toBe('one hundred and twenty-three'); + expect(numberstring(101, { and: true })).toBe('one hundred and one'); + expect(numberstring(100, { and: true })).toBe('one hundred'); + }); + + it('inserts and before a final group under one hundred', () => { + expect(numberstring(1001, { and: true })).toBe('one thousand and one'); + expect(numberstring(2000001, { and: true })).toBe('two million and one'); + expect(numberstring(1101, { and: true })).toBe('one thousand one hundred and one'); + }); + + it('does not add and where nothing follows', () => { + expect(numberstring(1000000, { and: true })).toBe('one million'); + expect(numberstring(1000, { and: true })).toBe('one thousand'); + expect(numberstring(42, { and: true })).toBe('forty-two'); + }); + + it('is off by default', () => { + expect(numberstring(123)).toBe('one hundred twenty-three'); + expect(numberstring(1001)).toBe('one thousand one'); + }); + + it('applies to the integer part of decimals', () => { + expect(numberstring('123.4', { and: true })).toBe('one hundred and twenty-three point four'); + expect(numberstring(1001.5, { and: true })).toBe('one thousand and one point five'); + }); + + it('flows through negatives and ordinals', () => { + expect(numberstring(-1001, { and: true })).toBe('negative one thousand and one'); + expect(ordinal(101, { and: true })).toBe('one hundred and first'); + expect(numberstring(1001, { and: true, cap: 'title' })).toBe('One Thousand And One'); + }); +}); + +describe('nth', () => { + it('uses st, nd, rd, th', () => { + expect(nth(1)).toBe('1st'); + expect(nth(2)).toBe('2nd'); + expect(nth(3)).toBe('3rd'); + expect(nth(4)).toBe('4th'); + expect(nth(0)).toBe('0th'); + }); + + it('handles the teens', () => { + expect(nth(11)).toBe('11th'); + expect(nth(12)).toBe('12th'); + expect(nth(13)).toBe('13th'); + expect(nth(111)).toBe('111th'); + expect(nth(112)).toBe('112th'); + expect(nth(113)).toBe('113th'); + }); + + it('handles larger numbers', () => { + expect(nth(21)).toBe('21st'); + expect(nth(22)).toBe('22nd'); + expect(nth(23)).toBe('23rd'); + expect(nth(101)).toBe('101st'); + expect(nth(1000)).toBe('1000th'); + }); + + it('accepts strings, BigInt, and negatives', () => { + expect(nth('42')).toBe('42nd'); + expect(nth(10n ** 20n + 1n)).toBe('100000000000000000001st'); + expect(nth(-1)).toBe('-1st'); + }); + + it('rejects invalid input', () => { + expect(nth(1.5)).toBe(false); + expect(nth('abc')).toBe(false); + expect(nth(NaN)).toBe(false); + expect(nth(null)).toBe(false); + }); +}); + +describe('compact', () => { + it('leaves small numbers alone', () => { + expect(compact(999)).toBe('999'); + expect(compact(12)).toBe('12'); + expect(compact(0.5)).toBe('0.5'); + expect(compact(-7)).toBe('-7'); + }); + + it('abbreviates thousands and up', () => { + expect(compact(1000)).toBe('1K'); + expect(compact(1500)).toBe('1.5K'); + expect(compact(1234567)).toBe('1.2M'); + expect(compact(2300000000)).toBe('2.3B'); + expect(compact(1e12)).toBe('1T'); + expect(compact(10n ** 21n)).toBe('1Sx'); + }); + + it('carries when rounding crosses a scale', () => { + expect(compact(999950)).toBe('1M'); + expect(compact(999999)).toBe('1M'); + expect(compact(999.99)).toBe('1K'); + expect(compact(999.999, { digits: 2 })).toBe('1K'); + expect(compact(-999.99)).toBe('-1K'); + expect(compact(999.99, { long: true })).toBe('1 thousand'); + expect(compact(999.4)).toBe('999.4'); + }); + + it('handles negatives, strings, and decimals', () => { + expect(compact(-1500)).toBe('-1.5K'); + expect(compact('1500.75')).toBe('1.5K'); + }); + + it('honors digits and long options', () => { + expect(compact(1234567, { digits: 2 })).toBe('1.23M'); + expect(compact(1234567, { digits: 0 })).toBe('1M'); + expect(compact(1500000, { long: true })).toBe('1.5 million'); + expect(compact(2000, { long: true })).toBe('2 thousand'); + }); + + it('ignores leading zeros when picking a scale', () => { + expect(compact('0001')).toBe('1'); + expect(compact('0001000')).toBe('1K'); + expect(compact('00')).toBe('0'); + }); + + it('keeps the top scale instead of failing at the upper bound', () => { + expect(compact(10n ** 36n - 1n)).toBe('1000Dc'); + expect(compact(10n ** 36n - 1n, { long: true })).toBe('1000 decillion'); + }); + + it('rejects invalid input', () => { + expect(compact('abc')).toBe(false); + expect(compact(NaN)).toBe(false); + expect(compact(Infinity)).toBe(false); + expect(compact(1e37)).toBe(false); + expect(compact({})).toBe(false); + }); +}); + +describe('fancy', () => { + it('defaults to circled digits', () => { + expect(fancy(42)).toBe('④②'); + expect(fancy(0)).toBe('⓪'); + }); + + it('supports every listed style', () => { + const expected = { + circled: '④②', + superscript: '⁴²', + subscript: '₄₂', + fullwidth: '42', + bold: '𝟒𝟐', + doublestruck: '𝟜𝟚', + sans: '𝟦𝟤', + monospace: '𝟺𝟸', + keycap: '4️⃣2️⃣', + emoji: '4️⃣2️⃣', + clock: '🕓🕑', + braille: '⠼⠙⠃' + }; + expect([...FANCY_STYLE_NAMES].sort()).toEqual(Object.keys(expected).sort()); + for (const [style, out] of Object.entries(expected)) { + expect(fancy(42, style)).toBe(out); + } + }); + + it('maps minus signs and decimal points', () => { + expect(fancy(-3.5, 'fullwidth')).toBe('-3.5'); + expect(fancy(-3.5, 'superscript')).toBe('⁻³˙⁵'); + expect(fancy(-3.5, 'braille')).toBe('⠼⠤⠉⠨⠑'); + expect(fancy('-42', 'circled')).toBe('−④②'); + expect(fancy(814, 'clock')).toBe('🕗🕐🕓'); + expect(fancy(10.5, 'clock')).toBe('🕐🕛·🕔'); + }); + + it('handles BigInt and exponent-form integers', () => { + expect(fancy(10n ** 3n, 'doublestruck')).toBe('𝟙𝟘𝟘𝟘'); + expect(fancy(1e21, 'subscript')).toBe('₁₀₀₀₀₀₀₀₀₀₀₀₀₀₀₀₀₀₀₀₀₀'); + }); + + it('rejects unknown styles and bad input', () => { + expect(fancy(42, 'wingdings')).toBe(false); + expect(fancy('abc')).toBe(false); + expect(fancy(NaN)).toBe(false); + expect(fancy(1.5e-7)).toBe(false); + }); +}); + +describe('egyptian', () => { + it('repeats symbols additively', () => { + expect(egyptian(1)).toBe('𓏺'); + expect(egyptian(9)).toBe('𓏺'.repeat(9)); + expect(egyptian(10)).toBe('𓎆'); + expect(egyptian(42)).toBe('𓎆𓎆𓎆𓎆𓏺𓏺'); + expect(egyptian(1000)).toBe('𓆼'); + expect(egyptian(1000000)).toBe('𓁨'); + expect(egyptian(1234567)).toBe('𓁨𓆐𓆐𓂭𓂭𓂭𓆼𓆼𓆼𓆼𓍢𓍢𓍢𓍢𓍢𓎆𓎆𓎆𓎆𓎆𓎆𓏺𓏺𓏺𓏺𓏺𓏺𓏺'); + }); + + it('rejects zero and values beyond 9,999,999', () => { + expect(egyptian(0)).toBe(false); + expect(egyptian(10000000)).toBe(false); + expect(egyptian(-1)).toBe(false); + expect(egyptian(1.5)).toBe(false); + }); +}); + +describe('babylonian', () => { + it('writes base-60 places with tens and ones wedges', () => { + expect(babylonian(1)).toBe('𒁹'); + expect(babylonian(10)).toBe('𒌋'); + expect(babylonian(42)).toBe('𒌋𒌋𒌋𒌋𒁹𒁹'); + expect(babylonian(59)).toBe('𒌋𒌋𒌋𒌋𒌋𒁹𒁹𒁹𒁹𒁹𒁹𒁹𒁹𒁹'); + expect(babylonian(60)).toBe('𒁹 𒑊'); + expect(babylonian(61)).toBe('𒁹 𒁹'); + expect(babylonian(3600)).toBe('𒁹 𒑊 𒑊'); + expect(babylonian(1984)).toBe('𒌋𒌋𒌋𒁹𒁹𒁹 𒁹𒁹𒁹𒁹'); + }); + + it('uses the placeholder for zero', () => { + expect(babylonian(0)).toBe('𒑊'); + }); + + it('accepts BigInt and rejects invalid input', () => { + expect(babylonian(10n ** 18n)).toMatch(/^𒁹/); + expect(babylonian(-1)).toBe(false); + expect(babylonian('x')).toBe(false); + }); +}); + +describe('greek', () => { + it('uses Ionic letters with keraia', () => { + expect(greek(1)).toBe('αʹ'); + expect(greek(6)).toBe('ϛʹ'); + expect(greek(42)).toBe('μβʹ'); + expect(greek(90)).toBe('ϟʹ'); + expect(greek(900)).toBe('ϡʹ'); + expect(greek(1999)).toBe('͵αϡϟθʹ'); + expect(greek(2026)).toBe('͵βκϛʹ'); + expect(greek(1000)).toBe('͵αʹ'); + }); + + it('rejects zero and values over 9999', () => { + expect(greek(0)).toBe(false); + expect(greek(10000)).toBe(false); + expect(greek(-5)).toBe(false); + }); +}); + +describe('formal Chinese and Japanese numerals', () => { + it('uses 大写 financial forms in Chinese', () => { + expect(chinese(42, { formal: true })).toBe('肆拾贰'); + expect(chinese(10, { formal: true })).toBe('壹拾'); + expect(chinese(1001, { formal: true })).toBe('壹仟零壹'); + expect(chinese(123456, { formal: true })).toBe('壹拾贰万叁仟肆佰伍拾陆'); + expect(chinese(100000001, { formal: true })).toBe('壹亿零壹'); + expect(chinese(10001, { formal: true })).toBe('壹万零壹'); + expect(chinese(10010, { formal: true })).toBe('壹万零壹拾'); + expect(chinese(10100, { formal: true })).toBe('壹万零壹佰'); + expect(chinese(0, { formal: true })).toBe('零'); + }); + + it('uses 大字 forms in Japanese', () => { + expect(japanese(42, { formal: true })).toBe('四拾弐'); + expect(japanese(10, { formal: true })).toBe('壱拾'); + expect(japanese(1000, { formal: true })).toBe('壱千'); + expect(japanese(1001, { formal: true })).toBe('壱千壱'); + expect(japanese(123456, { formal: true })).toBe('壱拾弐万参千四百五拾六'); + expect(japanese(0, { formal: true })).toBe('零'); + expect(japanese(0)).toBe('ゼロ'); + }); + + it('is off by default and reachable through toWords', () => { + expect(chinese(42)).toBe('四十二'); + expect(japanese(1000)).toBe('千'); + expect(toWords(42, { lang: 'zh', formal: true })).toBe('肆拾贰'); + expect(toWords(10000, { lang: 'ja', formal: true })).toBe('壱万'); + }); +}); + +describe('language aliases', () => { + it('accepts bahasa for Indonesian', () => { + expect(numberstring(42, { lang: 'bahasa' })).toBe('empat puluh dua'); + }); +}); + +describe('nato', () => { + it('reads digits with ICAO pronunciations', () => { + expect(nato(1984)).toBe('wun niner ait fower'); + expect(nato(42)).toBe('fower too'); + expect(nato(0)).toBe('zero'); + expect(nato('007')).toBe('zero zero seven'); + expect(nato('000')).toBe('zero zero zero'); + expect(nato('0000')).toBe('zero zero zero zero'); + expect(nato('00100')).toBe('zero zero wun zero zero'); + expect(nato('01200')).toBe('zero wun too zero zero'); + expect(nato(10000)).toBe('wun zero tousand'); + }); + + it('reads whole hundreds and thousands as words', () => { + expect(nato(500)).toBe('fife hundred'); + expect(nato(1000)).toBe('wun tousand'); + expect(nato(2500)).toBe('too tousand fife hundred'); + expect(nato(11000)).toBe('wun wun tousand'); + expect(nato(25000)).toBe('too fife tousand'); + expect(nato(100000)).toBe('wun zero zero zero zero zero'); + }); + + it('handles decimals, negatives, BigInt, and options', () => { + expect(nato(3.14)).toBe('tree decimal wun fower'); + expect(nato('123.45')).toBe('wun too tree decimal fower fife'); + expect(nato(-7)).toBe('minus seven'); + expect(nato(10n ** 3n)).toBe('wun tousand'); + expect(nato(2500, { digits: true })).toBe('too fife zero zero'); + expect(nato(1984, { cap: 'upper' })).toBe('WUN NINER AIT FOWER'); + }); + + it('is also exported as icao and military', () => { + expect(icao).toBe(nato); + expect(military).toBe(nato); + }); + + it('rejects invalid input', () => { + expect(nato('abc')).toBe(false); + expect(nato(NaN)).toBe(false); + expect(nato(Infinity)).toBe(false); + expect(nato(null)).toBe(false); + }); +}); + +describe('morse', () => { + it('encodes digits', () => { + expect(morse(0)).toBe('-----'); + expect(morse(42)).toBe('....- ..---'); + expect(morse('1984')).toBe('.---- ----. ---.. ....-'); + expect(morse(10n ** 3n)).toBe('.---- ----- ----- -----'); + }); + + it('encodes point and minus', () => { + expect(morse(3.1)).toBe('...-- .-.-.- .----'); + expect(morse(-5)).toBe('-....- .....'); + }); + + it('rejects invalid input', () => { + expect(morse('sos')).toBe(false); + expect(morse(NaN)).toBe(false); + expect(morse(Infinity)).toBe(false); + }); +}); + +describe('telephone oh option', () => { + it('says oh for zero', () => { + expect(telephone('555-0100', { oh: true })).toBe('five five five oh one oh oh'); + expect(telephone('555-0100')).toBe('five five five zero one zero zero'); + expect(telephone(8675309, { oh: true, cap: 'title' })).toBe('Eight Six Seven Five Three Oh Nine'); + }); +}); + +describe('scientific', () => { + it('formats with a superscript exponent by default', () => { + expect(scientific(1984)).toBe('1.984 × 10³'); + expect(scientific(42)).toBe('4.2 × 10¹'); + expect(scientific(1)).toBe('1 × 10⁰'); + expect(scientific(0)).toBe('0 × 10⁰'); + expect(scientific(100)).toBe('1 × 10²'); + expect(scientific(0.00042)).toBe('4.2 × 10⁻⁴'); + expect(scientific('0.5')).toBe('5 × 10⁻¹'); + expect(scientific(-1500)).toBe('-1.5 × 10³'); + }); + + it('keeps an exact mantissa for floats, BigInt, and exponent-form input', () => { + expect(scientific(1.5e-7)).toBe('1.5 × 10⁻⁷'); + expect(scientific(-1.5e-7)).toBe('-1.5 × 10⁻⁷'); + expect(scientific(1e21)).toBe('1 × 10²¹'); + expect(scientific(6.02214076e23)).toBe('6.02214076 × 10²³'); + expect(scientific(10n ** 36n - 1n)).toBe('1 × 10³⁶'); + expect(scientific(123456789012345)).toBe('1.23456789012 × 10¹⁴'); + }); + + it('rounds to the requested significant digits with carry', () => { + expect(scientific(1984, { digits: 2 })).toBe('2 × 10³'); + expect(scientific(1984, { digits: 3 })).toBe('1.98 × 10³'); + expect(scientific(999, { digits: 2 })).toBe('1 × 10³'); + }); + + it('supports caret, e, and words formats', () => { + expect(scientific(1984, { format: 'caret' })).toBe('1.984 × 10^3'); + expect(scientific(1984, { format: 'e' })).toBe('1.984e3'); + expect(scientific(0.00042, { format: 'e' })).toBe('4.2e-4'); + expect(scientific(1984, { format: 'words' })).toBe('one point nine eight four times ten to the third'); + expect(scientific(0.00042, { format: 'words' })).toBe('four point two times ten to the negative fourth'); + expect(scientific(1, { format: 'words' })).toBe('one'); + expect(scientific(-0.00042, { format: 'words', cap: 'title' })).toBe('Negative Four Point Two Times Ten To The Negative Fourth'); + }); + + it('rejects invalid input and formats', () => { + expect(scientific('abc')).toBe(false); + expect(scientific(NaN)).toBe(false); + expect(scientific(Infinity)).toBe(false); + expect(scientific(1, { format: 'latex' })).toBe(false); + }); +}); + +describe('radix, binary, octal, hex', () => { + it('converts integers between bases', () => { + expect(binary(42)).toBe('101010'); + expect(octal(42)).toBe('52'); + expect(hex(42)).toBe('2a'); + expect(radix(42, 36)).toBe('16'); + expect(hex('255')).toBe('ff'); + expect(binary(10n ** 20n)).toBe((10n ** 20n).toString(2)); + }); + + it('supports prefix, upper, pad, and negatives', () => { + expect(hex(255, { prefix: true, upper: true })).toBe('0xFF'); + expect(binary(-5, { prefix: true })).toBe('-0b101'); + expect(binary(5, { pad: 8 })).toBe('00000101'); + expect(octal(8, { prefix: true })).toBe('0o10'); + expect(radix(42, 36, { prefix: true })).toBe('16'); + }); + + it('rejects bad bases and non-integers', () => { + expect(radix(42, 1)).toBe(false); + expect(radix(42, 37)).toBe(false); + expect(radix(1.5)).toBe(false); + expect(radix('x')).toBe(false); + expect(binary(NaN)).toBe(false); + }); +}); + +describe('bytes', () => { + it('uses decimal units by default', () => { + expect(bytes(0)).toBe('0 B'); + expect(bytes(999)).toBe('999 B'); + expect(bytes(1000)).toBe('1 KB'); + expect(bytes(1536)).toBe('1.5 KB'); + expect(bytes(1048576)).toBe('1 MB'); + expect(bytes(1536000)).toBe('1.5 MB'); + expect(bytes(10n ** 15n)).toBe('1 PB'); + expect(bytes(999999)).toBe('1 MB'); + }); + + it('uses binary units on request', () => { + expect(bytes(1536, { binary: true })).toBe('1.5 KiB'); + expect(bytes(1048576, { binary: true })).toBe('1 MiB'); + expect(bytes(1023, { binary: true })).toBe('1023 B'); + }); + + it('spells out long form with plurals', () => { + expect(bytes(1536, { long: true })).toBe('one point five kilobytes'); + expect(bytes(1, { long: true })).toBe('one byte'); + expect(bytes(1000, { long: true })).toBe('one kilobyte'); + expect(bytes(1048576, { binary: true, long: true })).toBe('one mebibyte'); + }); + + it('honors digits and rejects invalid input', () => { + expect(bytes(1536, { digits: 0 })).toBe('2 KB'); + expect(bytes(1234567890, { digits: 6 })).toBe('1.234568 GB'); + expect(bytes(1234567890, { digits: 3 })).toBe('1.235 GB'); + expect(bytes(100000000000123456000000000000000000n, { digits: 6 })).toBe('100000000000.123456 YB'); + expect(bytes(10n ** 30n)).toBe('1000000 YB'); + expect(bytes(1999, { digits: 0 })).toBe('2 KB'); + expect(bytes(999999999, { digits: 2 })).toBe('1 GB'); + expect(bytes(1536, { digits: 1.5 })).toBe('1.5 KB'); + expect(bytes(1536, { digits: NaN })).toBe('1.5 KB'); + expect(bits(1536, { digits: Infinity })).toBe('1.5 kb'); + expect(compact(1536, { digits: 2.7 })).toBe('1.54K'); + expect(scientific(1984, { digits: 2.9 })).toBe('2 × 10³'); + expect(bytes(-1)).toBe(false); + expect(bytes(1.5)).toBe(false); + expect(bytes('abc')).toBe(false); + }); +}); + +describe('bits', () => { + it('uses bandwidth-style units', () => { + expect(bits(0)).toBe('0 b'); + expect(bits(999)).toBe('999 b'); + expect(bits(1000)).toBe('1 kb'); + expect(bits(1500000)).toBe('1.5 Mb'); + expect(bits(10n ** 9n)).toBe('1 Gb'); + }); + + it('supports binary units and long form', () => { + expect(bits(1536, { binary: true })).toBe('1.5 Kib'); + expect(bits(1500000, { long: true })).toBe('one point five megabits'); + expect(bits(1, { long: true })).toBe('one bit'); + expect(bits(1048576, { binary: true, long: true })).toBe('one mebibit'); + }); + + it('rejects invalid input', () => { + expect(bits(-1)).toBe(false); + expect(bits(2.5)).toBe(false); + }); +}); + +describe('clock', () => { + it('maps hours to clock faces', () => { + expect(clock(1)).toBe('🕐'); + expect(clock(3)).toBe('🕒'); + expect(clock(12)).toBe('🕛'); + expect(clock(0)).toBe('🕛'); + expect(clock(24)).toBe('🕛'); + expect(clock(15)).toBe('🕒'); + }); + + it('rounds H:MM to the nearest half hour', () => { + expect(clock('3:30')).toBe('🕞'); + expect(clock('3:14')).toBe('🕒'); + expect(clock('3:15')).toBe('🕞'); + expect(clock('12:44')).toBe('🕧'); + expect(clock('12:45')).toBe('🕐'); + expect(clock('23:50')).toBe('🕛'); + expect(clock('0:30')).toBe('🕧'); + }); + + it('rejects invalid input', () => { + expect(clock(25)).toBe(false); + expect(clock(1.5)).toBe(false); + expect(clock('x')).toBe(false); + expect(clock('3:60')).toBe(false); + expect(clock(null)).toBe(false); + }); +}); + +describe('cap casing styles', () => { + it('joins words for code-style casing', () => { + expect(numberstring(123, { cap: 'camel' })).toBe('oneHundredTwentyThree'); + expect(numberstring(123, { cap: 'pascal' })).toBe('OneHundredTwentyThree'); + expect(numberstring(123, { cap: 'snake' })).toBe('one_hundred_twenty_three'); + expect(numberstring(123, { cap: 'kebab' })).toBe('one-hundred-twenty-three'); + expect(numberstring(123, { cap: 'hyphen' })).toBe('one-hundred-twenty-three'); + expect(numberstring(123, { cap: 'constant' })).toBe('ONE_HUNDRED_TWENTY_THREE'); + expect(numberstring(123, { cap: 'screaming' })).toBe('ONE_HUNDRED_TWENTY_THREE'); + expect(numberstring(123, { cap: 'dot' })).toBe('one.hundred.twenty.three'); + }); + + it('sentence case capitalizes only the first letter', () => { + expect(numberstring(123, { cap: 'sentence' })).toBe('One hundred twenty-three'); + expect(numberstring(-5, { cap: 'sentence' })).toBe('Negative five'); + }); + + it('works through delegated paths and other helpers', () => { + expect(numberstring(-3.5, { cap: 'snake' })).toBe('negative_three_point_five'); + expect(numberstring(-123, { cap: 'camel' })).toBe('negativeOneHundredTwentyThree'); + expect(numberstring(-123, { cap: 'pascal' })).toBe('NegativeOneHundredTwentyThree'); + expect(numberstring(-5n, { cap: 'title', punc: '!' })).toBe('Negative Five!'); + expect(numberstring('42', { cap: 'camel', punc: '!' })).toBe('fortyTwo!'); + expect(numberstring(42, { lang: 'es', cap: 'kebab' })).toBe('cuarenta-y-dos'); + expect(numberstring(1001, { and: true, cap: 'constant' })).toBe('ONE_THOUSAND_AND_ONE'); + expect(ordinal(21, { cap: 'camel' })).toBe('twentyFirst'); + expect(decimal(3.14, { cap: 'pascal' })).toBe('ThreePointOneFour'); + expect(currency('$1.50', { cap: 'snake' })).toBe('one_dollar_and_fifty_cents'); + expect(nato(1984, { cap: 'kebab' })).toBe('wun-niner-ait-fower'); + }); + + it('leaves unknown styles alone and exports the list', () => { + expect(numberstring(42, { cap: 'wingdings' })).toBe('forty-two'); + expect(CAP_STYLES).toContain('camel'); + expect(CAP_STYLES).toContain('snake'); + expect(CAP_STYLES).toContain('hyphen'); + expect(CAP_STYLES).toContain('screaming'); + expect(roman(4, { lower: true })).toBe('iv'); + }); +}); + +describe('year beyond 9999', () => { + it('reads far-future years as cardinals', () => { + expect(year(10000)).toBe('ten thousand'); + expect(year(8675309)).toBe('eight million six hundred seventy-five thousand three hundred nine'); + expect(year(10n ** 6n)).toBe('one million'); + expect(year(1984n)).toBe('nineteen eighty-four'); + expect(year(12345, { cap: 'title' })).toBe('Twelve Thousand Three Hundred Forty-Five'); + }); + + it('still rejects negatives and non-integers', () => { + expect(year(-1)).toBe(false); + expect(year(-1n)).toBe(false); + expect(year(1.5)).toBe(false); + }); +}); diff --git a/test/fuzz.test.js b/test/fuzz.test.js new file mode 100644 index 0000000..cc3d145 --- /dev/null +++ b/test/fuzz.test.js @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest'; +import * as lib from '../index.js'; + +/** + * Every public function must return a string, number, bigint, or false for + * any input and any options object. Nothing may throw. This is the contract + * the README promises, and the one that lets callers skip try/catch. + */ + +const INPUTS = [ + 0, -0, 1, -1, 0.1, -0.5, 1e21, -1e21, 1e-7, NaN, Infinity, -Infinity, + Number.MAX_SAFE_INTEGER, Number.MAX_VALUE, Number.MIN_VALUE, + 0n, -1n, 10n ** 36n, 10n ** 37n, -(10n ** 36n), + '', '0', '-0', '00', '1e5', 'abc', '٤٢', '42abc', ' 42 ', '1,000', '1.2.3', '.5', '5.', '-', '--5', '🕒', + null, undefined, true, false, {}, [], [42], () => 1, Symbol('x') +]; + +const OPTIONS = [ + undefined, null, {}, { cap: 'camel' }, { cap: 'nope' }, { cap: 42 }, { lang: 'es' }, { lang: 'xx' }, { lang: 42 }, + { and: true }, { formal: true }, { punc: '!' }, { punc: 42 }, { point: 'dot' }, { digits: 99 }, { digits: -1 }, + { format: 'words' }, { format: 'zzz' }, { binary: true, long: true }, { prefix: true, pad: 100 }, { lower: true }, + { vertical: true }, { oh: true } +]; + +const RESULT_TYPES = new Set(['string', 'number', 'bigint', 'boolean']); + +describe('fuzz: no public function throws', () => { + const fns = Object.entries(lib).filter(([, v]) => typeof v === 'function'); + + it.each(fns.map(([name]) => name))('%s', (name) => { + const fn = lib[name]; + for (const input of INPUTS) { + for (const opt of OPTIONS) { + let result; + expect(() => { result = fn(input, opt); }).not.toThrow(); + expect(RESULT_TYPES.has(typeof result)).toBe(true); + if (typeof result === 'boolean') expect(result).toBe(false); + } + } + }); + + it('fraction and radix also survive hostile second arguments', () => { + for (const a of INPUTS) { + for (const b of INPUTS) { + expect(() => lib.fraction(a, b)).not.toThrow(); + expect(() => lib.radix(a, b)).not.toThrow(); + } + } + }); +}); diff --git a/test/index.test.js b/test/index.test.js index bb64775..e1bb6e8 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -78,9 +78,10 @@ describe('numberstring', () => { expect(numberstring(5000000000000000)).toBe('five quadrillion'); }); - it('returns false for regular numbers exceeding MAX_SAFE_INTEGER', () => { - // Regular numbers lose precision above MAX_SAFE_INTEGER - expect(numberstring(Number.MAX_SAFE_INTEGER + 1)).toBe(false); + it('widens regular numbers above MAX_SAFE_INTEGER to BigInt', () => { + // Precision is whatever the float holds; the value is spoken as-is + expect(numberstring(Number.MAX_SAFE_INTEGER + 1)).toBe(numberstring(BigInt(Number.MAX_SAFE_INTEGER) + 1n)); + expect(numberstring(1e21)).toBe('one sextillion'); }); it('handles MAX_SAFE_INTEGER', () => { @@ -134,9 +135,9 @@ describe('numberstring', () => { expect(result).toContain('sextillion'); }); - it('returns false for negative BigInts', () => { - expect(numberstring(-1n)).toBe(false); - expect(numberstring(-1000000000000000000n)).toBe(false); + it('converts negative BigInts', () => { + expect(numberstring(-1n)).toBe('negative one'); + expect(numberstring(-1000000000000000000n)).toBe('negative one quintillion'); }); it('returns false for BigInts exceeding decillions', () => { @@ -152,22 +153,21 @@ describe('numberstring', () => { }); describe('invalid inputs', () => { - it('returns false for non-numbers', () => { + it('returns false for non-numeric strings', () => { expect(numberstring('one')).toBe(false); - expect(numberstring('123')).toBe(false); + expect(numberstring('12abc')).toBe(false); + expect(numberstring('1e5')).toBe(false); + expect(numberstring('')).toBe(false); }); - it('returns false for NaN', () => { + it('returns false for NaN and Infinity', () => { expect(numberstring(NaN)).toBe(false); + expect(numberstring(Infinity)).toBe(false); + expect(numberstring(-Infinity)).toBe(false); }); - it('returns false for negative numbers', () => { - expect(numberstring(-1)).toBe(false); - expect(numberstring(-100)).toBe(false); - }); - - it('returns false for numbers exceeding MAX_SAFE_INTEGER', () => { - expect(numberstring(Number.MAX_SAFE_INTEGER + 1)).toBe(false); + it('converts numbers just past MAX_SAFE_INTEGER via BigInt', () => { + expect(numberstring(Number.MAX_SAFE_INTEGER + 1)).toBe(numberstring(BigInt(Number.MAX_SAFE_INTEGER) + 1n)); }); it('returns false for boolean values', () => { @@ -186,6 +186,68 @@ describe('numberstring', () => { }); }); + describe('forgiving input', () => { + it('converts negative numbers', () => { + expect(numberstring(-1)).toBe('negative one'); + expect(numberstring(-100)).toBe('negative one hundred'); + }); + + it('converts decimals', () => { + expect(numberstring(1.5)).toBe('one point five'); + expect(numberstring(-3.14)).toBe('negative three point one four'); + expect(numberstring(0.25, { point: 'dot' })).toBe('zero dot two five'); + }); + + it('converts numeric strings', () => { + expect(numberstring('42')).toBe('forty-two'); + expect(numberstring(' 1000 ')).toBe('one thousand'); + expect(numberstring('-7')).toBe('negative seven'); + expect(numberstring('3.5')).toBe('three point five'); + expect(numberstring('-0.5')).toBe('negative zero point five'); + }); + + it('widens large numbers to BigInt instead of rejecting them', () => { + expect(numberstring(1e21)).toBe('one sextillion'); + expect(numberstring(-1e21)).toBe('negative one sextillion'); + expect(numberstring(1e21, { lang: 'de' })).toBe('eine Trilliarde'); + expect(numberstring(Number.MAX_VALUE)).toBe(false); + }); + + it('converts long numeric strings as BigInt', () => { + expect(numberstring('1000000000000000000000')).toBe('one sextillion'); + expect(numberstring('-1000000000000000000')).toBe('negative one quintillion'); + }); + + it('honors the lang option', () => { + expect(numberstring(42, { lang: 'es' })).toBe('cuarenta y dos'); + expect(numberstring(42, { lang: 'FR' })).toBe('quarante-deux'); + expect(numberstring(42, { lang: 'en' })).toBe('forty-two'); + expect(numberstring(42, { lang: 'klingon' })).toBe('forty-two'); + expect(numberstring('42', { lang: 'de' })).toBe('zweiundvierzig'); + }); + + it('applies cap and punc once across delegated paths', () => { + expect(numberstring(-7, { cap: 'title', punc: '.' })).toBe('Negative Seven.'); + expect(numberstring(3.5, { cap: 'upper', punc: '!' })).toBe('THREE POINT FIVE!'); + expect(numberstring(42, { lang: 'de', cap: 'title', punc: '?' })).toBe('Zweiundvierzig?'); + expect(numberstring('-2.5', { punc: '!' })).toBe('negative two point five!'); + }); + + it('does not fall back to English for negatives or decimals in other languages', () => { + expect(numberstring(-5, { lang: 'es' })).toBe(false); + expect(numberstring(1.5, { lang: 'fr' })).toBe(false); + expect(numberstring('-2.5', { lang: 'de' })).toBe(false); + expect(numberstring(-1n, { lang: 'ru' })).toBe(false); + expect(numberstring(-5, { lang: 'en' })).toBe('negative five'); + }); + + it('still rejects values beyond 10^36', () => { + expect(numberstring(-1e37)).toBe(false); + expect(numberstring(1e37)).toBe(false); + expect(numberstring(-(10n ** 36n))).toBe(false); + }); + }); + describe('capitalization options', () => { it('applies lowercase (default behavior)', () => { expect(numberstring(500, { cap: 'lower' })).toBe('five hundred'); @@ -384,6 +446,21 @@ describe('decimal', () => { it('returns false for invalid input', () => { expect(decimal('abc')).toBe(false); expect(decimal(NaN)).toBe(false); + expect(decimal(Infinity)).toBe(false); + expect(decimal(null)).toBe(false); + }); + + it('expands exponent-form integers instead of truncating', () => { + expect(decimal(1e21)).toBe('one sextillion'); + expect(decimal(-1e21)).toBe('negative one sextillion'); + }); + + it('returns false for exponent-form fractions', () => { + expect(decimal(1.5e-7)).toBe(false); + }); + + it('keeps precision for long integer parts in strings', () => { + expect(decimal('1000000000000000000000.5')).toBe('one sextillion point five'); }); }); }); @@ -517,8 +594,30 @@ describe('roman', () => { expect(roman(-1)).toBe(false); }); - it('returns false for numbers over 3999', () => { - expect(roman(4000)).toBe(false); + it('uses vinculum notation above 3999', () => { + const bar = '\u0305'; + const dbl = '\u033F'; + expect(roman(4000)).toBe(`I${bar}V${bar}`); + expect(roman(4001)).toBe(`I${bar}V${bar}I`); + expect(roman(1000000)).toBe(`M${bar}`); + expect(roman(3999999)).toBe(`M${bar}M${bar}M${bar}C${bar}M${bar}X${bar}C${bar}I${bar}X${bar}CMXCIX`); + expect(roman(4000000)).toBe(`I${dbl}V${dbl}`); + expect(roman(8675309)).toBe(`V${dbl}I${dbl}I${dbl}I${dbl}D${bar}C${bar}L${bar}X${bar}X${bar}V${bar}CCCIX`); + expect(roman(1000000000)).toBe(`M${dbl}`); + expect(roman(3999999999)).toMatch(/^M\u033FM\u033FM\u033F/); + }); + + it('skips empty middle groups', () => { + expect(roman(1000001)).toBe('M\u0305I'); + expect(roman(2000000000)).toBe('M\u033FM\u033F'); + }); + + it('lowercases barred numerals', () => { + expect(roman(4000, { lower: true })).toBe('i\u0305v\u0305'); + }); + + it('returns false above 3,999,999,999', () => { + expect(roman(4000000000)).toBe(false); }); it('returns false for non-integers', () => { @@ -614,6 +713,14 @@ describe('parse', () => { expect(parse('')).toBe(false); expect(parse(123)).toBe(false); }); + + it('rejects adjacent simple words that are not tens + ones', () => { + expect(parse('nineteen eighty-four')).toBe(false); + expect(parse('ten five')).toBe(false); + expect(parse('one two')).toBe(false); + expect(parse('twenty two')).toBe(22); + expect(parse('two thousand twenty-four')).toBe(2024); + }); }); }); @@ -628,13 +735,23 @@ describe('spanish', () => { }); it('converts hundreds', () => { - expect(spanish(100)).toBe('ciento'); + expect(spanish(100)).toBe('cien'); + expect(spanish(101)).toBe('ciento uno'); expect(spanish(500)).toBe('quinientos'); + expect(spanish(100000)).toBe('cien mil'); }); it('converts thousands', () => { expect(spanish(1000)).toBe('mil'); expect(spanish(2000)).toBe('dos mil'); + expect(spanish(21000)).toBe('veintiún mil'); + }); + + it('apocopates uno before scale words', () => { + expect(spanish(1000000)).toBe('un millón'); + expect(spanish(2000000)).toBe('dos millones'); + expect(spanish(101000000)).toBe('ciento un millones'); + expect(spanish(21)).toBe('veintiuno'); }); it('applies capitalization', () => { @@ -680,6 +797,10 @@ describe('chinese', () => { it('handles zeros in the middle', () => { expect(chinese(101)).toBe('一百零一'); expect(chinese(1001)).toBe('一千零一'); + expect(chinese(10001)).toBe('一万零一'); + expect(chinese(10010)).toBe('一万零一十'); + expect(chinese(100000001)).toBe('一亿零一'); + expect(chinese(100010000)).toBe('一亿零一万'); }); it('converts thousands and wan', () => { @@ -881,7 +1002,7 @@ describe('year', () => { it('returns false for invalid years', () => { expect(year(-1)).toBe(false); - expect(year(10000)).toBe(false); + expect(year(10000)).toBe('ten thousand'); expect(year(3.14)).toBe(false); }); }); diff --git a/test/languages.test.js b/test/languages.test.js new file mode 100644 index 0000000..1734f39 --- /dev/null +++ b/test/languages.test.js @@ -0,0 +1,510 @@ +import { describe, it, expect } from 'vitest'; +import { toWords } from '../index.js'; + +/** + * Per-language spot checks on the numbers that trip up converters: + * teens, 21/71/80/91 (French, Danish quirks), 100 vs 101 vs 110 (zero + * insertion, "cien"/"cem"), 1000 vs 1001 vs 2000 (plurals, cases), + * 21000 (apocope / gender before scale words), 100000 (lakh vs 十万), + * and 1 vs 2 vs 21 million (singular/dual/plural, gender agreement). + * + * Expected values were generated from the library and hand-reviewed. + * Cells flagged with a comment are plausible but unverified by a native + * speaker; if you know better, fix the language file and the expectation. + */ + +const CASES = { + // English + en: [ + [0, 'zero'], + [1, 'one'], + [11, 'eleven'], + [21, 'twenty-one'], + [71, 'seventy-one'], + [80, 'eighty'], + [91, 'ninety-one'], + [100, 'one hundred'], + [101, 'one hundred one'], + [110, 'one hundred ten'], + [200, 'two hundred'], + [1000, 'one thousand'], + [1001, 'one thousand one'], + [2000, 'two thousand'], + [21000, 'twenty-one thousand'], + [100000, 'one hundred thousand'], + [1000000, 'one million'], + [2000000, 'two million'], + [21000000, 'twenty-one million'], + ], + // Spanish + es: [ + [0, 'cero'], + [1, 'uno'], + [11, 'once'], + [21, 'veintiuno'], + [71, 'setenta y uno'], + [80, 'ochenta'], + [91, 'noventa y uno'], + [100, 'cien'], + [101, 'ciento uno'], + [110, 'ciento diez'], + [200, 'doscientos'], + [1000, 'mil'], + [1001, 'mil uno'], + [2000, 'dos mil'], + [21000, 'veintiún mil'], + [100000, 'cien mil'], + [1000000, 'un millón'], + [2000000, 'dos millones'], + [21000000, 'veintiún millones'], + ], + // French + fr: [ + [0, 'zéro'], + [1, 'un'], + [11, 'onze'], + [21, 'vingt et un'], + [71, 'soixante et onze'], + [80, 'quatre-vingts'], + [91, 'quatre-vingt-onze'], + [100, 'cent'], + [101, 'cent un'], + [110, 'cent dix'], + [200, 'deux cents'], + [1000, 'mille'], + [1001, 'mille un'], + [2000, 'deux mille'], + [21000, 'vingt et un mille'], + [100000, 'cent mille'], + [1000000, 'un million'], + [2000000, 'deux millions'], + [21000000, 'vingt et un millions'], + ], + // German + de: [ + [0, 'null'], + [1, 'eins'], + [11, 'elf'], + [21, 'einundzwanzig'], + [71, 'einundsiebzig'], + [80, 'achtzig'], + [91, 'einundneunzig'], + [100, 'einhundert'], + [101, 'einhunderteins'], + [110, 'einhundertzehn'], + [200, 'zweihundert'], + [1000, 'eintausend'], + [1001, 'eintausendeins'], + [2000, 'zweitausend'], + [21000, 'einundzwanzigtausend'], + [100000, 'einhunderttausend'], + [1000000, 'eine Million'], + [2000000, 'zwei Millionen'], + [21000000, 'einundzwanzig Millionen'], + ], + // Danish + // UNVERIFIED: Compound "ethundrede en" is understandable but spoken Danish prefers "hundrede og en"; "en"/"et" for 1 depends on gender + da: [ + [0, 'nul'], + [1, 'en'], + [11, 'elleve'], + [21, 'enogtyve'], + [71, 'enoghalvfjerds'], + [80, 'firs'], + [91, 'enoghalvfems'], + [100, 'ethundrede'], + [101, 'ethundrede en'], + [110, 'ethundrede ti'], + [200, 'tohundrede'], + [1000, 'ettusind'], + [1001, 'ettusind en'], + [2000, 'totusind'], + [21000, 'enogtyvetusind'], + [100000, 'ethundrede tusind'], + [1000000, 'en million'], + [2000000, 'to millioner'], + [21000000, 'enogtyve millioner'], + ], + // Chinese + zh: [ + [0, '零'], + [1, '一'], + [11, '十一'], + [21, '二十一'], + [71, '七十一'], + [80, '八十'], + [91, '九十一'], + [100, '一百'], + [101, '一百零一'], + [110, '一百一十'], + [200, '二百'], + [1000, '一千'], + [1001, '一千零一'], + [2000, '二千'], + [21000, '二万一千'], + [100000, '十万'], + [1000000, '一百万'], + [2000000, '二百万'], + [21000000, '二千一百万'], + ], + // Hindi + hi: [ + [0, 'शून्य'], + [1, 'एक'], + [11, 'ग्यारह'], + [21, 'इक्कीस'], + [71, 'इकहत्तर'], + [80, 'अस्सी'], + [91, 'इक्यानवे'], + [100, 'एक सौ'], + [101, 'एक सौ एक'], + [110, 'एक सौ दस'], + [200, 'दो सौ'], + [1000, 'एक हज़ार'], + [1001, 'एक हज़ार एक'], + [2000, 'दो हज़ार'], + [21000, 'इक्कीस हज़ार'], + [100000, 'एक लाख'], + [1000000, 'दस लाख'], + [2000000, 'बीस लाख'], + [21000000, 'दो करोड़ दस लाख'], + ], + // Russian + ru: [ + [0, 'ноль'], + [1, 'один'], + [11, 'одиннадцать'], + [21, 'двадцать один'], + [71, 'семьдесят один'], + [80, 'восемьдесят'], + [91, 'девяносто один'], + [100, 'сто'], + [101, 'сто один'], + [110, 'сто десять'], + [200, 'двести'], + [1000, 'одна тысяча'], + [1001, 'одна тысяча один'], + [2000, 'две тысячи'], + [21000, 'двадцать одна тысяча'], + [100000, 'сто тысяч'], + [1000000, 'один миллион'], + [2000000, 'два миллиона'], + [21000000, 'двадцать один миллион'], + ], + // Portuguese + pt: [ + [0, 'zero'], + [1, 'um'], + [11, 'onze'], + [21, 'vinte e um'], + [71, 'setenta e um'], + [80, 'oitenta'], + [91, 'noventa e um'], + [100, 'cem'], + [101, 'cento e um'], + [110, 'cento e dez'], + [200, 'duzentos'], + [1000, 'mil'], + [1001, 'mil e um'], + [2000, 'dois mil'], + [21000, 'vinte e um mil'], + [100000, 'cem mil'], + [1000000, 'um milhão'], + [2000000, 'dois milhões'], + [21000000, 'vinte e um milhões'], + ], + // Japanese + ja: [ + [0, 'ゼロ'], + [1, '一'], + [11, '十一'], + [21, '二十一'], + [71, '七十一'], + [80, '八十'], + [91, '九十一'], + [100, '百'], + [101, '百一'], + [110, '百十'], + [200, '二百'], + [1000, '千'], + [1001, '千一'], + [2000, '二千'], + [21000, '二万一千'], + [100000, '十万'], + [1000000, '百万'], + [2000000, '二百万'], + [21000000, '二千百万'], + ], + // Korean + ko: [ + [0, '영'], + [1, '일'], + [11, '십일'], + [21, '이십일'], + [71, '칠십일'], + [80, '팔십'], + [91, '구십일'], + [100, '백'], + [101, '백일'], + [110, '백십'], + [200, '이백'], + [1000, '천'], + [1001, '천일'], + [2000, '이천'], + [21000, '이만천'], + [100000, '십만'], + [1000000, '백만'], + [2000000, '이백만'], + [21000000, '이천백만'], + ], + // Arabic + ar: [ + [0, 'صفر'], + [1, 'واحد'], + [11, 'أحد عشر'], + [21, 'واحد وعشرون'], + [71, 'واحد وسبعون'], + [80, 'ثمانون'], + [91, 'واحد وتسعون'], + [100, 'مائة'], + [101, 'مائة وواحد'], + [110, 'مائة وعشرة'], + [200, 'مائتان'], + [1000, 'ألف'], + [1001, 'ألف وواحد'], + [2000, 'ألفان'], + [21000, 'واحد وعشرون ألف'], + [100000, 'مائة ألف'], + [1000000, 'مليون'], + [2000000, 'مليونان'], + [21000000, 'واحد وعشرون مليون'], + ], + // Italian + it: [ + [0, 'zero'], + [1, 'uno'], + [11, 'undici'], + [21, 'ventuno'], + [71, 'settantuno'], + [80, 'ottanta'], + [91, 'novantuno'], + [100, 'cento'], + [101, 'centouno'], + [110, 'centodieci'], + [200, 'duecento'], + [1000, 'mille'], + [1001, 'milleuno'], + [2000, 'duemila'], + [21000, 'ventunmila'], + [100000, 'centomila'], + [1000000, 'un milione'], + [2000000, 'due milioni'], + [21000000, 'ventuno milioni'], + ], + // Dutch + nl: [ + [0, 'nul'], + [1, 'een'], + [11, 'elf'], + [21, 'eenentwintig'], + [71, 'eenenzeventig'], + [80, 'tachtig'], + [91, 'eenennegentig'], + [100, 'honderd'], + [101, 'honderdeen'], + [110, 'honderdtien'], + [200, 'tweehonderd'], + [1000, 'duizend'], + [1001, 'duizendeen'], + [2000, 'tweeduizend'], + [21000, 'eenentwintigduizend'], + [100000, 'honderdduizend'], + [1000000, 'een miljoen'], + [2000000, 'twee miljoen'], + [21000000, 'eenentwintig miljoen'], + ], + // Turkish + tr: [ + [0, 'sıfır'], + [1, 'bir'], + [11, 'on bir'], + [21, 'yirmi bir'], + [71, 'yetmiş bir'], + [80, 'seksen'], + [91, 'doksan bir'], + [100, 'yüz'], + [101, 'yüz bir'], + [110, 'yüz on'], + [200, 'iki yüz'], + [1000, 'bin'], + [1001, 'bin bir'], + [2000, 'iki bin'], + [21000, 'yirmi bir bin'], + [100000, 'yüz bin'], + [1000000, 'bir milyon'], + [2000000, 'iki milyon'], + [21000000, 'yirmi bir milyon'], + ], + // Polish + pl: [ + [0, 'zero'], + [1, 'jeden'], + [11, 'jedenaście'], + [21, 'dwadzieścia jeden'], + [71, 'siedemdziesiąt jeden'], + [80, 'osiemdziesiąt'], + [91, 'dziewięćdziesiąt jeden'], + [100, 'sto'], + [101, 'sto jeden'], + [110, 'sto dziesięć'], + [200, 'dwieście'], + [1000, 'tysiąc'], + [1001, 'tysiąc jeden'], + [2000, 'dwa tysiące'], + [21000, 'dwadzieścia jeden tysięcy'], + [100000, 'sto tysięcy'], + [1000000, 'jeden milion'], + [2000000, 'dwa miliony'], + [21000000, 'dwadzieścia jeden milionów'], + ], + // Swedish + // UNVERIFIED: Standalone 1 is usually "ett", and 101 is commonly written "etthundraett" + sv: [ + [0, 'noll'], + [1, 'en'], + [11, 'elva'], + [21, 'tjugoen'], + [71, 'sjuttioen'], + [80, 'åttio'], + [91, 'nittioen'], + [100, 'etthundra'], + [101, 'etthundra en'], + [110, 'etthundra tio'], + [200, 'tvåhundra'], + [1000, 'ettusen'], + [1001, 'ettusen en'], + [2000, 'tvåtusen'], + [21000, 'tjugoentusen'], + [100000, 'etthundra tusen'], + [1000000, 'en miljon'], + [2000000, 'två miljoner'], + [21000000, 'tjugoen miljoner'], + ], + // Indonesian + id: [ + [0, 'nol'], + [1, 'satu'], + [11, 'sebelas'], + [21, 'dua puluh satu'], + [71, 'tujuh puluh satu'], + [80, 'delapan puluh'], + [91, 'sembilan puluh satu'], + [100, 'seratus'], + [101, 'seratus satu'], + [110, 'seratus sepuluh'], + [200, 'dua ratus'], + [1000, 'seribu'], + [1001, 'seribu satu'], + [2000, 'dua ribu'], + [21000, 'dua puluh satu ribu'], + [100000, 'seratus ribu'], + [1000000, 'satu juta'], + [2000000, 'dua juta'], + [21000000, 'dua puluh satu juta'], + ], + // Thai + th: [ + [0, 'ศูนย์'], + [1, 'หนึ่ง'], + [11, 'สิบเอ็ด'], + [21, 'ยี่สิบเอ็ด'], + [71, 'เจ็ดสิบเอ็ด'], + [80, 'แปดสิบ'], + [91, 'เก้าสิบเอ็ด'], + [100, 'หนึ่งร้อย'], + [101, 'หนึ่งร้อยเอ็ด'], + [110, 'หนึ่งร้อยสิบ'], + [200, 'สองร้อย'], + [1000, 'หนึ่งพัน'], + [1001, 'หนึ่งพันเอ็ด'], + [2000, 'สองพัน'], + [21000, 'สองหมื่นหนึ่งพัน'], + [100000, 'หนึ่งแสน'], + [1000000, 'หนึ่งล้าน'], + [2000000, 'สองล้าน'], + [21000000, 'ยี่สิบเอ็ดล้าน'], + ], + // Norwegian + // UNVERIFIED: Spoken Norwegian prefers "hundre og en" over "etthundre en" + no: [ + [0, 'null'], + [1, 'en'], + [11, 'elleve'], + [21, 'tjueen'], + [71, 'syttien'], + [80, 'åtti'], + [91, 'nittien'], + [100, 'etthundre'], + [101, 'etthundre en'], + [110, 'etthundre ti'], + [200, 'tohundre'], + [1000, 'ettusen'], + [1001, 'ettusen en'], + [2000, 'totusen'], + [21000, 'tjueentusen'], + [100000, 'etthundre tusen'], + [1000000, 'en million'], + [2000000, 'to millioner'], + [21000000, 'tjueen millioner'], + ], + // Finnish + fi: [ + [0, 'nolla'], + [1, 'yksi'], + [11, 'yksitoista'], + [21, 'kaksikymmentäyksi'], + [71, 'seitsemänkymmentäyksi'], + [80, 'kahdeksankymmentä'], + [91, 'yhdeksänkymmentäyksi'], + [100, 'sata'], + [101, 'satayksi'], + [110, 'satakymmenen'], + [200, 'kaksisataa'], + [1000, 'tuhat'], + [1001, 'tuhatyksi'], + [2000, 'kaksituhatta'], + [21000, 'kaksikymmentäyksituhatta'], + [100000, 'satatuhatta'], + [1000000, 'yksi miljoona'], + [2000000, 'kaksi miljoonaa'], + [21000000, 'kaksikymmentäyksi miljoonaa'], + ], + // Icelandic + is: [ + [0, 'núll'], + [1, 'einn'], + [11, 'ellefu'], + [21, 'tuttugu og einn'], + [71, 'sjötíu og einn'], + [80, 'áttatíu'], + [91, 'níutíu og einn'], + [100, 'eitt hundrað'], + [101, 'eitt hundrað einn'], + [110, 'eitt hundrað tíu'], + [200, 'tvö hundruð'], + [1000, 'eitt þúsund'], + [1001, 'eitt þúsund einn'], + [2000, 'tvö þúsund'], + [21000, 'tuttugu og eitt þúsund'], + [100000, 'eitt hundrað þúsund'], + [1000000, 'ein milljón'], + [2000000, 'tvær milljónir'], + [21000000, 'tuttugu og ein milljón'], + ], +}; + +describe.each(Object.entries(CASES))('language spot check: %s', (lang, cases) => { + it.each(cases)('%i', (n, expected) => { + expect(toWords(n, { lang })).toBe(expected); + }); +}); diff --git a/vitest.config.js b/vitest.config.js index 1fad8a2..4efb526 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -2,10 +2,11 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { + exclude: ['**/node_modules/**', 'archive/**'], coverage: { provider: 'v8', reporter: ['text', 'lcov', 'html'], - exclude: ['vitest.config.js', 'eslint.config.js'] + exclude: ['vitest.config.js', 'eslint.config.js', 'archive/**', 'site/**', 'scripts/**', 'demo.js'] } } });