janito 0.5.0__py3-none-any.whl → 0.7.0__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.
- janito/__init__.py +0 -47
- janito/__main__.py +105 -17
- janito/agents/__init__.py +9 -9
- janito/agents/agent.py +10 -3
- janito/agents/claudeai.py +15 -34
- janito/agents/openai.py +5 -1
- janito/change/__init__.py +29 -16
- janito/change/__main__.py +0 -0
- janito/{analysis → change/analysis}/__init__.py +5 -15
- janito/change/analysis/__main__.py +7 -0
- janito/change/analysis/analyze.py +62 -0
- janito/change/analysis/formatting.py +78 -0
- janito/change/analysis/options.py +81 -0
- janito/{analysis → change/analysis}/prompts.py +33 -18
- janito/change/analysis/view/__init__.py +9 -0
- janito/change/analysis/view/terminal.py +181 -0
- janito/change/applier/__init__.py +5 -0
- janito/change/applier/file.py +58 -0
- janito/change/applier/main.py +156 -0
- janito/change/applier/text.py +247 -0
- janito/change/applier/workspace_dir.py +58 -0
- janito/change/core.py +124 -0
- janito/{changehistory.py → change/history.py} +12 -14
- janito/change/operations.py +7 -0
- janito/change/parser.py +287 -0
- janito/change/play.py +54 -0
- janito/change/preview.py +82 -0
- janito/change/prompts.py +121 -0
- janito/change/test.py +0 -0
- janito/change/validator.py +269 -0
- janito/{changeviewer → change/viewer}/__init__.py +3 -4
- janito/change/viewer/content.py +66 -0
- janito/{changeviewer → change/viewer}/diff.py +19 -4
- janito/change/viewer/panels.py +533 -0
- janito/change/viewer/styling.py +114 -0
- janito/{changeviewer → change/viewer}/themes.py +3 -5
- janito/clear_statement_parser/clear_statement_format.txt +328 -0
- janito/clear_statement_parser/examples.txt +326 -0
- janito/clear_statement_parser/models.py +104 -0
- janito/clear_statement_parser/parser.py +496 -0
- janito/cli/base.py +30 -0
- janito/cli/commands.py +75 -40
- janito/cli/functions.py +19 -194
- janito/cli/history.py +61 -0
- janito/common.py +65 -8
- janito/config.py +70 -5
- janito/demo/__init__.py +4 -0
- janito/demo/data.py +13 -0
- janito/demo/mock_data.py +20 -0
- janito/demo/operations.py +45 -0
- janito/demo/runner.py +59 -0
- janito/demo/scenarios.py +32 -0
- janito/prompt.py +36 -0
- janito/qa.py +6 -14
- janito/search_replace/README.md +192 -0
- janito/search_replace/__init__.py +7 -0
- janito/search_replace/__main__.py +21 -0
- janito/search_replace/core.py +120 -0
- janito/search_replace/logger.py +35 -0
- janito/search_replace/parser.py +52 -0
- janito/search_replace/play.py +61 -0
- janito/search_replace/replacer.py +36 -0
- janito/search_replace/searcher.py +411 -0
- janito/search_replace/strategy_result.py +10 -0
- janito/shell/__init__.py +38 -0
- janito/shell/bus.py +31 -0
- janito/shell/commands.py +136 -0
- janito/shell/history.py +20 -0
- janito/shell/processor.py +32 -0
- janito/shell/prompt.py +48 -0
- janito/shell/registry.py +60 -0
- janito/tui/__init__.py +21 -0
- janito/tui/base.py +22 -0
- janito/tui/flows/__init__.py +5 -0
- janito/tui/flows/changes.py +65 -0
- janito/tui/flows/content.py +128 -0
- janito/tui/flows/selection.py +117 -0
- janito/tui/screens/__init__.py +3 -0
- janito/tui/screens/app.py +1 -0
- janito/workspace/__init__.py +6 -0
- janito/workspace/analysis.py +121 -0
- janito/workspace/show.py +141 -0
- janito/workspace/stats.py +43 -0
- janito/workspace/types.py +98 -0
- janito/workspace/workset.py +108 -0
- janito/workspace/workspace.py +114 -0
- janito-0.7.0.dist-info/METADATA +167 -0
- janito-0.7.0.dist-info/RECORD +96 -0
- {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/WHEEL +1 -1
- janito/_contextparser.py +0 -113
- janito/analysis/display.py +0 -149
- janito/analysis/options.py +0 -112
- janito/change/applier.py +0 -269
- janito/change/content.py +0 -62
- janito/change/indentation.py +0 -33
- janito/change/position.py +0 -169
- janito/changeviewer/panels.py +0 -268
- janito/changeviewer/styling.py +0 -59
- janito/console/__init__.py +0 -3
- janito/console/commands.py +0 -112
- janito/console/core.py +0 -62
- janito/console/display.py +0 -157
- janito/fileparser.py +0 -334
- janito/prompts.py +0 -81
- janito/scan.py +0 -176
- janito/tests/test_fileparser.py +0 -26
- janito-0.5.0.dist-info/METADATA +0 -146
- janito-0.5.0.dist-info/RECORD +0 -45
- {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/entry_points.txt +0 -0
- {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,104 @@
|
|
1
|
+
# models.py
|
2
|
+
from dataclasses import dataclass
|
3
|
+
from typing import Dict, List, Union, Optional, Tuple
|
4
|
+
from enum import Enum
|
5
|
+
|
6
|
+
class LineType(Enum):
|
7
|
+
EMPTY = "empty"
|
8
|
+
COMMENT = "comment"
|
9
|
+
LIST_ITEM = "list_item"
|
10
|
+
STATEMENT = "statement"
|
11
|
+
LITERAL_BLOCK = "literal_block"
|
12
|
+
KEY_VALUE = "key_value"
|
13
|
+
BLOCK_BEGIN = "block_begin"
|
14
|
+
BLOCK_END = "block_end"
|
15
|
+
|
16
|
+
@dataclass
|
17
|
+
class ParseError(Exception):
|
18
|
+
line_number: int
|
19
|
+
statement_context: str
|
20
|
+
message: str
|
21
|
+
|
22
|
+
def __str__(self):
|
23
|
+
return f"Line {self.line_number}: {self.message} (in statement: {self.statement_context})"
|
24
|
+
|
25
|
+
class Statement:
|
26
|
+
def __init__(self, name: str):
|
27
|
+
self.name = name
|
28
|
+
self.parameters: Dict[str, Union[str, List, Dict]] = {}
|
29
|
+
self.blocks: List[Tuple[str, List[Statement]]] = []
|
30
|
+
|
31
|
+
def to_dict(self) -> Dict:
|
32
|
+
"""Convert the statement to a dictionary representation"""
|
33
|
+
result = {
|
34
|
+
'name': self.name,
|
35
|
+
'parameters': self.parameters.copy(),
|
36
|
+
'blocks': []
|
37
|
+
}
|
38
|
+
|
39
|
+
# Convert nested statements in blocks to dicts
|
40
|
+
for block_name, block_statements in self.blocks:
|
41
|
+
block_dicts = [stmt.to_dict() for stmt in block_statements]
|
42
|
+
result['blocks'].append({
|
43
|
+
'name': block_name,
|
44
|
+
'statements': block_dicts
|
45
|
+
})
|
46
|
+
|
47
|
+
return result
|
48
|
+
|
49
|
+
def __str__(self):
|
50
|
+
return self.__repr__()
|
51
|
+
|
52
|
+
def __repr__(self):
|
53
|
+
indent = 0
|
54
|
+
return self._repr_recursive(indent)
|
55
|
+
|
56
|
+
def _repr_recursive(self, indent: int, max_line_length: int = 80) -> str:
|
57
|
+
lines = []
|
58
|
+
prefix = " " * indent
|
59
|
+
|
60
|
+
# Add statement name
|
61
|
+
lines.append(f"{prefix}{self.name}")
|
62
|
+
|
63
|
+
# Add parameters with proper indentation
|
64
|
+
for key, value in self.parameters.items():
|
65
|
+
if isinstance(value, str) and '\n' in value:
|
66
|
+
# Handle multiline string values (literal blocks)
|
67
|
+
lines.append(f"{prefix} {key}:")
|
68
|
+
for line in value.split('\n'):
|
69
|
+
lines.append(f"{prefix} .{line}")
|
70
|
+
elif isinstance(value, list):
|
71
|
+
# Handle nested lists with proper indentation
|
72
|
+
lines.append(f"{prefix} {key}:")
|
73
|
+
lines.extend(self._format_list(value, indent + 2))
|
74
|
+
else:
|
75
|
+
# Handle simple key-value pairs
|
76
|
+
value_str = repr(value) if isinstance(value, str) else str(value)
|
77
|
+
if len(f"{prefix} {key}: {value_str}") <= max_line_length:
|
78
|
+
lines.append(f"{prefix} {key}: {value_str}")
|
79
|
+
else:
|
80
|
+
lines.append(f"{prefix} {key}:")
|
81
|
+
lines.append(f"{prefix} {value_str}")
|
82
|
+
|
83
|
+
# Add blocks with proper indentation and block markers
|
84
|
+
for block_name, block_statements in self.blocks:
|
85
|
+
lines.append(f"{prefix} /{block_name}")
|
86
|
+
for statement in block_statements:
|
87
|
+
lines.append(statement._repr_recursive(indent + 2))
|
88
|
+
lines.append(f"{prefix} {block_name}/")
|
89
|
+
|
90
|
+
return "\n".join(lines)
|
91
|
+
|
92
|
+
def _format_list(self, lst: List, indent: int) -> List[str]:
|
93
|
+
lines = []
|
94
|
+
prefix = " " * indent
|
95
|
+
|
96
|
+
def format_nested(items, depth=0):
|
97
|
+
for item in items:
|
98
|
+
if isinstance(item, list):
|
99
|
+
format_nested(item, depth + 1)
|
100
|
+
else:
|
101
|
+
lines.append(f"{prefix}{'-' * (depth + 1)} {item}")
|
102
|
+
|
103
|
+
format_nested(lst)
|
104
|
+
return lines
|
@@ -0,0 +1,496 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from typing import Dict, List, Union, Optional, Tuple
|
3
|
+
from enum import Enum
|
4
|
+
import re
|
5
|
+
|
6
|
+
class LineType(Enum):
|
7
|
+
EMPTY = "empty"
|
8
|
+
COMMENT = "comment"
|
9
|
+
LIST_ITEM = "list_item"
|
10
|
+
STATEMENT = "statement"
|
11
|
+
LITERAL_BLOCK = "literal_block"
|
12
|
+
KEY_VALUE = "key_value"
|
13
|
+
BLOCK_BEGIN = "block_begin"
|
14
|
+
BLOCK_END = "block_end"
|
15
|
+
|
16
|
+
@dataclass
|
17
|
+
class ParseError(Exception):
|
18
|
+
line_number: int
|
19
|
+
statement_context: str
|
20
|
+
message: str
|
21
|
+
|
22
|
+
def __str__(self):
|
23
|
+
return f"Line {self.line_number}: {self.message} (in statement: {self.statement_context})"
|
24
|
+
|
25
|
+
class Statement:
|
26
|
+
def __init__(self, name: str):
|
27
|
+
self.name = name
|
28
|
+
self.parameters: Dict[str, Union[str, List, Dict]] = {}
|
29
|
+
self.blocks: List[Tuple[str, List[Statement]]] = []
|
30
|
+
|
31
|
+
def to_dict(self) -> Dict:
|
32
|
+
"""Convert the statement to a dictionary representation"""
|
33
|
+
result = {
|
34
|
+
'name': self.name,
|
35
|
+
'parameters': self.parameters.copy(),
|
36
|
+
'blocks': []
|
37
|
+
}
|
38
|
+
|
39
|
+
# Convert nested statements in blocks to dicts
|
40
|
+
for block_name, block_statements in self.blocks:
|
41
|
+
block_dicts = [stmt.to_dict() for stmt in block_statements]
|
42
|
+
result['blocks'].append({
|
43
|
+
'name': block_name,
|
44
|
+
'statements': block_dicts
|
45
|
+
})
|
46
|
+
|
47
|
+
return result
|
48
|
+
|
49
|
+
def __str__(self):
|
50
|
+
return self.__repr__()
|
51
|
+
|
52
|
+
def __repr__(self):
|
53
|
+
indent = 0
|
54
|
+
return self._repr_recursive(indent)
|
55
|
+
|
56
|
+
def _repr_recursive(self, indent: int, max_line_length: int = 80) -> str:
|
57
|
+
lines = []
|
58
|
+
prefix = " " * indent
|
59
|
+
|
60
|
+
# Add statement name
|
61
|
+
lines.append(f"{prefix}{self.name}")
|
62
|
+
|
63
|
+
# Add parameters with proper indentation
|
64
|
+
for key, value in self.parameters.items():
|
65
|
+
if isinstance(value, str) and '\n' in value:
|
66
|
+
# Handle multiline string values (literal blocks)
|
67
|
+
lines.append(f"{prefix} {key}:")
|
68
|
+
for line in value.split('\n'):
|
69
|
+
lines.append(f"{prefix} .{line}")
|
70
|
+
elif isinstance(value, list):
|
71
|
+
# Handle nested lists with proper indentation
|
72
|
+
lines.append(f"{prefix} {key}:")
|
73
|
+
lines.extend(self._format_list(value, indent + 2))
|
74
|
+
else:
|
75
|
+
# Handle simple key-value pairs
|
76
|
+
value_str = repr(value) if isinstance(value, str) else str(value)
|
77
|
+
if len(f"{prefix} {key}: {value_str}") <= max_line_length:
|
78
|
+
lines.append(f"{prefix} {key}: {value_str}")
|
79
|
+
else:
|
80
|
+
lines.append(f"{prefix} {key}:")
|
81
|
+
lines.append(f"{prefix} {value_str}")
|
82
|
+
|
83
|
+
# Add blocks with proper indentation and block markers
|
84
|
+
for block_name, block_statements in self.blocks:
|
85
|
+
lines.append(f"{prefix} /{block_name}")
|
86
|
+
for statement in block_statements:
|
87
|
+
lines.append(statement._repr_recursive(indent + 2))
|
88
|
+
lines.append(f"{prefix} {block_name}/")
|
89
|
+
|
90
|
+
return "\n".join(lines)
|
91
|
+
|
92
|
+
def _format_list(self, lst: List, indent: int) -> List[str]:
|
93
|
+
lines = []
|
94
|
+
prefix = " " * indent
|
95
|
+
|
96
|
+
def format_nested(items, depth=0):
|
97
|
+
for item in items:
|
98
|
+
if isinstance(item, list):
|
99
|
+
format_nested(item, depth + 1)
|
100
|
+
else:
|
101
|
+
lines.append(f"{prefix}{'-' * (depth + 1)} {item}")
|
102
|
+
|
103
|
+
format_nested(lst)
|
104
|
+
return lines
|
105
|
+
|
106
|
+
class BlockContext:
|
107
|
+
def __init__(self, name: str, line_number: int, parent: Optional['BlockContext'] = None):
|
108
|
+
self.name = name
|
109
|
+
self.line_number = line_number
|
110
|
+
self.parent = parent
|
111
|
+
self.statements: List[Statement] = []
|
112
|
+
self.current_statement: Optional[Statement] = None
|
113
|
+
self.parameter_type: Optional[LineType] = None
|
114
|
+
self._block_counters: Dict[str, int] = {} # Track block counters by base name
|
115
|
+
|
116
|
+
def get_current_block_chain(self) -> List[str]:
|
117
|
+
"""Get list of all block base names in current chain"""
|
118
|
+
chain = []
|
119
|
+
current = self
|
120
|
+
while current and current.name != "root":
|
121
|
+
chain.append(current.get_base_name())
|
122
|
+
current = current.parent
|
123
|
+
return chain
|
124
|
+
|
125
|
+
# Fixed implementation:
|
126
|
+
def validate_block_name(self, name: str, line_number: int) -> str:
|
127
|
+
"""Generate internal tracking name for blocks with improved validation"""
|
128
|
+
# Convert name to lowercase for case-insensitive comparison
|
129
|
+
normalized_name = name.lower()
|
130
|
+
|
131
|
+
# Check if this block name exists in the current chain (case-insensitive)
|
132
|
+
current_chain = [block_name.lower() for block_name in self.get_current_block_chain()]
|
133
|
+
if normalized_name in current_chain:
|
134
|
+
raise ParseError(line_number, "",
|
135
|
+
f"Cannot open block '{name}' inside another block of the same name")
|
136
|
+
|
137
|
+
# Check if this block name exists in any parent context (case-insensitive)
|
138
|
+
if self.is_name_in_parent_chain(name):
|
139
|
+
raise ParseError(line_number, "",
|
140
|
+
f"Block name '{name}' cannot be used while a block with the same name is still open")
|
141
|
+
|
142
|
+
if name not in self._block_counters:
|
143
|
+
self._block_counters[name] = 0
|
144
|
+
|
145
|
+
# Increment counter and generate internal name if needed
|
146
|
+
self._block_counters[name] += 1
|
147
|
+
if self._block_counters[name] > 1:
|
148
|
+
return f"{name}#{self._block_counters[name]}"
|
149
|
+
|
150
|
+
return name
|
151
|
+
|
152
|
+
def get_base_name(self) -> str:
|
153
|
+
"""Get the base name without any internal counter"""
|
154
|
+
return self.name.split('#')[0]
|
155
|
+
|
156
|
+
def validate_block_end(self, name: str, line_number: int) -> bool:
|
157
|
+
"""Validate block end name matches current context"""
|
158
|
+
return name == self.get_base_name()
|
159
|
+
|
160
|
+
def new_statement(self):
|
161
|
+
"""Reset statement-specific tracking when starting a new statement"""
|
162
|
+
self._block_counters.clear()
|
163
|
+
self.current_statement = None
|
164
|
+
self.parameter_type = None
|
165
|
+
|
166
|
+
# The issue also requires an update to is_name_in_parent_chain:
|
167
|
+
def is_name_in_parent_chain(self, name: str) -> bool:
|
168
|
+
"""Check if a block name exists in the parent chain (case-insensitive)"""
|
169
|
+
normalized_name = name.lower()
|
170
|
+
current = self
|
171
|
+
while current and current.name != "root":
|
172
|
+
if current.get_base_name().lower() == normalized_name:
|
173
|
+
return True
|
174
|
+
current = current.parent
|
175
|
+
return False
|
176
|
+
|
177
|
+
class StatementParser:
|
178
|
+
def __init__(self):
|
179
|
+
self.max_list_depth = 5
|
180
|
+
self.max_block_depth = 10
|
181
|
+
self.errors: List[ParseError] = []
|
182
|
+
self.debug = False
|
183
|
+
|
184
|
+
def parse(self, content: str, debug: bool = False) -> List[Statement]:
|
185
|
+
self.debug = debug
|
186
|
+
self.errors = []
|
187
|
+
lines = content.splitlines()
|
188
|
+
|
189
|
+
if self.debug:
|
190
|
+
print("\nStarting parse with debug mode enabled")
|
191
|
+
|
192
|
+
context = BlockContext("root", -1)
|
193
|
+
self._parse_context(lines, 0, len(lines), context)
|
194
|
+
return context.statements
|
195
|
+
|
196
|
+
def _parse_context(self, lines: List[str], start: int, end: int, context: BlockContext) -> int:
|
197
|
+
"""Parse statements within the current block context"""
|
198
|
+
line_number = start
|
199
|
+
|
200
|
+
while line_number < end:
|
201
|
+
line = lines[line_number].strip()
|
202
|
+
line_type = self._determine_line_type(line)
|
203
|
+
|
204
|
+
if self.debug:
|
205
|
+
self._print_debug_info(line_number, line, line_type, context)
|
206
|
+
|
207
|
+
try:
|
208
|
+
# Handle each line type according to spec
|
209
|
+
if line_type in (LineType.EMPTY, LineType.COMMENT):
|
210
|
+
line_number += 1
|
211
|
+
continue
|
212
|
+
|
213
|
+
elif line_type == LineType.STATEMENT:
|
214
|
+
context.new_statement() # Reset statement context
|
215
|
+
context.current_statement = Statement(line)
|
216
|
+
context.statements.append(context.current_statement)
|
217
|
+
context.parameter_type = None
|
218
|
+
line_number += 1
|
219
|
+
|
220
|
+
elif line_type == LineType.BLOCK_BEGIN:
|
221
|
+
if not context.current_statement:
|
222
|
+
raise ParseError(line_number, "", "Block begin found outside statement")
|
223
|
+
|
224
|
+
base_name = self._validate_block_name(line[1:].strip(), line_number)
|
225
|
+
|
226
|
+
# Check if block name is used in any parent block
|
227
|
+
if context.is_name_in_parent_chain(base_name):
|
228
|
+
raise ParseError(line_number, "",
|
229
|
+
f"Block name '{base_name}' cannot be used while a block with the same name is still open")
|
230
|
+
|
231
|
+
tracked_name = context.validate_block_name(base_name, line_number)
|
232
|
+
new_context = BlockContext(tracked_name, line_number, context)
|
233
|
+
|
234
|
+
if self.debug:
|
235
|
+
print(f" Opening block '{tracked_name}' at line {line_number + 1}")
|
236
|
+
|
237
|
+
# Parse nested block
|
238
|
+
line_number = self._parse_context(lines, line_number + 1, end, new_context)
|
239
|
+
context.current_statement.blocks.append((tracked_name, new_context.statements))
|
240
|
+
continue
|
241
|
+
|
242
|
+
elif line_type == LineType.BLOCK_END:
|
243
|
+
base_name = line[:-1].strip() # Remove trailing slash
|
244
|
+
|
245
|
+
if not context.parent:
|
246
|
+
raise ParseError(line_number, "",
|
247
|
+
f"Unexpected block end '{base_name}', no blocks are currently open")
|
248
|
+
|
249
|
+
if not context.validate_block_end(base_name, line_number):
|
250
|
+
raise ParseError(line_number, "",
|
251
|
+
f"Mismatched block end '{base_name}', expected '{context.get_base_name()}' (opened at line {context.line_number + 1})")
|
252
|
+
|
253
|
+
if self.debug:
|
254
|
+
print(f" Closing block '{context.name}' opened at line {context.line_number + 1}")
|
255
|
+
|
256
|
+
return line_number + 1
|
257
|
+
|
258
|
+
else:
|
259
|
+
# Handle parameter types (key/value, list, literal block)
|
260
|
+
line_number = self._handle_parameter(line_number, lines, line_type, context)
|
261
|
+
|
262
|
+
except ParseError as e:
|
263
|
+
self.errors.append(e)
|
264
|
+
line_number += 1
|
265
|
+
|
266
|
+
# Check for unclosed blocks at end of input
|
267
|
+
if context.parent is not None:
|
268
|
+
raise ParseError(end, "",
|
269
|
+
f"Reached end of input without closing block '{context.name}' (opened at line {context.line_number + 1})")
|
270
|
+
|
271
|
+
return line_number
|
272
|
+
|
273
|
+
def _handle_parameter(self, line_number: int, lines: List, line_type: LineType, context: BlockContext) -> int:
|
274
|
+
"""Handle parameter parsing according to spec rules"""
|
275
|
+
if not context.current_statement:
|
276
|
+
raise ParseError(line_number, "", f"{line_type.value} found outside statement")
|
277
|
+
|
278
|
+
if context.parameter_type and context.parameter_type != line_type:
|
279
|
+
raise ParseError(line_number, context.current_statement.name,
|
280
|
+
"Cannot mix different parameter types")
|
281
|
+
|
282
|
+
context.parameter_type = line_type
|
283
|
+
|
284
|
+
if line_type == LineType.KEY_VALUE:
|
285
|
+
return self._parse_key_value(line_number, lines, context)
|
286
|
+
elif line_type == LineType.LIST_ITEM:
|
287
|
+
return self._parse_list_items(line_number, lines, context)
|
288
|
+
elif line_type == LineType.LITERAL_BLOCK:
|
289
|
+
return self._parse_literal_block(line_number, lines, context)
|
290
|
+
|
291
|
+
return line_number + 1
|
292
|
+
|
293
|
+
def _determine_line_type(self, line: str) -> LineType:
|
294
|
+
"""Determine the type of a line based on its content"""
|
295
|
+
if not line:
|
296
|
+
return LineType.EMPTY
|
297
|
+
if line.startswith("#"):
|
298
|
+
return LineType.COMMENT
|
299
|
+
if line.startswith("-"):
|
300
|
+
return LineType.LIST_ITEM
|
301
|
+
if line.startswith("."):
|
302
|
+
return LineType.LITERAL_BLOCK
|
303
|
+
if ":" in line:
|
304
|
+
return LineType.KEY_VALUE
|
305
|
+
if line.startswith("/"):
|
306
|
+
return LineType.BLOCK_BEGIN
|
307
|
+
if line.endswith("/"):
|
308
|
+
return LineType.BLOCK_END
|
309
|
+
if not all(c.isalnum() or c.isspace() for c in line):
|
310
|
+
raise ParseError(0, line, "Statements must contain only alphanumeric characters and spaces")
|
311
|
+
return LineType.STATEMENT
|
312
|
+
|
313
|
+
def _validate_block_name(self, name: str, line_number: int) -> str:
|
314
|
+
"""Validate block name contains only alphanumeric characters"""
|
315
|
+
if '#' in name:
|
316
|
+
raise ParseError(line_number, name, "Block names cannot contain '#' characters")
|
317
|
+
if not name.isalnum():
|
318
|
+
raise ParseError(line_number, name, "Block names must contain only alphanumeric characters")
|
319
|
+
return name
|
320
|
+
|
321
|
+
def _parse_key_value(self, line_number: int, lines: List[str], context: BlockContext) -> int:
|
322
|
+
"""Parse a key-value line, stripping whitespace after the colon"""
|
323
|
+
line = lines[line_number].strip()
|
324
|
+
key, value = line.split(":", 1)
|
325
|
+
key, value = key.strip(), value.strip()
|
326
|
+
|
327
|
+
if key in context.current_statement.parameters:
|
328
|
+
raise ParseError(line_number, context.current_statement.name, f"Duplicate key: {key}")
|
329
|
+
|
330
|
+
if not value: # Empty value means we expect a literal block or list to follow
|
331
|
+
line_number, value = self._parse_complex_value(lines, line_number + 1)
|
332
|
+
|
333
|
+
context.current_statement.parameters[key] = value
|
334
|
+
return line_number + 1
|
335
|
+
|
336
|
+
def _parse_complex_value(self, lines: List[str], start_line: int) -> tuple[int, Union[str, List]]:
|
337
|
+
if start_line >= len(lines):
|
338
|
+
raise ParseError(start_line - 1, "", "Expected literal block or list after empty value")
|
339
|
+
|
340
|
+
next_line = lines[start_line].strip()
|
341
|
+
if next_line.startswith("."):
|
342
|
+
return self._parse_literal_block_value(lines, start_line)
|
343
|
+
elif next_line.startswith("-"):
|
344
|
+
return self._parse_list_value(lines, start_line)
|
345
|
+
else:
|
346
|
+
raise ParseError(start_line, "", "Expected literal block or list after empty value")
|
347
|
+
|
348
|
+
def _parse_literal_block_value(self, lines: List[str], start_line: int) -> tuple[int, str]:
|
349
|
+
"""Parse a literal block value, preserving content after the leading dot"""
|
350
|
+
literal_lines = []
|
351
|
+
current_line = start_line
|
352
|
+
|
353
|
+
while current_line < len(lines):
|
354
|
+
line = lines[current_line].strip()
|
355
|
+
if not line or line.startswith("#"):
|
356
|
+
current_line += 1
|
357
|
+
continue
|
358
|
+
|
359
|
+
if not line.startswith("."):
|
360
|
+
break
|
361
|
+
|
362
|
+
content = line[1:] # Strip only the leading dot
|
363
|
+
literal_lines.append(content)
|
364
|
+
current_line += 1
|
365
|
+
|
366
|
+
if not literal_lines:
|
367
|
+
raise ParseError(start_line, "", "Empty literal block")
|
368
|
+
|
369
|
+
return current_line - 1, "\n".join(literal_lines)
|
370
|
+
|
371
|
+
def _parse_list_value(self, lines: List[str], start_line: int) -> tuple[int, List]:
|
372
|
+
result = []
|
373
|
+
current_depth = 0
|
374
|
+
current_line = start_line
|
375
|
+
|
376
|
+
while current_line < len(lines):
|
377
|
+
line = lines[current_line].strip()
|
378
|
+
if not line or line.startswith("#"):
|
379
|
+
current_line += 1
|
380
|
+
continue
|
381
|
+
|
382
|
+
if not line.startswith("-"):
|
383
|
+
break
|
384
|
+
|
385
|
+
depth = len(re.match(r'-+', line).group())
|
386
|
+
if depth > self.max_list_depth:
|
387
|
+
raise ParseError(current_line, "", f"Maximum list depth of {self.max_list_depth} exceeded")
|
388
|
+
|
389
|
+
if depth > current_depth + 1:
|
390
|
+
raise ParseError(current_line, "", f"Invalid list nesting: skipped level {current_depth + 1}")
|
391
|
+
|
392
|
+
content = line[depth:].strip()
|
393
|
+
if not content:
|
394
|
+
raise ParseError(current_line, "", "Empty list item")
|
395
|
+
|
396
|
+
current_node = result
|
397
|
+
for _ in range(depth - 1):
|
398
|
+
if not current_node or not isinstance(current_node[-1], list):
|
399
|
+
current_node.append([])
|
400
|
+
current_node = current_node[-1]
|
401
|
+
|
402
|
+
current_node.append(content)
|
403
|
+
current_depth = depth
|
404
|
+
current_line += 1
|
405
|
+
|
406
|
+
if not result:
|
407
|
+
raise ParseError(start_line, "", "Empty list")
|
408
|
+
|
409
|
+
return current_line - 1, result
|
410
|
+
|
411
|
+
def _parse_list_items(self, line_number: int, lines: List[str], context: BlockContext) -> int:
|
412
|
+
_, items = self._parse_list_value(lines, line_number)
|
413
|
+
context.current_statement.parameters["items"] = items
|
414
|
+
return line_number + 1
|
415
|
+
|
416
|
+
def _parse_literal_block(self, line_number: int, lines: List[str], context: BlockContext) -> int:
|
417
|
+
_, text = self._parse_literal_block_value(lines, line_number)
|
418
|
+
context.current_statement.parameters["text"] = text
|
419
|
+
return line_number + 1
|
420
|
+
|
421
|
+
def _print_debug_info(self, line_number: int, line: str, line_type: LineType, context: BlockContext) -> None:
|
422
|
+
"""Print debug information about current parsing state"""
|
423
|
+
print(f"\nLine {line_number + 1}: Type={line_type.value}, Content='{line}'")
|
424
|
+
|
425
|
+
# Print block context chain
|
426
|
+
current = context
|
427
|
+
if current.name != "root":
|
428
|
+
chain = []
|
429
|
+
while current and current.name != "root":
|
430
|
+
chain.append(f"'{current.name}' (line {current.line_number + 1})")
|
431
|
+
current = current.parent
|
432
|
+
print(" Block context:", " -> ".join(reversed(chain)))
|
433
|
+
|
434
|
+
def print_statements(self, statements: List[Statement]) -> None:
|
435
|
+
"""Print statements in a hierarchical structure format"""
|
436
|
+
for statement in statements:
|
437
|
+
print(f"\nStatement: {statement.name}")
|
438
|
+
self._print_statement_structure(statement, indent=2)
|
439
|
+
|
440
|
+
def _print_statement_structure(self, statement: Statement, indent: int = 0) -> None:
|
441
|
+
"""Recursively print statement structure"""
|
442
|
+
prefix = " " * indent
|
443
|
+
|
444
|
+
# Print parameters
|
445
|
+
if statement.parameters:
|
446
|
+
print(f"{prefix}Parameters:")
|
447
|
+
for key, value in statement.parameters.items():
|
448
|
+
if isinstance(value, str) and '\n' in value:
|
449
|
+
print(f"{prefix} {key}: <multiline-content>")
|
450
|
+
elif isinstance(value, list):
|
451
|
+
print(f"{prefix} {key}: <list-content>")
|
452
|
+
else:
|
453
|
+
print(f"{prefix} {key}: {value}")
|
454
|
+
|
455
|
+
# Print blocks
|
456
|
+
if statement.blocks:
|
457
|
+
print(f"{prefix}Blocks:")
|
458
|
+
for block_name, block_statements in statement.blocks:
|
459
|
+
print(f"{prefix} {block_name}:")
|
460
|
+
for nested_stmt in block_statements:
|
461
|
+
self._print_statement_structure(nested_stmt, indent + 4)
|
462
|
+
|
463
|
+
# Remove the incorrectly placed CommandParser code section
|
464
|
+
if __name__ == "__main__":
|
465
|
+
import sys
|
466
|
+
import argparse
|
467
|
+
|
468
|
+
parser = argparse.ArgumentParser(description='Parse a clear statement file')
|
469
|
+
parser.add_argument('file_path', help='Path to the input file')
|
470
|
+
parser.add_argument('--debug', '-d', action='store_true', help='Enable debug output')
|
471
|
+
|
472
|
+
args = parser.parse_args()
|
473
|
+
|
474
|
+
try:
|
475
|
+
with open(args.file_path, 'r') as f:
|
476
|
+
content = f.read()
|
477
|
+
|
478
|
+
statement_parser = StatementParser()
|
479
|
+
statements = statement_parser.parse(content, debug=args.debug)
|
480
|
+
|
481
|
+
if statement_parser.errors:
|
482
|
+
print("\nParsing errors:")
|
483
|
+
for error in statement_parser.errors:
|
484
|
+
print(error)
|
485
|
+
sys.exit(1)
|
486
|
+
else:
|
487
|
+
print(f"\nSuccessfully parsed statements from {args.file_path}:")
|
488
|
+
print("=" * 50)
|
489
|
+
statement_parser.print_statements(statements)
|
490
|
+
|
491
|
+
except FileNotFoundError:
|
492
|
+
print(f"Error: File '{args.file_path}' not found")
|
493
|
+
sys.exit(1)
|
494
|
+
except Exception as e:
|
495
|
+
print(f"Error: {str(e)}")
|
496
|
+
sys.exit(1)
|
janito/cli/base.py
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
from typing import Optional, List
|
2
|
+
from pathlib import Path
|
3
|
+
from rich.console import Console
|
4
|
+
from rich.prompt import Prompt
|
5
|
+
from datetime import datetime, timezone
|
6
|
+
|
7
|
+
class BaseCLIHandler:
|
8
|
+
def __init__(self):
|
9
|
+
self.console = Console()
|
10
|
+
|
11
|
+
def prompt_user(self, message: str, choices: List[str] = None) -> str:
|
12
|
+
"""Display a simple user prompt with optional choices"""
|
13
|
+
if choices:
|
14
|
+
self.console.print(f"\n[cyan]Options: {', '.join(choices)}[/cyan]")
|
15
|
+
return Prompt.ask(f"[bold cyan]> {message}[/bold cyan]")
|
16
|
+
|
17
|
+
def get_timestamp(self) -> str:
|
18
|
+
"""Get current UTC timestamp in YMD_HMS format"""
|
19
|
+
return datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
|
20
|
+
|
21
|
+
def save_to_history(self, content: str, prefix: str) -> Path:
|
22
|
+
"""Save content to history with timestamp"""
|
23
|
+
from janito.config import config
|
24
|
+
history_dir = config.workspace_dir / '.janito' / 'history'
|
25
|
+
history_dir.mkdir(parents=True, exist_ok=True)
|
26
|
+
timestamp = self.get_timestamp()
|
27
|
+
filename = f"{timestamp}_{prefix}.txt"
|
28
|
+
file_path = history_dir / filename
|
29
|
+
file_path.write_text(content)
|
30
|
+
return file_path
|