@sdxc/structured-fields 0.0.0-pre.1
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.md +21 -0
- package/README.md +269 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +71 -0
- package/dist/lib/base64.d.ts +24 -0
- package/dist/lib/base64.js +44 -0
- package/dist/lib/errors.d.ts +35 -0
- package/dist/lib/errors.js +41 -0
- package/dist/lib/parse.d.ts +19 -0
- package/dist/lib/parse.js +475 -0
- package/dist/lib/stringify.d.ts +20 -0
- package/dist/lib/stringify.js +352 -0
- package/dist/lib/types.d.ts +84 -0
- package/dist/lib/types.js +0 -0
- package/dist/lib/values.d.ts +53 -0
- package/dist/lib/values.js +65 -0
- package/dist/schema.d.ts +56 -0
- package/dist/schema.js +181 -0
- package/package.json +25 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { failure, success } from "@sdxc/result";
|
|
2
|
+
import { decodeBase64 } from "./base64.js";
|
|
3
|
+
import { StructuredFieldParseError } from "./errors.js";
|
|
4
|
+
import { Decimal, DisplayString, Token } from "./values.js";
|
|
5
|
+
/** Returned by a step that failed; the reason sits on the cursor. */
|
|
6
|
+
const FAILED = Symbol("failed");
|
|
7
|
+
/** Characters a Token may continue with: `tchar`, `:` and `/`. */
|
|
8
|
+
const TOKEN_CHAR = /^[!#$%&'*+\-.^_`|~0-9A-Za-z:/]$/;
|
|
9
|
+
/** Characters a key may continue with. */
|
|
10
|
+
const KEY_CHAR = /^[a-z0-9_\-.*]$/;
|
|
11
|
+
/** Characters a Byte Sequence's base64 content may use. */
|
|
12
|
+
const BASE64_CONTENT = /^[A-Za-z0-9+/=]*$/;
|
|
13
|
+
/** Anything outside visible ASCII and space, which a field value may not carry. */
|
|
14
|
+
const NON_ASCII = /[^\x20-\x7e\t]/;
|
|
15
|
+
/** Lowercase hex digits, the only form a Display String's percent-escapes may take. */
|
|
16
|
+
const LOWER_HEX = /^[0-9a-f]{2}$/;
|
|
17
|
+
/** Decodes a Display String's octets, refusing invalid UTF-8 and keeping a leading BOM. */
|
|
18
|
+
const UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
19
|
+
/**
|
|
20
|
+
* The parser's position in the field value, and the first failure it met.
|
|
21
|
+
*/
|
|
22
|
+
class Cursor {
|
|
23
|
+
text;
|
|
24
|
+
position = 0;
|
|
25
|
+
error = null;
|
|
26
|
+
/**
|
|
27
|
+
* @param text - The field value, several lines already joined with `,`
|
|
28
|
+
*/
|
|
29
|
+
constructor(text) {
|
|
30
|
+
this.text = text;
|
|
31
|
+
}
|
|
32
|
+
/** @returns The next character without consuming it, `""` at the end */
|
|
33
|
+
peek() {
|
|
34
|
+
return this.text.charAt(this.position);
|
|
35
|
+
}
|
|
36
|
+
/** @returns The next character, consumed, `""` at the end */
|
|
37
|
+
next() {
|
|
38
|
+
let char = this.text.charAt(this.position);
|
|
39
|
+
this.position++;
|
|
40
|
+
return char;
|
|
41
|
+
}
|
|
42
|
+
/** @returns Whether every character has been consumed */
|
|
43
|
+
get done() {
|
|
44
|
+
return this.position >= this.text.length;
|
|
45
|
+
}
|
|
46
|
+
/** Skips spaces, the only whitespace allowed between most tokens. */
|
|
47
|
+
skipSpaces() {
|
|
48
|
+
while (this.peek() === " ")
|
|
49
|
+
this.position++;
|
|
50
|
+
}
|
|
51
|
+
/** Skips optional whitespace (spaces and tabs), allowed around List and Dictionary commas. */
|
|
52
|
+
skipOWS() {
|
|
53
|
+
while (this.peek() === " " || this.peek() === "\t")
|
|
54
|
+
this.position++;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Records the failure and returns the marker, so a step writes `return cursor.fail(…)`.
|
|
58
|
+
*
|
|
59
|
+
* @param message - Why parsing stopped
|
|
60
|
+
* @returns The failure marker
|
|
61
|
+
*/
|
|
62
|
+
fail(message) {
|
|
63
|
+
this.error ??= new StructuredFieldParseError(message, this.position);
|
|
64
|
+
return FAILED;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Parses a field value as its definition's top-level type (RFC 9651 section 4.2).
|
|
69
|
+
*
|
|
70
|
+
* @param text - The field value, several field lines joined with `,`
|
|
71
|
+
* @param type - The top-level type the field's definition fixes
|
|
72
|
+
* @returns The model, or where the text stops being valid
|
|
73
|
+
*/
|
|
74
|
+
export function parseField(text, type) {
|
|
75
|
+
let cursor = new Cursor(text);
|
|
76
|
+
let invalid = NON_ASCII.exec(text);
|
|
77
|
+
if (invalid) {
|
|
78
|
+
cursor.position = invalid.index;
|
|
79
|
+
cursor.fail("Field value contains a character outside visible ASCII");
|
|
80
|
+
return failure(cursor.error ?? new StructuredFieldParseError("Invalid field", 0));
|
|
81
|
+
}
|
|
82
|
+
cursor.skipSpaces();
|
|
83
|
+
let value;
|
|
84
|
+
if (type === "list")
|
|
85
|
+
value = parseList(cursor);
|
|
86
|
+
else if (type === "dictionary")
|
|
87
|
+
value = parseDictionary(cursor);
|
|
88
|
+
else
|
|
89
|
+
value = parseItem(cursor);
|
|
90
|
+
if (value !== FAILED) {
|
|
91
|
+
cursor.skipSpaces();
|
|
92
|
+
if (!cursor.done)
|
|
93
|
+
value = cursor.fail("Unexpected character after the field value");
|
|
94
|
+
}
|
|
95
|
+
if (value === FAILED) {
|
|
96
|
+
return failure(cursor.error ?? new StructuredFieldParseError("Invalid field", cursor.position));
|
|
97
|
+
}
|
|
98
|
+
return success(value);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Parses a List (section 4.2.1); a trailing comma fails, an empty value is an empty List.
|
|
102
|
+
*
|
|
103
|
+
* @param cursor - The parser state
|
|
104
|
+
* @returns The members in field order
|
|
105
|
+
*/
|
|
106
|
+
function parseList(cursor) {
|
|
107
|
+
let members = [];
|
|
108
|
+
while (!cursor.done) {
|
|
109
|
+
let member = parseMember(cursor);
|
|
110
|
+
if (member === FAILED)
|
|
111
|
+
return FAILED;
|
|
112
|
+
members.push(member);
|
|
113
|
+
cursor.skipOWS();
|
|
114
|
+
if (cursor.done)
|
|
115
|
+
return members;
|
|
116
|
+
if (cursor.next() !== ",")
|
|
117
|
+
return cursor.fail("Expected `,` between List members");
|
|
118
|
+
cursor.skipOWS();
|
|
119
|
+
if (cursor.done)
|
|
120
|
+
return cursor.fail("Trailing `,` after the last List member");
|
|
121
|
+
}
|
|
122
|
+
return members;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Parses a Dictionary (section 4.2.2). A key without `=` is the Boolean `true`, and a
|
|
126
|
+
* repeated key overwrites the earlier value in the earlier position, which assignment onto
|
|
127
|
+
* a null-prototype object does by itself.
|
|
128
|
+
*
|
|
129
|
+
* @param cursor - The parser state
|
|
130
|
+
* @returns The members keyed in field order
|
|
131
|
+
*/
|
|
132
|
+
function parseDictionary(cursor) {
|
|
133
|
+
let dictionary = Object.create(null);
|
|
134
|
+
while (!cursor.done) {
|
|
135
|
+
let key = parseKey(cursor);
|
|
136
|
+
if (key === FAILED)
|
|
137
|
+
return FAILED;
|
|
138
|
+
let member;
|
|
139
|
+
if (cursor.peek() === "=") {
|
|
140
|
+
cursor.next();
|
|
141
|
+
member = parseMember(cursor);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
let params = parseParameters(cursor);
|
|
145
|
+
member = params === FAILED ? FAILED : { value: true, params };
|
|
146
|
+
}
|
|
147
|
+
if (member === FAILED)
|
|
148
|
+
return FAILED;
|
|
149
|
+
dictionary[key] = member;
|
|
150
|
+
cursor.skipOWS();
|
|
151
|
+
if (cursor.done)
|
|
152
|
+
return dictionary;
|
|
153
|
+
if (cursor.next() !== ",")
|
|
154
|
+
return cursor.fail("Expected `,` between Dictionary members");
|
|
155
|
+
cursor.skipOWS();
|
|
156
|
+
if (cursor.done)
|
|
157
|
+
return cursor.fail("Trailing `,` after the last Dictionary member");
|
|
158
|
+
}
|
|
159
|
+
return dictionary;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Parses an Item or an Inner List, whichever the next character starts.
|
|
163
|
+
*
|
|
164
|
+
* @param cursor - The parser state
|
|
165
|
+
* @returns The member
|
|
166
|
+
*/
|
|
167
|
+
function parseMember(cursor) {
|
|
168
|
+
return cursor.peek() === "(" ? parseInnerList(cursor) : parseItem(cursor);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Parses an Inner List (section 4.2.1.2): items separated by spaces inside parentheses,
|
|
172
|
+
* then the list's own parameters.
|
|
173
|
+
*
|
|
174
|
+
* @param cursor - The parser state
|
|
175
|
+
* @returns The Inner List
|
|
176
|
+
*/
|
|
177
|
+
function parseInnerList(cursor) {
|
|
178
|
+
cursor.next();
|
|
179
|
+
let items = [];
|
|
180
|
+
while (!cursor.done) {
|
|
181
|
+
cursor.skipSpaces();
|
|
182
|
+
if (cursor.peek() === ")") {
|
|
183
|
+
cursor.next();
|
|
184
|
+
let params = parseParameters(cursor);
|
|
185
|
+
if (params === FAILED)
|
|
186
|
+
return FAILED;
|
|
187
|
+
return { items, params };
|
|
188
|
+
}
|
|
189
|
+
let item = parseItem(cursor);
|
|
190
|
+
if (item === FAILED)
|
|
191
|
+
return FAILED;
|
|
192
|
+
items.push(item);
|
|
193
|
+
let after = cursor.peek();
|
|
194
|
+
if (after !== " " && after !== ")")
|
|
195
|
+
return cursor.fail("Expected a space or `)` in Inner List");
|
|
196
|
+
}
|
|
197
|
+
return cursor.fail("Inner List is missing its closing `)`");
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Parses an Item (section 4.2.3): a bare item, then its parameters.
|
|
201
|
+
*
|
|
202
|
+
* @param cursor - The parser state
|
|
203
|
+
* @returns The Item
|
|
204
|
+
*/
|
|
205
|
+
function parseItem(cursor) {
|
|
206
|
+
let value = parseBareItem(cursor);
|
|
207
|
+
if (value === FAILED)
|
|
208
|
+
return FAILED;
|
|
209
|
+
let params = parseParameters(cursor);
|
|
210
|
+
if (params === FAILED)
|
|
211
|
+
return FAILED;
|
|
212
|
+
return { value, params };
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Parses Parameters (section 4.2.3.2); a key without `=` is the Boolean `true`, and a
|
|
216
|
+
* repeated key overwrites the earlier value in the earlier position.
|
|
217
|
+
*
|
|
218
|
+
* @param cursor - The parser state
|
|
219
|
+
* @returns The parameters in field order, on a null-prototype object
|
|
220
|
+
*/
|
|
221
|
+
function parseParameters(cursor) {
|
|
222
|
+
let params = Object.create(null);
|
|
223
|
+
while (cursor.peek() === ";") {
|
|
224
|
+
cursor.next();
|
|
225
|
+
cursor.skipSpaces();
|
|
226
|
+
let key = parseKey(cursor);
|
|
227
|
+
if (key === FAILED)
|
|
228
|
+
return FAILED;
|
|
229
|
+
let value = true;
|
|
230
|
+
if (cursor.peek() === "=") {
|
|
231
|
+
cursor.next();
|
|
232
|
+
value = parseBareItem(cursor);
|
|
233
|
+
if (value === FAILED)
|
|
234
|
+
return FAILED;
|
|
235
|
+
}
|
|
236
|
+
params[key] = value;
|
|
237
|
+
}
|
|
238
|
+
return params;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Parses a key (section 4.2.3.3): a lowercase letter or `*`, then lowercase letters,
|
|
242
|
+
* digits, `_`, `-`, `.` and `*`.
|
|
243
|
+
*
|
|
244
|
+
* @param cursor - The parser state
|
|
245
|
+
* @returns The key
|
|
246
|
+
*/
|
|
247
|
+
function parseKey(cursor) {
|
|
248
|
+
let first = cursor.peek();
|
|
249
|
+
if (!/^[a-z*]$/.test(first))
|
|
250
|
+
return cursor.fail("Key must start with a lowercase letter or `*`");
|
|
251
|
+
let start = cursor.position;
|
|
252
|
+
while (KEY_CHAR.test(cursor.peek()))
|
|
253
|
+
cursor.position++;
|
|
254
|
+
return cursor.text.slice(start, cursor.position);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Parses a bare item (section 4.2.3.1), choosing the type from its first character.
|
|
258
|
+
*
|
|
259
|
+
* @param cursor - The parser state
|
|
260
|
+
* @returns The bare item
|
|
261
|
+
*/
|
|
262
|
+
function parseBareItem(cursor) {
|
|
263
|
+
let char = cursor.peek();
|
|
264
|
+
if (char === "-" || /^[0-9]$/.test(char))
|
|
265
|
+
return parseNumber(cursor);
|
|
266
|
+
if (char === '"')
|
|
267
|
+
return parseString(cursor);
|
|
268
|
+
if (char === "*" || /^[A-Za-z]$/.test(char))
|
|
269
|
+
return parseToken(cursor);
|
|
270
|
+
if (char === ":")
|
|
271
|
+
return parseByteSequence(cursor);
|
|
272
|
+
if (char === "?")
|
|
273
|
+
return parseBoolean(cursor);
|
|
274
|
+
if (char === "@")
|
|
275
|
+
return parseDate(cursor);
|
|
276
|
+
if (char === "%")
|
|
277
|
+
return parseDisplayString(cursor);
|
|
278
|
+
return cursor.fail("Expected a bare item");
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Parses an Integer or Decimal (section 4.2.4): at most 15 digits for an Integer, at most
|
|
282
|
+
* 12 integer and 3 fractional digits for a Decimal. Negative zero reads as zero.
|
|
283
|
+
*
|
|
284
|
+
* @param cursor - The parser state
|
|
285
|
+
* @returns A `number` for an Integer, a `Decimal` for a Decimal
|
|
286
|
+
*/
|
|
287
|
+
function parseNumber(cursor) {
|
|
288
|
+
let sign = 1;
|
|
289
|
+
if (cursor.peek() === "-") {
|
|
290
|
+
cursor.next();
|
|
291
|
+
sign = -1;
|
|
292
|
+
}
|
|
293
|
+
if (!/^[0-9]$/.test(cursor.peek()))
|
|
294
|
+
return cursor.fail("Expected a digit");
|
|
295
|
+
let digits = "";
|
|
296
|
+
let isDecimal = false;
|
|
297
|
+
while (!cursor.done) {
|
|
298
|
+
let char = cursor.peek();
|
|
299
|
+
if (/^[0-9]$/.test(char)) {
|
|
300
|
+
digits += char;
|
|
301
|
+
}
|
|
302
|
+
else if (!isDecimal && char === ".") {
|
|
303
|
+
if (digits.length > 12)
|
|
304
|
+
return cursor.fail("Decimal has more than 12 integer digits");
|
|
305
|
+
digits += char;
|
|
306
|
+
isDecimal = true;
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
cursor.next();
|
|
312
|
+
if (!isDecimal && digits.length > 15)
|
|
313
|
+
return cursor.fail("Integer has more than 15 digits");
|
|
314
|
+
if (isDecimal && digits.length > 16)
|
|
315
|
+
return cursor.fail("Decimal has too many digits");
|
|
316
|
+
}
|
|
317
|
+
if (!isDecimal)
|
|
318
|
+
return sign * Number(digits) + 0;
|
|
319
|
+
if (digits.endsWith("."))
|
|
320
|
+
return cursor.fail("Decimal ends with `.`");
|
|
321
|
+
if (digits.length - digits.indexOf(".") - 1 > 3) {
|
|
322
|
+
return cursor.fail("Decimal has more than 3 fractional digits");
|
|
323
|
+
}
|
|
324
|
+
return new Decimal(sign * Number(digits) + 0);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Parses a String (section 4.2.5): visible ASCII and space between double quotes, with
|
|
328
|
+
* only `"` and `\` escaped.
|
|
329
|
+
*
|
|
330
|
+
* @param cursor - The parser state
|
|
331
|
+
* @returns The unescaped string
|
|
332
|
+
*/
|
|
333
|
+
function parseString(cursor) {
|
|
334
|
+
cursor.next();
|
|
335
|
+
let output = "";
|
|
336
|
+
while (!cursor.done) {
|
|
337
|
+
let char = cursor.next();
|
|
338
|
+
if (char === "\\") {
|
|
339
|
+
if (cursor.done)
|
|
340
|
+
return cursor.fail("String ends inside an escape");
|
|
341
|
+
let escaped = cursor.next();
|
|
342
|
+
if (escaped !== '"' && escaped !== "\\")
|
|
343
|
+
return cursor.fail("Invalid escape in String");
|
|
344
|
+
output += escaped;
|
|
345
|
+
}
|
|
346
|
+
else if (char === '"') {
|
|
347
|
+
return output;
|
|
348
|
+
}
|
|
349
|
+
else if (char < " " || char > "~") {
|
|
350
|
+
return cursor.fail("String contains a character outside visible ASCII");
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
output += char;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return cursor.fail('String is missing its closing `"`');
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Parses a Token (section 4.2.6).
|
|
360
|
+
*
|
|
361
|
+
* @param cursor - The parser state
|
|
362
|
+
* @returns The Token
|
|
363
|
+
*/
|
|
364
|
+
function parseToken(cursor) {
|
|
365
|
+
let start = cursor.position;
|
|
366
|
+
cursor.next();
|
|
367
|
+
while (TOKEN_CHAR.test(cursor.peek()))
|
|
368
|
+
cursor.position++;
|
|
369
|
+
return new Token(cursor.text.slice(start, cursor.position));
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Parses a Byte Sequence (section 4.2.7): base64 between colons.
|
|
373
|
+
*
|
|
374
|
+
* @param cursor - The parser state
|
|
375
|
+
* @returns The bytes
|
|
376
|
+
*/
|
|
377
|
+
function parseByteSequence(cursor) {
|
|
378
|
+
cursor.next();
|
|
379
|
+
let end = cursor.text.indexOf(":", cursor.position);
|
|
380
|
+
if (end === -1)
|
|
381
|
+
return cursor.fail("Byte Sequence is missing its closing `:`");
|
|
382
|
+
let content = cursor.text.slice(cursor.position, end);
|
|
383
|
+
if (!BASE64_CONTENT.test(content))
|
|
384
|
+
return cursor.fail("Byte Sequence is not base64");
|
|
385
|
+
let bytes = decodeBase64(content);
|
|
386
|
+
if (bytes === null)
|
|
387
|
+
return cursor.fail("Byte Sequence is not valid base64");
|
|
388
|
+
cursor.position = end + 1;
|
|
389
|
+
return bytes;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Parses a Boolean (section 4.2.8): `?1` or `?0`.
|
|
393
|
+
*
|
|
394
|
+
* @param cursor - The parser state
|
|
395
|
+
* @returns The Boolean
|
|
396
|
+
*/
|
|
397
|
+
function parseBoolean(cursor) {
|
|
398
|
+
cursor.next();
|
|
399
|
+
let char = cursor.next();
|
|
400
|
+
if (char === "1")
|
|
401
|
+
return true;
|
|
402
|
+
if (char === "0")
|
|
403
|
+
return false;
|
|
404
|
+
cursor.position--;
|
|
405
|
+
return cursor.fail("Boolean must be `?1` or `?0`");
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Parses a Date (section 4.2.9): `@` and an Integer of seconds since the epoch. A value
|
|
409
|
+
* outside the range a JavaScript `Date` holds fails, which the RFC permits.
|
|
410
|
+
*
|
|
411
|
+
* @param cursor - The parser state
|
|
412
|
+
* @returns The Date
|
|
413
|
+
*/
|
|
414
|
+
function parseDate(cursor) {
|
|
415
|
+
cursor.next();
|
|
416
|
+
let seconds = parseNumber(cursor);
|
|
417
|
+
if (seconds === FAILED)
|
|
418
|
+
return FAILED;
|
|
419
|
+
if (typeof seconds !== "number")
|
|
420
|
+
return cursor.fail("Date must be an Integer");
|
|
421
|
+
let date = new Date(seconds * 1000);
|
|
422
|
+
if (Number.isNaN(date.getTime()))
|
|
423
|
+
return cursor.fail("Date is outside the supported range");
|
|
424
|
+
return date;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Parses a Display String (section 4.2.10): `%"`, visible ASCII with UTF-8 octets
|
|
428
|
+
* percent-encoded in lowercase hex, and `"`. Invalid UTF-8 fails.
|
|
429
|
+
*
|
|
430
|
+
* @param cursor - The parser state
|
|
431
|
+
* @returns The Display String
|
|
432
|
+
*/
|
|
433
|
+
function parseDisplayString(cursor) {
|
|
434
|
+
cursor.next();
|
|
435
|
+
if (cursor.next() !== '"')
|
|
436
|
+
return cursor.fail('Display String must start with `%"`');
|
|
437
|
+
let octets = [];
|
|
438
|
+
while (!cursor.done) {
|
|
439
|
+
let char = cursor.next();
|
|
440
|
+
if (char < " " || char > "~") {
|
|
441
|
+
return cursor.fail("Display String contains a character outside visible ASCII");
|
|
442
|
+
}
|
|
443
|
+
if (char === "%") {
|
|
444
|
+
let hex = cursor.text.slice(cursor.position, cursor.position + 2);
|
|
445
|
+
if (!LOWER_HEX.test(hex))
|
|
446
|
+
return cursor.fail("Display String escape must be lowercase hex");
|
|
447
|
+
octets.push(Number.parseInt(hex, 16));
|
|
448
|
+
cursor.position += 2;
|
|
449
|
+
}
|
|
450
|
+
else if (char === '"') {
|
|
451
|
+
let text = decodeUtf8(new Uint8Array(octets));
|
|
452
|
+
if (text === null)
|
|
453
|
+
return cursor.fail("Display String is not valid UTF-8");
|
|
454
|
+
return new DisplayString(text);
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
octets.push(char.charCodeAt(0));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return cursor.fail('Display String is missing its closing `"`');
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Decodes UTF-8 octets strictly.
|
|
464
|
+
*
|
|
465
|
+
* @param octets - The decoded percent-escapes and literal characters
|
|
466
|
+
* @returns The text, or `null` for an invalid sequence
|
|
467
|
+
*/
|
|
468
|
+
function decodeUtf8(octets) {
|
|
469
|
+
try {
|
|
470
|
+
return UTF8.decode(octets);
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The RFC 9651 section 4.1 serialization algorithms: canonical text for the three
|
|
3
|
+
* top-level types, with every range and character-set rule enforced, so a value either
|
|
4
|
+
* serializes to a field any conforming recipient reads or fails with where it went wrong.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { Result } from "@sdxc/result";
|
|
10
|
+
import type { SF } from "./types.js";
|
|
11
|
+
import { StructuredFieldStringifyError } from "./errors.js";
|
|
12
|
+
/**
|
|
13
|
+
* Serializes a value as its definition's top-level type (RFC 9651 section 4.1). An empty
|
|
14
|
+
* List or Dictionary serializes as `""`, which RFC 9651 reads as "do not send the field".
|
|
15
|
+
*
|
|
16
|
+
* @param value - The model, or members written in their shorthand forms
|
|
17
|
+
* @param type - The top-level type the field's definition fixes
|
|
18
|
+
* @returns The canonical text, or the path to the value with no representation
|
|
19
|
+
*/
|
|
20
|
+
export declare function stringifyField<Type extends SF.FieldType>(value: SF.InputOf[Type], type: Type): Result<string, StructuredFieldStringifyError>;
|