@shayc/open-board-format 0.4.0 → 0.4.1

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @shayc/open-board-format
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - e7c9b47: docs: polish public API JSDoc for better IntelliSense
8
+
9
+ - Document every exported type alias (`OBFBoard`, `OBFButton`, …) so hover tooltips show a description instead of just the expanded type
10
+ - Move `ParsedOBZ` field docs from `@property` tags onto the fields themselves so they surface in editors
11
+ - Clarify `data_url` as an API endpoint (not a `data:` URI) and note it is outside the `data` → `path` → `url` fallback chain
12
+ - Document `action` vs `actions` precedence per the OBF spec
13
+ - Correct fflate compression-level scale (0–9), standardize `@throws` phrasing, example quoting, and module headers; tag `buildJsonParseErrorMessage` as `@internal`
14
+
3
15
  ## 0.4.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -210,7 +210,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup (Node 22+, Vitest,
210
210
 
211
211
  ## Related
212
212
 
213
- - [Open Board Format specification](https://www.openboardformat.org/docs) — the official standard and format documentation.
213
+ - [Open Board Format specification](https://www.openboardformat.org/docs) — the official standard and format documentation. A 1:1 mirror is kept at [docs/external/open-board-format.md](docs/external/open-board-format.md) for offline reference.
214
214
  - [AAC Board AI](https://github.com/shayc/aac-board-ai) — an offline-first AAC web app built on this package, using on-device browser AI for grammar, tone, and translation ([live app](https://aacboard.app)).
215
215
 
216
216
  ## License
package/dist/index.d.mts CHANGED
@@ -1,62 +1,78 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  //#region src/schema.d.ts
4
- /**
5
- * Open Board Format (OBF) Zod Schemas
6
- *
7
- * These schemas represent the Open Board Format, designed for sharing communication boards and board sets
8
- * between Augmentative and Alternative Communication (AAC) applications.
9
- *
10
- * Official OBF specification: https://www.openboardformat.org/docs
11
- *
12
- * @author Shay Cojocaru
13
- * @license MIT
14
- */
15
4
  /** Unique board-element identifier, coerced to a non-empty string. */
16
5
  declare const OBFIDSchema: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
17
- type OBFID = z.infer<typeof OBFIDSchema>;
18
6
  /**
19
- * Format version of the Open Board Format, e.g., 'open-board-0.1'.
7
+ * Unique board-element identifier, coerced to a non-empty string.
8
+ * See {@link OBFIDSchema}.
20
9
  */
10
+ type OBFID = z.infer<typeof OBFIDSchema>;
11
+ /** Format version of the Open Board Format, e.g., `open-board-0.1`. */
21
12
  declare const OBFFormatVersionSchema: z.ZodString;
13
+ /**
14
+ * Format version of the Open Board Format, e.g., `open-board-0.1`.
15
+ * See {@link OBFFormatVersionSchema}.
16
+ */
22
17
  type OBFFormatVersion = z.infer<typeof OBFFormatVersionSchema>;
23
18
  /**
24
- * Locale identifier, typically a BCP 47 language tag (e.g., 'en', 'en-US', 'fr-CA').
25
- * Not strictly validated — any string is accepted.
19
+ * Locale identifier, typically a BCP 47 language tag (e.g., `en`, `en-US`,
20
+ * `fr-CA`). Not strictly validated — any string is accepted.
26
21
  */
27
22
  declare const OBFLocaleCodeSchema: z.ZodString;
23
+ /**
24
+ * Locale identifier, typically a BCP 47 language tag, e.g., `en`, `en-US`.
25
+ * See {@link OBFLocaleCodeSchema}.
26
+ */
28
27
  type OBFLocaleCode = z.infer<typeof OBFLocaleCodeSchema>;
29
28
  /**
30
- * Key–value pairs mapping symbolic names to their translations in a single locale.
29
+ * Translations for a single locale, keyed by the source string,
30
+ * e.g., `{ "hello": "hola" }`.
31
31
  */
32
32
  declare const OBFLocalizedStringsSchema: z.ZodRecord<z.ZodString, z.ZodString>;
33
+ /**
34
+ * Translations for a single locale, keyed by the source string.
35
+ * See {@link OBFLocalizedStringsSchema}.
36
+ */
33
37
  type OBFLocalizedStrings = z.infer<typeof OBFLocalizedStringsSchema>;
34
38
  /**
35
39
  * Locale-keyed dictionary of translated strings,
36
40
  * e.g., `{ en: { greeting: "Hello" }, fr: { greeting: "Bonjour" } }`.
37
41
  */
38
42
  declare const OBFStringsSchema: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>;
43
+ /**
44
+ * Locale-keyed dictionary of translated strings.
45
+ * See {@link OBFStringsSchema}.
46
+ */
39
47
  type OBFStrings = z.infer<typeof OBFStringsSchema>;
40
48
  /**
41
49
  * Spelling action: a `+` prefix followed by the text to append,
42
- * e.g., `"+hello"`.
50
+ * e.g., `+hello`.
43
51
  */
44
52
  declare const OBFSpellingActionSchema: z.ZodString;
53
+ /**
54
+ * Spelling action: a `+` prefix followed by the text to append, e.g., `+hello`.
55
+ * See {@link OBFSpellingActionSchema}.
56
+ */
45
57
  type OBFSpellingAction = z.infer<typeof OBFSpellingActionSchema>;
46
58
  /**
47
- * Specialty action prefixed with `:`, e.g., `":clear"`.
59
+ * Specialty action prefixed with `:`, e.g., `:clear`.
48
60
  * Custom extensions use the `:ext_` prefix.
49
61
  */
50
62
  declare const OBFSpecialtyActionSchema: z.ZodString;
51
- type OBFSpecialtyAction = z.infer<typeof OBFSpecialtyActionSchema>;
52
63
  /**
53
- * Union of spelling and specialty actions that a button can trigger.
64
+ * Specialty action prefixed with `:`, e.g., `:clear`.
65
+ * See {@link OBFSpecialtyActionSchema}.
54
66
  */
67
+ type OBFSpecialtyAction = z.infer<typeof OBFSpecialtyActionSchema>;
68
+ /** Union of spelling and specialty actions that a button can trigger. */
55
69
  declare const OBFButtonActionSchema: z.ZodUnion<readonly [z.ZodString, z.ZodString]>;
56
- type OBFButtonAction = z.infer<typeof OBFButtonActionSchema>;
57
70
  /**
58
- * License terms and attribution for a resource.
71
+ * Union of spelling and specialty actions that a button can trigger.
72
+ * See {@link OBFButtonActionSchema}.
59
73
  */
74
+ type OBFButtonAction = z.infer<typeof OBFButtonActionSchema>;
75
+ /** License terms and attribution for a resource. */
60
76
  declare const OBFLicenseSchema: z.ZodObject<{
61
77
  type: z.ZodString;
62
78
  copyright_notice_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
@@ -65,14 +81,22 @@ declare const OBFLicenseSchema: z.ZodObject<{
65
81
  author_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
66
82
  author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
67
83
  }, z.core.$loose>;
84
+ /**
85
+ * License terms and attribution for a resource.
86
+ * See {@link OBFLicenseSchema}.
87
+ */
68
88
  type OBFLicense = z.infer<typeof OBFLicenseSchema>;
69
89
  /**
70
90
  * Common properties for media resources (images and sounds).
71
91
  *
72
92
  * When multiple references are provided, they should be used in the following order:
73
- * 1. data
74
- * 2. path
75
- * 3. url
93
+ * 1. `data`
94
+ * 2. `path`
95
+ * 3. `url`
96
+ *
97
+ * `data_url` is not part of this fallback chain — it is an API endpoint for
98
+ * retrieving information about the resource, not an alternative source of
99
+ * the media bytes.
76
100
  */
77
101
  declare const OBFMediaSchema: z.ZodObject<{
78
102
  id: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
@@ -90,14 +114,20 @@ declare const OBFMediaSchema: z.ZodObject<{
90
114
  author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
91
115
  }, z.core.$loose>>;
92
116
  }, z.core.$loose>;
93
- type OBFMedia = z.infer<typeof OBFMediaSchema>;
94
117
  /**
95
- * Reference to a symbol in a proprietary symbol set (e.g., SymbolStix).
118
+ * Common properties for media resources (images and sounds).
119
+ * See {@link OBFMediaSchema}.
96
120
  */
121
+ type OBFMedia = z.infer<typeof OBFMediaSchema>;
122
+ /** Reference to a symbol in a proprietary symbol set (e.g., SymbolStix). */
97
123
  declare const OBFSymbolInfoSchema: z.ZodObject<{
98
124
  set: z.ZodString;
99
125
  filename: z.ZodString;
100
126
  }, z.core.$loose>;
127
+ /**
128
+ * Reference to a symbol in a proprietary symbol set.
129
+ * See {@link OBFSymbolInfoSchema}.
130
+ */
101
131
  type OBFSymbolInfo = z.infer<typeof OBFSymbolInfoSchema>;
102
132
  /**
103
133
  * Image resource, extending {@link OBFMediaSchema} with optional
@@ -131,6 +161,10 @@ declare const OBFImageSchema: z.ZodObject<{
131
161
  width: z.ZodOptional<z.ZodNumber>;
132
162
  height: z.ZodOptional<z.ZodNumber>;
133
163
  }, z.core.$loose>;
164
+ /**
165
+ * Image resource with optional symbol and dimension properties.
166
+ * See {@link OBFImageSchema}.
167
+ */
134
168
  type OBFImage = z.infer<typeof OBFImageSchema>;
135
169
  /**
136
170
  * Audio resource. Identical to {@link OBFMediaSchema} — no additional properties.
@@ -151,10 +185,12 @@ declare const OBFSoundSchema: z.ZodObject<{
151
185
  author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
152
186
  }, z.core.$loose>>;
153
187
  }, z.core.$loose>;
154
- type OBFSound = z.infer<typeof OBFSoundSchema>;
155
188
  /**
156
- * Reference to another board, resolved by ID, path, or URL.
189
+ * Audio resource, identical to {@link OBFMedia}.
190
+ * See {@link OBFSoundSchema}.
157
191
  */
192
+ type OBFSound = z.infer<typeof OBFSoundSchema>;
193
+ /** Reference to another board, resolved by ID, path, or URL. */
158
194
  declare const OBFLoadBoardSchema: z.ZodObject<{
159
195
  id: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string | undefined, string | number>>>;
160
196
  name: z.ZodOptional<z.ZodString>;
@@ -162,6 +198,10 @@ declare const OBFLoadBoardSchema: z.ZodObject<{
162
198
  url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
163
199
  path: z.ZodOptional<z.ZodString>;
164
200
  }, z.core.$loose>;
201
+ /**
202
+ * Reference to another board, resolved by ID, path, or URL.
203
+ * See {@link OBFLoadBoardSchema}.
204
+ */
165
205
  type OBFLoadBoard = z.infer<typeof OBFLoadBoardSchema>;
166
206
  /**
167
207
  * Interactive element on a board, optionally linked to images, sounds, and actions.
@@ -188,6 +228,10 @@ declare const OBFButtonSchema: z.ZodObject<{
188
228
  width: z.ZodOptional<z.ZodNumber>;
189
229
  height: z.ZodOptional<z.ZodNumber>;
190
230
  }, z.core.$loose>;
231
+ /**
232
+ * Interactive element on a board, optionally linked to images, sounds, and
233
+ * actions. See {@link OBFButtonSchema}.
234
+ */
191
235
  type OBFButton = z.infer<typeof OBFButtonSchema>;
192
236
  /**
193
237
  * Row-and-column layout that arranges buttons by their IDs.
@@ -197,6 +241,10 @@ declare const OBFGridSchema: z.ZodObject<{
197
241
  columns: z.ZodNumber;
198
242
  order: z.ZodArray<z.ZodArray<z.ZodUnion<readonly [z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>, z.ZodNull]>>>;
199
243
  }, z.core.$loose>;
244
+ /**
245
+ * Row-and-column layout that arranges buttons by their IDs.
246
+ * See {@link OBFGridSchema}.
247
+ */
200
248
  type OBFGrid = z.infer<typeof OBFGridSchema>;
201
249
  /**
202
250
  * Root object of an `.obf` file: the complete definition of a single communication board.
@@ -283,6 +331,10 @@ declare const OBFBoardSchema: z.ZodObject<{
283
331
  }, z.core.$loose>>;
284
332
  strings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>>;
285
333
  }, z.core.$loose>;
334
+ /**
335
+ * The complete definition of a single communication board — root object of
336
+ * an `.obf` file. See {@link OBFBoardSchema}.
337
+ */
286
338
  type OBFBoard = z.infer<typeof OBFBoardSchema>;
287
339
  /**
288
340
  * Table of contents for an `.obz` package, mapping resource IDs to their archive paths.
@@ -296,6 +348,10 @@ declare const OBFManifestSchema: z.ZodObject<{
296
348
  sounds: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
297
349
  }, z.core.$loose>;
298
350
  }, z.core.$loose>;
351
+ /**
352
+ * Table of contents for an `.obz` package, mapping resource IDs to their
353
+ * archive paths. See {@link OBFManifestSchema}.
354
+ */
299
355
  type OBFManifest = z.infer<typeof OBFManifestSchema>;
300
356
  //#endregion
301
357
  //#region src/obf.d.ts
@@ -308,7 +364,7 @@ type OBFManifest = z.infer<typeof OBFManifestSchema>;
308
364
  * @param json - The JSON string to parse.
309
365
  * @returns The validated board object.
310
366
  *
311
- * @throws {Error} If the JSON is malformed or does not conform to the OBF schema.
367
+ * @throws {Error} If the JSON is malformed or fails schema validation.
312
368
  */
313
369
  declare function parseOBF(json: string): OBFBoard;
314
370
  /**
@@ -329,7 +385,7 @@ declare function loadOBF(file: File): Promise<OBFBoard>;
329
385
  * @param data - The value to validate.
330
386
  * @returns The validated board object.
331
387
  *
332
- * @throws {Error} If the value does not conform to the OBF schema.
388
+ * @throws {Error} If the value fails schema validation.
333
389
  */
334
390
  declare function validateOBF(data: unknown): OBFBoard;
335
391
  /**
@@ -343,20 +399,22 @@ declare function stringifyOBF(board: OBFBoard): string;
343
399
  //#region src/obz.d.ts
344
400
  /**
345
401
  * Fully extracted contents of an `.obz` archive.
346
- *
347
- * @property manifest - The package table of contents.
348
- * @property boards - Board ID → validated board object.
349
- * @property rootBoard - The package's entry-point board — the one `manifest.root`
350
- * points at, already resolved. Same object as
351
- * `boards.get(rootBoard.id)`.
352
- * @property resources - Archive path → raw bytes for every entry in the archive,
353
- * including `manifest.json` and the `.obf` boards as well as
354
- * media such as images and sounds.
355
402
  */
356
403
  interface ParsedOBZ {
404
+ /** The package's table of contents. */
357
405
  manifest: OBFManifest;
406
+ /** Validated board objects keyed by board ID. */
358
407
  boards: Map<string, OBFBoard>;
408
+ /**
409
+ * The package's entry-point board — the one `manifest.root` points at,
410
+ * already resolved. Same object as `boards.get(rootBoard.id)`.
411
+ */
359
412
  rootBoard: OBFBoard;
413
+ /**
414
+ * Raw bytes for every entry in the archive, keyed by archive path —
415
+ * including `manifest.json` and the `.obf` boards as well as media
416
+ * such as images and sounds.
417
+ */
360
418
  resources: Map<string, Uint8Array>;
361
419
  }
362
420
  /**
@@ -406,9 +464,9 @@ declare function parseManifest(json: string): OBFManifest;
406
464
  * @returns A `Blob` containing the compressed OBZ archive.
407
465
  *
408
466
  * @throws {Error} If `rootBoardId` does not match any of the supplied boards.
409
- * @throws {Error} If two supplied boards share the same id.
467
+ * @throws {Error} If two supplied boards share the same ID.
410
468
  * @throws {Error} If a supplied board fails schema validation.
411
- * @throws {Error} If two boards declare the same media id with conflicting paths.
469
+ * @throws {Error} If two boards declare the same media ID with conflicting paths.
412
470
  * @throws {Error} If a board declares an image or sound `path` with no matching entry in `resources`.
413
471
  * @throws {Error} If a `resources` entry would overwrite the generated `manifest.json` or a board file.
414
472
  */
@@ -441,11 +499,11 @@ type LoadedBoard = {
441
499
  * Detect whether the input is a single OBF board or an OBZ package and load it
442
500
  * accordingly.
443
501
  *
444
- * The input is read once and its leading bytes are sniffed for the ZIP magic
445
- * prefix: a ZIP is treated as an `.obz` package, anything else as an `.obf`
446
- * board. This lets consumers accept either format from a single drag-and-drop,
447
- * file picker, or fetch response without inspecting the file extension or
448
- * re-deriving the OBF-vs-OBZ distinction themselves.
502
+ * Input that begins with the ZIP magic prefix is treated as an `.obz` package;
503
+ * anything else is parsed as an `.obf` board. The input is read once, so
504
+ * consumers can accept either format from a single drag-and-drop, file picker,
505
+ * or fetch response without inspecting the file extension or re-deriving the
506
+ * OBF-vs-OBZ distinction themselves.
449
507
  *
450
508
  * @param input - A `File` handle or `ArrayBuffer` holding `.obf` or `.obz` content.
451
509
  * @returns A discriminated union tagged by `format`.
@@ -456,13 +514,16 @@ type LoadedBoard = {
456
514
  declare function loadBoard(input: File | ArrayBuffer): Promise<LoadedBoard>;
457
515
  //#endregion
458
516
  //#region src/zip.d.ts
517
+ /**
518
+ * Minimal ZIP helpers over fflate: signature sniffing, unzip, and zip.
519
+ */
459
520
  /**
460
521
  * Decompress a ZIP archive into a map of file paths to raw bytes.
461
522
  *
462
523
  * @param archive - The ZIP archive as an `ArrayBuffer`.
463
524
  * @returns A map of file paths to their decompressed content.
464
525
  *
465
- * @throws {Error} If decompression fails.
526
+ * @throws {Error} If the archive is corrupt or cannot be decompressed.
466
527
  */
467
528
  declare function unzip(archive: ArrayBuffer): Promise<Map<string, Uint8Array>>;
468
529
  /**
@@ -475,7 +536,7 @@ declare function unzip(archive: ArrayBuffer): Promise<Map<string, Uint8Array>>;
475
536
  * @param entries - A map of file paths to their content bytes.
476
537
  * @returns The compressed archive as a `Uint8Array`.
477
538
  *
478
- * @throws {Error} If compression fails.
539
+ * @throws {Error} If fflate fails to compress an entry.
479
540
  */
480
541
  declare function zip(entries: Map<string, Uint8Array | ArrayBuffer>): Promise<Uint8Array>;
481
542
  /**
package/dist/index.mjs CHANGED
@@ -8,9 +8,6 @@ import { unzip as unzip$1, zip as zip$1 } from "fflate";
8
8
  * between Augmentative and Alternative Communication (AAC) applications.
9
9
  *
10
10
  * Official OBF specification: https://www.openboardformat.org/docs
11
- *
12
- * @author Shay Cojocaru
13
- * @license MIT
14
11
  */
15
12
  /** Optional URL that treats empty strings as undefined. */
16
13
  const OBFOptionalUrlSchema = z.union([z.url(), z.literal("")]).transform((val) => val === "" ? void 0 : val).optional();
@@ -23,17 +20,16 @@ const OBFOptionalIDSchema = z.union([z.string(), z.number()]).transform((val) =>
23
20
  }).optional();
24
21
  /** Unique board-element identifier, coerced to a non-empty string. */
25
22
  const OBFIDSchema = z.union([z.string(), z.number()]).transform((val) => String(val)).pipe(z.string().min(1));
26
- /**
27
- * Format version of the Open Board Format, e.g., 'open-board-0.1'.
28
- */
23
+ /** Format version of the Open Board Format, e.g., `open-board-0.1`. */
29
24
  const OBFFormatVersionSchema = z.string().regex(/^open-board-.+$/);
30
25
  /**
31
- * Locale identifier, typically a BCP 47 language tag (e.g., 'en', 'en-US', 'fr-CA').
32
- * Not strictly validated — any string is accepted.
26
+ * Locale identifier, typically a BCP 47 language tag (e.g., `en`, `en-US`,
27
+ * `fr-CA`). Not strictly validated — any string is accepted.
33
28
  */
34
29
  const OBFLocaleCodeSchema = z.string();
35
30
  /**
36
- * Key–value pairs mapping symbolic names to their translations in a single locale.
31
+ * Translations for a single locale, keyed by the source string,
32
+ * e.g., `{ "hello": "hola" }`.
37
33
  */
38
34
  const OBFLocalizedStringsSchema = z.record(z.string(), z.string());
39
35
  /**
@@ -43,23 +39,19 @@ const OBFLocalizedStringsSchema = z.record(z.string(), z.string());
43
39
  const OBFStringsSchema = z.record(z.string(), OBFLocalizedStringsSchema);
44
40
  /**
45
41
  * Spelling action: a `+` prefix followed by the text to append,
46
- * e.g., `"+hello"`.
42
+ * e.g., `+hello`.
47
43
  */
48
44
  const OBFSpellingActionSchema = z.string().regex(/^\+.+$/);
49
45
  /**
50
- * Specialty action prefixed with `:`, e.g., `":clear"`.
46
+ * Specialty action prefixed with `:`, e.g., `:clear`.
51
47
  * Custom extensions use the `:ext_` prefix.
52
48
  */
53
49
  const OBFSpecialtyActionSchema = z.string().regex(/^:[a-z][a-z0-9_-]*$/i);
54
- /**
55
- * Union of spelling and specialty actions that a button can trigger.
56
- */
50
+ /** Union of spelling and specialty actions that a button can trigger. */
57
51
  const OBFButtonActionSchema = z.union([OBFSpellingActionSchema, OBFSpecialtyActionSchema]);
58
- /**
59
- * License terms and attribution for a resource.
60
- */
52
+ /** License terms and attribution for a resource. */
61
53
  const OBFLicenseSchema = z.looseObject({
62
- /** Type of the license, e.g., 'CC-BY-SA'. */
54
+ /** Type of the license, e.g., `CC-BY-SA`. */
63
55
  type: z.string(),
64
56
  /** URL to the license terms. */
65
57
  copyright_notice_url: OBFOptionalUrlSchema,
@@ -76,31 +68,36 @@ const OBFLicenseSchema = z.looseObject({
76
68
  * Common properties for media resources (images and sounds).
77
69
  *
78
70
  * When multiple references are provided, they should be used in the following order:
79
- * 1. data
80
- * 2. path
81
- * 3. url
71
+ * 1. `data`
72
+ * 2. `path`
73
+ * 3. `url`
74
+ *
75
+ * `data_url` is not part of this fallback chain — it is an API endpoint for
76
+ * retrieving information about the resource, not an alternative source of
77
+ * the media bytes.
82
78
  */
83
79
  const OBFMediaSchema = z.looseObject({
84
80
  /** Unique identifier for the media resource. */
85
81
  id: OBFIDSchema,
86
- /** Data URI containing the media data. */
82
+ /** Media data inlined as a `data:` URI. */
87
83
  data: z.string().optional(),
88
- /** Path to the media file within an .obz package. */
84
+ /** Path to the media file within an `.obz` package. */
89
85
  path: z.string().optional(),
90
- /** Data URL to fetch the media programmatically. */
86
+ /**
87
+ * URL of an API endpoint for fetching the media programmatically —
88
+ * not a `data:` URI (that is `data`).
89
+ */
91
90
  data_url: OBFOptionalUrlSchema,
92
91
  /** URL to the media resource. */
93
92
  url: OBFOptionalUrlSchema,
94
- /** MIME type of the media, e.g., 'image/png', 'audio/mpeg'. */
93
+ /** MIME type of the media, e.g., `image/png`, `audio/mpeg`. */
95
94
  content_type: z.string().optional(),
96
95
  /** Licensing information for the media. */
97
96
  license: OBFLicenseSchema.optional()
98
97
  });
99
- /**
100
- * Reference to a symbol in a proprietary symbol set (e.g., SymbolStix).
101
- */
98
+ /** Reference to a symbol in a proprietary symbol set (e.g., SymbolStix). */
102
99
  const OBFSymbolInfoSchema = z.looseObject({
103
- /** Name of the symbol set, e.g., 'symbolstix'. */
100
+ /** Name of the symbol set, e.g., `symbolstix`. */
104
101
  set: z.string(),
105
102
  /** Filename of the symbol within the set. */
106
103
  filename: z.string()
@@ -127,19 +124,20 @@ const OBFImageSchema = OBFMediaSchema.extend({
127
124
  * Audio resource. Identical to {@link OBFMediaSchema} — no additional properties.
128
125
  */
129
126
  const OBFSoundSchema = OBFMediaSchema;
130
- /**
131
- * Reference to another board, resolved by ID, path, or URL.
132
- */
127
+ /** Reference to another board, resolved by ID, path, or URL. */
133
128
  const OBFLoadBoardSchema = z.looseObject({
134
129
  /** Unique identifier of the board to load. */
135
130
  id: OBFOptionalIDSchema,
136
131
  /** Name of the board to load. */
137
132
  name: z.string().optional(),
138
- /** Data URL to fetch the board programmatically. */
133
+ /**
134
+ * URL of an API endpoint for fetching the board programmatically —
135
+ * not a `data:` URI.
136
+ */
139
137
  data_url: OBFOptionalUrlSchema,
140
138
  /** URL to access the board via a web browser. */
141
139
  url: OBFOptionalUrlSchema,
142
- /** Path to the board within an .obz package. */
140
+ /** Path to the board within an `.obz` package. */
143
141
  path: z.string().optional()
144
142
  });
145
143
  /**
@@ -156,15 +154,21 @@ const OBFButtonSchema = z.looseObject({
156
154
  image_id: OBFOptionalIDSchema,
157
155
  /** Identifier of the sound associated with the button. */
158
156
  sound_id: OBFOptionalIDSchema,
159
- /** Action associated with the button. */
157
+ /**
158
+ * Action triggered by the button. When `actions` is also set, this is
159
+ * the single-action fallback for apps that support one action per button.
160
+ */
160
161
  action: OBFButtonActionSchema.optional(),
161
- /** List of multiple actions for the button, executed in order. */
162
+ /**
163
+ * Multiple actions executed in order. Apps that support it should
164
+ * prefer this over the single `action` fallback.
165
+ */
162
166
  actions: z.array(OBFButtonActionSchema).optional(),
163
167
  /** Information to load another board when this button is activated. */
164
168
  load_board: OBFLoadBoardSchema.optional(),
165
- /** Background color of the button in 'rgb' or 'rgba' format. */
169
+ /** Background color of the button in `rgb` or `rgba` format. */
166
170
  background_color: z.string().optional(),
167
- /** Border color of the button in 'rgb' or 'rgba' format. */
171
+ /** Border color of the button in `rgb` or `rgba` format. */
168
172
  border_color: z.string().optional(),
169
173
  /** Vertical position for absolute positioning (0.0 to 1.0). */
170
174
  top: z.number().min(0).max(1).optional(),
@@ -193,11 +197,11 @@ const OBFGridSchema = z.looseObject({
193
197
  * Root object of an `.obf` file: the complete definition of a single communication board.
194
198
  */
195
199
  const OBFBoardSchema = z.looseObject({
196
- /** Format version of the Open Board Format, e.g., 'open-board-0.1'. */
200
+ /** Format version of the Open Board Format, e.g., `open-board-0.1`. */
197
201
  format: OBFFormatVersionSchema,
198
202
  /** Unique identifier for the board. */
199
203
  id: OBFIDSchema,
200
- /** Locale of the board as a BCP 47 language tag, e.g., 'en', 'en-US'. */
204
+ /** Locale of the board as a BCP 47 language tag, e.g., `en`, `en-US`. */
201
205
  locale: OBFLocaleCodeSchema.optional(),
202
206
  /** List of buttons on the board. */
203
207
  buttons: z.array(OBFButtonSchema),
@@ -222,9 +226,9 @@ const OBFBoardSchema = z.looseObject({
222
226
  * Table of contents for an `.obz` package, mapping resource IDs to their archive paths.
223
227
  */
224
228
  const OBFManifestSchema = z.looseObject({
225
- /** Format version of the Open Board Format, e.g., 'open-board-0.1'. */
229
+ /** Format version of the Open Board Format, e.g., `open-board-0.1`. */
226
230
  format: OBFFormatVersionSchema,
227
- /** Path to the root board within the .obz package. */
231
+ /** Path to the root board within the `.obz` package. */
228
232
  root: z.string(),
229
233
  /** Mapping of IDs to paths for boards, images, and sounds. */
230
234
  paths: z.looseObject({
@@ -246,7 +250,12 @@ const UTF8_BOM = "";
246
250
  function stripBom(text) {
247
251
  return text.startsWith(UTF8_BOM) ? text.slice(1) : text;
248
252
  }
249
- /** Build a descriptive JSON parse-failure message, preserving the engine's reason when available. */
253
+ /**
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
+ */
250
259
  function buildJsonParseErrorMessage(label, error) {
251
260
  const reason = error instanceof Error ? error.message : "";
252
261
  return reason ? `Invalid ${label}: JSON parse failed — ${reason}` : `Invalid ${label}: JSON parse failed`;
@@ -260,7 +269,7 @@ function buildJsonParseErrorMessage(label, error) {
260
269
  * @param json - The JSON string to parse.
261
270
  * @returns The validated board object.
262
271
  *
263
- * @throws {Error} If the JSON is malformed or does not conform to the OBF schema.
272
+ * @throws {Error} If the JSON is malformed or fails schema validation.
264
273
  */
265
274
  function parseOBF(json) {
266
275
  const sanitized = stripBom(json);
@@ -292,7 +301,7 @@ async function loadOBF(file) {
292
301
  * @param data - The value to validate.
293
302
  * @returns The validated board object.
294
303
  *
295
- * @throws {Error} If the value does not conform to the OBF schema.
304
+ * @throws {Error} If the value fails schema validation.
296
305
  */
297
306
  function validateOBF(data) {
298
307
  const result = OBFBoardSchema.safeParse(data);
@@ -311,6 +320,9 @@ function stringifyOBF(board) {
311
320
  //#endregion
312
321
  //#region src/zip.ts
313
322
  /**
323
+ * Minimal ZIP helpers over fflate: signature sniffing, unzip, and zip.
324
+ */
325
+ /**
314
326
  * First two bytes of every ZIP archive — the ASCII letters `PK`,
315
327
  * after Phil Katz, creator of the format.
316
328
  *
@@ -318,7 +330,7 @@ function stringifyOBF(board) {
318
330
  * test lightweight and sufficient for distinguishing ZIP from JSON.
319
331
  */
320
332
  const ZIP_MAGIC = [80, 75];
321
- /** Balanced speed-vs-size deflate level used by fflate (1–9 scale). */
333
+ /** Balanced speed-vs-size deflate level, on fflate's 0–9 scale (0 = store). */
322
334
  const COMPRESSION_LEVEL = 6;
323
335
  /**
324
336
  * Decompress a ZIP archive into a map of file paths to raw bytes.
@@ -326,7 +338,7 @@ const COMPRESSION_LEVEL = 6;
326
338
  * @param archive - The ZIP archive as an `ArrayBuffer`.
327
339
  * @returns A map of file paths to their decompressed content.
328
340
  *
329
- * @throws {Error} If decompression fails.
341
+ * @throws {Error} If the archive is corrupt or cannot be decompressed.
330
342
  */
331
343
  function unzip(archive) {
332
344
  return new Promise((resolve, reject) => {
@@ -349,7 +361,7 @@ function unzip(archive) {
349
361
  * @param entries - A map of file paths to their content bytes.
350
362
  * @returns The compressed archive as a `Uint8Array`.
351
363
  *
352
- * @throws {Error} If compression fails.
364
+ * @throws {Error} If fflate fails to compress an entry.
353
365
  */
354
366
  function zip(entries) {
355
367
  return new Promise((resolve, reject) => {
@@ -378,6 +390,9 @@ function isZip(archive) {
378
390
  //#endregion
379
391
  //#region src/obz.ts
380
392
  /**
393
+ * Creation and extraction of `.obz` board packages.
394
+ */
395
+ /**
381
396
  * Read a `File` and extract its contents as a parsed OBZ package.
382
397
  *
383
398
  * This relies on the browser `File` API; for Node environments,
@@ -447,9 +462,9 @@ function parseManifest(json) {
447
462
  * @returns A `Blob` containing the compressed OBZ archive.
448
463
  *
449
464
  * @throws {Error} If `rootBoardId` does not match any of the supplied boards.
450
- * @throws {Error} If two supplied boards share the same id.
465
+ * @throws {Error} If two supplied boards share the same ID.
451
466
  * @throws {Error} If a supplied board fails schema validation.
452
- * @throws {Error} If two boards declare the same media id with conflicting paths.
467
+ * @throws {Error} If two boards declare the same media ID with conflicting paths.
453
468
  * @throws {Error} If a board declares an image or sound `path` with no matching entry in `resources`.
454
469
  * @throws {Error} If a `resources` entry would overwrite the generated `manifest.json` or a board file.
455
470
  */
@@ -496,7 +511,7 @@ async function createOBZ(boards, rootBoardId, resources) {
496
511
  * Walk every board's media collection and produce the `{ id -> path }` map
497
512
  * the spec calls "redundant but still required" for the OBZ manifest.
498
513
  *
499
- * Throws when two boards declare the same media id with conflicting paths
514
+ * Throws when two boards declare the same media ID with conflicting paths
500
515
  * — a silent OBZ that points at a non-existent file is worse than a clear error.
501
516
  */
502
517
  function collectMediaPaths(boards, kind) {
@@ -544,14 +559,17 @@ function extractBoards(manifest, entries) {
544
559
  //#endregion
545
560
  //#region src/load-board.ts
546
561
  /**
562
+ * Format-agnostic loading of `.obf` boards and `.obz` packages.
563
+ */
564
+ /**
547
565
  * Detect whether the input is a single OBF board or an OBZ package and load it
548
566
  * accordingly.
549
567
  *
550
- * The input is read once and its leading bytes are sniffed for the ZIP magic
551
- * prefix: a ZIP is treated as an `.obz` package, anything else as an `.obf`
552
- * board. This lets consumers accept either format from a single drag-and-drop,
553
- * file picker, or fetch response without inspecting the file extension or
554
- * re-deriving the OBF-vs-OBZ distinction themselves.
568
+ * Input that begins with the ZIP magic prefix is treated as an `.obz` package;
569
+ * anything else is parsed as an `.obf` board. The input is read once, so
570
+ * consumers can accept either format from a single drag-and-drop, file picker,
571
+ * or fetch response without inspecting the file extension or re-deriving the
572
+ * OBF-vs-OBZ distinction themselves.
555
573
  *
556
574
  * @param input - A `File` handle or `ArrayBuffer` holding `.obf` or `.obz` content.
557
575
  * @returns A discriminated union tagged by `format`.
@@ -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 * @author Shay Cojocaru\n * @license MIT\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));\nexport type OBFID = z.infer<typeof OBFIDSchema>;\n\n/**\n * Format version of the Open Board Format, e.g., 'open-board-0.1'.\n */\nexport const OBFFormatVersionSchema = z.string().regex(/^open-board-.+$/);\nexport type OBFFormatVersion = z.infer<typeof OBFFormatVersionSchema>;\n\n/**\n * Locale identifier, typically a BCP 47 language tag (e.g., 'en', 'en-US', 'fr-CA').\n * Not strictly validated — any string is accepted.\n */\nexport const OBFLocaleCodeSchema = z.string();\nexport type OBFLocaleCode = z.infer<typeof OBFLocaleCodeSchema>;\n\n/**\n * Key–value pairs mapping symbolic names to their translations in a single locale.\n */\nexport const OBFLocalizedStringsSchema = z.record(z.string(), z.string());\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);\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(/^\\+.+$/);\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);\nexport type OBFSpecialtyAction = z.infer<typeof OBFSpecialtyActionSchema>;\n\n/**\n * Union of spelling and specialty actions that a button can trigger.\n */\nexport const OBFButtonActionSchema = z.union([\n OBFSpellingActionSchema,\n OBFSpecialtyActionSchema,\n]);\nexport type OBFButtonAction = z.infer<typeof OBFButtonActionSchema>;\n\n/**\n * License terms and attribution for a resource.\n */\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\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 */\nexport const OBFMediaSchema = z.looseObject({\n /** Unique identifier for the media resource. */\n id: OBFIDSchema,\n /** Data URI containing the media data. */\n data: z.string().optional(),\n /** Path to the media file within an .obz package. */\n path: z.string().optional(),\n /** Data URL to fetch the media programmatically. */\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\nexport type OBFMedia = z.infer<typeof OBFMediaSchema>;\n\n/**\n * Reference to a symbol in a proprietary symbol set (e.g., SymbolStix).\n */\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});\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});\nexport type OBFImage = z.infer<typeof OBFImageSchema>;\n\n/**\n * Audio resource. Identical to {@link OBFMediaSchema} — no additional properties.\n */\nexport const OBFSoundSchema = OBFMediaSchema;\nexport type OBFSound = z.infer<typeof OBFSoundSchema>;\n\n/**\n * Reference to another board, resolved by ID, path, or URL.\n */\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 /** Data URL to fetch the board programmatically. */\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\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 /** Action associated with the button. */\n action: OBFButtonActionSchema.optional(),\n /** List of multiple actions for the button, executed in order. */\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\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 });\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\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 });\nexport type OBFManifest = z.infer<typeof OBFManifestSchema>;\n","import 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/** Build a descriptive JSON parse-failure message, preserving the engine's reason when available. */\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 does not conform to the OBF schema.\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 does not conform to the OBF schema.\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","import { 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 used by fflate (1–9 scale). */\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 decompression fails.\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 compression fails.\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","import { 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 *\n * @property manifest - The package table of contents.\n * @property boards - Board ID → validated board object.\n * @property rootBoard - The package's entry-point board — the one `manifest.root`\n * points at, already resolved. Same object as\n * `boards.get(rootBoard.id)`.\n * @property resources - Archive path → raw bytes for every entry in the archive,\n * including `manifest.json` and the `.obf` boards as well as\n * media such as images and sounds.\n */\nexport interface ParsedOBZ {\n manifest: OBFManifest;\n boards: Map<string, OBFBoard>;\n rootBoard: OBFBoard;\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","import { 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 * The input is read once and its leading bytes are sniffed for the ZIP magic\n * prefix: a ZIP is treated as an `.obz` package, anything else as an `.obf`\n * board. This lets consumers accept either format from a single drag-and-drop,\n * file picker, or fetch response without inspecting the file extension or\n * re-deriving the 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":";;;;;;;;;;;;;;;AAeA,MAAM,uBAAuB,EAC1B,MAAM,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,CAC/B,WAAW,QAAS,QAAQ,KAAK,KAAA,IAAY,GAAI,CAAC,CAClD,SAAS;;AAGZ,MAAM,yBAAyB,EAC5B,MAAM,CAAC,EAAE,MAAM,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,CACjC,WAAW,QAAS,QAAQ,KAAK,KAAA,IAAY,GAAI,CAAC,CAClD,SAAS;;AAGZ,MAAM,sBAAsB,EACzB,MAAM,CAAC,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,CAC/B,WAAW,QAAQ;CAClB,MAAM,MAAM,OAAO,GAAG;CACtB,OAAO,QAAQ,KAAK,KAAA,IAAY;AAClC,CAAC,CAAC,CACD,SAAS;;AAGZ,MAAa,cAAc,EACxB,MAAM,CAAC,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,CAC/B,WAAW,QAAQ,OAAO,GAAG,CAAC,CAAC,CAC/B,KAAK,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC;;;;AAMzB,MAAa,yBAAyB,EAAE,OAAO,CAAC,CAAC,MAAM,iBAAiB;;;;;AAOxE,MAAa,sBAAsB,EAAE,OAAO;;;;AAM5C,MAAa,4BAA4B,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC;;;;;AAOxE,MAAa,mBAAmB,EAAE,OAAO,EAAE,OAAO,GAAG,yBAAyB;;;;;AAO9E,MAAa,0BAA0B,EAAE,OAAO,CAAC,CAAC,MAAM,QAAQ;;;;;AAOhE,MAAa,2BAA2B,EACrC,OAAO,CAAC,CACR,MAAM,sBAAsB;;;;AAM/B,MAAa,wBAAwB,EAAE,MAAM,CAC3C,yBACA,wBACF,CAAC;;;;AAMD,MAAa,mBAAmB,EAAE,YAAY;;CAE5C,MAAM,EAAE,OAAO;;CAEf,sBAAsB;;CAEtB,YAAY;;CAEZ,aAAa,EAAE,OAAO,CAAC,CAAC,SAAS;;CAEjC,YAAY;;CAEZ,cAAc;AAChB,CAAC;;;;;;;;;AAYD,MAAa,iBAAiB,EAAE,YAAY;;CAE1C,IAAI;;CAEJ,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS;;CAE1B,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS;;CAE1B,UAAU;;CAEV,KAAK;;CAEL,cAAc,EAAE,OAAO,CAAC,CAAC,SAAS;;CAElC,SAAS,iBAAiB,SAAS;AACrC,CAAC;;;;AAOD,MAAa,sBAAsB,EAAE,YAAY;;CAE/C,KAAK,EAAE,OAAO;;CAEd,UAAU,EAAE,OAAO;AACrB,CAAC;;;;;;;;;;;AAaD,MAAa,iBAAiB,eAAe,OAAO;;CAElD,QAAQ,oBAAoB,SAAS;;CAErC,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;;CAE3B,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;AAC9B,CAAC;;;;AAMD,MAAa,iBAAiB;;;;AAM9B,MAAa,qBAAqB,EAAE,YAAY;;CAE9C,IAAI;;CAEJ,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS;;CAE1B,UAAU;;CAEV,KAAK;;CAEL,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS;AAC5B,CAAC;;;;AAOD,MAAa,kBAAkB,EAAE,YAAY;;CAE3C,IAAI;;CAEJ,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;;CAE3B,cAAc,EAAE,OAAO,CAAC,CAAC,SAAS;;CAElC,UAAU;;CAEV,UAAU;;CAEV,QAAQ,sBAAsB,SAAS;;CAEvC,SAAS,EAAE,MAAM,qBAAqB,CAAC,CAAC,SAAS;;CAEjD,YAAY,mBAAmB,SAAS;;CAExC,kBAAkB,EAAE,OAAO,CAAC,CAAC,SAAS;;CAEtC,cAAc,EAAE,OAAO,CAAC,CAAC,SAAS;;CAElC,KAAK,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;;CAEvC,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;;CAExC,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;;CAEzC,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;AAC5C,CAAC;;;;AAOD,MAAa,gBAAgB,EAC1B,YAAY;;CAEX,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC;;CAE5B,SAAS,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC;;;;;CAK/B,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;AAC1D,CAAC,CAAC,CACD,QAAQ,MAAM,EAAE,MAAM,WAAW,EAAE,MAAM,EACxC,SAAS,oCACX,CAAC,CAAC,CACD,QAAQ,MAAM,EAAE,MAAM,OAAO,QAAQ,IAAI,WAAW,EAAE,OAAO,GAAG,EAC/D,SAAS,kDACX,CAAC;;;;AAMH,MAAa,iBAAiB,EAAE,YAAY;;CAE1C,QAAQ;;CAER,IAAI;;CAEJ,QAAQ,oBAAoB,SAAS;;CAErC,SAAS,EAAE,MAAM,eAAe;;CAEhC,KAAK;;CAEL,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS;;CAE1B,kBAAkB,EAAE,OAAO,CAAC,CAAC,SAAS;;CAEtC,MAAM;;CAEN,QAAQ,EAAE,MAAM,cAAc,CAAC,CAAC,SAAS;;CAEzC,QAAQ,EAAE,MAAM,cAAc,CAAC,CAAC,SAAS;;CAEzC,SAAS,iBAAiB,SAAS;;CAEnC,SAAS,iBAAiB,SAAS;AACrC,CAAC;;;;AAOD,MAAa,oBAAoB,EAC9B,YAAY;;CAEX,QAAQ;;CAER,MAAM,EAAE,OAAO;;CAEf,OAAO,EAAE,YAAY;;EAEnB,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC;;EAEvC,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC;;EAEvC,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS;CACpD,CAAC;AACH,CAAC,CAAC,CACD,QAAQ,MAAM,OAAO,OAAO,EAAE,MAAM,MAAM,CAAC,CAAC,SAAS,EAAE,IAAI,GAAG;CAC7D,SAAS;CACT,MAAM,CAAC,MAAM;AACf,CAAC;;;ACjTH,MAAM,WAAW;;AAGjB,SAAS,SAAS,MAAsB;CACtC,OAAO,KAAK,WAAW,QAAQ,IAAI,KAAK,MAAM,CAAC,IAAI;AACrD;;AAGA,SAAgB,2BACd,OACA,OACQ;CACR,MAAM,SAAS,iBAAiB,QAAQ,MAAM,UAAU;CACxD,OAAO,SACH,WAAW,MAAM,wBAAwB,WACzC,WAAW,MAAM;AACvB;;;;;;;;;;;;AAaA,SAAgB,SAAS,MAAwB;CAC/C,MAAM,YAAY,SAAS,IAAI;CAE/B,IAAI;CAEJ,IAAI;EACF,WAAW,KAAK,MAAM,SAAS;CACjC,SAAS,OAAO;EACd,MAAM,IAAI,MAAM,2BAA2B,OAAO,KAAK,GAAG,EAAE,OAAO,MAAM,CAAC;CAC5E;CAEA,OAAO,YAAY,QAAQ;AAC7B;;;;;;;;;;;;AAaA,eAAsB,QAAQ,MAA+B;CAE3D,OAAO,SAAS,MADG,KAAK,KAAK,CACT;AACtB;;;;;;;;;AAUA,SAAgB,YAAY,MAAyB;CACnD,MAAM,SAAS,eAAe,UAAU,IAAI;CAE5C,IAAI,CAAC,OAAO,SACV,MAAM,IAAI,MAAM,gBAAgB,OAAO,MAAM,SAAS;CAGxD,OAAO,OAAO;AAChB;;;;;;;AAQA,SAAgB,aAAa,OAAyB;CACpD,OAAO,KAAK,UAAU,OAAO,MAAM,CAAC;AACtC;;;;;;;;;;AC/EA,MAAM,YAAY,CAAC,IAAM,EAAI;;AAG7B,MAAM,oBAAoB;;;;;;;;;AAU1B,SAAgB,MAAM,SAAwD;CAC5E,OAAO,IAAI,SAAS,SAAS,WAAW;EAGtC,QAAY,IAFW,WAAW,OAEb,IAAI,OAAO,YAAY;GAC1C,IAAI,OAAO;IACT,uBAAO,IAAI,MAAM,oBAAoB,MAAM,WAAW,OAAO,KAAK,GAAG,CAAC;IACtE;GACF;GAIA,QAAQ,IAFgB,IAAwB,OAAO,QAAQ,OAAO,CAEpD,CAAC;EACrB,CAAC;CACH,CAAC;AACH;;;;;;;;;;;;;AAcA,SAAgB,IACd,SACqB;CACrB,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,cAA0C,CAAC;EAEjD,KAAK,MAAM,CAAC,MAAM,YAAY,SAC5B,YAAY,QACV,mBAAmB,aAAa,UAAU,IAAI,WAAW,OAAO;EAGpE,MAAU,aAAa,EAAE,OAAO,kBAAkB,IAAI,OAAO,WAAW;GACtE,IAAI,OAAO;IACT,uBAAO,IAAI,MAAM,kBAAkB,MAAM,WAAW,OAAO,KAAK,GAAG,CAAC;IACpE;GACF;GAEA,QAAQ,MAAM;EAChB,CAAC;CACH,CAAC;AACH;;;;;;;;AASA,SAAgB,MAAM,SAA+B;CACnD,MAAM,QAAQ,IAAI,WAAW,OAAO;CAEpC,OACE,MAAM,UAAU,UAAU,UAC1B,UAAU,OAAO,MAAM,UAAU,MAAM,WAAW,IAAI;AAE1D;;;;;;;;;;;;;;ACpDA,eAAsB,QAAQ,MAAgC;CAE5D,OAAO,WAAW,MADI,KAAK,YAAY,CACd;AAC3B;;;;;;;;;;;;;AAcA,eAAsB,WAAW,SAA0C;CACzE,IAAI,CAAC,MAAM,OAAO,GAChB,MAAM,IAAI,MAAM,6BAA6B;CAG/C,MAAM,UAAU,MAAM,MAAM,OAAO;CAEnC,MAAM,WAAW,gBAAgB,OAAO;CACxC,MAAM,EAAE,QAAQ,cAAc,cAAc,UAAU,OAAO;CAE7D,OAAO;EAAE;EAAU;EAAQ;EAAW,WAAW;CAAQ;AAC3D;;;;;;;;;;AAWA,SAAgB,cAAc,MAA2B;CACvD,IAAI;CAEJ,IAAI;EACF,OAAO,KAAK,MAAM,IAAI;CACxB,SAAS,OAAO;EACd,MAAM,IAAI,MAAM,2BAA2B,YAAY,KAAK,GAAG,EAC7D,OAAO,MACT,CAAC;CACH;CAEA,MAAM,SAAS,kBAAkB,UAAU,IAAI;CAE/C,IAAI,CAAC,OAAO,SACV,MAAM,IAAI,MAAM,qBAAqB,OAAO,MAAM,SAAS;CAG7D,OAAO,OAAO;AAChB;;;;;;;;;;;;;;;;;;;AAoBA,eAAsB,UACpB,QACA,aACA,WACe;CACf,IAAI,CAAC,OAAO,MAAM,UAAU,MAAM,OAAO,WAAW,GAClD,MAAM,IAAI,MACR,6BAA6B,YAAY,oCAC3C;CAGF,MAAM,+BAAe,IAAI,IAAY;CACrC,KAAK,MAAM,SAAS,QAAQ;EAC1B,IAAI,aAAa,IAAI,MAAM,EAAE,GAC3B,MAAM,IAAI,MACR,oCAAoC,MAAM,GAAG,8CAC/C;EAEF,aAAa,IAAI,MAAM,EAAE;CAC3B;CAEA,MAAM,0BAAU,IAAI,IAAsC;CAE1D,MAAM,aAAa,OAAO,YACxB,OAAO,KAAK,UAAU,CAAC,MAAM,IAAI,UAAU,MAAM,GAAG,KAAK,CAAC,CAC5D;CAEA,MAAM,aAAa,kBAAkB,QAAQ,QAAQ;CACrD,MAAM,aAAa,kBAAkB,QAAQ,QAAQ;CAErD,MAAM,iBAAiB,kBAAkB,UAAU;EACjD,QAAQ;EACR,MAAM,UAAU,YAAY;EAC5B,OAAO;GACL,QAAQ;GACR,QAAQ;GACR,GAAI,OAAO,KAAK,UAAU,CAAC,CAAC,SAAS,IAAI,EAAE,QAAQ,WAAW,IAAI,CAAC;EACrE;CACF,CAAC;CAED,IAAI,CAAC,eAAe,SAClB,MAAM,IAAI,MACR,uDAAuD,eAAe,MAAM,SAC9E;CAGF,MAAM,WAAW,eAAe;CAEhC,MAAM,UAAU,IAAI,YAAY;CAEhC,QAAQ,IACN,iBACA,QAAQ,OAAO,KAAK,UAAU,UAAU,MAAM,CAAC,CAAC,CAClD;CAEA,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,SAAS,eAAe,UAAU,KAAK;EAC7C,IAAI,CAAC,OAAO,SACV,MAAM,IAAI,MACR,uBAAuB,MAAM,GAAG,wBAAwB,OAAO,MAAM,SACvE;EAEF,MAAM,OAAO,UAAU,OAAO,KAAK,GAAG;EACtC,QAAQ,IAAI,MAAM,QAAQ,OAAO,KAAK,UAAU,OAAO,MAAM,MAAM,CAAC,CAAC,CAAC;CACxE;CAEA,IAAI,WACF,KAAK,MAAM,CAAC,MAAM,UAAU,WAAW;EACrC,IAAI,QAAQ,IAAI,IAAI,GAClB,MAAM,IAAI,MACR,+BAA+B,KAAK,oDACtC;EAEF,QAAQ,IAAI,MAAM,KAAK;CACzB;CAGF,mBAAmB,SAAS,YAAY,OAAO;CAC/C,mBAAmB,SAAS,YAAY,OAAO;CAE/C,MAAM,aAAa,MAAM,IAAI,OAAO;CACpC,OAAO,IAAI,KAAK,CAAC,UAAU,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAC3D;;;;;;;;AAaA,SAAS,kBACP,QACA,MACwB;CACxB,MAAM,QAAgC,CAAC;CAEvC,KAAK,MAAM,SAAS,QAClB,KAAK,MAAM,SAAS,MAAM,SAAS,CAAC,GAAG;EACrC,IAAI,MAAM,SAAS,KAAA,GACjB;EAGF,MAAM,WAAW,MAAM,MAAM;EAC7B,IAAI,aAAa,KAAA,KAAa,aAAa,MAAM,MAC/C,MAAM,IAAI,MACR,gBAAgB,KAAK,OAAO,MAAM,GAAG,+BAA+B,SAAS,SAAS,MAAM,KAAK,EACnG;EAEF,MAAM,MAAM,MAAM,MAAM;CAC1B;CAGF,OAAO;AACT;;;;;;;;AASA,SAAS,mBACP,MACA,OACA,SACM;CACN,KAAK,MAAM,CAAC,IAAI,SAAS,OAAO,QAAQ,KAAK,GAC3C,IAAI,CAAC,QAAQ,IAAI,IAAI,GACnB,MAAM,IAAI,MACR,gBAAgB,KAAK,IAAI,GAAG,gBAAgB,KAAK,wCACnD;AAGN;AAEA,SAAS,gBAAgB,SAA+C;CACtE,MAAM,gBAAgB,QAAQ,IAAI,eAAe;CAEjD,IAAI,CAAC,eACH,MAAM,IAAI,MAAM,oCAAoC;CAItD,OAAO,cADc,IAAI,YAAY,CAAC,CAAC,OAAO,aACd,CAAC;AACnC;AAEA,SAAS,cACP,UACA,SACwD;CACxD,MAAM,yBAAS,IAAI,IAAsB;CACzC,IAAI;CAEJ,KAAK,MAAM,CAAC,IAAI,SAAS,OAAO,QAAQ,SAAS,MAAM,MAAM,GAAG;EAC9D,MAAM,aAAa,QAAQ,IAAI,IAAI;EAEnC,IAAI,CAAC,YACH,MAAM,IAAI,MACR,uBAAuB,GAAG,8CAA8C,KAAK,EAC/E;EAIF,MAAM,QAAQ,SADI,IAAI,YAAY,CAAC,CAAC,OAAO,UACZ,CAAC;EAEhC,IAAI,MAAM,OAAO,IACf,MAAM,IAAI,MACR,0BAA0B,KAAK,YAAY,MAAM,GAAG,qCAAqC,GAAG,EAC9F;EAGF,OAAO,IAAI,IAAI,KAAK;EAEpB,IAAI,SAAS,SAAS,MACpB,YAAY;CAEhB;CAEA,IAAI,CAAC,WAGH,MAAM,IAAI,MACR,4BAA4B,SAAS,KAAK,4BAC5C;CAGF,OAAO;EAAE;EAAQ;CAAU;AAC7B;;;;;;;;;;;;;;;;;;;ACxQA,eAAsB,UACpB,OACsB;CACtB,MAAM,SACJ,iBAAiB,cAAc,QAAQ,MAAM,MAAM,YAAY;CAEjE,IAAI,MAAM,MAAM,GACd,OAAO;EAAE,QAAQ;EAAO,SAAS,MAAM,WAAW,MAAM;CAAE;CAG5D,OAAO;EAAE,QAAQ;EAAO,OAAO,SAAS,IAAI,YAAY,CAAC,CAAC,OAAO,MAAM,CAAC;CAAE;AAC5E"}
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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shayc/open-board-format",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
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>",