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 +8 -8
- package/dist/index.d.mts +133 -42
- package/dist/index.mjs +191 -82
- package/package.json +1 -1
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,
|
|
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
|
|
54
|
+
// Access boards and resources
|
|
55
55
|
const homeBoard = parsed.boards.get("1");
|
|
56
|
-
const imageBytes = parsed.
|
|
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(
|
|
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(
|
|
118
|
-
| `zip(
|
|
119
|
-
| `unzip(
|
|
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,
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
40
|
-
*
|
|
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
|
-
*
|
|
46
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
102
|
+
* Image resource, extending {@link OBFMediaSchema} with optional
|
|
103
|
+
* symbol and dimension properties.
|
|
103
104
|
*
|
|
104
|
-
* When resolving the image,
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
355
|
+
resources: Map<string, Uint8Array>;
|
|
312
356
|
}
|
|
313
357
|
/**
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
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
|
-
*
|
|
321
|
-
*
|
|
322
|
-
* @
|
|
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(
|
|
378
|
+
declare function extractOBZ(archive: ArrayBuffer): Promise<ParsedOBZ>;
|
|
325
379
|
/**
|
|
326
|
-
* Parse
|
|
327
|
-
*
|
|
328
|
-
*
|
|
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
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
45
|
-
*
|
|
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
|
-
*
|
|
50
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
94
|
+
* Image resource, extending {@link OBFMediaSchema} with optional
|
|
95
|
+
* symbol and dimension properties.
|
|
95
96
|
*
|
|
96
|
-
* When resolving the image,
|
|
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
|
-
*
|
|
109
|
+
* Audio resource. Identical to {@link OBFMediaSchema} — no additional properties.
|
|
109
110
|
*/
|
|
110
111
|
const OBFSoundSchema = OBFMediaSchema;
|
|
111
112
|
/**
|
|
112
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
183
|
-
let
|
|
203
|
+
const sanitized = stripBom(json);
|
|
204
|
+
let rawBoard;
|
|
184
205
|
try {
|
|
185
|
-
|
|
206
|
+
rawBoard = JSON.parse(sanitized);
|
|
186
207
|
} catch (error) {
|
|
187
|
-
throw new Error(
|
|
208
|
+
throw new Error(buildParseErrorMessage(error));
|
|
188
209
|
}
|
|
189
|
-
return validateOBF(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
221
|
-
for (const [path, content] of
|
|
222
|
-
zip$1(
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
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
|
-
*
|
|
248
|
-
*
|
|
249
|
-
* @
|
|
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(
|
|
252
|
-
if (!isZip(
|
|
253
|
-
const
|
|
254
|
-
const
|
|
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
|
-
|
|
348
|
+
boards: extractBoards(manifest, entries),
|
|
349
|
+
resources: entries
|
|
271
350
|
};
|
|
272
351
|
}
|
|
273
352
|
/**
|
|
274
|
-
* Parse
|
|
275
|
-
*
|
|
276
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
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
|
|
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:
|
|
392
|
+
boards: boardPaths,
|
|
298
393
|
images: {},
|
|
299
394
|
sounds: {}
|
|
300
395
|
}
|
|
301
396
|
});
|
|
302
|
-
const
|
|
303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|