Created
November 13, 2025 09:57
-
-
Save JGalego/ae7433c2bbe219e53c28f9a84079266b to your computer and use it in GitHub Desktop.
A simple JSON / JSONLines to TOON Converter π°
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| r""" | |
| A simple JSON / JSONLines to TOON Converter | |
| .------.. | |
| - - | |
| / \ | |
| / \ | |
| / .--._ .---. | | |
| | / -__- \ | | |
| | | | | | |
| || ._ _. || | |
| || o o || | |
| || _ |_ || | |
| C| (o\_/o) |O Uhhh, this computer | |
| \ _____ / is like, busted or | |
| \ ( /#####\ ) / something. So go away. | |
| \ `=====' / | |
| \ -___- / | |
| | | | |
| /-_____-\ | |
| / \ | |
| / \ | |
| /__| AC / DC |__\ | |
| | || |\ \ | |
| """ | |
| import json | |
| import re | |
| import sys | |
| def should_quote(s): | |
| """Quote strings only when TOON spec requires it""" | |
| if not isinstance(s, str): | |
| return s | |
| # Quote if: empty, whitespace, reserved words, numbers, or special chars | |
| needs_quotes = ( | |
| s == '' or # empty string | |
| s[0:1] == ' ' or s[-1:] == ' ' or # leading/trailing spaces | |
| s in ['true', 'false', 'null'] or # reserved literals | |
| re.match(r'^-?\d+(\.\d+)?(e[+-]?\d+)?$', s, re.I) or # numeric pattern | |
| re.match(r'^0\d+$', s) or # leading zero numbers | |
| any(c in s for c in ':\"\\[]{},-') # structural characters | |
| ) | |
| if needs_quotes: | |
| escaped = s.replace('\\', '\\\\').replace('"', '\\"') | |
| escaped = escaped.replace('\n', '\\n').replace('\r', '\\r').replace('\t', '\\t') | |
| return f'"{escaped}"' | |
| return s | |
| def _convert_primitive(value): | |
| """Convert primitive JSON values to TOON format""" | |
| if value is None: | |
| return 'null' | |
| if isinstance(value, bool): | |
| return 'true' if value else 'false' | |
| if isinstance(value, (int, float)): | |
| # Convert floats that are whole numbers to integers | |
| return str(int(value) if isinstance(value, float) and value == int(value) else value) | |
| if isinstance(value, str): | |
| return should_quote(value) | |
| return None | |
| def _is_uniform_primitive_objects(array): | |
| """Check if array contains uniform objects with only primitive values""" | |
| if not array or not isinstance(array[0], dict): | |
| return False | |
| first_keys = array[0].keys() | |
| return all(isinstance(x, dict) and x.keys() == first_keys and | |
| all(not isinstance(y, (dict, list)) for y in x.values()) | |
| for x in array) | |
| def _convert_array_tabular(array): | |
| """Convert array to tabular TOON format""" | |
| keys = list(array[0].keys()) | |
| header = f'[{len(array)}]{{{",".join(keys)}}}:' | |
| rows = '\n'.join(' ' + ','.join(str(to_toon(x[k])) for k in keys) for x in array) | |
| return f'{header}\n{rows}' | |
| def _convert_array_inline(array): | |
| """Convert array to inline TOON format""" | |
| return f'[{len(array)}]: ' + ','.join(str(to_toon(x)) for x in array) | |
| def _convert_array_list(array): | |
| """Convert array to list TOON format""" | |
| items = [] | |
| for x in array: | |
| if isinstance(x, dict) and x: | |
| # Object as list item: first field on hyphen line, rest indented | |
| keys = list(x.keys()) | |
| first_line = f'- {keys[0]}: {to_toon(x[keys[0]])}' | |
| remaining = ''.join(f'\n {k}: {to_toon(x[k])}' for k in keys[1:]) | |
| items.append(first_line + remaining) | |
| else: | |
| items.append(f'- {to_toon(x)}') | |
| return f'[{len(array)}]:\n ' + '\n '.join(items) | |
| def _convert_array(value): | |
| """Convert JSON array to TOON format""" | |
| if not value: | |
| return '[0]:' # empty array | |
| # Tabular format: uniform objects with primitive values only | |
| if _is_uniform_primitive_objects(value): | |
| return _convert_array_tabular(value) | |
| # Inline format: primitive values only | |
| if all(not isinstance(x, (dict, list)) for x in value): | |
| return _convert_array_inline(value) | |
| # List format: mixed or complex values | |
| return _convert_array_list(value) | |
| def _convert_object(value, depth=0): | |
| """Convert JSON object to TOON format""" | |
| if not value: | |
| return '' # empty object | |
| lines = [] | |
| for key in value: | |
| # Quote key if it doesn't match identifier pattern | |
| key_str = should_quote(key) if not re.match(r'^[A-Za-z_][A-Za-z0-9_.]*$', key) else key | |
| indent = ' ' * depth | |
| if isinstance(value[key], list): | |
| # Array: put header on same line as key | |
| array_toon = to_toon(value[key], depth) or '' | |
| if '\n' in array_toon: | |
| lines.append(indent + key_str + array_toon.replace('\n', '\n' + ' ' * depth)) | |
| else: | |
| lines.append(indent + key_str + array_toon) | |
| elif not isinstance(value[key], (dict, list)) or not value[key]: | |
| # Primitive or empty: inline format | |
| lines.append(f'{indent}{key_str}: {to_toon(value[key], depth + 1)}') | |
| else: | |
| # Nested object: key on its own line, content indented | |
| lines.append(f'{indent}{key_str}:\n{to_toon(value[key], depth + 1)}') | |
| return '\n'.join(lines) | |
| def to_toon(value, depth=0): | |
| """Convert JSON value to TOON format""" | |
| # Try primitive conversion first | |
| primitive_result = _convert_primitive(value) | |
| if primitive_result is not None: | |
| return primitive_result | |
| # Handle arrays | |
| if isinstance(value, list): | |
| return _convert_array(value) | |
| # Handle objects | |
| if isinstance(value, dict): | |
| return _convert_object(value, depth) | |
| # Fallback for unexpected types | |
| return str(value) | |
| def _read_input_lines(): | |
| """Read and filter input lines from stdin""" | |
| input_lines = [] | |
| for line in sys.stdin: | |
| line = line.strip() | |
| if line: # Skip empty lines | |
| input_lines.append(line) | |
| return input_lines | |
| def _is_jsonlines_format(input_lines): | |
| """Determine if input is JSONLines format""" | |
| if len(input_lines) > 1: | |
| return True | |
| # Single line - try parsing as single JSON first | |
| try: | |
| json.loads(input_lines[0]) | |
| return False | |
| except json.JSONDecodeError: | |
| return True | |
| def _process_jsonlines(input_lines): | |
| """Process JSONLines format input""" | |
| objects = [] | |
| for i, line in enumerate(input_lines, 1): | |
| try: | |
| parsed_json = json.loads(line) | |
| objects.append(parsed_json) | |
| except json.JSONDecodeError as e: | |
| print(f"Error parsing line {i}: {e}", file=sys.stderr) | |
| continue | |
| # Output each object as TOON with separators | |
| for i, obj in enumerate(objects): | |
| if i > 0: | |
| print("---") # Separator between objects | |
| toon_output = to_toon(obj) | |
| print(toon_output) | |
| def _process_single_json(input_lines): | |
| """Process single JSON object input""" | |
| try: | |
| # Join all lines in case JSON is pretty-printed across multiple lines | |
| json_input = '\n'.join(input_lines) | |
| parsed_json = json.loads(json_input) | |
| toon_output = to_toon(parsed_json) | |
| print(toon_output) | |
| except json.JSONDecodeError as e: | |
| print(f"Error parsing JSON: {e}", file=sys.stderr) | |
| sys.exit(1) | |
| def process_input(): | |
| """Process JSON or JSONLines input from stdin""" | |
| input_lines = _read_input_lines() | |
| if not input_lines: | |
| return | |
| if _is_jsonlines_format(input_lines): | |
| _process_jsonlines(input_lines) | |
| else: | |
| _process_single_json(input_lines) | |
| # Main program: read JSON/JSONLines from stdin and output TOON | |
| if __name__ == "__main__": | |
| process_input() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment