File: j2.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright © 2020-2025 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the “Software”), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 /*
  26 Single-file source-code for j2: this version has no http(s) support. Even
  27 the unit-tests from the original j2 are omitted.
  28 
  29 To compile a smaller-sized command-line app, you can use the `go` command as
  30 follows:
  31 
  32 go build -ldflags "-s -w" -trimpath j2.go
  33 */
  34 
  35 package main
  36 
  37 import (
  38     "bufio"
  39     "encoding/json"
  40     "errors"
  41     "io"
  42     "os"
  43 )
  44 
  45 const info = `
  46 j2 [filepath...]
  47 
  48 Json-2 indents valid JSON input into multi-line JSON which uses 2 spaces for
  49 each indentation level.
  50 `
  51 
  52 // errNoMoreOutput is a generic dummy output-error, which is meant to be
  53 // ultimately ignored, being just an excuse to quit the app immediately
  54 // and successfully
  55 var errNoMoreOutput = errors.New(`no more output`)
  56 
  57 func main() {
  58     if len(os.Args) > 1 {
  59         switch os.Args[1] {
  60         case `-h`, `--h`, `-help`, `--help`:
  61             os.Stderr.WriteString(info[1:])
  62             return
  63         }
  64     }
  65 
  66     if len(os.Args) > 2 {
  67         const msg = "\x1b[31mmultiple inputs not allowed\x1b[0m\n"
  68         os.Stderr.WriteString(msg)
  69         os.Exit(1)
  70     }
  71 
  72     // figure out whether input should come from a named file or from stdin
  73     path := `-`
  74     if len(os.Args) > 1 {
  75         path = os.Args[1]
  76     }
  77 
  78     err := handleInput(os.Stdout, path)
  79     if err != nil && err != io.EOF && err != errNoMoreOutput {
  80         os.Stderr.WriteString("\x1b[31m")
  81         os.Stderr.WriteString(err.Error())
  82         os.Stderr.WriteString("\x1b[0m\n")
  83         os.Exit(1)
  84     }
  85 }
  86 
  87 // handleInput simplifies control-flow for func main
  88 func handleInput(w io.Writer, path string) error {
  89     if path == `-` {
  90         return convert(w, os.Stdin)
  91     }
  92 
  93     // if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) {
  94     //  resp, err := http.Get(path)
  95     //  if err != nil {
  96     //      return err
  97     //  }
  98     //  defer resp.Body.Close()
  99     //  return convert(w, resp.Body)
 100     // }
 101 
 102     f, err := os.Open(path)
 103     if err != nil {
 104         // on windows, file-not-found error messages may mention `CreateFile`,
 105         // even when trying to open files in read-only mode
 106         return errors.New(`can't open file named ` + path)
 107     }
 108     defer f.Close()
 109     return convert(w, f)
 110 }
 111 
 112 // convert simplifies control-flow for func handleInput
 113 func convert(w io.Writer, r io.Reader) error {
 114     bw := bufio.NewWriter(w)
 115     defer bw.Flush()
 116     return json2(bw, r)
 117 }
 118 
 119 // escapedStringBytes helps func handleString treat all string bytes quickly
 120 // and correctly, using their officially-supported JSON escape sequences
 121 //
 122 // https://www.rfc-editor.org/rfc/rfc8259#section-7
 123 var escapedStringBytes = [256][]byte{
 124     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
 125     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
 126     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
 127     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
 128     {'\\', 'b'}, {'\\', 't'},
 129     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
 130     {'\\', 'f'}, {'\\', 'r'},
 131     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
 132     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
 133     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
 134     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
 135     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
 136     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
 137     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
 138     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
 139     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
 140     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
 141     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
 142     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
 143     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
 144     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
 145     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
 146     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
 147     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
 148     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
 149     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
 150     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
 151     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
 152     {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
 153     {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
 154     {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
 155     {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
 156     {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
 157     {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
 158     {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
 159     {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
 160     {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
 161     {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
 162     {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
 163     {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
 164     {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
 165     {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
 166     {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
 167     {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
 168 }
 169 
 170 // writeSpaces does what it says, minimizing calls to write-like funcs
 171 func writeSpaces(w *bufio.Writer, n int) {
 172     const spaces = `                                `
 173     if n < 1 {
 174         return
 175     }
 176 
 177     for n >= len(spaces) {
 178         w.WriteString(spaces)
 179         n -= len(spaces)
 180     }
 181     w.WriteString(spaces[:n])
 182 }
 183 
 184 // json2 does it all, given a reader and a writer
 185 func json2(w *bufio.Writer, r io.Reader) error {
 186     dec := json.NewDecoder(r)
 187     // avoid parsing numbers, so unusually-long numbers are kept verbatim,
 188     // even if JSON parsers aren't required to guarantee such input-fidelity
 189     // for numbers
 190     dec.UseNumber()
 191 
 192     t, err := dec.Token()
 193     if err == io.EOF {
 194         return errors.New(`input has no JSON values`)
 195     }
 196 
 197     if err = handleToken(w, dec, t, 0, 0); err != nil {
 198         return err
 199     }
 200     // don't forget ending the last line for the last value
 201     w.WriteByte('\n')
 202 
 203     _, err = dec.Token()
 204     if err == io.EOF {
 205         // input is over, so it's a success
 206         return nil
 207     }
 208 
 209     if err == nil {
 210         // a successful `read` is a failure, as it means there are
 211         // trailing JSON tokens
 212         return errors.New(`unexpected trailing data`)
 213     }
 214 
 215     // any other error, perhaps some invalid-JSON-syntax-type error
 216     return err
 217 }
 218 
 219 // handleToken handles recursion for func json2
 220 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token, pre, level int) error {
 221     switch t := t.(type) {
 222     case json.Delim:
 223         switch t {
 224         case json.Delim('['):
 225             return handleArray(w, dec, pre, level+1)
 226         case json.Delim('{'):
 227             return handleObject(w, dec, pre, level+1)
 228         default:
 229             return errors.New(`unsupported JSON syntax ` + string(t))
 230         }
 231 
 232     case nil:
 233         writeSpaces(w, 2*pre)
 234         w.WriteString(`null`)
 235         return nil
 236 
 237     case bool:
 238         writeSpaces(w, 2*pre)
 239         if t {
 240             w.WriteString(`true`)
 241         } else {
 242             w.WriteString(`false`)
 243         }
 244         return nil
 245 
 246     case json.Number:
 247         writeSpaces(w, 2*pre)
 248         w.WriteString(t.String())
 249         return nil
 250 
 251     case string:
 252         return handleString(w, t, pre)
 253 
 254     default:
 255         // return fmt.Errorf(`unsupported token type %T`, t)
 256         return errors.New(`invalid JSON token`)
 257     }
 258 }
 259 
 260 // handleArray handles arrays for func handleToken
 261 func handleArray(w *bufio.Writer, dec *json.Decoder, pre, level int) error {
 262     writeSpaces(w, 2*pre)
 263     w.WriteByte('[')
 264 
 265     for i := 0; true; i++ {
 266         t, err := dec.Token()
 267         if err == io.EOF {
 268             endComposite(i, w, level-1, ']')
 269             return nil
 270         }
 271 
 272         if err != nil {
 273             return err
 274         }
 275 
 276         if t == json.Delim(']') {
 277             endComposite(i, w, level-1, ']')
 278             return nil
 279         }
 280 
 281         if i > 0 {
 282             _, err := w.WriteString(",\n")
 283             if err != nil {
 284                 return errNoMoreOutput
 285             }
 286         } else {
 287             w.WriteByte('\n')
 288         }
 289 
 290         err = handleToken(w, dec, t, level, level)
 291         if err != nil {
 292             return err
 293         }
 294     }
 295 
 296     // make the compiler happy
 297     return nil
 298 }
 299 
 300 // handleObject handles objects for func handleToken
 301 func handleObject(w *bufio.Writer, dec *json.Decoder, pre, level int) error {
 302     writeSpaces(w, 2*pre)
 303     w.WriteByte('{')
 304 
 305     for i := 0; true; i++ {
 306         t, err := dec.Token()
 307         if err == io.EOF {
 308             endComposite(i, w, pre, '}')
 309             return nil
 310         }
 311 
 312         if err != nil {
 313             return err
 314         }
 315 
 316         if t == json.Delim('}') {
 317             endComposite(i, w, pre, '}')
 318             return nil
 319         }
 320 
 321         if i > 0 {
 322             _, err := w.WriteString(",\n")
 323             if err != nil {
 324                 return errNoMoreOutput
 325             }
 326         } else {
 327             w.WriteByte('\n')
 328         }
 329 
 330         k, ok := t.(string)
 331         if !ok {
 332             return errors.New(`expected a string for a key-value pair`)
 333         }
 334 
 335         err = handleString(w, k, level)
 336         if err != nil {
 337             return err
 338         }
 339 
 340         w.WriteString(": ")
 341 
 342         t, err = dec.Token()
 343         if err == io.EOF {
 344             return errors.New(`expected a value for a key-value pair`)
 345         }
 346 
 347         err = handleToken(w, dec, t, 0, level)
 348         if err != nil {
 349             return err
 350         }
 351     }
 352 
 353     // make the compiler happy
 354     return nil
 355 }
 356 
 357 // handleString handles strings for func handleToken, and keys for func
 358 // handleObject
 359 func handleString(w *bufio.Writer, s string, level int) error {
 360     writeSpaces(w, 2*level)
 361     w.WriteByte('"')
 362     for i := range s {
 363         w.Write(escapedStringBytes[s[i]])
 364     }
 365     w.WriteByte('"')
 366     return nil
 367 }
 368 
 369 // endComposite handles common closing logic which is used twice both in each
 370 // of func handleArray and func handleObject
 371 func endComposite(i int, w *bufio.Writer, pre int, end byte) {
 372     if i > 0 {
 373         w.WriteByte('\n')
 374         writeSpaces(w, 2*pre)
 375     }
 376     w.WriteByte(end)
 377 }