@shayc/open-board-format 0.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/LICENSE +21 -0
- package/README.md +160 -0
- package/dist/index.d.mts +436 -0
- package/dist/index.mjs +500 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shay Cojocaru
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# @shayc/open-board-format
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@shayc/open-board-format)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
A type-safe toolkit to parse, validate, and create Open Board Format (OBF/OBZ) files for Augmentative and Alternative Communication (AAC) applications.
|
|
7
|
+
|
|
8
|
+
[Open Board Format](https://www.openboardformat.org/) is an open standard for representing AAC communication boards. It defines two file types:
|
|
9
|
+
|
|
10
|
+
- **OBF** (`.obf`) — A JSON file describing a single communication board (buttons, images, sounds, grid layout, metadata).
|
|
11
|
+
- **OBZ** (`.obz`) — A ZIP archive containing one or more `.obf` boards along with their associated media and a `manifest.json`.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Parse & Validate:** Parse and validate OBF boards from JSON strings, objects, or `File` handles.
|
|
16
|
+
- **Create & Extract:** Serialize boards to JSON, and create or extract OBZ packages (ZIP archives with boards, images, and sounds).
|
|
17
|
+
- **Zod Schemas:** Every OBF type has a corresponding [Zod](https://zod.dev/) schema for runtime validation or API contracts, with full TypeScript types inferred directly.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @shayc/open-board-format
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
### Parse a single board (OBF)
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { parseOBF, validateOBF, loadOBF } from "@shayc/open-board-format";
|
|
31
|
+
|
|
32
|
+
// Parse from a JSON string
|
|
33
|
+
const board = parseOBF(jsonString);
|
|
34
|
+
console.log(board.name); // "My Board"
|
|
35
|
+
|
|
36
|
+
// Validate an unknown object (throws on failure)
|
|
37
|
+
const validated = validateOBF(untrustedData);
|
|
38
|
+
|
|
39
|
+
// Load from a browser File object
|
|
40
|
+
const fromFile = await loadOBF(file);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Extract an OBZ package
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { loadOBZ, extractOBZ } from "@shayc/open-board-format";
|
|
47
|
+
|
|
48
|
+
// From a File (e.g. drag-and-drop)
|
|
49
|
+
const { manifest, boards, resources } = await loadOBZ(file);
|
|
50
|
+
|
|
51
|
+
// Or from an ArrayBuffer (e.g. fetch response)
|
|
52
|
+
const parsed = await extractOBZ(buffer);
|
|
53
|
+
|
|
54
|
+
// Access boards and resources
|
|
55
|
+
const homeBoard = parsed.boards.get("1");
|
|
56
|
+
const imageBytes = parsed.resources.get("images/logo.png");
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Create an OBZ package
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { createOBZ } from "@shayc/open-board-format";
|
|
63
|
+
import type { OBFBoard } from "@shayc/open-board-format";
|
|
64
|
+
|
|
65
|
+
const boards: OBFBoard[] = [
|
|
66
|
+
{
|
|
67
|
+
format: "open-board-0.1",
|
|
68
|
+
id: "board-1",
|
|
69
|
+
buttons: [{ id: "btn-1", label: "Hello" }],
|
|
70
|
+
grid: { rows: 1, columns: 1, order: [["btn-1"]] },
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const pngBytes = new Uint8Array(/* ... */);
|
|
75
|
+
const resources = new Map([["images/logo.png", pngBytes]]);
|
|
76
|
+
const blob = await createOBZ(boards, "board-1", resources);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Use Zod schemas directly
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { OBFBoardSchema } from "@shayc/open-board-format";
|
|
83
|
+
|
|
84
|
+
const result = OBFBoardSchema.safeParse(data);
|
|
85
|
+
|
|
86
|
+
if (result.success) {
|
|
87
|
+
console.log(result.data.buttons);
|
|
88
|
+
} else {
|
|
89
|
+
console.error(result.error.issues);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## API
|
|
94
|
+
|
|
95
|
+
### OBF (single board)
|
|
96
|
+
|
|
97
|
+
| Function | Description |
|
|
98
|
+
| --------------------- | ------------------------------------------------------------ |
|
|
99
|
+
| `parseOBF(json)` | Parse a JSON string into a validated `OBFBoard` |
|
|
100
|
+
| `validateOBF(data)` | Validate an unknown object as `OBFBoard` (throws on failure) |
|
|
101
|
+
| `stringifyOBF(board)` | Serialize an `OBFBoard` to a JSON string |
|
|
102
|
+
| `loadOBF(file)` | Load an `OBFBoard` from a browser `File` |
|
|
103
|
+
|
|
104
|
+
### OBZ (board package)
|
|
105
|
+
|
|
106
|
+
| Function | Description |
|
|
107
|
+
| --------------------------------------- | ------------------------------------------------------------- |
|
|
108
|
+
| `loadOBZ(file)` | Load an OBZ package from a browser `File` |
|
|
109
|
+
| `extractOBZ(archive)` | Extract boards, manifest, and resources from an `ArrayBuffer` |
|
|
110
|
+
| `createOBZ(boards, rootId, resources?)` | Create an OBZ package as a `Blob` |
|
|
111
|
+
| `parseManifest(json)` | Parse a `manifest.json` string into a validated `OBFManifest` |
|
|
112
|
+
|
|
113
|
+
### Utilities
|
|
114
|
+
|
|
115
|
+
| Function | Description |
|
|
116
|
+
| --------------- | -------------------------------------------------------- |
|
|
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
|
+
|
|
121
|
+
### Types
|
|
122
|
+
|
|
123
|
+
| Type | Description |
|
|
124
|
+
| --------------------- | ----------------------------------------------------------------------- |
|
|
125
|
+
| `OBFBoard` | A single communication board |
|
|
126
|
+
| `OBFGrid` | Grid layout (rows, columns, order) |
|
|
127
|
+
| `OBFButton` | A button on the board |
|
|
128
|
+
| `OBFButtonAction` | Button action (spelling or specialty) |
|
|
129
|
+
| `OBFSpellingAction` | Spelling action (e.g., `+s`) |
|
|
130
|
+
| `OBFSpecialtyAction` | Specialty action (e.g., `:clear`) |
|
|
131
|
+
| `OBFLoadBoard` | Reference to load another board |
|
|
132
|
+
| `OBFMedia` | Common media properties (base for `OBFImage` and `OBFSound`) |
|
|
133
|
+
| `OBFImage` | An image resource (extends `OBFMedia`) |
|
|
134
|
+
| `OBFSound` | A sound resource (extends `OBFMedia`) |
|
|
135
|
+
| `OBFSymbolInfo` | Symbol set reference |
|
|
136
|
+
| `OBFManifest` | OBZ package manifest |
|
|
137
|
+
| `ParsedOBZ` | Return type of `extractOBZ` / `loadOBZ` — `{ manifest, boards, resources }` |
|
|
138
|
+
| `OBFID` | Unique identifier (string, coerced from number) |
|
|
139
|
+
| `OBFFormatVersion` | Format version string (e.g., `open-board-0.1`) |
|
|
140
|
+
| `OBFLicense` | Licensing information |
|
|
141
|
+
| `OBFLocaleCode` | BCP 47 locale code |
|
|
142
|
+
| `OBFLocalizedStrings` | Key-value string translations |
|
|
143
|
+
| `OBFStrings` | Multi-locale string translations |
|
|
144
|
+
|
|
145
|
+
## Development
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npm install # Install dependencies
|
|
149
|
+
npm test # Run tests (vitest)
|
|
150
|
+
npm run build # Build for production (tsdown)
|
|
151
|
+
npm run typecheck # Type-check without emitting
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Related
|
|
155
|
+
|
|
156
|
+
- [Open Board Format specification](https://www.openboardformat.org/docs) — Official standard and format documentation
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
[MIT](LICENSE) © Shay Cojocaru
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
//#region src/schema.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Open Board Format (OBF) Zod Schemas
|
|
6
|
+
*
|
|
7
|
+
* These schemas represent the Open Board Format, designed for sharing communication boards and board sets
|
|
8
|
+
* between Augmentative and Alternative Communication (AAC) applications.
|
|
9
|
+
*
|
|
10
|
+
* Official OBF specification: https://www.openboardformat.org/docs
|
|
11
|
+
*
|
|
12
|
+
* @author Shay Cojocaru
|
|
13
|
+
* @license MIT
|
|
14
|
+
*/
|
|
15
|
+
/** Unique board-element identifier, coerced to a non-empty string. */
|
|
16
|
+
declare const OBFIDSchema: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
|
|
17
|
+
type OBFID = z.infer<typeof OBFIDSchema>;
|
|
18
|
+
/**
|
|
19
|
+
* Format version of the Open Board Format, e.g., 'open-board-0.1'.
|
|
20
|
+
*/
|
|
21
|
+
declare const OBFFormatVersionSchema: z.ZodString;
|
|
22
|
+
type OBFFormatVersion = z.infer<typeof OBFFormatVersionSchema>;
|
|
23
|
+
/**
|
|
24
|
+
* Locale code as per BCP 47 language tags, e.g., 'en', 'en-US', 'fr-CA'.
|
|
25
|
+
*/
|
|
26
|
+
declare const OBFLocaleCodeSchema: z.ZodString;
|
|
27
|
+
type OBFLocaleCode = z.infer<typeof OBFLocaleCodeSchema>;
|
|
28
|
+
/**
|
|
29
|
+
* Key–value pairs mapping symbolic names to their translations in a single locale.
|
|
30
|
+
*/
|
|
31
|
+
declare const OBFLocalizedStringsSchema: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
32
|
+
type OBFLocalizedStrings = z.infer<typeof OBFLocalizedStringsSchema>;
|
|
33
|
+
/**
|
|
34
|
+
* Locale-keyed dictionary of translated strings,
|
|
35
|
+
* e.g., `{ en: { greeting: "Hello" }, fr: { greeting: "Bonjour" } }`.
|
|
36
|
+
*/
|
|
37
|
+
declare const OBFStringsSchema: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
38
|
+
type OBFStrings = z.infer<typeof OBFStringsSchema>;
|
|
39
|
+
/**
|
|
40
|
+
* Spelling action: a `+` prefix followed by the text to append,
|
|
41
|
+
* e.g., `"+hello"`.
|
|
42
|
+
*/
|
|
43
|
+
declare const OBFSpellingActionSchema: z.ZodString;
|
|
44
|
+
type OBFSpellingAction = z.infer<typeof OBFSpellingActionSchema>;
|
|
45
|
+
/**
|
|
46
|
+
* Specialty action prefixed with `:`, e.g., `":clear"`.
|
|
47
|
+
* Custom extensions use the `:ext_` prefix.
|
|
48
|
+
*/
|
|
49
|
+
declare const OBFSpecialtyActionSchema: z.ZodString;
|
|
50
|
+
type OBFSpecialtyAction = z.infer<typeof OBFSpecialtyActionSchema>;
|
|
51
|
+
/**
|
|
52
|
+
* Union of spelling and specialty actions that a button can trigger.
|
|
53
|
+
*/
|
|
54
|
+
declare const OBFButtonActionSchema: z.ZodUnion<readonly [z.ZodString, z.ZodString]>;
|
|
55
|
+
type OBFButtonAction = z.infer<typeof OBFButtonActionSchema>;
|
|
56
|
+
/**
|
|
57
|
+
* License terms and attribution for a resource.
|
|
58
|
+
*/
|
|
59
|
+
declare const OBFLicenseSchema: z.ZodObject<{
|
|
60
|
+
type: z.ZodString;
|
|
61
|
+
copyright_notice_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
62
|
+
source_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
63
|
+
author_name: z.ZodOptional<z.ZodString>;
|
|
64
|
+
author_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
65
|
+
author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
66
|
+
}, z.core.$loose>;
|
|
67
|
+
type OBFLicense = z.infer<typeof OBFLicenseSchema>;
|
|
68
|
+
/**
|
|
69
|
+
* Common properties for media resources (images and sounds).
|
|
70
|
+
*
|
|
71
|
+
* When multiple references are provided, they should be used in the following order:
|
|
72
|
+
* 1. data
|
|
73
|
+
* 2. path
|
|
74
|
+
* 3. url
|
|
75
|
+
*/
|
|
76
|
+
declare const OBFMediaSchema: z.ZodObject<{
|
|
77
|
+
id: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
|
|
78
|
+
data: z.ZodOptional<z.ZodString>;
|
|
79
|
+
path: z.ZodOptional<z.ZodString>;
|
|
80
|
+
data_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
81
|
+
url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
82
|
+
content_type: z.ZodOptional<z.ZodString>;
|
|
83
|
+
license: z.ZodOptional<z.ZodObject<{
|
|
84
|
+
type: z.ZodString;
|
|
85
|
+
copyright_notice_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
86
|
+
source_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
87
|
+
author_name: z.ZodOptional<z.ZodString>;
|
|
88
|
+
author_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
89
|
+
author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
90
|
+
}, z.core.$loose>>;
|
|
91
|
+
}, z.core.$loose>;
|
|
92
|
+
type OBFMedia = z.infer<typeof OBFMediaSchema>;
|
|
93
|
+
/**
|
|
94
|
+
* Reference to a symbol in a proprietary symbol set (e.g., SymbolStix).
|
|
95
|
+
*/
|
|
96
|
+
declare const OBFSymbolInfoSchema: z.ZodObject<{
|
|
97
|
+
set: z.ZodString;
|
|
98
|
+
filename: z.ZodString;
|
|
99
|
+
}, z.core.$loose>;
|
|
100
|
+
type OBFSymbolInfo = z.infer<typeof OBFSymbolInfoSchema>;
|
|
101
|
+
/**
|
|
102
|
+
* Image resource, extending {@link OBFMediaSchema} with optional
|
|
103
|
+
* symbol and dimension properties.
|
|
104
|
+
*
|
|
105
|
+
* When resolving the image, consumers should prefer sources in this order:
|
|
106
|
+
* 1. `data`
|
|
107
|
+
* 2. `path`
|
|
108
|
+
* 3. `url`
|
|
109
|
+
* 4. `symbol`
|
|
110
|
+
*/
|
|
111
|
+
declare const OBFImageSchema: z.ZodIntersection<z.ZodObject<{
|
|
112
|
+
id: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
|
|
113
|
+
data: z.ZodOptional<z.ZodString>;
|
|
114
|
+
path: z.ZodOptional<z.ZodString>;
|
|
115
|
+
data_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
116
|
+
url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
117
|
+
content_type: z.ZodOptional<z.ZodString>;
|
|
118
|
+
license: z.ZodOptional<z.ZodObject<{
|
|
119
|
+
type: z.ZodString;
|
|
120
|
+
copyright_notice_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
121
|
+
source_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
122
|
+
author_name: z.ZodOptional<z.ZodString>;
|
|
123
|
+
author_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
124
|
+
author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
125
|
+
}, z.core.$loose>>;
|
|
126
|
+
}, z.core.$loose>, z.ZodObject<{
|
|
127
|
+
symbol: z.ZodOptional<z.ZodObject<{
|
|
128
|
+
set: z.ZodString;
|
|
129
|
+
filename: z.ZodString;
|
|
130
|
+
}, z.core.$loose>>;
|
|
131
|
+
width: z.ZodOptional<z.ZodNumber>;
|
|
132
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
133
|
+
}, z.core.$strip>>;
|
|
134
|
+
type OBFImage = z.infer<typeof OBFImageSchema>;
|
|
135
|
+
/**
|
|
136
|
+
* Audio resource. Identical to {@link OBFMediaSchema} — no additional properties.
|
|
137
|
+
*/
|
|
138
|
+
declare const OBFSoundSchema: z.ZodObject<{
|
|
139
|
+
id: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
|
|
140
|
+
data: z.ZodOptional<z.ZodString>;
|
|
141
|
+
path: z.ZodOptional<z.ZodString>;
|
|
142
|
+
data_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
143
|
+
url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
144
|
+
content_type: z.ZodOptional<z.ZodString>;
|
|
145
|
+
license: z.ZodOptional<z.ZodObject<{
|
|
146
|
+
type: z.ZodString;
|
|
147
|
+
copyright_notice_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
148
|
+
source_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
149
|
+
author_name: z.ZodOptional<z.ZodString>;
|
|
150
|
+
author_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
151
|
+
author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
152
|
+
}, z.core.$loose>>;
|
|
153
|
+
}, z.core.$loose>;
|
|
154
|
+
type OBFSound = z.infer<typeof OBFSoundSchema>;
|
|
155
|
+
/**
|
|
156
|
+
* Reference to another board, resolved by ID, path, or URL.
|
|
157
|
+
*/
|
|
158
|
+
declare const OBFLoadBoardSchema: z.ZodObject<{
|
|
159
|
+
id: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string | undefined, string | number>>>;
|
|
160
|
+
name: z.ZodOptional<z.ZodString>;
|
|
161
|
+
data_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
162
|
+
url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
163
|
+
path: z.ZodOptional<z.ZodString>;
|
|
164
|
+
}, z.core.$loose>;
|
|
165
|
+
type OBFLoadBoard = z.infer<typeof OBFLoadBoardSchema>;
|
|
166
|
+
/**
|
|
167
|
+
* Interactive element on a board, optionally linked to images, sounds, and actions.
|
|
168
|
+
*/
|
|
169
|
+
declare const OBFButtonSchema: z.ZodObject<{
|
|
170
|
+
id: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
|
|
171
|
+
label: z.ZodOptional<z.ZodString>;
|
|
172
|
+
vocalization: z.ZodOptional<z.ZodString>;
|
|
173
|
+
image_id: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string | undefined, string | number>>>;
|
|
174
|
+
sound_id: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string | undefined, string | number>>>;
|
|
175
|
+
action: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodString]>>;
|
|
176
|
+
actions: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodString]>>>;
|
|
177
|
+
load_board: z.ZodOptional<z.ZodObject<{
|
|
178
|
+
id: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string | undefined, string | number>>>;
|
|
179
|
+
name: z.ZodOptional<z.ZodString>;
|
|
180
|
+
data_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
181
|
+
url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
182
|
+
path: z.ZodOptional<z.ZodString>;
|
|
183
|
+
}, z.core.$loose>>;
|
|
184
|
+
background_color: z.ZodOptional<z.ZodString>;
|
|
185
|
+
border_color: z.ZodOptional<z.ZodString>;
|
|
186
|
+
top: z.ZodOptional<z.ZodNumber>;
|
|
187
|
+
left: z.ZodOptional<z.ZodNumber>;
|
|
188
|
+
width: z.ZodOptional<z.ZodNumber>;
|
|
189
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
190
|
+
}, z.core.$loose>;
|
|
191
|
+
type OBFButton = z.infer<typeof OBFButtonSchema>;
|
|
192
|
+
/**
|
|
193
|
+
* Row-and-column layout that arranges buttons by their IDs.
|
|
194
|
+
*/
|
|
195
|
+
declare const OBFGridSchema: z.ZodObject<{
|
|
196
|
+
rows: z.ZodNumber;
|
|
197
|
+
columns: z.ZodNumber;
|
|
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.$loose>;
|
|
200
|
+
type OBFGrid = z.infer<typeof OBFGridSchema>;
|
|
201
|
+
/**
|
|
202
|
+
* Root object of an `.obf` file: the complete definition of a single communication board.
|
|
203
|
+
*/
|
|
204
|
+
declare const OBFBoardSchema: z.ZodObject<{
|
|
205
|
+
format: z.ZodString;
|
|
206
|
+
id: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
|
|
207
|
+
locale: z.ZodOptional<z.ZodString>;
|
|
208
|
+
buttons: z.ZodArray<z.ZodObject<{
|
|
209
|
+
id: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
|
|
210
|
+
label: z.ZodOptional<z.ZodString>;
|
|
211
|
+
vocalization: z.ZodOptional<z.ZodString>;
|
|
212
|
+
image_id: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string | undefined, string | number>>>;
|
|
213
|
+
sound_id: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string | undefined, string | number>>>;
|
|
214
|
+
action: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodString]>>;
|
|
215
|
+
actions: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodString]>>>;
|
|
216
|
+
load_board: z.ZodOptional<z.ZodObject<{
|
|
217
|
+
id: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string | undefined, string | number>>>;
|
|
218
|
+
name: z.ZodOptional<z.ZodString>;
|
|
219
|
+
data_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
220
|
+
url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
221
|
+
path: z.ZodOptional<z.ZodString>;
|
|
222
|
+
}, z.core.$loose>>;
|
|
223
|
+
background_color: z.ZodOptional<z.ZodString>;
|
|
224
|
+
border_color: z.ZodOptional<z.ZodString>;
|
|
225
|
+
top: z.ZodOptional<z.ZodNumber>;
|
|
226
|
+
left: z.ZodOptional<z.ZodNumber>;
|
|
227
|
+
width: z.ZodOptional<z.ZodNumber>;
|
|
228
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
229
|
+
}, z.core.$loose>>;
|
|
230
|
+
url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
231
|
+
name: z.ZodOptional<z.ZodString>;
|
|
232
|
+
description_html: z.ZodOptional<z.ZodString>;
|
|
233
|
+
grid: z.ZodObject<{
|
|
234
|
+
rows: z.ZodNumber;
|
|
235
|
+
columns: z.ZodNumber;
|
|
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.$loose>;
|
|
238
|
+
images: z.ZodOptional<z.ZodArray<z.ZodIntersection<z.ZodObject<{
|
|
239
|
+
id: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
|
|
240
|
+
data: z.ZodOptional<z.ZodString>;
|
|
241
|
+
path: z.ZodOptional<z.ZodString>;
|
|
242
|
+
data_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
243
|
+
url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
244
|
+
content_type: z.ZodOptional<z.ZodString>;
|
|
245
|
+
license: z.ZodOptional<z.ZodObject<{
|
|
246
|
+
type: z.ZodString;
|
|
247
|
+
copyright_notice_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
248
|
+
source_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
249
|
+
author_name: z.ZodOptional<z.ZodString>;
|
|
250
|
+
author_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
251
|
+
author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
252
|
+
}, z.core.$loose>>;
|
|
253
|
+
}, z.core.$loose>, z.ZodObject<{
|
|
254
|
+
symbol: z.ZodOptional<z.ZodObject<{
|
|
255
|
+
set: z.ZodString;
|
|
256
|
+
filename: z.ZodString;
|
|
257
|
+
}, z.core.$loose>>;
|
|
258
|
+
width: z.ZodOptional<z.ZodNumber>;
|
|
259
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
260
|
+
}, z.core.$strip>>>>;
|
|
261
|
+
sounds: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
262
|
+
id: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
|
|
263
|
+
data: z.ZodOptional<z.ZodString>;
|
|
264
|
+
path: z.ZodOptional<z.ZodString>;
|
|
265
|
+
data_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
266
|
+
url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
267
|
+
content_type: z.ZodOptional<z.ZodString>;
|
|
268
|
+
license: z.ZodOptional<z.ZodObject<{
|
|
269
|
+
type: z.ZodString;
|
|
270
|
+
copyright_notice_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
271
|
+
source_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
272
|
+
author_name: z.ZodOptional<z.ZodString>;
|
|
273
|
+
author_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
274
|
+
author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
275
|
+
}, z.core.$loose>>;
|
|
276
|
+
}, z.core.$loose>>>;
|
|
277
|
+
license: z.ZodOptional<z.ZodObject<{
|
|
278
|
+
type: z.ZodString;
|
|
279
|
+
copyright_notice_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
280
|
+
source_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
281
|
+
author_name: z.ZodOptional<z.ZodString>;
|
|
282
|
+
author_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
283
|
+
author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
284
|
+
}, z.core.$loose>>;
|
|
285
|
+
strings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>>;
|
|
286
|
+
}, z.core.$loose>;
|
|
287
|
+
type OBFBoard = z.infer<typeof OBFBoardSchema>;
|
|
288
|
+
/**
|
|
289
|
+
* Table of contents for an `.obz` package, mapping resource IDs to their archive paths.
|
|
290
|
+
*/
|
|
291
|
+
declare const OBFManifestSchema: z.ZodObject<{
|
|
292
|
+
format: z.ZodString;
|
|
293
|
+
root: z.ZodString;
|
|
294
|
+
paths: z.ZodObject<{
|
|
295
|
+
boards: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
296
|
+
images: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
297
|
+
sounds: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
298
|
+
}, z.core.$loose>;
|
|
299
|
+
}, z.core.$loose>;
|
|
300
|
+
type OBFManifest = z.infer<typeof OBFManifestSchema>;
|
|
301
|
+
//#endregion
|
|
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
|
+
*/
|
|
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
|
+
*/
|
|
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
|
+
*/
|
|
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
|
+
*/
|
|
342
|
+
declare function stringifyOBF(board: OBFBoard): string;
|
|
343
|
+
//#endregion
|
|
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
|
+
*/
|
|
352
|
+
interface ParsedOBZ {
|
|
353
|
+
manifest: OBFManifest;
|
|
354
|
+
boards: Map<string, OBFBoard>;
|
|
355
|
+
resources: Map<string, Uint8Array>;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
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.
|
|
367
|
+
*/
|
|
368
|
+
declare function loadOBZ(file: File): Promise<ParsedOBZ>;
|
|
369
|
+
/**
|
|
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.
|
|
377
|
+
*/
|
|
378
|
+
declare function extractOBZ(archive: ArrayBuffer): Promise<ParsedOBZ>;
|
|
379
|
+
/**
|
|
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.
|
|
387
|
+
*/
|
|
388
|
+
declare function parseManifest(json: string): OBFManifest;
|
|
389
|
+
/**
|
|
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.
|
|
401
|
+
*/
|
|
402
|
+
declare function createOBZ(boards: OBFBoard[], rootBoardId: string, resources?: Map<string, Uint8Array | ArrayBuffer>): Promise<Blob>;
|
|
403
|
+
//#endregion
|
|
404
|
+
//#region src/zip.d.ts
|
|
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;
|
|
435
|
+
//#endregion
|
|
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
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { unzip as unzip$1, zip as zip$1 } from "fflate";
|
|
3
|
+
//#region src/schema.ts
|
|
4
|
+
/**
|
|
5
|
+
* Open Board Format (OBF) Zod Schemas
|
|
6
|
+
*
|
|
7
|
+
* These schemas represent the Open Board Format, designed for sharing communication boards and board sets
|
|
8
|
+
* between Augmentative and Alternative Communication (AAC) applications.
|
|
9
|
+
*
|
|
10
|
+
* Official OBF specification: https://www.openboardformat.org/docs
|
|
11
|
+
*
|
|
12
|
+
* @author Shay Cojocaru
|
|
13
|
+
* @license MIT
|
|
14
|
+
*/
|
|
15
|
+
/** Optional URL that treats empty strings as undefined. */
|
|
16
|
+
const OBFOptionalUrlSchema = z.union([z.url(), z.literal("")]).transform((val) => val === "" ? void 0 : val).optional();
|
|
17
|
+
/** Optional email that treats empty strings as undefined. */
|
|
18
|
+
const OBFOptionalEmailSchema = z.union([z.email(), z.literal("")]).transform((val) => val === "" ? void 0 : val).optional();
|
|
19
|
+
/** Optional ID that treats empty strings as undefined. */
|
|
20
|
+
const OBFOptionalIDSchema = z.union([z.string(), z.number()]).transform((val) => {
|
|
21
|
+
const str = String(val);
|
|
22
|
+
return str === "" ? void 0 : str;
|
|
23
|
+
}).optional();
|
|
24
|
+
/** Unique board-element identifier, coerced to a non-empty string. */
|
|
25
|
+
const OBFIDSchema = z.union([z.string(), z.number()]).transform((val) => String(val)).pipe(z.string().min(1));
|
|
26
|
+
/**
|
|
27
|
+
* Format version of the Open Board Format, e.g., 'open-board-0.1'.
|
|
28
|
+
*/
|
|
29
|
+
const OBFFormatVersionSchema = z.string().regex(/^open-board-.+$/);
|
|
30
|
+
/**
|
|
31
|
+
* Locale code as per BCP 47 language tags, e.g., 'en', 'en-US', 'fr-CA'.
|
|
32
|
+
*/
|
|
33
|
+
const OBFLocaleCodeSchema = z.string();
|
|
34
|
+
/**
|
|
35
|
+
* Key–value pairs mapping symbolic names to their translations in a single locale.
|
|
36
|
+
*/
|
|
37
|
+
const OBFLocalizedStringsSchema = z.record(z.string(), z.string());
|
|
38
|
+
/**
|
|
39
|
+
* Locale-keyed dictionary of translated strings,
|
|
40
|
+
* e.g., `{ en: { greeting: "Hello" }, fr: { greeting: "Bonjour" } }`.
|
|
41
|
+
*/
|
|
42
|
+
const OBFStringsSchema = z.record(z.string(), OBFLocalizedStringsSchema);
|
|
43
|
+
/**
|
|
44
|
+
* Spelling action: a `+` prefix followed by the text to append,
|
|
45
|
+
* e.g., `"+hello"`.
|
|
46
|
+
*/
|
|
47
|
+
const OBFSpellingActionSchema = z.string().regex(/^\+.+$/);
|
|
48
|
+
/**
|
|
49
|
+
* Specialty action prefixed with `:`, e.g., `":clear"`.
|
|
50
|
+
* Custom extensions use the `:ext_` prefix.
|
|
51
|
+
*/
|
|
52
|
+
const OBFSpecialtyActionSchema = z.string().regex(/^:[a-z][a-z0-9_-]*$/i);
|
|
53
|
+
/**
|
|
54
|
+
* Union of spelling and specialty actions that a button can trigger.
|
|
55
|
+
*/
|
|
56
|
+
const OBFButtonActionSchema = z.union([OBFSpellingActionSchema, OBFSpecialtyActionSchema]);
|
|
57
|
+
/**
|
|
58
|
+
* License terms and attribution for a resource.
|
|
59
|
+
*/
|
|
60
|
+
const OBFLicenseSchema = z.looseObject({
|
|
61
|
+
/** Type of the license, e.g., 'CC-BY-SA'. */
|
|
62
|
+
type: z.string(),
|
|
63
|
+
/** URL to the license terms. */
|
|
64
|
+
copyright_notice_url: OBFOptionalUrlSchema,
|
|
65
|
+
/** Source URL of the resource. */
|
|
66
|
+
source_url: OBFOptionalUrlSchema,
|
|
67
|
+
/** Name of the author. */
|
|
68
|
+
author_name: z.string().optional(),
|
|
69
|
+
/** URL of the author's webpage. */
|
|
70
|
+
author_url: OBFOptionalUrlSchema,
|
|
71
|
+
/** Email address of the author. */
|
|
72
|
+
author_email: OBFOptionalEmailSchema
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* Common properties for media resources (images and sounds).
|
|
76
|
+
*
|
|
77
|
+
* When multiple references are provided, they should be used in the following order:
|
|
78
|
+
* 1. data
|
|
79
|
+
* 2. path
|
|
80
|
+
* 3. url
|
|
81
|
+
*/
|
|
82
|
+
const OBFMediaSchema = z.looseObject({
|
|
83
|
+
/** Unique identifier for the media resource. */
|
|
84
|
+
id: OBFIDSchema,
|
|
85
|
+
/** Data URI containing the media data. */
|
|
86
|
+
data: z.string().optional(),
|
|
87
|
+
/** Path to the media file within an .obz package. */
|
|
88
|
+
path: z.string().optional(),
|
|
89
|
+
/** Data URL to fetch the media programmatically. */
|
|
90
|
+
data_url: OBFOptionalUrlSchema,
|
|
91
|
+
/** URL to the media resource. */
|
|
92
|
+
url: OBFOptionalUrlSchema,
|
|
93
|
+
/** MIME type of the media, e.g., 'image/png', 'audio/mpeg'. */
|
|
94
|
+
content_type: z.string().optional(),
|
|
95
|
+
/** Licensing information for the media. */
|
|
96
|
+
license: OBFLicenseSchema.optional()
|
|
97
|
+
});
|
|
98
|
+
/**
|
|
99
|
+
* Reference to a symbol in a proprietary symbol set (e.g., SymbolStix).
|
|
100
|
+
*/
|
|
101
|
+
const OBFSymbolInfoSchema = z.looseObject({
|
|
102
|
+
/** Name of the symbol set, e.g., 'symbolstix'. */
|
|
103
|
+
set: z.string(),
|
|
104
|
+
/** Filename of the symbol within the set. */
|
|
105
|
+
filename: z.string()
|
|
106
|
+
});
|
|
107
|
+
/**
|
|
108
|
+
* Image resource, extending {@link OBFMediaSchema} with optional
|
|
109
|
+
* symbol and dimension properties.
|
|
110
|
+
*
|
|
111
|
+
* When resolving the image, consumers should prefer sources in this order:
|
|
112
|
+
* 1. `data`
|
|
113
|
+
* 2. `path`
|
|
114
|
+
* 3. `url`
|
|
115
|
+
* 4. `symbol`
|
|
116
|
+
*/
|
|
117
|
+
const OBFImageSchema = OBFMediaSchema.and(z.object({
|
|
118
|
+
/** Information about a symbol from a proprietary symbol set. */
|
|
119
|
+
symbol: OBFSymbolInfoSchema.optional(),
|
|
120
|
+
/** Width of the image in pixels. */
|
|
121
|
+
width: z.number().optional(),
|
|
122
|
+
/** Height of the image in pixels. */
|
|
123
|
+
height: z.number().optional()
|
|
124
|
+
}));
|
|
125
|
+
/**
|
|
126
|
+
* Audio resource. Identical to {@link OBFMediaSchema} — no additional properties.
|
|
127
|
+
*/
|
|
128
|
+
const OBFSoundSchema = OBFMediaSchema;
|
|
129
|
+
/**
|
|
130
|
+
* Reference to another board, resolved by ID, path, or URL.
|
|
131
|
+
*/
|
|
132
|
+
const OBFLoadBoardSchema = z.looseObject({
|
|
133
|
+
/** Unique identifier of the board to load. */
|
|
134
|
+
id: OBFOptionalIDSchema,
|
|
135
|
+
/** Name of the board to load. */
|
|
136
|
+
name: z.string().optional(),
|
|
137
|
+
/** Data URL to fetch the board programmatically. */
|
|
138
|
+
data_url: OBFOptionalUrlSchema,
|
|
139
|
+
/** URL to access the board via a web browser. */
|
|
140
|
+
url: OBFOptionalUrlSchema,
|
|
141
|
+
/** Path to the board within an .obz package. */
|
|
142
|
+
path: z.string().optional()
|
|
143
|
+
});
|
|
144
|
+
/**
|
|
145
|
+
* Interactive element on a board, optionally linked to images, sounds, and actions.
|
|
146
|
+
*/
|
|
147
|
+
const OBFButtonSchema = z.looseObject({
|
|
148
|
+
/** Unique identifier for the button. */
|
|
149
|
+
id: OBFIDSchema,
|
|
150
|
+
/** Label text displayed on the button. */
|
|
151
|
+
label: z.string().optional(),
|
|
152
|
+
/** Alternative text for vocalization when the button is activated. */
|
|
153
|
+
vocalization: z.string().optional(),
|
|
154
|
+
/** Identifier of the image associated with the button. */
|
|
155
|
+
image_id: OBFOptionalIDSchema,
|
|
156
|
+
/** Identifier of the sound associated with the button. */
|
|
157
|
+
sound_id: OBFOptionalIDSchema,
|
|
158
|
+
/** Action associated with the button. */
|
|
159
|
+
action: OBFButtonActionSchema.optional(),
|
|
160
|
+
/** List of multiple actions for the button, executed in order. */
|
|
161
|
+
actions: z.array(OBFButtonActionSchema).optional(),
|
|
162
|
+
/** Information to load another board when this button is activated. */
|
|
163
|
+
load_board: OBFLoadBoardSchema.optional(),
|
|
164
|
+
/** Background color of the button in 'rgb' or 'rgba' format. */
|
|
165
|
+
background_color: z.string().optional(),
|
|
166
|
+
/** Border color of the button in 'rgb' or 'rgba' format. */
|
|
167
|
+
border_color: z.string().optional(),
|
|
168
|
+
/** Vertical position for absolute positioning (0.0 to 1.0). */
|
|
169
|
+
top: z.number().min(0).max(1).optional(),
|
|
170
|
+
/** Horizontal position for absolute positioning (0.0 to 1.0). */
|
|
171
|
+
left: z.number().min(0).max(1).optional(),
|
|
172
|
+
/** Width of the button for absolute positioning (0.0 to 1.0). */
|
|
173
|
+
width: z.number().min(0).max(1).optional(),
|
|
174
|
+
/** Height of the button for absolute positioning (0.0 to 1.0). */
|
|
175
|
+
height: z.number().min(0).max(1).optional()
|
|
176
|
+
});
|
|
177
|
+
/**
|
|
178
|
+
* Row-and-column layout that arranges buttons by their IDs.
|
|
179
|
+
*/
|
|
180
|
+
const OBFGridSchema = z.looseObject({
|
|
181
|
+
/** Number of rows in the grid. */
|
|
182
|
+
rows: z.number().int().min(1),
|
|
183
|
+
/** Number of columns in the grid. */
|
|
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
|
+
*/
|
|
189
|
+
order: z.array(z.array(z.union([OBFIDSchema, z.null()])))
|
|
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" });
|
|
191
|
+
/**
|
|
192
|
+
* Root object of an `.obf` file: the complete definition of a single communication board.
|
|
193
|
+
*/
|
|
194
|
+
const OBFBoardSchema = z.looseObject({
|
|
195
|
+
/** Format version of the Open Board Format, e.g., 'open-board-0.1'. */
|
|
196
|
+
format: OBFFormatVersionSchema,
|
|
197
|
+
/** Unique identifier for the board. */
|
|
198
|
+
id: OBFIDSchema,
|
|
199
|
+
/** Locale of the board as a BCP 47 language tag, e.g., 'en', 'en-US'. */
|
|
200
|
+
locale: OBFLocaleCodeSchema.optional(),
|
|
201
|
+
/** List of buttons on the board. */
|
|
202
|
+
buttons: z.array(OBFButtonSchema),
|
|
203
|
+
/** URL where the board can be accessed or downloaded. */
|
|
204
|
+
url: OBFOptionalUrlSchema,
|
|
205
|
+
/** Name of the board. */
|
|
206
|
+
name: z.string().optional(),
|
|
207
|
+
/** Description of the board in HTML format. */
|
|
208
|
+
description_html: z.string().optional(),
|
|
209
|
+
/** Grid layout information for arranging buttons. */
|
|
210
|
+
grid: OBFGridSchema,
|
|
211
|
+
/** List of images used in the board. */
|
|
212
|
+
images: z.array(OBFImageSchema).optional(),
|
|
213
|
+
/** List of sounds used in the board. */
|
|
214
|
+
sounds: z.array(OBFSoundSchema).optional(),
|
|
215
|
+
/** Licensing information for the board. */
|
|
216
|
+
license: OBFLicenseSchema.optional(),
|
|
217
|
+
/** String translations for multiple locales. */
|
|
218
|
+
strings: OBFStringsSchema.optional()
|
|
219
|
+
});
|
|
220
|
+
/**
|
|
221
|
+
* Table of contents for an `.obz` package, mapping resource IDs to their archive paths.
|
|
222
|
+
*/
|
|
223
|
+
const OBFManifestSchema = z.looseObject({
|
|
224
|
+
/** Format version of the Open Board Format, e.g., 'open-board-0.1'. */
|
|
225
|
+
format: OBFFormatVersionSchema,
|
|
226
|
+
/** Path to the root board within the .obz package. */
|
|
227
|
+
root: z.string(),
|
|
228
|
+
/** Mapping of IDs to paths for boards, images, and sounds. */
|
|
229
|
+
paths: z.looseObject({
|
|
230
|
+
/** Mapping of board IDs to their file paths. */
|
|
231
|
+
boards: z.record(z.string(), z.string()),
|
|
232
|
+
/** Mapping of image IDs to their file paths. */
|
|
233
|
+
images: z.record(z.string(), z.string()),
|
|
234
|
+
/** Mapping of sound IDs to their file paths. */
|
|
235
|
+
sounds: z.record(z.string(), z.string()).optional()
|
|
236
|
+
})
|
|
237
|
+
});
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/obf.ts
|
|
240
|
+
const UTF8_BOM = "";
|
|
241
|
+
/** Strip a leading UTF-8 BOM, which some editors silently prepend. */
|
|
242
|
+
function stripBom(text) {
|
|
243
|
+
return text.startsWith(UTF8_BOM) ? text.slice(1) : text;
|
|
244
|
+
}
|
|
245
|
+
/** Build a descriptive parse-failure message, preserving the engine's reason when available. */
|
|
246
|
+
function buildParseErrorMessage(error) {
|
|
247
|
+
const reason = error instanceof Error ? error.message : "";
|
|
248
|
+
return reason ? `Invalid OBF: JSON parse failed — ${reason}` : "Invalid OBF: JSON parse failed";
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Parse a JSON string into a validated OBF board.
|
|
252
|
+
*
|
|
253
|
+
* Strips an optional UTF-8 BOM prefix before parsing and throws a
|
|
254
|
+
* descriptive error if the input is malformed or fails schema validation.
|
|
255
|
+
*
|
|
256
|
+
* @param json - The JSON string to parse.
|
|
257
|
+
* @returns The validated board object.
|
|
258
|
+
*
|
|
259
|
+
* @throws {Error} If the JSON is malformed or does not conform to the OBF schema.
|
|
260
|
+
*/
|
|
261
|
+
function parseOBF(json) {
|
|
262
|
+
const sanitized = stripBom(json);
|
|
263
|
+
let rawBoard;
|
|
264
|
+
try {
|
|
265
|
+
rawBoard = JSON.parse(sanitized);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
throw new Error(buildParseErrorMessage(error));
|
|
268
|
+
}
|
|
269
|
+
return validateOBF(rawBoard);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Read a `File` and parse its contents as a validated OBF board.
|
|
273
|
+
*
|
|
274
|
+
* This relies on the browser `File` API; for Node environments,
|
|
275
|
+
* read the file to a string and pass it to {@link parseOBF} instead.
|
|
276
|
+
*
|
|
277
|
+
* @param file - A `File` handle pointing to an `.obf` file.
|
|
278
|
+
* @returns The validated board object.
|
|
279
|
+
*
|
|
280
|
+
* @throws {Error} If the file content is malformed or fails schema validation.
|
|
281
|
+
*/
|
|
282
|
+
async function loadOBF(file) {
|
|
283
|
+
return parseOBF(await file.text());
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Validate an unknown value against the OBF board schema.
|
|
287
|
+
*
|
|
288
|
+
* @param data - The value to validate.
|
|
289
|
+
* @returns The validated board object.
|
|
290
|
+
*
|
|
291
|
+
* @throws {Error} If the value does not conform to the OBF schema.
|
|
292
|
+
*/
|
|
293
|
+
function validateOBF(data) {
|
|
294
|
+
const result = OBFBoardSchema.safeParse(data);
|
|
295
|
+
if (!result.success) throw new Error(`Invalid OBF: ${result.error.message}`);
|
|
296
|
+
return result.data;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Stringify an OBF board to a pretty-printed JSON string.
|
|
300
|
+
*
|
|
301
|
+
* @param board - The board to stringify.
|
|
302
|
+
* @returns A JSON string with two-space indentation.
|
|
303
|
+
*/
|
|
304
|
+
function stringifyOBF(board) {
|
|
305
|
+
return JSON.stringify(board, null, 2);
|
|
306
|
+
}
|
|
307
|
+
//#endregion
|
|
308
|
+
//#region src/zip.ts
|
|
309
|
+
/**
|
|
310
|
+
* First two bytes of every ZIP archive — the ASCII letters `PK`,
|
|
311
|
+
* after Phil Katz, creator of the format.
|
|
312
|
+
*
|
|
313
|
+
* Only the 2-byte prefix is checked intentionally: this keeps the
|
|
314
|
+
* test lightweight and sufficient for distinguishing ZIP from JSON.
|
|
315
|
+
*/
|
|
316
|
+
const ZIP_MAGIC = [80, 75];
|
|
317
|
+
/** Balanced speed-vs-size deflate level used by fflate (1–9 scale). */
|
|
318
|
+
const COMPRESSION_LEVEL = 6;
|
|
319
|
+
/**
|
|
320
|
+
* Decompress a ZIP archive into a map of file paths to raw bytes.
|
|
321
|
+
*
|
|
322
|
+
* @param archive - The ZIP archive as an `ArrayBuffer`.
|
|
323
|
+
* @returns A map of file paths to their decompressed content.
|
|
324
|
+
*
|
|
325
|
+
* @throws {Error} If decompression fails.
|
|
326
|
+
*/
|
|
327
|
+
function unzip(archive) {
|
|
328
|
+
return new Promise((resolve, reject) => {
|
|
329
|
+
unzip$1(new Uint8Array(archive), (error, entries) => {
|
|
330
|
+
if (error) {
|
|
331
|
+
reject(/* @__PURE__ */ new Error(`Failed to unzip: ${error.message ?? String(error)}`));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
resolve(new Map(Object.entries(entries)));
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Compress a map of file paths and contents into a single ZIP archive.
|
|
340
|
+
*
|
|
341
|
+
* Accepts both `Uint8Array` and `ArrayBuffer` values so callers can
|
|
342
|
+
* pass the output of {@link unzip} directly or supply raw `ArrayBuffer`s
|
|
343
|
+
* without converting first.
|
|
344
|
+
*
|
|
345
|
+
* @param entries - A map of file paths to their content bytes.
|
|
346
|
+
* @returns The compressed archive as a `Uint8Array`.
|
|
347
|
+
*
|
|
348
|
+
* @throws {Error} If compression fails.
|
|
349
|
+
*/
|
|
350
|
+
function zip(entries) {
|
|
351
|
+
return new Promise((resolve, reject) => {
|
|
352
|
+
const pathToBytes = {};
|
|
353
|
+
for (const [path, content] of entries) pathToBytes[path] = content instanceof Uint8Array ? content : new Uint8Array(content);
|
|
354
|
+
zip$1(pathToBytes, { level: COMPRESSION_LEVEL }, (error, result) => {
|
|
355
|
+
if (error) {
|
|
356
|
+
reject(/* @__PURE__ */ new Error(`Failed to zip: ${error.message ?? String(error)}`));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
resolve(result);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Test whether an `ArrayBuffer` begins with the two-byte ZIP magic
|
|
365
|
+
* prefix (`PK`).
|
|
366
|
+
*
|
|
367
|
+
* @param archive - The buffer to inspect.
|
|
368
|
+
* @returns `true` if the buffer starts with the ZIP signature.
|
|
369
|
+
*/
|
|
370
|
+
function isZip(archive) {
|
|
371
|
+
const bytes = new Uint8Array(archive);
|
|
372
|
+
return bytes.length >= ZIP_MAGIC.length && ZIP_MAGIC.every((byte, index) => bytes[index] === byte);
|
|
373
|
+
}
|
|
374
|
+
//#endregion
|
|
375
|
+
//#region src/obz.ts
|
|
376
|
+
/**
|
|
377
|
+
* Read a `File` and extract its contents as a parsed OBZ package.
|
|
378
|
+
*
|
|
379
|
+
* This relies on the browser `File` API; for Node environments,
|
|
380
|
+
* read the file to an `ArrayBuffer` and pass it to {@link extractOBZ} instead.
|
|
381
|
+
*
|
|
382
|
+
* @param file - A `File` handle pointing to an `.obz` archive.
|
|
383
|
+
* @returns The parsed manifest, boards, and binary resources.
|
|
384
|
+
*
|
|
385
|
+
* @throws {Error} If the file is not a valid ZIP or the manifest is missing.
|
|
386
|
+
*/
|
|
387
|
+
async function loadOBZ(file) {
|
|
388
|
+
return extractOBZ(await file.arrayBuffer());
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Decompress an OBZ archive and return its manifest, boards, and resources.
|
|
392
|
+
*
|
|
393
|
+
* @param archive - The OBZ archive as an `ArrayBuffer`.
|
|
394
|
+
* @returns The parsed manifest, a map of board IDs to validated boards,
|
|
395
|
+
* and a map of file paths to their binary content.
|
|
396
|
+
*
|
|
397
|
+
* @throws {Error} If the archive is not a valid ZIP or the manifest is missing.
|
|
398
|
+
*/
|
|
399
|
+
async function extractOBZ(archive) {
|
|
400
|
+
if (!isZip(archive)) throw new Error("Invalid OBZ: not a ZIP file");
|
|
401
|
+
const entries = await unzip(archive);
|
|
402
|
+
const manifest = extractManifest(entries);
|
|
403
|
+
return {
|
|
404
|
+
manifest,
|
|
405
|
+
boards: extractBoards(manifest, entries),
|
|
406
|
+
resources: entries
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Parse and validate an OBZ manifest — the table of contents that maps
|
|
411
|
+
* board IDs to their file paths within the archive.
|
|
412
|
+
*
|
|
413
|
+
* @param json - A JSON string representing the manifest.
|
|
414
|
+
* @returns The validated manifest object.
|
|
415
|
+
*
|
|
416
|
+
* @throws {Error} If the JSON is malformed or fails schema validation.
|
|
417
|
+
*/
|
|
418
|
+
function parseManifest(json) {
|
|
419
|
+
let data;
|
|
420
|
+
try {
|
|
421
|
+
data = JSON.parse(json);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
throw new Error(`Invalid manifest: JSON parse failed${error?.message ? ` — ${error.message}` : ""}`);
|
|
424
|
+
}
|
|
425
|
+
const result = OBFManifestSchema.safeParse(data);
|
|
426
|
+
if (!result.success) throw new Error(`Invalid manifest: ${result.error.message}`);
|
|
427
|
+
return result.data;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Bundle boards and optional resources into a compressed OBZ archive.
|
|
431
|
+
*
|
|
432
|
+
* A manifest is generated automatically from the supplied boards,
|
|
433
|
+
* using the `rootBoardId` to designate the entry-point board.
|
|
434
|
+
*
|
|
435
|
+
* @param boards - The boards to include in the archive.
|
|
436
|
+
* @param rootBoardId - The ID of the board that serves as the archive's entry point.
|
|
437
|
+
* @param resources - Optional map of file paths to binary content (images, sounds, etc.).
|
|
438
|
+
* @returns A `Blob` containing the compressed OBZ archive.
|
|
439
|
+
*
|
|
440
|
+
* @throws {Error} If `rootBoardId` does not match any of the supplied boards.
|
|
441
|
+
*/
|
|
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`);
|
|
444
|
+
const entries = /* @__PURE__ */ new Map();
|
|
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");
|
|
448
|
+
const manifest = OBFManifestSchema.parse({
|
|
449
|
+
format: "open-board-0.1",
|
|
450
|
+
root: `boards/${rootBoardId}.obf`,
|
|
451
|
+
paths: {
|
|
452
|
+
boards: boardPaths,
|
|
453
|
+
images: imagePaths,
|
|
454
|
+
...Object.keys(soundPaths).length > 0 ? { sounds: soundPaths } : {}
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
const encoder = new TextEncoder();
|
|
458
|
+
entries.set("manifest.json", encoder.encode(JSON.stringify(manifest, null, 2)));
|
|
459
|
+
for (const board of boards) {
|
|
460
|
+
const path = `boards/${board.id}.obf`;
|
|
461
|
+
entries.set(path, encoder.encode(JSON.stringify(board, null, 2)));
|
|
462
|
+
}
|
|
463
|
+
if (resources) for (const [path, bytes] of resources) entries.set(path, bytes);
|
|
464
|
+
const compressed = await zip(entries);
|
|
465
|
+
return new Blob([new Uint8Array(compressed)], { type: "application/zip" });
|
|
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
|
+
}
|
|
484
|
+
function extractManifest(entries) {
|
|
485
|
+
const manifestBytes = entries.get("manifest.json");
|
|
486
|
+
if (!manifestBytes) throw new Error("Invalid OBZ: missing manifest.json");
|
|
487
|
+
return parseManifest(new TextDecoder().decode(manifestBytes));
|
|
488
|
+
}
|
|
489
|
+
function extractBoards(manifest, entries) {
|
|
490
|
+
const boards = /* @__PURE__ */ new Map();
|
|
491
|
+
for (const [id, path] of Object.entries(manifest.paths.boards)) {
|
|
492
|
+
const boardBytes = entries.get(path);
|
|
493
|
+
if (!boardBytes) throw new Error(`Board "${id}" declared in manifest but missing at path "${path}"`);
|
|
494
|
+
const boardJson = new TextDecoder().decode(boardBytes);
|
|
495
|
+
boards.set(id, parseOBF(boardJson));
|
|
496
|
+
}
|
|
497
|
+
return boards;
|
|
498
|
+
}
|
|
499
|
+
//#endregion
|
|
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
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shayc/open-board-format",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Parse, validate, and create Open Board Format (OBF/OBZ) files for AAC applications.",
|
|
6
|
+
"author": "Shay Cojocaru <shayc@outlook.com>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/shayc/open-board-format#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/shayc/open-board-format.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/shayc/open-board-format/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"aac",
|
|
18
|
+
"open-board-format",
|
|
19
|
+
"communication-board"
|
|
20
|
+
],
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./dist/index.mjs",
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"types": "./dist/index.d.mts",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsdown",
|
|
32
|
+
"dev": "tsdown --watch",
|
|
33
|
+
"test": "vitest",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"prepublishOnly": "npm run build"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
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
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"fflate": "^0.8.2",
|
|
46
|
+
"zod": "^4.4.3"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
51
|
+
}
|