koffing 0.5.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +64 -151
  3. package/dist/data-CP51LAc9.cjs +404 -0
  4. package/dist/data-CRHBhbCJ.js +345 -0
  5. package/dist/index.cjs +441 -0
  6. package/dist/index.d.cts +29 -0
  7. package/dist/index.d.ts +29 -0
  8. package/dist/index.js +433 -0
  9. package/dist/types-BVzP_Ko4.d.cts +62 -0
  10. package/dist/types-BVzP_Ko4.d.ts +62 -0
  11. package/dist/validator.cjs +3563 -0
  12. package/dist/validator.d.cts +14 -0
  13. package/dist/validator.d.ts +14 -0
  14. package/dist/validator.js +3562 -0
  15. package/package.json +46 -41
  16. package/Makefile +0 -21
  17. package/build/asset-manifest.json +0 -22
  18. package/build/favicon.png +0 -0
  19. package/build/index.html +0 -1
  20. package/build/precache-manifest.5382b78dea2866c8a94cee003da35d84.js +0 -26
  21. package/build/robots.txt +0 -3
  22. package/build/service-worker.js +0 -39
  23. package/build/static/css/main.102b9840.chunk.css +0 -2
  24. package/build/static/css/main.102b9840.chunk.css.map +0 -1
  25. package/build/static/js/2.4b3dd85a.chunk.js +0 -3
  26. package/build/static/js/2.4b3dd85a.chunk.js.LICENSE.txt +0 -67
  27. package/build/static/js/2.4b3dd85a.chunk.js.map +0 -1
  28. package/build/static/js/main.07f66bec.chunk.js +0 -2
  29. package/build/static/js/main.07f66bec.chunk.js.map +0 -1
  30. package/build/static/js/runtime-main.525bb0ff.js +0 -2
  31. package/build/static/js/runtime-main.525bb0ff.js.map +0 -1
  32. package/public/favicon.png +0 -0
  33. package/public/index.html +0 -30
  34. package/public/robots.txt +0 -3
  35. package/src/App.test.js +0 -9
  36. package/src/components/App.js +0 -18
  37. package/src/components/AppFooter/AppFooter.css.js +0 -12
  38. package/src/components/AppFooter/AppFooter.js +0 -31
  39. package/src/components/AppFooter/index.js +0 -3
  40. package/src/components/AppHeader/AppHeader.css.js +0 -21
  41. package/src/components/AppHeader/AppHeader.js +0 -34
  42. package/src/components/AppHeader/index.js +0 -3
  43. package/src/components/Board/Board.css.js +0 -21
  44. package/src/components/Board/Board.js +0 -110
  45. package/src/components/Board/index.js +0 -3
  46. package/src/components/BoardInput/BoardInput.css.js +0 -14
  47. package/src/components/BoardInput/BoardInput.js +0 -36
  48. package/src/components/BoardInput/index.js +0 -3
  49. package/src/components/BoardOutput/BoardOutput.css.js +0 -17
  50. package/src/components/BoardOutput/BoardOutput.js +0 -36
  51. package/src/components/BoardOutput/index.js +0 -3
  52. package/src/components/Code/Code.css.js +0 -10
  53. package/src/components/Code/Code.js +0 -28
  54. package/src/components/Code/index.js +0 -3
  55. package/src/components/StyledComponent.js +0 -24
  56. package/src/components/ThemedApp.js +0 -25
  57. package/src/core/Koffing.js +0 -71
  58. package/src/core/Pokemon.js +0 -181
  59. package/src/core/PokemonTeam.js +0 -78
  60. package/src/core/PokemonTeamSet.js +0 -52
  61. package/src/core/ShowdownParser.js +0 -280
  62. package/src/core/index.js +0 -7
  63. package/src/img/koffing-shiny.png +0 -0
  64. package/src/img/koffing.png +0 -0
  65. package/src/index.css +0 -42
  66. package/src/index.js +0 -17
  67. package/src/serviceWorker.js +0 -141
  68. package/src/setupTests.js +0 -5
  69. package/src/teams/example.koffing.js +0 -25
  70. package/src/tools/base64.js +0 -25
  71. package/src/tools/history.js +0 -17
  72. package/src/tools/index.js +0 -4
  73. package/src/variables.json +0 -5
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
- The MIT License
1
+ MIT License
2
2
 
3
- Copyright (c) 2018 Javi Aguilar (@itsjavi)
3
+ Copyright (c) 2022 Javi Aguilar
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,187 +1,100 @@
1
1
  # Koffing
2
- Koffing is a Pokemon Showdown Team parser that converts your strategies to
3
- machine-readable JSON code.
4
2
 
5
- ![koffing](src/img/koffing.png) 🔁![koffing](src/img/koffing-shiny.png)
3
+ A dependency-free Pokémon Showdown parser and formatter for JavaScript and TypeScript. Supports ESM and CommonJS. The API uses plain data and pure functions; it deliberately replaces the previous classes and `name`/`nickname` schema.
6
4
 
