formatted-json-stringify
Version:
An advanced & customisable version of the javascript JSON.stringify() function.
203 lines (179 loc) • 8.42 kB
text/typescript
/**A type which matches all available formatters in this package. */
export type AnyFormatter = custom.BaseFormatter|DefaultFormatter|PropertyFormatter|TextFormatter|ObjectFormatter|ArrayFormatter|ObjectSwitchFormatter
export namespace custom {
/**All valid variable types in a JSON file. */
export type ValidJsonType = number|string|boolean|null|object|ValidJsonType[]
/**## BaseFormatter `class`
* The base of all formatters. This class can't be used directly, but needs to be extended from when creating custom formatters!
*/
export class BaseFormatter {
/**The name of this variable. Used as key in objects. */
name: string
/**Set this to `false` when this is a global variable or you don't want the key/name to be rendered. */
showKey: boolean
constructor(name:string|null){
this.name = name ?? ""
this.showKey = !(name == null)
}
/**Parse a variable trough this formatter! Returns a JSON string like `JSON.stringify()` */
stringify(data:ValidJsonType): string {
throw new Error("FJS.BaseFormatter: Tried to use uninplemented stringify() function!")
}
}
/**## ObjectSwitchData `interface`
* The data for a single "object switch" in the `ObjectSwitchFormatter`!
*/
export interface ObjectSwitchData {
/**The key to match. */
key:any,
/**The value to match. */
value:any,
/**The formatter to use for the object when the key and value match! */
formatter:ObjectFormatter
}
}
/**## DefaultFormatter `class`
* You can use this formatter when you don't know the contents of the variable!
*
* It just uses the default `JSON.stringify` under the hood!
*/
export class DefaultFormatter extends custom.BaseFormatter {
/**When enabled, objects & arrays will be rendered multiline instead of inline! */
multiline: boolean
/**The space or indentation for this object/array. 4 spaces by default. */
space: string
constructor(name:string|null, multiline:boolean, space?:string){
super(name)
this.multiline = multiline
this.space = space ?? " "
}
stringify(data:custom.ValidJsonType){
if (typeof data == "undefined") throw new Error(`FJS.PropertyFormatter: Property '${this.name}' is 'undefined' which is not allowed in JSON files!`)
const key = this.showKey ? `"${this.name}":` : ""
const value = JSON.stringify(data,null,(this.multiline ? this.space : undefined))
return key+value
}
}
/**## PropertyFormatter `class`
* The formatter responsible for formatting `boolean`, `string`, `number` & `null` variables!
*/
export class PropertyFormatter extends custom.BaseFormatter {
stringify(data:number|string|boolean|null){
if (typeof data == "undefined") throw new Error(`FJS.PropertyFormatter: Property '${this.name}' is 'undefined' which is not allowed in JSON files!`)
const key = this.showKey ? `"${this.name}":` : ""
const value = JSON.stringify(data)
return key+value
}
}
/**## TextFormatter `class`
* The formatter responsible for adding custom text between properties in an object!
*/
export class TextFormatter extends custom.BaseFormatter {
/**The text to write on this row. */
text: string
constructor(text?:string){
super(null)
this.text = text ?? ""
}
stringify(): string {
return this.text
}
}
/**## ObjectFormatter `class`
* The formatter responsible for formatting `object` variables!
*/
export class ObjectFormatter extends custom.BaseFormatter {
/**When enabled, the object will be rendered multiline instead of inline! */
multiline: boolean
/**A collection of all the child-formatters in this object. */
children: custom.BaseFormatter[]
/**When enabled, the object will still be rendered multiline when it's empty! */
multilineWhenEmpty: boolean
/**The space or indentation for this object. 4 spaces by default. */
space: string
constructor(name:string|null, multiline:boolean, children:custom.BaseFormatter[], multilineWhenEmpty?:boolean, space?:string){
super(name)
this.multiline = multiline
this.children = children
this.multilineWhenEmpty = multilineWhenEmpty ?? false
this.space = space ?? " "
}
stringify(data:object): string {
const children = this.children.map((child,index) => {
const comma = (this.children.length == index+1) ? "" : ","
if (child instanceof TextFormatter) return this.#indentWithoutFirst(child.stringify())
else{
if (typeof data[child.name] == "undefined") throw new Error(`FJS.ObjectFormatter: Object property '${child.name}' is 'undefined' which is not allowed in JSON files!`)
return this.#indentWithoutFirst(child.stringify(data[child.name])+comma)
}
})
const key = this.showKey ? `"${this.name}":` : ""
const renderMultiline = this.multiline && (children.length > 0 || this.multilineWhenEmpty)
const value = renderMultiline ? `{\n${this.space}${children.join(`\n${this.space}`)}\n}` : `{${children.join("")}}`
return key+value
}
/**Private function for indenting all lines except the first row. */
#indentWithoutFirst(text:string){
return text.split("\n").map((row,index) => {
if (index == 0) return row
else return this.space+row
}).join("\n")
}
}
/**## ArrayFormatter `class`
* The formatter responsible for formatting `array` variables!
*/
export class ArrayFormatter extends custom.BaseFormatter {
/**When enabled, the array will be rendered multiline instead of inline! */
multiline: boolean
/**The formatter that will be executed on all variables in the array. */
property: custom.BaseFormatter
/**When enabled, the object will still be rendered multiline when it's empty! */
multilineWhenEmpty: boolean
/**The space or indentation for this array. 4 spaces by default. */
space: string
constructor(name:string|null, multiline:boolean, property:custom.BaseFormatter, multilineWhenEmpty?:boolean, space?:string){
super(name)
this.multiline = multiline
this.property = property
this.multilineWhenEmpty = multilineWhenEmpty ?? false
this.space = space ?? " "
}
stringify(data:custom.ValidJsonType[]): string {
const children = data.map((child,index) => {
if (typeof child == "undefined") throw new Error(`FJS.ArrayFormatter: Value #${index} of array is 'undefined' which is not allowed in JSON files!`)
const comma = (data.length == index+1) ? "" : ","
return this.#indentWithoutFirst(this.property.stringify(child)+comma)
})
const key = this.showKey ? `"${this.name}":` : ""
const renderMultiline = this.multiline && (children.length > 0 || this.multilineWhenEmpty)
const value = renderMultiline ? `[\n${this.space}${children.join(`\n${this.space}`)}\n]` : `[${children.join("")}]`
return key+value
}
/**Private function for indenting all lines except the first row. */
#indentWithoutFirst(text:string){
return text.split("\n").map((row,index) => {
if (index == 0) return row
else return this.space+row
}).join("\n")
}
}
/**## ObjectSwitchFormatter `class`
* Use this utility class to switch `ObjectFormatter`'s based on a `key` and `value` match in the object.
*
* This could be used in combination with an `ArrayFormatter` to allow different objects to exist in the same array!
*/
export class ObjectSwitchFormatter extends custom.BaseFormatter {
/**A list of all available formatters to check for an object. */
formatters: custom.ObjectSwitchData[]
constructor(name:string|null, formatters:custom.ObjectSwitchData[]){
super(name)
this.formatters = formatters
}
stringify(data:object): string {
const result = this.formatters.find((formatter) => data[formatter.key] === formatter.value)
if (!result) throw new Error("FJS.ObjectSwitchFormatter: No formatter matches the given object!")
const formatter = result.formatter
return formatter.stringify(data)
}
}