File: nj/go.mod 1 module nj 2 3 go 1.18 File: nj/info.txt 1 nj [filepath...] 2 3 Nice Json reads JSON, and emits it back as ANSI-styled indented lines, using 4 2 spaces for each indentation level. File: nj/main.go 1 package main 2 3 import ( 4 "bufio" 5 "encoding/json" 6 "errors" 7 "io" 8 "os" 9 10 _ "embed" 11 ) 12 13 //go:embed info.txt 14 var info string 15 16 // indent is how many spaces each indentation level uses 17 const indent = 2 18 19 const ( 20 // boolStyle is bluish, and very distinct from all other colors used 21 boolStyle = "\x1b[38;5;74m" 22 23 // keyStyle is magenta, and very distinct from normal strings 24 keyStyle = "\x1b[38;5;99m" 25 26 // nullStyle is a light-gray, just like syntax elements, but the word 27 // `null` is wide enough to stand out from syntax items at a glance 28 nullStyle = syntaxStyle 29 30 // numberStyle is a nice green 31 numberStyle = "\x1b[38;5;29m" 32 33 // stringStyle used to be bluish, but it's better to keep it plain, 34 // which also minimizes how many different colors the output can show 35 stringStyle = "" 36 37 // stringStyle is bluish, but clearly darker than boolStyle 38 // stringStyle = "\x1b[38;5;24m" 39 40 // syntaxStyle is a light-gray, not too light, not too dark 41 syntaxStyle = "\x1b[38;5;249m" 42 ) 43 44 // errNoMoreOutput is a way to successfully quit the app right away, its 45 // message never shown 46 var errNoMoreOutput = errors.New(`no more output`) 47 48 func main() { 49 if len(os.Args) > 1 { 50 switch os.Args[1] { 51 case `-h`, `--h`, `-help`, `--help`: 52 os.Stderr.WriteString(info) 53 return 54 } 55 } 56 57 if len(os.Args) > 2 { 58 showError(errors.New(`multiple inputs not allowed`)) 59 os.Exit(1) 60 } 61 62 var err error 63 if len(os.Args) == 1 || (len(os.Args) == 2 && os.Args[1] == `-`) { 64 // handle lack of filepath arg, or `-` as the filepath 65 err = niceJSON(os.Stdout, os.Stdin) 66 } else { 67 // handle being given a normal filepath 68 err = handleFile(os.Stdout, os.Args[1]) 69 } 70 71 if err != nil && err != errNoMoreOutput { 72 showError(err) 73 os.Exit(1) 74 } 75 } 76 77 // showError standardizes how errors look in this app 78 func showError(err error) { 79 os.Stderr.WriteString("\x1b[31m") 80 os.Stderr.WriteString(err.Error()) 81 os.Stderr.WriteString("\x1b[0m\n") 82 } 83 84 // writeSpaces does what it says, minimizing calls to write-like funcs 85 func writeSpaces(w *bufio.Writer, n int) { 86 const spaces = ` ` 87 for n >= len(spaces) { 88 w.WriteString(spaces) 89 n -= len(spaces) 90 } 91 if n > 0 { 92 w.WriteString(spaces[:n]) 93 } 94 } 95 96 func handleFile(w io.Writer, path string) error { 97 // if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) { 98 // resp, err := http.Get(path) 99 // if err != nil { 100 // return err 101 // } 102 // defer resp.Body.Close() 103 // return niceJSON(w, resp.Body) 104 // } 105 106 f, err := os.Open(path) 107 if err != nil { 108 // on windows, file-not-found error messages may mention `CreateFile`, 109 // even when trying to open files in read-only mode 110 return errors.New(`can't open file named ` + path) 111 } 112 defer f.Close() 113 114 return niceJSON(w, f) 115 } 116 117 func niceJSON(w io.Writer, r io.Reader) error { 118 bw := bufio.NewWriter(w) 119 defer bw.Flush() 120 121 dec := json.NewDecoder(r) 122 // using string-like json.Number values instead of float64 ones avoids 123 // unneeded reformatting of numbers; reformatting parsed float64 values 124 // can potentially even drop/change decimals, causing the output not to 125 // match the input digits exactly, which is best to avoid 126 dec.UseNumber() 127 128 t, err := dec.Token() 129 if err == io.EOF { 130 return errors.New(`empty input isn't valid JSON`) 131 } 132 if err != nil { 133 return err 134 } 135 136 if err := handleToken(bw, dec, t, 0, 0); err != nil { 137 return err 138 } 139 // don't forget to end the last output line 140 bw.WriteByte('\n') 141 142 if _, err := dec.Token(); err != io.EOF { 143 return errors.New(`unexpected trailing JSON data`) 144 } 145 return nil 146 } 147 148 func handleToken(w *bufio.Writer, d *json.Decoder, t json.Token, pre, level int) error { 149 switch t := t.(type) { 150 case json.Delim: 151 switch t { 152 case json.Delim('['): 153 return handleArray(w, d, pre, level) 154 155 case json.Delim('{'): 156 return handleObject(w, d, pre, level) 157 158 default: 159 // return fmt.Errorf(`unsupported JSON delimiter %v`, t) 160 return errors.New(`unsupported JSON delimiter`) 161 } 162 163 case nil: 164 return handleNull(w, pre) 165 166 case bool: 167 return handleBoolean(w, t, pre) 168 169 case string: 170 return handleString(w, t, pre) 171 172 case json.Number: 173 return handleNumber(w, t, pre) 174 175 default: 176 // return fmt.Errorf(`unsupported token type %T`, t) 177 return errors.New(`unsupported token type`) 178 } 179 } 180 181 func handleArray(w *bufio.Writer, d *json.Decoder, pre, level int) error { 182 for i := 0; true; i++ { 183 t, err := d.Token() 184 if err != nil { 185 return err 186 } 187 188 if t == json.Delim(']') { 189 if i == 0 { 190 writeSpaces(w, indent*pre) 191 w.WriteString(syntaxStyle + "[]\x1b[0m") 192 } else { 193 w.WriteString("\n") 194 writeSpaces(w, indent*level) 195 w.WriteString(syntaxStyle + "]\x1b[0m") 196 } 197 return nil 198 } 199 200 if i == 0 { 201 writeSpaces(w, indent*pre) 202 w.WriteString(syntaxStyle + "[\x1b[0m\n") 203 } else { 204 // this is a good spot to check for early-quit opportunities 205 _, err = w.WriteString(syntaxStyle + ",\x1b[0m\n") 206 if err != nil { 207 return errNoMoreOutput 208 } 209 } 210 211 writeSpaces(w, indent*(level+1)) 212 if err := handleToken(w, d, t, level, level+1); err != nil { 213 return err 214 } 215 } 216 217 // make the compiler happy 218 return nil 219 } 220 221 func handleBoolean(w *bufio.Writer, b bool, pre int) error { 222 writeSpaces(w, indent*pre) 223 if b { 224 w.WriteString(boolStyle + "true\x1b[0m") 225 } else { 226 w.WriteString(boolStyle + "false\x1b[0m") 227 } 228 return nil 229 } 230 231 func handleKey(w *bufio.Writer, s string, pre int) error { 232 writeSpaces(w, indent*pre) 233 w.WriteString(syntaxStyle + "\"\x1b[0m" + keyStyle) 234 w.WriteString(s) 235 w.WriteString(syntaxStyle + "\":\x1b[0m ") 236 return nil 237 } 238 239 func handleNull(w *bufio.Writer, pre int) error { 240 writeSpaces(w, indent*pre) 241 w.WriteString(nullStyle + "null\x1b[0m") 242 return nil 243 } 244 245 func handleNumber(w *bufio.Writer, n json.Number, pre int) error { 246 writeSpaces(w, indent*pre) 247 w.WriteString(numberStyle) 248 w.WriteString(n.String()) 249 w.WriteString("\x1b[0m") 250 return nil 251 } 252 253 func handleObject(w *bufio.Writer, d *json.Decoder, pre, level int) error { 254 for i := 0; true; i++ { 255 t, err := d.Token() 256 if err != nil { 257 return err 258 } 259 260 if t == json.Delim('}') { 261 if i == 0 { 262 writeSpaces(w, indent*pre) 263 w.WriteString(syntaxStyle + "{}\x1b[0m") 264 } else { 265 w.WriteString("\n") 266 writeSpaces(w, indent*level) 267 w.WriteString(syntaxStyle + "}\x1b[0m") 268 } 269 return nil 270 } 271 272 if i == 0 { 273 writeSpaces(w, indent*pre) 274 w.WriteString(syntaxStyle + "{\x1b[0m\n") 275 } else { 276 // this is a good spot to check for early-quit opportunities 277 _, err = w.WriteString(syntaxStyle + ",\x1b[0m\n") 278 if err != nil { 279 return errNoMoreOutput 280 } 281 } 282 283 // the stdlib's JSON parser is supposed to complain about non-string 284 // keys anyway, but make sure just in case 285 k, ok := t.(string) 286 if !ok { 287 return errors.New(`expected key to be a string`) 288 } 289 if err := handleKey(w, k, level+1); err != nil { 290 return err 291 } 292 293 // handle value 294 t, err = d.Token() 295 if err != nil { 296 return err 297 } 298 if err := handleToken(w, d, t, 0, level+1); err != nil { 299 return err 300 } 301 } 302 303 // make the compiler happy 304 return nil 305 } 306 307 func handleString(w *bufio.Writer, s string, pre int) error { 308 writeSpaces(w, indent*pre) 309 w.WriteString(syntaxStyle + "\"\x1b[0m" + stringStyle) 310 w.WriteString(s) 311 w.WriteString(syntaxStyle + "\"\x1b[0m") 312 return nil 313 } File: nj/mit-license.txt 1 The MIT License (MIT) 2 3 Copyright © 2024 pacman64 4 5 Permission is hereby granted, free of charge, to any person obtaining a copy of 6 this software and associated documentation files (the “Software”), to deal 7 in the Software without restriction, including without limitation the rights to 8 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 of the Software, and to permit persons to whom the Software is furnished to do 10 so, subject to the following conditions: 11 12 The above copyright notice and this permission notice shall be included in all 13 copies or substantial portions of the Software. 14 15 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 SOFTWARE.