Skip to content

Instantly share code, notes, and snippets.

@ddlsmurf
Last active May 22, 2021 11:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ddlsmurf/ff7f11ab2d353bfa3ca40a9fa426f0ba to your computer and use it in GitHub Desktop.
Save ddlsmurf/ff7f11ab2d353bfa3ca40a9fa426f0ba to your computer and use it in GitHub Desktop.
This is a very hastily cobbled together JSON prettifyer that doesn't bug me about input validity as long as unambiguous. Also it warns about a few validity issues (not all). #tool
{ Transform } = require('stream')
EOL = "\n"
Utils =
spaces: (num) -> if num <= 0 then '' else (new Array(num + 1)).join(" ")
ljust: (str, len) -> str + Utils.spaces(len - str.length)
streamToString: (stream, cb) ->
len = 0
buffer = []
stream
.on('end', -> cb(null, Buffer.concat(buffer, len)))
.on('data', (chunk) -> buffer.push(chunk) ; len += chunk.length)
.on('error', (e) -> cb(e ? true) ; cb = null)
maxLength: (strs, map) ->
max = strs.reduce(((acc, str) -> Math.max((if map then map(str) else str).length, acc)), 0)
indent: (str, depth = 1) ->
sp = Utils.spaces(depth * 2)
str.replace(/(?:\n)|(?:\r\n?)/g, EOL + sp)
class TokenKinds
constructor: (list) ->
@id = {}
@names = []
@list = []
@width = 0
for [name, rx] in list
unless (id = @id[name])?
id = @id[name] = @names.length
@width = Math.max(@width, name.length)
@names.push name
throw new Error("Invalid rx (must start with ^): #{JSON.stringify([name, rx.toString()])}") if rx.toString().charAt(1) != "^"
@list.push {name, id, rx}
@
pretty: (token) ->
"#{Utils.ljust @names[token[0]] ? "#{token[0]}", @width}: #{JSON.stringify(token[1])}"
printTokensStream: ->
tokens = @
new Transform
decodeStrings: false
writableObjectMode: true
readableObjectMode: false
transform: (chunk, encoding, cb) ->
cb(null, tokens.pretty(chunk) + EOL)
match: (str) ->
for token_kind in @list
if (match = token_kind.rx.exec(str))
token_kind.rx.lastIndex = 0
return [ token_kind.id, match[0] ]
null
lexerStream: ->
buffer = ''
tokens = @
new Transform
decodeStrings: false
writableObjectMode: false
readableObjectMode: true
transform: (chunk, encoding, cb) ->
throw new Error("Expected string got #{JSON.stringify(chunk)}") if typeof chunk != 'string'
buffer += chunk
while (token = tokens.match(buffer))?
@push(token)
buffer = buffer.substr(token[1].length)
cb()
flush: (cb) -> cb(if buffer == '' then null else new Error("Buffer not empty: #{JSON.stringify(buffer)}"))
INLINE_MAX_LEN = 100
KEY_LJUST_MAX = 20
JSON_TOKENS = new TokenKinds [
[ 'COMMENT_NL', /^\/\/[^\r\n]*/ ],
[ 'COMMENT', /^\/\*(?:[^\/]|(?:\/[^*]))*\*\// ],
[ 'VALUE', /^(?:(?:null)|(?:undefined)|(?:true)|(?:false))\b/ ],
[ 'VALUE', /^(?:[-+]\s*)?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?/ ],
[ 'STRING', /^"[^"\r\n\\]*(?:\\.[^"\r\n\\]*)*"/ ],
[ 'STRING', /^'[^'\r\n\\]*(?:\\.[^'\r\n\\]*)*'/ ],
[ 'WS', /^\s+/ ],
[ 'OBJ_START', /^{/ ],
[ 'OBJ_END', /^}/ ],
[ 'ARRAY_START', /^\[/ ],
[ 'ARRAY_END', /^\]/ ],
[ 'COMMA', /^,/ ],
[ 'PROP_SEP', /^:/ ],
]
class Stack
constructor: (initial) ->
@list = []
@top = null
@push initial if initial
push: (val) ->
@list.push @top = val
val
pop: ->
throw new Error("Empty stack") if @list.length == 0
val = @list.pop()
@top = @list[@list.length - 1]
val
debug = (a...) -> console.log(a...)
debug = ->
class JSONTokenPrettyfier extends Transform
JSON_VALUES =
value: -1
array: -2
object: -3
listToString = (list, start, finish, len, map) ->
return "#{start} #{finish}" if list.length == 0
items = list.map (i) -> map(i)
items.sort()
if len > INLINE_MAX_LEN
"#{start} #{Utils.indent("\n" + items.join("," + EOL))} #{finish}"
else
items = items.join(", ")
"#{start} #{items} #{finish}"
class BaseState
constructor: (@parent) ->
write: (value, last = true) ->
throw new Error("Error already wrote #{JSON.stringify(this)} (writing #{JSON.stringify(value)})") unless @parent?
@parent.push value
delete @parent if last
!!last
push: (token) ->
switch token[0]
when JSON_TOKENS.id.VALUE, JSON_TOKENS.id.STRING, JSON_VALUES.value
@write([JSON_VALUES.value, token[1]])
when JSON_TOKENS.id.OBJ_START
new ObjectState(@)
when JSON_TOKENS.id.ARRAY_START
new ArrayState(@)
else
throw new Error("Unexpected token #{JSON.stringify(token)}")
expect: -> "litteral or object or array"
class RootState extends BaseState
expect: -> "(root)"
class ObjectState extends BaseState
constructor: () ->
super
@items = []
@expectingItem = true
@length = 0
push: (token) ->
switch token[0]
when JSON_TOKENS.id.OBJ_END
debug "object complete #{JSON.stringify(@items)}"
if @expectingValueForKey?
throw new Error("Expected prop then value for key #{JSON.stringify(@expectingValueForKey)}") if !@gotPropSep
throw new Error("Expected value for key #{JSON.stringify(@expectingValueForKey)}")
console.warn "Warning: Unexpected trailing comma" if @expectingItem && @items.length > 0
maxLen = if @length > INLINE_MAX_LEN then Utils.maxLength @items, (i) -> i[0][1] else 0
maxLen = Math.min(maxLen, KEY_LJUST_MAX)
return @write([JSON_VALUES.value, listToString(@items, "{", "}", @length, (e) -> "#{Utils.ljust e[0][1], maxLen}: #{e[1][1]}")])
when JSON_TOKENS.id.COMMA
console.warn "Unexpected comma" if @expectingItem
@expectingItem = true
return null
when JSON_TOKENS.id.PROP_SEP
throw new Error("':' but not key value") unless @expectingValueForKey?
@gotPropSep = true
when JSON_TOKENS.id.VALUE, JSON_TOKENS.id.STRING, JSON_VALUES.value
if @expectingItem
console.warn "Warning: Non string key #{token[1]}" if token[0] != JSON_TOKENS.id.STRING || (token[1].charAt(0) != '"')
@expectingValueForKey = token
@gotPropSep = false
else if @expectingValueForKey?
@length += @expectingValueForKey[1].length + token[1].length + 2
@items.push [ @expectingValueForKey, token ]
@expectingValueForKey = null
@expectingItem = false
return null
else
super
expect: ->
if @expectingItem then "'\"key\": value' or '}'" else (if @expectingValueForKey? then ((if @gotPropSep then "" else ": ") + "value for key (#{@expectingValueForKey[1]})") else "',' or '}'")
class ArrayState extends BaseState
constructor: () ->
super
@items = []
@length = 0
@expectingItem = true
expect: ->
if @expectingItem then "value/obj/array or ']'" else "',' or ']'"
push: (token) ->
switch token[0]
when JSON_TOKENS.id.ARRAY_END
debug "array complete #{JSON.stringify(@items)}"
console.warn "Warning: Unexpected trailing comma" if @expectingItem && @items.length > 0
return @write([JSON_VALUES.value, listToString(@items, "[", "]", @length, (e) -> "#{e[1]}")])
when JSON_TOKENS.id.COMMA
console.warn "Warning: Unexpected comma" if @expectingItem
@expectingItem = true
return null
when JSON_TOKENS.id.VALUE, JSON_TOKENS.id.STRING, JSON_VALUES.value
@items.push token
@length += token[1].length
@expectingItem = false
return null
else
super
constructor: () ->
super objectMode: true,
decodeStrings: false
@stateStack = new Stack new RootState(@)
_transform: (chunk, encoding, cb) ->
try
switch chunk[0]
when JSON_TOKENS.id.WS
break
when JSON_TOKENS.id.COMMENT
console.warn "Warning: comment #{chunk[1]}"
else
debug JSON_TOKENS.pretty(chunk)
throw new Error("Nothing more expected bug got #{JSON.stringify(chunk)}") unless @stateStack.top?
result = @stateStack.top.push chunk
if result == true
@stateStack.pop() while @stateStack.top? && !(@stateStack.top.parent?)
else if result
@stateStack.push result
cb()
catch e
cb(e)
_flush: (cb) -> cb(if !@stateStack.top? then null else new Error("EOF but expected #{JSON.stringify(@stateStack.list.reverse().map((l) -> l.expect()))}"))
testOn = (str...) ->
stream = JSON_TOKENS.lexerStream()
pipe = stream
.pipe new JSONTokenPrettyfier()
.pipe JSON_TOKENS.printTokensStream()
Utils.streamToString pipe, (err, res) ->
throw err if err
console.log "Testing: #{str.join('')}"
console.log res.toString('utf8')
console.log ""
stream.write(s) for s in str
stream.end()
process.stdin.setEncoding('utf8')
.pipe JSON_TOKENS.lexerStream()
.pipe new JSONTokenPrettyfier()
.pipe new Transform(decodeStrings: false, objectMode: true, transform: (chunk, encoding, cb) -> cb(null, "#{chunk[1]}\n"))
.pipe process.stdout
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment