hbsig 0.0.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/cjs/bin_to_str.js +44 -0
- package/cjs/collect-body-keys.js +470 -0
- package/cjs/encode-array-item.js +110 -0
- package/cjs/encode-utils.js +236 -0
- package/cjs/encode.js +1318 -0
- package/cjs/erl_json.js +317 -0
- package/cjs/erl_str.js +1037 -0
- package/cjs/flat.js +222 -0
- package/cjs/http-message-signatures/httpbis.js +489 -0
- package/cjs/http-message-signatures/index.js +25 -0
- package/cjs/http-message-signatures/structured-header.js +129 -0
- package/cjs/httpsig.js +716 -0
- package/cjs/httpsig2.js +1160 -0
- package/cjs/id.js +470 -0
- package/cjs/index.js +63 -0
- package/cjs/send.js +194 -0
- package/cjs/signer-utils.js +617 -0
- package/cjs/signer.js +606 -0
- package/cjs/structured.js +296 -0
- package/cjs/test.js +27 -0
- package/cjs/utils.js +42 -0
- package/esm/bin_to_str.js +46 -0
- package/esm/collect-body-keys.js +436 -0
- package/esm/encode-array-item.js +112 -0
- package/esm/encode-utils.js +185 -0
- package/esm/encode.js +1219 -0
- package/esm/erl_json.js +289 -0
- package/esm/erl_str.js +1139 -0
- package/esm/flat.js +196 -0
- package/esm/http-message-signatures/httpbis.js +438 -0
- package/esm/http-message-signatures/index.js +4 -0
- package/esm/http-message-signatures/structured-header.js +105 -0
- package/esm/httpsig.js +658 -0
- package/esm/httpsig2.js +1097 -0
- package/esm/id.js +459 -0
- package/esm/index.js +4 -0
- package/esm/package.json +3 -0
- package/esm/send.js +124 -0
- package/esm/signer-utils.js +494 -0
- package/esm/signer.js +452 -0
- package/esm/structured.js +269 -0
- package/esm/test.js +6 -0
- package/esm/utils.js +28 -0
- package/package.json +28 -0
package/esm/erl_str.js
ADDED
|
@@ -0,0 +1,1139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Erlang term string parser and formatter
|
|
3
|
+
* Converts between Erlang term strings and JavaScript values
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse an Erlang term string into JavaScript values
|
|
8
|
+
* @param {string} str - Erlang term string
|
|
9
|
+
* @param {boolean} binaryMode - If true, keep binaries as Buffers; if false, convert to strings
|
|
10
|
+
* @returns {*} JavaScript value
|
|
11
|
+
*/
|
|
12
|
+
export function erl_str_from(str, binaryMode = false) {
|
|
13
|
+
// Handle the new response format
|
|
14
|
+
if (str.startsWith("#erl_response{")) {
|
|
15
|
+
const rawMatch = str.match(/#erl_response\{raw=(.*?),formatted=(.*?)\}$/s)
|
|
16
|
+
if (rawMatch && rawMatch[1] && rawMatch[2]) {
|
|
17
|
+
const rawStr = rawMatch[1]
|
|
18
|
+
const formattedStr = rawMatch[2]
|
|
19
|
+
|
|
20
|
+
if (binaryMode) {
|
|
21
|
+
// In binary mode, just parse formatted
|
|
22
|
+
const parser = new ErlangParser(formattedStr, true)
|
|
23
|
+
return parser.parse()
|
|
24
|
+
} else {
|
|
25
|
+
// Build a type map from raw, then parse formatted with type info
|
|
26
|
+
const typeMap = buildTypeMap(rawStr)
|
|
27
|
+
const parser = new TypeAwareParser(formattedStr, typeMap)
|
|
28
|
+
return parser.parse()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Fallback for non-response format
|
|
34
|
+
const parser = new ErlangParser(str, binaryMode)
|
|
35
|
+
return parser.parse()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse body field specially - convert to string only for multipart bodies
|
|
40
|
+
* @param {*} parsed - Parsed Erlang term object
|
|
41
|
+
* @returns {*} Object with body field potentially converted to string
|
|
42
|
+
*/
|
|
43
|
+
export function parse_body(parsed) {
|
|
44
|
+
if (!parsed || typeof parsed !== "object") {
|
|
45
|
+
return parsed
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// If there's a body field
|
|
49
|
+
if ("body" in parsed) {
|
|
50
|
+
if (Buffer.isBuffer(parsed.body)) {
|
|
51
|
+
// Check if this looks like a multipart body
|
|
52
|
+
// Multipart bodies start with "--" boundary
|
|
53
|
+
const bodyStart = parsed.body.toString(
|
|
54
|
+
"binary",
|
|
55
|
+
0,
|
|
56
|
+
Math.min(100, parsed.body.length)
|
|
57
|
+
)
|
|
58
|
+
if (
|
|
59
|
+
bodyStart.startsWith("--") &&
|
|
60
|
+
parsed["content-type"] &&
|
|
61
|
+
parsed["content-type"].includes("multipart")
|
|
62
|
+
) {
|
|
63
|
+
// It's a multipart body, convert to string
|
|
64
|
+
return {
|
|
65
|
+
...parsed,
|
|
66
|
+
body: parsed.body.toString("binary"),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Not multipart, keep as Buffer
|
|
70
|
+
return parsed
|
|
71
|
+
}
|
|
72
|
+
// Already a string, return as-is
|
|
73
|
+
return parsed
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return parsed
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Build a map of paths to types by analyzing the raw string
|
|
80
|
+
function buildTypeMap(rawStr) {
|
|
81
|
+
const typeMap = new Map()
|
|
82
|
+
|
|
83
|
+
function detectBinaryType(str, startPos) {
|
|
84
|
+
// Check what follows <<
|
|
85
|
+
let pos = startPos + 2
|
|
86
|
+
while (pos < str.length && /\s/.test(str[pos])) pos++
|
|
87
|
+
|
|
88
|
+
if (pos < str.length && str[pos] === '"') {
|
|
89
|
+
// It looks like a string, but check if it has escape sequences
|
|
90
|
+
// If it has \NNN octal escapes, it's actually a binary that Erlang formatted as string
|
|
91
|
+
let scanPos = pos + 1
|
|
92
|
+
let hasOctalEscapes = false
|
|
93
|
+
|
|
94
|
+
while (
|
|
95
|
+
scanPos < str.length &&
|
|
96
|
+
!(
|
|
97
|
+
str[scanPos] === '"' &&
|
|
98
|
+
str[scanPos + 1] === ">" &&
|
|
99
|
+
str[scanPos + 2] === ">"
|
|
100
|
+
)
|
|
101
|
+
) {
|
|
102
|
+
if (str[scanPos] === "\\" && scanPos + 1 < str.length) {
|
|
103
|
+
const nextChar = str[scanPos + 1]
|
|
104
|
+
// Check for octal escape \NNN
|
|
105
|
+
if (/[0-7]/.test(nextChar)) {
|
|
106
|
+
hasOctalEscapes = true
|
|
107
|
+
break
|
|
108
|
+
}
|
|
109
|
+
scanPos += 2
|
|
110
|
+
} else {
|
|
111
|
+
scanPos++
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// If it has octal escapes, treat as binary
|
|
116
|
+
if (hasOctalEscapes) {
|
|
117
|
+
return "binary"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return "string"
|
|
121
|
+
}
|
|
122
|
+
return "binary"
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function scanValue(str, pos, path) {
|
|
126
|
+
while (pos < str.length && /\s/.test(str[pos])) pos++
|
|
127
|
+
|
|
128
|
+
if (pos >= str.length) return pos
|
|
129
|
+
|
|
130
|
+
if (str[pos] === "#" && str[pos + 1] === "{") {
|
|
131
|
+
// Map
|
|
132
|
+
pos += 2
|
|
133
|
+
let mapIndex = 0
|
|
134
|
+
|
|
135
|
+
while (pos < str.length) {
|
|
136
|
+
while (pos < str.length && /\s/.test(str[pos])) pos++
|
|
137
|
+
if (str[pos] === "}") return pos + 1
|
|
138
|
+
|
|
139
|
+
// Parse key
|
|
140
|
+
const keyStart = pos
|
|
141
|
+
if (str[pos] === "<" && str[pos + 1] === "<") {
|
|
142
|
+
// Binary key
|
|
143
|
+
const keyType = detectBinaryType(str, pos)
|
|
144
|
+
let keyEnd = pos + 2
|
|
145
|
+
|
|
146
|
+
if (keyType === "string") {
|
|
147
|
+
keyEnd++ // skip "
|
|
148
|
+
while (
|
|
149
|
+
keyEnd < str.length &&
|
|
150
|
+
!(
|
|
151
|
+
str[keyEnd] === '"' &&
|
|
152
|
+
str[keyEnd + 1] === ">" &&
|
|
153
|
+
str[keyEnd + 2] === ">"
|
|
154
|
+
)
|
|
155
|
+
) {
|
|
156
|
+
if (str[keyEnd] === "\\") keyEnd++
|
|
157
|
+
keyEnd++
|
|
158
|
+
}
|
|
159
|
+
keyEnd += 3
|
|
160
|
+
} else {
|
|
161
|
+
let depth = 1
|
|
162
|
+
while (depth > 0 && keyEnd < str.length) {
|
|
163
|
+
if (str[keyEnd] === "<" && str[keyEnd + 1] === "<") {
|
|
164
|
+
depth++
|
|
165
|
+
keyEnd += 2
|
|
166
|
+
} else if (str[keyEnd] === ">" && str[keyEnd + 1] === ">") {
|
|
167
|
+
depth--
|
|
168
|
+
keyEnd += 2
|
|
169
|
+
} else {
|
|
170
|
+
keyEnd++
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
pos = keyEnd
|
|
176
|
+
} else {
|
|
177
|
+
// Regular key
|
|
178
|
+
while (pos < str.length && !/[\s=,}]/.test(str[pos])) pos++
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Skip =>
|
|
182
|
+
while (pos < str.length && /\s/.test(str[pos])) pos++
|
|
183
|
+
if (str[pos] === "=" && str[pos + 1] === ">") pos += 2
|
|
184
|
+
while (pos < str.length && /\s/.test(str[pos])) pos++
|
|
185
|
+
|
|
186
|
+
// Parse value and record type if binary
|
|
187
|
+
if (str[pos] === "<" && str[pos + 1] === "<") {
|
|
188
|
+
const type = detectBinaryType(str, pos)
|
|
189
|
+
const pathKey = [...path, mapIndex].join(".")
|
|
190
|
+
typeMap.set(pathKey, type)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
pos = scanValue(str, pos, [...path, mapIndex])
|
|
194
|
+
mapIndex++
|
|
195
|
+
|
|
196
|
+
// Skip comma
|
|
197
|
+
while (pos < str.length && /\s/.test(str[pos])) pos++
|
|
198
|
+
if (str[pos] === ",") pos++
|
|
199
|
+
}
|
|
200
|
+
} else if (str[pos] === "<" && str[pos + 1] === "<") {
|
|
201
|
+
// Binary
|
|
202
|
+
const type = detectBinaryType(str, pos)
|
|
203
|
+
const pathKey = path.join(".")
|
|
204
|
+
typeMap.set(pathKey, type)
|
|
205
|
+
|
|
206
|
+
pos += 2
|
|
207
|
+
if (type === "string" || str[pos] === '"') {
|
|
208
|
+
// Skip past string literal
|
|
209
|
+
if (str[pos] === '"') pos++ // skip opening quote
|
|
210
|
+
while (
|
|
211
|
+
pos < str.length &&
|
|
212
|
+
!(str[pos] === '"' && str[pos + 1] === ">" && str[pos + 2] === ">")
|
|
213
|
+
) {
|
|
214
|
+
if (str[pos] === "\\") pos++
|
|
215
|
+
pos++
|
|
216
|
+
}
|
|
217
|
+
return pos + 3
|
|
218
|
+
} else {
|
|
219
|
+
let depth = 1
|
|
220
|
+
while (depth > 0 && pos < str.length) {
|
|
221
|
+
if (str[pos] === "<" && str[pos + 1] === "<") {
|
|
222
|
+
depth++
|
|
223
|
+
pos += 2
|
|
224
|
+
} else if (str[pos] === ">" && str[pos + 1] === ">") {
|
|
225
|
+
depth--
|
|
226
|
+
pos += 2
|
|
227
|
+
} else {
|
|
228
|
+
pos++
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return pos
|
|
232
|
+
}
|
|
233
|
+
} else if (str[pos] === "[") {
|
|
234
|
+
// List
|
|
235
|
+
pos++
|
|
236
|
+
let listIndex = 0
|
|
237
|
+
|
|
238
|
+
while (pos < str.length) {
|
|
239
|
+
while (pos < str.length && /\s/.test(str[pos])) pos++
|
|
240
|
+
if (str[pos] === "]") return pos + 1
|
|
241
|
+
|
|
242
|
+
pos = scanValue(str, pos, [...path, listIndex])
|
|
243
|
+
listIndex++
|
|
244
|
+
|
|
245
|
+
while (pos < str.length && /\s/.test(str[pos])) pos++
|
|
246
|
+
if (str[pos] === ",") pos++
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
// Skip other values
|
|
250
|
+
while (pos < str.length && !/[\s,\]}=>]/.test(str[pos])) {
|
|
251
|
+
pos++
|
|
252
|
+
}
|
|
253
|
+
return pos
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return pos
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
scanValue(rawStr, 0, [])
|
|
260
|
+
return typeMap
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Parser that uses type information
|
|
264
|
+
class TypeAwareParser {
|
|
265
|
+
constructor(str, typeMap) {
|
|
266
|
+
this.str = str
|
|
267
|
+
this.typeMap = typeMap
|
|
268
|
+
this.pos = 0
|
|
269
|
+
this.path = []
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
parse() {
|
|
273
|
+
const result = this.parseValue()
|
|
274
|
+
this.skipWhitespace()
|
|
275
|
+
if (this.pos < this.str.length) {
|
|
276
|
+
throw new Error(`Unexpected content at position ${this.pos}`)
|
|
277
|
+
}
|
|
278
|
+
return result
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
peek(offset = 0) {
|
|
282
|
+
return this.str[this.pos + offset] || ""
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
advance(count = 1) {
|
|
286
|
+
this.pos += count
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
skipWhitespace() {
|
|
290
|
+
while (this.pos < this.str.length && /\s/.test(this.peek())) {
|
|
291
|
+
this.advance()
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
parseValue() {
|
|
296
|
+
this.skipWhitespace()
|
|
297
|
+
|
|
298
|
+
const ch = this.peek()
|
|
299
|
+
const ch2 = this.peek(1)
|
|
300
|
+
|
|
301
|
+
if (ch === "#" && ch2 === "{") {
|
|
302
|
+
return this.parseMap()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (ch === "<" && ch2 === "<") {
|
|
306
|
+
return this.parseBinary()
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (ch === "[") {
|
|
310
|
+
return this.parseList()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (ch === "'") {
|
|
314
|
+
return this.parseQuotedAtom()
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (ch === '"') {
|
|
318
|
+
return this.parseQuotedString()
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return this.parseAtomOrNumber()
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
parseMap() {
|
|
325
|
+
this.advance(2) // skip #{
|
|
326
|
+
const map = {}
|
|
327
|
+
let mapIndex = 0
|
|
328
|
+
|
|
329
|
+
while (true) {
|
|
330
|
+
this.skipWhitespace()
|
|
331
|
+
|
|
332
|
+
if (this.peek() === "}") {
|
|
333
|
+
this.advance()
|
|
334
|
+
break
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
this.path.push(mapIndex)
|
|
338
|
+
const key = this.parseValue()
|
|
339
|
+
this.path.pop()
|
|
340
|
+
|
|
341
|
+
this.skipWhitespace()
|
|
342
|
+
|
|
343
|
+
if (this.peek() !== "=" || this.peek(1) !== ">") {
|
|
344
|
+
throw new Error("Expected =>")
|
|
345
|
+
}
|
|
346
|
+
this.advance(2)
|
|
347
|
+
|
|
348
|
+
this.skipWhitespace()
|
|
349
|
+
|
|
350
|
+
this.path.push(mapIndex)
|
|
351
|
+
const value = this.parseValue()
|
|
352
|
+
this.path.pop()
|
|
353
|
+
|
|
354
|
+
const jsKey =
|
|
355
|
+
key instanceof Buffer
|
|
356
|
+
? key.toString("utf8")
|
|
357
|
+
: typeof key === "symbol"
|
|
358
|
+
? Symbol.keyFor(key) || String(key)
|
|
359
|
+
: String(key)
|
|
360
|
+
|
|
361
|
+
map[jsKey] = value
|
|
362
|
+
mapIndex++
|
|
363
|
+
|
|
364
|
+
this.skipWhitespace()
|
|
365
|
+
if (this.peek() === ",") {
|
|
366
|
+
this.advance()
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return map
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
parseBinary() {
|
|
374
|
+
const currentPath = this.path.join(".")
|
|
375
|
+
const type = this.typeMap.get(currentPath) || "binary"
|
|
376
|
+
|
|
377
|
+
// Parse the formatted version (always byte format)
|
|
378
|
+
this.advance(2) // skip <<
|
|
379
|
+
|
|
380
|
+
if (this.peek() === ">" && this.peek(1) === ">") {
|
|
381
|
+
this.advance(2)
|
|
382
|
+
return Buffer.alloc(0)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const bytes = []
|
|
386
|
+
while (!(this.peek() === ">" && this.peek(1) === ">")) {
|
|
387
|
+
this.skipWhitespace()
|
|
388
|
+
|
|
389
|
+
let num = ""
|
|
390
|
+
while (/\d/.test(this.peek())) {
|
|
391
|
+
num += this.peek()
|
|
392
|
+
this.advance()
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (num) bytes.push(parseInt(num, 10))
|
|
396
|
+
|
|
397
|
+
this.skipWhitespace()
|
|
398
|
+
if (this.peek() === ",") this.advance()
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
this.advance(2) // skip >>
|
|
402
|
+
|
|
403
|
+
const buffer = Buffer.from(bytes)
|
|
404
|
+
|
|
405
|
+
// If it was a string literal in raw, convert to string
|
|
406
|
+
if (type === "string") {
|
|
407
|
+
return buffer.toString("utf8")
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// For binaries that are not explicitly marked as strings, check if they're valid UTF-8
|
|
411
|
+
// that contains reasonable string content (no control characters except \t, \n, \r)
|
|
412
|
+
try {
|
|
413
|
+
const str = buffer.toString("utf8")
|
|
414
|
+
// Check if the string round-trips correctly
|
|
415
|
+
if (Buffer.from(str, "utf8").equals(buffer)) {
|
|
416
|
+
// It's valid UTF-8, but also check if it contains reasonable characters
|
|
417
|
+
let hasReasonableChars = true
|
|
418
|
+
for (let i = 0; i < str.length; i++) {
|
|
419
|
+
const code = str.charCodeAt(i)
|
|
420
|
+
// Allow printable chars, space, tab, newline, carriage return, and Unicode
|
|
421
|
+
if (code < 0x20 && code !== 0x09 && code !== 0x0a && code !== 0x0d) {
|
|
422
|
+
hasReasonableChars = false
|
|
423
|
+
break
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (hasReasonableChars) {
|
|
428
|
+
return str
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
} catch (e) {
|
|
432
|
+
// Not valid UTF-8, keep as buffer
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return buffer
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
parseQuotedString() {
|
|
439
|
+
this.advance() // skip opening "
|
|
440
|
+
let str = ""
|
|
441
|
+
|
|
442
|
+
while (this.peek() !== '"') {
|
|
443
|
+
if (this.peek() === "\\") {
|
|
444
|
+
this.advance()
|
|
445
|
+
const escaped = this.peek()
|
|
446
|
+
switch (escaped) {
|
|
447
|
+
case '"':
|
|
448
|
+
str += '"'
|
|
449
|
+
break
|
|
450
|
+
case "\\":
|
|
451
|
+
str += "\\"
|
|
452
|
+
break
|
|
453
|
+
case "n":
|
|
454
|
+
str += "\n"
|
|
455
|
+
break
|
|
456
|
+
case "r":
|
|
457
|
+
str += "\r"
|
|
458
|
+
break
|
|
459
|
+
case "t":
|
|
460
|
+
str += "\t"
|
|
461
|
+
break
|
|
462
|
+
default:
|
|
463
|
+
str += escaped
|
|
464
|
+
}
|
|
465
|
+
this.advance()
|
|
466
|
+
} else {
|
|
467
|
+
str += this.peek()
|
|
468
|
+
this.advance()
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
this.advance() // skip closing "
|
|
473
|
+
return str
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
parseList() {
|
|
477
|
+
this.advance() // skip [
|
|
478
|
+
const list = []
|
|
479
|
+
let listIndex = 0
|
|
480
|
+
|
|
481
|
+
while (true) {
|
|
482
|
+
this.skipWhitespace()
|
|
483
|
+
|
|
484
|
+
if (this.peek() === "]") {
|
|
485
|
+
this.advance()
|
|
486
|
+
break
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
this.path.push(listIndex)
|
|
490
|
+
list.push(this.parseValue())
|
|
491
|
+
this.path.pop()
|
|
492
|
+
|
|
493
|
+
listIndex++
|
|
494
|
+
|
|
495
|
+
this.skipWhitespace()
|
|
496
|
+
if (this.peek() === ",") {
|
|
497
|
+
this.advance()
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return list
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
parseQuotedAtom() {
|
|
505
|
+
this.advance() // skip '
|
|
506
|
+
|
|
507
|
+
let atom = ""
|
|
508
|
+
while (this.peek() !== "'") {
|
|
509
|
+
if (this.peek() === "\\") {
|
|
510
|
+
this.advance()
|
|
511
|
+
const escaped = this.peek()
|
|
512
|
+
switch (escaped) {
|
|
513
|
+
case "'":
|
|
514
|
+
atom += "'"
|
|
515
|
+
break
|
|
516
|
+
case "\\":
|
|
517
|
+
atom += "\\"
|
|
518
|
+
break
|
|
519
|
+
case "n":
|
|
520
|
+
atom += "\n"
|
|
521
|
+
break
|
|
522
|
+
case "r":
|
|
523
|
+
atom += "\r"
|
|
524
|
+
break
|
|
525
|
+
case "t":
|
|
526
|
+
atom += "\t"
|
|
527
|
+
break
|
|
528
|
+
default:
|
|
529
|
+
atom += escaped
|
|
530
|
+
}
|
|
531
|
+
this.advance()
|
|
532
|
+
} else {
|
|
533
|
+
atom += this.peek()
|
|
534
|
+
this.advance()
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
this.advance() // skip closing '
|
|
539
|
+
|
|
540
|
+
if (atom === "null") return null
|
|
541
|
+
if (atom === "true") return true
|
|
542
|
+
if (atom === "false") return false
|
|
543
|
+
|
|
544
|
+
return Symbol.for(atom)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
parseAtomOrNumber() {
|
|
548
|
+
let token = ""
|
|
549
|
+
|
|
550
|
+
// Parse unquoted atom/number
|
|
551
|
+
while (this.pos < this.str.length) {
|
|
552
|
+
const ch = this.peek()
|
|
553
|
+
|
|
554
|
+
if (
|
|
555
|
+
ch === "," ||
|
|
556
|
+
ch === "]" ||
|
|
557
|
+
ch === "}" ||
|
|
558
|
+
ch === ")" ||
|
|
559
|
+
(ch === "=" && this.peek(1) === ">")
|
|
560
|
+
) {
|
|
561
|
+
break
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (/\s/.test(ch)) {
|
|
565
|
+
let i = this.pos
|
|
566
|
+
while (i < this.str.length && /\s/.test(this.str[i])) i++
|
|
567
|
+
|
|
568
|
+
if (i >= this.str.length) break
|
|
569
|
+
|
|
570
|
+
const nextCh = this.str[i]
|
|
571
|
+
if (
|
|
572
|
+
nextCh === "," ||
|
|
573
|
+
nextCh === "]" ||
|
|
574
|
+
nextCh === "}" ||
|
|
575
|
+
nextCh === ")" ||
|
|
576
|
+
(nextCh === "=" && i + 1 < this.str.length && this.str[i + 1] === ">")
|
|
577
|
+
) {
|
|
578
|
+
break
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
token += ch
|
|
583
|
+
this.advance()
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
token = token.trim()
|
|
587
|
+
|
|
588
|
+
if (token === "null") return null
|
|
589
|
+
if (token === "true") return true
|
|
590
|
+
if (token === "false") return false
|
|
591
|
+
|
|
592
|
+
if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(token)) {
|
|
593
|
+
return parseFloat(token)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return Symbol.for(token)
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Parser implementation
|
|
601
|
+
class ErlangParser {
|
|
602
|
+
constructor(str, binaryMode = false) {
|
|
603
|
+
this.str = str
|
|
604
|
+
this.pos = 0
|
|
605
|
+
this.binaryMode = binaryMode
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
parse() {
|
|
609
|
+
const result = this.parseValue()
|
|
610
|
+
this.skipWhitespace()
|
|
611
|
+
if (this.pos < this.str.length) {
|
|
612
|
+
throw new Error(
|
|
613
|
+
`Unexpected content at position ${this.pos}: ${this.str.slice(this.pos, this.pos + 50)}`
|
|
614
|
+
)
|
|
615
|
+
}
|
|
616
|
+
return result
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
peek(offset = 0) {
|
|
620
|
+
return this.str[this.pos + offset] || ""
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
advance(count = 1) {
|
|
624
|
+
this.pos += count
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
skipWhitespace() {
|
|
628
|
+
while (this.pos < this.str.length && /\s/.test(this.peek())) {
|
|
629
|
+
this.advance()
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
parseValue() {
|
|
634
|
+
this.skipWhitespace()
|
|
635
|
+
|
|
636
|
+
const ch = this.peek()
|
|
637
|
+
const ch2 = this.peek(1)
|
|
638
|
+
|
|
639
|
+
// Map: #{...}
|
|
640
|
+
if (ch === "#" && ch2 === "{") {
|
|
641
|
+
return this.parseMap()
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Binary: <<...>>
|
|
645
|
+
if (ch === "<" && ch2 === "<") {
|
|
646
|
+
return this.parseBinary()
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// List: [...]
|
|
650
|
+
if (ch === "[") {
|
|
651
|
+
return this.parseList()
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Quoted atom: 'atom'
|
|
655
|
+
if (ch === "'") {
|
|
656
|
+
return this.parseQuotedAtom()
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Quoted string: "..."
|
|
660
|
+
if (ch === '"') {
|
|
661
|
+
return this.parseQuotedString()
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Number or unquoted atom
|
|
665
|
+
return this.parseAtomOrNumber()
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
parseMap() {
|
|
669
|
+
this.advance(2) // skip #{
|
|
670
|
+
const map = {}
|
|
671
|
+
|
|
672
|
+
while (true) {
|
|
673
|
+
this.skipWhitespace()
|
|
674
|
+
|
|
675
|
+
if (this.peek() === "}") {
|
|
676
|
+
this.advance()
|
|
677
|
+
break
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Parse key
|
|
681
|
+
const key = this.parseValue()
|
|
682
|
+
this.skipWhitespace()
|
|
683
|
+
|
|
684
|
+
// Expect =>
|
|
685
|
+
if (this.peek() !== "=" || this.peek(1) !== ">") {
|
|
686
|
+
throw new Error(`Expected => at position ${this.pos}`)
|
|
687
|
+
}
|
|
688
|
+
this.advance(2)
|
|
689
|
+
|
|
690
|
+
this.skipWhitespace()
|
|
691
|
+
|
|
692
|
+
// Parse value
|
|
693
|
+
const value = this.parseValue()
|
|
694
|
+
|
|
695
|
+
// Convert key to string
|
|
696
|
+
let jsKey
|
|
697
|
+
if (key instanceof Buffer) {
|
|
698
|
+
jsKey = key.toString("utf8")
|
|
699
|
+
} else if (typeof key === "symbol") {
|
|
700
|
+
jsKey = Symbol.keyFor(key) || key.description || String(key)
|
|
701
|
+
} else {
|
|
702
|
+
jsKey = String(key)
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
map[jsKey] = value
|
|
706
|
+
|
|
707
|
+
this.skipWhitespace()
|
|
708
|
+
if (this.peek() === ",") {
|
|
709
|
+
this.advance()
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return map
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
parseBinary() {
|
|
717
|
+
this.advance(2) // skip <<
|
|
718
|
+
|
|
719
|
+
// String binary: <<"...">>
|
|
720
|
+
if (this.peek() === '"') {
|
|
721
|
+
this.advance() // skip "
|
|
722
|
+
let content = ""
|
|
723
|
+
|
|
724
|
+
while (
|
|
725
|
+
!(this.peek() === '"' && this.peek(1) === ">" && this.peek(2) === ">")
|
|
726
|
+
) {
|
|
727
|
+
if (this.pos >= this.str.length) {
|
|
728
|
+
throw new Error("Unterminated string binary")
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (this.peek() === "\\") {
|
|
732
|
+
this.advance()
|
|
733
|
+
const escaped = this.peek()
|
|
734
|
+
|
|
735
|
+
// Check for octal escape sequences \NNN
|
|
736
|
+
if (/[0-7]/.test(escaped)) {
|
|
737
|
+
let octal = escaped
|
|
738
|
+
this.advance()
|
|
739
|
+
|
|
740
|
+
// Get up to 2 more octal digits
|
|
741
|
+
if (/[0-7]/.test(this.peek())) {
|
|
742
|
+
octal += this.peek()
|
|
743
|
+
this.advance()
|
|
744
|
+
|
|
745
|
+
if (/[0-7]/.test(this.peek())) {
|
|
746
|
+
octal += this.peek()
|
|
747
|
+
this.advance()
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Convert octal to character
|
|
752
|
+
const charCode = parseInt(octal, 8)
|
|
753
|
+
content += String.fromCharCode(charCode)
|
|
754
|
+
} else {
|
|
755
|
+
// Regular escape sequences
|
|
756
|
+
switch (escaped) {
|
|
757
|
+
case '"':
|
|
758
|
+
content += '"'
|
|
759
|
+
break
|
|
760
|
+
case "\\":
|
|
761
|
+
content += "\\"
|
|
762
|
+
break
|
|
763
|
+
case "n":
|
|
764
|
+
content += "\n"
|
|
765
|
+
break
|
|
766
|
+
case "r":
|
|
767
|
+
content += "\r"
|
|
768
|
+
break
|
|
769
|
+
case "t":
|
|
770
|
+
content += "\t"
|
|
771
|
+
break
|
|
772
|
+
default:
|
|
773
|
+
content += escaped
|
|
774
|
+
}
|
|
775
|
+
this.advance()
|
|
776
|
+
}
|
|
777
|
+
} else {
|
|
778
|
+
content += this.peek()
|
|
779
|
+
this.advance()
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
this.advance(3) // skip ">>
|
|
784
|
+
|
|
785
|
+
// Check for structured field format
|
|
786
|
+
if (
|
|
787
|
+
content.startsWith(":") &&
|
|
788
|
+
content.endsWith(":") &&
|
|
789
|
+
content.length >= 2
|
|
790
|
+
) {
|
|
791
|
+
if (content === "::") {
|
|
792
|
+
return Buffer.alloc(0)
|
|
793
|
+
}
|
|
794
|
+
try {
|
|
795
|
+
const base64 = content.slice(1, -1)
|
|
796
|
+
return Buffer.from(base64, "base64")
|
|
797
|
+
} catch (e) {
|
|
798
|
+
// Not valid base64, treat as regular string
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// In binary mode, return as Buffer; otherwise check if it's printable
|
|
803
|
+
if (this.binaryMode) {
|
|
804
|
+
return Buffer.from(content, "utf8")
|
|
805
|
+
} else {
|
|
806
|
+
// Check if the content is printable
|
|
807
|
+
const bytes = Buffer.from(content, "utf8")
|
|
808
|
+
const isPrintable = Array.from(bytes).every(b => b >= 32 && b <= 126)
|
|
809
|
+
|
|
810
|
+
// If it contains non-printable characters, return as Buffer
|
|
811
|
+
if (!isPrintable) {
|
|
812
|
+
return bytes
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Otherwise return as string
|
|
816
|
+
return content
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Empty binary: <<>>
|
|
821
|
+
if (this.peek() === ">" && this.peek(1) === ">") {
|
|
822
|
+
this.advance(2)
|
|
823
|
+
return Buffer.alloc(0)
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Byte binary: <<1,2,3>>
|
|
827
|
+
const bytes = []
|
|
828
|
+
while (!(this.peek() === ">" && this.peek(1) === ">")) {
|
|
829
|
+
if (this.pos >= this.str.length) {
|
|
830
|
+
throw new Error("Unterminated byte binary")
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
this.skipWhitespace()
|
|
834
|
+
|
|
835
|
+
let num = ""
|
|
836
|
+
while (/\d/.test(this.peek())) {
|
|
837
|
+
num += this.peek()
|
|
838
|
+
this.advance()
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (num) {
|
|
842
|
+
bytes.push(parseInt(num, 10))
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
this.skipWhitespace()
|
|
846
|
+
if (this.peek() === ",") {
|
|
847
|
+
this.advance()
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
this.advance(2) // skip >>
|
|
852
|
+
return Buffer.from(bytes)
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
parseQuotedString() {
|
|
856
|
+
this.advance() // skip "
|
|
857
|
+
let str = ""
|
|
858
|
+
|
|
859
|
+
while (this.peek() !== '"') {
|
|
860
|
+
if (this.peek() === "\\") {
|
|
861
|
+
this.advance()
|
|
862
|
+
const escaped = this.peek()
|
|
863
|
+
switch (escaped) {
|
|
864
|
+
case '"':
|
|
865
|
+
str += '"'
|
|
866
|
+
break
|
|
867
|
+
case "\\":
|
|
868
|
+
str += "\\"
|
|
869
|
+
break
|
|
870
|
+
case "n":
|
|
871
|
+
str += "\n"
|
|
872
|
+
break
|
|
873
|
+
case "r":
|
|
874
|
+
str += "\r"
|
|
875
|
+
break
|
|
876
|
+
case "t":
|
|
877
|
+
str += "\t"
|
|
878
|
+
break
|
|
879
|
+
default:
|
|
880
|
+
str += escaped
|
|
881
|
+
}
|
|
882
|
+
this.advance()
|
|
883
|
+
} else {
|
|
884
|
+
str += this.peek()
|
|
885
|
+
this.advance()
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
this.advance() // skip closing "
|
|
890
|
+
return str
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
parseList() {
|
|
894
|
+
this.advance() // skip [
|
|
895
|
+
const list = []
|
|
896
|
+
|
|
897
|
+
while (true) {
|
|
898
|
+
this.skipWhitespace()
|
|
899
|
+
|
|
900
|
+
if (this.peek() === "]") {
|
|
901
|
+
this.advance()
|
|
902
|
+
break
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
list.push(this.parseValue())
|
|
906
|
+
|
|
907
|
+
this.skipWhitespace()
|
|
908
|
+
if (this.peek() === ",") {
|
|
909
|
+
this.advance()
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
return list
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
parseQuotedAtom() {
|
|
917
|
+
this.advance() // skip '
|
|
918
|
+
let atom = ""
|
|
919
|
+
|
|
920
|
+
while (this.peek() !== "'") {
|
|
921
|
+
if (this.pos >= this.str.length) {
|
|
922
|
+
throw new Error("Unterminated quoted atom")
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
if (this.peek() === "\\") {
|
|
926
|
+
this.advance()
|
|
927
|
+
const escaped = this.peek()
|
|
928
|
+
switch (escaped) {
|
|
929
|
+
case "'":
|
|
930
|
+
atom += "'"
|
|
931
|
+
break
|
|
932
|
+
case "\\":
|
|
933
|
+
atom += "\\"
|
|
934
|
+
break
|
|
935
|
+
case "n":
|
|
936
|
+
atom += "\n"
|
|
937
|
+
break // Literal newline
|
|
938
|
+
case "r":
|
|
939
|
+
atom += "\r"
|
|
940
|
+
break
|
|
941
|
+
case "t":
|
|
942
|
+
atom += "\t"
|
|
943
|
+
break
|
|
944
|
+
default:
|
|
945
|
+
atom += escaped
|
|
946
|
+
}
|
|
947
|
+
this.advance()
|
|
948
|
+
} else {
|
|
949
|
+
atom += this.peek()
|
|
950
|
+
this.advance()
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
this.advance() // skip closing '
|
|
955
|
+
|
|
956
|
+
// Special atoms that become JS primitives
|
|
957
|
+
if (atom === "null") return null
|
|
958
|
+
if (atom === "true") return true
|
|
959
|
+
if (atom === "false") return false
|
|
960
|
+
|
|
961
|
+
return Symbol.for(atom)
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
parseAtomOrNumber() {
|
|
965
|
+
const startPos = this.pos
|
|
966
|
+
let token = ""
|
|
967
|
+
|
|
968
|
+
// Simple approach: collect until we hit a known delimiter
|
|
969
|
+
while (this.pos < this.str.length) {
|
|
970
|
+
const ch = this.peek()
|
|
971
|
+
const ch2 = this.peek(1)
|
|
972
|
+
|
|
973
|
+
// Stop at these delimiters
|
|
974
|
+
if (ch === ",") break
|
|
975
|
+
if (ch === "]") break
|
|
976
|
+
if (ch === "}") break
|
|
977
|
+
if (ch === ")") break
|
|
978
|
+
if (ch === "=" && ch2 === ">") break
|
|
979
|
+
|
|
980
|
+
// Handle backslash specially
|
|
981
|
+
if (ch === "\\") {
|
|
982
|
+
// In unquoted atoms, backslash is just a regular character
|
|
983
|
+
token += ch
|
|
984
|
+
this.advance()
|
|
985
|
+
|
|
986
|
+
// Don't try to interpret the next character as an escape
|
|
987
|
+
if (this.pos < this.str.length) {
|
|
988
|
+
token += this.peek()
|
|
989
|
+
this.advance()
|
|
990
|
+
}
|
|
991
|
+
continue
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// For whitespace, check if it's trailing
|
|
995
|
+
if (/\s/.test(ch)) {
|
|
996
|
+
// Scan ahead to find next non-whitespace
|
|
997
|
+
let i = this.pos
|
|
998
|
+
while (i < this.str.length && /\s/.test(this.str[i])) {
|
|
999
|
+
i++
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Check what comes after whitespace
|
|
1003
|
+
if (i >= this.str.length) {
|
|
1004
|
+
// Hit end of string
|
|
1005
|
+
break
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const nextCh = this.str[i]
|
|
1009
|
+
if (
|
|
1010
|
+
nextCh === "," ||
|
|
1011
|
+
nextCh === "]" ||
|
|
1012
|
+
nextCh === "}" ||
|
|
1013
|
+
nextCh === ")"
|
|
1014
|
+
) {
|
|
1015
|
+
// This whitespace is trailing, not part of atom
|
|
1016
|
+
break
|
|
1017
|
+
}
|
|
1018
|
+
if (
|
|
1019
|
+
nextCh === "=" &&
|
|
1020
|
+
i + 1 < this.str.length &&
|
|
1021
|
+
this.str[i + 1] === ">"
|
|
1022
|
+
) {
|
|
1023
|
+
// Whitespace before =>
|
|
1024
|
+
break
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Include this character in the atom
|
|
1029
|
+
token += ch
|
|
1030
|
+
this.advance()
|
|
1031
|
+
|
|
1032
|
+
// Safety check to prevent infinite loops
|
|
1033
|
+
if (this.pos - startPos > 1000) {
|
|
1034
|
+
throw new Error(`Atom too long at position ${startPos}`)
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Trim only trailing whitespace
|
|
1039
|
+
token = token.trimEnd()
|
|
1040
|
+
|
|
1041
|
+
if (!token) {
|
|
1042
|
+
throw new Error(`Empty atom at position ${this.pos}`)
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Handle special atoms
|
|
1046
|
+
if (token === "null") return null
|
|
1047
|
+
if (token === "true") return true
|
|
1048
|
+
if (token === "false") return false
|
|
1049
|
+
|
|
1050
|
+
// Try to parse as number
|
|
1051
|
+
if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(token)) {
|
|
1052
|
+
return parseFloat(token)
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Return as atom
|
|
1056
|
+
return Symbol.for(token)
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* Format JavaScript values as Erlang term strings
|
|
1062
|
+
* @param {*} value - JavaScript value
|
|
1063
|
+
* @returns {string} Erlang term string
|
|
1064
|
+
*/
|
|
1065
|
+
export function erl_str_to(value) {
|
|
1066
|
+
return formatValue(value)
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// Formatter implementation
|
|
1070
|
+
function formatValue(value) {
|
|
1071
|
+
if (value === null) return "null"
|
|
1072
|
+
if (value === undefined) return "undefined"
|
|
1073
|
+
if (typeof value === "boolean") return value.toString()
|
|
1074
|
+
|
|
1075
|
+
if (typeof value === "number") {
|
|
1076
|
+
return value.toString()
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (typeof value === "string") {
|
|
1080
|
+
// Format as string binary
|
|
1081
|
+
return `<<"${escapeString(value)}">>`
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
if (typeof value === "symbol") {
|
|
1085
|
+
const key = Symbol.keyFor(value)
|
|
1086
|
+
const name = key || value.description || ""
|
|
1087
|
+
|
|
1088
|
+
// Special symbols
|
|
1089
|
+
if (name === "null") return "null"
|
|
1090
|
+
if (name === "true") return "true"
|
|
1091
|
+
if (name === "false") return "false"
|
|
1092
|
+
if (name === "undefined") return "undefined"
|
|
1093
|
+
|
|
1094
|
+
// Check if needs quoting
|
|
1095
|
+
if (/^[a-z][a-zA-Z0-9_]*$/.test(name)) {
|
|
1096
|
+
return name
|
|
1097
|
+
} else {
|
|
1098
|
+
return `'${escapeString(name)}'`
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
if (value instanceof Buffer || value instanceof Uint8Array) {
|
|
1103
|
+
const bytes = Array.from(value)
|
|
1104
|
+
|
|
1105
|
+
// Check if it's printable
|
|
1106
|
+
const isPrintable = bytes.every(b => b >= 32 && b <= 126)
|
|
1107
|
+
|
|
1108
|
+
if (isPrintable) {
|
|
1109
|
+
const str = Buffer.from(value).toString()
|
|
1110
|
+
return `<<"${escapeString(str)}">>`
|
|
1111
|
+
} else {
|
|
1112
|
+
return `<<${bytes.join(",")}>>`
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
if (Array.isArray(value)) {
|
|
1117
|
+
const items = value.map(formatValue)
|
|
1118
|
+
return `[${items.join(",")}]`
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
if (typeof value === "object" && value !== null) {
|
|
1122
|
+
const entries = Object.entries(value).map(([k, v]) => {
|
|
1123
|
+
const key = `<<"${escapeString(k)}">>`
|
|
1124
|
+
return `${key} => ${formatValue(v)}`
|
|
1125
|
+
})
|
|
1126
|
+
return `#{${entries.join(",")}}`
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
return String(value)
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function escapeString(str) {
|
|
1133
|
+
return str
|
|
1134
|
+
.replace(/\\/g, "\\\\")
|
|
1135
|
+
.replace(/"/g, '\\"')
|
|
1136
|
+
.replace(/\n/g, "\\n")
|
|
1137
|
+
.replace(/\r/g, "\\r")
|
|
1138
|
+
.replace(/\t/g, "\\t")
|
|
1139
|
+
}
|