open-board-format 1.0.2 → 1.0.4

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/README.md CHANGED
@@ -46,14 +46,14 @@ const fromFile = await loadOBF(file);
46
46
  import { loadOBZ, extractOBZ } from "open-board-format";
47
47
 
48
48
  // From a File (e.g. drag-and-drop)
49
- const { manifest, boards, files } = await loadOBZ(file);
49
+ const { manifest, boards, resources } = await loadOBZ(file);
50
50
 
51
51
  // Or from an ArrayBuffer (e.g. fetch response)
52
52
  const parsed = await extractOBZ(buffer);
53
53
 
54
- // Access boards and raw files
54
+ // Access boards and resources
55
55
  const homeBoard = parsed.boards.get("1");
56
- const imageBytes = parsed.files.get("images/logo.png");
56
+ const imageBytes = parsed.resources.get("images/logo.png");
57
57
  ```
58
58
 
59
59
  ### Create an OBZ package
@@ -106,7 +106,7 @@ if (result.success) {
106
106
  | Function | Description |
107
107
  | --------------------------------------- | ------------------------------------------------------------- |
108
108
  | `loadOBZ(file)` | Load an OBZ package from a browser `File` |
109
- | `extractOBZ(buffer)` | Extract boards, manifest, and files from an `ArrayBuffer` |
109
+ | `extractOBZ(archive)` | Extract boards, manifest, and resources from an `ArrayBuffer` |
110
110
  | `createOBZ(boards, rootId, resources?)` | Create an OBZ package as a `Blob` |
111
111
  | `parseManifest(json)` | Parse a `manifest.json` string into a validated `OBFManifest` |
112
112
 
@@ -114,9 +114,9 @@ if (result.success) {
114
114
 
115
115
  | Function | Description |
116
116
  | --------------- | -------------------------------------------------------- |
117
- | `isZip(buffer)` | Check if an `ArrayBuffer` starts with a ZIP magic number |
118
- | `zip(files)` | Create a ZIP from a map of paths to buffers |
119
- | `unzip(buffer)` | Extract a ZIP into a map of paths to `Uint8Array` |
117
+ | `isZip(archive)` | Check if an `ArrayBuffer` starts with a ZIP magic number |
118
+ | `zip(entries)` | Create a ZIP from a map of paths to buffers |
119
+ | `unzip(archive)` | Extract a ZIP into a map of paths to `Uint8Array` |
120
120
 
121
121
  ### Types
122
122
 
@@ -134,7 +134,7 @@ if (result.success) {
134
134
  | `OBFSound` | A sound resource (extends `OBFMedia`) |
135
135
  | `OBFSymbolInfo` | Symbol set reference |
136
136
  | `OBFManifest` | OBZ package manifest |
137
- | `ParsedOBZ` | Return type of `extractOBZ` / `loadOBZ` — `{ manifest, boards, files }` |
137
+ | `ParsedOBZ` | Return type of `extractOBZ` / `loadOBZ` — `{ manifest, boards, resources }` |
138
138
  | `OBFID` | Unique identifier (string, coerced from number) |
139
139
  | `OBFFormatVersion` | Format version string (e.g., `open-board-0.1`) |
140
140
  | `OBFLicense` | Licensing information |
package/dist/index.d.mts CHANGED
@@ -12,7 +12,7 @@ import { z } from "zod";
12
12
  * @author Shay Cojocaru
13
13
  * @license MIT
14
14
  */
15
- /** Unique identifier as a string. Must be a non-empty string or number (coerced to string). */
15
+ /** Unique board-element identifier, coerced to a non-empty string. */
16
16
  declare const OBFIDSchema: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
17
17
  type OBFID = z.infer<typeof OBFIDSchema>;
18
18
  /**
@@ -26,35 +26,35 @@ type OBFFormatVersion = z.infer<typeof OBFFormatVersionSchema>;
26
26
  declare const OBFLocaleCodeSchema: z.ZodString;
27
27
  type OBFLocaleCode = z.infer<typeof OBFLocaleCodeSchema>;
28
28
  /**
29
- * Mapping of string keys to localized string values.
29
+ * Key–value pairs mapping symbolic names to their translations in a single locale.
30
30
  */
31
31
  declare const OBFLocalizedStringsSchema: z.ZodRecord<z.ZodString, z.ZodString>;
32
32
  type OBFLocalizedStrings = z.infer<typeof OBFLocalizedStringsSchema>;
33
33
  /**
34
- * String translations for multiple locales.
34
+ * Locale-keyed dictionary of translated strings,
35
+ * e.g., `{ en: { greeting: "Hello" }, fr: { greeting: "Bonjour" } }`.
35
36
  */
36
37
  declare const OBFStringsSchema: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>;
37
38
  type OBFStrings = z.infer<typeof OBFStringsSchema>;
38
39
  /**
39
- * Represents custom actions for spelling.
40
- * Prefixed with '+' followed by the text to append.
40
+ * Spelling action: a `+` prefix followed by the text to append,
41
+ * e.g., `"+hello"`.
41
42
  */
42
43
  declare const OBFSpellingActionSchema: z.ZodString;
43
44
  type OBFSpellingAction = z.infer<typeof OBFSpellingActionSchema>;
44
45
  /**
45
- * Represents specialty actions.
46
- * Standard actions are prefixed with ':'.
47
- * Custom actions start with ':ext_'.
46
+ * Specialty action prefixed with `:`, e.g., `":clear"`.
47
+ * Custom extensions use the `:ext_` prefix.
48
48
  */
49
49
  declare const OBFSpecialtyActionSchema: z.ZodString;
50
50
  type OBFSpecialtyAction = z.infer<typeof OBFSpecialtyActionSchema>;
51
51
  /**
52
- * Possible actions associated with a button.
52
+ * Union of spelling and specialty actions that a button can trigger.
53
53
  */
54
54
  declare const OBFButtonActionSchema: z.ZodUnion<readonly [z.ZodString, z.ZodString]>;
55
55
  type OBFButtonAction = z.infer<typeof OBFButtonActionSchema>;
56
56
  /**
57
- * Licensing information for a resource.
57
+ * License terms and attribution for a resource.
58
58
  */
59
59
  declare const OBFLicenseSchema: z.ZodObject<{
60
60
  type: z.ZodString;
@@ -91,7 +91,7 @@ declare const OBFMediaSchema: z.ZodObject<{
91
91
  }, z.core.$strip>;
92
92
  type OBFMedia = z.infer<typeof OBFMediaSchema>;
93
93
  /**
94
- * Information about a symbol from a proprietary symbol set.
94
+ * Reference to a symbol in a proprietary symbol set (e.g., SymbolStix).
95
95
  */
96
96
  declare const OBFSymbolInfoSchema: z.ZodObject<{
97
97
  set: z.ZodString;
@@ -99,13 +99,14 @@ declare const OBFSymbolInfoSchema: z.ZodObject<{
99
99
  }, z.core.$strip>;
100
100
  type OBFSymbolInfo = z.infer<typeof OBFSymbolInfoSchema>;
101
101
  /**
102
- * Represents an image resource.
102
+ * Image resource, extending {@link OBFMediaSchema} with optional
103
+ * symbol and dimension properties.
103
104
  *
104
- * When resolving the image, if multiple references are provided, they should be used in the following order:
105
- * 1. data
106
- * 2. path
107
- * 3. url
108
- * 4. symbol
105
+ * When resolving the image, consumers should prefer sources in this order:
106
+ * 1. `data`
107
+ * 2. `path`
108
+ * 3. `url`
109
+ * 4. `symbol`
109
110
  */
110
111
  declare const OBFImageSchema: z.ZodIntersection<z.ZodObject<{
111
112
  id: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
@@ -132,7 +133,7 @@ declare const OBFImageSchema: z.ZodIntersection<z.ZodObject<{
132
133
  }, z.core.$strip>>;
133
134
  type OBFImage = z.infer<typeof OBFImageSchema>;
134
135
  /**
135
- * Represents a sound resource.
136
+ * Audio resource. Identical to {@link OBFMediaSchema} — no additional properties.
136
137
  */
137
138
  declare const OBFSoundSchema: z.ZodObject<{
138
139
  id: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
@@ -152,7 +153,7 @@ declare const OBFSoundSchema: z.ZodObject<{
152
153
  }, z.core.$strip>;
153
154
  type OBFSound = z.infer<typeof OBFSoundSchema>;
154
155
  /**
155
- * Information needed to load another board.
156
+ * Reference to another board, resolved by ID, path, or URL.
156
157
  */
157
158
  declare const OBFLoadBoardSchema: z.ZodObject<{
158
159
  id: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string | undefined, string | number>>>;
@@ -163,7 +164,7 @@ declare const OBFLoadBoardSchema: z.ZodObject<{
163
164
  }, z.core.$strip>;
164
165
  type OBFLoadBoard = z.infer<typeof OBFLoadBoardSchema>;
165
166
  /**
166
- * Represents a button on the board.
167
+ * Interactive element on a board, optionally linked to images, sounds, and actions.
167
168
  */
168
169
  declare const OBFButtonSchema: z.ZodObject<{
169
170
  id: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
@@ -189,7 +190,7 @@ declare const OBFButtonSchema: z.ZodObject<{
189
190
  }, z.core.$strip>;
190
191
  type OBFButton = z.infer<typeof OBFButtonSchema>;
191
192
  /**
192
- * Grid layout information for the board.
193
+ * Row-and-column layout that arranges buttons by their IDs.
193
194
  */
194
195
  declare const OBFGridSchema: z.ZodObject<{
195
196
  rows: z.ZodNumber;
@@ -198,7 +199,7 @@ declare const OBFGridSchema: z.ZodObject<{
198
199
  }, z.core.$strip>;
199
200
  type OBFGrid = z.infer<typeof OBFGridSchema>;
200
201
  /**
201
- * Represents the root object of an OBF file, defining the structure and layout of a board.
202
+ * Root object of an `.obf` file: the complete definition of a single communication board.
202
203
  */
203
204
  declare const OBFBoardSchema: z.ZodObject<{
204
205
  format: z.ZodString;
@@ -285,7 +286,7 @@ declare const OBFBoardSchema: z.ZodObject<{
285
286
  }, z.core.$strip>;
286
287
  type OBFBoard = z.infer<typeof OBFBoardSchema>;
287
288
  /**
288
- * Manifest file in an .obz package.
289
+ * Table of contents for an `.obz` package, mapping resource IDs to their archive paths.
289
290
  */
290
291
  declare const OBFManifestSchema: z.ZodObject<{
291
292
  format: z.ZodString;
@@ -299,47 +300,137 @@ declare const OBFManifestSchema: z.ZodObject<{
299
300
  type OBFManifest = z.infer<typeof OBFManifestSchema>;
300
301
  //#endregion
301
302
  //#region src/obf.d.ts
303
+ /**
304
+ * Parse a JSON string into a validated OBF board.
305
+ *
306
+ * Strips an optional UTF-8 BOM prefix before parsing and throws a
307
+ * descriptive error if the input is malformed or fails schema validation.
308
+ *
309
+ * @param json - The JSON string to parse.
310
+ * @returns The validated board object.
311
+ *
312
+ * @throws {Error} If the JSON is malformed or does not conform to the OBF schema.
313
+ */
302
314
  declare function parseOBF(json: string): OBFBoard;
315
+ /**
316
+ * Read a `File` and parse its contents as a validated OBF board.
317
+ *
318
+ * This relies on the browser `File` API; for Node environments,
319
+ * read the file to a string and pass it to {@link parseOBF} instead.
320
+ *
321
+ * @param file - A `File` handle pointing to an `.obf` file.
322
+ * @returns The validated board object.
323
+ *
324
+ * @throws {Error} If the file content is malformed or fails schema validation.
325
+ */
303
326
  declare function loadOBF(file: File): Promise<OBFBoard>;
327
+ /**
328
+ * Validate an unknown value against the OBF board schema.
329
+ *
330
+ * @param data - The value to validate.
331
+ * @returns The validated board object.
332
+ *
333
+ * @throws {Error} If the value does not conform to the OBF schema.
334
+ */
304
335
  declare function validateOBF(data: unknown): OBFBoard;
336
+ /**
337
+ * Stringify an OBF board to a pretty-printed JSON string.
338
+ *
339
+ * @param board - The board to stringify.
340
+ * @returns A JSON string with two-space indentation.
341
+ */
305
342
  declare function stringifyOBF(board: OBFBoard): string;
306
343
  //#endregion
307
344
  //#region src/obz.d.ts
345
+ /**
346
+ * Fully extracted contents of an `.obz` archive.
347
+ *
348
+ * @property manifest - The package table of contents.
349
+ * @property boards - Board ID → validated board object.
350
+ * @property resources - Archive path → raw binary content (images, sounds, etc.).
351
+ */
308
352
  interface ParsedOBZ {
309
353
  manifest: OBFManifest;
310
354
  boards: Map<string, OBFBoard>;
311
- files: Map<string, Uint8Array>;
355
+ resources: Map<string, Uint8Array>;
312
356
  }
313
357
  /**
314
- * Load OBZ package from File
315
- * @param file - File object
316
- * @returns Parsed OBZ with manifest, boards, and files
358
+ * Read a `File` and extract its contents as a parsed OBZ package.
359
+ *
360
+ * This relies on the browser `File` API; for Node environments,
361
+ * read the file to an `ArrayBuffer` and pass it to {@link extractOBZ} instead.
362
+ *
363
+ * @param file - A `File` handle pointing to an `.obz` archive.
364
+ * @returns The parsed manifest, boards, and binary resources.
365
+ *
366
+ * @throws {Error} If the file is not a valid ZIP or the manifest is missing.
317
367
  */
318
368
  declare function loadOBZ(file: File): Promise<ParsedOBZ>;
319
369
  /**
320
- * Extract OBZ package from ArrayBuffer
321
- * @param buffer - OBZ file as ArrayBuffer
322
- * @returns Parsed OBZ with manifest, boards, and files
370
+ * Decompress an OBZ archive and return its manifest, boards, and resources.
371
+ *
372
+ * @param archive - The OBZ archive as an `ArrayBuffer`.
373
+ * @returns The parsed manifest, a map of board IDs to validated boards,
374
+ * and a map of file paths to their binary content.
375
+ *
376
+ * @throws {Error} If the archive is not a valid ZIP or the manifest is missing.
323
377
  */
324
- declare function extractOBZ(buffer: ArrayBuffer): Promise<ParsedOBZ>;
378
+ declare function extractOBZ(archive: ArrayBuffer): Promise<ParsedOBZ>;
325
379
  /**
326
- * Parse manifest.json from OBZ package
327
- * @param json - Manifest JSON string
328
- * @returns Validated Manifest object
380
+ * Parse and validate an OBZ manifest — the table of contents that maps
381
+ * board IDs to their file paths within the archive.
382
+ *
383
+ * @param json - A JSON string representing the manifest.
384
+ * @returns The validated manifest object.
385
+ *
386
+ * @throws {Error} If the JSON is malformed or fails schema validation.
329
387
  */
330
388
  declare function parseManifest(json: string): OBFManifest;
331
389
  /**
332
- * Create OBZ package from boards and resources
333
- * @param boards - Array of Board objects
334
- * @param rootBoardId - ID of the root board
335
- * @param resources - Optional map of resource paths to buffers
336
- * @returns OBZ package as Blob
390
+ * Bundle boards and optional resources into a compressed OBZ archive.
391
+ *
392
+ * A manifest is generated automatically from the supplied boards,
393
+ * using the `rootBoardId` to designate the entry-point board.
394
+ *
395
+ * @param boards - The boards to include in the archive.
396
+ * @param rootBoardId - The ID of the board that serves as the archive's entry point.
397
+ * @param resources - Optional map of file paths to binary content (images, sounds, etc.).
398
+ * @returns A `Blob` containing the compressed OBZ archive.
399
+ *
400
+ * @throws {Error} If `rootBoardId` does not match any of the supplied boards.
337
401
  */
338
402
  declare function createOBZ(boards: OBFBoard[], rootBoardId: string, resources?: Map<string, Uint8Array | ArrayBuffer>): Promise<Blob>;
339
403
  //#endregion
340
404
  //#region src/zip.d.ts
341
- declare function unzip(buffer: ArrayBuffer): Promise<Map<string, Uint8Array>>;
342
- declare function zip(files: Map<string, Uint8Array | ArrayBuffer>): Promise<Uint8Array>;
343
- declare function isZip(buffer: ArrayBuffer): boolean;
405
+ /**
406
+ * Decompress a ZIP archive into a map of file paths to raw bytes.
407
+ *
408
+ * @param archive - The ZIP archive as an `ArrayBuffer`.
409
+ * @returns A map of file paths to their decompressed content.
410
+ *
411
+ * @throws {Error} If decompression fails.
412
+ */
413
+ declare function unzip(archive: ArrayBuffer): Promise<Map<string, Uint8Array>>;
414
+ /**
415
+ * Compress a map of file paths and contents into a single ZIP archive.
416
+ *
417
+ * Accepts both `Uint8Array` and `ArrayBuffer` values so callers can
418
+ * pass the output of {@link unzip} directly or supply raw `ArrayBuffer`s
419
+ * without converting first.
420
+ *
421
+ * @param entries - A map of file paths to their content bytes.
422
+ * @returns The compressed archive as a `Uint8Array`.
423
+ *
424
+ * @throws {Error} If compression fails.
425
+ */
426
+ declare function zip(entries: Map<string, Uint8Array | ArrayBuffer>): Promise<Uint8Array>;
427
+ /**
428
+ * Test whether an `ArrayBuffer` begins with the two-byte ZIP magic
429
+ * prefix (`PK`).
430
+ *
431
+ * @param archive - The buffer to inspect.
432
+ * @returns `true` if the buffer starts with the ZIP signature.
433
+ */
434
+ declare function isZip(archive: ArrayBuffer): boolean;
344
435
  //#endregion
345
436
  export { type OBFBoard, OBFBoardSchema, type OBFButton, type OBFButtonAction, OBFButtonActionSchema, OBFButtonSchema, type OBFFormatVersion, OBFFormatVersionSchema, type OBFGrid, OBFGridSchema, type OBFID, OBFIDSchema, type OBFImage, OBFImageSchema, type OBFLicense, OBFLicenseSchema, type OBFLoadBoard, OBFLoadBoardSchema, type OBFLocaleCode, OBFLocaleCodeSchema, type OBFLocalizedStrings, OBFLocalizedStringsSchema, type OBFManifest, OBFManifestSchema, type OBFMedia, OBFMediaSchema, type OBFSound, OBFSoundSchema, type OBFSpecialtyAction, OBFSpecialtyActionSchema, type OBFSpellingAction, OBFSpellingActionSchema, type OBFStrings, OBFStringsSchema, type OBFSymbolInfo, OBFSymbolInfoSchema, type ParsedOBZ, createOBZ, extractOBZ, isZip, loadOBF, loadOBZ, parseManifest, parseOBF, stringifyOBF, unzip, validateOBF, zip };
package/dist/index.mjs CHANGED
@@ -22,7 +22,7 @@ const OBFOptionalIDSchema = z.union([z.string(), z.number()]).transform((val) =>
22
22
  const str = String(val);
23
23
  return str === "" ? void 0 : str;
24
24
  }).optional();
25
- /** Unique identifier as a string. Must be a non-empty string or number (coerced to string). */
25
+ /** Unique board-element identifier, coerced to a non-empty string. */
26
26
  const OBFIDSchema = z.union([z.string(), z.number()]).transform((val) => String(val)).pipe(z.string().min(1));
27
27
  /**
28
28
  * Format version of the Open Board Format, e.g., 'open-board-0.1'.
@@ -33,30 +33,30 @@ const OBFFormatVersionSchema = z.string().regex(/^open-board-.+$/);
33
33
  */
34
34
  const OBFLocaleCodeSchema = z.string();
35
35
  /**
36
- * Mapping of string keys to localized string values.
36
+ * Key–value pairs mapping symbolic names to their translations in a single locale.
37
37
  */
38
38
  const OBFLocalizedStringsSchema = z.record(z.string(), z.string());
39
39
  /**
40
- * String translations for multiple locales.
40
+ * Locale-keyed dictionary of translated strings,
41
+ * e.g., `{ en: { greeting: "Hello" }, fr: { greeting: "Bonjour" } }`.
41
42
  */
42
43
  const OBFStringsSchema = z.record(z.string(), OBFLocalizedStringsSchema);
43
44
  /**
44
- * Represents custom actions for spelling.
45
- * Prefixed with '+' followed by the text to append.
45
+ * Spelling action: a `+` prefix followed by the text to append,
46
+ * e.g., `"+hello"`.
46
47
  */
47
48
  const OBFSpellingActionSchema = z.string().regex(/^\+.+$/);
48
49
  /**
49
- * Represents specialty actions.
50
- * Standard actions are prefixed with ':'.
51
- * Custom actions start with ':ext_'.
50
+ * Specialty action prefixed with `:`, e.g., `":clear"`.
51
+ * Custom extensions use the `:ext_` prefix.
52
52
  */
53
53
  const OBFSpecialtyActionSchema = z.string().regex(/^:[a-z][a-z0-9_-]*$/i);
54
54
  /**
55
- * Possible actions associated with a button.
55
+ * Union of spelling and specialty actions that a button can trigger.
56
56
  */
57
57
  const OBFButtonActionSchema = z.union([OBFSpellingActionSchema, OBFSpecialtyActionSchema]);
58
58
  /**
59
- * Licensing information for a resource.
59
+ * License terms and attribution for a resource.
60
60
  */
61
61
  const OBFLicenseSchema = z.object({
62
62
  type: z.string(),
@@ -84,20 +84,21 @@ const OBFMediaSchema = z.object({
84
84
  license: OBFLicenseSchema.optional()
85
85
  });
86
86
  /**
87
- * Information about a symbol from a proprietary symbol set.
87
+ * Reference to a symbol in a proprietary symbol set (e.g., SymbolStix).
88
88
  */
89
89
  const OBFSymbolInfoSchema = z.object({
90
90
  set: z.string(),
91
91
  filename: z.string()
92
92
  });
93
93
  /**
94
- * Represents an image resource.
94
+ * Image resource, extending {@link OBFMediaSchema} with optional
95
+ * symbol and dimension properties.
95
96
  *
96
- * When resolving the image, if multiple references are provided, they should be used in the following order:
97
- * 1. data
98
- * 2. path
99
- * 3. url
100
- * 4. symbol
97
+ * When resolving the image, consumers should prefer sources in this order:
98
+ * 1. `data`
99
+ * 2. `path`
100
+ * 3. `url`
101
+ * 4. `symbol`
101
102
  */
102
103
  const OBFImageSchema = OBFMediaSchema.and(z.object({
103
104
  symbol: OBFSymbolInfoSchema.optional(),
@@ -105,11 +106,11 @@ const OBFImageSchema = OBFMediaSchema.and(z.object({
105
106
  height: z.number().optional()
106
107
  }));
107
108
  /**
108
- * Represents a sound resource.
109
+ * Audio resource. Identical to {@link OBFMediaSchema} — no additional properties.
109
110
  */
110
111
  const OBFSoundSchema = OBFMediaSchema;
111
112
  /**
112
- * Information needed to load another board.
113
+ * Reference to another board, resolved by ID, path, or URL.
113
114
  */
114
115
  const OBFLoadBoardSchema = z.object({
115
116
  id: OBFOptionalIDSchema,
@@ -119,7 +120,7 @@ const OBFLoadBoardSchema = z.object({
119
120
  path: z.string().optional()
120
121
  });
121
122
  /**
122
- * Represents a button on the board.
123
+ * Interactive element on a board, optionally linked to images, sounds, and actions.
123
124
  */
124
125
  const OBFButtonSchema = z.object({
125
126
  id: OBFIDSchema,
@@ -138,7 +139,7 @@ const OBFButtonSchema = z.object({
138
139
  height: z.number().min(0).max(1).optional()
139
140
  });
140
141
  /**
141
- * Grid layout information for the board.
142
+ * Row-and-column layout that arranges buttons by their IDs.
142
143
  */
143
144
  const OBFGridSchema = z.object({
144
145
  rows: z.number().int().min(1),
@@ -146,7 +147,7 @@ const OBFGridSchema = z.object({
146
147
  order: z.array(z.array(z.union([OBFIDSchema, z.null()])))
147
148
  }).refine((g) => g.order.length === g.rows, { message: "Grid order length must match rows" }).refine((g) => g.order.every((row) => row.length === g.columns), { message: "Each grid row must have length equal to columns" });
148
149
  /**
149
- * Represents the root object of an OBF file, defining the structure and layout of a board.
150
+ * Root object of an `.obf` file: the complete definition of a single communication board.
150
151
  */
151
152
  const OBFBoardSchema = z.object({
152
153
  format: OBFFormatVersionSchema,
@@ -163,7 +164,7 @@ const OBFBoardSchema = z.object({
163
164
  strings: OBFStringsSchema.optional()
164
165
  });
165
166
  /**
166
- * Manifest file in an .obz package.
167
+ * Table of contents for an `.obz` package, mapping resource IDs to their archive paths.
167
168
  */
168
169
  const OBFManifestSchema = z.object({
169
170
  format: OBFFormatVersionSchema,
@@ -178,48 +179,121 @@ const OBFManifestSchema = z.object({
178
179
  //#endregion
179
180
  //#region src/obf.ts
180
181
  const UTF8_BOM = "";
182
+ /** Strip a leading UTF-8 BOM, which some editors silently prepend. */
183
+ function stripBom(text) {
184
+ return text.startsWith(UTF8_BOM) ? text.slice(1) : text;
185
+ }
186
+ /** Build a descriptive parse-failure message, preserving the engine's reason when available. */
187
+ function buildParseErrorMessage(error) {
188
+ const reason = error instanceof Error ? error.message : "";
189
+ return reason ? `Invalid OBF: JSON parse failed — ${reason}` : "Invalid OBF: JSON parse failed";
190
+ }
191
+ /**
192
+ * Parse a JSON string into a validated OBF board.
193
+ *
194
+ * Strips an optional UTF-8 BOM prefix before parsing and throws a
195
+ * descriptive error if the input is malformed or fails schema validation.
196
+ *
197
+ * @param json - The JSON string to parse.
198
+ * @returns The validated board object.
199
+ *
200
+ * @throws {Error} If the JSON is malformed or does not conform to the OBF schema.
201
+ */
181
202
  function parseOBF(json) {
182
- const trimmed = json.startsWith(UTF8_BOM) ? json.slice(1) : json;
183
- let data;
203
+ const sanitized = stripBom(json);
204
+ let rawBoard;
184
205
  try {
185
- data = JSON.parse(trimmed);
206
+ rawBoard = JSON.parse(sanitized);
186
207
  } catch (error) {
187
- throw new Error(`Invalid OBF: JSON parse failed${error?.message ? ` — ${error.message}` : ""}`);
208
+ throw new Error(buildParseErrorMessage(error));
188
209
  }
189
- return validateOBF(data);
210
+ return validateOBF(rawBoard);
190
211
  }
212
+ /**
213
+ * Read a `File` and parse its contents as a validated OBF board.
214
+ *
215
+ * This relies on the browser `File` API; for Node environments,
216
+ * read the file to a string and pass it to {@link parseOBF} instead.
217
+ *
218
+ * @param file - A `File` handle pointing to an `.obf` file.
219
+ * @returns The validated board object.
220
+ *
221
+ * @throws {Error} If the file content is malformed or fails schema validation.
222
+ */
191
223
  async function loadOBF(file) {
192
224
  return parseOBF(await file.text());
193
225
  }
226
+ /**
227
+ * Validate an unknown value against the OBF board schema.
228
+ *
229
+ * @param data - The value to validate.
230
+ * @returns The validated board object.
231
+ *
232
+ * @throws {Error} If the value does not conform to the OBF schema.
233
+ */
194
234
  function validateOBF(data) {
195
235
  const result = OBFBoardSchema.safeParse(data);
196
236
  if (!result.success) throw new Error(`Invalid OBF: ${result.error.message}`);
197
237
  return result.data;
198
238
  }
239
+ /**
240
+ * Stringify an OBF board to a pretty-printed JSON string.
241
+ *
242
+ * @param board - The board to stringify.
243
+ * @returns A JSON string with two-space indentation.
244
+ */
199
245
  function stringifyOBF(board) {
200
246
  return JSON.stringify(board, null, 2);
201
247
  }
202
248
 
203
249
  //#endregion
204
250
  //#region src/zip.ts
205
- function unzip(buffer) {
251
+ /**
252
+ * First two bytes of every ZIP archive — the ASCII letters `PK`,
253
+ * after Phil Katz, creator of the format.
254
+ *
255
+ * Only the 2-byte prefix is checked intentionally: this keeps the
256
+ * test lightweight and sufficient for distinguishing ZIP from JSON.
257
+ */
258
+ const ZIP_MAGIC = [80, 75];
259
+ /** Balanced speed-vs-size deflate level used by fflate (1–9 scale). */
260
+ const COMPRESSION_LEVEL = 6;
261
+ /**
262
+ * Decompress a ZIP archive into a map of file paths to raw bytes.
263
+ *
264
+ * @param archive - The ZIP archive as an `ArrayBuffer`.
265
+ * @returns A map of file paths to their decompressed content.
266
+ *
267
+ * @throws {Error} If decompression fails.
268
+ */
269
+ function unzip(archive) {
206
270
  return new Promise((resolve, reject) => {
207
- unzip$1(new Uint8Array(buffer), (error, result) => {
271
+ unzip$1(new Uint8Array(archive), (error, entries) => {
208
272
  if (error) {
209
273
  reject(/* @__PURE__ */ new Error(`Failed to unzip: ${error.message ?? String(error)}`));
210
274
  return;
211
275
  }
212
- const files = /* @__PURE__ */ new Map();
213
- for (const [path, bytes] of Object.entries(result)) files.set(path, bytes);
214
- resolve(files);
276
+ resolve(new Map(Object.entries(entries)));
215
277
  });
216
278
  });
217
279
  }
218
- function zip(files) {
280
+ /**
281
+ * Compress a map of file paths and contents into a single ZIP archive.
282
+ *
283
+ * Accepts both `Uint8Array` and `ArrayBuffer` values so callers can
284
+ * pass the output of {@link unzip} directly or supply raw `ArrayBuffer`s
285
+ * without converting first.
286
+ *
287
+ * @param entries - A map of file paths to their content bytes.
288
+ * @returns The compressed archive as a `Uint8Array`.
289
+ *
290
+ * @throws {Error} If compression fails.
291
+ */
292
+ function zip(entries) {
219
293
  return new Promise((resolve, reject) => {
220
- const input = {};
221
- for (const [path, content] of files) input[path] = content instanceof Uint8Array ? content : new Uint8Array(content);
222
- zip$1(input, { level: 6 }, (error, result) => {
294
+ const pathToBytes = {};
295
+ for (const [path, content] of entries) pathToBytes[path] = content instanceof Uint8Array ? content : new Uint8Array(content);
296
+ zip$1(pathToBytes, { level: COMPRESSION_LEVEL }, (error, result) => {
223
297
  if (error) {
224
298
  reject(/* @__PURE__ */ new Error(`Failed to zip: ${error.message ?? String(error)}`));
225
299
  return;
@@ -228,87 +302,122 @@ function zip(files) {
228
302
  });
229
303
  });
230
304
  }
231
- function isZip(buffer) {
232
- const view = new Uint8Array(buffer);
233
- return view.length >= 2 && view[0] === 80 && view[1] === 75;
305
+ /**
306
+ * Test whether an `ArrayBuffer` begins with the two-byte ZIP magic
307
+ * prefix (`PK`).
308
+ *
309
+ * @param archive - The buffer to inspect.
310
+ * @returns `true` if the buffer starts with the ZIP signature.
311
+ */
312
+ function isZip(archive) {
313
+ const bytes = new Uint8Array(archive);
314
+ return bytes.length >= ZIP_MAGIC.length && ZIP_MAGIC.every((byte, index) => bytes[index] === byte);
234
315
  }
235
316
 
236
317
  //#endregion
237
318
  //#region src/obz.ts
238
319
  /**
239
- * Load OBZ package from File
240
- * @param file - File object
241
- * @returns Parsed OBZ with manifest, boards, and files
320
+ * Read a `File` and extract its contents as a parsed OBZ package.
321
+ *
322
+ * This relies on the browser `File` API; for Node environments,
323
+ * read the file to an `ArrayBuffer` and pass it to {@link extractOBZ} instead.
324
+ *
325
+ * @param file - A `File` handle pointing to an `.obz` archive.
326
+ * @returns The parsed manifest, boards, and binary resources.
327
+ *
328
+ * @throws {Error} If the file is not a valid ZIP or the manifest is missing.
242
329
  */
243
330
  async function loadOBZ(file) {
244
331
  return extractOBZ(await file.arrayBuffer());
245
332
  }
246
333
  /**
247
- * Extract OBZ package from ArrayBuffer
248
- * @param buffer - OBZ file as ArrayBuffer
249
- * @returns Parsed OBZ with manifest, boards, and files
334
+ * Decompress an OBZ archive and return its manifest, boards, and resources.
335
+ *
336
+ * @param archive - The OBZ archive as an `ArrayBuffer`.
337
+ * @returns The parsed manifest, a map of board IDs to validated boards,
338
+ * and a map of file paths to their binary content.
339
+ *
340
+ * @throws {Error} If the archive is not a valid ZIP or the manifest is missing.
250
341
  */
251
- async function extractOBZ(buffer) {
252
- if (!isZip(buffer)) throw new Error("Invalid OBZ: not a ZIP file");
253
- const files = await unzip(buffer);
254
- const manifestBuffer = files.get("manifest.json");
255
- if (!manifestBuffer) throw new Error("Invalid OBZ: missing manifest.json");
256
- const manifest = parseManifest(new TextDecoder().decode(manifestBuffer));
257
- const boards = /* @__PURE__ */ new Map();
258
- for (const [id, path] of Object.entries(manifest.paths.boards)) {
259
- const boardBuffer = files.get(path);
260
- if (!boardBuffer) {
261
- console.warn(`Board ${id} not found at ${path}`);
262
- continue;
263
- }
264
- const board = parseOBF(new TextDecoder().decode(boardBuffer));
265
- boards.set(id, board);
266
- }
342
+ async function extractOBZ(archive) {
343
+ if (!isZip(archive)) throw new Error("Invalid OBZ: not a ZIP file");
344
+ const entries = await unzip(archive);
345
+ const manifest = extractManifest(entries);
267
346
  return {
268
347
  manifest,
269
- boards,
270
- files
348
+ boards: extractBoards(manifest, entries),
349
+ resources: entries
271
350
  };
272
351
  }
273
352
  /**
274
- * Parse manifest.json from OBZ package
275
- * @param json - Manifest JSON string
276
- * @returns Validated Manifest object
353
+ * Parse and validate an OBZ manifest — the table of contents that maps
354
+ * board IDs to their file paths within the archive.
355
+ *
356
+ * @param json - A JSON string representing the manifest.
357
+ * @returns The validated manifest object.
358
+ *
359
+ * @throws {Error} If the JSON is malformed or fails schema validation.
277
360
  */
278
361
  function parseManifest(json) {
279
- const data = JSON.parse(json);
362
+ let data;
363
+ try {
364
+ data = JSON.parse(json);
365
+ } catch (error) {
366
+ throw new Error(`Invalid manifest: JSON parse failed${error?.message ? ` — ${error.message}` : ""}`);
367
+ }
280
368
  const result = OBFManifestSchema.safeParse(data);
281
369
  if (!result.success) throw new Error(`Invalid manifest: ${result.error.message}`);
282
370
  return result.data;
283
371
  }
284
372
  /**
285
- * Create OBZ package from boards and resources
286
- * @param boards - Array of Board objects
287
- * @param rootBoardId - ID of the root board
288
- * @param resources - Optional map of resource paths to buffers
289
- * @returns OBZ package as Blob
373
+ * Bundle boards and optional resources into a compressed OBZ archive.
374
+ *
375
+ * A manifest is generated automatically from the supplied boards,
376
+ * using the `rootBoardId` to designate the entry-point board.
377
+ *
378
+ * @param boards - The boards to include in the archive.
379
+ * @param rootBoardId - The ID of the board that serves as the archive's entry point.
380
+ * @param resources - Optional map of file paths to binary content (images, sounds, etc.).
381
+ * @returns A `Blob` containing the compressed OBZ archive.
382
+ *
383
+ * @throws {Error} If `rootBoardId` does not match any of the supplied boards.
290
384
  */
291
385
  async function createOBZ(boards, rootBoardId, resources) {
292
- const files = /* @__PURE__ */ new Map();
386
+ const entries = /* @__PURE__ */ new Map();
387
+ const boardPaths = Object.fromEntries(boards.map((board) => [board.id, `boards/${board.id}.obf`]));
293
388
  const manifest = OBFManifestSchema.parse({
294
389
  format: "open-board-0.1",
295
390
  root: `boards/${rootBoardId}.obf`,
296
391
  paths: {
297
- boards: Object.fromEntries(boards.map((board) => [board.id, `boards/${board.id}.obf`])),
392
+ boards: boardPaths,
298
393
  images: {},
299
394
  sounds: {}
300
395
  }
301
396
  });
302
- const manifestJSON = JSON.stringify(manifest, null, 2);
303
- files.set("manifest.json", new TextEncoder().encode(manifestJSON).buffer);
397
+ const encoder = new TextEncoder();
398
+ entries.set("manifest.json", encoder.encode(JSON.stringify(manifest, null, 2)));
304
399
  for (const board of boards) {
305
- const boardJSON = JSON.stringify(board, null, 2);
306
400
  const path = `boards/${board.id}.obf`;
307
- files.set(path, new TextEncoder().encode(boardJSON).buffer);
401
+ entries.set(path, encoder.encode(JSON.stringify(board, null, 2)));
402
+ }
403
+ if (resources) for (const [path, bytes] of resources) entries.set(path, bytes);
404
+ const compressed = await zip(entries);
405
+ return new Blob([new Uint8Array(compressed)], { type: "application/zip" });
406
+ }
407
+ function extractManifest(entries) {
408
+ const manifestBytes = entries.get("manifest.json");
409
+ if (!manifestBytes) throw new Error("Invalid OBZ: missing manifest.json");
410
+ return parseManifest(new TextDecoder().decode(manifestBytes));
411
+ }
412
+ function extractBoards(manifest, entries) {
413
+ const boards = /* @__PURE__ */ new Map();
414
+ for (const [id, path] of Object.entries(manifest.paths.boards)) {
415
+ const boardBytes = entries.get(path);
416
+ if (!boardBytes) throw new Error(`Board "${id}" declared in manifest but missing at path "${path}"`);
417
+ const boardJson = new TextDecoder().decode(boardBytes);
418
+ boards.set(id, parseOBF(boardJson));
308
419
  }
309
- if (resources) for (const [path, buffer] of resources.entries()) files.set(path, buffer);
310
- const zipBuffer = await zip(files);
311
- return new Blob([new Uint8Array(zipBuffer)], { type: "application/zip" });
420
+ return boards;
312
421
  }
313
422
 
314
423
  //#endregion
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "open-board-format",
3
3
  "type": "module",
4
- "version": "1.0.2",
4
+ "version": "1.0.4",
5
5
  "description": "Parse, validate, and create Open Board Format (OBF/OBZ) files for AAC applications.",
6
6
  "author": "Shay Cojocaru <shayc@outlook.com>",
7
7
  "license": "MIT",