7
- [Pokémon Showdown](https://www.pokemonshowdown.com/) is the most popular online Pokémon battle simulator,
8
- one of the best ways to quickly try your strategies for the official Pokémon VGC Championships. This library is just
9
- a parser for the teams generated by the [Showdown Team Builder](https://play.pokemonshowdown.com/teambuilder) exports.
10
-
11
- This repository holds the source code for the library as well as the code for the Online Parser.
12
- The [Online Parser](https://itsjavi.github.io/koffing) does not require you to download
13
- any library or script, it is recommended if all you want is to sanitize, prettify or convert to JSON your
14
- Showdown team exports.
15
-
16
- ## Features
17
-
18
- - Compatible with web browsers and NodeJS, including ES6+ imports
19
- - Converts from Showdown code to JSON
20
- - Converts from JSON to Showdown code
21
- - Sanitizes Showdown code (removing invalid data, applying max and mins values, etc.)
22
- - Prettifies Showdown code (proper indentation, line breaks and sorting of the data, etc.)
23
- - [Online Parser](https://itsjavi.github.io/koffing), which is using the library
24
- featuring all the above mentioned.
25
-
26
- ## Installation
27
-
28
- As a package:
29
- ```bash
30
- npm i --save koffing
31
- ```
32
-
33
- In the browser, using a CDN:
34
- ```html
35
- <script src="https://cdn.rawgit.com/itsjavi/koffing/0.4.0/dist/koffing.min.js"></script>
36
- ```
37
-
38
- ## Usage
39
-
40
- ### Javascript (ES5, web)
41
- ```html
42
- <script src="dist/koffing.min.js"></script>
43
- <script>
44
- var teamCode = `=== [gen7] Folder 1/Example Team ===`;
45
- var parsedTeam = Koffing.parse(teamCode);
46
- console.log(parsedTeam);
47
-
48
- // Note that the ShowdownParser class, which is used internally by the Koffing class
49
- // is also exposed in the global scope:
50
- console.log(new ShowdownParser(teamCode).parse());
51
- </script>
5
+ ```sh
6
+ pnpm add koffing
52
7
  ```
53
8
 
54
- ### Javascript (ES6+, universal)
55
- ```js
56
- "use strict";
57
-
58
- import {Koffing} from 'koffing';
59
-
60
- const teamCode = `
61
- === [gen7] Folder 1/Example Team ===
62
-
63
- Smogon (Koffing) (F) @ Eviolite
64
- Level: 5
65
- Ability: Levitate
66
- Shiny: Yes
67
- Happiness: 255
68
- EVs: 36 HP / 236 Def / 236 SpD
69
- IVs: 31 HP / 30 Atk / 31 SpA / 30 SpD / 31 Spe
70
- Bold Nature
71
- - Will-O-Wisp
72
- - Pain Split
73
- - Sludge Bomb
74
- - Fire Blast`;
75
-
76
-
77
- let parsedTeam = Koffing.parse(teamCode);
78
-
79
- // This will log a PokémonTeamSet object:
80
- console.log(parsedTeam);
81
- // Convert it back to the Showdown format (prettified):
82
- console.log(parsedTeam.toShowdown());
83
- // which is the same as:
84
- console.log(parsedTeam.toString());
85
- // and the same as:
86
- console.log(parsedTeam + "");
87
- // You can also convert the object to JSON:
88
- console.log(parsedTeam.toJson());
89
- // which is the same as:
90
- console.log(Koffing.toJson(teamCode));
9
+ ```ts
10
+ import { parse, parseJSON, exportTeam, exportTeams } from "koffing";
11
+
12
+ const result = parse("Smogon (Koffing) @ Eviolite\nAbility: Levitate\n- Sludge Bomb");
13
+ console.log(result.diagnostics);
14
+ const sets = result.teams[0]!.pokemon;
15
+ // [{ species: "Koffing", name: "Smogon", item: "Eviolite", ... }]
16
+ const json = JSON.stringify(sets, null, 2); // Showdown-shaped set array
17
+ const imported = parseJSON(json);
18
+ const showdown = exportTeam(sets); // No team metadata header
19
+ const backup = exportTeams(imported.teams); // Preserves named team metadata
91
20
  ```
92
21
 
93
- ### NodeJS
94
- ```js
95
- const Koffing = require('koffing').Koffing;
96
-
97
- const teamCode = `=== [gen7] Folder 1/Example Team ===`;
98
- let parsedTeam = Koffing.parse(teamCode);
99
- console.log(parsedTeam);
100
- ```
101
-
102
-
103
- ## Class Reference
104
- The `Koffing` class is basically a wrapper of the `ShowdownParser` class, the difference is that it can be used statically.
105
-
106
- `ShowdownParser` class methods:
107
- - `parse(): PokemonTeamSet`: Parses the Showdown code and returns a `PokemonTeamSet` object.
108
- - `format(): String`: Parses the Showdown code and returns a prettified and sanitized version of it.
109
- - `toString(): String`: Basically returns the internal Showdown code string as it is.
110
-
111
- `Koffing` class methods:
112
- - `static parse(showdownCode): PokemonTeamSet`: Wrapper for `ShowdownParser.prototype.parse()`
113
- - `static format(showdownCode): String`: Wrapper for `ShowdownParser.prototype.format()`
114
- - `static toJson(showdownCode): String`: Wrapper for `ShowdownParser.prototype.parse().toJson()`
115
- - `static toShowdown(jsonCode): String`: Wrapper for `ShowdownParser.prototype.parse().toShowdown()`
22
+ ## Parsing and diagnostics
116
23
 
24
+ `parse(text, options?)` returns `{ teams, diagnostics }`. Each team contains `pokemon: PokemonSet[]` and optional `name`, `format`, and `folder`. Headerless input does not invent metadata. `parseJSON(value, options?)` accepts JSON text or plain data: a single set, an array of sets, a `{ pokemon, ...metadata }` team, or a `{ teams }` collection.
117
25
 
118
- ## React App
26
+ Set fields follow Showdown: required `species` and `moves`, optional `name` for a nickname, plus `item`, `ability`, `gender`, `nature`, `evs`, `ivs`, `level`, `shiny`, `happiness`, `pokeball`, `hpType`, `dynamaxLevel`, `gigantamax`, and `teraType`. Stat tables can be sparse.
119
27
 
120
- This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
28
+ Supported text includes nicknames, genders, item headers, team backups, `Trait`/`Friendship`/`Ball` aliases, `Item:`/`Nickname:`/`Species:`/`Move:`, bracketed abilities, bare `Shiny`/`Gigantamax`, and EV nature modifiers. Hidden Power moves use the internal `Hidden Power Ice` form with `hpType`, and export with brackets. Explicit happiness and Hidden Power fields take precedence over inferred values. Unknown species and move names are retained without database lookup.
121
29
 
122
- ### Available Scripts
30
+ Permissive mode is the default. Diagnostics identify unknown or malformed lines, duplicate fields, and suspicious numeric values; text diagnostics include one-based line numbers. Supported values are preserved, including levels over 100 and moves beyond four. Strict mode rejects these diagnostics. This is semantic preservation, not a byte-for-byte document editor: whitespace is normalized, repeated fields use the last accepted value, and unsupported lines are omitted with diagnostics. Always inspect diagnostics before replacing the original input.
123
31
 
124
- In the project directory, you can run:
32
+ ```ts
33
+ import { parse, KoffingError } from "koffing";
125
34
 
126
- #### `yarn start`
127
-
128
- Runs the app in the development mode.<br />
129
- Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
35
+ try {
36
+ parse(input, { mode: "strict" });
37
+ } catch (error) {
38
+ if (error instanceof KoffingError) console.error(error.diagnostics);
39
+ }
40
+ ```
130
41
 
131
- The page will reload if you make edits.<br />
132
- You will also see any lint errors in the console.
42
+ Strict mode throws on parsing diagnostics. Unsafe JSON structures, resource-limit violations, and packed team strings fail in either parsing mode. Packed formats are not interpreted as Pokémon names. Use upstream Showdown for pack/unpack support.
133
43
 
134
- #### `yarn test`
44
+ ## Parser scope and safety
135
45
 
136
- Launches the test runner in the interactive watch mode.<br />
137
- See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
46
+ Koffing retains generic sanity diagnostics for traditional numeric ranges, EV totals, and move counts. These parser checks do not use a game database. Species, moves, abilities, items, natures, balls, and types remain open strings when parsing. New text syntax or fields may still require parser support. The optional `koffing/validator` entry point adds snapshot-based identifier checks; learnsets and format legality remain outside its scope.
138
47
 
139
- #### `yarn build`
48
+ ### Optional identifier and basic legality validation
140
49
 
141
- Builds the app for production to the `build` folder.<br />
142
- It correctly bundles React in production mode and optimizes the build for the best performance.
50
+ ```ts
51
+ import { parse } from "koffing";
52
+ import { validate } from "koffing/validator";
143
53
 
144
- The build is minified and the filenames include the hashes.<br />
145
- Your app is ready to be deployed!
54
+ const parsed = parse("Koffing @ Eviolite\nAbility: Levitate\n- Sludge Bomb");
55
+ const result = validate({ teams: parsed.teams });
56
+ console.log(result.valid, result.diagnostics);
57
+ // Also accepts a set, set array, team wrapper, or JSON string, like parseJSON.
58
+ ```
146
59
 
147
- See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
60
+ This separate entry point keeps lookup data out of the core parser bundle. `validate(input, options?)` accepts unknown data without coercion or mutation and returns `{ valid, diagnostics }`. It checks runtime field types, known fields, finite integers, level 1–100, happiness 0–255, Dynamax level 0–10, IVs 0–31, EVs 0–255 with a 510 total, one to six Pokémon per team, and one to four distinct moves. EVs retain the historical 255 cap because no generation is assumed. Missing optional fields and empty optional strings remain unspecified. Shape failures return the first error; other checks can return multiple errors. Strict mode throws on diagnostics; resource-limit violations always throw. Inspect parser diagnostics separately when validating parsed text.
148
61
 
149
- #### `yarn eject`
62
+ Species, moves, items, abilities, natures, balls, Hidden Power types, and Tera types are checked against committed JSON tables. Display names and lowercase alphanumeric IDs compare equivalently; aliases are not resolved. Balls must be ball items; Hidden Power excludes Normal, Fairy, and Stellar. Typed Hidden Power variants count as one move for duplicate checks. Snapshot identifiers include past and nonstandard Showdown entries. A passing result is only a basic sanity check: it does not prove obtainability, species/ability compatibility, learnsets, event restrictions, generation availability, battle-form eligibility, or format legality. Species/item clauses and generation-specific EV rules are intentionally not imposed.
150
63
 
151
- **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
64
+ Run `pnpm --filter koffing lookups:generate` to dump sorted, deduplicated IDs from the lockfile-pinned development-only `pokemon-showdown` dependency into `src/lookups/*.json`; `source.json` records its version and scope. `pnpm --filter koffing lookups:check` detects stale tables without writing. Review and commit regenerated files when updating Showdown. Runtime validation has no Showdown dependency or network requests.
152
65
 
153
- If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
66
+ ### Validation and explicit sanitization
154
67
 
155
- Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
68
+ ```ts
69
+ import { validateTeam, sanitizeTeam } from "koffing";
156
70
 
157
- You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
71
+ const issues = validateTeam(sets);
72
+ const { pokemon, diagnostics } = sanitizeTeam(sets, {
73
+ maxLevel: 100,
74
+ maxMovesPerPokemon: 4,
75
+ });
76
+ ```
158
77
 
159
- ### Learn More
78
+ `validateTeam` reports generic numeric ranges, move counts, and EV totals. `sanitizeTeam` clones input, clamps supported numeric fields and truncates moves only when explicitly called, and reports changes. EVs use a 510-point budget allocated in HP, Atk, Def, SpA, SpD, Spe order, capped at 255 per stat. It is not a competitive legality validator. Sanitizing does not establish legality or choose an optimal EV distribution.
160
79
 
161
- You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
80
+ `exportTeam(pokemon)` and `exportTeams(teams)` follow Showdown's trusted-object approach: they read typed data directly without cloning, descriptor inspection, unknown-property checks, diagnostics, or resource budgets. They accept no parsing options. Extra properties are ignored and nonfinite numbers are omitted; finite values, explicit defaults, and extra moves are preserved. For untrusted input, call `parseJSON` or `validateTeam` explicitly before export. Export does not escape delimiters or control characters, so callers are responsible for supplying representable text fields; arbitrary strings need not round-trip. Exports are Showdown text, not HTML.
162
81
 
163
- To learn React, check out the [React documentation](https://reactjs.org/).
82
+ ## Resource limits
164
83
 
84
+ Parsing and validation entry points accept `limits` overrides. Defaults are 2,000,000 UTF-16 input code units, 16,384 code units per line, 1,000 teams, 10,000 Pokémon in total, 256 moves per Pokémon, and 1,000 diagnostics. Limits are resource budgets, not battle rules. Exceeding a limit throws; it never returns a silently truncated collection. Overrides must be positive safe integers.
165
85
 
166
- ## License
86
+ For object inputs to parsing and validation, the input budget conservatively counts known string lengths plus 32 units per string and 512 per set. Small custom budgets may therefore reject objects whose compact JSON would fit. Export does not impose input or output budgets.
167
87
 
168
- This software is copyrighted and licensed under the
169
- [MIT license](https://github.com/itsjavi/koffing/LICENSE).
88
+ ```ts
89
+ parse(input, { limits: { maxInputLength: 100_000, maxPokemon: 60 } });
90
+ ```
170
91
 
171
- ### Disclaimer
92
+ JSON parsing and validation accept ordinary data objects and reject accessor properties and non-plain records. Export reads properties directly, including getters. JavaScript proxies are executable objects and should not be treated as untrusted JSON; accept JSON text at trust boundaries.
172
93
 
173
- This software comes bundled with data and graphics extracted from the
174
- Pokémon series of video games. Some terminology from the Pokémon franchise is
175
- also necessarily used within the software itself. This is all the intellectual
176
- property of Nintendo, Creatures, inc., and GAME FREAK, inc. and is protected by
177
- various copyrights and trademarks.
94
+ ## Compatibility and verification
178
95
 
179
- The authors believe that the use of this intellectual property for a fan reference
180
- is covered by fair use and that the software is significantly impaired without said
181
- property included. Any use of this copyrighted property is at your own legal risk.
96
+ The compatibility target is Showdown's exported text and sparse `PokemonSet` shape. Koffing retains backup metadata separately. Formatting need not be byte-identical to Showdown: explicit defaults can remain visible and harmless whitespace differs. Species aliases, generation-specific defaults, Hidden Power IV inference, and legality checks require game data and are intentionally not inferred by the core. Call a game-data resolver or Showdown validator separately when needed.
182
97
 
183
- This software is not affiliated in any way with Nintendo,
184
- Pokémon or any other game company.
98
+ Differential tests compare supported semantic behavior with the official server implementation. The development-only reference version and source commit are recorded in `test/upstream.json`, and `pnpm-lock.yaml` fixes its installation. This dependency also supplies the optional validator's generated ID tables. Updating the reference requires reviewing its expectations and regenerating those tables. Additional tests cover client syntax, old fixtures, malformed inputs, limits, serialization boundaries, optional validation, and round trips. There are no web app tests.
185
99
 
186
- A complete revision history of this software is available from
187
- https://github.com/itsjavi/koffing
100
+ Run `pnpm test` for tests and `pnpm bench` for Vitest benchmarks. Benchmarks cover normal teams, large backups, and malformed long lines; they are observations rather than CI speed thresholds. Upstream and Koffing perform different validation and metadata work, so throughput is not a like-for-like measure of every feature.
@@ -0,0 +1,404 @@
1
+ //#region src/limits.ts
2
+ const DEFAULT_LIMITS = Object.freeze({
3
+ maxInputLength: 2e6,
4
+ maxLineLength: 16384,
5
+ maxTeams: 1e3,
6
+ maxPokemon: 1e4,
7
+ maxMoves: 256,
8
+ maxDiagnostics: 1e3
9
+ });
10
+ var KoffingError = class extends Error {
11
+ diagnostics;
12
+ constructor(message, diagnostics) {
13
+ super(message);
14
+ this.name = "KoffingError";
15
+ this.diagnostics = diagnostics;
16
+ }
17
+ };
18
+ function resolveLimits(options = {}) {
19
+ if (options.mode !== void 0 && options.mode !== "strict" && options.mode !== "permissive") throw new TypeError("mode must be strict or permissive");
20
+ if (options.limits === void 0) return DEFAULT_LIMITS;
21
+ const limits = {
22
+ ...DEFAULT_LIMITS,
23
+ ...options.limits
24
+ };
25
+ for (const [key, value] of Object.entries(limits)) if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(`${key} must be a positive safe integer`);
26
+ return limits;
27
+ }
28
+ function fail(code, message, line) {
29
+ throw new KoffingError(message, [{
30
+ code,
31
+ message,
32
+ severity: "error",
33
+ ...line === void 0 ? {} : { line }
34
+ }]);
35
+ }
36
+ function checkInput(input, limits) {
37
+ if (typeof input !== "string") throw new TypeError("Input must be a string");
38
+ if (input.length > limits.maxInputLength) fail("input-limit", `Input exceeds ${limits.maxInputLength} code units`);
39
+ }
40
+ function finish(diagnostics, options) {
41
+ if (options.mode === "strict" && diagnostics.length) throw new KoffingError(diagnostics[0].message, diagnostics);
42
+ }
43
+ function report(diagnostics, diagnostic, limits) {
44
+ if (diagnostics.length >= limits.maxDiagnostics) fail("diagnostic-limit", "Too many parsing issues", diagnostic.line);
45
+ diagnostics.push(diagnostic);
46
+ }
47
+ //#endregion
48
+ //#region src/data.ts
49
+ const strings = [
50
+ "species",
51
+ "name",
52
+ "item",
53
+ "ability",
54
+ "nature",
55
+ "pokeball",
56
+ "hpType",
57
+ "teraType"
58
+ ];
59
+ const numbers = [
60
+ "level",
61
+ "happiness",
62
+ "dynamaxLevel"
63
+ ];
64
+ const flags = ["shiny", "gigantamax"];
65
+ const stats = [
66
+ "hp",
67
+ "atk",
68
+ "def",
69
+ "spa",
70
+ "spd",
71
+ "spe"
72
+ ];
73
+ const statKeys = new Set(stats);
74
+ const teamKeys = /* @__PURE__ */ new Set([
75
+ "name",
76
+ "format",
77
+ "folder",
78
+ "pokemon"
79
+ ]);
80
+ const collectionKeys = /* @__PURE__ */ new Set(["teams"]);
81
+ const setKeys = /* @__PURE__ */ new Set([
82
+ ...strings,
83
+ ...numbers,
84
+ ...flags,
85
+ "gender",
86
+ "evs",
87
+ "ivs",
88
+ "moves"
89
+ ]);
90
+ function objectKeys(value, path, limits) {
91
+ if (typeof value !== "object" || value === null || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) fail("invalid-json", `${path} must be a plain object`);
92
+ const keys = Reflect.ownKeys(value);
93
+ if (keys.length > limits.maxDiagnostics + 32) fail("property-limit", `${path} contains too many properties`);
94
+ return keys;
95
+ }
96
+ function property(value, key, path, limits) {
97
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
98
+ if (typeof key !== "string" || !("value" in descriptor)) fail("invalid-json", `${path} contains an accessor or symbol`);
99
+ if (key.length > Math.max(32, limits.maxLineLength)) fail("property-limit", `${path} contains an oversized property name`);
100
+ return descriptor.value;
101
+ }
102
+ function object(value, path, limits) {
103
+ const keys = objectKeys(value, path, limits);
104
+ const result = Object.create(null);
105
+ for (const key of keys) result[key] = property(value, key, path, limits);
106
+ return result;
107
+ }
108
+ function array(value, path, maximum) {
109
+ if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) fail("invalid-json", `${path} must be an array`);
110
+ if (value.length > maximum) fail("collection-limit", `${path} exceeds ${maximum} entries`);
111
+ if (Reflect.ownKeys(value).length !== value.length + 1) fail("invalid-json", `${path} contains non-index properties or holes`);
112
+ const result = [];
113
+ for (let i = 0; i < value.length; i++) {
114
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(i));
115
+ if (!descriptor || !("value" in descriptor)) fail("invalid-json", `${path} must not contain holes or accessors`);
116
+ result.push(descriptor.value);
117
+ }
118
+ return result;
119
+ }
120
+ function text(value, path, limits, budget) {
121
+ if (typeof value !== "string" || /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/u.test(value) || value !== value.trim()) fail("invalid-text", `${path} must be trimmed text without control characters`);
122
+ if (value.length > limits.maxLineLength) fail("line-limit", `${path} exceeds ${limits.maxLineLength} code units`);
123
+ budget.used += value.length + 32;
124
+ if (budget.used > limits.maxInputLength) fail("input-limit", "Aggregate data exceeds configured input limit");
125
+ return value;
126
+ }
127
+ function finite(value, path) {
128
+ if (typeof value !== "number" || !Number.isFinite(value)) fail("invalid-number", `${path} must be finite`);
129
+ return value;
130
+ }
131
+ function unknownKeys(source, known, path, diagnostics, limits) {
132
+ for (const key of Object.keys(source)) if (!known.has(key)) report(diagnostics, {
133
+ code: "unknown-field",
134
+ severity: "warning",
135
+ path: `${path}.${key}`,
136
+ message: `Unknown field ${path}.${key}`
137
+ }, limits);
138
+ }
139
+ function readSet(value, path, diagnostics, limits, budget) {
140
+ const keys = objectKeys(value, path, limits);
141
+ budget.used += 512;
142
+ if (budget.used > limits.maxInputLength) fail("input-limit", "Aggregate data exceeds configured input limit");
143
+ const result = {
144
+ species: "",
145
+ moves: []
146
+ };
147
+ let hasMoves = false;
148
+ for (const rawKey of keys) {
149
+ const entry = property(value, rawKey, path, limits);
150
+ const key = rawKey;
151
+ if (!setKeys.has(key)) {
152
+ report(diagnostics, {
153
+ code: "unknown-field",
154
+ severity: "warning",
155
+ path: `${path}.${key}`,
156
+ message: `Unknown field ${path}.${key}`
157
+ }, limits);
158
+ continue;
159
+ }
160
+ if (entry === void 0 && key !== "species" && key !== "moves") continue;
161
+ switch (key) {
162
+ case "species":
163
+ case "name":
164
+ case "item":
165
+ case "ability":
166
+ case "nature":
167
+ case "pokeball":
168
+ case "hpType":
169
+ case "teraType":
170
+ result[key] = text(entry, `${path}.${key}`, limits, budget);
171
+ break;
172
+ case "level":
173
+ case "happiness":
174
+ case "dynamaxLevel":
175
+ result[key] = finite(entry, `${path}.${key}`);
176
+ break;
177
+ case "shiny":
178
+ case "gigantamax":
179
+ if (typeof entry !== "boolean") fail("invalid-boolean", `${path}.${key} must be boolean`);
180
+ result[key] = entry;
181
+ break;
182
+ case "gender":
183
+ if (entry !== "M" && entry !== "F" && entry !== "N" && entry !== "") fail("invalid-gender", `${path}.gender must be M, F, N or empty`);
184
+ result.gender = entry;
185
+ break;
186
+ case "evs":
187
+ case "ivs": {
188
+ const statPath = `${path}.${key}`;
189
+ const values = {};
190
+ for (const rawStat of objectKeys(entry, statPath, limits)) {
191
+ const number = property(entry, rawStat, statPath, limits);
192
+ const stat = rawStat;
193
+ if (!statKeys.has(stat)) report(diagnostics, {
194
+ code: "unknown-field",
195
+ severity: "warning",
196
+ path: `${statPath}.${stat}`,
197
+ message: `Unknown field ${statPath}.${stat}`
198
+ }, limits);
199
+ else if (number !== void 0) values[stat] = finite(number, `${statPath}.${stat}`);
200
+ }
201
+ result[key] = values;
202
+ break;
203
+ }
204
+ case "moves": {
205
+ hasMoves = true;
206
+ const moves = array(entry, `${path}.moves`, limits.maxMoves);
207
+ for (let index = 0; index < moves.length; index++) if (!text(moves[index], `${path}.moves[${index}]`, limits, budget)) fail("invalid-move", `${path}.moves[${index}] cannot be empty`);
208
+ result.moves = moves;
209
+ break;
210
+ }
211
+ }
212
+ }
213
+ if (!result.species) fail("invalid-species", `${path}.species is required`);
214
+ if (!hasMoves) fail("invalid-json", `${path}.moves must be an array`);
215
+ return result;
216
+ }
217
+ /** Decode and copy known data only. Shape errors are fatal; unknown fields are diagnosed. */
218
+ function parseJSON(input, options = {}) {
219
+ const limits = resolveLimits(options);
220
+ let value = input;
221
+ if (typeof value === "string") {
222
+ checkInput(value, limits);
223
+ try {
224
+ value = JSON.parse(value);
225
+ } catch {
226
+ fail("invalid-json", "Invalid JSON syntax");
227
+ }
228
+ }
229
+ const diagnostics = [];
230
+ const budget = { used: 0 };
231
+ let rawTeams;
232
+ if (Array.isArray(value)) rawTeams = [{ pokemon: value }];
233
+ else {
234
+ const source = object(value, "input", limits);
235
+ if (Object.hasOwn(source, "teams")) {
236
+ unknownKeys(source, collectionKeys, "input", diagnostics, limits);
237
+ rawTeams = array(source.teams, "teams", limits.maxTeams);
238
+ } else if (Object.hasOwn(source, "pokemon")) rawTeams = [source];
239
+ else rawTeams = [{ pokemon: [source] }];
240
+ }
241
+ if (rawTeams.length > limits.maxTeams) fail("collection-limit", "Too many teams");
242
+ let total = 0;
243
+ const teams = rawTeams.map((raw, index) => {
244
+ const path = `teams[${index}]`;
245
+ const source = object(raw, path, limits);
246
+ unknownKeys(source, teamKeys, path, diagnostics, limits);
247
+ const members = array(source.pokemon, `${path}.pokemon`, limits.maxPokemon - total);
248
+ total += members.length;
249
+ const team = { pokemon: members.map((member, i) => readSet(member, `${path}.pokemon[${i}]`, diagnostics, limits, budget)) };
250
+ for (const key of [
251
+ "name",
252
+ "format",
253
+ "folder"
254
+ ]) if (source[key] !== void 0) team[key] = text(source[key], `${path}.${key}`, limits, budget);
255
+ return team;
256
+ });
257
+ for (const [index, team] of teams.entries()) checkRanges(team.pokemon, `teams[${index}].pokemon`, diagnostics, limits);
258
+ finish(diagnostics, options);
259
+ return {
260
+ teams,
261
+ diagnostics
262
+ };
263
+ }
264
+ /** Generic traditional-format checks, not species or format legality validation. */
265
+ function checkRanges(pokemon, path, diagnostics, limits) {
266
+ const add = (code, path, message) => report(diagnostics, {
267
+ code,
268
+ severity: "warning",
269
+ path,
270
+ message
271
+ }, limits);
272
+ for (const [i, set] of pokemon.entries()) {
273
+ const prefix = `${path}[${i}]`;
274
+ const range = (value, min, max, field, code = "number-range") => {
275
+ if (value !== void 0 && (!Number.isInteger(value) || value < min || value > max)) add(code, `${prefix}.${field}`, `Expected an integer from ${min} to ${max}; value preserved`);
276
+ };
277
+ range(set.level, 1, 100, "level");
278
+ range(set.happiness, 0, 255, "happiness");
279
+ range(set.dynamaxLevel, 0, 10, "dynamaxLevel");
280
+ let evTotal = 0;
281
+ for (const stat of stats) {
282
+ const ev = set.evs?.[stat];
283
+ const iv = set.ivs?.[stat];
284
+ if (ev !== void 0) {
285
+ evTotal += ev;
286
+ range(ev, 0, 255, `evs.${stat}`, "stat-range");
287
+ }
288
+ if (iv !== void 0) range(iv, 0, 31, `ivs.${stat}`, "stat-range");
289
+ }
290
+ if (evTotal > 510) add("ev-total", `${prefix}.evs`, "Traditional EV total exceeds 510");
291
+ if (set.moves.length > 4) add("move-count", `${prefix}.moves`, "Traditional sets contain at most four moves");
292
+ }
293
+ }
294
+ function validateTeam(pokemon, options = {}) {
295
+ return parseJSON(pokemon, options).diagnostics;
296
+ }
297
+ /** Explicit, deterministic traditional-range sanitization; never mutates its input. */
298
+ function sanitizeTeam(pokemon, options = {}) {
299
+ const maxLevel = options.maxLevel ?? 100;
300
+ const maxMoves = options.maxMovesPerPokemon ?? 4;
301
+ if (!Number.isSafeInteger(maxLevel) || maxLevel < 1 || !Number.isSafeInteger(maxMoves) || maxMoves < 0) throw new TypeError("Sanitization limits must be valid nonnegative integers (maxLevel >= 1)");
302
+ const parsed = parseJSON(pokemon, {
303
+ ...options,
304
+ mode: "permissive"
305
+ });
306
+ const result = parsed.teams[0].pokemon;
307
+ const diagnostics = parsed.diagnostics;
308
+ const limits = resolveLimits(options);
309
+ const changed = (path) => report(diagnostics, {
310
+ code: "sanitized",
311
+ severity: "warning",
312
+ path,
313
+ message: `Sanitized ${path}`
314
+ }, limits);
315
+ const clamp = (value, min, max, path) => {
316
+ const next = Math.max(min, Math.min(max, Math.trunc(value)));
317
+ if (next !== value) changed(path);
318
+ return next;
319
+ };
320
+ for (const [i, set] of result.entries()) {
321
+ const path = `pokemon[${i}]`;
322
+ if (set.level !== void 0) set.level = clamp(set.level, 1, maxLevel, `${path}.level`);
323
+ if (set.happiness !== void 0) set.happiness = clamp(set.happiness, 0, 255, `${path}.happiness`);
324
+ if (set.dynamaxLevel !== void 0) set.dynamaxLevel = clamp(set.dynamaxLevel, 0, 10, `${path}.dynamaxLevel`);
325
+ let evBudget = 510;
326
+ for (const stat of stats) {
327
+ if (set.evs?.[stat] !== void 0) {
328
+ set.evs[stat] = clamp(set.evs[stat], 0, Math.min(255, evBudget), `${path}.evs.${stat}`);
329
+ evBudget -= set.evs[stat];
330
+ }
331
+ if (set.ivs?.[stat] !== void 0) set.ivs[stat] = clamp(set.ivs[stat], 0, 31, `${path}.ivs.${stat}`);
332
+ }
333
+ if (set.moves.length > maxMoves) {
334
+ set.moves = set.moves.slice(0, maxMoves);
335
+ changed(`${path}.moves`);
336
+ }
337
+ }
338
+ finish(diagnostics, options);
339
+ return {
340
+ pokemon: result,
341
+ diagnostics
342
+ };
343
+ }
344
+ //#endregion
345
+ Object.defineProperty(exports, "DEFAULT_LIMITS", {
346
+ enumerable: true,
347
+ get: function() {
348
+ return DEFAULT_LIMITS;
349
+ }
350
+ });
351
+ Object.defineProperty(exports, "KoffingError", {
352
+ enumerable: true,
353
+ get: function() {
354
+ return KoffingError;
355
+ }
356
+ });
357
+ Object.defineProperty(exports, "checkInput", {
358
+ enumerable: true,
359
+ get: function() {
360
+ return checkInput;
361
+ }
362
+ });
363
+ Object.defineProperty(exports, "fail", {
364
+ enumerable: true,
365
+ get: function() {
366
+ return fail;
367
+ }
368
+ });
369
+ Object.defineProperty(exports, "finish", {
370
+ enumerable: true,
371
+ get: function() {
372
+ return finish;
373
+ }
374
+ });
375
+ Object.defineProperty(exports, "parseJSON", {
376
+ enumerable: true,
377
+ get: function() {
378
+ return parseJSON;
379
+ }
380
+ });
381
+ Object.defineProperty(exports, "report", {
382
+ enumerable: true,
383
+ get: function() {
384
+ return report;
385
+ }
386
+ });
387
+ Object.defineProperty(exports, "resolveLimits", {
388
+ enumerable: true,
389
+ get: function() {
390
+ return resolveLimits;
391
+ }
392
+ });
393
+ Object.defineProperty(exports, "sanitizeTeam", {
394
+ enumerable: true,
395
+ get: function() {
396
+ return sanitizeTeam;
397
+ }
398
+ });
399
+ Object.defineProperty(exports, "validateTeam", {
400
+ enumerable: true,
401
+ get: function() {
402
+ return validateTeam;
403
+ }
404
+ });