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 +8 -8
- package/dist/index.d.mts +47 -19
- package/dist/index.mjs +112 -59
- 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
|
@@ -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
|
-
|
|
331
|
+
resources: Map<string, Uint8Array>;
|
|
312
332
|
}
|
|
313
333
|
/**
|
|
314
|
-
* Load OBZ
|
|
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
|
|
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(
|
|
340
|
+
declare function extractOBZ(archive: ArrayBuffer): Promise<ParsedOBZ>;
|
|
325
341
|
/**
|
|
326
|
-
* Parse manifest
|
|
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
|
-
*
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
183
|
-
let
|
|
197
|
+
const sanitized = stripBom(json);
|
|
198
|
+
let rawBoard;
|
|
184
199
|
try {
|
|
185
|
-
|
|
200
|
+
rawBoard = JSON.parse(sanitized);
|
|
186
201
|
} catch (error) {
|
|
187
|
-
throw new Error(
|
|
202
|
+
throw new Error(buildParseErrorMessage(error));
|
|
188
203
|
}
|
|
189
|
-
return validateOBF(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
221
|
-
for (const [path, content] of
|
|
222
|
-
zip$1(
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
}
|
|
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
|
-
|
|
308
|
+
boards: extractBoards(manifest, entries),
|
|
309
|
+
resources: entries
|
|
271
310
|
};
|
|
272
311
|
}
|
|
273
312
|
/**
|
|
274
|
-
* Parse manifest
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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:
|
|
336
|
+
boards: boardPaths,
|
|
298
337
|
images: {},
|
|
299
338
|
sounds: {}
|
|
300
339
|
}
|
|
301
340
|
});
|
|
302
|
-
const
|
|
303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|