open-board-format 1.0.4 → 1.1.0

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/dist/index.d.mts CHANGED
@@ -63,7 +63,7 @@ declare const OBFLicenseSchema: z.ZodObject<{
63
63
  author_name: z.ZodOptional<z.ZodString>;
64
64
  author_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
65
65
  author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
66
- }, z.core.$strip>;
66
+ }, z.core.$loose>;
67
67
  type OBFLicense = z.infer<typeof OBFLicenseSchema>;
68
68
  /**
69
69
  * Common properties for media resources (images and sounds).
@@ -87,8 +87,8 @@ declare const OBFMediaSchema: z.ZodObject<{
87
87
  author_name: z.ZodOptional<z.ZodString>;
88
88
  author_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
89
89
  author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
90
- }, z.core.$strip>>;
91
- }, z.core.$strip>;
90
+ }, z.core.$loose>>;
91
+ }, z.core.$loose>;
92
92
  type OBFMedia = z.infer<typeof OBFMediaSchema>;
93
93
  /**
94
94
  * Reference to a symbol in a proprietary symbol set (e.g., SymbolStix).
@@ -96,7 +96,7 @@ type OBFMedia = z.infer<typeof OBFMediaSchema>;
96
96
  declare const OBFSymbolInfoSchema: z.ZodObject<{
97
97
  set: z.ZodString;
98
98
  filename: z.ZodString;
99
- }, z.core.$strip>;
99
+ }, z.core.$loose>;
100
100
  type OBFSymbolInfo = z.infer<typeof OBFSymbolInfoSchema>;
101
101
  /**
102
102
  * Image resource, extending {@link OBFMediaSchema} with optional
@@ -122,12 +122,12 @@ declare const OBFImageSchema: z.ZodIntersection<z.ZodObject<{
122
122
  author_name: z.ZodOptional<z.ZodString>;
123
123
  author_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
124
124
  author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
125
- }, z.core.$strip>>;
126
- }, z.core.$strip>, z.ZodObject<{
125
+ }, z.core.$loose>>;
126
+ }, z.core.$loose>, z.ZodObject<{
127
127
  symbol: z.ZodOptional<z.ZodObject<{
128
128
  set: z.ZodString;
129
129
  filename: z.ZodString;
130
- }, z.core.$strip>>;
130
+ }, z.core.$loose>>;
131
131
  width: z.ZodOptional<z.ZodNumber>;
132
132
  height: z.ZodOptional<z.ZodNumber>;
133
133
  }, z.core.$strip>>;
@@ -149,8 +149,8 @@ declare const OBFSoundSchema: z.ZodObject<{
149
149
  author_name: z.ZodOptional<z.ZodString>;
150
150
  author_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
151
151
  author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
152
- }, z.core.$strip>>;
153
- }, z.core.$strip>;
152
+ }, z.core.$loose>>;
153
+ }, z.core.$loose>;
154
154
  type OBFSound = z.infer<typeof OBFSoundSchema>;
155
155
  /**
156
156
  * Reference to another board, resolved by ID, path, or URL.
@@ -161,7 +161,7 @@ declare const OBFLoadBoardSchema: z.ZodObject<{
161
161
  data_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
162
162
  url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
163
163
  path: z.ZodOptional<z.ZodString>;
164
- }, z.core.$strip>;
164
+ }, z.core.$loose>;
165
165
  type OBFLoadBoard = z.infer<typeof OBFLoadBoardSchema>;
166
166
  /**
167
167
  * Interactive element on a board, optionally linked to images, sounds, and actions.
@@ -180,14 +180,14 @@ declare const OBFButtonSchema: z.ZodObject<{
180
180
  data_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
181
181
  url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
182
182
  path: z.ZodOptional<z.ZodString>;
183
- }, z.core.$strip>>;
183
+ }, z.core.$loose>>;
184
184
  background_color: z.ZodOptional<z.ZodString>;
185
185
  border_color: z.ZodOptional<z.ZodString>;
186
186
  top: z.ZodOptional<z.ZodNumber>;
187
187
  left: z.ZodOptional<z.ZodNumber>;
188
188
  width: z.ZodOptional<z.ZodNumber>;
189
189
  height: z.ZodOptional<z.ZodNumber>;
190
- }, z.core.$strip>;
190
+ }, z.core.$loose>;
191
191
  type OBFButton = z.infer<typeof OBFButtonSchema>;
192
192
  /**
193
193
  * Row-and-column layout that arranges buttons by their IDs.
@@ -196,7 +196,7 @@ declare const OBFGridSchema: z.ZodObject<{
196
196
  rows: z.ZodNumber;
197
197
  columns: z.ZodNumber;
198
198
  order: z.ZodArray<z.ZodArray<z.ZodUnion<readonly [z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>, z.ZodNull]>>>;
199
- }, z.core.$strip>;
199
+ }, z.core.$loose>;
200
200
  type OBFGrid = z.infer<typeof OBFGridSchema>;
201
201
  /**
202
202
  * Root object of an `.obf` file: the complete definition of a single communication board.
@@ -219,14 +219,14 @@ declare const OBFBoardSchema: z.ZodObject<{
219
219
  data_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
220
220
  url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
221
221
  path: z.ZodOptional<z.ZodString>;
222
- }, z.core.$strip>>;
222
+ }, z.core.$loose>>;
223
223
  background_color: z.ZodOptional<z.ZodString>;
224
224
  border_color: z.ZodOptional<z.ZodString>;
225
225
  top: z.ZodOptional<z.ZodNumber>;
226
226
  left: z.ZodOptional<z.ZodNumber>;
227
227
  width: z.ZodOptional<z.ZodNumber>;
228
228
  height: z.ZodOptional<z.ZodNumber>;
229
- }, z.core.$strip>>;
229
+ }, z.core.$loose>>;
230
230
  url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
231
231
  name: z.ZodOptional<z.ZodString>;
232
232
  description_html: z.ZodOptional<z.ZodString>;
@@ -234,7 +234,7 @@ declare const OBFBoardSchema: z.ZodObject<{
234
234
  rows: z.ZodNumber;
235
235
  columns: z.ZodNumber;
236
236
  order: z.ZodArray<z.ZodArray<z.ZodUnion<readonly [z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>, z.ZodNull]>>>;
237
- }, z.core.$strip>;
237
+ }, z.core.$loose>;
238
238
  images: z.ZodOptional<z.ZodArray<z.ZodIntersection<z.ZodObject<{
239
239
  id: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
240
240
  data: z.ZodOptional<z.ZodString>;
@@ -249,12 +249,12 @@ declare const OBFBoardSchema: z.ZodObject<{
249
249
  author_name: z.ZodOptional<z.ZodString>;
250
250
  author_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
251
251
  author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
252
- }, z.core.$strip>>;
253
- }, z.core.$strip>, z.ZodObject<{
252
+ }, z.core.$loose>>;
253
+ }, z.core.$loose>, z.ZodObject<{
254
254
  symbol: z.ZodOptional<z.ZodObject<{
255
255
  set: z.ZodString;
256
256
  filename: z.ZodString;
257
- }, z.core.$strip>>;
257
+ }, z.core.$loose>>;
258
258
  width: z.ZodOptional<z.ZodNumber>;
259
259
  height: z.ZodOptional<z.ZodNumber>;
260
260
  }, z.core.$strip>>>>;
@@ -272,8 +272,8 @@ declare const OBFBoardSchema: z.ZodObject<{
272
272
  author_name: z.ZodOptional<z.ZodString>;
273
273
  author_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
274
274
  author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
275
- }, z.core.$strip>>;
276
- }, z.core.$strip>>>;
275
+ }, z.core.$loose>>;
276
+ }, z.core.$loose>>>;
277
277
  license: z.ZodOptional<z.ZodObject<{
278
278
  type: z.ZodString;
279
279
  copyright_notice_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
@@ -281,9 +281,9 @@ declare const OBFBoardSchema: z.ZodObject<{
281
281
  author_name: z.ZodOptional<z.ZodString>;
282
282
  author_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
283
283
  author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
284
- }, z.core.$strip>>;
284
+ }, z.core.$loose>>;
285
285
  strings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>>;
286
- }, z.core.$strip>;
286
+ }, z.core.$loose>;
287
287
  type OBFBoard = z.infer<typeof OBFBoardSchema>;
288
288
  /**
289
289
  * Table of contents for an `.obz` package, mapping resource IDs to their archive paths.
@@ -295,8 +295,8 @@ declare const OBFManifestSchema: z.ZodObject<{
295
295
  boards: z.ZodRecord<z.ZodString, z.ZodString>;
296
296
  images: z.ZodRecord<z.ZodString, z.ZodString>;
297
297
  sounds: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
298
- }, z.core.$strip>;
299
- }, z.core.$strip>;
298
+ }, z.core.$loose>;
299
+ }, z.core.$loose>;
300
300
  type OBFManifest = z.infer<typeof OBFManifestSchema>;
301
301
  //#endregion
302
302
  //#region src/obf.d.ts
package/dist/index.mjs CHANGED
@@ -1,6 +1,5 @@
1
1
  import { z } from "zod";
2
2
  import { unzip as unzip$1, zip as zip$1 } from "fflate";
3
-
4
3
  //#region src/schema.ts
5
4
  /**
6
5
  * Open Board Format (OBF) Zod Schemas
@@ -58,12 +57,18 @@ const OBFButtonActionSchema = z.union([OBFSpellingActionSchema, OBFSpecialtyActi
58
57
  /**
59
58
  * License terms and attribution for a resource.
60
59
  */
