@shayc/open-board-format 0.4.2 → 0.6.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/CHANGELOG.md +20 -0
- package/README.md +44 -8
- package/dist/index.d.mts +114 -20
- package/dist/index.mjs +184 -49
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @shayc/open-board-format
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0977576: Align board validation with the OBF spec:
|
|
8
|
+
|
|
9
|
+
- **Buttons:** absolute positioning now requires all of `top`, `left`, `width`, and `height` together (or none of them), matching the spec's "all four attributes are required for all buttons" rule. A partial positioning set (e.g. only `top`) is now rejected; grid-only buttons are unaffected.
|
|
10
|
+
- **OBZ manifest:** `paths.images` is now optional (consistent with `paths.sounds`), so manifests that omit empty image/sound maps validate. `paths.boards` remains required.
|
|
11
|
+
|
|
12
|
+
## 0.5.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- 24535c7: Throw a typed `OBFError` instead of plain `Error`. Every failure now carries a discriminated `error.info`, so consumers branch on `error.info.code` (e.g. `"missing-resource"`, `"invalid-board"`, `"not-zip"`) and read structured fields off each variant — no message parsing.
|
|
17
|
+
|
|
18
|
+
- New exports: `OBFError` (class), and the `OBFErrorInfo`, `OBFErrorCode`, and `OBFIssue` types.
|
|
19
|
+
- Validation failures (`invalid-board`, `invalid-manifest`) expose the Zod `issues` list on `error.info`. The underlying error — the `ZodError`, or the `JSON.parse`/fflate failure for `not-json`/`unreadable-zip`/`zip-failed` — is always on the standard `error.cause` and never duplicated on `info`.
|
|
20
|
+
- An `internal` code marks a library-invariant violation (a bug here), not something callers can recover from.
|
|
21
|
+
- **Breaking:** thrown errors are now `OBFError` (`error.name` is `"OBFError"`) and message strings changed. Branch on `error.info.code` rather than matching `error.message`. The internal `buildJsonParseErrorMessage` helper was removed.
|
|
22
|
+
|
|
3
23
|
## 0.4.2
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -175,16 +175,52 @@ import { OBFButtonSchema, OBFManifestSchema } from "@shayc/open-board-format";
|
|
|
175
175
|
|
|
176
176
|
## Errors
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
Every failure throws an `OBFError`. Branch on `error.info.code` — a discriminated union where each `code` carries exactly the fields relevant to that failure. Don't match on `error.message`; the message is human-readable and may change between releases.
|
|
179
179
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
180
|
+
```ts
|
|
181
|
+
import { loadBoard, OBFError } from "@shayc/open-board-format";
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
await loadBoard(file);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (!(error instanceof OBFError)) throw error;
|
|
187
|
+
|
|
188
|
+
switch (error.info.code) {
|
|
189
|
+
case "missing-resource":
|
|
190
|
+
// `kind`, `mediaId`, and `path` are all typed and present here
|
|
191
|
+
console.warn(`Missing ${error.info.kind} at ${error.info.path}`);
|
|
192
|
+
break;
|
|
193
|
+
case "invalid-board":
|
|
194
|
+
// `issues` is the Zod issue list — which field failed and why
|
|
195
|
+
console.error(error.info.issues);
|
|
196
|
+
break;
|
|
197
|
+
default:
|
|
198
|
+
console.error(error.message);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
186
202
|
|
|
187
|
-
|
|
203
|
+
The `code` values, grouped by what they describe:
|
|
204
|
+
|
|
205
|
+
| Group | `info.code` | Key fields (on `info`) |
|
|
206
|
+
| ----------- | ------------------- | -------------------------------- |
|
|
207
|
+
| Decoding | `not-json` | `source` |
|
|
208
|
+
| | `not-zip` | — |
|
|
209
|
+
| | `unreadable-zip` | — |
|
|
210
|
+
| Validation | `invalid-board` | `issues`, `boardId?` |
|
|
211
|
+
| | `invalid-manifest` | `issues` |
|
|
212
|
+
| Read (OBZ) | `missing-manifest` | — |
|
|
213
|
+
| | `missing-board` | `boardId`, `path` |
|
|
214
|
+
| | `board-id-mismatch` | `path`, `declaredId`, `actualId` |
|
|
215
|
+
| Write (OBZ) | `unknown-root` | `rootBoardId` |
|
|
216
|
+
| | `duplicate-board` | `boardId` |
|
|
217
|
+
| | `missing-resource` | `kind`, `mediaId`, `path` |
|
|
218
|
+
| | `conflicting-paths` | `kind`, `mediaId`, `paths` |
|
|
219
|
+
| | `path-collision` | `path` |
|
|
220
|
+
| | `zip-failed` | — |
|
|
221
|
+
| Internal | `internal` | `detail` |
|
|
222
|
+
|
|
223
|
+
`OBFErrorInfo` and `OBFErrorCode` are exported for exhaustive handling. The underlying error, when there is one, is always on the standard `error.cause` — never duplicated on `info`. Validation failures (`invalid-board`, `invalid-manifest`) put the `ZodError` there, so you can call `z.treeifyError(error.cause)` for nested, UI-friendly output, while `info.issues` (Zod's issue type, re-exported as `OBFIssue`) gives you the flat list directly. For `not-json` / `*-zip` failures `error.cause` is the underlying parser or fflate error. An `internal` code signals a bug in this library that callers can't recover from — please report it.
|
|
188
224
|
|
|
189
225
|
## Security
|
|
190
226
|
|
package/dist/index.d.mts
CHANGED
|
@@ -344,7 +344,7 @@ declare const OBFManifestSchema: z.ZodObject<{
|
|
|
344
344
|
root: z.ZodString;
|
|
345
345
|
paths: z.ZodObject<{
|
|
346
346
|
boards: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
347
|
-
images: z.ZodRecord<z.ZodString, z.ZodString
|
|
347
|
+
images: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
348
348
|
sounds: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
349
349
|
}, z.core.$loose>;
|
|
350
350
|
}, z.core.$loose>;
|
|
@@ -354,6 +354,89 @@ declare const OBFManifestSchema: z.ZodObject<{
|
|
|
354
354
|
*/
|
|
355
355
|
type OBFManifest = z.infer<typeof OBFManifestSchema>;
|
|
356
356
|
//#endregion
|
|
357
|
+
//#region src/errors.d.ts
|
|
358
|
+
/**
|
|
359
|
+
* A single schema validation problem — Zod's issue shape, re-exported under a
|
|
360
|
+
* domain name. `z.core.$ZodIssue` is the type Zod v4 designates for libraries
|
|
361
|
+
* built on it (the bare `z.ZodIssue` is deprecated in its favor); aliasing it
|
|
362
|
+
* gives consumers a stable OBF name without reaching into Zod's `core` export.
|
|
363
|
+
*/
|
|
364
|
+
type OBFIssue = z.core.$ZodIssue;
|
|
365
|
+
/**
|
|
366
|
+
* Discriminated description of why an {@link OBFError} was thrown.
|
|
367
|
+
*
|
|
368
|
+
* Switch on `code`; each variant carries the fields relevant to it. When a
|
|
369
|
+
* failure wraps an underlying error it lives on the standard `error.cause`,
|
|
370
|
+
* never duplicated here. The only optional field is `invalid-board`'s
|
|
371
|
+
* `boardId`, absent when validation runs on a value with no known id.
|
|
372
|
+
*/
|
|
373
|
+
type OBFErrorInfo = /** Input was not parseable JSON. */{
|
|
374
|
+
code: "not-json";
|
|
375
|
+
source: "board" | "manifest";
|
|
376
|
+
} /** An OBZ archive was expected, but the bytes are not a ZIP. */ | {
|
|
377
|
+
code: "not-zip";
|
|
378
|
+
} /** A ZIP archive could not be decompressed. */ | {
|
|
379
|
+
code: "unreadable-zip";
|
|
380
|
+
} /** A board failed schema validation. `boardId` is set when known. */ | {
|
|
381
|
+
code: "invalid-board";
|
|
382
|
+
boardId?: string;
|
|
383
|
+
issues: readonly OBFIssue[];
|
|
384
|
+
} /** A manifest failed schema validation. */ | {
|
|
385
|
+
code: "invalid-manifest";
|
|
386
|
+
issues: readonly OBFIssue[];
|
|
387
|
+
} /** The archive has no `manifest.json`. */ | {
|
|
388
|
+
code: "missing-manifest";
|
|
389
|
+
} /** A board the manifest declares is absent from the archive. */ | {
|
|
390
|
+
code: "missing-board";
|
|
391
|
+
boardId: string;
|
|
392
|
+
path: string;
|
|
393
|
+
} /** A board's `id` disagrees with the id the manifest declares for it. */ | {
|
|
394
|
+
code: "board-id-mismatch";
|
|
395
|
+
path: string;
|
|
396
|
+
declaredId: string;
|
|
397
|
+
actualId: string;
|
|
398
|
+
} /** `rootBoardId` matches none of the supplied boards. */ | {
|
|
399
|
+
code: "unknown-root";
|
|
400
|
+
rootBoardId: string;
|
|
401
|
+
} /** Two supplied boards share the same `id`. */ | {
|
|
402
|
+
code: "duplicate-board";
|
|
403
|
+
boardId: string;
|
|
404
|
+
} /** A board declares a media `path` with no matching resource. */ | {
|
|
405
|
+
code: "missing-resource";
|
|
406
|
+
kind: "image" | "sound";
|
|
407
|
+
mediaId: string;
|
|
408
|
+
path: string;
|
|
409
|
+
} /** Two boards declare the same media id with different paths. */ | {
|
|
410
|
+
code: "conflicting-paths";
|
|
411
|
+
kind: "image" | "sound";
|
|
412
|
+
mediaId: string;
|
|
413
|
+
paths: [string, string];
|
|
414
|
+
} /** A supplied resource would overwrite a generated board or the manifest. */ | {
|
|
415
|
+
code: "path-collision";
|
|
416
|
+
path: string;
|
|
417
|
+
} /** The archive could not be compressed. */ | {
|
|
418
|
+
code: "zip-failed";
|
|
419
|
+
} /** An internal invariant was violated — a bug in this library; please report. */ | {
|
|
420
|
+
code: "internal";
|
|
421
|
+
detail: string;
|
|
422
|
+
};
|
|
423
|
+
/** Every `code` an {@link OBFError} can carry. */
|
|
424
|
+
type OBFErrorCode = OBFErrorInfo["code"];
|
|
425
|
+
/**
|
|
426
|
+
* The single error type thrown by `@shayc/open-board-format`.
|
|
427
|
+
*
|
|
428
|
+
* Branch on {@link OBFError.info} (a discriminated {@link OBFErrorInfo}) rather
|
|
429
|
+
* than parsing {@link OBFError.message}. Any underlying error — a `JSON.parse`
|
|
430
|
+
* failure, a `ZodError`, or an fflate error — is on the standard `error.cause`.
|
|
431
|
+
*/
|
|
432
|
+
declare class OBFError extends Error {
|
|
433
|
+
/** Structured, discriminated description of the failure. */
|
|
434
|
+
readonly info: OBFErrorInfo;
|
|
435
|
+
constructor(info: OBFErrorInfo, options?: {
|
|
436
|
+
cause?: unknown;
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
//#endregion
|
|
357
440
|
//#region src/obf.d.ts
|
|
358
441
|
/**
|
|
359
442
|
* Parse a JSON string into a validated OBF board.
|
|
@@ -364,7 +447,8 @@ type OBFManifest = z.infer<typeof OBFManifestSchema>;
|
|
|
364
447
|
* @param json - The JSON string to parse.
|
|
365
448
|
* @returns The validated board object.
|
|
366
449
|
*
|
|
367
|
-
* @throws {
|
|
450
|
+
* @throws {@link OBFError} with `info.code` `"not-json"` if the JSON is
|
|
451
|
+
* malformed, or `"invalid-board"` if it fails schema validation.
|
|
368
452
|
*/
|
|
369
453
|
declare function parseOBF(json: string): OBFBoard;
|
|
370
454
|
/**
|
|
@@ -376,7 +460,8 @@ declare function parseOBF(json: string): OBFBoard;
|
|
|
376
460
|
* @param file - A `File` handle pointing to an `.obf` file.
|
|
377
461
|
* @returns The validated board object.
|
|
378
462
|
*
|
|
379
|
-
* @throws {
|
|
463
|
+
* @throws {@link OBFError} with `info.code` `"not-json"` if the file content is
|
|
464
|
+
* malformed, or `"invalid-board"` if it fails schema validation.
|
|
380
465
|
*/
|
|
381
466
|
declare function loadOBF(file: File): Promise<OBFBoard>;
|
|
382
467
|
/**
|
|
@@ -385,7 +470,8 @@ declare function loadOBF(file: File): Promise<OBFBoard>;
|
|
|
385
470
|
* @param data - The value to validate.
|
|
386
471
|
* @returns The validated board object.
|
|
387
472
|
*
|
|
388
|
-
* @throws {
|
|
473
|
+
* @throws {@link OBFError} with `info.code` `"invalid-board"` if the value fails
|
|
474
|
+
* schema validation. `info.issues` holds the underlying Zod issues.
|
|
389
475
|
*/
|
|
390
476
|
declare function validateOBF(data: unknown): OBFBoard;
|
|
391
477
|
/**
|
|
@@ -426,7 +512,8 @@ interface ParsedOBZ {
|
|
|
426
512
|
* @param file - A `File` handle pointing to an `.obz` archive.
|
|
427
513
|
* @returns The parsed manifest, boards, root board, and binary resources.
|
|
428
514
|
*
|
|
429
|
-
* @throws {
|
|
515
|
+
* @throws {@link OBFError} — the same failures as {@link extractOBZ}, which
|
|
516
|
+
* this delegates to.
|
|
430
517
|
*/
|
|
431
518
|
declare function loadOBZ(file: File): Promise<ParsedOBZ>;
|
|
432
519
|
/**
|
|
@@ -437,9 +524,10 @@ declare function loadOBZ(file: File): Promise<ParsedOBZ>;
|
|
|
437
524
|
* the resolved root board, and a map of file paths to their
|
|
438
525
|
* binary content.
|
|
439
526
|
*
|
|
440
|
-
* @throws {
|
|
441
|
-
*
|
|
442
|
-
*
|
|
527
|
+
* @throws {@link OBFError}; branch on `info.code`: `"not-zip"`,
|
|
528
|
+
* `"unreadable-zip"`, `"missing-manifest"`, `"not-json"` or
|
|
529
|
+
* `"invalid-manifest"` (bad manifest), `"missing-board"`,
|
|
530
|
+
* `"board-id-mismatch"`, or `"invalid-board"` (a board fails validation).
|
|
443
531
|
*/
|
|
444
532
|
declare function extractOBZ(archive: ArrayBuffer): Promise<ParsedOBZ>;
|
|
445
533
|
/**
|
|
@@ -449,7 +537,8 @@ declare function extractOBZ(archive: ArrayBuffer): Promise<ParsedOBZ>;
|
|
|
449
537
|
* @param json - A JSON string representing the manifest.
|
|
450
538
|
* @returns The validated manifest object.
|
|
451
539
|
*
|
|
452
|
-
* @throws {
|
|
540
|
+
* @throws {@link OBFError} with `info.code` `"not-json"` if the JSON is
|
|
541
|
+
* malformed, or `"invalid-manifest"` if it fails schema validation.
|
|
453
542
|
*/
|
|
454
543
|
declare function parseManifest(json: string): OBFManifest;
|
|
455
544
|
/**
|
|
@@ -458,17 +547,19 @@ declare function parseManifest(json: string): OBFManifest;
|
|
|
458
547
|
* A manifest is generated automatically from the supplied boards,
|
|
459
548
|
* using the `rootBoardId` to designate the entry-point board.
|
|
460
549
|
*
|
|
550
|
+
* Every failure is an {@link OBFError}; branch on `info.code`.
|
|
551
|
+
*
|
|
461
552
|
* @param boards - The boards to include in the archive.
|
|
462
553
|
* @param rootBoardId - The ID of the board that serves as the archive's entry point.
|
|
463
554
|
* @param resources - Optional map of file paths to binary content (images, sounds, etc.).
|
|
464
555
|
* @returns A `Blob` containing the compressed OBZ archive.
|
|
465
556
|
*
|
|
466
|
-
* @throws {
|
|
467
|
-
* @throws {
|
|
468
|
-
* @throws {
|
|
469
|
-
* @throws {
|
|
470
|
-
* @throws {
|
|
471
|
-
* @throws {
|
|
557
|
+
* @throws {@link OBFError} `"unknown-root"` if `rootBoardId` does not match any of the supplied boards.
|
|
558
|
+
* @throws {@link OBFError} `"duplicate-board"` if two supplied boards share the same ID.
|
|
559
|
+
* @throws {@link OBFError} `"invalid-board"` if a supplied board fails schema validation.
|
|
560
|
+
* @throws {@link OBFError} `"conflicting-paths"` if two boards declare the same media ID with conflicting paths.
|
|
561
|
+
* @throws {@link OBFError} `"missing-resource"` if a board declares an image or sound `path` with no matching entry in `resources`.
|
|
562
|
+
* @throws {@link OBFError} `"path-collision"` if a `resources` entry would overwrite the generated `manifest.json` or a board file.
|
|
472
563
|
*/
|
|
473
564
|
declare function createOBZ(boards: OBFBoard[], rootBoardId: string, resources?: Map<string, Uint8Array | ArrayBuffer>): Promise<Blob>;
|
|
474
565
|
//#endregion
|
|
@@ -508,8 +599,9 @@ type LoadedBoard = {
|
|
|
508
599
|
* @param input - A `File` handle or `ArrayBuffer` holding `.obf` or `.obz` content.
|
|
509
600
|
* @returns A discriminated union tagged by `format`.
|
|
510
601
|
*
|
|
511
|
-
* @throws {
|
|
512
|
-
*
|
|
602
|
+
* @throws {@link OBFError} — the OBZ failures of {@link extractOBZ} when the
|
|
603
|
+
* input is an archive, or the OBF failures of {@link parseOBF} otherwise.
|
|
604
|
+
* Branch on `error.info.code`.
|
|
513
605
|
*/
|
|
514
606
|
declare function loadBoard(input: File | ArrayBuffer): Promise<LoadedBoard>;
|
|
515
607
|
//#endregion
|
|
@@ -523,7 +615,8 @@ declare function loadBoard(input: File | ArrayBuffer): Promise<LoadedBoard>;
|
|
|
523
615
|
* @param archive - The ZIP archive as an `ArrayBuffer`.
|
|
524
616
|
* @returns A map of file paths to their decompressed content.
|
|
525
617
|
*
|
|
526
|
-
* @throws {
|
|
618
|
+
* @throws {@link OBFError} with `info.code` `"unreadable-zip"` if the archive is
|
|
619
|
+
* corrupt or cannot be decompressed.
|
|
527
620
|
*/
|
|
528
621
|
declare function unzip(archive: ArrayBuffer): Promise<Map<string, Uint8Array>>;
|
|
529
622
|
/**
|
|
@@ -536,7 +629,8 @@ declare function unzip(archive: ArrayBuffer): Promise<Map<string, Uint8Array>>;
|
|
|
536
629
|
* @param entries - A map of file paths to their content bytes.
|
|
537
630
|
* @returns The compressed archive as a `Uint8Array`.
|
|
538
631
|
*
|
|
539
|
-
* @throws {
|
|
632
|
+
* @throws {@link OBFError} with `info.code` `"zip-failed"` if fflate fails to
|
|
633
|
+
* compress an entry.
|
|
540
634
|
*/
|
|
541
635
|
declare function zip(entries: Map<string, Uint8Array | ArrayBuffer>): Promise<Uint8Array>;
|
|
542
636
|
/**
|
|
@@ -548,5 +642,5 @@ declare function zip(entries: Map<string, Uint8Array | ArrayBuffer>): Promise<Ui
|
|
|
548
642
|
*/
|
|
549
643
|
declare function isZip(archive: ArrayBuffer): boolean;
|
|
550
644
|
//#endregion
|
|
551
|
-
export { type LoadedBoard, type OBFBoard, OBFBoardSchema, type OBFButton, type OBFButtonAction, OBFButtonActionSchema, OBFButtonSchema, type OBFFormatVersion, OBFFormatVersionSchema, type OBFGrid, OBFGridSchema, type OBFID, OBFIDSchema, type OBFImage, OBFImageSchema, type OBFLicense, OBFLicenseSchema, type OBFLoadBoard, OBFLoadBoardSchema, type OBFLocaleCode, OBFLocaleCodeSchema, type OBFLocalizedStrings, OBFLocalizedStringsSchema, type OBFManifest, OBFManifestSchema, type OBFMedia, OBFMediaSchema, type OBFSound, OBFSoundSchema, type OBFSpecialtyAction, OBFSpecialtyActionSchema, type OBFSpellingAction, OBFSpellingActionSchema, type OBFStrings, OBFStringsSchema, type OBFSymbolInfo, OBFSymbolInfoSchema, type ParsedOBZ, createOBZ, extractOBZ, isZip, loadBoard, loadOBF, loadOBZ, parseManifest, parseOBF, stringifyOBF, unzip, validateOBF, zip };
|
|
645
|
+
export { type LoadedBoard, type OBFBoard, OBFBoardSchema, type OBFButton, type OBFButtonAction, OBFButtonActionSchema, OBFButtonSchema, OBFError, type OBFErrorCode, type OBFErrorInfo, type OBFFormatVersion, OBFFormatVersionSchema, type OBFGrid, OBFGridSchema, type OBFID, OBFIDSchema, type OBFImage, OBFImageSchema, type OBFIssue, type OBFLicense, OBFLicenseSchema, type OBFLoadBoard, OBFLoadBoardSchema, type OBFLocaleCode, OBFLocaleCodeSchema, type OBFLocalizedStrings, OBFLocalizedStringsSchema, type OBFManifest, OBFManifestSchema, type OBFMedia, OBFMediaSchema, type OBFSound, OBFSoundSchema, type OBFSpecialtyAction, OBFSpecialtyActionSchema, type OBFSpellingAction, OBFSpellingActionSchema, type OBFStrings, OBFStringsSchema, type OBFSymbolInfo, OBFSymbolInfoSchema, type ParsedOBZ, createOBZ, extractOBZ, isZip, loadBoard, loadOBF, loadOBZ, parseManifest, parseOBF, stringifyOBF, unzip, validateOBF, zip };
|
|
552
646
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
CHANGED
|
@@ -178,7 +178,15 @@ const OBFButtonSchema = z.looseObject({
|
|
|
178
178
|
width: z.number().min(0).max(1).optional(),
|
|
179
179
|
/** Height of the button for absolute positioning (0.0 to 1.0). */
|
|
180
180
|
height: z.number().min(0).max(1).optional()
|
|
181
|
-
})
|
|
181
|
+
}).refine((b) => {
|
|
182
|
+
const set = [
|
|
183
|
+
b.top,
|
|
184
|
+
b.left,
|
|
185
|
+
b.width,
|
|
186
|
+
b.height
|
|
187
|
+
].filter((v) => v !== void 0);
|
|
188
|
+
return set.length === 0 || set.length === 4;
|
|
189
|
+
}, { message: "Absolute positioning requires all of top, left, width, and height (or none)" });
|
|
182
190
|
/**
|
|
183
191
|
* Row-and-column layout that arranges buttons by their IDs.
|
|
184
192
|
*/
|
|
@@ -235,7 +243,7 @@ const OBFManifestSchema = z.looseObject({
|
|
|
235
243
|
/** Mapping of board IDs to their file paths. */
|
|
236
244
|
boards: z.record(z.string(), z.string()),
|
|
237
245
|
/** Mapping of image IDs to their file paths. */
|
|
238
|
-
images: z.record(z.string(), z.string()),
|
|
246
|
+
images: z.record(z.string(), z.string()).optional(),
|
|
239
247
|
/** Mapping of sound IDs to their file paths. */
|
|
240
248
|
sounds: z.record(z.string(), z.string()).optional()
|
|
241
249
|
})
|
|
@@ -244,23 +252,85 @@ const OBFManifestSchema = z.looseObject({
|
|
|
244
252
|
path: ["root"]
|
|
245
253
|
});
|
|
246
254
|
//#endregion
|
|
255
|
+
//#region src/errors.ts
|
|
256
|
+
/**
|
|
257
|
+
* Typed errors for `@shayc/open-board-format`.
|
|
258
|
+
*
|
|
259
|
+
* Every failure thrown by this package is an {@link OBFError} carrying a
|
|
260
|
+
* discriminated {@link OBFErrorInfo} on its `info` property. Switch on
|
|
261
|
+
* `error.info.code` to get exactly the structured context for that failure —
|
|
262
|
+
* the human-readable `message` is derived from `info` and is not part of the
|
|
263
|
+
* stable contract.
|
|
264
|
+
*
|
|
265
|
+
* ```ts
|
|
266
|
+
* try {
|
|
267
|
+
* await loadBoard(file);
|
|
268
|
+
* } catch (error) {
|
|
269
|
+
* if (!(error instanceof OBFError)) throw error;
|
|
270
|
+
* switch (error.info.code) {
|
|
271
|
+
* case "missing-resource":
|
|
272
|
+
* reupload(error.info.kind, error.info.path); // both fully typed
|
|
273
|
+
* break;
|
|
274
|
+
* case "invalid-board":
|
|
275
|
+
* showIssues(error.info.issues);
|
|
276
|
+
* break;
|
|
277
|
+
* }
|
|
278
|
+
* }
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
/**
|
|
282
|
+
* The single error type thrown by `@shayc/open-board-format`.
|
|
283
|
+
*
|
|
284
|
+
* Branch on {@link OBFError.info} (a discriminated {@link OBFErrorInfo}) rather
|
|
285
|
+
* than parsing {@link OBFError.message}. Any underlying error — a `JSON.parse`
|
|
286
|
+
* failure, a `ZodError`, or an fflate error — is on the standard `error.cause`.
|
|
287
|
+
*/
|
|
288
|
+
var OBFError = class extends Error {
|
|
289
|
+
/** Structured, discriminated description of the failure. */
|
|
290
|
+
info;
|
|
291
|
+
constructor(info, options) {
|
|
292
|
+
super(formatOBFError(info), options);
|
|
293
|
+
this.name = "OBFError";
|
|
294
|
+
this.info = info;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
/** Derive a human-readable message from an {@link OBFErrorInfo}. */
|
|
298
|
+
function formatOBFError(info) {
|
|
299
|
+
switch (info.code) {
|
|
300
|
+
case "not-json": return `Invalid ${info.source === "manifest" ? "OBZ manifest" : "OBF"}: not valid JSON`;
|
|
301
|
+
case "not-zip": return "Invalid OBZ: not a ZIP file";
|
|
302
|
+
case "unreadable-zip": return "ZIP archive could not be read";
|
|
303
|
+
case "invalid-board": return `Invalid OBF ${info.boardId ? `board "${info.boardId}"` : "board"}:\n${prettifyIssues(info.issues)}`;
|
|
304
|
+
case "invalid-manifest": return `Invalid OBZ manifest:\n${prettifyIssues(info.issues)}`;
|
|
305
|
+
case "missing-manifest": return "Invalid OBZ: missing manifest.json";
|
|
306
|
+
case "missing-board": return `Invalid OBZ: board "${info.boardId}" is declared in the manifest but missing at "${info.path}"`;
|
|
307
|
+
case "board-id-mismatch": return `Invalid OBZ: board at "${info.path}" has id "${info.actualId}" but the manifest declares it as "${info.declaredId}"`;
|
|
308
|
+
case "unknown-root": return `Invalid OBZ: rootBoardId "${info.rootBoardId}" does not match any supplied board`;
|
|
309
|
+
case "duplicate-board": return `Invalid OBZ: duplicate board id "${info.boardId}" — board ids must be unique within a package`;
|
|
310
|
+
case "missing-resource": return `Invalid OBZ: ${info.kind} "${info.mediaId}" references "${info.path}" but no matching resource was supplied`;
|
|
311
|
+
case "conflicting-paths": return `Invalid OBZ: ${info.kind} id "${info.mediaId}" maps to conflicting paths "${info.paths[0]}" and "${info.paths[1]}"`;
|
|
312
|
+
case "path-collision": return `Invalid OBZ: resource path "${info.path}" collides with a generated board or manifest entry`;
|
|
313
|
+
case "zip-failed": return "Failed to build ZIP archive";
|
|
314
|
+
case "internal": return `Internal error (please report): ${info.detail}`;
|
|
315
|
+
/* v8 ignore start -- exhaustiveness guard: unreachable, enforced at compile time */
|
|
316
|
+
default: return info;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/** Render schema issues using Zod's pretty formatter. */
|
|
320
|
+
function prettifyIssues(issues) {
|
|
321
|
+
return z.prettifyError(new z.ZodError([...issues]));
|
|
322
|
+
}
|
|
323
|
+
//#endregion
|
|
247
324
|
//#region src/obf.ts
|
|
325
|
+
/**
|
|
326
|
+
* Parsing, validation, and serialization for single `.obf` board files.
|
|
327
|
+
*/
|
|
248
328
|
const UTF8_BOM = "";
|
|
249
329
|
/** Strip a leading UTF-8 BOM, which some editors silently prepend. */
|
|
250
330
|
function stripBom(text) {
|
|
251
331
|
return text.startsWith(UTF8_BOM) ? text.slice(1) : text;
|
|
252
332
|
}
|
|
253
333
|
/**
|
|
254
|
-
* Build a descriptive JSON parse-failure message, preserving the engine's
|
|
255
|
-
* reason when available.
|
|
256
|
-
*
|
|
257
|
-
* @internal Exported for reuse by the OBZ module — not part of the public API.
|
|
258
|
-
*/
|
|
259
|
-
function buildJsonParseErrorMessage(label, error) {
|
|
260
|
-
const reason = error instanceof Error ? error.message : "";
|
|
261
|
-
return reason ? `Invalid ${label}: JSON parse failed — ${reason}` : `Invalid ${label}: JSON parse failed`;
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
334
|
* Parse a JSON string into a validated OBF board.
|
|
265
335
|
*
|
|
266
336
|
* Strips an optional UTF-8 BOM prefix before parsing and throws a
|
|
@@ -269,7 +339,8 @@ function buildJsonParseErrorMessage(label, error) {
|
|
|
269
339
|
* @param json - The JSON string to parse.
|
|
270
340
|
* @returns The validated board object.
|
|
271
341
|
*
|
|
272
|
-
* @throws {
|
|
342
|
+
* @throws {@link OBFError} with `info.code` `"not-json"` if the JSON is
|
|
343
|
+
* malformed, or `"invalid-board"` if it fails schema validation.
|
|
273
344
|
*/
|
|
274
345
|
function parseOBF(json) {
|
|
275
346
|
const sanitized = stripBom(json);
|
|
@@ -277,7 +348,10 @@ function parseOBF(json) {
|
|
|
277
348
|
try {
|
|
278
349
|
rawBoard = JSON.parse(sanitized);
|
|
279
350
|
} catch (error) {
|
|
280
|
-
throw new
|
|
351
|
+
throw new OBFError({
|
|
352
|
+
code: "not-json",
|
|
353
|
+
source: "board"
|
|
354
|
+
}, { cause: error });
|
|
281
355
|
}
|
|
282
356
|
return validateOBF(rawBoard);
|
|
283
357
|
}
|
|
@@ -290,7 +364,8 @@ function parseOBF(json) {
|
|
|
290
364
|
* @param file - A `File` handle pointing to an `.obf` file.
|
|
291
365
|
* @returns The validated board object.
|
|
292
366
|
*
|
|
293
|
-
* @throws {
|
|
367
|
+
* @throws {@link OBFError} with `info.code` `"not-json"` if the file content is
|
|
368
|
+
* malformed, or `"invalid-board"` if it fails schema validation.
|
|
294
369
|
*/
|
|
295
370
|
async function loadOBF(file) {
|
|
296
371
|
return parseOBF(await file.text());
|
|
@@ -301,11 +376,15 @@ async function loadOBF(file) {
|
|
|
301
376
|
* @param data - The value to validate.
|
|
302
377
|
* @returns The validated board object.
|
|
303
378
|
*
|
|
304
|
-
* @throws {
|
|
379
|
+
* @throws {@link OBFError} with `info.code` `"invalid-board"` if the value fails
|
|
380
|
+
* schema validation. `info.issues` holds the underlying Zod issues.
|
|
305
381
|
*/
|
|
306
382
|
function validateOBF(data) {
|
|
307
383
|
const result = OBFBoardSchema.safeParse(data);
|
|
308
|
-
if (!result.success) throw new
|
|
384
|
+
if (!result.success) throw new OBFError({
|
|
385
|
+
code: "invalid-board",
|
|
386
|
+
issues: result.error.issues
|
|
387
|
+
}, { cause: result.error });
|
|
309
388
|
return result.data;
|
|
310
389
|
}
|
|
311
390
|
/**
|
|
@@ -338,13 +417,14 @@ const COMPRESSION_LEVEL = 6;
|
|
|
338
417
|
* @param archive - The ZIP archive as an `ArrayBuffer`.
|
|
339
418
|
* @returns A map of file paths to their decompressed content.
|
|
340
419
|
*
|
|
341
|
-
* @throws {
|
|
420
|
+
* @throws {@link OBFError} with `info.code` `"unreadable-zip"` if the archive is
|
|
421
|
+
* corrupt or cannot be decompressed.
|
|
342
422
|
*/
|
|
343
423
|
function unzip(archive) {
|
|
344
424
|
return new Promise((resolve, reject) => {
|
|
345
425
|
unzip$1(new Uint8Array(archive), (error, entries) => {
|
|
346
426
|
if (error) {
|
|
347
|
-
reject(
|
|
427
|
+
reject(new OBFError({ code: "unreadable-zip" }, { cause: error }));
|
|
348
428
|
return;
|
|
349
429
|
}
|
|
350
430
|
resolve(new Map(Object.entries(entries)));
|
|
@@ -361,7 +441,8 @@ function unzip(archive) {
|
|
|
361
441
|
* @param entries - A map of file paths to their content bytes.
|
|
362
442
|
* @returns The compressed archive as a `Uint8Array`.
|
|
363
443
|
*
|
|
364
|
-
* @throws {
|
|
444
|
+
* @throws {@link OBFError} with `info.code` `"zip-failed"` if fflate fails to
|
|
445
|
+
* compress an entry.
|
|
365
446
|
*/
|
|
366
447
|
function zip(entries) {
|
|
367
448
|
return new Promise((resolve, reject) => {
|
|
@@ -369,7 +450,7 @@ function zip(entries) {
|
|
|
369
450
|
for (const [path, content] of entries) pathToBytes[path] = content instanceof Uint8Array ? content : new Uint8Array(content);
|
|
370
451
|
zip$1(pathToBytes, { level: COMPRESSION_LEVEL }, (error, result) => {
|
|
371
452
|
if (error) {
|
|
372
|
-
reject(
|
|
453
|
+
reject(new OBFError({ code: "zip-failed" }, { cause: error }));
|
|
373
454
|
return;
|
|
374
455
|
}
|
|
375
456
|
resolve(result);
|
|
@@ -401,7 +482,8 @@ function isZip(archive) {
|
|
|
401
482
|
* @param file - A `File` handle pointing to an `.obz` archive.
|
|
402
483
|
* @returns The parsed manifest, boards, root board, and binary resources.
|
|
403
484
|
*
|
|
404
|
-
* @throws {
|
|
485
|
+
* @throws {@link OBFError} — the same failures as {@link extractOBZ}, which
|
|
486
|
+
* this delegates to.
|
|
405
487
|
*/
|
|
406
488
|
async function loadOBZ(file) {
|
|
407
489
|
return extractOBZ(await file.arrayBuffer());
|
|
@@ -414,12 +496,13 @@ async function loadOBZ(file) {
|
|
|
414
496
|
* the resolved root board, and a map of file paths to their
|
|
415
497
|
* binary content.
|
|
416
498
|
*
|
|
417
|
-
* @throws {
|
|
418
|
-
*
|
|
419
|
-
*
|
|
499
|
+
* @throws {@link OBFError}; branch on `info.code`: `"not-zip"`,
|
|
500
|
+
* `"unreadable-zip"`, `"missing-manifest"`, `"not-json"` or
|
|
501
|
+
* `"invalid-manifest"` (bad manifest), `"missing-board"`,
|
|
502
|
+
* `"board-id-mismatch"`, or `"invalid-board"` (a board fails validation).
|
|
420
503
|
*/
|
|
421
504
|
async function extractOBZ(archive) {
|
|
422
|
-
if (!isZip(archive)) throw new
|
|
505
|
+
if (!isZip(archive)) throw new OBFError({ code: "not-zip" });
|
|
423
506
|
const entries = await unzip(archive);
|
|
424
507
|
const manifest = extractManifest(entries);
|
|
425
508
|
const { boards, rootBoard } = extractBoards(manifest, entries);
|
|
@@ -437,17 +520,24 @@ async function extractOBZ(archive) {
|
|
|
437
520
|
* @param json - A JSON string representing the manifest.
|
|
438
521
|
* @returns The validated manifest object.
|
|
439
522
|
*
|
|
440
|
-
* @throws {
|
|
523
|
+
* @throws {@link OBFError} with `info.code` `"not-json"` if the JSON is
|
|
524
|
+
* malformed, or `"invalid-manifest"` if it fails schema validation.
|
|
441
525
|
*/
|
|
442
526
|
function parseManifest(json) {
|
|
443
527
|
let data;
|
|
444
528
|
try {
|
|
445
529
|
data = JSON.parse(json);
|
|
446
530
|
} catch (error) {
|
|
447
|
-
throw new
|
|
531
|
+
throw new OBFError({
|
|
532
|
+
code: "not-json",
|
|
533
|
+
source: "manifest"
|
|
534
|
+
}, { cause: error });
|
|
448
535
|
}
|
|
449
536
|
const result = OBFManifestSchema.safeParse(data);
|
|
450
|
-
if (!result.success) throw new
|
|
537
|
+
if (!result.success) throw new OBFError({
|
|
538
|
+
code: "invalid-manifest",
|
|
539
|
+
issues: result.error.issues
|
|
540
|
+
}, { cause: result.error });
|
|
451
541
|
return result.data;
|
|
452
542
|
}
|
|
453
543
|
/**
|
|
@@ -456,23 +546,31 @@ function parseManifest(json) {
|
|
|
456
546
|
* A manifest is generated automatically from the supplied boards,
|
|
457
547
|
* using the `rootBoardId` to designate the entry-point board.
|
|
458
548
|
*
|
|
549
|
+
* Every failure is an {@link OBFError}; branch on `info.code`.
|
|
550
|
+
*
|
|
459
551
|
* @param boards - The boards to include in the archive.
|
|
460
552
|
* @param rootBoardId - The ID of the board that serves as the archive's entry point.
|
|
461
553
|
* @param resources - Optional map of file paths to binary content (images, sounds, etc.).
|
|
462
554
|
* @returns A `Blob` containing the compressed OBZ archive.
|
|
463
555
|
*
|
|
464
|
-
* @throws {
|
|
465
|
-
* @throws {
|
|
466
|
-
* @throws {
|
|
467
|
-
* @throws {
|
|
468
|
-
* @throws {
|
|
469
|
-
* @throws {
|
|
556
|
+
* @throws {@link OBFError} `"unknown-root"` if `rootBoardId` does not match any of the supplied boards.
|
|
557
|
+
* @throws {@link OBFError} `"duplicate-board"` if two supplied boards share the same ID.
|
|
558
|
+
* @throws {@link OBFError} `"invalid-board"` if a supplied board fails schema validation.
|
|
559
|
+
* @throws {@link OBFError} `"conflicting-paths"` if two boards declare the same media ID with conflicting paths.
|
|
560
|
+
* @throws {@link OBFError} `"missing-resource"` if a board declares an image or sound `path` with no matching entry in `resources`.
|
|
561
|
+
* @throws {@link OBFError} `"path-collision"` if a `resources` entry would overwrite the generated `manifest.json` or a board file.
|
|
470
562
|
*/
|
|
471
563
|
async function createOBZ(boards, rootBoardId, resources) {
|
|
472
|
-
if (!boards.some((board) => board.id === rootBoardId)) throw new
|
|
564
|
+
if (!boards.some((board) => board.id === rootBoardId)) throw new OBFError({
|
|
565
|
+
code: "unknown-root",
|
|
566
|
+
rootBoardId
|
|
567
|
+
});
|
|
473
568
|
const seenBoardIds = /* @__PURE__ */ new Set();
|
|
474
569
|
for (const board of boards) {
|
|
475
|
-
if (seenBoardIds.has(board.id)) throw new
|
|
570
|
+
if (seenBoardIds.has(board.id)) throw new OBFError({
|
|
571
|
+
code: "duplicate-board",
|
|
572
|
+
boardId: board.id
|
|
573
|
+
});
|
|
476
574
|
seenBoardIds.add(board.id);
|
|
477
575
|
}
|
|
478
576
|
const entries = /* @__PURE__ */ new Map();
|
|
@@ -488,18 +586,30 @@ async function createOBZ(boards, rootBoardId, resources) {
|
|
|
488
586
|
...Object.keys(soundPaths).length > 0 ? { sounds: soundPaths } : {}
|
|
489
587
|
}
|
|
490
588
|
});
|
|
491
|
-
|
|
589
|
+
/* v8 ignore start -- defensive: the manifest is built from already-validated inputs */
|
|
590
|
+
if (!manifestResult.success) throw new OBFError({
|
|
591
|
+
code: "internal",
|
|
592
|
+
detail: "generated manifest failed validation"
|
|
593
|
+
}, { cause: manifestResult.error });
|
|
594
|
+
/* v8 ignore stop */
|
|
492
595
|
const manifest = manifestResult.data;
|
|
493
596
|
const encoder = new TextEncoder();
|
|
494
597
|
entries.set("manifest.json", encoder.encode(JSON.stringify(manifest, null, 2)));
|
|
495
598
|
for (const board of boards) {
|
|
496
599
|
const result = OBFBoardSchema.safeParse(board);
|
|
497
|
-
if (!result.success) throw new
|
|
600
|
+
if (!result.success) throw new OBFError({
|
|
601
|
+
code: "invalid-board",
|
|
602
|
+
boardId: board.id,
|
|
603
|
+
issues: result.error.issues
|
|
604
|
+
}, { cause: result.error });
|
|
498
605
|
const path = `boards/${result.data.id}.obf`;
|
|
499
606
|
entries.set(path, encoder.encode(JSON.stringify(result.data, null, 2)));
|
|
500
607
|
}
|
|
501
608
|
if (resources) for (const [path, bytes] of resources) {
|
|
502
|
-
if (entries.has(path)) throw new
|
|
609
|
+
if (entries.has(path)) throw new OBFError({
|
|
610
|
+
code: "path-collision",
|
|
611
|
+
path
|
|
612
|
+
});
|
|
503
613
|
entries.set(path, bytes);
|
|
504
614
|
}
|
|
505
615
|
assertPathsPresent("image", imagePaths, entries);
|
|
@@ -519,7 +629,12 @@ function collectMediaPaths(boards, kind) {
|
|
|
519
629
|
for (const board of boards) for (const media of board[kind] ?? []) {
|
|
520
630
|
if (media.path === void 0) continue;
|
|
521
631
|
const existing = paths[media.id];
|
|
522
|
-
if (existing !== void 0 && existing !== media.path) throw new
|
|
632
|
+
if (existing !== void 0 && existing !== media.path) throw new OBFError({
|
|
633
|
+
code: "conflicting-paths",
|
|
634
|
+
kind: kind === "images" ? "image" : "sound",
|
|
635
|
+
mediaId: media.id,
|
|
636
|
+
paths: [existing, media.path]
|
|
637
|
+
});
|
|
523
638
|
paths[media.id] = media.path;
|
|
524
639
|
}
|
|
525
640
|
return paths;
|
|
@@ -532,11 +647,16 @@ function collectMediaPaths(boards, kind) {
|
|
|
532
647
|
* media are never flagged.
|
|
533
648
|
*/
|
|
534
649
|
function assertPathsPresent(kind, paths, entries) {
|
|
535
|
-
for (const [id, path] of Object.entries(paths)) if (!entries.has(path)) throw new
|
|
650
|
+
for (const [id, path] of Object.entries(paths)) if (!entries.has(path)) throw new OBFError({
|
|
651
|
+
code: "missing-resource",
|
|
652
|
+
kind,
|
|
653
|
+
mediaId: id,
|
|
654
|
+
path
|
|
655
|
+
});
|
|
536
656
|
}
|
|
537
657
|
function extractManifest(entries) {
|
|
538
658
|
const manifestBytes = entries.get("manifest.json");
|
|
539
|
-
if (!manifestBytes) throw new
|
|
659
|
+
if (!manifestBytes) throw new OBFError({ code: "missing-manifest" });
|
|
540
660
|
return parseManifest(new TextDecoder().decode(manifestBytes));
|
|
541
661
|
}
|
|
542
662
|
function extractBoards(manifest, entries) {
|
|
@@ -544,13 +664,27 @@ function extractBoards(manifest, entries) {
|
|
|
544
664
|
let rootBoard;
|
|
545
665
|
for (const [id, path] of Object.entries(manifest.paths.boards)) {
|
|
546
666
|
const boardBytes = entries.get(path);
|
|
547
|
-
if (!boardBytes) throw new
|
|
667
|
+
if (!boardBytes) throw new OBFError({
|
|
668
|
+
code: "missing-board",
|
|
669
|
+
boardId: id,
|
|
670
|
+
path
|
|
671
|
+
});
|
|
548
672
|
const board = parseOBF(new TextDecoder().decode(boardBytes));
|
|
549
|
-
if (board.id !== id) throw new
|
|
673
|
+
if (board.id !== id) throw new OBFError({
|
|
674
|
+
code: "board-id-mismatch",
|
|
675
|
+
path,
|
|
676
|
+
declaredId: id,
|
|
677
|
+
actualId: board.id
|
|
678
|
+
});
|
|
550
679
|
boards.set(id, board);
|
|
551
680
|
if (path === manifest.root) rootBoard = board;
|
|
552
681
|
}
|
|
553
|
-
|
|
682
|
+
/* v8 ignore start -- defensive: OBFManifestSchema guarantees root ∈ paths.boards */
|
|
683
|
+
if (!rootBoard) throw new OBFError({
|
|
684
|
+
code: "internal",
|
|
685
|
+
detail: `root board "${manifest.root}" not found in paths.boards`
|
|
686
|
+
});
|
|
687
|
+
/* v8 ignore stop */
|
|
554
688
|
return {
|
|
555
689
|
boards,
|
|
556
690
|
rootBoard
|
|
@@ -574,8 +708,9 @@ function extractBoards(manifest, entries) {
|
|
|
574
708
|
* @param input - A `File` handle or `ArrayBuffer` holding `.obf` or `.obz` content.
|
|
575
709
|
* @returns A discriminated union tagged by `format`.
|
|
576
710
|
*
|
|
577
|
-
* @throws {
|
|
578
|
-
*
|
|
711
|
+
* @throws {@link OBFError} — the OBZ failures of {@link extractOBZ} when the
|
|
712
|
+
* input is an archive, or the OBF failures of {@link parseOBF} otherwise.
|
|
713
|
+
* Branch on `error.info.code`.
|
|
579
714
|
*/
|
|
580
715
|
async function loadBoard(input) {
|
|
581
716
|
const buffer = input instanceof ArrayBuffer ? input : await input.arrayBuffer();
|
|
@@ -589,6 +724,6 @@ async function loadBoard(input) {
|
|
|
589
724
|
};
|
|
590
725
|
}
|
|
591
726
|
//#endregion
|
|
592
|
-
export { OBFBoardSchema, OBFButtonActionSchema, OBFButtonSchema, OBFFormatVersionSchema, OBFGridSchema, OBFIDSchema, OBFImageSchema, OBFLicenseSchema, OBFLoadBoardSchema, OBFLocaleCodeSchema, OBFLocalizedStringsSchema, OBFManifestSchema, OBFMediaSchema, OBFSoundSchema, OBFSpecialtyActionSchema, OBFSpellingActionSchema, OBFStringsSchema, OBFSymbolInfoSchema, createOBZ, extractOBZ, isZip, loadBoard, loadOBF, loadOBZ, parseManifest, parseOBF, stringifyOBF, unzip, validateOBF, zip };
|
|
727
|
+
export { OBFBoardSchema, OBFButtonActionSchema, OBFButtonSchema, OBFError, OBFFormatVersionSchema, OBFGridSchema, OBFIDSchema, OBFImageSchema, OBFLicenseSchema, OBFLoadBoardSchema, OBFLocaleCodeSchema, OBFLocalizedStringsSchema, OBFManifestSchema, OBFMediaSchema, OBFSoundSchema, OBFSpecialtyActionSchema, OBFSpellingActionSchema, OBFStringsSchema, OBFSymbolInfoSchema, createOBZ, extractOBZ, isZip, loadBoard, loadOBF, loadOBZ, parseManifest, parseOBF, stringifyOBF, unzip, validateOBF, zip };
|
|
593
728
|
|
|
594
729
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/schema.ts","../src/obf.ts","../src/zip.ts","../src/obz.ts","../src/load-board.ts"],"sourcesContent":["/**\n * Open Board Format (OBF) Zod Schemas\n *\n * These schemas represent the Open Board Format, designed for sharing communication boards and board sets\n * between Augmentative and Alternative Communication (AAC) applications.\n *\n * Official OBF specification: https://www.openboardformat.org/docs\n */\n\nimport { z } from \"zod\";\n\n/** Optional URL that treats empty strings as undefined. */\nconst OBFOptionalUrlSchema = z\n .union([z.url(), z.literal(\"\")])\n .transform((val) => (val === \"\" ? undefined : val))\n .optional();\n\n/** Optional email that treats empty strings as undefined. */\nconst OBFOptionalEmailSchema = z\n .union([z.email(), z.literal(\"\")])\n .transform((val) => (val === \"\" ? undefined : val))\n .optional();\n\n/** Optional ID that treats empty strings as undefined. */\nconst OBFOptionalIDSchema = z\n .union([z.string(), z.number()])\n .transform((val) => {\n const str = String(val);\n return str === \"\" ? undefined : str;\n })\n .optional();\n\n/** Unique board-element identifier, coerced to a non-empty string. */\nexport const OBFIDSchema = z\n .union([z.string(), z.number()])\n .transform((val) => String(val))\n .pipe(z.string().min(1));\n\n/**\n * Unique board-element identifier, coerced to a non-empty string.\n * See {@link OBFIDSchema}.\n */\nexport type OBFID = z.infer<typeof OBFIDSchema>;\n\n/** Format version of the Open Board Format, e.g., `open-board-0.1`. */\nexport const OBFFormatVersionSchema = z.string().regex(/^open-board-.+$/);\n\n/**\n * Format version of the Open Board Format, e.g., `open-board-0.1`.\n * See {@link OBFFormatVersionSchema}.\n */\nexport type OBFFormatVersion = z.infer<typeof OBFFormatVersionSchema>;\n\n/**\n * Locale identifier, typically a BCP 47 language tag (e.g., `en`, `en-US`,\n * `fr-CA`). Not strictly validated — any string is accepted.\n */\nexport const OBFLocaleCodeSchema = z.string();\n\n/**\n * Locale identifier, typically a BCP 47 language tag, e.g., `en`, `en-US`.\n * See {@link OBFLocaleCodeSchema}.\n */\nexport type OBFLocaleCode = z.infer<typeof OBFLocaleCodeSchema>;\n\n/**\n * Translations for a single locale, keyed by the source string,\n * e.g., `{ \"hello\": \"hola\" }`.\n */\nexport const OBFLocalizedStringsSchema = z.record(z.string(), z.string());\n\n/**\n * Translations for a single locale, keyed by the source string.\n * See {@link OBFLocalizedStringsSchema}.\n */\nexport type OBFLocalizedStrings = z.infer<typeof OBFLocalizedStringsSchema>;\n\n/**\n * Locale-keyed dictionary of translated strings,\n * e.g., `{ en: { greeting: \"Hello\" }, fr: { greeting: \"Bonjour\" } }`.\n */\nexport const OBFStringsSchema = z.record(z.string(), OBFLocalizedStringsSchema);\n\n/**\n * Locale-keyed dictionary of translated strings.\n * See {@link OBFStringsSchema}.\n */\nexport type OBFStrings = z.infer<typeof OBFStringsSchema>;\n\n/**\n * Spelling action: a `+` prefix followed by the text to append,\n * e.g., `+hello`.\n */\nexport const OBFSpellingActionSchema = z.string().regex(/^\\+.+$/);\n\n/**\n * Spelling action: a `+` prefix followed by the text to append, e.g., `+hello`.\n * See {@link OBFSpellingActionSchema}.\n */\nexport type OBFSpellingAction = z.infer<typeof OBFSpellingActionSchema>;\n\n/**\n * Specialty action prefixed with `:`, e.g., `:clear`.\n * Custom extensions use the `:ext_` prefix.\n */\nexport const OBFSpecialtyActionSchema = z\n .string()\n .regex(/^:[a-z][a-z0-9_-]*$/i);\n\n/**\n * Specialty action prefixed with `:`, e.g., `:clear`.\n * See {@link OBFSpecialtyActionSchema}.\n */\nexport type OBFSpecialtyAction = z.infer<typeof OBFSpecialtyActionSchema>;\n\n/** Union of spelling and specialty actions that a button can trigger. */\nexport const OBFButtonActionSchema = z.union([\n OBFSpellingActionSchema,\n OBFSpecialtyActionSchema,\n]);\n\n/**\n * Union of spelling and specialty actions that a button can trigger.\n * See {@link OBFButtonActionSchema}.\n */\nexport type OBFButtonAction = z.infer<typeof OBFButtonActionSchema>;\n\n/** License terms and attribution for a resource. */\nexport const OBFLicenseSchema = z.looseObject({\n /** Type of the license, e.g., `CC-BY-SA`. */\n type: z.string(),\n /** URL to the license terms. */\n copyright_notice_url: OBFOptionalUrlSchema,\n /** Source URL of the resource. */\n source_url: OBFOptionalUrlSchema,\n /** Name of the author. */\n author_name: z.string().optional(),\n /** URL of the author's webpage. */\n author_url: OBFOptionalUrlSchema,\n /** Email address of the author. */\n author_email: OBFOptionalEmailSchema,\n});\n\n/**\n * License terms and attribution for a resource.\n * See {@link OBFLicenseSchema}.\n */\nexport type OBFLicense = z.infer<typeof OBFLicenseSchema>;\n\n/**\n * Common properties for media resources (images and sounds).\n *\n * When multiple references are provided, they should be used in the following order:\n * 1. `data`\n * 2. `path`\n * 3. `url`\n *\n * `data_url` is not part of this fallback chain — it is an API endpoint for\n * retrieving information about the resource, not an alternative source of\n * the media bytes.\n */\nexport const OBFMediaSchema = z.looseObject({\n /** Unique identifier for the media resource. */\n id: OBFIDSchema,\n /** Media data inlined as a `data:` URI. */\n data: z.string().optional(),\n /** Path to the media file within an `.obz` package. */\n path: z.string().optional(),\n /**\n * URL of an API endpoint for fetching the media programmatically —\n * not a `data:` URI (that is `data`).\n */\n data_url: OBFOptionalUrlSchema,\n /** URL to the media resource. */\n url: OBFOptionalUrlSchema,\n /** MIME type of the media, e.g., `image/png`, `audio/mpeg`. */\n content_type: z.string().optional(),\n /** Licensing information for the media. */\n license: OBFLicenseSchema.optional(),\n});\n\n/**\n * Common properties for media resources (images and sounds).\n * See {@link OBFMediaSchema}.\n */\nexport type OBFMedia = z.infer<typeof OBFMediaSchema>;\n\n/** Reference to a symbol in a proprietary symbol set (e.g., SymbolStix). */\nexport const OBFSymbolInfoSchema = z.looseObject({\n /** Name of the symbol set, e.g., `symbolstix`. */\n set: z.string(),\n /** Filename of the symbol within the set. */\n filename: z.string(),\n});\n\n/**\n * Reference to a symbol in a proprietary symbol set.\n * See {@link OBFSymbolInfoSchema}.\n */\nexport type OBFSymbolInfo = z.infer<typeof OBFSymbolInfoSchema>;\n\n/**\n * Image resource, extending {@link OBFMediaSchema} with optional\n * symbol and dimension properties.\n *\n * When resolving the image, consumers should prefer sources in this order:\n * 1. `data`\n * 2. `path`\n * 3. `url`\n * 4. `symbol`\n */\nexport const OBFImageSchema = OBFMediaSchema.extend({\n /** Information about a symbol from a proprietary symbol set. */\n symbol: OBFSymbolInfoSchema.optional(),\n /** Width of the image in pixels. */\n width: z.number().optional(),\n /** Height of the image in pixels. */\n height: z.number().optional(),\n});\n\n/**\n * Image resource with optional symbol and dimension properties.\n * See {@link OBFImageSchema}.\n */\nexport type OBFImage = z.infer<typeof OBFImageSchema>;\n\n/**\n * Audio resource. Identical to {@link OBFMediaSchema} — no additional properties.\n */\nexport const OBFSoundSchema = OBFMediaSchema;\n\n/**\n * Audio resource, identical to {@link OBFMedia}.\n * See {@link OBFSoundSchema}.\n */\nexport type OBFSound = z.infer<typeof OBFSoundSchema>;\n\n/** Reference to another board, resolved by ID, path, or URL. */\nexport const OBFLoadBoardSchema = z.looseObject({\n /** Unique identifier of the board to load. */\n id: OBFOptionalIDSchema,\n /** Name of the board to load. */\n name: z.string().optional(),\n /**\n * URL of an API endpoint for fetching the board programmatically —\n * not a `data:` URI.\n */\n data_url: OBFOptionalUrlSchema,\n /** URL to access the board via a web browser. */\n url: OBFOptionalUrlSchema,\n /** Path to the board within an `.obz` package. */\n path: z.string().optional(),\n});\n\n/**\n * Reference to another board, resolved by ID, path, or URL.\n * See {@link OBFLoadBoardSchema}.\n */\nexport type OBFLoadBoard = z.infer<typeof OBFLoadBoardSchema>;\n\n/**\n * Interactive element on a board, optionally linked to images, sounds, and actions.\n */\nexport const OBFButtonSchema = z.looseObject({\n /** Unique identifier for the button. */\n id: OBFIDSchema,\n /** Label text displayed on the button. */\n label: z.string().optional(),\n /** Alternative text for vocalization when the button is activated. */\n vocalization: z.string().optional(),\n /** Identifier of the image associated with the button. */\n image_id: OBFOptionalIDSchema,\n /** Identifier of the sound associated with the button. */\n sound_id: OBFOptionalIDSchema,\n /**\n * Action triggered by the button. When `actions` is also set, this is\n * the single-action fallback for apps that support one action per button.\n */\n action: OBFButtonActionSchema.optional(),\n /**\n * Multiple actions executed in order. Apps that support it should\n * prefer this over the single `action` fallback.\n */\n actions: z.array(OBFButtonActionSchema).optional(),\n /** Information to load another board when this button is activated. */\n load_board: OBFLoadBoardSchema.optional(),\n /** Background color of the button in `rgb` or `rgba` format. */\n background_color: z.string().optional(),\n /** Border color of the button in `rgb` or `rgba` format. */\n border_color: z.string().optional(),\n /** Vertical position for absolute positioning (0.0 to 1.0). */\n top: z.number().min(0).max(1).optional(),\n /** Horizontal position for absolute positioning (0.0 to 1.0). */\n left: z.number().min(0).max(1).optional(),\n /** Width of the button for absolute positioning (0.0 to 1.0). */\n width: z.number().min(0).max(1).optional(),\n /** Height of the button for absolute positioning (0.0 to 1.0). */\n height: z.number().min(0).max(1).optional(),\n});\n\n/**\n * Interactive element on a board, optionally linked to images, sounds, and\n * actions. See {@link OBFButtonSchema}.\n */\nexport type OBFButton = z.infer<typeof OBFButtonSchema>;\n\n/**\n * Row-and-column layout that arranges buttons by their IDs.\n */\nexport const OBFGridSchema = z\n .looseObject({\n /** Number of rows in the grid. */\n rows: z.number().int().min(1),\n /** Number of columns in the grid. */\n columns: z.number().int().min(1),\n /**\n * 2D array representing the order of buttons by their IDs.\n * Each sub-array corresponds to a row, and each element is a button ID or null for empty slots.\n */\n order: z.array(z.array(z.union([OBFIDSchema, z.null()]))),\n })\n .refine((g) => g.order.length === g.rows, {\n message: \"Grid order length must match rows\",\n })\n .refine((g) => g.order.every((row) => row.length === g.columns), {\n message: \"Each grid row must have length equal to columns\",\n });\n\n/**\n * Row-and-column layout that arranges buttons by their IDs.\n * See {@link OBFGridSchema}.\n */\nexport type OBFGrid = z.infer<typeof OBFGridSchema>;\n\n/**\n * Root object of an `.obf` file: the complete definition of a single communication board.\n */\nexport const OBFBoardSchema = z.looseObject({\n /** Format version of the Open Board Format, e.g., `open-board-0.1`. */\n format: OBFFormatVersionSchema,\n /** Unique identifier for the board. */\n id: OBFIDSchema,\n /** Locale of the board as a BCP 47 language tag, e.g., `en`, `en-US`. */\n locale: OBFLocaleCodeSchema.optional(),\n /** List of buttons on the board. */\n buttons: z.array(OBFButtonSchema),\n /** URL where the board can be accessed or downloaded. */\n url: OBFOptionalUrlSchema,\n /** Name of the board. */\n name: z.string().optional(),\n /** Description of the board in HTML format. */\n description_html: z.string().optional(),\n /** Grid layout information for arranging buttons. */\n grid: OBFGridSchema,\n /** List of images used in the board. */\n images: z.array(OBFImageSchema).optional(),\n /** List of sounds used in the board. */\n sounds: z.array(OBFSoundSchema).optional(),\n /** Licensing information for the board. */\n license: OBFLicenseSchema.optional(),\n /** String translations for multiple locales. */\n strings: OBFStringsSchema.optional(),\n});\n\n/**\n * The complete definition of a single communication board — root object of\n * an `.obf` file. See {@link OBFBoardSchema}.\n */\nexport type OBFBoard = z.infer<typeof OBFBoardSchema>;\n\n/**\n * Table of contents for an `.obz` package, mapping resource IDs to their archive paths.\n */\nexport const OBFManifestSchema = z\n .looseObject({\n /** Format version of the Open Board Format, e.g., `open-board-0.1`. */\n format: OBFFormatVersionSchema,\n /** Path to the root board within the `.obz` package. */\n root: z.string(),\n /** Mapping of IDs to paths for boards, images, and sounds. */\n paths: z.looseObject({\n /** Mapping of board IDs to their file paths. */\n boards: z.record(z.string(), z.string()),\n /** Mapping of image IDs to their file paths. */\n images: z.record(z.string(), z.string()),\n /** Mapping of sound IDs to their file paths. */\n sounds: z.record(z.string(), z.string()).optional(),\n }),\n })\n .refine((m) => Object.values(m.paths.boards).includes(m.root), {\n message: \"root must be listed in paths.boards\",\n path: [\"root\"],\n });\n\n/**\n * Table of contents for an `.obz` package, mapping resource IDs to their\n * archive paths. See {@link OBFManifestSchema}.\n */\nexport type OBFManifest = z.infer<typeof OBFManifestSchema>;\n","/**\n * Parsing, validation, and serialization for single `.obf` board files.\n */\n\nimport type { OBFBoard } from \"./schema\";\nimport { OBFBoardSchema } from \"./schema\";\n\nconst UTF8_BOM = \"\\uFEFF\";\n\n/** Strip a leading UTF-8 BOM, which some editors silently prepend. */\nfunction stripBom(text: string): string {\n return text.startsWith(UTF8_BOM) ? text.slice(1) : text;\n}\n\n/**\n * Build a descriptive JSON parse-failure message, preserving the engine's\n * reason when available.\n *\n * @internal Exported for reuse by the OBZ module — not part of the public API.\n */\nexport function buildJsonParseErrorMessage(\n label: string,\n error: unknown,\n): string {\n const reason = error instanceof Error ? error.message : \"\";\n return reason\n ? `Invalid ${label}: JSON parse failed — ${reason}`\n : `Invalid ${label}: JSON parse failed`;\n}\n\n/**\n * Parse a JSON string into a validated OBF board.\n *\n * Strips an optional UTF-8 BOM prefix before parsing and throws a\n * descriptive error if the input is malformed or fails schema validation.\n *\n * @param json - The JSON string to parse.\n * @returns The validated board object.\n *\n * @throws {Error} If the JSON is malformed or fails schema validation.\n */\nexport function parseOBF(json: string): OBFBoard {\n const sanitized = stripBom(json);\n\n let rawBoard: unknown;\n\n try {\n rawBoard = JSON.parse(sanitized) as unknown;\n } catch (error) {\n throw new Error(buildJsonParseErrorMessage(\"OBF\", error), { cause: error });\n }\n\n return validateOBF(rawBoard);\n}\n\n/**\n * Read a `File` and parse its contents as a validated OBF board.\n *\n * This relies on the browser `File` API; for Node environments,\n * read the file to a string and pass it to {@link parseOBF} instead.\n *\n * @param file - A `File` handle pointing to an `.obf` file.\n * @returns The validated board object.\n *\n * @throws {Error} If the file content is malformed or fails schema validation.\n */\nexport async function loadOBF(file: File): Promise<OBFBoard> {\n const json = await file.text();\n return parseOBF(json);\n}\n\n/**\n * Validate an unknown value against the OBF board schema.\n *\n * @param data - The value to validate.\n * @returns The validated board object.\n *\n * @throws {Error} If the value fails schema validation.\n */\nexport function validateOBF(data: unknown): OBFBoard {\n const result = OBFBoardSchema.safeParse(data);\n\n if (!result.success) {\n throw new Error(`Invalid OBF: ${result.error.message}`);\n }\n\n return result.data;\n}\n\n/**\n * Stringify an OBF board to a pretty-printed JSON string.\n *\n * @param board - The board to stringify.\n * @returns A JSON string with two-space indentation.\n */\nexport function stringifyOBF(board: OBFBoard): string {\n return JSON.stringify(board, null, 2);\n}\n","/**\n * Minimal ZIP helpers over fflate: signature sniffing, unzip, and zip.\n */\n\nimport { unzip as fflateUnzip, zip as fflateZip } from \"fflate\";\n\n/**\n * First two bytes of every ZIP archive — the ASCII letters `PK`,\n * after Phil Katz, creator of the format.\n *\n * Only the 2-byte prefix is checked intentionally: this keeps the\n * test lightweight and sufficient for distinguishing ZIP from JSON.\n */\nconst ZIP_MAGIC = [0x50, 0x4b] as const;\n\n/** Balanced speed-vs-size deflate level, on fflate's 0–9 scale (0 = store). */\nconst COMPRESSION_LEVEL = 6;\n\n/**\n * Decompress a ZIP archive into a map of file paths to raw bytes.\n *\n * @param archive - The ZIP archive as an `ArrayBuffer`.\n * @returns A map of file paths to their decompressed content.\n *\n * @throws {Error} If the archive is corrupt or cannot be decompressed.\n */\nexport function unzip(archive: ArrayBuffer): Promise<Map<string, Uint8Array>> {\n return new Promise((resolve, reject) => {\n const compressed = new Uint8Array(archive);\n\n fflateUnzip(compressed, (error, entries) => {\n if (error) {\n reject(new Error(`Failed to unzip: ${error.message ?? String(error)}`));\n return;\n }\n\n const pathToBytes = new Map<string, Uint8Array>(Object.entries(entries));\n\n resolve(pathToBytes);\n });\n });\n}\n\n/**\n * Compress a map of file paths and contents into a single ZIP archive.\n *\n * Accepts both `Uint8Array` and `ArrayBuffer` values so callers can\n * pass the output of {@link unzip} directly or supply raw `ArrayBuffer`s\n * without converting first.\n *\n * @param entries - A map of file paths to their content bytes.\n * @returns The compressed archive as a `Uint8Array`.\n *\n * @throws {Error} If fflate fails to compress an entry.\n */\nexport function zip(\n entries: Map<string, Uint8Array | ArrayBuffer>,\n): Promise<Uint8Array> {\n return new Promise((resolve, reject) => {\n const pathToBytes: Record<string, Uint8Array> = {};\n\n for (const [path, content] of entries) {\n pathToBytes[path] =\n content instanceof Uint8Array ? content : new Uint8Array(content);\n }\n\n fflateZip(pathToBytes, { level: COMPRESSION_LEVEL }, (error, result) => {\n if (error) {\n reject(new Error(`Failed to zip: ${error.message ?? String(error)}`));\n return;\n }\n\n resolve(result);\n });\n });\n}\n\n/**\n * Test whether an `ArrayBuffer` begins with the two-byte ZIP magic\n * prefix (`PK`).\n *\n * @param archive - The buffer to inspect.\n * @returns `true` if the buffer starts with the ZIP signature.\n */\nexport function isZip(archive: ArrayBuffer): boolean {\n const bytes = new Uint8Array(archive);\n\n return (\n bytes.length >= ZIP_MAGIC.length &&\n ZIP_MAGIC.every((byte, index) => bytes[index] === byte)\n );\n}\n","/**\n * Creation and extraction of `.obz` board packages.\n */\n\nimport { buildJsonParseErrorMessage, parseOBF } from \"./obf\";\nimport type { OBFBoard, OBFManifest } from \"./schema\";\nimport { OBFBoardSchema, OBFManifestSchema } from \"./schema\";\nimport { isZip, unzip, zip } from \"./zip\";\n\n/**\n * Fully extracted contents of an `.obz` archive.\n */\nexport interface ParsedOBZ {\n /** The package's table of contents. */\n manifest: OBFManifest;\n /** Validated board objects keyed by board ID. */\n boards: Map<string, OBFBoard>;\n /**\n * The package's entry-point board — the one `manifest.root` points at,\n * already resolved. Same object as `boards.get(rootBoard.id)`.\n */\n rootBoard: OBFBoard;\n /**\n * Raw bytes for every entry in the archive, keyed by archive path —\n * including `manifest.json` and the `.obf` boards as well as media\n * such as images and sounds.\n */\n resources: Map<string, Uint8Array>;\n}\n\n/**\n * Read a `File` and extract its contents as a parsed OBZ package.\n *\n * This relies on the browser `File` API; for Node environments,\n * read the file to an `ArrayBuffer` and pass it to {@link extractOBZ} instead.\n *\n * @param file - A `File` handle pointing to an `.obz` archive.\n * @returns The parsed manifest, boards, root board, and binary resources.\n *\n * @throws {Error} Same failures as {@link extractOBZ}, which this delegates to.\n */\nexport async function loadOBZ(file: File): Promise<ParsedOBZ> {\n const archive = await file.arrayBuffer();\n return extractOBZ(archive);\n}\n\n/**\n * Decompress an OBZ archive and return its manifest, boards, and resources.\n *\n * @param archive - The OBZ archive as an `ArrayBuffer`.\n * @returns The parsed manifest, a map of board IDs to validated boards,\n * the resolved root board, and a map of file paths to their\n * binary content.\n *\n * @throws {Error} If the archive is not a valid ZIP, the manifest is missing,\n * a board declared in the manifest is missing or fails validation, or a\n * board's `id` differs from the ID the manifest declares for it.\n */\nexport async function extractOBZ(archive: ArrayBuffer): Promise<ParsedOBZ> {\n if (!isZip(archive)) {\n throw new Error(\"Invalid OBZ: not a ZIP file\");\n }\n\n const entries = await unzip(archive);\n\n const manifest = extractManifest(entries);\n const { boards, rootBoard } = extractBoards(manifest, entries);\n\n return { manifest, boards, rootBoard, resources: entries };\n}\n\n/**\n * Parse and validate an OBZ manifest — the table of contents that maps\n * board IDs to their file paths within the archive.\n *\n * @param json - A JSON string representing the manifest.\n * @returns The validated manifest object.\n *\n * @throws {Error} If the JSON is malformed or fails schema validation.\n */\nexport function parseManifest(json: string): OBFManifest {\n let data: unknown;\n\n try {\n data = JSON.parse(json) as unknown;\n } catch (error) {\n throw new Error(buildJsonParseErrorMessage(\"manifest\", error), {\n cause: error,\n });\n }\n\n const result = OBFManifestSchema.safeParse(data);\n\n if (!result.success) {\n throw new Error(`Invalid manifest: ${result.error.message}`);\n }\n\n return result.data;\n}\n\n/**\n * Bundle boards and optional resources into a compressed OBZ archive.\n *\n * A manifest is generated automatically from the supplied boards,\n * using the `rootBoardId` to designate the entry-point board.\n *\n * @param boards - The boards to include in the archive.\n * @param rootBoardId - The ID of the board that serves as the archive's entry point.\n * @param resources - Optional map of file paths to binary content (images, sounds, etc.).\n * @returns A `Blob` containing the compressed OBZ archive.\n *\n * @throws {Error} If `rootBoardId` does not match any of the supplied boards.\n * @throws {Error} If two supplied boards share the same ID.\n * @throws {Error} If a supplied board fails schema validation.\n * @throws {Error} If two boards declare the same media ID with conflicting paths.\n * @throws {Error} If a board declares an image or sound `path` with no matching entry in `resources`.\n * @throws {Error} If a `resources` entry would overwrite the generated `manifest.json` or a board file.\n */\nexport async function createOBZ(\n boards: OBFBoard[],\n rootBoardId: string,\n resources?: Map<string, Uint8Array | ArrayBuffer>,\n): Promise<Blob> {\n if (!boards.some((board) => board.id === rootBoardId)) {\n throw new Error(\n `Invalid OBZ: rootBoardId \"${rootBoardId}\" does not match any supplied board`,\n );\n }\n\n const seenBoardIds = new Set<string>();\n for (const board of boards) {\n if (seenBoardIds.has(board.id)) {\n throw new Error(\n `Invalid OBZ: duplicate board id \"${board.id}\" — board ids must be unique within a package`,\n );\n }\n seenBoardIds.add(board.id);\n }\n\n const entries = new Map<string, Uint8Array | ArrayBuffer>();\n\n const boardPaths = Object.fromEntries(\n boards.map((board) => [board.id, `boards/${board.id}.obf`]),\n );\n\n const imagePaths = collectMediaPaths(boards, \"images\");\n const soundPaths = collectMediaPaths(boards, \"sounds\");\n\n const manifestResult = OBFManifestSchema.safeParse({\n format: \"open-board-0.1\",\n root: `boards/${rootBoardId}.obf`,\n paths: {\n boards: boardPaths,\n images: imagePaths,\n ...(Object.keys(soundPaths).length > 0 ? { sounds: soundPaths } : {}),\n },\n });\n\n if (!manifestResult.success) {\n throw new Error(\n `Invalid OBZ: generated manifest failed validation — ${manifestResult.error.message}`,\n );\n }\n\n const manifest = manifestResult.data;\n\n const encoder = new TextEncoder();\n\n entries.set(\n \"manifest.json\",\n encoder.encode(JSON.stringify(manifest, null, 2)),\n );\n\n for (const board of boards) {\n const result = OBFBoardSchema.safeParse(board);\n if (!result.success) {\n throw new Error(\n `Invalid OBZ: board \"${board.id}\" failed validation — ${result.error.message}`,\n );\n }\n const path = `boards/${result.data.id}.obf`;\n entries.set(path, encoder.encode(JSON.stringify(result.data, null, 2)));\n }\n\n if (resources) {\n for (const [path, bytes] of resources) {\n if (entries.has(path)) {\n throw new Error(\n `Invalid OBZ: resource path \"${path}\" collides with a generated board or manifest entry`,\n );\n }\n entries.set(path, bytes);\n }\n }\n\n assertPathsPresent(\"image\", imagePaths, entries);\n assertPathsPresent(\"sound\", soundPaths, entries);\n\n const compressed = await zip(entries);\n return new Blob([compressed], { type: \"application/zip\" });\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Walk every board's media collection and produce the `{ id -> path }` map\n * the spec calls \"redundant but still required\" for the OBZ manifest.\n *\n * Throws when two boards declare the same media ID with conflicting paths\n * — a silent OBZ that points at a non-existent file is worse than a clear error.\n */\nfunction collectMediaPaths(\n boards: OBFBoard[],\n kind: \"images\" | \"sounds\",\n): Record<string, string> {\n const paths: Record<string, string> = {};\n\n for (const board of boards) {\n for (const media of board[kind] ?? []) {\n if (media.path === undefined) {\n continue;\n }\n\n const existing = paths[media.id];\n if (existing !== undefined && existing !== media.path) {\n throw new Error(\n `Invalid OBZ: ${kind} id \"${media.id}\" maps to conflicting paths \"${existing}\" and \"${media.path}\"`,\n );\n }\n paths[media.id] = media.path;\n }\n }\n\n return paths;\n}\n\n/**\n * Assert that every media path the generated manifest declares exists as an\n * archive entry — the same contract {@link extractOBZ} assumes when reading.\n *\n * Only media that declared a `path` reach this check, so `url`/`data`-only\n * media are never flagged.\n */\nfunction assertPathsPresent(\n kind: \"image\" | \"sound\",\n paths: Record<string, string>,\n entries: Map<string, Uint8Array | ArrayBuffer>,\n): void {\n for (const [id, path] of Object.entries(paths)) {\n if (!entries.has(path)) {\n throw new Error(\n `Invalid OBZ: ${kind} \"${id}\" references \"${path}\" but no matching resource was supplied`,\n );\n }\n }\n}\n\nfunction extractManifest(entries: Map<string, Uint8Array>): OBFManifest {\n const manifestBytes = entries.get(\"manifest.json\");\n\n if (!manifestBytes) {\n throw new Error(\"Invalid OBZ: missing manifest.json\");\n }\n\n const manifestJson = new TextDecoder().decode(manifestBytes);\n return parseManifest(manifestJson);\n}\n\nfunction extractBoards(\n manifest: OBFManifest,\n entries: Map<string, Uint8Array>,\n): { boards: Map<string, OBFBoard>; rootBoard: OBFBoard } {\n const boards = new Map<string, OBFBoard>();\n let rootBoard: OBFBoard | undefined;\n\n for (const [id, path] of Object.entries(manifest.paths.boards)) {\n const boardBytes = entries.get(path);\n\n if (!boardBytes) {\n throw new Error(\n `Invalid OBZ: board \"${id}\" declared in manifest but missing at path \"${path}\"`,\n );\n }\n\n const boardJson = new TextDecoder().decode(boardBytes);\n const board = parseOBF(boardJson);\n\n if (board.id !== id) {\n throw new Error(\n `Invalid OBZ: board at \"${path}\" has id \"${board.id}\" but the manifest declares it as \"${id}\"`,\n );\n }\n\n boards.set(id, board);\n\n if (path === manifest.root) {\n rootBoard = board;\n }\n }\n\n if (!rootBoard) {\n // Unreachable for validated manifests: the schema requires `root` to be\n // listed in `paths.boards`. Kept as a guard for hand-built manifests.\n throw new Error(\n `Invalid OBZ: root board \"${manifest.root}\" not found in paths.boards`,\n );\n }\n\n return { boards, rootBoard };\n}\n","/**\n * Format-agnostic loading of `.obf` boards and `.obz` packages.\n */\n\nimport { parseOBF } from \"./obf\";\nimport { extractOBZ } from \"./obz\";\nimport type { ParsedOBZ } from \"./obz\";\nimport type { OBFBoard } from \"./schema\";\nimport { isZip } from \"./zip\";\n\n/**\n * Result of {@link loadBoard} — a discriminated union over the two file\n * shapes the Open Board Format defines.\n *\n * Switch on `format` to narrow:\n *\n * ```ts\n * const loaded = await loadBoard(file);\n * if (loaded.format === \"obz\") {\n * loaded.archive.rootBoard; // home board of the ParsedOBZ archive\n * } else {\n * loaded.board; // OBFBoard\n * }\n * ```\n */\nexport type LoadedBoard =\n | { format: \"obz\"; archive: ParsedOBZ }\n | { format: \"obf\"; board: OBFBoard };\n\n/**\n * Detect whether the input is a single OBF board or an OBZ package and load it\n * accordingly.\n *\n * Input that begins with the ZIP magic prefix is treated as an `.obz` package;\n * anything else is parsed as an `.obf` board. The input is read once, so\n * consumers can accept either format from a single drag-and-drop, file picker,\n * or fetch response without inspecting the file extension or re-deriving the\n * OBF-vs-OBZ distinction themselves.\n *\n * @param input - A `File` handle or `ArrayBuffer` holding `.obf` or `.obz` content.\n * @returns A discriminated union tagged by `format`.\n *\n * @throws {Error} If an OBZ archive is malformed or its manifest is missing,\n * or if an OBF board is malformed or fails schema validation.\n */\nexport async function loadBoard(\n input: File | ArrayBuffer,\n): Promise<LoadedBoard> {\n const buffer =\n input instanceof ArrayBuffer ? input : await input.arrayBuffer();\n\n if (isZip(buffer)) {\n return { format: \"obz\", archive: await extractOBZ(buffer) };\n }\n\n return { format: \"obf\", board: parseOBF(new TextDecoder().decode(buffer)) };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["_exhaustive"],"sources":["../src/schema.ts","../src/errors.ts","../src/obf.ts","../src/zip.ts","../src/obz.ts","../src/load-board.ts"],"sourcesContent":["/**\n * Open Board Format (OBF) Zod Schemas\n *\n * These schemas represent the Open Board Format, designed for sharing communication boards and board sets\n * between Augmentative and Alternative Communication (AAC) applications.\n *\n * Official OBF specification: https://www.openboardformat.org/docs\n */\n\nimport { z } from \"zod\";\n\n/** Optional URL that treats empty strings as undefined. */\nconst OBFOptionalUrlSchema = z\n .union([z.url(), z.literal(\"\")])\n .transform((val) => (val === \"\" ? undefined : val))\n .optional();\n\n/** Optional email that treats empty strings as undefined. */\nconst OBFOptionalEmailSchema = z\n .union([z.email(), z.literal(\"\")])\n .transform((val) => (val === \"\" ? undefined : val))\n .optional();\n\n/** Optional ID that treats empty strings as undefined. */\nconst OBFOptionalIDSchema = z\n .union([z.string(), z.number()])\n .transform((val) => {\n const str = String(val);\n return str === \"\" ? undefined : str;\n })\n .optional();\n\n/** Unique board-element identifier, coerced to a non-empty string. */\nexport const OBFIDSchema = z\n .union([z.string(), z.number()])\n .transform((val) => String(val))\n .pipe(z.string().min(1));\n\n/**\n * Unique board-element identifier, coerced to a non-empty string.\n * See {@link OBFIDSchema}.\n */\nexport type OBFID = z.infer<typeof OBFIDSchema>;\n\n/** Format version of the Open Board Format, e.g., `open-board-0.1`. */\nexport const OBFFormatVersionSchema = z.string().regex(/^open-board-.+$/);\n\n/**\n * Format version of the Open Board Format, e.g., `open-board-0.1`.\n * See {@link OBFFormatVersionSchema}.\n */\nexport type OBFFormatVersion = z.infer<typeof OBFFormatVersionSchema>;\n\n/**\n * Locale identifier, typically a BCP 47 language tag (e.g., `en`, `en-US`,\n * `fr-CA`). Not strictly validated — any string is accepted.\n */\nexport const OBFLocaleCodeSchema = z.string();\n\n/**\n * Locale identifier, typically a BCP 47 language tag, e.g., `en`, `en-US`.\n * See {@link OBFLocaleCodeSchema}.\n */\nexport type OBFLocaleCode = z.infer<typeof OBFLocaleCodeSchema>;\n\n/**\n * Translations for a single locale, keyed by the source string,\n * e.g., `{ \"hello\": \"hola\" }`.\n */\nexport const OBFLocalizedStringsSchema = z.record(z.string(), z.string());\n\n/**\n * Translations for a single locale, keyed by the source string.\n * See {@link OBFLocalizedStringsSchema}.\n */\nexport type OBFLocalizedStrings = z.infer<typeof OBFLocalizedStringsSchema>;\n\n/**\n * Locale-keyed dictionary of translated strings,\n * e.g., `{ en: { greeting: \"Hello\" }, fr: { greeting: \"Bonjour\" } }`.\n */\nexport const OBFStringsSchema = z.record(z.string(), OBFLocalizedStringsSchema);\n\n/**\n * Locale-keyed dictionary of translated strings.\n * See {@link OBFStringsSchema}.\n */\nexport type OBFStrings = z.infer<typeof OBFStringsSchema>;\n\n/**\n * Spelling action: a `+` prefix followed by the text to append,\n * e.g., `+hello`.\n */\nexport const OBFSpellingActionSchema = z.string().regex(/^\\+.+$/);\n\n/**\n * Spelling action: a `+` prefix followed by the text to append, e.g., `+hello`.\n * See {@link OBFSpellingActionSchema}.\n */\nexport type OBFSpellingAction = z.infer<typeof OBFSpellingActionSchema>;\n\n/**\n * Specialty action prefixed with `:`, e.g., `:clear`.\n * Custom extensions use the `:ext_` prefix.\n */\nexport const OBFSpecialtyActionSchema = z\n .string()\n .regex(/^:[a-z][a-z0-9_-]*$/i);\n\n/**\n * Specialty action prefixed with `:`, e.g., `:clear`.\n * See {@link OBFSpecialtyActionSchema}.\n */\nexport type OBFSpecialtyAction = z.infer<typeof OBFSpecialtyActionSchema>;\n\n/** Union of spelling and specialty actions that a button can trigger. */\nexport const OBFButtonActionSchema = z.union([\n OBFSpellingActionSchema,\n OBFSpecialtyActionSchema,\n]);\n\n/**\n * Union of spelling and specialty actions that a button can trigger.\n * See {@link OBFButtonActionSchema}.\n */\nexport type OBFButtonAction = z.infer<typeof OBFButtonActionSchema>;\n\n/** License terms and attribution for a resource. */\nexport const OBFLicenseSchema = z.looseObject({\n /** Type of the license, e.g., `CC-BY-SA`. */\n type: z.string(),\n /** URL to the license terms. */\n copyright_notice_url: OBFOptionalUrlSchema,\n /** Source URL of the resource. */\n source_url: OBFOptionalUrlSchema,\n /** Name of the author. */\n author_name: z.string().optional(),\n /** URL of the author's webpage. */\n author_url: OBFOptionalUrlSchema,\n /** Email address of the author. */\n author_email: OBFOptionalEmailSchema,\n});\n\n/**\n * License terms and attribution for a resource.\n * See {@link OBFLicenseSchema}.\n */\nexport type OBFLicense = z.infer<typeof OBFLicenseSchema>;\n\n/**\n * Common properties for media resources (images and sounds).\n *\n * When multiple references are provided, they should be used in the following order:\n * 1. `data`\n * 2. `path`\n * 3. `url`\n *\n * `data_url` is not part of this fallback chain — it is an API endpoint for\n * retrieving information about the resource, not an alternative source of\n * the media bytes.\n */\nexport const OBFMediaSchema = z.looseObject({\n /** Unique identifier for the media resource. */\n id: OBFIDSchema,\n /** Media data inlined as a `data:` URI. */\n data: z.string().optional(),\n /** Path to the media file within an `.obz` package. */\n path: z.string().optional(),\n /**\n * URL of an API endpoint for fetching the media programmatically —\n * not a `data:` URI (that is `data`).\n */\n data_url: OBFOptionalUrlSchema,\n /** URL to the media resource. */\n url: OBFOptionalUrlSchema,\n /** MIME type of the media, e.g., `image/png`, `audio/mpeg`. */\n content_type: z.string().optional(),\n /** Licensing information for the media. */\n license: OBFLicenseSchema.optional(),\n});\n\n/**\n * Common properties for media resources (images and sounds).\n * See {@link OBFMediaSchema}.\n */\nexport type OBFMedia = z.infer<typeof OBFMediaSchema>;\n\n/** Reference to a symbol in a proprietary symbol set (e.g., SymbolStix). */\nexport const OBFSymbolInfoSchema = z.looseObject({\n /** Name of the symbol set, e.g., `symbolstix`. */\n set: z.string(),\n /** Filename of the symbol within the set. */\n filename: z.string(),\n});\n\n/**\n * Reference to a symbol in a proprietary symbol set.\n * See {@link OBFSymbolInfoSchema}.\n */\nexport type OBFSymbolInfo = z.infer<typeof OBFSymbolInfoSchema>;\n\n/**\n * Image resource, extending {@link OBFMediaSchema} with optional\n * symbol and dimension properties.\n *\n * When resolving the image, consumers should prefer sources in this order:\n * 1. `data`\n * 2. `path`\n * 3. `url`\n * 4. `symbol`\n */\nexport const OBFImageSchema = OBFMediaSchema.extend({\n /** Information about a symbol from a proprietary symbol set. */\n symbol: OBFSymbolInfoSchema.optional(),\n /** Width of the image in pixels. */\n width: z.number().optional(),\n /** Height of the image in pixels. */\n height: z.number().optional(),\n});\n\n/**\n * Image resource with optional symbol and dimension properties.\n * See {@link OBFImageSchema}.\n */\nexport type OBFImage = z.infer<typeof OBFImageSchema>;\n\n/**\n * Audio resource. Identical to {@link OBFMediaSchema} — no additional properties.\n */\nexport const OBFSoundSchema = OBFMediaSchema;\n\n/**\n * Audio resource, identical to {@link OBFMedia}.\n * See {@link OBFSoundSchema}.\n */\nexport type OBFSound = z.infer<typeof OBFSoundSchema>;\n\n/** Reference to another board, resolved by ID, path, or URL. */\nexport const OBFLoadBoardSchema = z.looseObject({\n /** Unique identifier of the board to load. */\n id: OBFOptionalIDSchema,\n /** Name of the board to load. */\n name: z.string().optional(),\n /**\n * URL of an API endpoint for fetching the board programmatically —\n * not a `data:` URI.\n */\n data_url: OBFOptionalUrlSchema,\n /** URL to access the board via a web browser. */\n url: OBFOptionalUrlSchema,\n /** Path to the board within an `.obz` package. */\n path: z.string().optional(),\n});\n\n/**\n * Reference to another board, resolved by ID, path, or URL.\n * See {@link OBFLoadBoardSchema}.\n */\nexport type OBFLoadBoard = z.infer<typeof OBFLoadBoardSchema>;\n\n/**\n * Interactive element on a board, optionally linked to images, sounds, and actions.\n */\nexport const OBFButtonSchema = z\n .looseObject({\n /** Unique identifier for the button. */\n id: OBFIDSchema,\n /** Label text displayed on the button. */\n label: z.string().optional(),\n /** Alternative text for vocalization when the button is activated. */\n vocalization: z.string().optional(),\n /** Identifier of the image associated with the button. */\n image_id: OBFOptionalIDSchema,\n /** Identifier of the sound associated with the button. */\n sound_id: OBFOptionalIDSchema,\n /**\n * Action triggered by the button. When `actions` is also set, this is\n * the single-action fallback for apps that support one action per button.\n */\n action: OBFButtonActionSchema.optional(),\n /**\n * Multiple actions executed in order. Apps that support it should\n * prefer this over the single `action` fallback.\n */\n actions: z.array(OBFButtonActionSchema).optional(),\n /** Information to load another board when this button is activated. */\n load_board: OBFLoadBoardSchema.optional(),\n /** Background color of the button in `rgb` or `rgba` format. */\n background_color: z.string().optional(),\n /** Border color of the button in `rgb` or `rgba` format. */\n border_color: z.string().optional(),\n /** Vertical position for absolute positioning (0.0 to 1.0). */\n top: z.number().min(0).max(1).optional(),\n /** Horizontal position for absolute positioning (0.0 to 1.0). */\n left: z.number().min(0).max(1).optional(),\n /** Width of the button for absolute positioning (0.0 to 1.0). */\n width: z.number().min(0).max(1).optional(),\n /** Height of the button for absolute positioning (0.0 to 1.0). */\n height: z.number().min(0).max(1).optional(),\n })\n .refine(\n (b) => {\n const set = [b.top, b.left, b.width, b.height].filter(\n (v) => v !== undefined,\n );\n return set.length === 0 || set.length === 4;\n },\n {\n message:\n \"Absolute positioning requires all of top, left, width, and height (or none)\",\n },\n );\n\n/**\n * Interactive element on a board, optionally linked to images, sounds, and\n * actions. See {@link OBFButtonSchema}.\n */\nexport type OBFButton = z.infer<typeof OBFButtonSchema>;\n\n/**\n * Row-and-column layout that arranges buttons by their IDs.\n */\nexport const OBFGridSchema = z\n .looseObject({\n /** Number of rows in the grid. */\n rows: z.number().int().min(1),\n /** Number of columns in the grid. */\n columns: z.number().int().min(1),\n /**\n * 2D array representing the order of buttons by their IDs.\n * Each sub-array corresponds to a row, and each element is a button ID or null for empty slots.\n */\n order: z.array(z.array(z.union([OBFIDSchema, z.null()]))),\n })\n .refine((g) => g.order.length === g.rows, {\n message: \"Grid order length must match rows\",\n })\n .refine((g) => g.order.every((row) => row.length === g.columns), {\n message: \"Each grid row must have length equal to columns\",\n });\n\n/**\n * Row-and-column layout that arranges buttons by their IDs.\n * See {@link OBFGridSchema}.\n */\nexport type OBFGrid = z.infer<typeof OBFGridSchema>;\n\n/**\n * Root object of an `.obf` file: the complete definition of a single communication board.\n */\nexport const OBFBoardSchema = z.looseObject({\n /** Format version of the Open Board Format, e.g., `open-board-0.1`. */\n format: OBFFormatVersionSchema,\n /** Unique identifier for the board. */\n id: OBFIDSchema,\n /** Locale of the board as a BCP 47 language tag, e.g., `en`, `en-US`. */\n locale: OBFLocaleCodeSchema.optional(),\n /** List of buttons on the board. */\n buttons: z.array(OBFButtonSchema),\n /** URL where the board can be accessed or downloaded. */\n url: OBFOptionalUrlSchema,\n /** Name of the board. */\n name: z.string().optional(),\n /** Description of the board in HTML format. */\n description_html: z.string().optional(),\n /** Grid layout information for arranging buttons. */\n grid: OBFGridSchema,\n /** List of images used in the board. */\n images: z.array(OBFImageSchema).optional(),\n /** List of sounds used in the board. */\n sounds: z.array(OBFSoundSchema).optional(),\n /** Licensing information for the board. */\n license: OBFLicenseSchema.optional(),\n /** String translations for multiple locales. */\n strings: OBFStringsSchema.optional(),\n});\n\n/**\n * The complete definition of a single communication board — root object of\n * an `.obf` file. See {@link OBFBoardSchema}.\n */\nexport type OBFBoard = z.infer<typeof OBFBoardSchema>;\n\n/**\n * Table of contents for an `.obz` package, mapping resource IDs to their archive paths.\n */\nexport const OBFManifestSchema = z\n .looseObject({\n /** Format version of the Open Board Format, e.g., `open-board-0.1`. */\n format: OBFFormatVersionSchema,\n /** Path to the root board within the `.obz` package. */\n root: z.string(),\n /** Mapping of IDs to paths for boards, images, and sounds. */\n paths: z.looseObject({\n /** Mapping of board IDs to their file paths. */\n boards: z.record(z.string(), z.string()),\n /** Mapping of image IDs to their file paths. */\n images: z.record(z.string(), z.string()).optional(),\n /** Mapping of sound IDs to their file paths. */\n sounds: z.record(z.string(), z.string()).optional(),\n }),\n })\n .refine((m) => Object.values(m.paths.boards).includes(m.root), {\n message: \"root must be listed in paths.boards\",\n path: [\"root\"],\n });\n\n/**\n * Table of contents for an `.obz` package, mapping resource IDs to their\n * archive paths. See {@link OBFManifestSchema}.\n */\nexport type OBFManifest = z.infer<typeof OBFManifestSchema>;\n","/**\n * Typed errors for `@shayc/open-board-format`.\n *\n * Every failure thrown by this package is an {@link OBFError} carrying a\n * discriminated {@link OBFErrorInfo} on its `info` property. Switch on\n * `error.info.code` to get exactly the structured context for that failure —\n * the human-readable `message` is derived from `info` and is not part of the\n * stable contract.\n *\n * ```ts\n * try {\n * await loadBoard(file);\n * } catch (error) {\n * if (!(error instanceof OBFError)) throw error;\n * switch (error.info.code) {\n * case \"missing-resource\":\n * reupload(error.info.kind, error.info.path); // both fully typed\n * break;\n * case \"invalid-board\":\n * showIssues(error.info.issues);\n * break;\n * }\n * }\n * ```\n */\n\nimport { z } from \"zod\";\n\n/**\n * A single schema validation problem — Zod's issue shape, re-exported under a\n * domain name. `z.core.$ZodIssue` is the type Zod v4 designates for libraries\n * built on it (the bare `z.ZodIssue` is deprecated in its favor); aliasing it\n * gives consumers a stable OBF name without reaching into Zod's `core` export.\n */\nexport type OBFIssue = z.core.$ZodIssue;\n\n/**\n * Discriminated description of why an {@link OBFError} was thrown.\n *\n * Switch on `code`; each variant carries the fields relevant to it. When a\n * failure wraps an underlying error it lives on the standard `error.cause`,\n * never duplicated here. The only optional field is `invalid-board`'s\n * `boardId`, absent when validation runs on a value with no known id.\n */\nexport type OBFErrorInfo =\n // --- decoding (underlying parser/decompressor error on `error.cause`) ---\n /** Input was not parseable JSON. */\n | { code: \"not-json\"; source: \"board\" | \"manifest\" }\n /** An OBZ archive was expected, but the bytes are not a ZIP. */\n | { code: \"not-zip\" }\n /** A ZIP archive could not be decompressed. */\n | { code: \"unreadable-zip\" }\n // --- validation (underlying `ZodError` on `error.cause`) ---\n /** A board failed schema validation. `boardId` is set when known. */\n | { code: \"invalid-board\"; boardId?: string; issues: readonly OBFIssue[] }\n /** A manifest failed schema validation. */\n | { code: \"invalid-manifest\"; issues: readonly OBFIssue[] }\n // --- archive structure (reading an .obz) ---\n /** The archive has no `manifest.json`. */\n | { code: \"missing-manifest\" }\n /** A board the manifest declares is absent from the archive. */\n | { code: \"missing-board\"; boardId: string; path: string }\n /** A board's `id` disagrees with the id the manifest declares for it. */\n | {\n code: \"board-id-mismatch\";\n path: string;\n declaredId: string;\n actualId: string;\n }\n // --- archive assembly (createOBZ) ---\n /** `rootBoardId` matches none of the supplied boards. */\n | { code: \"unknown-root\"; rootBoardId: string }\n /** Two supplied boards share the same `id`. */\n | { code: \"duplicate-board\"; boardId: string }\n /** A board declares a media `path` with no matching resource. */\n | {\n code: \"missing-resource\";\n kind: \"image\" | \"sound\";\n mediaId: string;\n path: string;\n }\n /** Two boards declare the same media id with different paths. */\n | {\n code: \"conflicting-paths\";\n kind: \"image\" | \"sound\";\n mediaId: string;\n paths: [string, string];\n }\n /** A supplied resource would overwrite a generated board or the manifest. */\n | { code: \"path-collision\"; path: string }\n /** The archive could not be compressed. */\n | { code: \"zip-failed\" }\n /** An internal invariant was violated — a bug in this library; please report. */\n | { code: \"internal\"; detail: string };\n\n/** Every `code` an {@link OBFError} can carry. */\nexport type OBFErrorCode = OBFErrorInfo[\"code\"];\n\n/**\n * The single error type thrown by `@shayc/open-board-format`.\n *\n * Branch on {@link OBFError.info} (a discriminated {@link OBFErrorInfo}) rather\n * than parsing {@link OBFError.message}. Any underlying error — a `JSON.parse`\n * failure, a `ZodError`, or an fflate error — is on the standard `error.cause`.\n */\nexport class OBFError extends Error {\n /** Structured, discriminated description of the failure. */\n readonly info: OBFErrorInfo;\n\n constructor(info: OBFErrorInfo, options?: { cause?: unknown }) {\n super(formatOBFError(info), options);\n this.name = \"OBFError\";\n this.info = info;\n }\n}\n\n/** Derive a human-readable message from an {@link OBFErrorInfo}. */\nfunction formatOBFError(info: OBFErrorInfo): string {\n switch (info.code) {\n case \"not-json\":\n return `Invalid ${info.source === \"manifest\" ? \"OBZ manifest\" : \"OBF\"}: not valid JSON`;\n case \"not-zip\":\n return \"Invalid OBZ: not a ZIP file\";\n case \"unreadable-zip\":\n return \"ZIP archive could not be read\";\n case \"invalid-board\": {\n const subject = info.boardId ? `board \"${info.boardId}\"` : \"board\";\n return `Invalid OBF ${subject}:\\n${prettifyIssues(info.issues)}`;\n }\n case \"invalid-manifest\":\n return `Invalid OBZ manifest:\\n${prettifyIssues(info.issues)}`;\n case \"missing-manifest\":\n return \"Invalid OBZ: missing manifest.json\";\n case \"missing-board\":\n return `Invalid OBZ: board \"${info.boardId}\" is declared in the manifest but missing at \"${info.path}\"`;\n case \"board-id-mismatch\":\n return `Invalid OBZ: board at \"${info.path}\" has id \"${info.actualId}\" but the manifest declares it as \"${info.declaredId}\"`;\n case \"unknown-root\":\n return `Invalid OBZ: rootBoardId \"${info.rootBoardId}\" does not match any supplied board`;\n case \"duplicate-board\":\n return `Invalid OBZ: duplicate board id \"${info.boardId}\" — board ids must be unique within a package`;\n case \"missing-resource\":\n return `Invalid OBZ: ${info.kind} \"${info.mediaId}\" references \"${info.path}\" but no matching resource was supplied`;\n case \"conflicting-paths\":\n return `Invalid OBZ: ${info.kind} id \"${info.mediaId}\" maps to conflicting paths \"${info.paths[0]}\" and \"${info.paths[1]}\"`;\n case \"path-collision\":\n return `Invalid OBZ: resource path \"${info.path}\" collides with a generated board or manifest entry`;\n case \"zip-failed\":\n return \"Failed to build ZIP archive\";\n case \"internal\":\n return `Internal error (please report): ${info.detail}`;\n /* v8 ignore start -- exhaustiveness guard: unreachable, enforced at compile time */\n default: {\n const _exhaustive: never = info;\n return _exhaustive;\n }\n /* v8 ignore stop */\n }\n}\n\n/** Render schema issues using Zod's pretty formatter. */\nfunction prettifyIssues(issues: readonly OBFIssue[]): string {\n return z.prettifyError(new z.ZodError([...issues]));\n}\n","/**\n * Parsing, validation, and serialization for single `.obf` board files.\n */\n\nimport { OBFError } from \"./errors\";\nimport type { OBFBoard } from \"./schema\";\nimport { OBFBoardSchema } from \"./schema\";\n\nconst UTF8_BOM = \"\\uFEFF\";\n\n/** Strip a leading UTF-8 BOM, which some editors silently prepend. */\nfunction stripBom(text: string): string {\n return text.startsWith(UTF8_BOM) ? text.slice(1) : text;\n}\n\n/**\n * Parse a JSON string into a validated OBF board.\n *\n * Strips an optional UTF-8 BOM prefix before parsing and throws a\n * descriptive error if the input is malformed or fails schema validation.\n *\n * @param json - The JSON string to parse.\n * @returns The validated board object.\n *\n * @throws {@link OBFError} with `info.code` `\"not-json\"` if the JSON is\n * malformed, or `\"invalid-board\"` if it fails schema validation.\n */\nexport function parseOBF(json: string): OBFBoard {\n const sanitized = stripBom(json);\n\n let rawBoard: unknown;\n\n try {\n rawBoard = JSON.parse(sanitized) as unknown;\n } catch (error) {\n throw new OBFError({ code: \"not-json\", source: \"board\" }, { cause: error });\n }\n\n return validateOBF(rawBoard);\n}\n\n/**\n * Read a `File` and parse its contents as a validated OBF board.\n *\n * This relies on the browser `File` API; for Node environments,\n * read the file to a string and pass it to {@link parseOBF} instead.\n *\n * @param file - A `File` handle pointing to an `.obf` file.\n * @returns The validated board object.\n *\n * @throws {@link OBFError} with `info.code` `\"not-json\"` if the file content is\n * malformed, or `\"invalid-board\"` if it fails schema validation.\n */\nexport async function loadOBF(file: File): Promise<OBFBoard> {\n const json = await file.text();\n return parseOBF(json);\n}\n\n/**\n * Validate an unknown value against the OBF board schema.\n *\n * @param data - The value to validate.\n * @returns The validated board object.\n *\n * @throws {@link OBFError} with `info.code` `\"invalid-board\"` if the value fails\n * schema validation. `info.issues` holds the underlying Zod issues.\n */\nexport function validateOBF(data: unknown): OBFBoard {\n const result = OBFBoardSchema.safeParse(data);\n\n if (!result.success) {\n throw new OBFError(\n { code: \"invalid-board\", issues: result.error.issues },\n { cause: result.error },\n );\n }\n\n return result.data;\n}\n\n/**\n * Stringify an OBF board to a pretty-printed JSON string.\n *\n * @param board - The board to stringify.\n * @returns A JSON string with two-space indentation.\n */\nexport function stringifyOBF(board: OBFBoard): string {\n return JSON.stringify(board, null, 2);\n}\n","/**\n * Minimal ZIP helpers over fflate: signature sniffing, unzip, and zip.\n */\n\nimport { unzip as fflateUnzip, zip as fflateZip } from \"fflate\";\nimport { OBFError } from \"./errors\";\n\n/**\n * First two bytes of every ZIP archive — the ASCII letters `PK`,\n * after Phil Katz, creator of the format.\n *\n * Only the 2-byte prefix is checked intentionally: this keeps the\n * test lightweight and sufficient for distinguishing ZIP from JSON.\n */\nconst ZIP_MAGIC = [0x50, 0x4b] as const;\n\n/** Balanced speed-vs-size deflate level, on fflate's 0–9 scale (0 = store). */\nconst COMPRESSION_LEVEL = 6;\n\n/**\n * Decompress a ZIP archive into a map of file paths to raw bytes.\n *\n * @param archive - The ZIP archive as an `ArrayBuffer`.\n * @returns A map of file paths to their decompressed content.\n *\n * @throws {@link OBFError} with `info.code` `\"unreadable-zip\"` if the archive is\n * corrupt or cannot be decompressed.\n */\nexport function unzip(archive: ArrayBuffer): Promise<Map<string, Uint8Array>> {\n return new Promise((resolve, reject) => {\n const compressed = new Uint8Array(archive);\n\n fflateUnzip(compressed, (error, entries) => {\n if (error) {\n reject(new OBFError({ code: \"unreadable-zip\" }, { cause: error }));\n return;\n }\n\n const pathToBytes = new Map<string, Uint8Array>(Object.entries(entries));\n\n resolve(pathToBytes);\n });\n });\n}\n\n/**\n * Compress a map of file paths and contents into a single ZIP archive.\n *\n * Accepts both `Uint8Array` and `ArrayBuffer` values so callers can\n * pass the output of {@link unzip} directly or supply raw `ArrayBuffer`s\n * without converting first.\n *\n * @param entries - A map of file paths to their content bytes.\n * @returns The compressed archive as a `Uint8Array`.\n *\n * @throws {@link OBFError} with `info.code` `\"zip-failed\"` if fflate fails to\n * compress an entry.\n */\nexport function zip(\n entries: Map<string, Uint8Array | ArrayBuffer>,\n): Promise<Uint8Array> {\n return new Promise((resolve, reject) => {\n const pathToBytes: Record<string, Uint8Array> = {};\n\n for (const [path, content] of entries) {\n pathToBytes[path] =\n content instanceof Uint8Array ? content : new Uint8Array(content);\n }\n\n fflateZip(pathToBytes, { level: COMPRESSION_LEVEL }, (error, result) => {\n if (error) {\n reject(new OBFError({ code: \"zip-failed\" }, { cause: error }));\n return;\n }\n\n resolve(result);\n });\n });\n}\n\n/**\n * Test whether an `ArrayBuffer` begins with the two-byte ZIP magic\n * prefix (`PK`).\n *\n * @param archive - The buffer to inspect.\n * @returns `true` if the buffer starts with the ZIP signature.\n */\nexport function isZip(archive: ArrayBuffer): boolean {\n const bytes = new Uint8Array(archive);\n\n return (\n bytes.length >= ZIP_MAGIC.length &&\n ZIP_MAGIC.every((byte, index) => bytes[index] === byte)\n );\n}\n","/**\n * Creation and extraction of `.obz` board packages.\n */\n\nimport { OBFError } from \"./errors\";\nimport { parseOBF } from \"./obf\";\nimport type { OBFBoard, OBFManifest } from \"./schema\";\nimport { OBFBoardSchema, OBFManifestSchema } from \"./schema\";\nimport { isZip, unzip, zip } from \"./zip\";\n\n/**\n * Fully extracted contents of an `.obz` archive.\n */\nexport interface ParsedOBZ {\n /** The package's table of contents. */\n manifest: OBFManifest;\n /** Validated board objects keyed by board ID. */\n boards: Map<string, OBFBoard>;\n /**\n * The package's entry-point board — the one `manifest.root` points at,\n * already resolved. Same object as `boards.get(rootBoard.id)`.\n */\n rootBoard: OBFBoard;\n /**\n * Raw bytes for every entry in the archive, keyed by archive path —\n * including `manifest.json` and the `.obf` boards as well as media\n * such as images and sounds.\n */\n resources: Map<string, Uint8Array>;\n}\n\n/**\n * Read a `File` and extract its contents as a parsed OBZ package.\n *\n * This relies on the browser `File` API; for Node environments,\n * read the file to an `ArrayBuffer` and pass it to {@link extractOBZ} instead.\n *\n * @param file - A `File` handle pointing to an `.obz` archive.\n * @returns The parsed manifest, boards, root board, and binary resources.\n *\n * @throws {@link OBFError} — the same failures as {@link extractOBZ}, which\n * this delegates to.\n */\nexport async function loadOBZ(file: File): Promise<ParsedOBZ> {\n const archive = await file.arrayBuffer();\n return extractOBZ(archive);\n}\n\n/**\n * Decompress an OBZ archive and return its manifest, boards, and resources.\n *\n * @param archive - The OBZ archive as an `ArrayBuffer`.\n * @returns The parsed manifest, a map of board IDs to validated boards,\n * the resolved root board, and a map of file paths to their\n * binary content.\n *\n * @throws {@link OBFError}; branch on `info.code`: `\"not-zip\"`,\n * `\"unreadable-zip\"`, `\"missing-manifest\"`, `\"not-json\"` or\n * `\"invalid-manifest\"` (bad manifest), `\"missing-board\"`,\n * `\"board-id-mismatch\"`, or `\"invalid-board\"` (a board fails validation).\n */\nexport async function extractOBZ(archive: ArrayBuffer): Promise<ParsedOBZ> {\n if (!isZip(archive)) {\n throw new OBFError({ code: \"not-zip\" });\n }\n\n const entries = await unzip(archive);\n\n const manifest = extractManifest(entries);\n const { boards, rootBoard } = extractBoards(manifest, entries);\n\n return { manifest, boards, rootBoard, resources: entries };\n}\n\n/**\n * Parse and validate an OBZ manifest — the table of contents that maps\n * board IDs to their file paths within the archive.\n *\n * @param json - A JSON string representing the manifest.\n * @returns The validated manifest object.\n *\n * @throws {@link OBFError} with `info.code` `\"not-json\"` if the JSON is\n * malformed, or `\"invalid-manifest\"` if it fails schema validation.\n */\nexport function parseManifest(json: string): OBFManifest {\n let data: unknown;\n\n try {\n data = JSON.parse(json) as unknown;\n } catch (error) {\n throw new OBFError(\n { code: \"not-json\", source: \"manifest\" },\n { cause: error },\n );\n }\n\n const result = OBFManifestSchema.safeParse(data);\n\n if (!result.success) {\n throw new OBFError(\n { code: \"invalid-manifest\", issues: result.error.issues },\n { cause: result.error },\n );\n }\n\n return result.data;\n}\n\n/**\n * Bundle boards and optional resources into a compressed OBZ archive.\n *\n * A manifest is generated automatically from the supplied boards,\n * using the `rootBoardId` to designate the entry-point board.\n *\n * Every failure is an {@link OBFError}; branch on `info.code`.\n *\n * @param boards - The boards to include in the archive.\n * @param rootBoardId - The ID of the board that serves as the archive's entry point.\n * @param resources - Optional map of file paths to binary content (images, sounds, etc.).\n * @returns A `Blob` containing the compressed OBZ archive.\n *\n * @throws {@link OBFError} `\"unknown-root\"` if `rootBoardId` does not match any of the supplied boards.\n * @throws {@link OBFError} `\"duplicate-board\"` if two supplied boards share the same ID.\n * @throws {@link OBFError} `\"invalid-board\"` if a supplied board fails schema validation.\n * @throws {@link OBFError} `\"conflicting-paths\"` if two boards declare the same media ID with conflicting paths.\n * @throws {@link OBFError} `\"missing-resource\"` if a board declares an image or sound `path` with no matching entry in `resources`.\n * @throws {@link OBFError} `\"path-collision\"` if a `resources` entry would overwrite the generated `manifest.json` or a board file.\n */\nexport async function createOBZ(\n boards: OBFBoard[],\n rootBoardId: string,\n resources?: Map<string, Uint8Array | ArrayBuffer>,\n): Promise<Blob> {\n if (!boards.some((board) => board.id === rootBoardId)) {\n throw new OBFError({ code: \"unknown-root\", rootBoardId });\n }\n\n const seenBoardIds = new Set<string>();\n for (const board of boards) {\n if (seenBoardIds.has(board.id)) {\n throw new OBFError({ code: \"duplicate-board\", boardId: board.id });\n }\n seenBoardIds.add(board.id);\n }\n\n const entries = new Map<string, Uint8Array | ArrayBuffer>();\n\n const boardPaths = Object.fromEntries(\n boards.map((board) => [board.id, `boards/${board.id}.obf`]),\n );\n\n const imagePaths = collectMediaPaths(boards, \"images\");\n const soundPaths = collectMediaPaths(boards, \"sounds\");\n\n const manifestResult = OBFManifestSchema.safeParse({\n format: \"open-board-0.1\",\n root: `boards/${rootBoardId}.obf`,\n paths: {\n boards: boardPaths,\n images: imagePaths,\n ...(Object.keys(soundPaths).length > 0 ? { sounds: soundPaths } : {}),\n },\n });\n\n /* v8 ignore start -- defensive: the manifest is built from already-validated inputs */\n if (!manifestResult.success) {\n throw new OBFError(\n { code: \"internal\", detail: \"generated manifest failed validation\" },\n { cause: manifestResult.error },\n );\n }\n /* v8 ignore stop */\n\n const manifest = manifestResult.data;\n\n const encoder = new TextEncoder();\n\n entries.set(\n \"manifest.json\",\n encoder.encode(JSON.stringify(manifest, null, 2)),\n );\n\n for (const board of boards) {\n const result = OBFBoardSchema.safeParse(board);\n if (!result.success) {\n throw new OBFError(\n {\n code: \"invalid-board\",\n boardId: board.id,\n issues: result.error.issues,\n },\n { cause: result.error },\n );\n }\n const path = `boards/${result.data.id}.obf`;\n entries.set(path, encoder.encode(JSON.stringify(result.data, null, 2)));\n }\n\n if (resources) {\n for (const [path, bytes] of resources) {\n if (entries.has(path)) {\n throw new OBFError({ code: \"path-collision\", path });\n }\n entries.set(path, bytes);\n }\n }\n\n assertPathsPresent(\"image\", imagePaths, entries);\n assertPathsPresent(\"sound\", soundPaths, entries);\n\n const compressed = await zip(entries);\n return new Blob([compressed], { type: \"application/zip\" });\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Walk every board's media collection and produce the `{ id -> path }` map\n * the spec calls \"redundant but still required\" for the OBZ manifest.\n *\n * Throws when two boards declare the same media ID with conflicting paths\n * — a silent OBZ that points at a non-existent file is worse than a clear error.\n */\nfunction collectMediaPaths(\n boards: OBFBoard[],\n kind: \"images\" | \"sounds\",\n): Record<string, string> {\n const paths: Record<string, string> = {};\n\n for (const board of boards) {\n for (const media of board[kind] ?? []) {\n if (media.path === undefined) {\n continue;\n }\n\n const existing = paths[media.id];\n if (existing !== undefined && existing !== media.path) {\n throw new OBFError({\n code: \"conflicting-paths\",\n kind: kind === \"images\" ? \"image\" : \"sound\",\n mediaId: media.id,\n paths: [existing, media.path],\n });\n }\n paths[media.id] = media.path;\n }\n }\n\n return paths;\n}\n\n/**\n * Assert that every media path the generated manifest declares exists as an\n * archive entry — the same contract {@link extractOBZ} assumes when reading.\n *\n * Only media that declared a `path` reach this check, so `url`/`data`-only\n * media are never flagged.\n */\nfunction assertPathsPresent(\n kind: \"image\" | \"sound\",\n paths: Record<string, string>,\n entries: Map<string, Uint8Array | ArrayBuffer>,\n): void {\n for (const [id, path] of Object.entries(paths)) {\n if (!entries.has(path)) {\n throw new OBFError({\n code: \"missing-resource\",\n kind,\n mediaId: id,\n path,\n });\n }\n }\n}\n\nfunction extractManifest(entries: Map<string, Uint8Array>): OBFManifest {\n const manifestBytes = entries.get(\"manifest.json\");\n\n if (!manifestBytes) {\n throw new OBFError({ code: \"missing-manifest\" });\n }\n\n const manifestJson = new TextDecoder().decode(manifestBytes);\n return parseManifest(manifestJson);\n}\n\nfunction extractBoards(\n manifest: OBFManifest,\n entries: Map<string, Uint8Array>,\n): { boards: Map<string, OBFBoard>; rootBoard: OBFBoard } {\n const boards = new Map<string, OBFBoard>();\n let rootBoard: OBFBoard | undefined;\n\n for (const [id, path] of Object.entries(manifest.paths.boards)) {\n const boardBytes = entries.get(path);\n\n if (!boardBytes) {\n throw new OBFError({ code: \"missing-board\", boardId: id, path });\n }\n\n const boardJson = new TextDecoder().decode(boardBytes);\n const board = parseOBF(boardJson);\n\n if (board.id !== id) {\n throw new OBFError({\n code: \"board-id-mismatch\",\n path,\n declaredId: id,\n actualId: board.id,\n });\n }\n\n boards.set(id, board);\n\n if (path === manifest.root) {\n rootBoard = board;\n }\n }\n\n // `OBFManifestSchema` requires `root` to be one of `paths.boards`, so the loop\n // above always assigns `rootBoard` for the validated manifests we receive.\n /* v8 ignore start -- defensive: OBFManifestSchema guarantees root ∈ paths.boards */\n if (!rootBoard) {\n throw new OBFError({\n code: \"internal\",\n detail: `root board \"${manifest.root}\" not found in paths.boards`,\n });\n }\n /* v8 ignore stop */\n\n return { boards, rootBoard };\n}\n","/**\n * Format-agnostic loading of `.obf` boards and `.obz` packages.\n */\n\nimport { parseOBF } from \"./obf\";\nimport { extractOBZ } from \"./obz\";\nimport type { ParsedOBZ } from \"./obz\";\nimport type { OBFBoard } from \"./schema\";\nimport { isZip } from \"./zip\";\n\n/**\n * Result of {@link loadBoard} — a discriminated union over the two file\n * shapes the Open Board Format defines.\n *\n * Switch on `format` to narrow:\n *\n * ```ts\n * const loaded = await loadBoard(file);\n * if (loaded.format === \"obz\") {\n * loaded.archive.rootBoard; // home board of the ParsedOBZ archive\n * } else {\n * loaded.board; // OBFBoard\n * }\n * ```\n */\nexport type LoadedBoard =\n | { format: \"obz\"; archive: ParsedOBZ }\n | { format: \"obf\"; board: OBFBoard };\n\n/**\n * Detect whether the input is a single OBF board or an OBZ package and load it\n * accordingly.\n *\n * Input that begins with the ZIP magic prefix is treated as an `.obz` package;\n * anything else is parsed as an `.obf` board. The input is read once, so\n * consumers can accept either format from a single drag-and-drop, file picker,\n * or fetch response without inspecting the file extension or re-deriving the\n * OBF-vs-OBZ distinction themselves.\n *\n * @param input - A `File` handle or `ArrayBuffer` holding `.obf` or `.obz` content.\n * @returns A discriminated union tagged by `format`.\n *\n * @throws {@link OBFError} — the OBZ failures of {@link extractOBZ} when the\n * input is an archive, or the OBF failures of {@link parseOBF} otherwise.\n * Branch on `error.info.code`.\n */\nexport async function loadBoard(\n input: File | ArrayBuffer,\n): Promise<LoadedBoard> {\n const buffer =\n input instanceof ArrayBuffer ? input : await input.arrayBuffer();\n\n if (isZip(buffer)) {\n return { format: \"obz\", archive: await extractOBZ(buffer) };\n }\n\n return { format: \"obf\", board: parseOBF(new TextDecoder().decode(buffer)) };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shayc/open-board-format",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A TypeScript toolkit for Open Board Format — the open standard for Augmentative and Alternative Communication (AAC) boards.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Shay Cojocaru <shayc@outlook.com>",
|