mirror of
https://github.com/idanoo/GoScrobble.git
synced 2024-11-26 02:15:15 +00:00
721 lines
14 KiB
JavaScript
721 lines
14 KiB
JavaScript
'use strict'
|
|
|
|
const check = require('check-types')
|
|
const error = require('./error')
|
|
const EventEmitter = require('events').EventEmitter
|
|
const events = require('./events')
|
|
const promise = require('./promise')
|
|
|
|
const terminators = {
|
|
obj: '}',
|
|
arr: ']'
|
|
}
|
|
|
|
const escapes = {
|
|
/* eslint-disable quote-props */
|
|
'"': '"',
|
|
'\\': '\\',
|
|
'/': '/',
|
|
'b': '\b',
|
|
'f': '\f',
|
|
'n': '\n',
|
|
'r': '\r',
|
|
't': '\t'
|
|
/* eslint-enable quote-props */
|
|
}
|
|
|
|
module.exports = initialise
|
|
|
|
/**
|
|
* Public function `walk`.
|
|
*
|
|
* Returns an event emitter and asynchronously walks a stream of JSON data,
|
|
* emitting events as it encounters tokens. The event emitter is decorated
|
|
* with a `pause` method that can be called to pause processing.
|
|
*
|
|
* @param stream: Readable instance representing the incoming JSON.
|
|
*
|
|
* @option yieldRate: The number of data items to process per timeslice,
|
|
* default is 16384.
|
|
*
|
|
* @option Promise: The promise constructor to use, defaults to bluebird.
|
|
*
|
|
* @option ndjson: Set this to true to parse newline-delimited JSON.
|
|
**/
|
|
function initialise (stream, options = {}) {
|
|
check.assert.instanceStrict(stream, require('stream').Readable, 'Invalid stream argument')
|
|
|
|
const currentPosition = {
|
|
line: 1,
|
|
column: 1
|
|
}
|
|
const emitter = new EventEmitter()
|
|
const handlers = {
|
|
arr: value,
|
|
obj: property
|
|
}
|
|
const json = []
|
|
const lengths = []
|
|
const previousPosition = {}
|
|
const Promise = promise(options)
|
|
const scopes = []
|
|
const yieldRate = options.yieldRate || 16384
|
|
const shouldHandleNdjson = !! options.ndjson
|
|
|
|
let index = 0
|
|
let isStreamEnded = false
|
|
let isWalkBegun = false
|
|
let isWalkEnded = false
|
|
let isWalkingString = false
|
|
let hasEndedLine = true
|
|
let count = 0
|
|
let resumeFn
|
|
let pause
|
|
let cachedCharacter
|
|
|
|
stream.setEncoding('utf8')
|
|
stream.on('data', readStream)
|
|
stream.on('end', endStream)
|
|
stream.on('error', err => {
|
|
emitter.emit(events.error, err)
|
|
endStream()
|
|
})
|
|
|
|
emitter.pause = () => {
|
|
let resolve
|
|
pause = new Promise(res => resolve = res)
|
|
return () => {
|
|
pause = null
|
|
count = 0
|
|
|
|
if (shouldHandleNdjson && isStreamEnded && isWalkEnded) {
|
|
emit(events.end)
|
|
} else {
|
|
resolve()
|
|
}
|
|
}
|
|
}
|
|
|
|
return emitter
|
|
|
|
function readStream (chunk) {
|
|
addChunk(chunk)
|
|
|
|
if (isWalkBegun) {
|
|
return resume()
|
|
}
|
|
|
|
isWalkBegun = true
|
|
value()
|
|
}
|
|
|
|
function addChunk (chunk) {
|
|
json.push(chunk)
|
|
|
|
const chunkLength = chunk.length
|
|
lengths.push({
|
|
item: chunkLength,
|
|
aggregate: length() + chunkLength
|
|
})
|
|
}
|
|
|
|
function length () {
|
|
const chunkCount = lengths.length
|
|
|
|
if (chunkCount === 0) {
|
|
return 0
|
|
}
|
|
|
|
return lengths[chunkCount - 1].aggregate
|
|
}
|
|
|
|
function value () {
|
|
/* eslint-disable no-underscore-dangle */
|
|
if (++count % yieldRate !== 0) {
|
|
return _do()
|
|
}
|
|
|
|
return new Promise(resolve => {
|
|
setImmediate(() => _do().then(resolve))
|
|
})
|
|
|
|
function _do () {
|
|
return awaitNonWhitespace()
|
|
.then(next)
|
|
.then(handleValue)
|
|
.catch(() => {})
|
|
}
|
|
/* eslint-enable no-underscore-dangle */
|
|
}
|
|
|
|
function awaitNonWhitespace () {
|
|
return wait()
|
|
|
|
function wait () {
|
|
return awaitCharacter()
|
|
.then(step)
|
|
}
|
|
|
|
function step () {
|
|
if (isWhitespace(character())) {
|
|
return next().then(wait)
|
|
}
|
|
}
|
|
}
|
|
|
|
function awaitCharacter () {
|
|
let resolve, reject
|
|
|
|
if (index < length()) {
|
|
return Promise.resolve()
|
|
}
|
|
|
|
if (isStreamEnded) {
|
|
setImmediate(endWalk)
|
|
return Promise.reject()
|
|
}
|
|
|
|
resumeFn = after
|
|
|
|
return new Promise((res, rej) => {
|
|
resolve = res
|
|
reject = rej
|
|
})
|
|
|
|
function after () {
|
|
if (index < length()) {
|
|
return resolve()
|
|
}
|
|
|
|
reject()
|
|
|
|
if (isStreamEnded) {
|
|
setImmediate(endWalk)
|
|
}
|
|
}
|
|
}
|
|
|
|
function character () {
|
|
if (cachedCharacter) {
|
|
return cachedCharacter
|
|
}
|
|
|
|
if (lengths[0].item > index) {
|
|
return cachedCharacter = json[0][index]
|
|
}
|
|
|
|
const len = lengths.length
|
|
for (let i = 1; i < len; ++i) {
|
|
const { aggregate, item } = lengths[i]
|
|
if (aggregate > index) {
|
|
return cachedCharacter = json[i][index + item - aggregate]
|
|
}
|
|
}
|
|
}
|
|
|
|
function isWhitespace (char) {
|
|
switch (char) {
|
|
case '\n':
|
|
if (shouldHandleNdjson && scopes.length === 0) {
|
|
return false
|
|
}
|
|
case ' ':
|
|
case '\t':
|
|
case '\r':
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
function next () {
|
|
return awaitCharacter().then(after)
|
|
|
|
function after () {
|
|
const result = character()
|
|
|
|
cachedCharacter = null
|
|
index += 1
|
|
previousPosition.line = currentPosition.line
|
|
previousPosition.column = currentPosition.column
|
|
|
|
if (result === '\n') {
|
|
currentPosition.line += 1
|
|
currentPosition.column = 1
|
|
} else {
|
|
currentPosition.column += 1
|
|
}
|
|
|
|
if (index > lengths[0].aggregate) {
|
|
json.shift()
|
|
|
|
const difference = lengths.shift().item
|
|
index -= difference
|
|
|
|
lengths.forEach(len => len.aggregate -= difference)
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|
|
|
|
function handleValue (char) {
|
|
if (shouldHandleNdjson && scopes.length === 0) {
|
|
if (char === '\n') {
|
|
hasEndedLine = true
|
|
return emit(events.endLine)
|
|
.then(value)
|
|
}
|
|
|
|
if (! hasEndedLine) {
|
|
return fail(char, '\n', previousPosition)
|
|
.then(value)
|
|
}
|
|
|
|
hasEndedLine = false
|
|
}
|
|
|
|
switch (char) {
|
|
case '[':
|
|
return array()
|
|
case '{':
|
|
return object()
|
|
case '"':
|
|
return string()
|
|
case '0':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
case '-':
|
|
case '.':
|
|
return number(char)
|
|
case 'f':
|
|
return literalFalse()
|
|
case 'n':
|
|
return literalNull()
|
|
case 't':
|
|
return literalTrue()
|
|
default:
|
|
return fail(char, 'value', previousPosition)
|
|
.then(value)
|
|
}
|
|
}
|
|
|
|
function array () {
|
|
return scope(events.array, value)
|
|
}
|
|
|
|
function scope (event, contentHandler) {
|
|
return emit(event)
|
|
.then(() => {
|
|
scopes.push(event)
|
|
return endScope(event)
|
|
})
|
|
.then(contentHandler)
|
|
}
|
|
|
|
function emit (...args) {
|
|
return (pause || Promise.resolve())
|
|
.then(() => {
|
|
try {
|
|
emitter.emit(...args)
|
|
} catch (err) {
|
|
try {
|
|
emitter.emit(events.error, err)
|
|
} catch (_) {
|
|
// When calling user code, anything is possible
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
function endScope (scp) {
|
|
return awaitNonWhitespace()
|
|
.then(() => {
|
|
if (character() === terminators[scp]) {
|
|
return emit(events.endPrefix + scp)
|
|
.then(() => {
|
|
scopes.pop()
|
|
return next()
|
|
})
|
|
.then(endValue)
|
|
}
|
|
})
|
|
.catch(endWalk)
|
|
}
|
|
|
|
function endValue () {
|
|
return awaitNonWhitespace()
|
|
.then(after)
|
|
.catch(endWalk)
|
|
|
|
function after () {
|
|
if (scopes.length === 0) {
|
|
if (shouldHandleNdjson) {
|
|
return value()
|
|
}
|
|
|
|
return fail(character(), 'EOF', currentPosition)
|
|
.then(value)
|
|
}
|
|
|
|
return checkScope()
|
|
}
|
|
|
|
function checkScope () {
|
|
const scp = scopes[scopes.length - 1]
|
|
const handler = handlers[scp]
|
|
|
|
return endScope(scp)
|
|
.then(() => {
|
|
if (scopes.length > 0) {
|
|
return checkCharacter(character(), ',', currentPosition)
|
|
}
|
|
})
|
|
.then(result => {
|
|
if (result) {
|
|
return next()
|
|
}
|
|
})
|
|
.then(handler)
|
|
}
|
|
}
|
|
|
|
function fail (actual, expected, position) {
|
|
return emit(
|
|
events.dataError,
|
|
error.create(
|
|
actual,
|
|
expected,
|
|
position.line,
|
|
position.column
|
|
)
|
|
)
|
|
}
|
|
|
|
function checkCharacter (char, expected, position) {
|
|
if (char === expected) {
|
|
return Promise.resolve(true)
|
|
}
|
|
|
|
return fail(char, expected, position)
|
|
.then(false)
|
|
}
|
|
|
|
function object () {
|
|
return scope(events.object, property)
|
|
}
|
|
|
|
function property () {
|
|
return awaitNonWhitespace()
|
|
.then(next)
|
|
.then(propertyName)
|
|
}
|
|
|
|
function propertyName (char) {
|
|
return checkCharacter(char, '"', previousPosition)
|
|
.then(() => walkString(events.property))
|
|
.then(awaitNonWhitespace)
|
|
.then(next)
|
|
.then(propertyValue)
|
|
}
|
|
|
|
function propertyValue (char) {
|
|
return checkCharacter(char, ':', previousPosition)
|
|
.then(value)
|
|
}
|
|
|
|
function walkString (event) {
|
|
let isEscaping = false
|
|
const str = []
|
|
|
|
isWalkingString = true
|
|
|
|
return next().then(step)
|
|
|
|
function step (char) {
|
|
if (isEscaping) {
|
|
isEscaping = false
|
|
|
|
return escape(char).then(escaped => {
|
|
str.push(escaped)
|
|
return next().then(step)
|
|
})
|
|
}
|
|
|
|
if (char === '\\') {
|
|
isEscaping = true
|
|
return next().then(step)
|
|
}
|
|
|
|
if (char !== '"') {
|
|
str.push(char)
|
|
return next().then(step)
|
|
}
|
|
|
|
isWalkingString = false
|
|
return emit(event, str.join(''))
|
|
}
|
|
}
|
|
|
|
function escape (char) {
|
|
if (escapes[char]) {
|
|
return Promise.resolve(escapes[char])
|
|
}
|
|
|
|
if (char === 'u') {
|
|
return escapeHex()
|
|
}
|
|
|
|
return fail(char, 'escape character', previousPosition)
|
|
.then(() => `\\${char}`)
|
|
}
|
|
|
|
function escapeHex () {
|
|
let hexits = []
|
|
|
|
return next().then(step.bind(null, 0))
|
|
|
|
function step (idx, char) {
|
|
if (isHexit(char)) {
|
|
hexits.push(char)
|
|
}
|
|
|
|
if (idx < 3) {
|
|
return next().then(step.bind(null, idx + 1))
|
|
}
|
|
|
|
hexits = hexits.join('')
|
|
|
|
if (hexits.length === 4) {
|
|
return String.fromCharCode(parseInt(hexits, 16))
|
|
}
|
|
|
|
return fail(char, 'hex digit', previousPosition)
|
|
.then(() => `\\u${hexits}${char}`)
|
|
}
|
|
}
|
|
|
|
function string () {
|
|
return walkString(events.string).then(endValue)
|
|
}
|
|
|
|
function number (firstCharacter) {
|
|
let digits = [ firstCharacter ]
|
|
|
|
return walkDigits().then(addDigits.bind(null, checkDecimalPlace))
|
|
|
|
function addDigits (step, result) {
|
|
digits = digits.concat(result.digits)
|
|
|
|
if (result.atEnd) {
|
|
return endNumber()
|
|
}
|
|
|
|
return step()
|
|
}
|
|
|
|
function checkDecimalPlace () {
|
|
if (character() === '.') {
|
|
return next()
|
|
.then(char => {
|
|
digits.push(char)
|
|
return walkDigits()
|
|
})
|
|
.then(addDigits.bind(null, checkExponent))
|
|
}
|
|
|
|
return checkExponent()
|
|
}
|
|
|
|
function checkExponent () {
|
|
if (character() === 'e' || character() === 'E') {
|
|
return next()
|
|
.then(char => {
|
|
digits.push(char)
|
|
return awaitCharacter()
|
|
})
|
|
.then(checkSign)
|
|
.catch(fail.bind(null, 'EOF', 'exponent', currentPosition))
|
|
}
|
|
|
|
return endNumber()
|
|
}
|
|
|
|
function checkSign () {
|
|
if (character() === '+' || character() === '-') {
|
|
return next().then(char => {
|
|
digits.push(char)
|
|
return readExponent()
|
|
})
|
|
}
|
|
|
|
return readExponent()
|
|
}
|
|
|
|
function readExponent () {
|
|
return walkDigits().then(addDigits.bind(null, endNumber))
|
|
}
|
|
|
|
function endNumber () {
|
|
return emit(events.number, parseFloat(digits.join('')))
|
|
.then(endValue)
|
|
}
|
|
}
|
|
|
|
function walkDigits () {
|
|
const digits = []
|
|
|
|
return wait()
|
|
|
|
function wait () {
|
|
return awaitCharacter()
|
|
.then(step)
|
|
.catch(atEnd)
|
|
}
|
|
|
|
function step () {
|
|
if (isDigit(character())) {
|
|
return next().then(char => {
|
|
digits.push(char)
|
|
return wait()
|
|
})
|
|
}
|
|
|
|
return { digits, atEnd: false }
|
|
}
|
|
|
|
function atEnd () {
|
|
return { digits, atEnd: true }
|
|
}
|
|
}
|
|
|
|
function literalFalse () {
|
|
return literal([ 'a', 'l', 's', 'e' ], false)
|
|
}
|
|
|
|
function literal (expectedCharacters, val) {
|
|
let actual, expected, invalid
|
|
|
|
return wait()
|
|
|
|
function wait () {
|
|
return awaitCharacter()
|
|
.then(step)
|
|
.catch(atEnd)
|
|
}
|
|
|
|
function step () {
|
|
if (invalid || expectedCharacters.length === 0) {
|
|
return atEnd()
|
|
}
|
|
|
|
return next().then(afterNext)
|
|
}
|
|
|
|
function atEnd () {
|
|
return Promise.resolve()
|
|
.then(() => {
|
|
if (invalid) {
|
|
return fail(actual, expected, previousPosition)
|
|
}
|
|
|
|
if (expectedCharacters.length > 0) {
|
|
return fail('EOF', expectedCharacters.shift(), currentPosition)
|
|
}
|
|
|
|
return done()
|
|
})
|
|
.then(endValue)
|
|
}
|
|
|
|
function afterNext (char) {
|
|
actual = char
|
|
expected = expectedCharacters.shift()
|
|
|
|
if (actual !== expected) {
|
|
invalid = true
|
|
}
|
|
|
|
return wait()
|
|
}
|
|
|
|
function done () {
|
|
return emit(events.literal, val)
|
|
}
|
|
}
|
|
|
|
function literalNull () {
|
|
return literal([ 'u', 'l', 'l' ], null)
|
|
}
|
|
|
|
function literalTrue () {
|
|
return literal([ 'r', 'u', 'e' ], true)
|
|
}
|
|
|
|
function endStream () {
|
|
isStreamEnded = true
|
|
|
|
if (isWalkBegun) {
|
|
return resume()
|
|
}
|
|
|
|
endWalk()
|
|
}
|
|
|
|
function resume () {
|
|
if (resumeFn) {
|
|
resumeFn()
|
|
resumeFn = null
|
|
}
|
|
}
|
|
|
|
function endWalk () {
|
|
if (isWalkEnded) {
|
|
return Promise.resolve()
|
|
}
|
|
|
|
isWalkEnded = true
|
|
|
|
return Promise.resolve()
|
|
.then(() => {
|
|
if (isWalkingString) {
|
|
return fail('EOF', '"', currentPosition)
|
|
}
|
|
})
|
|
.then(popScopes)
|
|
.then(() => emit(events.end))
|
|
}
|
|
|
|
function popScopes () {
|
|
if (scopes.length === 0) {
|
|
return Promise.resolve()
|
|
}
|
|
|
|
return fail('EOF', terminators[scopes.pop()], currentPosition)
|
|
.then(popScopes)
|
|
}
|
|
}
|
|
|
|
function isHexit (character) {
|
|
return isDigit(character) ||
|
|
isInRange(character, 'A', 'F') ||
|
|
isInRange(character, 'a', 'f')
|
|
}
|
|
|
|
function isDigit (character) {
|
|
return isInRange(character, '0', '9')
|
|
}
|
|
|
|
function isInRange (character, lower, upper) {
|
|
const code = character.charCodeAt(0)
|
|
|
|
return code >= lower.charCodeAt(0) && code <= upper.charCodeAt(0)
|
|
}
|