-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstringify.js
More file actions
348 lines (301 loc) · 10.3 KB
/
stringify.js
File metadata and controls
348 lines (301 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
export class Decorated {
constructor(decostr, value) {
validateIdentifier(decostr)
// ?todo: if (decostr === 'bigint' && typeof value !== 'bigint') throw '!!!'
// also for: @date and other builtins
this.decostr = decostr
this.value = value
}
}
/**
*
* @param {string} str
* @returns
*/
export const validateIdentifier = (str) => {
// token(/[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*/u)
if (str.length === 0) {
throw Error("Invalid zero-length identifier")
}
// note: could simplify to that:
if (/^[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*$/u.test(str) === false) {
const codePoints = str[Symbol.iterator]()
const first = codePoints.next().value
if (/[$_\p{ID_Start}]/.test(first) === false) {
throw Error(`Invalid first code point in identifier '${str}': ${first}`)
}
let i = 1
for (const point of codePoints) {
if (/[$\u200c\u200d\p{ID_Continue}]/.test(point) === false) {
throw Error(`Invalid code point #${i} in identifier '${str}': ${point}`)
}
++i
}
console.error('Bug in validateIdentifier!')
throw Error(`Invalid identifier: ${str}`)
}
return true
}
/// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
// note: Well-formed JSON.stringify() is implemented
// todo: many tests
export const stringify = (value, replacer, space) => {
// todo: perhaps accept options object as second parameter
// then providing the third parameter should be invalid
// options would be sth like {replacer, space, mods?, ...}
let indent = ''
if (typeof space === 'number' || space instanceof Number) {
for (let i = 0; i < 10 && i < space; ++i) {
indent += ' '
}
} else if (typeof space === 'string' || space instanceof String) {
indent = space.slice(0, 10)
}
let cindent = ''
let selectProps = null
let replaceFn
if (Array.isArray(replacer)) {
selectProps = new Set()
for (const it of replacer) {
if (typeof it === 'string') selectProps.add(it)
else if (typeof it === 'number'
|| it instanceof String
|| it instanceof Number) selectProps.add(it.toString())
}
} else if (typeof replacer === 'function') {
replaceFn = replacer
}
const opts = {
indent,
cindent,
selectProps,
replaceFn,
key: '',
parent: null,
// ?todo: make this a Map from object to path for better error messages
seen: new Set()
}
return stringifyvalue(value, opts)
}
// note: special treatment of Dates
Date.prototype.toFitzJSON = function() {
return new Decorated('date', this.toISOString())
}
const stringifyvalue = (value, opts) => {
const {replaceFn, key} = opts
if (replaceFn !== undefined) {
const {parent} = opts
value = replaceFn.call(parent, key, value)
}
if (value === null) return 'null'
// unbox primitives
if ( value instanceof Boolean
|| value instanceof Number
|| value instanceof String
|| value instanceof BigInt ) {
value = value.valueOf()
}
// note: cycle-detect as early as possible
if (typeof value === 'object') {
if (opts.seen.has(value)) throw TypeError(`Converting circular structure to fitzJSON`)
opts.seen.add(value)
}
if (typeof value.toFitzJSON === 'function') {
value = value.toFitzJSON(key)
} else if (typeof value.toJSON === 'function') {
value = value.toJSON(key)
}
//
// simple types
//
if (value === true) return 'true'
if (value === false) return 'false'
if (Number.isNaN(value)) return 'NaN'
if (typeof value === 'number') return value.toString()
if (typeof value === 'bigint') return `@bigint ${value.toString()}`
if (typeof value === 'string') return stringifystring(value, opts)
//
// disappearing types
//
// ?todo: move further up?
// todo: perhaps stringify symbol and undefined to something more useful
if (typeof value === 'function') return undefined
if (typeof value === 'symbol') return undefined
if (typeof value === 'undefined') return undefined
//
// complex types
//
if (Array.isArray(value)) return stringifyarray(value, opts)
// todo: perhaps serialize Map as @Map or object as @object
// todo?: perhopas serialize Set as @Set [...]
if (value instanceof Map) return stringifymap(value, opts)
if (value instanceof Decorated) return `@${value.decostr} ${stringifyvalue(value.value, opts)}`
if (typeof value === 'object') return stringifyobject(value, opts)
throw Error('bug in stringify')
}
// note: Well-formed JSON.stringify() is implemented
// todo: consider escaping \u2028 and \u2029 the way firefox does
const stringifystring = (value, opts) => {
// jsonstring: $ => choice(
// '""',
// seq('"', $.string_content, token.immediate('"'))
// ),
if (value === "") return '""'
// ?todo: instead of building string_content char by char, build it slice by slice
let string_content = ''
let isLeading = false, prev = ''
// note: can't use for...of because we want to check for lone surrogates
// so we must look at code units rather than code points
for (let i = 0; i < value.length; ++i) {
const c = value[i]
if (isLeading) {
isLeading = false
if (c >= '\uDC00' && c <= '\uDFFF') {
// ok -- correct surrogate pair -- insert code units unescaped
string_content += prev + c
continue
} else {
// incorrect surrogate pair -- insert previous escaped, process current code unit normally
string_content += '\\u' + prev.charCodeAt(0).toString(16)
}
}
if (c === '"') string_content += '\\"'
else if (c === '\\') string_content += '\\\\'
else if (c === '\b') string_content += '\\b'
else if (c === '\f') string_content += '\\f'
else if (c === '\n') string_content += '\\n'
else if (c === '\r') string_content += '\\r'
else if (c === '\t') string_content += '\\t'
else if (c <= '\u001F' && c >= '\u0010') string_content += '\\u00' + c.toString(16)
else if (c < '\u0010') string_content += '\\u000' + c.toString(16)
else if (c >= '\uD800' && c <= '\uDBFF') {
// possibly leading surrogate -- wait and see if trailing is next
isLeading = true
prev = c
} else if (c >= '\uDC00' && c <= '\uDFFF') {
// trailing surrogate without leading -- escape
string_content += '\\u' + (c).charCodeAt(0).toString(16)
}
// todo: perhaps option to \u escape characters > 255 or so
else string_content += c
}
// process outstanding lone leading surrogate
if (isLeading) {
// incorrect surrogate pair -- insert previous escaped, process current code unit normally
string_content += '\\u' + prev.charCodeAt(0).toString(16)
}
return `"${string_content}"`
// string_content: $ => repeat1(choice(
// // a character is: [\u0020-\u10FFFF] - '"' - '\'
// // in other words: any code point except control characters and '"' and '\'
// // we will express that with a negated character class
// // note: U+0001–U+001F are the control characters
// token.immediate(prec(1, /[^\\"\u0001-\u001F]+/)),
// $.escape_sequence,
// )),
// escape_sequence: $ => token.immediate(seq(
// '\\',
// /(\"|\\|\/|b|f|n|r|t|u[0-9a-fA-F]{4})/
// )),
// return JSON.stringify(value)
}
const stringifyarray = (value, opts) => {
// item: $ => //falias($, 'value',
// seq(
// falias($, 'decorators', repeat($.decorator)),
// field('disabled', optional($.disabled)),
// $._plainval,
// falias($, 'pipes', repeat($.pipe)),
// ),
// list: $ => seq('[',
// items($),
// ']'),
// const items = $ => sep(
// $.item,
// $._valsep,
// )
// const sep = (item, valsep) => seq(
// repeat(seq(item, valsep)),
// optional(item),
// )
if (value.length === 0) return '[]'
const {indent} = opts
const opts2 = {...opts, parent: value}
if (indent === '') {
const items = stringifyarrayitems(value, opts2)
return `[${items.join(',')}]`
}
const {cindent} = opts2
const ncindent = cindent + indent
const nopts = {
...opts2,
cindent: ncindent
}
const items = stringifyarrayitems(value, nopts)
return `[\n${ncindent}${items.join(`,\n${ncindent}`)}\n${cindent}]`
}
const stringifyarrayitems = (value, opts) => {
const items = []
for (let i = 0; i < value.length; ++i) {
const it = value[i]
const str = stringifyvalue(it, {...opts, key: i.toString()})
items.push(str === undefined? 'null': str)
}
return items
}
/**
*
* @param {Map} value
* @returns
*/
const stringifymap = (value, opts) => {
const entries = [...value.entries()]
// for now we'll support only string keys
for (const [k, v] of entries) {
if (typeof k !== 'string') throw Error('oops')
}
return stringifyentries(entries, {...opts, parent: value})
}
const stringifyobject = (value, opts) => {
const entries = Object.entries(value)
return stringifyentries(entries, {...opts, parent: value})
}
const stringifyentries = (entries, opts) => {
// entry: $ => prec(2, seq(
// falias($, 'decorators', repeat($.decorator)),
// field('disabled', optional($.disabled)),
// field('key', $.key),
// falias($, 'pipes', repeat($.pipe)),
// // note: /(\s)/ has to have the parens; otherwise tree-sitter somehow conflates this with /\s/ in extras and very weird things start happening, such as not respecting token.immediate and accepting jsonstrings with spaces
// choice(':', /(\s)/),
// field('value', $.value),
// )),
if (entries.length === 0) return '{}'
const {selectProps} = opts
const selectedEntries = selectProps === null?
entries:
entries.filter(([k, v]) => selectProps.has(k))
const {indent} = opts
if (indent === '') {
const its = stringifyentriesitems(selectedEntries, opts, ':')
return `{${its.join(',')}}`
}
const {cindent} = opts
const ncindent = cindent + indent
const nopts = {
...opts,
cindent: ncindent
}
const its = stringifyentriesitems(selectedEntries, nopts, ': ')
return `{\n${ncindent}${its.join(`,\n${ncindent}`)}\n${cindent}}`
}
const stringifyentriesitems = (selectedEntries, opts, sep) => {
const its = []
for (const [k, v] of selectedEntries) {
const nopts = {...opts, key: k}
const value = stringifyvalue(v, nopts)
if (value === undefined) continue
its.push(stringifystring(k, nopts) + sep + value)
}
return its
}