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.
Files changed (110) hide show
  1. janito/__init__.py +0 -47
  2. janito/__main__.py +105 -17
  3. janito/agents/__init__.py +9 -9
  4. janito/agents/agent.py +10 -3
  5. janito/agents/claudeai.py +15 -34
  6. janito/agents/openai.py +5 -1
  7. janito/change/__init__.py +29 -16
  8. janito/change/__main__.py +0 -0
  9. janito/{analysis → change/analysis}/__init__.py +5 -15
  10. janito/change/analysis/__main__.py +7 -0
  11. janito/change/analysis/analyze.py +62 -0
  12. janito/change/analysis/formatting.py +78 -0
  13. janito/change/analysis/options.py +81 -0
  14. janito/{analysis → change/analysis}/prompts.py +33 -18
  15. janito/change/analysis/view/__init__.py +9 -0
  16. janito/change/analysis/view/terminal.py +181 -0
  17. janito/change/applier/__init__.py +5 -0
  18. janito/change/applier/file.py +58 -0
  19. janito/change/applier/main.py +156 -0
  20. janito/change/applier/text.py +247 -0
  21. janito/change/applier/workspace_dir.py +58 -0
  22. janito/change/core.py +124 -0
  23. janito/{changehistory.py → change/history.py} +12 -14
  24. janito/change/operations.py +7 -0
  25. janito/change/parser.py +287 -0
  26. janito/change/play.py +54 -0
  27. janito/change/preview.py +82 -0
  28. janito/change/prompts.py +121 -0
  29. janito/change/test.py +0 -0
  30. janito/change/validator.py +269 -0
  31. janito/{changeviewer → change/viewer}/__init__.py +3 -4
  32. janito/change/viewer/content.py +66 -0
  33. janito/{changeviewer → change/viewer}/diff.py +19 -4
  34. janito/change/viewer/panels.py +533 -0
  35. janito/change/viewer/styling.py +114 -0
  36. janito/{changeviewer → change/viewer}/themes.py +3 -5
  37. janito/clear_statement_parser/clear_statement_format.txt +328 -0
  38. janito/clear_statement_parser/examples.txt +326 -0
  39. janito/clear_statement_parser/models.py +104 -0
  40. janito/clear_statement_parser/parser.py +496 -0
  41. janito/cli/base.py +30 -0
  42. janito/cli/commands.py +75 -40
  43. janito/cli/functions.py +19 -194
  44. janito/cli/history.py +61 -0
  45. janito/common.py +65 -8
  46. janito/config.py +70 -5
  47. janito/demo/__init__.py +4 -0
  48. janito/demo/data.py +13 -0
  49. janito/demo/mock_data.py +20 -0
  50. janito/demo/operations.py +45 -0
  51. janito/demo/runner.py +59 -0
  52. janito/demo/scenarios.py +32 -0
  53. janito/prompt.py +36 -0
  54. janito/qa.py +6 -14
  55. janito/search_replace/README.md +192 -0
  56. janito/search_replace/__init__.py +7 -0
  57. janito/search_replace/__main__.py +21 -0
  58. janito/search_replace/core.py +120 -0
  59. janito/search_replace/logger.py +35 -0
  60. janito/search_replace/parser.py +52 -0
  61. janito/search_replace/play.py +61 -0
  62. janito/search_replace/replacer.py +36 -0
  63. janito/search_replace/searcher.py +411 -0
  64. janito/search_replace/strategy_result.py +10 -0
  65. janito/shell/__init__.py +38 -0
  66. janito/shell/bus.py +31 -0
  67. janito/shell/commands.py +136 -0
  68. janito/shell/history.py +20 -0
  69. janito/shell/processor.py +32 -0
  70. janito/shell/prompt.py +48 -0
  71. janito/shell/registry.py +60 -0
  72. janito/tui/__init__.py +21 -0
  73. janito/tui/base.py +22 -0
  74. janito/tui/flows/__init__.py +5 -0
  75. janito/tui/flows/changes.py +65 -0
  76. janito/tui/flows/content.py +128 -0
  77. janito/tui/flows/selection.py +117 -0
  78. janito/tui/screens/__init__.py +3 -0
  79. janito/tui/screens/app.py +1 -0
  80. janito/workspace/__init__.py +6 -0
  81. janito/workspace/analysis.py +121 -0
  82. janito/workspace/show.py +141 -0
  83. janito/workspace/stats.py +43 -0
  84. janito/workspace/types.py +98 -0
  85. janito/workspace/workset.py +108 -0
  86. janito/workspace/workspace.py +114 -0
  87. janito-0.7.0.dist-info/METADATA +167 -0
  88. janito-0.7.0.dist-info/RECORD +96 -0
  89. {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/WHEEL +1 -1
  90. janito/_contextparser.py +0 -113
  91. janito/analysis/display.py +0 -149
  92. janito/analysis/options.py +0 -112
  93. janito/change/applier.py +0 -269
  94. janito/change/content.py +0 -62
  95. janito/change/indentation.py +0 -33
  96. janito/change/position.py +0 -169
  97. janito/changeviewer/panels.py +0 -268
  98. janito/changeviewer/styling.py +0 -59
  99. janito/console/__init__.py +0 -3
  100. janito/console/commands.py +0 -112
  101. janito/console/core.py +0 -62
  102. janito/console/display.py +0 -157
  103. janito/fileparser.py +0 -334
  104. janito/prompts.py +0 -81
  105. janito/scan.py +0 -176
  106. janito/tests/test_fileparser.py +0 -26
  107. janito-0.5.0.dist-info/METADATA +0 -146
  108. janito-0.5.0.dist-info/RECORD +0 -45
  109. {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/entry_points.txt +0 -0
  110. {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