flow-toon-format 0.9.0b2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flow_toon_format-0.9.0b2.dist-info/METADATA +200 -0
- flow_toon_format-0.9.0b2.dist-info/RECORD +24 -0
- flow_toon_format-0.9.0b2.dist-info/WHEEL +4 -0
- flow_toon_format-0.9.0b2.dist-info/entry_points.txt +2 -0
- flow_toon_format-0.9.0b2.dist-info/licenses/LICENSE +24 -0
- toon_format/__init__.py +40 -0
- toon_format/__main__.py +13 -0
- toon_format/_literal_utils.py +70 -0
- toon_format/_parsing_utils.py +167 -0
- toon_format/_scanner.py +289 -0
- toon_format/_string_utils.py +169 -0
- toon_format/_validation.py +150 -0
- toon_format/cli.py +217 -0
- toon_format/constants.py +84 -0
- toon_format/decoder.py +788 -0
- toon_format/encoder.py +56 -0
- toon_format/encoders.py +456 -0
- toon_format/logging_config.py +92 -0
- toon_format/normalize.py +237 -0
- toon_format/primitives.py +171 -0
- toon_format/py.typed +0 -0
- toon_format/types.py +64 -0
- toon_format/utils.py +187 -0
- toon_format/writer.py +53 -0
toon_format/cli.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Copyright (c) 2025 TOON Format Organization
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Command-line interface for TOON encoding/decoding.
|
|
4
|
+
|
|
5
|
+
Provides the `toon` command-line tool for converting between JSON and TOON formats.
|
|
6
|
+
Supports auto-detection based on file extensions and content, with options for
|
|
7
|
+
delimiters, indentation, and validation modes.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from . import decode, encode
|
|
16
|
+
from .types import DecodeOptions, EncodeOptions
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main() -> int:
|
|
20
|
+
"""Main CLI entry point."""
|
|
21
|
+
parser = argparse.ArgumentParser(
|
|
22
|
+
prog="toon",
|
|
23
|
+
description="Convert between JSON and TOON formats",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"input",
|
|
28
|
+
type=str,
|
|
29
|
+
help="Input file path (or - for stdin)",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"-o",
|
|
34
|
+
"--output",
|
|
35
|
+
type=str,
|
|
36
|
+
help="Output file path (prints to stdout if omitted)",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"-e",
|
|
41
|
+
"--encode",
|
|
42
|
+
action="store_true",
|
|
43
|
+
help="Force encode mode (overrides auto-detection)",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"-d",
|
|
48
|
+
"--decode",
|
|
49
|
+
action="store_true",
|
|
50
|
+
help="Force decode mode (overrides auto-detection)",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--delimiter",
|
|
55
|
+
type=str,
|
|
56
|
+
choices=[",", "\t", "|"],
|
|
57
|
+
default=",",
|
|
58
|
+
help='Array delimiter: , (comma), \\t (tab), | (pipe) (default: ",")',
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"--indent",
|
|
63
|
+
type=int,
|
|
64
|
+
default=2,
|
|
65
|
+
help="Indentation size (default: 2)",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"--length-marker",
|
|
70
|
+
action="store_true",
|
|
71
|
+
help="Add # prefix to array lengths (e.g., items[#3])",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--no-strict",
|
|
76
|
+
action="store_true",
|
|
77
|
+
help="Disable strict validation when decoding",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
args = parser.parse_args()
|
|
81
|
+
|
|
82
|
+
# Read input
|
|
83
|
+
try:
|
|
84
|
+
if args.input == "-":
|
|
85
|
+
input_text = sys.stdin.read()
|
|
86
|
+
input_path = None
|
|
87
|
+
else:
|
|
88
|
+
input_path = Path(args.input)
|
|
89
|
+
if not input_path.exists():
|
|
90
|
+
print(f"Error: Input file not found: {args.input}", file=sys.stderr)
|
|
91
|
+
return 1
|
|
92
|
+
input_text = input_path.read_text(encoding="utf-8")
|
|
93
|
+
except Exception as e:
|
|
94
|
+
print(f"Error reading input: {e}", file=sys.stderr)
|
|
95
|
+
return 1
|
|
96
|
+
|
|
97
|
+
# Determine operation mode
|
|
98
|
+
if args.encode and args.decode:
|
|
99
|
+
print("Error: Cannot specify both --encode and --decode", file=sys.stderr)
|
|
100
|
+
return 1
|
|
101
|
+
|
|
102
|
+
if args.encode:
|
|
103
|
+
mode = "encode"
|
|
104
|
+
elif args.decode:
|
|
105
|
+
mode = "decode"
|
|
106
|
+
else:
|
|
107
|
+
# Auto-detect based on file extension
|
|
108
|
+
if input_path:
|
|
109
|
+
if input_path.suffix.lower() == ".json":
|
|
110
|
+
mode = "encode"
|
|
111
|
+
elif input_path.suffix.lower() == ".toon":
|
|
112
|
+
mode = "decode"
|
|
113
|
+
else:
|
|
114
|
+
# Try to detect by content
|
|
115
|
+
try:
|
|
116
|
+
json.loads(input_text)
|
|
117
|
+
mode = "encode"
|
|
118
|
+
except json.JSONDecodeError:
|
|
119
|
+
mode = "decode"
|
|
120
|
+
else:
|
|
121
|
+
# No file path, try to detect by content
|
|
122
|
+
try:
|
|
123
|
+
json.loads(input_text)
|
|
124
|
+
mode = "encode"
|
|
125
|
+
except json.JSONDecodeError:
|
|
126
|
+
mode = "decode"
|
|
127
|
+
|
|
128
|
+
# Process
|
|
129
|
+
try:
|
|
130
|
+
if mode == "encode":
|
|
131
|
+
output_text = encode_json_to_toon(
|
|
132
|
+
input_text,
|
|
133
|
+
delimiter=args.delimiter,
|
|
134
|
+
indent=args.indent,
|
|
135
|
+
length_marker=args.length_marker,
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
output_text = decode_toon_to_json(
|
|
139
|
+
input_text,
|
|
140
|
+
indent=args.indent,
|
|
141
|
+
strict=not args.no_strict,
|
|
142
|
+
)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
print(f"Error during {mode}: {e}", file=sys.stderr)
|
|
145
|
+
return 1
|
|
146
|
+
|
|
147
|
+
# Write output
|
|
148
|
+
try:
|
|
149
|
+
if args.output:
|
|
150
|
+
output_path = Path(args.output)
|
|
151
|
+
output_path.write_text(output_text, encoding="utf-8")
|
|
152
|
+
else:
|
|
153
|
+
print(output_text)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
print(f"Error writing output: {e}", file=sys.stderr)
|
|
156
|
+
return 1
|
|
157
|
+
|
|
158
|
+
return 0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def encode_json_to_toon(
|
|
162
|
+
json_text: str,
|
|
163
|
+
delimiter: str = ",",
|
|
164
|
+
indent: int = 2,
|
|
165
|
+
length_marker: bool = False,
|
|
166
|
+
) -> str:
|
|
167
|
+
"""Encode JSON text to TOON format.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
json_text: JSON input string
|
|
171
|
+
delimiter: Delimiter character
|
|
172
|
+
indent: Indentation size
|
|
173
|
+
length_marker: Whether to add # prefix
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
TOON-formatted string
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
json.JSONDecodeError: If JSON is invalid
|
|
180
|
+
"""
|
|
181
|
+
data = json.loads(json_text)
|
|
182
|
+
|
|
183
|
+
options: EncodeOptions = {
|
|
184
|
+
"indent": indent,
|
|
185
|
+
"delimiter": delimiter,
|
|
186
|
+
"lengthMarker": "#" if length_marker else False,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return encode(data, options)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def decode_toon_to_json(
|
|
193
|
+
toon_text: str,
|
|
194
|
+
indent: int = 2,
|
|
195
|
+
strict: bool = True,
|
|
196
|
+
) -> str:
|
|
197
|
+
"""Decode TOON text to JSON format.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
toon_text: TOON input string
|
|
201
|
+
indent: Indentation size
|
|
202
|
+
strict: Whether to use strict validation
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
JSON-formatted string
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
ToonDecodeError: If TOON is invalid
|
|
209
|
+
"""
|
|
210
|
+
options = DecodeOptions(indent=indent, strict=strict)
|
|
211
|
+
data = decode(toon_text, options)
|
|
212
|
+
|
|
213
|
+
return json.dumps(data, indent=2, ensure_ascii=False)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
if __name__ == "__main__":
|
|
217
|
+
sys.exit(main())
|
toon_format/constants.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Copyright (c) 2025 TOON Format Organization
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Constants for TOON format encoding and decoding.
|
|
4
|
+
|
|
5
|
+
Defines all string literals, characters, and configuration values used throughout
|
|
6
|
+
the TOON implementation. Centralizes magic values for maintainability.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Dict
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .types import Delimiter
|
|
13
|
+
|
|
14
|
+
# region List markers
|
|
15
|
+
LIST_ITEM_MARKER = "-"
|
|
16
|
+
LIST_ITEM_PREFIX = "- "
|
|
17
|
+
# endregion
|
|
18
|
+
|
|
19
|
+
# region Structural characters
|
|
20
|
+
COMMA: "Delimiter" = ","
|
|
21
|
+
COLON = ":"
|
|
22
|
+
SPACE = " "
|
|
23
|
+
PIPE: "Delimiter" = "|"
|
|
24
|
+
# endregion
|
|
25
|
+
|
|
26
|
+
# region Brackets and braces
|
|
27
|
+
OPEN_BRACKET = "["
|
|
28
|
+
CLOSE_BRACKET = "]"
|
|
29
|
+
OPEN_BRACE = "{"
|
|
30
|
+
CLOSE_BRACE = "}"
|
|
31
|
+
# endregion
|
|
32
|
+
|
|
33
|
+
# region Literals
|
|
34
|
+
NULL_LITERAL = "null"
|
|
35
|
+
TRUE_LITERAL = "true"
|
|
36
|
+
FALSE_LITERAL = "false"
|
|
37
|
+
# endregion
|
|
38
|
+
|
|
39
|
+
# region Escape characters
|
|
40
|
+
BACKSLASH = "\\"
|
|
41
|
+
DOUBLE_QUOTE = '"'
|
|
42
|
+
NEWLINE = "\n"
|
|
43
|
+
CARRIAGE_RETURN = "\r"
|
|
44
|
+
TAB: "Delimiter" = "\t"
|
|
45
|
+
# endregion
|
|
46
|
+
|
|
47
|
+
# region Delimiters
|
|
48
|
+
DELIMITERS: Dict[str, "Delimiter"] = {
|
|
49
|
+
"comma": COMMA,
|
|
50
|
+
"tab": TAB,
|
|
51
|
+
"pipe": PIPE,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
DEFAULT_DELIMITER: "Delimiter" = DELIMITERS["comma"]
|
|
55
|
+
# endregion
|
|
56
|
+
|
|
57
|
+
# region Regex patterns
|
|
58
|
+
# Pattern strings are compiled in modules that use them
|
|
59
|
+
STRUCTURAL_CHARS_REGEX = r"[\[\]{}]"
|
|
60
|
+
CONTROL_CHARS_REGEX = r"[\n\r\t]"
|
|
61
|
+
NUMERIC_REGEX = r"^-?\d+(?:\.\d+)?(?:e[+-]?\d+)?$"
|
|
62
|
+
OCTAL_REGEX = r"^0\d+$"
|
|
63
|
+
VALID_KEY_REGEX = r"^[A-Z_][\w.]*$"
|
|
64
|
+
HEADER_LENGTH_REGEX = r"^#?(\d+)([\|\t])?$"
|
|
65
|
+
INTEGER_REGEX = r"^-?\d+$"
|
|
66
|
+
# endregion
|
|
67
|
+
|
|
68
|
+
# region Escape sequence maps
|
|
69
|
+
ESCAPE_SEQUENCES = {
|
|
70
|
+
BACKSLASH: "\\\\",
|
|
71
|
+
DOUBLE_QUOTE: '\\"',
|
|
72
|
+
NEWLINE: "\\n",
|
|
73
|
+
CARRIAGE_RETURN: "\\r",
|
|
74
|
+
TAB: "\\t",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
UNESCAPE_SEQUENCES = {
|
|
78
|
+
"n": NEWLINE,
|
|
79
|
+
"r": CARRIAGE_RETURN,
|
|
80
|
+
"t": TAB,
|
|
81
|
+
"\\": BACKSLASH,
|
|
82
|
+
'"': DOUBLE_QUOTE,
|
|
83
|
+
}
|
|
84
|
+
# endregion
|