open-board-format 1.0.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 +209 -0
- package/dist/index.d.mts +345 -0
- package/dist/index.mjs +315 -0
- package/package.json +48 -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,209 @@
|
|
|
1
|
+
# open-board-format
|
|
2
|
+
|
|
3
|
+
A type-safe toolkit to parse, validate, and create Open Board Format (OBF/OBZ) files for Augmentative and Alternative Communication (AAC) applications.
|
|
4
|
+
|
|
5
|
+
## What is Open Board Format?
|
|
6
|
+
|
|
7
|
+
[Open Board Format](https://www.openboardformat.org/) is an open standard for representing AAC communication boards — grids of labeled buttons that a person taps to communicate. It defines two file types:
|
|
8
|
+
|
|
9
|
+
- **OBF** (`.obf`) — A JSON file describing a single communication board: its buttons, images, sounds, grid layout, and metadata.
|
|
10
|
+
- **OBZ** (`.obz`) — A ZIP archive containing one or more `.obf` boards along with their images, sounds, and a `manifest.json` that ties everything together.
|
|
11
|
+
|
|
12
|
+
The format enables users and practitioners to **move boards between AAC apps** without starting from scratch — a common pain point in the AAC community.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- **Parse & validate** OBF boards from JSON strings, objects, or `File` handles
|
|
17
|
+
- **Create & extract** OBZ packages (ZIP archives with boards, images, and sounds)
|
|
18
|
+
- **[Zod](https://zod.dev/) schemas** for every OBF type — use them for runtime validation, form building, or API contracts
|
|
19
|
+
- **Full TypeScript types** inferred from schemas — no separate type maintenance
|
|
20
|
+
- **Spec-compliant coercion** — numeric IDs are coerced to strings, empty strings become `undefined`, and UTF-8 BOM is handled automatically
|
|
21
|
+
- **Tree-shakeable** ESM build with no side effects (`"sideEffects": false` in `package.json`)
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install open-board-format
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> Zero peer dependencies. Powered by [zod](https://zod.dev/) for validation and [fflate](https://github.com/101arrowz/fflate) for ZIP — both listed as standard `dependencies` in `package.json`, so your bundler deduplicates them normally.
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
All examples below assume the following imports:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import {
|
|
37
|
+
parseOBF,
|
|
38
|
+
validateOBF,
|
|
39
|
+
stringifyOBF,
|
|
40
|
+
loadOBF,
|
|
41
|
+
extractOBZ,
|
|
42
|
+
loadOBZ,
|
|
43
|
+
createOBZ,
|
|
44
|
+
OBFBoardSchema,
|
|
45
|
+
} from "open-board-format";
|
|
46
|
+
import type { OBFBoard, ParsedOBZ } from "open-board-format";
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Parse a single board (OBF)
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// Parse from a JSON string
|
|
53
|
+
const board = parseOBF(jsonString);
|
|
54
|
+
|
|
55
|
+
console.log(board.name); // "My Board"
|
|
56
|
+
console.log(board.buttons[0]); // { id: "1", label: "hello", image_id: "img1" }
|
|
57
|
+
|
|
58
|
+
// Validate an unknown object (throws on failure)
|
|
59
|
+
const validated = validateOBF(untrustedData);
|
|
60
|
+
|
|
61
|
+
// Serialize back to a formatted JSON string
|
|
62
|
+
const json = stringifyOBF(board);
|
|
63
|
+
|
|
64
|
+
// Load from a File object (e.g. browser file-input)
|
|
65
|
+
const fromFile = await loadOBF(file);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Extract an OBZ package
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
// From a File (e.g. drag-and-drop)
|
|
72
|
+
const { manifest, boards, files }: ParsedOBZ = await loadOBZ(file);
|
|
73
|
+
|
|
74
|
+
// Or from an ArrayBuffer (e.g. fetch response)
|
|
75
|
+
const parsed = await extractOBZ(buffer);
|
|
76
|
+
|
|
77
|
+
// Access boards by ID
|
|
78
|
+
const rootPath = parsed.manifest.root; // "boards/1.obf"
|
|
79
|
+
const homeBoard = parsed.boards.get("1");
|
|
80
|
+
|
|
81
|
+
// Access raw files (images, sounds, etc.)
|
|
82
|
+
const imageBytes = parsed.files.get("images/logo.png");
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`ParsedOBZ` has the following shape:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
interface ParsedOBZ {
|
|
89
|
+
manifest: OBFManifest; // Validated manifest.json
|
|
90
|
+
boards: Map<string, OBFBoard>; // Board ID → parsed board
|
|
91
|
+
files: Map<string, Uint8Array>; // File path → raw bytes
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Create an OBZ package
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const boards: OBFBoard[] = [
|
|
99
|
+
{
|
|
100
|
+
format: "open-board-0.1",
|
|
101
|
+
id: "board-1",
|
|
102
|
+
buttons: [{ id: "btn-1", label: "Hello" }],
|
|
103
|
+
grid: { rows: 1, columns: 1, order: [["btn-1"]] },
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
// Optional: include image/sound resources
|
|
108
|
+
const resources = new Map([["images/logo.png", pngBytes]]);
|
|
109
|
+
|
|
110
|
+
const blob = await createOBZ(boards, "board-1", resources);
|
|
111
|
+
// blob is a Blob you can download, upload, or store
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Use Zod schemas directly
|
|
115
|
+
|
|
116
|
+
Every OBF type is exported as both a **Zod schema** (for runtime validation) and a **TypeScript type** (for static analysis):
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
// Safe parsing (returns { success, data, error })
|
|
120
|
+
const result = OBFBoardSchema.safeParse(data);
|
|
121
|
+
|
|
122
|
+
if (result.success) {
|
|
123
|
+
console.log(result.data.buttons);
|
|
124
|
+
} else {
|
|
125
|
+
console.error(result.error.issues);
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## API reference
|
|
130
|
+
|
|
131
|
+
### OBF functions
|
|
132
|
+
|
|
133
|
+
| Signature | Description |
|
|
134
|
+
| ---------------------------------------- | -------------------------------------------------------------- |
|
|
135
|
+
| `parseOBF(json: string): OBFBoard` | Parse a JSON string into a validated board. Handles UTF-8 BOM. |
|
|
136
|
+
| `validateOBF(data: unknown): OBFBoard` | Validate an unknown value against the board schema. |
|
|
137
|
+
| `stringifyOBF(board: OBFBoard): string` | Serialize a board to a formatted JSON string. |
|
|
138
|
+
| `loadOBF(file: File): Promise<OBFBoard>` | Read and parse a board from a `File` object. |
|
|
139
|
+
|
|
140
|
+
### OBZ functions
|
|
141
|
+
|
|
142
|
+
| Signature | Description |
|
|
143
|
+
| -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
|
|
144
|
+
| `extractOBZ(buffer: ArrayBuffer): Promise<ParsedOBZ>` | Extract boards and files from an OBZ archive buffer. |
|
|
145
|
+
| `loadOBZ(file: File): Promise<ParsedOBZ>` | Read and extract an OBZ package from a `File` object. |
|
|
146
|
+
| `createOBZ(boards: OBFBoard[], rootBoardId: string, resources?: Map<string, Uint8Array>): Promise<Blob>` | Create an OBZ archive from boards and optional resources. |
|
|
147
|
+
| `parseManifest(json: string): OBFManifest` | Parse and validate a `manifest.json` string. |
|
|
148
|
+
|
|
149
|
+
### ZIP utilities
|
|
150
|
+
|
|
151
|
+
| Signature | Description |
|
|
152
|
+
| -------------------------------------------------------------- | ------------------------------------------------------- |
|
|
153
|
+
| `zip(files): Promise<Uint8Array>` | Create a ZIP archive from a map of paths to content. |
|
|
154
|
+
| `unzip(buffer: ArrayBuffer): Promise<Map<string, Uint8Array>>` | Extract files from a ZIP archive. |
|
|
155
|
+
| `isZip(buffer: ArrayBuffer): boolean` | Check whether a buffer starts with the ZIP magic bytes. |
|
|
156
|
+
|
|
157
|
+
### Schemas & types
|
|
158
|
+
|
|
159
|
+
<details>
|
|
160
|
+
<summary><strong>All exported schemas & types</strong></summary>
|
|
161
|
+
|
|
162
|
+
| Schema | Type | Description |
|
|
163
|
+
| --------------------------- | --------------------- | ----------------------------------------------- |
|
|
164
|
+
| `OBFBoardSchema` | `OBFBoard` | Root board object |
|
|
165
|
+
| `OBFButtonSchema` | `OBFButton` | Button with label, action, colors, positioning |
|
|
166
|
+
| `OBFGridSchema` | `OBFGrid` | Grid layout (rows, columns, order) |
|
|
167
|
+
| `OBFImageSchema` | `OBFImage` | Image resource with dimensions and symbol info |
|
|
168
|
+
| `OBFSoundSchema` | `OBFSound` | Sound resource |
|
|
169
|
+
| `OBFMediaSchema` | `OBFMedia` | Common media base (shared by images & sounds) |
|
|
170
|
+
| `OBFLicenseSchema` | `OBFLicense` | Licensing and attribution |
|
|
171
|
+
| `OBFManifestSchema` | `OBFManifest` | OBZ manifest (`manifest.json`) |
|
|
172
|
+
| `OBFLoadBoardSchema` | `OBFLoadBoard` | Reference to another board |
|
|
173
|
+
| `OBFButtonActionSchema` | `OBFButtonAction` | Spelling or specialty action |
|
|
174
|
+
| `OBFSpellingActionSchema` | `OBFSpellingAction` | Spelling action (`+a`, `+oo`, …) |
|
|
175
|
+
| `OBFSpecialtyActionSchema` | `OBFSpecialtyAction` | Specialty action (`:clear`, `:home`, …) |
|
|
176
|
+
| `OBFIDSchema` | `OBFID` | Unique identifier (string, coerced from number) |
|
|
177
|
+
| `OBFFormatVersionSchema` | `OBFFormatVersion` | Format version string |
|
|
178
|
+
| `OBFLocaleCodeSchema` | `OBFLocaleCode` | BCP 47 locale code |
|
|
179
|
+
| `OBFLocalizedStringsSchema` | `OBFLocalizedStrings` | Key-value string translations |
|
|
180
|
+
| `OBFStringsSchema` | `OBFStrings` | Multi-locale string map |
|
|
181
|
+
| `OBFSymbolInfoSchema` | `OBFSymbolInfo` | Proprietary symbol set reference |
|
|
182
|
+
|
|
183
|
+
</details>
|
|
184
|
+
|
|
185
|
+
## Spec notes
|
|
186
|
+
|
|
187
|
+
This library targets the **`open-board-0.1`** format version. Key behaviors:
|
|
188
|
+
|
|
189
|
+
- **ID coercion** — Numeric IDs (common in real-world files) are automatically coerced to strings, per the spec's parsing guidelines.
|
|
190
|
+
- **Media resolution order** — When an image or sound has multiple references, resolve in order: `data` → `path` → `url` → `symbol`.
|
|
191
|
+
- **Grid validation** — The `order` array must have exactly `rows` sub-arrays, each with exactly `columns` entries.
|
|
192
|
+
- **Extensions** — Properties prefixed with `ext_` are passed through. The schemas rely on Zod's default object parsing behavior, which preserves unrecognized keys — so custom `ext_` fields survive parsing and serialization without throwing validation errors.
|
|
193
|
+
|
|
194
|
+
## Development
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
npm install # Install dependencies
|
|
198
|
+
npm test # Run tests (vitest)
|
|
199
|
+
npm run build # Build for production (tsdown)
|
|
200
|
+
npm run typecheck # Type-check without emitting
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Related
|
|
204
|
+
|
|
205
|
+
- [Open Board Format specification](https://www.openboardformat.org/docs) — Official standard and format documentation
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
[MIT](LICENSE) © Shay Cojocaru
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
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 identifier as a string. Must be a non-empty string or number (coerced to 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
|
+
* Mapping of string keys to localized string values.
|
|
30
|
+
*/
|
|
31
|
+
declare const OBFLocalizedStringsSchema: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
32
|
+
type OBFLocalizedStrings = z.infer<typeof OBFLocalizedStringsSchema>;
|
|
33
|
+
/**
|
|
34
|
+
* String translations for multiple locales.
|
|
35
|
+
*/
|
|
36
|
+
declare const OBFStringsSchema: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
37
|
+
type OBFStrings = z.infer<typeof OBFStringsSchema>;
|
|
38
|
+
/**
|
|
39
|
+
* Represents custom actions for spelling.
|
|
40
|
+
* Prefixed with '+' followed by the text to append.
|
|
41
|
+
*/
|
|
42
|
+
declare const OBFSpellingActionSchema: z.ZodString;
|
|
43
|
+
type OBFSpellingAction = z.infer<typeof OBFSpellingActionSchema>;
|
|
44
|
+
/**
|
|
45
|
+
* Represents specialty actions.
|
|
46
|
+
* Standard actions are prefixed with ':'.
|
|
47
|
+
* Custom actions start with ':ext_'.
|
|
48
|
+
*/
|
|
49
|
+
declare const OBFSpecialtyActionSchema: z.ZodString;
|
|
50
|
+
type OBFSpecialtyAction = z.infer<typeof OBFSpecialtyActionSchema>;
|
|
51
|
+
/**
|
|
52
|
+
* Possible actions associated with a button.
|
|
53
|
+
*/
|
|
54
|
+
declare const OBFButtonActionSchema: z.ZodUnion<readonly [z.ZodString, z.ZodString]>;
|
|
55
|
+
type OBFButtonAction = z.infer<typeof OBFButtonActionSchema>;
|
|
56
|
+
/**
|
|
57
|
+
* Licensing information 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.$strip>;
|
|
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.$strip>>;
|
|
91
|
+
}, z.core.$strip>;
|
|
92
|
+
type OBFMedia = z.infer<typeof OBFMediaSchema>;
|
|
93
|
+
/**
|
|
94
|
+
* Information about a symbol from a proprietary symbol set.
|
|
95
|
+
*/
|
|
96
|
+
declare const OBFSymbolInfoSchema: z.ZodObject<{
|
|
97
|
+
set: z.ZodString;
|
|
98
|
+
filename: z.ZodString;
|
|
99
|
+
}, z.core.$strip>;
|
|
100
|
+
type OBFSymbolInfo = z.infer<typeof OBFSymbolInfoSchema>;
|
|
101
|
+
/**
|
|
102
|
+
* Represents an image resource.
|
|
103
|
+
*
|
|
104
|
+
* When resolving the image, if multiple references are provided, they should be used in the following order:
|
|
105
|
+
* 1. data
|
|
106
|
+
* 2. path
|
|
107
|
+
* 3. url
|
|
108
|
+
* 4. symbol
|
|
109
|
+
*/
|
|
110
|
+
declare const OBFImageSchema: z.ZodIntersection<z.ZodObject<{
|
|
111
|
+
id: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
|
|
112
|
+
data: z.ZodOptional<z.ZodString>;
|
|
113
|
+
path: z.ZodOptional<z.ZodString>;
|
|
114
|
+
data_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
115
|
+
url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
116
|
+
content_type: z.ZodOptional<z.ZodString>;
|
|
117
|
+
license: z.ZodOptional<z.ZodObject<{
|
|
118
|
+
type: z.ZodString;
|
|
119
|
+
copyright_notice_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
120
|
+
source_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
121
|
+
author_name: z.ZodOptional<z.ZodString>;
|
|
122
|
+
author_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
123
|
+
author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
124
|
+
}, z.core.$strip>>;
|
|
125
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
126
|
+
symbol: z.ZodOptional<z.ZodObject<{
|
|
127
|
+
set: z.ZodString;
|
|
128
|
+
filename: z.ZodString;
|
|
129
|
+
}, z.core.$strip>>;
|
|
130
|
+
width: z.ZodOptional<z.ZodNumber>;
|
|
131
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
132
|
+
}, z.core.$strip>>;
|
|
133
|
+
type OBFImage = z.infer<typeof OBFImageSchema>;
|
|
134
|
+
/**
|
|
135
|
+
* Represents a sound resource.
|
|
136
|
+
*/
|
|
137
|
+
declare const OBFSoundSchema: z.ZodObject<{
|
|
138
|
+
id: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
|
|
139
|
+
data: z.ZodOptional<z.ZodString>;
|
|
140
|
+
path: z.ZodOptional<z.ZodString>;
|
|
141
|
+
data_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
142
|
+
url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
143
|
+
content_type: z.ZodOptional<z.ZodString>;
|
|
144
|
+
license: z.ZodOptional<z.ZodObject<{
|
|
145
|
+
type: z.ZodString;
|
|
146
|
+
copyright_notice_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
147
|
+
source_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
148
|
+
author_name: z.ZodOptional<z.ZodString>;
|
|
149
|
+
author_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
150
|
+
author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
151
|
+
}, z.core.$strip>>;
|
|
152
|
+
}, z.core.$strip>;
|
|
153
|
+
type OBFSound = z.infer<typeof OBFSoundSchema>;
|
|
154
|
+
/**
|
|
155
|
+
* Information needed to load another board.
|
|
156
|
+
*/
|
|
157
|
+
declare const OBFLoadBoardSchema: z.ZodObject<{
|
|
158
|
+
id: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string | undefined, string | number>>>;
|
|
159
|
+
name: z.ZodOptional<z.ZodString>;
|
|
160
|
+
data_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
161
|
+
url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
162
|
+
path: z.ZodOptional<z.ZodString>;
|
|
163
|
+
}, z.core.$strip>;
|
|
164
|
+
type OBFLoadBoard = z.infer<typeof OBFLoadBoardSchema>;
|
|
165
|
+
/**
|
|
166
|
+
* Represents a button on the board.
|
|
167
|
+
*/
|
|
168
|
+
declare const OBFButtonSchema: z.ZodObject<{
|
|
169
|
+
id: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
|
|
170
|
+
label: z.ZodOptional<z.ZodString>;
|
|
171
|
+
vocalization: z.ZodOptional<z.ZodString>;
|
|
172
|
+
image_id: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string | undefined, string | number>>>;
|
|
173
|
+
sound_id: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string | undefined, string | number>>>;
|
|
174
|
+
action: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodString]>>;
|
|
175
|
+
actions: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodString]>>>;
|
|
176
|
+
load_board: z.ZodOptional<z.ZodObject<{
|
|
177
|
+
id: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string | undefined, string | number>>>;
|
|
178
|
+
name: z.ZodOptional<z.ZodString>;
|
|
179
|
+
data_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
180
|
+
url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
181
|
+
path: z.ZodOptional<z.ZodString>;
|
|
182
|
+
}, z.core.$strip>>;
|
|
183
|
+
background_color: z.ZodOptional<z.ZodString>;
|
|
184
|
+
border_color: z.ZodOptional<z.ZodString>;
|
|
185
|
+
top: z.ZodOptional<z.ZodNumber>;
|
|
186
|
+
left: z.ZodOptional<z.ZodNumber>;
|
|
187
|
+
width: z.ZodOptional<z.ZodNumber>;
|
|
188
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
189
|
+
}, z.core.$strip>;
|
|
190
|
+
type OBFButton = z.infer<typeof OBFButtonSchema>;
|
|
191
|
+
/**
|
|
192
|
+
* Grid layout information for the board.
|
|
193
|
+
*/
|
|
194
|
+
declare const OBFGridSchema: z.ZodObject<{
|
|
195
|
+
rows: z.ZodNumber;
|
|
196
|
+
columns: z.ZodNumber;
|
|
197
|
+
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]>>>;
|
|
198
|
+
}, z.core.$strip>;
|
|
199
|
+
type OBFGrid = z.infer<typeof OBFGridSchema>;
|
|
200
|
+
/**
|
|
201
|
+
* Represents the root object of an OBF file, defining the structure and layout of a board.
|
|
202
|
+
*/
|
|
203
|
+
declare const OBFBoardSchema: z.ZodObject<{
|
|
204
|
+
format: z.ZodString;
|
|
205
|
+
id: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
|
|
206
|
+
locale: z.ZodOptional<z.ZodString>;
|
|
207
|
+
buttons: z.ZodArray<z.ZodObject<{
|
|
208
|
+
id: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
|
|
209
|
+
label: z.ZodOptional<z.ZodString>;
|
|
210
|
+
vocalization: z.ZodOptional<z.ZodString>;
|
|
211
|
+
image_id: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string | undefined, string | number>>>;
|
|
212
|
+
sound_id: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string | undefined, string | number>>>;
|
|
213
|
+
action: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodString]>>;
|
|
214
|
+
actions: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodString]>>>;
|
|
215
|
+
load_board: z.ZodOptional<z.ZodObject<{
|
|
216
|
+
id: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string | undefined, string | number>>>;
|
|
217
|
+
name: z.ZodOptional<z.ZodString>;
|
|
218
|
+
data_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
219
|
+
url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
220
|
+
path: z.ZodOptional<z.ZodString>;
|
|
221
|
+
}, z.core.$strip>>;
|
|
222
|
+
background_color: z.ZodOptional<z.ZodString>;
|
|
223
|
+
border_color: z.ZodOptional<z.ZodString>;
|
|
224
|
+
top: z.ZodOptional<z.ZodNumber>;
|
|
225
|
+
left: z.ZodOptional<z.ZodNumber>;
|
|
226
|
+
width: z.ZodOptional<z.ZodNumber>;
|
|
227
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
228
|
+
}, z.core.$strip>>;
|
|
229
|
+
url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
230
|
+
name: z.ZodOptional<z.ZodString>;
|
|
231
|
+
description_html: z.ZodOptional<z.ZodString>;
|
|
232
|
+
grid: z.ZodObject<{
|
|
233
|
+
rows: z.ZodNumber;
|
|
234
|
+
columns: z.ZodNumber;
|
|
235
|
+
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]>>>;
|
|
236
|
+
}, z.core.$strip>;
|
|
237
|
+
images: z.ZodOptional<z.ZodArray<z.ZodIntersection<z.ZodObject<{
|
|
238
|
+
id: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
|
|
239
|
+
data: z.ZodOptional<z.ZodString>;
|
|
240
|
+
path: z.ZodOptional<z.ZodString>;
|
|
241
|
+
data_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
242
|
+
url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
243
|
+
content_type: z.ZodOptional<z.ZodString>;
|
|
244
|
+
license: z.ZodOptional<z.ZodObject<{
|
|
245
|
+
type: z.ZodString;
|
|
246
|
+
copyright_notice_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
247
|
+
source_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
248
|
+
author_name: z.ZodOptional<z.ZodString>;
|
|
249
|
+
author_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
250
|
+
author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
251
|
+
}, z.core.$strip>>;
|
|
252
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
253
|
+
symbol: z.ZodOptional<z.ZodObject<{
|
|
254
|
+
set: z.ZodString;
|
|
255
|
+
filename: z.ZodString;
|
|
256
|
+
}, z.core.$strip>>;
|
|
257
|
+
width: z.ZodOptional<z.ZodNumber>;
|
|
258
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
259
|
+
}, z.core.$strip>>>>;
|
|
260
|
+
sounds: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
261
|
+
id: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<string, string | number>>, z.ZodString>;
|
|
262
|
+
data: z.ZodOptional<z.ZodString>;
|
|
263
|
+
path: z.ZodOptional<z.ZodString>;
|
|
264
|
+
data_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
265
|
+
url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
266
|
+
content_type: z.ZodOptional<z.ZodString>;
|
|
267
|
+
license: z.ZodOptional<z.ZodObject<{
|
|
268
|
+
type: z.ZodString;
|
|
269
|
+
copyright_notice_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
270
|
+
source_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
271
|
+
author_name: z.ZodOptional<z.ZodString>;
|
|
272
|
+
author_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
273
|
+
author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
274
|
+
}, z.core.$strip>>;
|
|
275
|
+
}, z.core.$strip>>>;
|
|
276
|
+
license: z.ZodOptional<z.ZodObject<{
|
|
277
|
+
type: z.ZodString;
|
|
278
|
+
copyright_notice_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
279
|
+
source_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
280
|
+
author_name: z.ZodOptional<z.ZodString>;
|
|
281
|
+
author_url: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodURL, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
282
|
+
author_email: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodEmail, z.ZodLiteral<"">]>, z.ZodTransform<string | undefined, string>>>;
|
|
283
|
+
}, z.core.$strip>>;
|
|
284
|
+
strings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>>;
|
|
285
|
+
}, z.core.$strip>;
|
|
286
|
+
type OBFBoard = z.infer<typeof OBFBoardSchema>;
|
|
287
|
+
/**
|
|
288
|
+
* Manifest file in an .obz package.
|
|
289
|
+
*/
|
|
290
|
+
declare const OBFManifestSchema: z.ZodObject<{
|
|
291
|
+
format: z.ZodString;
|
|
292
|
+
root: z.ZodString;
|
|
293
|
+
paths: z.ZodObject<{
|
|
294
|
+
boards: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
295
|
+
images: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
296
|
+
sounds: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
297
|
+
}, z.core.$strip>;
|
|
298
|
+
}, z.core.$strip>;
|
|
299
|
+
type OBFManifest = z.infer<typeof OBFManifestSchema>;
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region src/obf.d.ts
|
|
302
|
+
declare function parseOBF(json: string): OBFBoard;
|
|
303
|
+
declare function loadOBF(file: File): Promise<OBFBoard>;
|
|
304
|
+
declare function validateOBF(data: unknown): OBFBoard;
|
|
305
|
+
declare function stringifyOBF(board: OBFBoard): string;
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region src/obz.d.ts
|
|
308
|
+
interface ParsedOBZ {
|
|
309
|
+
manifest: OBFManifest;
|
|
310
|
+
boards: Map<string, OBFBoard>;
|
|
311
|
+
files: Map<string, Uint8Array>;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Load OBZ package from File
|
|
315
|
+
* @param file - File object
|
|
316
|
+
* @returns Parsed OBZ with manifest, boards, and files
|
|
317
|
+
*/
|
|
318
|
+
declare function loadOBZ(file: File): Promise<ParsedOBZ>;
|
|
319
|
+
/**
|
|
320
|
+
* Extract OBZ package from ArrayBuffer
|
|
321
|
+
* @param buffer - OBZ file as ArrayBuffer
|
|
322
|
+
* @returns Parsed OBZ with manifest, boards, and files
|
|
323
|
+
*/
|
|
324
|
+
declare function extractOBZ(buffer: ArrayBuffer): Promise<ParsedOBZ>;
|
|
325
|
+
/**
|
|
326
|
+
* Parse manifest.json from OBZ package
|
|
327
|
+
* @param json - Manifest JSON string
|
|
328
|
+
* @returns Validated Manifest object
|
|
329
|
+
*/
|
|
330
|
+
declare function parseManifest(json: string): OBFManifest;
|
|
331
|
+
/**
|
|
332
|
+
* Create OBZ package from boards and resources
|
|
333
|
+
* @param boards - Array of Board objects
|
|
334
|
+
* @param rootBoardId - ID of the root board
|
|
335
|
+
* @param resources - Optional map of resource paths to buffers
|
|
336
|
+
* @returns OBZ package as Blob
|
|
337
|
+
*/
|
|
338
|
+
declare function createOBZ(boards: OBFBoard[], rootBoardId: string, resources?: Map<string, Uint8Array | ArrayBuffer>): Promise<Blob>;
|
|
339
|
+
//#endregion
|
|
340
|
+
//#region src/zip.d.ts
|
|
341
|
+
declare function unzip(buffer: ArrayBuffer): Promise<Map<string, Uint8Array>>;
|
|
342
|
+
declare function zip(files: Map<string, Uint8Array | ArrayBuffer>): Promise<Uint8Array>;
|
|
343
|
+
declare function isZip(buffer: ArrayBuffer): boolean;
|
|
344
|
+
//#endregion
|
|
345
|
+
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,315 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { unzip as unzip$1, zip as zip$1 } from "fflate";
|
|
3
|
+
|
|
4
|
+
//#region src/schema.ts
|
|
5
|
+
/**
|
|
6
|
+
* Open Board Format (OBF) Zod Schemas
|
|
7
|
+
*
|
|
8
|
+
* These schemas represent the Open Board Format, designed for sharing communication boards and board sets
|
|
9
|
+
* between Augmentative and Alternative Communication (AAC) applications.
|
|
10
|
+
*
|
|
11
|
+
* Official OBF specification: https://www.openboardformat.org/docs
|
|
12
|
+
*
|
|
13
|
+
* @author Shay Cojocaru
|
|
14
|
+
* @license MIT
|
|
15
|
+
*/
|
|
16
|
+
/** Optional URL that treats empty strings as undefined. */
|
|
17
|
+
const OBFOptionalUrlSchema = z.union([z.url(), z.literal("")]).transform((val) => val === "" ? void 0 : val).optional();
|
|
18
|
+
/** Optional email that treats empty strings as undefined. */
|
|
19
|
+
const OBFOptionalEmailSchema = z.union([z.email(), z.literal("")]).transform((val) => val === "" ? void 0 : val).optional();
|
|
20
|
+
/** Optional ID that treats empty strings as undefined. */
|
|
21
|
+
const OBFOptionalIDSchema = z.union([z.string(), z.number()]).transform((val) => {
|
|
22
|
+
const str = String(val);
|
|
23
|
+
return str === "" ? void 0 : str;
|
|
24
|
+
}).optional();
|
|
25
|
+
/** Unique identifier as a string. Must be a non-empty string or number (coerced to string). */
|
|
26
|
+
const OBFIDSchema = z.union([z.string(), z.number()]).transform((val) => String(val)).pipe(z.string().min(1));
|
|
27
|
+
/**
|
|
28
|
+
* Format version of the Open Board Format, e.g., 'open-board-0.1'.
|
|
29
|
+
*/
|
|
30
|
+
const OBFFormatVersionSchema = z.string().regex(/^open-board-.+$/);
|
|
31
|
+
/**
|
|
32
|
+
* Locale code as per BCP 47 language tags, e.g., 'en', 'en-US', 'fr-CA'.
|
|
33
|
+
*/
|
|
34
|
+
const OBFLocaleCodeSchema = z.string();
|
|
35
|
+
/**
|
|
36
|
+
* Mapping of string keys to localized string values.
|
|
37
|
+
*/
|
|
38
|
+
const OBFLocalizedStringsSchema = z.record(z.string(), z.string());
|
|
39
|
+
/**
|
|
40
|
+
* String translations for multiple locales.
|
|
41
|
+
*/
|
|
42
|
+
const OBFStringsSchema = z.record(z.string(), OBFLocalizedStringsSchema);
|
|
43
|
+
/**
|
|
44
|
+
* Represents custom actions for spelling.
|
|
45
|
+
* Prefixed with '+' followed by the text to append.
|
|
46
|
+
*/
|
|
47
|
+
const OBFSpellingActionSchema = z.string().regex(/^\+.+$/);
|
|
48
|
+
/**
|
|
49
|
+
* Represents specialty actions.
|
|
50
|
+
* Standard actions are prefixed with ':'.
|
|
51
|
+
* Custom actions start with ':ext_'.
|
|
52
|
+
*/
|
|
53
|
+
const OBFSpecialtyActionSchema = z.string().regex(/^:[a-z][a-z0-9_-]*$/i);
|
|
54
|
+
/**
|
|
55
|
+
* Possible actions associated with a button.
|
|
56
|
+
*/
|
|
57
|
+
const OBFButtonActionSchema = z.union([OBFSpellingActionSchema, OBFSpecialtyActionSchema]);
|
|
58
|
+
/**
|
|
59
|
+
* Licensing information for a resource.
|
|
60
|
+
*/
|
|
61
|
+
const OBFLicenseSchema = z.object({
|
|
62
|
+
type: z.string(),
|
|
63
|
+
copyright_notice_url: OBFOptionalUrlSchema,
|
|
64
|
+
source_url: OBFOptionalUrlSchema,
|
|
65
|
+
author_name: z.string().optional(),
|
|
66
|
+
author_url: OBFOptionalUrlSchema,
|
|
67
|
+
author_email: OBFOptionalEmailSchema
|
|
68
|
+
});
|
|
69
|
+
/**
|
|
70
|
+
* Common properties for media resources (images and sounds).
|
|
71
|
+
*
|
|
72
|
+
* When multiple references are provided, they should be used in the following order:
|
|
73
|
+
* 1. data
|
|
74
|
+
* 2. path
|
|
75
|
+
* 3. url
|
|
76
|
+
*/
|
|
77
|
+
const OBFMediaSchema = z.object({
|
|
78
|
+
id: OBFIDSchema,
|
|
79
|
+
data: z.string().optional(),
|
|
80
|
+
path: z.string().optional(),
|
|
81
|
+
data_url: OBFOptionalUrlSchema,
|
|
82
|
+
url: OBFOptionalUrlSchema,
|
|
83
|
+
content_type: z.string().optional(),
|
|
84
|
+
license: OBFLicenseSchema.optional()
|
|
85
|
+
});
|
|
86
|
+
/**
|
|
87
|
+
* Information about a symbol from a proprietary symbol set.
|
|
88
|
+
*/
|
|
89
|
+
const OBFSymbolInfoSchema = z.object({
|
|
90
|
+
set: z.string(),
|
|
91
|
+
filename: z.string()
|
|
92
|
+
});
|
|
93
|
+
/**
|
|
94
|
+
* Represents an image resource.
|
|
95
|
+
*
|
|
96
|
+
* When resolving the image, if multiple references are provided, they should be used in the following order:
|
|
97
|
+
* 1. data
|
|
98
|
+
* 2. path
|
|
99
|
+
* 3. url
|
|
100
|
+
* 4. symbol
|
|
101
|
+
*/
|
|
102
|
+
const OBFImageSchema = OBFMediaSchema.and(z.object({
|
|
103
|
+
symbol: OBFSymbolInfoSchema.optional(),
|
|
104
|
+
width: z.number().optional(),
|
|
105
|
+
height: z.number().optional()
|
|
106
|
+
}));
|
|
107
|
+
/**
|
|
108
|
+
* Represents a sound resource.
|
|
109
|
+
*/
|
|
110
|
+
const OBFSoundSchema = OBFMediaSchema;
|
|
111
|
+
/**
|
|
112
|
+
* Information needed to load another board.
|
|
113
|
+
*/
|
|
114
|
+
const OBFLoadBoardSchema = z.object({
|
|
115
|
+
id: OBFOptionalIDSchema,
|
|
116
|
+
name: z.string().optional(),
|
|
117
|
+
data_url: OBFOptionalUrlSchema,
|
|
118
|
+
url: OBFOptionalUrlSchema,
|
|
119
|
+
path: z.string().optional()
|
|
120
|
+
});
|
|
121
|
+
/**
|
|
122
|
+
* Represents a button on the board.
|
|
123
|
+
*/
|
|
124
|
+
const OBFButtonSchema = z.object({
|
|
125
|
+
id: OBFIDSchema,
|
|
126
|
+
label: z.string().optional(),
|
|
127
|
+
vocalization: z.string().optional(),
|
|
128
|
+
image_id: OBFOptionalIDSchema,
|
|
129
|
+
sound_id: OBFOptionalIDSchema,
|
|
130
|
+
action: OBFButtonActionSchema.optional(),
|
|
131
|
+
actions: z.array(OBFButtonActionSchema).optional(),
|
|
132
|
+
load_board: OBFLoadBoardSchema.optional(),
|
|
133
|
+
background_color: z.string().optional(),
|
|
134
|
+
border_color: z.string().optional(),
|
|
135
|
+
top: z.number().min(0).max(1).optional(),
|
|
136
|
+
left: z.number().min(0).max(1).optional(),
|
|
137
|
+
width: z.number().min(0).max(1).optional(),
|
|
138
|
+
height: z.number().min(0).max(1).optional()
|
|
139
|
+
});
|
|
140
|
+
/**
|
|
141
|
+
* Grid layout information for the board.
|
|
142
|
+
*/
|
|
143
|
+
const OBFGridSchema = z.object({
|
|
144
|
+
rows: z.number().int().min(1),
|
|
145
|
+
columns: z.number().int().min(1),
|
|
146
|
+
order: z.array(z.array(z.union([OBFIDSchema, z.null()])))
|
|
147
|
+
}).refine((g) => g.order.length === g.rows, { message: "Grid order length must match rows" }).refine((g) => g.order.every((row) => row.length === g.columns), { message: "Each grid row must have length equal to columns" });
|
|
148
|
+
/**
|
|
149
|
+
* Represents the root object of an OBF file, defining the structure and layout of a board.
|
|
150
|
+
*/
|
|
151
|
+
const OBFBoardSchema = z.object({
|
|
152
|
+
format: OBFFormatVersionSchema,
|
|
153
|
+
id: OBFIDSchema,
|
|
154
|
+
locale: OBFLocaleCodeSchema.optional(),
|
|
155
|
+
buttons: z.array(OBFButtonSchema),
|
|
156
|
+
url: OBFOptionalUrlSchema,
|
|
157
|
+
name: z.string().optional(),
|
|
158
|
+
description_html: z.string().optional(),
|
|
159
|
+
grid: OBFGridSchema,
|
|
160
|
+
images: z.array(OBFImageSchema).optional(),
|
|
161
|
+
sounds: z.array(OBFSoundSchema).optional(),
|
|
162
|
+
license: OBFLicenseSchema.optional(),
|
|
163
|
+
strings: OBFStringsSchema.optional()
|
|
164
|
+
});
|
|
165
|
+
/**
|
|
166
|
+
* Manifest file in an .obz package.
|
|
167
|
+
*/
|
|
168
|
+
const OBFManifestSchema = z.object({
|
|
169
|
+
format: OBFFormatVersionSchema,
|
|
170
|
+
root: z.string(),
|
|
171
|
+
paths: z.object({
|
|
172
|
+
boards: z.record(z.string(), z.string()),
|
|
173
|
+
images: z.record(z.string(), z.string()),
|
|
174
|
+
sounds: z.record(z.string(), z.string()).optional()
|
|
175
|
+
})
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/obf.ts
|
|
180
|
+
const UTF8_BOM = "";
|
|
181
|
+
function parseOBF(json) {
|
|
182
|
+
const trimmed = json.startsWith(UTF8_BOM) ? json.slice(1) : json;
|
|
183
|
+
let data;
|
|
184
|
+
try {
|
|
185
|
+
data = JSON.parse(trimmed);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
throw new Error(`Invalid OBF: JSON parse failed${error?.message ? ` — ${error.message}` : ""}`);
|
|
188
|
+
}
|
|
189
|
+
return validateOBF(data);
|
|
190
|
+
}
|
|
191
|
+
async function loadOBF(file) {
|
|
192
|
+
return parseOBF(await file.text());
|
|
193
|
+
}
|
|
194
|
+
function validateOBF(data) {
|
|
195
|
+
const result = OBFBoardSchema.safeParse(data);
|
|
196
|
+
if (!result.success) throw new Error(`Invalid OBF: ${result.error.message}`);
|
|
197
|
+
return result.data;
|
|
198
|
+
}
|
|
199
|
+
function stringifyOBF(board) {
|
|
200
|
+
return JSON.stringify(board, null, 2);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/zip.ts
|
|
205
|
+
function unzip(buffer) {
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
unzip$1(new Uint8Array(buffer), (error, result) => {
|
|
208
|
+
if (error) {
|
|
209
|
+
reject(/* @__PURE__ */ new Error(`Failed to unzip: ${error.message ?? String(error)}`));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const files = /* @__PURE__ */ new Map();
|
|
213
|
+
for (const [path, bytes] of Object.entries(result)) files.set(path, bytes);
|
|
214
|
+
resolve(files);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function zip(files) {
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
const input = {};
|
|
221
|
+
for (const [path, content] of files) input[path] = content instanceof Uint8Array ? content : new Uint8Array(content);
|
|
222
|
+
zip$1(input, { level: 6 }, (error, result) => {
|
|
223
|
+
if (error) {
|
|
224
|
+
reject(/* @__PURE__ */ new Error(`Failed to zip: ${error.message ?? String(error)}`));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
resolve(result);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
function isZip(buffer) {
|
|
232
|
+
const view = new Uint8Array(buffer);
|
|
233
|
+
return view.length >= 2 && view[0] === 80 && view[1] === 75;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/obz.ts
|
|
238
|
+
/**
|
|
239
|
+
* Load OBZ package from File
|
|
240
|
+
* @param file - File object
|
|
241
|
+
* @returns Parsed OBZ with manifest, boards, and files
|
|
242
|
+
*/
|
|
243
|
+
async function loadOBZ(file) {
|
|
244
|
+
return extractOBZ(await file.arrayBuffer());
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Extract OBZ package from ArrayBuffer
|
|
248
|
+
* @param buffer - OBZ file as ArrayBuffer
|
|
249
|
+
* @returns Parsed OBZ with manifest, boards, and files
|
|
250
|
+
*/
|
|
251
|
+
async function extractOBZ(buffer) {
|
|
252
|
+
if (!isZip(buffer)) throw new Error("Invalid OBZ: not a ZIP file");
|
|
253
|
+
const files = await unzip(buffer);
|
|
254
|
+
const manifestBuffer = files.get("manifest.json");
|
|
255
|
+
if (!manifestBuffer) throw new Error("Invalid OBZ: missing manifest.json");
|
|
256
|
+
const manifest = parseManifest(new TextDecoder().decode(manifestBuffer));
|
|
257
|
+
const boards = /* @__PURE__ */ new Map();
|
|
258
|
+
for (const [id, path] of Object.entries(manifest.paths.boards)) {
|
|
259
|
+
const boardBuffer = files.get(path);
|
|
260
|
+
if (!boardBuffer) {
|
|
261
|
+
console.warn(`Board ${id} not found at ${path}`);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const board = parseOBF(new TextDecoder().decode(boardBuffer));
|
|
265
|
+
boards.set(id, board);
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
manifest,
|
|
269
|
+
boards,
|
|
270
|
+
files
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Parse manifest.json from OBZ package
|
|
275
|
+
* @param json - Manifest JSON string
|
|
276
|
+
* @returns Validated Manifest object
|
|
277
|
+
*/
|
|
278
|
+
function parseManifest(json) {
|
|
279
|
+
const data = JSON.parse(json);
|
|
280
|
+
const result = OBFManifestSchema.safeParse(data);
|
|
281
|
+
if (!result.success) throw new Error(`Invalid manifest: ${result.error.message}`);
|
|
282
|
+
return result.data;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Create OBZ package from boards and resources
|
|
286
|
+
* @param boards - Array of Board objects
|
|
287
|
+
* @param rootBoardId - ID of the root board
|
|
288
|
+
* @param resources - Optional map of resource paths to buffers
|
|
289
|
+
* @returns OBZ package as Blob
|
|
290
|
+
*/
|
|
291
|
+
async function createOBZ(boards, rootBoardId, resources) {
|
|
292
|
+
const files = /* @__PURE__ */ new Map();
|
|
293
|
+
const manifest = OBFManifestSchema.parse({
|
|
294
|
+
format: "open-board-0.1",
|
|
295
|
+
root: `boards/${rootBoardId}.obf`,
|
|
296
|
+
paths: {
|
|
297
|
+
boards: Object.fromEntries(boards.map((board) => [board.id, `boards/${board.id}.obf`])),
|
|
298
|
+
images: {},
|
|
299
|
+
sounds: {}
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
const manifestJSON = JSON.stringify(manifest, null, 2);
|
|
303
|
+
files.set("manifest.json", new TextEncoder().encode(manifestJSON).buffer);
|
|
304
|
+
for (const board of boards) {
|
|
305
|
+
const boardJSON = JSON.stringify(board, null, 2);
|
|
306
|
+
const path = `boards/${board.id}.obf`;
|
|
307
|
+
files.set(path, new TextEncoder().encode(boardJSON).buffer);
|
|
308
|
+
}
|
|
309
|
+
if (resources) for (const [path, buffer] of resources.entries()) files.set(path, buffer);
|
|
310
|
+
const zipBuffer = await zip(files);
|
|
311
|
+
return new Blob([new Uint8Array(zipBuffer)], { type: "application/zip" });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
//#endregion
|
|
315
|
+
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,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "open-board-format",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.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.3.0",
|
|
39
|
+
"bumpp": "^10.4.1",
|
|
40
|
+
"tsdown": "^0.20.3",
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"vitest": "^4.0.18"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"fflate": "^0.8.2",
|
|
46
|
+
"zod": "^4.3.6"
|
|
47
|
+
}
|
|
48
|
+
}
|