char/fjson
friendly JSON implementation in TypeScript
git clone https://git.t4t.associates/char/fjson
bb03820
main
1import type { Options } from "./options.ts" ; 2import { type JsonObject , type JsonValue , toJsonValue } from "./parse.ts" ; 3 4const WIDTH = 80 ; 5 6function isObject ( value :JsonValue ) :value isJsonObject { 7return typeof value === "object" && value !== null && ! Array . isArray ( value ); 8} 9 10/** render an object key (leaving simple identifiers unquoted) */ 11export function keyText ( key :string ) :string { 12if ( / ^[$_a-zA-Z][$_a-zA-Z0-9]*$ / . test ( key )) return key ; 13return JSON . stringify ( key ); 14} 15 16function scalar ( value :null | boolean | number | string ) :string { 17if ( typeof value === "number" ) { 18return Number . isFinite ( value ) ?String ( value ) :"null" ; 19} 20return JSON . stringify ( value ); 21} 22 23function flat ( value :JsonValue ) :string | undefined { 24if ( Array . isArray ( value )) return value . length === 0 ?"[]" :undefined ; 25if ( isObject ( value )) { 26return Object . keys ( value ). length === 0 ?"{}" :undefined ; 27} 28return scalar ( value ); 29} 30 31interface Output { 32text :string ; 33} 34 35function write ( 36value :JsonValue , 37implicit :boolean , 38indent :number , 39out :Output , 40) :void { 41const single = flat ( value ); 42if ( single !== undefined ) { 43out . text += single ; 44return ; 45} 46if ( isObject ( value )) { 47const child = implicit ?indent :indent + 2 ; 48if ( ! implicit ) out . text += "{\n" ; 49Object . keys ( value ). forEach (( key , index ) => { 50if ( index > 0 ) out . text += "\n" ; 51out . text += ` ${ " " . repeat ( child )} ${ keyText ( key )} : ` ; 52write ( value [ key ], false , child , out ); 53}); 54if ( ! implicit ) out . text += `\n ${ " " . repeat ( indent )} }` ; 55return ; 56} 57const items = value as JsonValue []; 58const inline = items . map ( flat ); 59if ( inline . every (( text ) :text isstring => text !== undefined )) { 60const text = `[ ${ inline . join ( ", " )} ]` ; 61const line = out . text . slice ( out . text . lastIndexOf ( "\n" ) + 1 ); 62if ([ ...line ]. length + [ ...text ]. length <= WIDTH ) { 63out . text += text ; 64return ; 65} 66} 67out . text += "[\n" ; 68items . forEach (( item , index ) => { 69if ( index > 0 ) out . text += "\n" ; 70out . text += " " . repeat ( indent + 2 ); 71write ( item , false , indent + 2 , out ); 72}); 73out . text += `\n ${ " " . repeat ( indent )} ]` ; 74} 75 76/** 77* Convert a JavaScript value into a pretty-printed fJSON document: two-space 78* indentation, unquoted identifier keys, implicit nonempty root objects, and 79* newline-separated entries. Short scalar arrays stay inline; longer arrays 80* and nested containers expand toward an 80-column width. No final newline. 81* 82* Values pass through JSON semantics first, so `undefined`, functions, 83* symbols, bigints, and circular structures fail with `TypeError`, and 84* non-finite numbers print as `null`. Values beyond the nesting limit fail 85* with an `FjsonError` of kind `"nesting"` rather than producing text that 86* cannot be parsed back. 87*/ 88export function stringify ( value :unknown , options ?:Options ) :string { 89const out :Output = { text :"" }; 90write ( toJsonValue ( value , options ), true , 0 , out ); 91return out . text ; 92} 93 94/** pretty-print validated JSON */ 95export function printJson ( value :JsonValue , implicit :boolean ) :string { 96const out :Output = { text :"" }; 97write ( value , implicit , 0 , out ); 98return out . text ; 99}