#!/usr/bin/python3 # The MIT License (MIT) # # Copyright © 2020-2025 pacman64 # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the “Software”), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from json import dumps, load from math import modf from sys import argv, exit, stderr, stdin, stdout info = ''' njson [filepath/URI...] Nice JSON indents and styles JSON data. When not given a filepath to read data from, this tool reads from standard input. ''' if len(argv) > 1 and argv[1] in ('-h', '--h', '-help', '--help'): print(info.strip()) exit(0) # level2indent speeds up func indent level2indent = tuple(i * ' ' for i in range(64)) def indent(w, level: int) -> None: 'Emit 2 spaces for each indentation level.' if level < 1: return if level < len(level2indent): w.write(level2indent[level]) return while level >= len(level2indent): w.write(level2indent[-1]) level -= len(level2indent) w.write(level2indent[level]) def restyle(w, data, pre: int = 0, level: int = 0) -> None: 'This func is where all the recursive output-action starts/happens.' f = type2func[type(data)] if f != None: f(w, data, pre, level) # don't forget to end the final line, if at `top-level` if level == 0: w.write('\n') else: raise ValueError(f'unsupported type {type(data)}') def restyle_bool(w, data: bool, pre: int, level: int) -> None: indent(w, pre) if data: w.write('\x1b[38;2;95;175;215mtrue\x1b[0m') else: w.write('\x1b[38;2;95;175;215mfalse\x1b[0m') def restyle_dict(w, data: dict, pre: int, level: int) -> None: if len(data) == 0: indent(w, pre) w.write('\x1b[38;2;168;168;168m{}\x1b[0m') return indent(w, pre) w.write('\x1b[38;2;168;168;168m{\x1b[0m\n') for (i, k) in enumerate(data): if i > 0: w.write('\x1b[38;2;168;168;168m,\x1b[0m\n') restyle_key(w, k, level + 1) restyle(w, data[k], 0, level + 1) w.write('\n') indent(w, level) w.write('\x1b[38;2;168;168;168m}\x1b[0m') def pick_numeric_style(n) -> str: if n > 0: return '\x1b[38;2;0;135;95m' if n < 0: return '\x1b[38;2;204;0;0m' return '\x1b[38;2;0;135;95m' def restyle_float(w, data: float, pre: int, level: int) -> None: indent(w, pre) # can't figure out a direct float-format option which both avoids the # chance of scientific notation (invalid as JSON), and the chance of # unneeded trailing decimal 0s; neither {data:-1} nor {data:-1f} work if modf(data)[0] == 0.0: w.write(f'{pick_numeric_style(data)}{data}\x1b[0m') else: s = f'{data:-1f}'.rstrip('0') w.write(f'{pick_numeric_style(data)}{s}\x1b[0m') def restyle_int(w, data: int, pre: int, level: int) -> None: indent(w, pre) w.write(f'{pick_numeric_style(data)}{data}\x1b[0m') def restyle_key(w, k: str, level: int) -> None: indent(w, level) s = '\x1b[38;2;168;168;168m' w.write(f'{s}"\x1b[38;2;135;135;225m{dumps(k)[1:-1]}{s}": \x1b[0m') def restyle_list(w, data: list, pre: int, level: int) -> None: if len(data) == 0: indent(w, pre) w.write('\x1b[38;2;168;168;168m[]\x1b[0m') return indent(w, pre) w.write('\x1b[38;2;168;168;168m[\x1b[0m\n') for (i, e) in enumerate(data): if i > 0: w.write('\x1b[38;2;168;168;168m,\x1b[0m\n') restyle(w, e, level + 1, level + 1) w.write('\n') indent(w, level) w.write('\x1b[38;2;168;168;168m]\x1b[0m') def restyle_none(w, data: bool, pre: int, level: int) -> None: indent(w, pre) w.write('\x1b[38;2;168;168;168mnull\x1b[0m') def restyle_str(w, data: str, pre: int, level: int) -> None: indent(w, pre) s = '\x1b[38;2;168;168;168m' w.write(f'{s}"\x1b[0m{dumps(data)[1:-1]}{s}"\x1b[0m') def seems_url(s: str) -> bool: protocols = ('https://', 'http://', 'file://', 'ftp://', 'data:') return any(s.startswith(p) for p in protocols) # type2func helps func restyle dispatch styler funcs, using values' types # as the keys; while JSON doesn't have int values, the stdlib auto-promotes # round JSON numbers into ints, and so this table has a func for those too type2func = { type(None): restyle_none, bool: restyle_bool, int: restyle_int, float: restyle_float, str: restyle_str, list: restyle_list, tuple: restyle_list, dict: restyle_dict, } try: args = argv[1:] if len(argv) < 2 or argv[1] != '--' else argv[2:] name = args[0] if len(args) > 0 else '-' if len(args) > 1: raise ValueError('multiple inputs not allowed') if name == '-': restyle(stdout, load(stdin)) elif seems_url(name): from urllib.request import urlopen with urlopen(name) as inp: restyle(stdout, load(inp)) else: with open(name, encoding='utf-8') as inp: restyle(stdout, load(inp)) except BrokenPipeError: # quit quietly, instead of showing a confusing error message stderr.close() exit(0) except KeyboardInterrupt: exit(2) except Exception as e: print(f'\x1b[31m{e}\x1b[0m', file=stderr) exit(1)