312 lines
7.8 KiB
JavaScript
312 lines
7.8 KiB
JavaScript
|
|
import { Type } from '../token.js'
|
||
|
|
import { encodeCustom } from '../encode.js'
|
||
|
|
import { encodeErrPrefix } from '../common.js'
|
||
|
|
import { asU8A, fromString } from '../byte-utils.js'
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @typedef {import('../../interface').EncodeOptions} EncodeOptions
|
||
|
|
* @typedef {import('../token').Token} Token
|
||
|
|
* @typedef {import('../bl').Bl} Bl
|
||
|
|
*/
|
||
|
|
|
||
|
|
class JSONEncoder extends Array {
|
||
|
|
constructor () {
|
||
|
|
super()
|
||
|
|
/** @type {{type:Type,elements:number}[]} */
|
||
|
|
this.inRecursive = []
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {Bl} buf
|
||
|
|
*/
|
||
|
|
prefix (buf) {
|
||
|
|
const recurs = this.inRecursive[this.inRecursive.length - 1]
|
||
|
|
if (recurs) {
|
||
|
|
if (recurs.type === Type.array) {
|
||
|
|
recurs.elements++
|
||
|
|
if (recurs.elements !== 1) { // >first
|
||
|
|
buf.push([44]) // ','
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (recurs.type === Type.map) {
|
||
|
|
recurs.elements++
|
||
|
|
if (recurs.elements !== 1) { // >first
|
||
|
|
if (recurs.elements % 2 === 1) { // key
|
||
|
|
buf.push([44]) // ','
|
||
|
|
} else {
|
||
|
|
buf.push([58]) // ':'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {Bl} buf
|
||
|
|
* @param {Token} token
|
||
|
|
*/
|
||
|
|
[Type.uint.major] (buf, token) {
|
||
|
|
this.prefix(buf)
|
||
|
|
const is = String(token.value)
|
||
|
|
const isa = []
|
||
|
|
for (let i = 0; i < is.length; i++) {
|
||
|
|
isa[i] = is.charCodeAt(i)
|
||
|
|
}
|
||
|
|
buf.push(isa)
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {Bl} buf
|
||
|
|
* @param {Token} token
|
||
|
|
*/
|
||
|
|
[Type.negint.major] (buf, token) {
|
||
|
|
// @ts-ignore hack
|
||
|
|
this[Type.uint.major](buf, token)
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {Bl} _buf
|
||
|
|
* @param {Token} _token
|
||
|
|
*/
|
||
|
|
[Type.bytes.major] (_buf, _token) {
|
||
|
|
throw new Error(`${encodeErrPrefix} unsupported type: Uint8Array`)
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {Bl} buf
|
||
|
|
* @param {Token} token
|
||
|
|
*/
|
||
|
|
[Type.string.major] (buf, token) {
|
||
|
|
this.prefix(buf)
|
||
|
|
// buf.push(34) // '"'
|
||
|
|
// encodeUtf8(token.value, byts)
|
||
|
|
// buf.push(34) // '"'
|
||
|
|
const byts = fromString(JSON.stringify(token.value))
|
||
|
|
buf.push(byts.length > 32 ? asU8A(byts) : byts)
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {Bl} buf
|
||
|
|
* @param {Token} _token
|
||
|
|
*/
|
||
|
|
[Type.array.major] (buf, _token) {
|
||
|
|
this.prefix(buf)
|
||
|
|
this.inRecursive.push({ type: Type.array, elements: 0 })
|
||
|
|
buf.push([91]) // '['
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {Bl} buf
|
||
|
|
* @param {Token} _token
|
||
|
|
*/
|
||
|
|
[Type.map.major] (buf, _token) {
|
||
|
|
this.prefix(buf)
|
||
|
|
this.inRecursive.push({ type: Type.map, elements: 0 })
|
||
|
|
buf.push([123]) // '{'
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {Bl} _buf
|
||
|
|
* @param {Token} _token
|
||
|
|
*/
|
||
|
|
[Type.tag.major] (_buf, _token) {}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {Bl} buf
|
||
|
|
* @param {Token} token
|
||
|
|
*/
|
||
|
|
[Type.float.major] (buf, token) {
|
||
|
|
if (token.type.name === 'break') {
|
||
|
|
const recurs = this.inRecursive.pop()
|
||
|
|
if (recurs) {
|
||
|
|
if (recurs.type === Type.array) {
|
||
|
|
buf.push([93]) // ']'
|
||
|
|
} else if (recurs.type === Type.map) {
|
||
|
|
buf.push([125]) // '}'
|
||
|
|
/* c8 ignore next 3 */
|
||
|
|
} else {
|
||
|
|
throw new Error('Unexpected recursive type; this should not happen!')
|
||
|
|
}
|
||
|
|
return
|
||
|
|
}
|
||
|
|
/* c8 ignore next 2 */
|
||
|
|
throw new Error('Unexpected break; this should not happen!')
|
||
|
|
}
|
||
|
|
if (token.value === undefined) {
|
||
|
|
throw new Error(`${encodeErrPrefix} unsupported type: undefined`)
|
||
|
|
}
|
||
|
|
|
||
|
|
this.prefix(buf)
|
||
|
|
if (token.type.name === 'true') {
|
||
|
|
buf.push([116, 114, 117, 101]) // 'true'
|
||
|
|
return
|
||
|
|
} else if (token.type.name === 'false') {
|
||
|
|
buf.push([102, 97, 108, 115, 101]) // 'false'
|
||
|
|
return
|
||
|
|
} else if (token.type.name === 'null') {
|
||
|
|
buf.push([110, 117, 108, 108]) // 'null'
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// number
|
||
|
|
const is = String(token.value)
|
||
|
|
const isa = []
|
||
|
|
let dp = false
|
||
|
|
for (let i = 0; i < is.length; i++) {
|
||
|
|
isa[i] = is.charCodeAt(i)
|
||
|
|
if (!dp && (isa[i] === 46 || isa[i] === 101 || isa[i] === 69)) { // '[.eE]'
|
||
|
|
dp = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!dp) { // need a decimal point for floats
|
||
|
|
isa.push(46) // '.'
|
||
|
|
isa.push(48) // '0'
|
||
|
|
}
|
||
|
|
buf.push(isa)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// The below code is mostly taken and modified from https://github.com/feross/buffer
|
||
|
|
// Licensed MIT. Copyright (c) Feross Aboukhadijeh
|
||
|
|
// function encodeUtf8 (string, byts) {
|
||
|
|
// let codePoint
|
||
|
|
// const length = string.length
|
||
|
|
// let leadSurrogate = null
|
||
|
|
|
||
|
|
// for (let i = 0; i < length; ++i) {
|
||
|
|
// codePoint = string.charCodeAt(i)
|
||
|
|
|
||
|
|
// // is surrogate component
|
||
|
|
// if (codePoint > 0xd7ff && codePoint < 0xe000) {
|
||
|
|
// // last char was a lead
|
||
|
|
// if (!leadSurrogate) {
|
||
|
|
// // no lead yet
|
||
|
|
// /* c8 ignore next 9 */
|
||
|
|
// if (codePoint > 0xdbff) {
|
||
|
|
// // unexpected trail
|
||
|
|
// byts.push(0xef, 0xbf, 0xbd)
|
||
|
|
// continue
|
||
|
|
// } else if (i + 1 === length) {
|
||
|
|
// // unpaired lead
|
||
|
|
// byts.push(0xef, 0xbf, 0xbd)
|
||
|
|
// continue
|
||
|
|
// }
|
||
|
|
|
||
|
|
// // valid lead
|
||
|
|
// leadSurrogate = codePoint
|
||
|
|
|
||
|
|
// continue
|
||
|
|
// }
|
||
|
|
|
||
|
|
// // 2 leads in a row
|
||
|
|
// /* c8 ignore next 5 */
|
||
|
|
// if (codePoint < 0xdc00) {
|
||
|
|
// byts.push(0xef, 0xbf, 0xbd)
|
||
|
|
// leadSurrogate = codePoint
|
||
|
|
// continue
|
||
|
|
// }
|
||
|
|
|
||
|
|
// // valid surrogate pair
|
||
|
|
// codePoint = (leadSurrogate - 0xd800 << 10 | codePoint - 0xdc00) + 0x10000
|
||
|
|
// /* c8 ignore next 4 */
|
||
|
|
// } else if (leadSurrogate) {
|
||
|
|
// // valid bmp char, but last char was a lead
|
||
|
|
// byts.push(0xef, 0xbf, 0xbd)
|
||
|
|
// }
|
||
|
|
|
||
|
|
// leadSurrogate = null
|
||
|
|
|
||
|
|
// // encode utf8
|
||
|
|
// if (codePoint < 0x80) {
|
||
|
|
// // special JSON escapes
|
||
|
|
// switch (codePoint) {
|
||
|
|
// case 8: // '\b'
|
||
|
|
// byts.push(92, 98) // '\\b'
|
||
|
|
// continue
|
||
|
|
// case 9: // '\t'
|
||
|
|
// byts.push(92, 116) // '\\t'
|
||
|
|
// continue
|
||
|
|
// case 10: // '\n'
|
||
|
|
// byts.push(92, 110) // '\\n'
|
||
|
|
// continue
|
||
|
|
// case 12: // '\f'
|
||
|
|
// byts.push(92, 102) // '\\f'
|
||
|
|
// continue
|
||
|
|
// case 13: // '\r'
|
||
|
|
// byts.push(92, 114) // '\\r'
|
||
|
|
// continue
|
||
|
|
// case 34: // '"'
|
||
|
|
// byts.push(92, 34) // '\\"'
|
||
|
|
// continue
|
||
|
|
// case 92: // '\\'
|
||
|
|
// byts.push(92, 92) // '\\\\'
|
||
|
|
// continue
|
||
|
|
// }
|
||
|
|
|
||
|
|
// byts.push(codePoint)
|
||
|
|
// } else if (codePoint < 0x800) {
|
||
|
|
// /* c8 ignore next 1 */
|
||
|
|
// byts.push(
|
||
|
|
// codePoint >> 0x6 | 0xc0,
|
||
|
|
// codePoint & 0x3f | 0x80
|
||
|
|
// )
|
||
|
|
// } else if (codePoint < 0x10000) {
|
||
|
|
// /* c8 ignore next 1 */
|
||
|
|
// byts.push(
|
||
|
|
// codePoint >> 0xc | 0xe0,
|
||
|
|
// codePoint >> 0x6 & 0x3f | 0x80,
|
||
|
|
// codePoint & 0x3f | 0x80
|
||
|
|
// )
|
||
|
|
// /* c8 ignore next 9 */
|
||
|
|
// } else if (codePoint < 0x110000) {
|
||
|
|
// byts.push(
|
||
|
|
// codePoint >> 0x12 | 0xf0,
|
||
|
|
// codePoint >> 0xc & 0x3f | 0x80,
|
||
|
|
// codePoint >> 0x6 & 0x3f | 0x80,
|
||
|
|
// codePoint & 0x3f | 0x80
|
||
|
|
// )
|
||
|
|
// } else {
|
||
|
|
// /* c8 ignore next 2 */
|
||
|
|
// throw new Error('Invalid code point')
|
||
|
|
// }
|
||
|
|
// }
|
||
|
|
// }
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {(Token|Token[])[]} e1
|
||
|
|
* @param {(Token|Token[])[]} e2
|
||
|
|
* @returns {number}
|
||
|
|
*/
|
||
|
|
function mapSorter (e1, e2) {
|
||
|
|
if (Array.isArray(e1[0]) || Array.isArray(e2[0])) {
|
||
|
|
throw new Error(`${encodeErrPrefix} complex map keys are not supported`)
|
||
|
|
}
|
||
|
|
const keyToken1 = e1[0]
|
||
|
|
const keyToken2 = e2[0]
|
||
|
|
if (keyToken1.type !== Type.string || keyToken2.type !== Type.string) {
|
||
|
|
throw new Error(`${encodeErrPrefix} non-string map keys are not supported`)
|
||
|
|
}
|
||
|
|
if (keyToken1 < keyToken2) {
|
||
|
|
return -1
|
||
|
|
}
|
||
|
|
if (keyToken1 > keyToken2) {
|
||
|
|
return 1
|
||
|
|
}
|
||
|
|
/* c8 ignore next 1 */
|
||
|
|
throw new Error(`${encodeErrPrefix} unexpected duplicate map keys, this is not supported`)
|
||
|
|
}
|
||
|
|
|
||
|
|
const defaultEncodeOptions = { addBreakTokens: true, mapSorter }
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {any} data
|
||
|
|
* @param {EncodeOptions} [options]
|
||
|
|
* @returns {Uint8Array}
|
||
|
|
*/
|
||
|
|
function encode (data, options) {
|
||
|
|
options = Object.assign({}, defaultEncodeOptions, options)
|
||
|
|
return encodeCustom(data, new JSONEncoder(), options)
|
||
|
|
}
|
||
|
|
|
||
|
|
export { encode }
|