61
- const OBFLicenseSchema = z.object({
60
+ const OBFLicenseSchema = z.looseObject({
61
+ /** Type of the license, e.g., 'CC-BY-SA'. */
62
62
  type: z.string(),
63
+ /** URL to the license terms. */
63
64
  copyright_notice_url: OBFOptionalUrlSchema,
65
+ /** Source URL of the resource. */
64
66
  source_url: OBFOptionalUrlSchema,
67
+ /** Name of the author. */
65
68
  author_name: z.string().optional(),
69
+ /** URL of the author's webpage. */
66
70
  author_url: OBFOptionalUrlSchema,
71
+ /** Email address of the author. */
67
72
  author_email: OBFOptionalEmailSchema
68
73
  });
69
74
  /**
@@ -74,20 +79,29 @@ const OBFLicenseSchema = z.object({
74
79
  * 2. path
75
80
  * 3. url
76
81
  */
77
- const OBFMediaSchema = z.object({
82
+ const OBFMediaSchema = z.looseObject({
83
+ /** Unique identifier for the media resource. */
78
84
  id: OBFIDSchema,
85
+ /** Data URI containing the media data. */
79
86
  data: z.string().optional(),
87
+ /** Path to the media file within an .obz package. */
80
88
  path: z.string().optional(),
89
+ /** Data URL to fetch the media programmatically. */
81
90
  data_url: OBFOptionalUrlSchema,
91
+ /** URL to the media resource. */
82
92
  url: OBFOptionalUrlSchema,
93
+ /** MIME type of the media, e.g., 'image/png', 'audio/mpeg'. */
83
94
  content_type: z.string().optional(),
95
+ /** Licensing information for the media. */
84
96
  license: OBFLicenseSchema.optional()
85
97
  });
86
98
  /**
87
99
  * Reference to a symbol in a proprietary symbol set (e.g., SymbolStix).
88
100
  */
89
- const OBFSymbolInfoSchema = z.object({
101
+ const OBFSymbolInfoSchema = z.looseObject({
102
+ /** Name of the symbol set, e.g., 'symbolstix'. */
90
103
  set: z.string(),
104
+ /** Filename of the symbol within the set. */
91
105
  filename: z.string()
92
106
  });
93
107
  /**
@@ -101,8 +115,11 @@ const OBFSymbolInfoSchema = z.object({
101
115
  * 4. `symbol`
102
116
  */
103
117
  const OBFImageSchema = OBFMediaSchema.and(z.object({
118
+ /** Information about a symbol from a proprietary symbol set. */
104
119
  symbol: OBFSymbolInfoSchema.optional(),
120
+ /** Width of the image in pixels. */
105
121
  width: z.number().optional(),
122
+ /** Height of the image in pixels. */
106
123
  height: z.number().optional()
107
124
  }));
108
125
  /**
@@ -112,70 +129,112 @@ const OBFSoundSchema = OBFMediaSchema;
112
129
  /**
113
130
  * Reference to another board, resolved by ID, path, or URL.
114
131
  */
115
- const OBFLoadBoardSchema = z.object({
132
+ const OBFLoadBoardSchema = z.looseObject({
133
+ /** Unique identifier of the board to load. */
116
134
  id: OBFOptionalIDSchema,
135
+ /** Name of the board to load. */
117
136
  name: z.string().optional(),
137
+ /** Data URL to fetch the board programmatically. */
118
138
  data_url: OBFOptionalUrlSchema,
139
+ /** URL to access the board via a web browser. */
119
140
  url: OBFOptionalUrlSchema,
141
+ /** Path to the board within an .obz package. */
120
142
  path: z.string().optional()
121
143
  });
122
144
  /**
123
145
  * Interactive element on a board, optionally linked to images, sounds, and actions.
124
146
  */
125
- const OBFButtonSchema = z.object({
147
+ const OBFButtonSchema = z.looseObject({
148
+ /** Unique identifier for the button. */
126
149
  id: OBFIDSchema,
150
+ /** Label text displayed on the button. */
127
151
  label: z.string().optional(),
152
+ /** Alternative text for vocalization when the button is activated. */
128
153
  vocalization: z.string().optional(),
154
+ /** Identifier of the image associated with the button. */
129
155
  image_id: OBFOptionalIDSchema,
156
+ /** Identifier of the sound associated with the button. */
130
157
  sound_id: OBFOptionalIDSchema,
158
+ /** Action associated with the button. */
131
159
  action: OBFButtonActionSchema.optional(),
160
+ /** List of multiple actions for the button, executed in order. */
132
161
  actions: z.array(OBFButtonActionSchema).optional(),
162
+ /** Information to load another board when this button is activated. */
133
163
  load_board: OBFLoadBoardSchema.optional(),
164
+ /** Background color of the button in 'rgb' or 'rgba' format. */
134
165
  background_color: z.string().optional(),
166
+ /** Border color of the button in 'rgb' or 'rgba' format. */
135
167
  border_color: z.string().optional(),
168
+ /** Vertical position for absolute positioning (0.0 to 1.0). */
136
169
  top: z.number().min(0).max(1).optional(),
170
+ /** Horizontal position for absolute positioning (0.0 to 1.0). */
137
171
  left: z.number().min(0).max(1).optional(),
172
+ /** Width of the button for absolute positioning (0.0 to 1.0). */
138
173
  width: z.number().min(0).max(1).optional(),
174
+ /** Height of the button for absolute positioning (0.0 to 1.0). */
139
175
  height: z.number().min(0).max(1).optional()
140
176
  });
141
177
  /**
142
178
  * Row-and-column layout that arranges buttons by their IDs.
143
179
  */
144
- const OBFGridSchema = z.object({
180
+ const OBFGridSchema = z.looseObject({
181
+ /** Number of rows in the grid. */
145
182
  rows: z.number().int().min(1),
183
+ /** Number of columns in the grid. */
146
184
  columns: z.number().int().min(1),
185
+ /**
186
+ * 2D array representing the order of buttons by their IDs.
187
+ * Each sub-array corresponds to a row, and each element is a button ID or null for empty slots.
188
+ */
147
189
  order: z.array(z.array(z.union([OBFIDSchema, z.null()])))
148
190
  }).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" });
149
191
  /**
150
192
  * Root object of an `.obf` file: the complete definition of a single communication board.
151
193
  */
152
- const OBFBoardSchema = z.object({
194
+ const OBFBoardSchema = z.looseObject({
195
+ /** Format version of the Open Board Format, e.g., 'open-board-0.1'. */
153
196
  format: OBFFormatVersionSchema,
197
+ /** Unique identifier for the board. */
154
198
  id: OBFIDSchema,
199
+ /** Locale of the board as a BCP 47 language tag, e.g., 'en', 'en-US'. */
155
200
  locale: OBFLocaleCodeSchema.optional(),
201
+ /** List of buttons on the board. */
156
202
  buttons: z.array(OBFButtonSchema),
203
+ /** URL where the board can be accessed or downloaded. */
157
204
  url: OBFOptionalUrlSchema,
205
+ /** Name of the board. */
158
206
  name: z.string().optional(),
207
+ /** Description of the board in HTML format. */
159
208
  description_html: z.string().optional(),
209
+ /** Grid layout information for arranging buttons. */
160
210
  grid: OBFGridSchema,
211
+ /** List of images used in the board. */
161
212
  images: z.array(OBFImageSchema).optional(),
213
+ /** List of sounds used in the board. */
162
214
  sounds: z.array(OBFSoundSchema).optional(),
215
+ /** Licensing information for the board. */
163
216
  license: OBFLicenseSchema.optional(),
217
+ /** String translations for multiple locales. */
164
218
  strings: OBFStringsSchema.optional()
165
219
  });
166
220
  /**
167
221
  * Table of contents for an `.obz` package, mapping resource IDs to their archive paths.
168
222
  */
169
- const OBFManifestSchema = z.object({
223
+ const OBFManifestSchema = z.looseObject({
224
+ /** Format version of the Open Board Format, e.g., 'open-board-0.1'. */
170
225
  format: OBFFormatVersionSchema,
226
+ /** Path to the root board within the .obz package. */
171
227
  root: z.string(),
172
- paths: z.object({
228
+ /** Mapping of IDs to paths for boards, images, and sounds. */
229
+ paths: z.looseObject({
230
+ /** Mapping of board IDs to their file paths. */
173
231
  boards: z.record(z.string(), z.string()),
232
+ /** Mapping of image IDs to their file paths. */
174
233
  images: z.record(z.string(), z.string()),
234
+ /** Mapping of sound IDs to their file paths. */
175
235
  sounds: z.record(z.string(), z.string()).optional()
176
236
  })
177
237
  });
178
-
179
238
  //#endregion
180
239
  //#region src/obf.ts
181
240
  const UTF8_BOM = "";
@@ -245,7 +304,6 @@ function validateOBF(data) {
245
304
  function stringifyOBF(board) {
246
305
  return JSON.stringify(board, null, 2);
247
306
  }
248
-
249
307
  //#endregion
250
308
  //#region src/zip.ts
251
309
  /**
@@ -313,7 +371,6 @@ function isZip(archive) {
313
371
  const bytes = new Uint8Array(archive);
314
372
  return bytes.length >= ZIP_MAGIC.length && ZIP_MAGIC.every((byte, index) => bytes[index] === byte);
315
373
  }
316
-
317
374
  //#endregion
318
375
  //#region src/obz.ts
319
376
  /**
@@ -383,15 +440,18 @@ function parseManifest(json) {
383
440
  * @throws {Error} If `rootBoardId` does not match any of the supplied boards.
384
441
  */
385
442
  async function createOBZ(boards, rootBoardId, resources) {
443
+ if (!boards.some((board) => board.id === rootBoardId)) throw new Error(`Invalid OBZ: rootBoardId "${rootBoardId}" does not match any supplied board`);
386
444
  const entries = /* @__PURE__ */ new Map();
387
445
  const boardPaths = Object.fromEntries(boards.map((board) => [board.id, `boards/${board.id}.obf`]));
446
+ const imagePaths = collectMediaPaths(boards, "images");
447
+ const soundPaths = collectMediaPaths(boards, "sounds");
388
448
  const manifest = OBFManifestSchema.parse({
389
449
  format: "open-board-0.1",
390
450
  root: `boards/${rootBoardId}.obf`,
391
451
  paths: {
392
452
  boards: boardPaths,
393
- images: {},
394
- sounds: {}
453
+ images: imagePaths,
454
+ ...Object.keys(soundPaths).length > 0 ? { sounds: soundPaths } : {}
395
455
  }
396
456
  });
397
457
  const encoder = new TextEncoder();
@@ -404,6 +464,23 @@ async function createOBZ(boards, rootBoardId, resources) {
404
464
  const compressed = await zip(entries);
405
465
  return new Blob([new Uint8Array(compressed)], { type: "application/zip" });
406
466
  }
467
+ /**
468
+ * Walk every board's media collection and produce the `{ id -> path }` map
469
+ * the spec calls "redundant but still required" for the OBZ manifest.
470
+ *
471
+ * Throws when two boards declare the same media id with conflicting paths
472
+ * — a silent OBZ that points at a non-existent file is worse than a clear error.
473
+ */
474
+ function collectMediaPaths(boards, kind) {
475
+ const paths = {};
476
+ for (const board of boards) for (const media of board[kind] ?? []) {
477
+ if (media.path === void 0) continue;
478
+ const existing = paths[media.id];
479
+ if (existing !== void 0 && existing !== media.path) throw new Error(`Invalid OBZ: ${kind} id "${media.id}" maps to conflicting paths "${existing}" and "${media.path}"`);
480
+ paths[media.id] = media.path;
481
+ }
482
+ return paths;
483
+ }
407
484
  function extractManifest(entries) {
408
485
  const manifestBytes = entries.get("manifest.json");
409
486
  if (!manifestBytes) throw new Error("Invalid OBZ: missing manifest.json");
@@ -419,6 +496,5 @@ function extractBoards(manifest, entries) {
419
496
  }
420
497
  return boards;
421
498
  }
422
-
423
499
  //#endregion
424
- export { OBFBoardSchema, OBFButtonActionSchema, OBFButtonSchema, OBFFormatVersionSchema, OBFGridSchema, OBFIDSchema, OBFImageSchema, OBFLicenseSchema, OBFLoadBoardSchema, OBFLocaleCodeSchema, OBFLocalizedStringsSchema, OBFManifestSchema, OBFMediaSchema, OBFSoundSchema, OBFSpecialtyActionSchema, OBFSpellingActionSchema, OBFStringsSchema, OBFSymbolInfoSchema, createOBZ, extractOBZ, isZip, loadOBF, loadOBZ, parseManifest, parseOBF, stringifyOBF, unzip, validateOBF, zip };
500
+ export { OBFBoardSchema, OBFButtonActionSchema, OBFButtonSchema, OBFFormatVersionSchema, OBFGridSchema, OBFIDSchema, OBFImageSchema, OBFLicenseSchema, OBFLoadBoardSchema, OBFLocaleCodeSchema, OBFLocalizedStringsSchema, OBFManifestSchema, OBFMediaSchema, OBFSoundSchema, OBFSpecialtyActionSchema, OBFSpellingActionSchema, OBFStringsSchema, OBFSymbolInfoSchema, createOBZ, extractOBZ, isZip, loadOBF, loadOBZ, parseManifest, parseOBF, stringifyOBF, unzip, validateOBF, zip };
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",
4
+ "version": "1.1.0",
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",
@@ -35,14 +35,14 @@
35
35
  "prepublishOnly": "npm run build"
36
36
  },
37
37
  "devDependencies": {
38
- "@types/node": "^25.3.0",
39
- "bumpp": "^10.4.1",
40
- "tsdown": "^0.20.3",
41
- "typescript": "^5.9.3",
42
- "vitest": "^4.0.18"
38
+ "@types/node": "^25.7.0",
39
+ "bumpp": "^11.1.0",
40
+ "tsdown": "^0.22.0",
41
+ "typescript": "~6.0.3",
42
+ "vitest": "^4.1.6"
43
43
  },
44
44
  "dependencies": {
45
45
  "fflate": "^0.8.2",
46
- "zod": "^4.3.6"
46
+ "zod": "^4.4.3"
47
47
  }
48
48
  }