@textmode/document 0.0.0-stage → 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 +111 -0
- package/README.md +60 -2
- package/dist/constants.d.ts +29 -0
- package/dist/constants.js +29 -0
- package/dist/decode-shapes.d.ts +4 -0
- package/dist/decode-shapes.js +108 -0
- package/dist/decode.d.ts +24 -0
- package/dist/decode.js +88 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +12 -0
- package/dist/parse.d.ts +12 -0
- package/dist/parse.js +24 -0
- package/dist/types/canonical.d.ts +24 -0
- package/dist/types/canonical.js +1 -0
- package/dist/types/decode.d.ts +38 -0
- package/dist/types/decode.js +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.js +1 -0
- package/dist/types/legacy.d.ts +25 -0
- package/dist/types/legacy.js +1 -0
- package/dist/types/wire.d.ts +100 -0
- package/dist/types/wire.js +1 -0
- package/dist/validation.d.ts +37 -0
- package/dist/validation.js +205 -0
- package/docs/consumer-guide.md +197 -0
- package/docs/maintenance.md +56 -0
- package/docs/specification-2.0.md +298 -0
- package/package.json +81 -5
- package/schemas/2.0/schema.d.ts +3 -0
- package/schemas/2.0/schema.json +357 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# Textmode document format 2.0
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
This document defines `textmode.document` version `2.0.0`. The canonical
|
|
6
|
+
specification, schema, and decoder are maintained by `textmode-document` and
|
|
7
|
+
published as `@textmode/document`. The raw schema is available from
|
|
8
|
+
`@textmode/document/schema/2.0` and [the repository](../schemas/2.0/schema.json).
|
|
9
|
+
The schema's `$id` is a stable identifier, not a hosted download URL.
|
|
10
|
+
|
|
11
|
+
The ESM-only package root exposes the decoder and types. Package version `1.0.0`
|
|
12
|
+
is independent of file version `2.0.0`. Test fixtures are private repository data;
|
|
13
|
+
complete public documents appear in the [consumer guide](consumer-guide.md).
|
|
14
|
+
|
|
15
|
+
The format represents editable cell data. It does not represent final rendered
|
|
16
|
+
pixels, post-processing, an editor project, or authored animation.
|
|
17
|
+
|
|
18
|
+
## Encoding
|
|
19
|
+
|
|
20
|
+
A textmode document is a UTF-8 JSON object. Producers should follow the I-JSON
|
|
21
|
+
profile in RFC 7493:
|
|
22
|
+
|
|
23
|
+
- object member names are unique;
|
|
24
|
+
- integers remain within the interoperable safe range;
|
|
25
|
+
- non-finite numbers are not emitted;
|
|
26
|
+
- timestamps use an uppercase, timezone-qualified RFC 3339 representation.
|
|
27
|
+
|
|
28
|
+
These are producer requirements and recommendations. The JavaScript parser uses
|
|
29
|
+
native `JSON.parse`, which does not detect duplicate object keys and retains the
|
|
30
|
+
last value. No duplicate-key detection is claimed by this codec.
|
|
31
|
+
|
|
32
|
+
The media type is `application/json`. `.textmode.json` is a suggested filename
|
|
33
|
+
suffix; existing exporter downloads use `.json` by default. Extensions and
|
|
34
|
+
media types are discovery hints; consumers must route a decoded object using
|
|
35
|
+
its `format` member.
|
|
36
|
+
|
|
37
|
+
## Root discriminator
|
|
38
|
+
|
|
39
|
+
Every v2 document contains:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"format": "textmode.document",
|
|
44
|
+
"formatVersion": "2.0.0",
|
|
45
|
+
"target": "selected"
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`target` is an on-disk shape discriminator:
|
|
50
|
+
|
|
51
|
+
- `selected` contains one cell snapshot in `layer`;
|
|
52
|
+
- `all` contains a layer stack in `layers`.
|
|
53
|
+
|
|
54
|
+
It is not an instruction to an exporter and must not be extended with temporal
|
|
55
|
+
values such as `animation`. Authored animation requires a distinct document or
|
|
56
|
+
container format.
|
|
57
|
+
|
|
58
|
+
## Version compatibility
|
|
59
|
+
|
|
60
|
+
The version is SemVer-shaped for release communication. Version 2 does not
|
|
61
|
+
define reader negotiation or a formal extension mechanism. Consumers therefore
|
|
62
|
+
accept only explicitly tested versions and migrations; they must not assume an
|
|
63
|
+
unknown `2.x` version is compatible.
|
|
64
|
+
|
|
65
|
+
For a supported version, consumers ignore unknown ordinary object members.
|
|
66
|
+
The canonical decoder omits these members from its new output object; it does
|
|
67
|
+
not preserve them for reserialization.
|
|
68
|
+
They reject unknown values of tagged members such as `format`, `target`, and
|
|
69
|
+
`encoding` because those values change interpretation.
|
|
70
|
+
|
|
71
|
+
Version 2 does not define `extensionsUsed`, `extensionsRequired`, extension
|
|
72
|
+
namespacing, or an extension registry. A later revision must define those
|
|
73
|
+
rules before claiming extension compatibility.
|
|
74
|
+
|
|
75
|
+
## Selected cell snapshot
|
|
76
|
+
|
|
77
|
+
The `selected` shape contains:
|
|
78
|
+
|
|
79
|
+
- legacy grid pixel extent in `canvas`;
|
|
80
|
+
- logical grid dimensions and cell metrics in `grid`;
|
|
81
|
+
- an informational source ID and complete cell rows in `layer`.
|
|
82
|
+
|
|
83
|
+
This shape is a cell snapshot, not a lossless layer document. It does not carry
|
|
84
|
+
visibility, opacity, blend mode, offsets, layer rotation, or font identity.
|
|
85
|
+
|
|
86
|
+
## Layer stack
|
|
87
|
+
|
|
88
|
+
The `all` shape contains one or more layers. Each layer includes:
|
|
89
|
+
|
|
90
|
+
- informational ID;
|
|
91
|
+
- visibility and opacity;
|
|
92
|
+
- blend mode name;
|
|
93
|
+
- x/y offsets and z rotation;
|
|
94
|
+
- logical grid and cell metrics;
|
|
95
|
+
- complete cell rows.
|
|
96
|
+
|
|
97
|
+
The order of `layers` is bottom-to-top stack order. The exporter emits the base
|
|
98
|
+
layer first, followed by user layers. There is no fixed format-level layer count;
|
|
99
|
+
the decoder enforces the configurable budget.
|
|
100
|
+
|
|
101
|
+
`id` is informational source identity, scoped to the source snapshot. It is not a
|
|
102
|
+
persistent identity across files. The v2 decoder does not require IDs to be unique;
|
|
103
|
+
consumers must not infer reference semantics from them.
|
|
104
|
+
|
|
105
|
+
Offsets are compositing offsets in pixels. In the textmode.js compositor, positive
|
|
106
|
+
x moves right and positive y moves down. `rotationZ` is clockwise degrees about
|
|
107
|
+
the layer's rectangular center; the center remains the rotation origin when an
|
|
108
|
+
offset is applied. The decoder preserves layer rotation without normalization.
|
|
109
|
+
Opacity is from 0 through 1 and multiplies the layer presentation independently
|
|
110
|
+
of cell alpha. Invisible layers retain their cell data.
|
|
111
|
+
|
|
112
|
+
The textmode.js exporter writes `normal`, `additive`, `multiply`, `screen`,
|
|
113
|
+
`subtract`, `darken`, `lighten`, `overlay`, `softLight`, `hardLight`, `colorDodge`,
|
|
114
|
+
`colorBurn`, `difference`, or `exclusion`. The wire schema and decoder accept any
|
|
115
|
+
nonempty bounded blend-mode name. Consumers must decide whether they support it
|
|
116
|
+
before presentation; unknown names are not silently converted to `normal`.
|
|
117
|
+
|
|
118
|
+
## Canvas field
|
|
119
|
+
|
|
120
|
+
The v2 field named `canvas` is the grid pixel extent, not necessarily the HTML
|
|
121
|
+
canvas or final output surface. Its values obey:
|
|
122
|
+
|
|
123
|
+
```text
|
|
124
|
+
canvas.width = grid.cols × grid.cellWidth
|
|
125
|
+
canvas.height = grid.rows × grid.cellHeight
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
For an `all` document, the root extent is derived from the first layer's grid.
|
|
129
|
+
Consumers must not infer composite placement or clipping from this field.
|
|
130
|
+
|
|
131
|
+
A future major format may replace this legacy name with `gridPixelSize`; v2
|
|
132
|
+
semantics must not be changed in place.
|
|
133
|
+
|
|
134
|
+
## Cell encoding
|
|
135
|
+
|
|
136
|
+
Version 2 defines one encoding:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"encoding": "object-rows-v1",
|
|
141
|
+
"rows": []
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The encoding is dense and row-major:
|
|
146
|
+
|
|
147
|
+
- `rows.length` equals `grid.rows`;
|
|
148
|
+
- every row length equals `grid.cols`;
|
|
149
|
+
- a cell's `x` equals its array index within the row;
|
|
150
|
+
- a cell's `y` equals the row index;
|
|
151
|
+
- every grid coordinate is present, including transparent cells.
|
|
152
|
+
|
|
153
|
+
Consumers reject coordinate disagreement rather than silently repairing or
|
|
154
|
+
reordering cells.
|
|
155
|
+
|
|
156
|
+
## Glyph identity
|
|
157
|
+
|
|
158
|
+
`character` contains a non-empty, bounded, well-formed Unicode glyph string.
|
|
159
|
+
Vector fonts commonly use one Unicode scalar, while tileset maps may use an
|
|
160
|
+
extended grapheme cluster containing combining marks, variation selectors, or
|
|
161
|
+
zero-width joiners. Unpaired surrogates are invalid. The default codec budget
|
|
162
|
+
is 64 Unicode scalar values per glyph string.
|
|
163
|
+
|
|
164
|
+
No Unicode normalization is implied. Consumers preserve the string exactly and
|
|
165
|
+
apply product-specific representability rules after protocol decoding.
|
|
166
|
+
|
|
167
|
+
## Colors
|
|
168
|
+
|
|
169
|
+
Foreground and background colors use either:
|
|
170
|
+
|
|
171
|
+
- `#RRGGBB`, whose alpha is implicitly 255;
|
|
172
|
+
- `#RRGGBBAA`;
|
|
173
|
+
- `{ "r": 0, "g": 0, "b": 0, "a": 255 }` with integer channels from
|
|
174
|
+
0 through 255.
|
|
175
|
+
|
|
176
|
+
The canonical decoder normalizes colors to RGBA channel objects.
|
|
177
|
+
|
|
178
|
+
A cell is visually empty only when both foreground and background alpha are
|
|
179
|
+
zero. A space with a visible background remains meaningful cell data.
|
|
180
|
+
|
|
181
|
+
## Transforms
|
|
182
|
+
|
|
183
|
+
Every cell contains:
|
|
184
|
+
|
|
185
|
+
- `invert`: boolean;
|
|
186
|
+
- `flipX`: boolean;
|
|
187
|
+
- `flipY`: boolean;
|
|
188
|
+
- `rotation`: finite degrees.
|
|
189
|
+
|
|
190
|
+
The canonical decoder normalizes rotation into `[0, 360)`. It therefore
|
|
191
|
+
normalizes both `360` and `-360` to `0` and `-90` to `270`.
|
|
192
|
+
|
|
193
|
+
Consumers must reject unsupported meaningful transforms instead of
|
|
194
|
+
approximating them. In particular, swapping foreground and background is not a
|
|
195
|
+
general replacement for inversion when alpha is present.
|
|
196
|
+
|
|
197
|
+
## Metadata
|
|
198
|
+
|
|
199
|
+
Metadata is optional and informational:
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"createdAt": "2026-08-28T12:00:00.000Z",
|
|
204
|
+
"generator": {
|
|
205
|
+
"name": "textmode.export.js",
|
|
206
|
+
"version": "1.8.1"
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`generator.name` identifies the producer and is not restricted to
|
|
212
|
+
`textmode.export.js`. If metadata is present, `createdAt` must be a real
|
|
213
|
+
calendar date and time in the uppercase, timezone-qualified RFC 3339 profile:
|
|
214
|
+
seconds are required, fractional seconds are optional, and leap seconds are
|
|
215
|
+
not supported in version 2. The decoder preserves valid timestamps as written.
|
|
216
|
+
|
|
217
|
+
Consumers must not use metadata to select executable behavior, fetch resources,
|
|
218
|
+
or bypass document validation.
|
|
219
|
+
|
|
220
|
+
## Safety limits
|
|
221
|
+
|
|
222
|
+
The shared codec enforces structural safety limits and accepts caller-supplied
|
|
223
|
+
budgets. Every override must be a positive safe integer; invalid configuration
|
|
224
|
+
throws `TypeError`. Invalid document data instead returns a typed failure.
|
|
225
|
+
|
|
226
|
+
| Limit | Default | Counting rule |
|
|
227
|
+
| ------------------------- | ---------- | ---------------------------------------------------------- |
|
|
228
|
+
| `maxCells` | 16,777,216 | Cumulative cells across the document. |
|
|
229
|
+
| `maxLayers` | 4,096 | Layers in an `all` document; selected has one snapshot. |
|
|
230
|
+
| `maxLayerIdLength` | 1,024 | UTF-16 code units per identifier. |
|
|
231
|
+
| `maxCharacterLength` | 64 | Unicode scalar values per glyph string. |
|
|
232
|
+
| `maxMetadataStringLength` | 4,096 | UTF-16 code units per metadata string and blend-mode name. |
|
|
233
|
+
|
|
234
|
+
These budgets apply after JSON parsing and do not bound file bytes or parser
|
|
235
|
+
memory. The schema's fixed glyph `maxLength` remains 64 even when an app raises
|
|
236
|
+
the decoder's glyph budget. Product-specific policy belongs to the product. For example,
|
|
237
|
+
`create.textmode.art` may accept fewer cells than another document consumer.
|
|
238
|
+
|
|
239
|
+
Consumers should:
|
|
240
|
+
|
|
241
|
+
- check file bytes before reading;
|
|
242
|
+
- cap total layers and total decoded cells;
|
|
243
|
+
- use checked multiplication for grid dimensions;
|
|
244
|
+
- bound identifiers and metadata strings;
|
|
245
|
+
- parse large files away from latency-sensitive UI work;
|
|
246
|
+
- abort work when the owning import session is cancelled.
|
|
247
|
+
|
|
248
|
+
Documents never authorize URL fetching, font installation, or code execution.
|
|
249
|
+
|
|
250
|
+
## Legacy migration
|
|
251
|
+
|
|
252
|
+
The compatibility decoder recognizes:
|
|
253
|
+
|
|
254
|
+
- `textmode.layer@1.0.0` as a selected cell snapshot;
|
|
255
|
+
- `textmode.layer@1.1.0` as a layer stack.
|
|
256
|
+
|
|
257
|
+
Both migrate to the canonical v2 union and return a migration warning. Callers
|
|
258
|
+
may disable legacy decoding.
|
|
259
|
+
|
|
260
|
+
## JSON Schema and runtime decoder
|
|
261
|
+
|
|
262
|
+
The specification defines meaning and cross-field rules. The JSON Schema describes the portable structure and is suitable for
|
|
263
|
+
documentation and general tooling. The runtime decoder remains authoritative
|
|
264
|
+
for:
|
|
265
|
+
|
|
266
|
+
- cross-field row and grid invariants;
|
|
267
|
+
- grid pixel extent equality;
|
|
268
|
+
- total resource budgets;
|
|
269
|
+
- well-formed and bounded glyph-string validation;
|
|
270
|
+
- rotation and color normalization;
|
|
271
|
+
- legacy migration;
|
|
272
|
+
- stable error codes and JSON paths.
|
|
273
|
+
|
|
274
|
+
## Required fields
|
|
275
|
+
|
|
276
|
+
| Object | Required fields | Optional fields |
|
|
277
|
+
| --------------- | ------------------------------------------------------------------------------------------- | --------------- |
|
|
278
|
+
| Selected root | `format`, `formatVersion`, `target`, `canvas`, `grid`, `layer` | `metadata` |
|
|
279
|
+
| All root | `format`, `formatVersion`, `target`, `canvas`, `layers` | `metadata` |
|
|
280
|
+
| Canvas | `width`, `height` | None |
|
|
281
|
+
| Grid | `cols`, `rows`, `cellWidth`, `cellHeight` | None |
|
|
282
|
+
| Selected layer | `id`, `cells` | None |
|
|
283
|
+
| Stack layer | `id`, `visible`, `opacity`, `blendMode`, `offsetX`, `offsetY`, `rotationZ`, `grid`, `cells` | None |
|
|
284
|
+
| Cell collection | `encoding`, `rows` | None |
|
|
285
|
+
| Cell | `x`, `y`, `character`, `foreground`, `background`, `transform` | None |
|
|
286
|
+
| Transform | `invert`, `flipX`, `flipY`, `rotation` | None |
|
|
287
|
+
| Metadata | `createdAt`, `generator` | None |
|
|
288
|
+
| Generator | `name`, `version` | None |
|
|
289
|
+
|
|
290
|
+
Unknown ordinary members are allowed in all these objects but omitted by the
|
|
291
|
+
canonical decoder. All schema references are local. Timestamp validation needs
|
|
292
|
+
`date-time` format assertions enabled in the selected schema validator. The
|
|
293
|
+
schema alone does not enforce every decoder rule or configurable budget.
|
|
294
|
+
|
|
295
|
+
Version 2 has no font identity or embedded font assets. Glyph strings and cell
|
|
296
|
+
metrics cannot identify an exact font. It also has no editor history, project
|
|
297
|
+
preferences, authored frame timeline, or post-processing representation. An
|
|
298
|
+
importer must apply its own font and supported-feature policy.
|
package/package.json
CHANGED
|
@@ -1,6 +1,82 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@textmode/document",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Portable textmode document format, JSON Schema, types, and dependency-free decoder.",
|
|
5
|
+
"license": "CC0-1.0",
|
|
6
|
+
"author": "humanbydefinition <hello@textmode.art> (https://code.textmode.art)",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"devEngines": {
|
|
10
|
+
"runtime": {
|
|
11
|
+
"name": "node",
|
|
12
|
+
"version": ">=24.0.0",
|
|
13
|
+
"onFail": "error"
|
|
14
|
+
},
|
|
15
|
+
"packageManager": {
|
|
16
|
+
"name": "npm",
|
|
17
|
+
"version": "^11.0.0",
|
|
18
|
+
"onFail": "error"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=24"
|
|
23
|
+
},
|
|
24
|
+
"packageManager": "npm@11.16.0",
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./schema/2.0": {
|
|
33
|
+
"types": "./schemas/2.0/schema.d.ts",
|
|
34
|
+
"default": "./schemas/2.0/schema.json"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist/**/*.js",
|
|
39
|
+
"dist/**/*.d.ts",
|
|
40
|
+
"schemas/2.0/schema.json",
|
|
41
|
+
"schemas/2.0/schema.d.ts",
|
|
42
|
+
"docs/*.md"
|
|
43
|
+
],
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/humanbydefinition/textmode-document.git"
|
|
50
|
+
},
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/humanbydefinition/textmode-document/issues"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://github.com/humanbydefinition/textmode-document#readme",
|
|
55
|
+
"scripts": {
|
|
56
|
+
"clean": "node --input-type=module -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\"",
|
|
57
|
+
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
58
|
+
"format": "textmode-lint format",
|
|
59
|
+
"format:check": "textmode-lint format:check",
|
|
60
|
+
"lint": "textmode-lint lint src tests && node --check scripts/check-package.mjs",
|
|
61
|
+
"lint:markdown": "textmode-lint lint:markdown",
|
|
62
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
63
|
+
"docs:check": "textmode-docs check",
|
|
64
|
+
"docs:build": "textmode-docs build",
|
|
65
|
+
"test": "vitest run",
|
|
66
|
+
"check:package": "node scripts/check-package.mjs",
|
|
67
|
+
"check": "npm run format:check && npm run lint && npm run lint:markdown && npm run typecheck && npm run docs:check && npm test && npm run build && npm run check:package",
|
|
68
|
+
"prepack": "npm run build",
|
|
69
|
+
"prepare": "npm run build"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@textmode/docs": "^0.3.0",
|
|
73
|
+
"@textmode/lint": "^0.1.2",
|
|
74
|
+
"@textmode/tsconfig": "^0.2.2",
|
|
75
|
+
"@types/node": "^26.4.1",
|
|
76
|
+
"ajv": "^8.20.0",
|
|
77
|
+
"ajv-formats": "^3.0.1",
|
|
78
|
+
"typescript": "^6.0.3",
|
|
79
|
+
"vitest": "^5.0.0"
|
|
80
|
+
},
|
|
81
|
+
"prettier": "@textmode/lint/prettier"
|
|
82
|
+
}
|