hb-lib-tools
Version:
homebridge-lib Command-Line Tools`
206 lines (194 loc) • 7.13 kB
JavaScript
// hb-lib-tools/lib/JsonFormatter.js
//
// Library for Homebridge plugins.
// Copyright © 2018-2025 Erik Baauw. All rights reserved.
import { OptionParser } from 'hb-lib-tools/OptionParser'
/** JSON formatter.
* <br>See {@link JsonFormatter}.
* @name JsonFormatter
* @type {Class}
* @memberof module:hb-lib-tools
*/
/** JSON formatter.
*
* Class to format (pretty-print) JavaScript types to formatted JSON strings.
* This class is the engine under the `json` command-line tool.
*/
class JsonFormatter {
/** Create a new instance of a JSON formatter.
*
* The parameters configure how the JSON should be formatted.
* @param {object} params - Parameters.
* @param {boolean} [params.ascii=false] - Output path:value in plain text
* instead of JSON.
* <br>Command-line equivalent: `json -a`
* @param {string} params.fromPath - Limit output to key/values under path.
* <br>Set top level below path.
* <br>Command-line equivalent: `json -p `_path_
* @param {boolean} [params.jsonArray=false] - Output JSON array of objects
* for each key/value pair.<br>
* Each object contains two key/value pairs: key `keys` with an array
* of keys as value and key `value` with the value as value.
* <br>Command-line equivalent: `json -j`
* @param {boolean} [params.joinKeys=false] - Output JSON array of objects
* for each key/value pair.<br>
* Each object contains one key/value pair: the path (concatenated
* keys separated by '/') as key and the value as value.
* <br>Command-line equivalent: `json -u`
* @param {boolean} [params.keysOnly=false] - Limit output to keys.
* <br>With `joinKeys` output JSON array of paths.
* <br>Command-line equivalent: `json -k`
* @param {boolean} [params.leavesOnly=false] - Limit output to leaf
* (non-array, non-object) key/values.
* <br>Command-line equivalent: `json -l`
* @param {?integer} params.maxDepth - Limit output to levels above depth.
* <br>Command-line equivalent: `json -d `_depth_
* @param {boolean} [params.noWhiteSpace=false] - Do not include spaces nor
* newlines in the output.
* <br>Command-line equivalent: `json -n`
* @param {boolean} [params.sortKeys=false] - Sort object key/value pairs
* alphabetically on key.
* <br>Command-line equivalent: `json -s`
* @param {boolean} [params.topOnly=false] - Limit output to top-level key/values.
* <br>Command-line equivalent: `json -t`
* @param {boolean} [params.valuesOnly=false] - Limit output to values.
* <br>With `joinKeys` output JSON array of values.
* <br>Command-line equivalent: `json -v`
*/
constructor (params = {}) {
this.options = {}
const optionParser = new OptionParser(this.options)
optionParser.boolKey('sortKeys')
optionParser.boolKey('noWhiteSpace')
optionParser.boolKey('jsonArray')
optionParser.boolKey('joinKeys')
optionParser.boolKey('ascii')
optionParser.boolKey('topOnly')
optionParser.intKey('maxDepth', 0)
optionParser.pathKey('fromPath')
optionParser.boolKey('leavesOnly')
optionParser.boolKey('keysOnly')
optionParser.boolKey('valuesOnly')
optionParser.parse(params)
if (this.options.ascii) {
this.options.noWhiteSpace = true
this.options.joinKeys = true
}
if (
this.options.joinKeys || this.options.topOnly || this.options.leavesOnly ||
this.options.keysOnly || this.options.valuesOnly
) {
this.options.jsonArray = true
}
}
_forEach (value, callback) {
function forEach (keys, value, depth) {
const isCollection = (typeof (value) === 'object' && value != null)
if (value === undefined) {
return
}
if (
!isCollection ||
(!this.options.leavesOnly && (!this.options.topOnly || depth === 1))
) {
callback(keys, value)
}
if (
isCollection && (!this.options.topOnly || depth === 0) &&
depth !== this.options.maxDepth
) {
const list = Object.keys(value)
if (this.options.sortKeys && !Array.isArray(value)) {
list.sort()
}
for (const key of list) {
forEach.call(this, keys.concat([key]), value[key], depth + 1)
}
}
}
forEach.call(this, [], value, 0)
}
_map (value, callback) {
const array = []
this._forEach(value, (keys, value) => {
array.push(callback(keys, value))
})
return array
}
_format (value, maxDepth = this.options.maxDepth, withIndent = ' ') {
function format (value, depth, indent) {
const noNewline = this.options.noWhiteSpace || depth >= maxDepth
const nl = this.options.noWhiteSpace ? '' : noNewline ? ' ' : '\n'
const sp = this.options.noWhiteSpace ? '' : ' '
const nlsp = noNewline ? '' : '\n'
const wi = noNewline ? '' : withIndent
const id = noNewline ? '' : indent
if (value === undefined) {
return ''
}
if (typeof (value) !== 'object' || value == null) {
return JSON.stringify(value)
}
const array = []
const list = Object.keys(value)
if (this.options.sortKeys && !Array.isArray(value)) {
list.sort()
}
for (const key of list) {
if (value[key] !== undefined) {
const k = Array.isArray(value) ? '' : `"${key}":${sp}`
const v = format.call(this, value[key], depth + 1, `${wi}${id}`)
array.push(k + v)
}
}
let s = array.join(`,${nl}${wi}${id}`)
if (s !== '') {
s = `${nlsp}${wi}${id}${s}${nlsp}${id}`
}
return Array.isArray(value) ? `[${s}]` : `{${s}}`
}
return format.call(this, value, 0, '')
}
/** Transform javascript value into a formatted JSON string.
*
* @param {*} value - The JavaScript value.
* @return {string} json - The formatted JSON string.
*/
stringify (value) {
if (this.options.fromPath != null) {
const a = this.options.fromPath.slice(1).split('/')
for (const key of a) {
if (typeof (value) === 'object' && value != null) {
value = value[key]
} else {
value = undefined
}
}
}
if (!this.options.jsonArray) {
return this._format(value)
}
const array = this._map(value, (keys, value) => {
if (this.options.ascii) {
if (this.options.keysOnly) { return `/${keys.join('/')}` }
if (this.options.valuesOnly) { return this._format(value) }
return `/${keys.join('/')}:${this._format(value)}`
} else if (this.options.joinKeys) {
if (this.options.keysOnly) { return `/${keys.join('/')}` }
if (this.options.valuesOnly) { return value }
const obj = {}
obj[`/${keys.join('/')}`] = value
return obj
} else {
if (this.options.keysOnly) { return { keys } }
if (this.options.valuesOnly) { return { value } }
return { keys, value }
}
})
if (this.options.ascii) {
return array.join('\n')
}
return this._format(array, 1)
}
}
export { JsonFormatter }