open-board-format 1.0.2 → 1.0.3

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
@@ -299,47 +299,75 @@ declare const OBFManifestSchema: z.ZodObject<{
299
299
  type OBFManifest = z.infer<typeof OBFManifestSchema>;
300
300
  //#endregion
301
301
  //#region src/obf.d.ts
302
+ /**
303
+ * Parse a JSON string into a validated OBF board.
304
+ *
305
+ * Handles an optional UTF-8 BOM prefix and throws a descriptive
306
+ * error if the input is malformed or fails schema validation.
307
+ */
302
308
  declare function parseOBF(json: string): OBFBoard;
309
+ /**
310
+ * Read a `File` handle and parse its contents as an OBF board.
311
+ *
312
+ * This relies on the browser `File` API; for Node environments,
313
+ * read the file to a string and pass it to {@link parseOBF} instead.
314
+ */
303
315
  declare function loadOBF(file: File): Promise<OBFBoard>;
316
+ /**
317
+ * Validate an unknown value against the OBF board schema.
318
+ *
319
+ * @throws {Error} If the value does not conform to the schema.
320
+ */
304
321
  declare function validateOBF(data: unknown): OBFBoard;
322
+ /**
323
+ * Serialize an OBF board to a pretty-printed JSON string.
324
+ */
305
325
  declare function stringifyOBF(board: OBFBoard): string;
306
326
  //#endregion
307
327
  //#region src/obz.d.ts
308
328
  interface ParsedOBZ {
309
329
  manifest: OBFManifest;
310
330
  boards: Map<string, OBFBoard>;
311
- files: Map<string, Uint8Array>;
331
+ resources: Map<string, Uint8Array>;
312
332
  }
313
333
  /**
314
- * Load OBZ package from File
315
- * @param file - File object
316
- * @returns Parsed OBZ with manifest, boards, and files
334
+ * Load an OBZ archive from a user-selected file.
317
335
  */
318
336
  declare function loadOBZ(file: File): Promise<ParsedOBZ>;
319
337
  /**
320
- * Extract OBZ package from ArrayBuffer
321
- * @param buffer - OBZ file as ArrayBuffer
322
- * @returns Parsed OBZ with manifest, boards, and files
338
+ * Extract boards and resources from a raw OBZ archive.
323
339
  */
324
- declare function extractOBZ(buffer: ArrayBuffer): Promise<ParsedOBZ>;
340
+ declare function extractOBZ(archive: ArrayBuffer): Promise<ParsedOBZ>;
325
341
  /**
326
- * Parse manifest.json from OBZ package
327
- * @param json - Manifest JSON string
328
- * @returns Validated Manifest object
342
+ * Parse and validate a manifest JSON string.
329
343
  */
330
344
  declare function parseManifest(json: string): OBFManifest;
331
345
  /**
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
346
+ * Bundle boards and optional resources into a downloadable OBZ archive.
337
347
  */
338
348
  declare function createOBZ(boards: OBFBoard[], rootBoardId: string, resources?: Map<string, Uint8Array | ArrayBuffer>): Promise<Blob>;
339
349
  //#endregion
340
350
  //#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;
351
+ /**
352
+ * Decompress a ZIP archive into a map of file paths to raw bytes.
353
+ *
354
+ * @param archive - The raw ZIP bytes to decompress.
355
+ */
356
+ declare function unzip(archive: ArrayBuffer): Promise<Map<string, Uint8Array>>;
357
+ /**
358
+ * Compress a map of file paths and contents into a single ZIP archive.
359
+ *
360
+ * Accepts both `Uint8Array` and `ArrayBuffer` values so callers can
361
+ * pass the output of `unzip` directly or supply raw `ArrayBuffer`s
362
+ * without converting first.
363
+ *
364
+ * @param entries - A map of file paths to their content bytes.
365
+ */
366
+ declare function zip(entries: Map<string, Uint8Array | ArrayBuffer>): Promise<Uint8Array>;
367
+ /**
368
+ * Test whether an `ArrayBuffer` begins with the 2-byte ZIP
369
+ * magic prefix (`PK`).
370
+ */
371
+ declare function isZip(archive: ArrayBuffer): boolean;
344
372
  //#endregion
345
373
  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
@@ -178,48 +178,99 @@ const OBFManifestSchema = z.object({
178
178
  //#endregion
179
179
  //#region src/obf.ts
180
180
  const UTF8_BOM = "";
181
+ /** Strip a leading UTF-8 BOM, which some editors silently prepend. */
182
+ function stripBom(text) {
183
+ return text.startsWith(UTF8_BOM) ? text.slice(1) : text;
184
+ }
185
+ /** Build a descriptive parse-failure message, preserving the engine's reason when available. */
186
+ function buildParseErrorMessage(error) {
187
+ const reason = error instanceof Error ? error.message : "";
188
+ return reason ? `Invalid OBF: JSON parse failed — ${reason}` : "Invalid OBF: JSON parse failed";
189
+ }
190
+ /**
191
+ * Parse a JSON string into a validated OBF board.
192
+ *
193
+ * Handles an optional UTF-8 BOM prefix and throws a descriptive
194
+ * error if the input is malformed or fails schema validation.
195
+ */
181
196
  function parseOBF(json) {
182
- const trimmed = json.startsWith(UTF8_BOM) ? json.slice(1) : json;
183
- let data;
197
+ const sanitized = stripBom(json);
198
+ let rawBoard;
184
199
  try {
185
- data = JSON.parse(trimmed);
200
+ rawBoard = JSON.parse(sanitized);
186
201
  } catch (error) {
187
- throw new Error(`Invalid OBF: JSON parse failed${error?.message ? ` — ${error.message}` : ""}`);
202
+ throw new Error(buildParseErrorMessage(error));
188
203
  }
189
- return validateOBF(data);
204
+ return validateOBF(rawBoard);
190
205
  }
206
+ /**
207
+ * Read a `File` handle and parse its contents as an OBF board.
208
+ *
209
+ * This relies on the browser `File` API; for Node environments,
210
+ * read the file to a string and pass it to {@link parseOBF} instead.
211
+ */
191
212
  async function loadOBF(file) {
192
213
  return parseOBF(await file.text());
193
214
  }
215
+ /**
216
+ * Validate an unknown value against the OBF board schema.
217
+ *
218
+ * @throws {Error} If the value does not conform to the schema.
219
+ */
194
220
  function validateOBF(data) {
195
221
  const result = OBFBoardSchema.safeParse(data);
196
222
  if (!result.success) throw new Error(`Invalid OBF: ${result.error.message}`);
197
223
  return result.data;
198
224
  }
225
+ /**
226
+ * Serialize an OBF board to a pretty-printed JSON string.
227
+ */
199
228
  function stringifyOBF(board) {
200
229
  return JSON.stringify(board, null, 2);
201
230
  }
202
231
 
203
232
  //#endregion
204
233
  //#region src/zip.ts
205
- function unzip(buffer) {
234
+ /**
235
+ * First two bytes of every ZIP archive — the ASCII letters `PK`,
236
+ * after Phil Katz, creator of the format.
237
+ *
238
+ * Only the 2-byte prefix is checked intentionally: this keeps the
239
+ * test lightweight and sufficient for distinguishing ZIP from JSON.
240
+ */
241
+ const ZIP_MAGIC = [80, 75];
242
+ /** Balanced speed-vs-size deflate level used by fflate (1–9 scale). */
243
+ const COMPRESSION_LEVEL = 6;
244
+ /**
245
+ * Decompress a ZIP archive into a map of file paths to raw bytes.
246
+ *
247
+ * @param archive - The raw ZIP bytes to decompress.
248
+ */
249
+ function unzip(archive) {
206
250
  return new Promise((resolve, reject) => {
207
- unzip$1(new Uint8Array(buffer), (error, result) => {
251
+ unzip$1(new Uint8Array(archive), (error, entries) => {
208
252
  if (error) {
209
253
  reject(/* @__PURE__ */ new Error(`Failed to unzip: ${error.message ?? String(error)}`));
210
254
  return;
211
255
  }
212
- const files = /* @__PURE__ */ new Map();
213
- for (const [path, bytes] of Object.entries(result)) files.set(path, bytes);
214
- resolve(files);
256
+ resolve(new Map(Object.entries(entries)));
215
257
  });
216
258
  });
217
259
  }
218
- function zip(files) {
260
+ /**
261
+ * Compress a map of file paths and contents into a single ZIP archive.
262
+ *
263
+ * Accepts both `Uint8Array` and `ArrayBuffer` values so callers can
264
+ * pass the output of `unzip` directly or supply raw `ArrayBuffer`s
265
+ * without converting first.
266
+ *
267
+ * @param entries - A map of file paths to their content bytes.
268
+ */
269
+ function zip(entries) {
219
270
  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) => {
271
+ const pathToBytes = {};
272
+ for (const [path, content] of entries) pathToBytes[path] = content instanceof Uint8Array ? content : new Uint8Array(content);
273
+ zip$1(pathToBytes, { level: COMPRESSION_LEVEL }, (error, result) => {
223
274
  if (error) {
224
275
  reject(/* @__PURE__ */ new Error(`Failed to zip: ${error.message ?? String(error)}`));
225
276
  return;
@@ -228,87 +279,89 @@ function zip(files) {
228
279
  });
229
280
  });
230
281
  }
231
- function isZip(buffer) {
232
- const view = new Uint8Array(buffer);
233
- return view.length >= 2 && view[0] === 80 && view[1] === 75;
282
+ /**
283
+ * Test whether an `ArrayBuffer` begins with the 2-byte ZIP
284
+ * magic prefix (`PK`).
285
+ */
286
+ function isZip(archive) {
287
+ const bytes = new Uint8Array(archive);
288
+ return bytes.length >= ZIP_MAGIC.length && ZIP_MAGIC.every((byte, index) => bytes[index] === byte);
234
289
  }
235
290
 
236
291
  //#endregion
237
292
  //#region src/obz.ts
238
293
  /**
239
- * Load OBZ package from File
240
- * @param file - File object
241
- * @returns Parsed OBZ with manifest, boards, and files
294
+ * Load an OBZ archive from a user-selected file.
242
295
  */
243
296
  async function loadOBZ(file) {
244
297
  return extractOBZ(await file.arrayBuffer());
245
298
  }
246
299
  /**
247
- * Extract OBZ package from ArrayBuffer
248
- * @param buffer - OBZ file as ArrayBuffer
249
- * @returns Parsed OBZ with manifest, boards, and files
300
+ * Extract boards and resources from a raw OBZ archive.
250
301
  */
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
- }
302
+ async function extractOBZ(archive) {
303
+ if (!isZip(archive)) throw new Error("Invalid OBZ: not a ZIP file");
304
+ const entries = await unzip(archive);
305
+ const manifest = extractManifest(entries);
267
306
  return {
268
307
  manifest,
269
- boards,
270
- files
308
+ boards: extractBoards(manifest, entries),
309
+ resources: entries
271
310
  };
272
311
  }
273
312
  /**
274
- * Parse manifest.json from OBZ package
275
- * @param json - Manifest JSON string
276
- * @returns Validated Manifest object
313
+ * Parse and validate a manifest JSON string.
277
314
  */
278
315
  function parseManifest(json) {
279
- const data = JSON.parse(json);
316
+ let data;
317
+ try {
318
+ data = JSON.parse(json);
319
+ } catch (error) {
320
+ throw new Error(`Invalid manifest: JSON parse failed${error?.message ? ` — ${error.message}` : ""}`);
321
+ }
280
322
  const result = OBFManifestSchema.safeParse(data);
281
323
  if (!result.success) throw new Error(`Invalid manifest: ${result.error.message}`);
282
324
  return result.data;
283
325
  }
284
326
  /**
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
327
+ * Bundle boards and optional resources into a downloadable OBZ archive.
290
328
  */
291
329
  async function createOBZ(boards, rootBoardId, resources) {
292
- const files = /* @__PURE__ */ new Map();
330
+ const entries = /* @__PURE__ */ new Map();
331
+ const boardPaths = Object.fromEntries(boards.map((board) => [board.id, `boards/${board.id}.obf`]));
293
332
  const manifest = OBFManifestSchema.parse({
294
333
  format: "open-board-0.1",
295
334
  root: `boards/${rootBoardId}.obf`,
296
335
  paths: {
297
- boards: Object.fromEntries(boards.map((board) => [board.id, `boards/${board.id}.obf`])),
336
+ boards: boardPaths,
298
337
  images: {},
299
338
  sounds: {}
300
339
  }
301
340
  });
302
- const manifestJSON = JSON.stringify(manifest, null, 2);
303
- files.set("manifest.json", new TextEncoder().encode(manifestJSON).buffer);
341
+ const encoder = new TextEncoder();
342
+ entries.set("manifest.json", encoder.encode(JSON.stringify(manifest, null, 2)));
304
343
  for (const board of boards) {
305
- const boardJSON = JSON.stringify(board, null, 2);
306
344
  const path = `boards/${board.id}.obf`;
307
- files.set(path, new TextEncoder().encode(boardJSON).buffer);
345
+ entries.set(path, encoder.encode(JSON.stringify(board, null, 2)));
346
+ }
347
+ if (resources) for (const [path, bytes] of resources) entries.set(path, bytes);
348
+ const compressed = await zip(entries);
349
+ return new Blob([new Uint8Array(compressed)], { type: "application/zip" });
350
+ }
351
+ function extractManifest(entries) {
352
+ const manifestBytes = entries.get("manifest.json");
353
+ if (!manifestBytes) throw new Error("Invalid OBZ: missing manifest.json");
354
+ return parseManifest(new TextDecoder().decode(manifestBytes));
355
+ }
356
+ function extractBoards(manifest, entries) {
357
+ const boards = /* @__PURE__ */ new Map();
358
+ for (const [id, path] of Object.entries(manifest.paths.boards)) {
359
+ const boardBytes = entries.get(path);
360
+ if (!boardBytes) throw new Error(`Board "${id}" declared in manifest but missing at path "${path}"`);
361
+ const boardJson = new TextDecoder().decode(boardBytes);
362
+ boards.set(id, parseOBF(boardJson));
308
363
  }
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" });
364
+ return boards;
312
365
  }
313
366
 
314
367
  //#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.3",
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",