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.
- package/LICENSE +2 -2
- package/README.md +64 -151
- package/dist/data-CP51LAc9.cjs +404 -0
- package/dist/data-CRHBhbCJ.js +345 -0
- package/dist/index.cjs +441 -0
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +433 -0
- package/dist/types-BVzP_Ko4.d.cts +62 -0
- package/dist/types-BVzP_Ko4.d.ts +62 -0
- package/dist/validator.cjs +3563 -0
- package/dist/validator.d.cts +14 -0
- package/dist/validator.d.ts +14 -0
- package/dist/validator.js +3562 -0
- package/package.json +46 -41
- package/Makefile +0 -21
- package/build/asset-manifest.json +0 -22
- package/build/favicon.png +0 -0
- package/build/index.html +0 -1
- package/build/precache-manifest.5382b78dea2866c8a94cee003da35d84.js +0 -26
- package/build/robots.txt +0 -3
- package/build/service-worker.js +0 -39
- package/build/static/css/main.102b9840.chunk.css +0 -2
- package/build/static/css/main.102b9840.chunk.css.map +0 -1
- package/build/static/js/2.4b3dd85a.chunk.js +0 -3
- package/build/static/js/2.4b3dd85a.chunk.js.LICENSE.txt +0 -67
- package/build/static/js/2.4b3dd85a.chunk.js.map +0 -1
- package/build/static/js/main.07f66bec.chunk.js +0 -2
- package/build/static/js/main.07f66bec.chunk.js.map +0 -1
- package/build/static/js/runtime-main.525bb0ff.js +0 -2
- package/build/static/js/runtime-main.525bb0ff.js.map +0 -1
- package/public/favicon.png +0 -0
- package/public/index.html +0 -30
- package/public/robots.txt +0 -3
- package/src/App.test.js +0 -9
- package/src/components/App.js +0 -18
- package/src/components/AppFooter/AppFooter.css.js +0 -12
- package/src/components/AppFooter/AppFooter.js +0 -31
- package/src/components/AppFooter/index.js +0 -3
- package/src/components/AppHeader/AppHeader.css.js +0 -21
- package/src/components/AppHeader/AppHeader.js +0 -34
- package/src/components/AppHeader/index.js +0 -3
- package/src/components/Board/Board.css.js +0 -21
- package/src/components/Board/Board.js +0 -110
- package/src/components/Board/index.js +0 -3
- package/src/components/BoardInput/BoardInput.css.js +0 -14
- package/src/components/BoardInput/BoardInput.js +0 -36
- package/src/components/BoardInput/index.js +0 -3
- package/src/components/BoardOutput/BoardOutput.css.js +0 -17
- package/src/components/BoardOutput/BoardOutput.js +0 -36
- package/src/components/BoardOutput/index.js +0 -3
- package/src/components/Code/Code.css.js +0 -10
- package/src/components/Code/Code.js +0 -28
- package/src/components/Code/index.js +0 -3
- package/src/components/StyledComponent.js +0 -24
- package/src/components/ThemedApp.js +0 -25
- package/src/core/Koffing.js +0 -71
- package/src/core/Pokemon.js +0 -181
- package/src/core/PokemonTeam.js +0 -78
- package/src/core/PokemonTeamSet.js +0 -52
- package/src/core/ShowdownParser.js +0 -280
- package/src/core/index.js +0 -7
- package/src/img/koffing-shiny.png +0 -0
- package/src/img/koffing.png +0 -0
- package/src/index.css +0 -42
- package/src/index.js +0 -17
- package/src/serviceWorker.js +0 -141
- package/src/setupTests.js +0 -5
- package/src/teams/example.koffing.js +0 -25
- package/src/tools/base64.js +0 -25
- package/src/tools/history.js +0 -17
- package/src/tools/index.js +0 -4
- package/src/variables.json +0 -5
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
+
```ts
|
|
33
|
+
import { parse, KoffingError } from "koffing";
|
|
125
34
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
35
|
+
try {
|
|
36
|
+
parse(input, { mode: "strict" });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error instanceof KoffingError) console.error(error.diagnostics);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
130
41
|
|
|
131
|
-
|
|
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
|
-
|
|
44
|
+
## Parser scope and safety
|
|
135
45
|
|
|
136
|
-
|
|
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
|
-
|
|
48
|
+
### Optional identifier and basic legality validation
|
|
140
49
|
|
|
141
|
-
|
|
142
|
-
|
|
50
|
+
```ts
|
|
51
|
+
import { parse } from "koffing";
|
|
52
|
+
import { validate } from "koffing/validator";
|
|
143
53
|
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
+
### Validation and explicit sanitization
|
|
154
67
|
|
|
155
|
-
|
|
68
|
+
```ts
|
|
69
|
+
import { validateTeam, sanitizeTeam } from "koffing";
|
|
156
70
|
|
|
157
|
-
|
|
71
|
+
const issues = validateTeam(sets);
|
|
72
|
+
const { pokemon, diagnostics } = sanitizeTeam(sets, {
|
|
73
|
+
maxLevel: 100,
|
|
74
|
+
maxMovesPerPokemon: 4,
|
|
75
|
+
});
|
|
76
|
+
```
|
|
158
77
|
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
88
|
+
```ts
|
|
89
|
+
parse(input, { limits: { maxInputLength: 100_000, maxPokemon: 60 } });
|
|
90
|
+
```
|
|
170
91
|
|
|
171
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
});
|