janito 1.9.0__py3-none-any.whl → 1.11.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 (106) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/api_exceptions.py +4 -0
  3. janito/agent/config.py +1 -1
  4. janito/agent/config_defaults.py +2 -26
  5. janito/agent/conversation.py +163 -122
  6. janito/agent/conversation_api.py +246 -168
  7. janito/agent/conversation_ui.py +1 -1
  8. janito/agent/{conversation_history.py → llm_conversation_history.py} +30 -1
  9. janito/agent/openai_client.py +38 -23
  10. janito/agent/openai_schema_generator.py +162 -129
  11. janito/agent/platform_discovery.py +134 -77
  12. janito/agent/profile_manager.py +5 -5
  13. janito/agent/rich_message_handler.py +80 -31
  14. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +20 -4
  15. janito/agent/test_openai_schema_generator.py +93 -0
  16. janito/agent/tool_base.py +7 -2
  17. janito/agent/tool_executor.py +54 -49
  18. janito/agent/tool_registry.py +5 -2
  19. janito/agent/tool_use_tracker.py +26 -5
  20. janito/agent/tools/__init__.py +8 -3
  21. janito/agent/tools/create_directory.py +3 -1
  22. janito/agent/tools/create_file.py +7 -1
  23. janito/agent/tools/fetch_url.py +40 -3
  24. janito/agent/tools/find_files.py +29 -14
  25. janito/agent/tools/get_file_outline/core.py +7 -8
  26. janito/agent/tools/get_file_outline/python_outline.py +139 -95
  27. janito/agent/tools/get_file_outline/search_outline.py +3 -1
  28. janito/agent/tools/get_lines.py +98 -64
  29. janito/agent/tools/move_file.py +59 -31
  30. janito/agent/tools/open_url.py +31 -0
  31. janito/agent/tools/present_choices.py +3 -1
  32. janito/agent/tools/python_command_runner.py +149 -0
  33. janito/agent/tools/python_file_runner.py +147 -0
  34. janito/agent/tools/python_stdin_runner.py +153 -0
  35. janito/agent/tools/remove_directory.py +3 -1
  36. janito/agent/tools/remove_file.py +5 -1
  37. janito/agent/tools/replace_file.py +12 -2
  38. janito/agent/tools/replace_text_in_file.py +195 -149
  39. janito/agent/tools/run_bash_command.py +30 -69
  40. janito/agent/tools/run_powershell_command.py +138 -105
  41. janito/agent/tools/search_text/__init__.py +1 -0
  42. janito/agent/tools/search_text/core.py +176 -0
  43. janito/agent/tools/search_text/match_lines.py +58 -0
  44. janito/agent/tools/search_text/pattern_utils.py +65 -0
  45. janito/agent/tools/search_text/traverse_directory.py +127 -0
  46. janito/agent/tools/validate_file_syntax/core.py +43 -30
  47. janito/agent/tools/validate_file_syntax/html_validator.py +21 -5
  48. janito/agent/tools/validate_file_syntax/markdown_validator.py +77 -34
  49. janito/agent/tools_utils/action_type.py +7 -0
  50. janito/agent/tools_utils/dir_walk_utils.py +3 -2
  51. janito/agent/tools_utils/formatting.py +47 -21
  52. janito/agent/tools_utils/gitignore_utils.py +89 -40
  53. janito/agent/tools_utils/test_gitignore_utils.py +46 -0
  54. janito/agent/tools_utils/utils.py +7 -1
  55. janito/cli/_print_config.py +63 -61
  56. janito/cli/arg_parser.py +13 -12
  57. janito/cli/cli_main.py +137 -147
  58. janito/cli/config_commands.py +112 -109
  59. janito/cli/main.py +152 -174
  60. janito/cli/one_shot.py +40 -26
  61. janito/i18n/__init__.py +1 -1
  62. janito/rich_utils.py +46 -8
  63. janito/shell/commands/__init__.py +2 -4
  64. janito/shell/commands/conversation_restart.py +3 -1
  65. janito/shell/commands/edit.py +3 -0
  66. janito/shell/commands/history_view.py +3 -3
  67. janito/shell/commands/lang.py +3 -0
  68. janito/shell/commands/livelogs.py +5 -3
  69. janito/shell/commands/prompt.py +6 -0
  70. janito/shell/commands/session.py +3 -0
  71. janito/shell/commands/session_control.py +3 -0
  72. janito/shell/commands/termweb_log.py +8 -0
  73. janito/shell/commands/tools.py +3 -0
  74. janito/shell/commands/track.py +36 -0
  75. janito/shell/commands/utility.py +13 -18
  76. janito/shell/commands/verbose.py +3 -4
  77. janito/shell/input_history.py +62 -0
  78. janito/shell/main.py +160 -181
  79. janito/shell/session/config.py +83 -75
  80. janito/shell/session/manager.py +0 -21
  81. janito/shell/ui/interactive.py +97 -75
  82. janito/termweb/static/editor.css +32 -33
  83. janito/termweb/static/editor.css.bak +140 -22
  84. janito/termweb/static/editor.html +12 -7
  85. janito/termweb/static/editor.html.bak +16 -11
  86. janito/termweb/static/editor.js +94 -40
  87. janito/termweb/static/editor.js.bak +97 -65
  88. janito/termweb/static/index.html +1 -2
  89. janito/termweb/static/index.html.bak +1 -1
  90. janito/termweb/static/termweb.css +1 -22
  91. janito/termweb/static/termweb.css.bak +6 -4
  92. janito/termweb/static/termweb.js +0 -6
  93. janito/termweb/static/termweb.js.bak +1 -2
  94. janito/tests/test_rich_utils.py +44 -0
  95. janito/web/app.py +0 -75
  96. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/METADATA +61 -42
  97. janito-1.11.0.dist-info/RECORD +163 -0
  98. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/WHEEL +1 -1
  99. janito/agent/providers.py +0 -77
  100. janito/agent/tools/run_python_command.py +0 -161
  101. janito/agent/tools/search_text.py +0 -204
  102. janito/shell/commands/sum.py +0 -49
  103. janito-1.9.0.dist-info/RECORD +0 -151
  104. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/entry_points.txt +0 -0
  105. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/licenses/LICENSE +0 -0
  106. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,127 @@
1
+ import os
2
+ from janito.agent.tools_utils.gitignore_utils import GitignoreFilter
3
+ from .match_lines import match_line, should_limit, read_file_lines
4
+
5
+
6
+ def walk_directory(search_path, max_depth):
7
+ if max_depth == 1:
8
+ walk_result = next(os.walk(search_path), None)
9
+ if walk_result is None:
10
+ return [(search_path, [], [])]
11
+ else:
12
+ return [walk_result]
13
+ else:
14
+ return os.walk(search_path)
15
+
16
+
17
+ def filter_dirs(dirs, root, gitignore_filter):
18
+ return [d for d in dirs if not gitignore_filter.is_ignored(os.path.join(root, d))]
19
+
20
+
21
+ def process_file_count_only(
22
+ file_path, per_file_counts, pattern, regex, use_regex, max_results, total_results
23
+ ):
24
+ match_count, file_limit_reached, _ = read_file_lines(
25
+ file_path,
26
+ pattern,
27
+ regex,
28
+ use_regex,
29
+ True,
30
+ max_results,
31
+ total_results + sum(count for _, count in per_file_counts),
32
+ )
33
+ if match_count > 0:
34
+ per_file_counts.append((file_path, match_count))
35
+ return file_limit_reached
36
+
37
+
38
+ def process_file_collect(
39
+ file_path,
40
+ dir_output,
41
+ per_file_counts,
42
+ pattern,
43
+ regex,
44
+ use_regex,
45
+ max_results,
46
+ total_results,
47
+ ):
48
+ actual_match_count, file_limit_reached, file_lines_output = read_file_lines(
49
+ file_path,
50
+ pattern,
51
+ regex,
52
+ use_regex,
53
+ False,
54
+ max_results,
55
+ total_results + len(dir_output),
56
+ )
57
+ dir_output.extend(file_lines_output)
58
+ if actual_match_count > 0:
59
+ per_file_counts.append((file_path, actual_match_count))
60
+ return file_limit_reached
61
+
62
+
63
+ def should_limit_depth(root, search_path, max_depth, dirs):
64
+ if max_depth > 0:
65
+ rel_root = os.path.relpath(root, search_path)
66
+ if rel_root != ".":
67
+ depth = rel_root.count(os.sep) + 1
68
+ if depth >= max_depth:
69
+ del dirs[:]
70
+
71
+
72
+ def traverse_directory(
73
+ search_path,
74
+ pattern,
75
+ regex,
76
+ use_regex,
77
+ max_depth,
78
+ max_results,
79
+ total_results,
80
+ count_only,
81
+ ):
82
+ dir_output = []
83
+ dir_limit_reached = False
84
+ per_file_counts = []
85
+ walker = walk_directory(search_path, max_depth)
86
+ gitignore_filter = GitignoreFilter(search_path)
87
+
88
+ for root, dirs, files in walker:
89
+ dirs[:] = filter_dirs(dirs, root, gitignore_filter)
90
+ for file in files:
91
+ file_path = os.path.join(root, file)
92
+ if gitignore_filter.is_ignored(file_path):
93
+ continue
94
+ if count_only:
95
+ file_limit_reached = process_file_count_only(
96
+ file_path,
97
+ per_file_counts,
98
+ pattern,
99
+ regex,
100
+ use_regex,
101
+ max_results,
102
+ total_results,
103
+ )
104
+ if file_limit_reached:
105
+ dir_limit_reached = True
106
+ break
107
+ else:
108
+ file_limit_reached = process_file_collect(
109
+ file_path,
110
+ dir_output,
111
+ per_file_counts,
112
+ pattern,
113
+ regex,
114
+ use_regex,
115
+ max_results,
116
+ total_results,
117
+ )
118
+ if file_limit_reached:
119
+ dir_limit_reached = True
120
+ break
121
+ if dir_limit_reached:
122
+ break
123
+ should_limit_depth(root, search_path, max_depth, dirs)
124
+ if count_only:
125
+ return per_file_counts, dir_limit_reached, []
126
+ else:
127
+ return dir_output, dir_limit_reached, per_file_counts
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  from janito.i18n import tr
3
3
  from janito.agent.tool_base import ToolBase
4
+ from janito.agent.tools_utils.action_type import ActionType
4
5
  from janito.agent.tool_registry import register_tool
5
6
  from janito.agent.tools_utils.utils import display_path
6
7
 
@@ -15,39 +16,47 @@ from .js_validator import validate_js
15
16
  from .css_validator import validate_css
16
17
 
17
18
 
19
+ def _get_validator(ext):
20
+ """Return the appropriate validator function for the file extension."""
21
+ mapping = {
22
+ ".py": validate_python,
23
+ ".pyw": validate_python,
24
+ ".json": validate_json,
25
+ ".yml": validate_yaml,
26
+ ".yaml": validate_yaml,
27
+ ".ps1": validate_ps1,
28
+ ".xml": validate_xml,
29
+ ".html": validate_html,
30
+ ".htm": validate_html,
31
+ ".md": validate_markdown,
32
+ ".js": validate_js,
33
+ ".css": validate_css,
34
+ }
35
+ return mapping.get(ext)
36
+
37
+
38
+ def _handle_validation_error(e, report_warning):
39
+ msg = tr("\u26a0\ufe0f Warning: Syntax error: {error}", error=e)
40
+ if report_warning:
41
+ report_warning(msg)
42
+ return msg
43
+
44
+
18
45
  def validate_file_syntax(
19
46
  file_path: str, report_info=None, report_warning=None, report_success=None
20
47
  ) -> str:
21
48
  ext = os.path.splitext(file_path)[1].lower()
49
+ validator = _get_validator(ext)
22
50
  try:
23
- if ext in [".py", ".pyw"]:
24
- return validate_python(file_path)
25
- elif ext == ".json":
26
- return validate_json(file_path)
27
- elif ext in [".yml", ".yaml"]:
28
- return validate_yaml(file_path)
29
- elif ext == ".ps1":
30
- return validate_ps1(file_path)
31
- elif ext == ".xml":
32
- return validate_xml(file_path)
33
- elif ext in (".html", ".htm"):
34
- return validate_html(file_path)
35
- elif ext == ".md":
36
- return validate_markdown(file_path)
37
- elif ext == ".js":
38
- return validate_js(file_path)
39
- elif ext == ".css":
40
- return validate_css(file_path)
51
+ if validator:
52
+ return validator(file_path)
41
53
  else:
42
- msg = tr("⚠️ Warning: Unsupported file extension: {ext}", ext=ext)
54
+ msg = tr("\u26a0\ufe0f Warning: Unsupported file extension: {ext}", ext=ext)
43
55
  if report_warning:
44
56
  report_warning(msg)
45
57
  return msg
46
58
  except Exception as e:
47
- msg = tr("⚠️ Warning: Syntax error: {error}", error=e)
48
- if report_warning:
49
- report_warning(msg)
50
- return msg
59
+ return _handle_validation_error(e, report_warning)
51
60
 
52
61
 
53
62
  @register_tool(name="validate_file_syntax")
@@ -69,15 +78,19 @@ class ValidateFileSyntaxTool(ToolBase):
69
78
  file_path (str): Path to the file to validate.
70
79
  Returns:
71
80
  str: Validation status message. Example:
72
- - " Syntax OK"
73
- - "⚠️ Warning: Syntax error: <error message>"
74
- - "⚠️ Warning: Unsupported file extension: <ext>"
81
+ - "\u2705 Syntax OK"
82
+ - "\u26a0\ufe0f Warning: Syntax error: <error message>"
83
+ - "\u26a0\ufe0f Warning: Unsupported file extension: <ext>"
75
84
  """
76
85
 
77
86
  def run(self, file_path: str) -> str:
78
87
  disp_path = display_path(file_path)
79
88
  self.report_info(
80
- tr("🔎 Validating syntax for file '{disp_path}' ...", disp_path=disp_path)
89
+ ActionType.READ,
90
+ tr(
91
+ "\U0001f50e Validate syntax for file '{disp_path}' ...",
92
+ disp_path=disp_path,
93
+ ),
81
94
  )
82
95
  result = validate_file_syntax(
83
96
  file_path,
@@ -85,8 +98,8 @@ class ValidateFileSyntaxTool(ToolBase):
85
98
  report_warning=self.report_warning,
86
99
  report_success=self.report_success,
87
100
  )
88
- if result.startswith(""):
101
+ if result.startswith("\u2705"):
89
102
  self.report_success(result)
90
- elif result.startswith("⚠️"):
91
- self.report_warning(tr("⚠️ ") + result.lstrip("⚠️ "))
103
+ elif result.startswith("\u26a0\ufe0f"):
104
+ self.report_warning(tr("\u26a0\ufe0f ") + result.lstrip("\u26a0\ufe0f "))
92
105
  return result
@@ -4,9 +4,19 @@ from lxml import etree
4
4
 
5
5
 
6
6
  def validate_html(file_path: str) -> str:
7
- warnings = []
7
+ html_content = _read_html_content(file_path)
8
+ warnings = _find_js_outside_script(html_content)
9
+ lxml_error = _parse_html_and_collect_errors(file_path)
10
+ msg = _build_result_message(warnings, lxml_error)
11
+ return msg
12
+
13
+
14
+ def _read_html_content(file_path):
8
15
  with open(file_path, "r", encoding="utf-8") as f:
9
- html_content = f.read()
16
+ return f.read()
17
+
18
+
19
+ def _find_js_outside_script(html_content):
10
20
  script_blocks = [
11
21
  m.span()
12
22
  for m in re.finditer(
@@ -21,6 +31,7 @@ def validate_html(file_path: str) -> str:
21
31
  r"^\s*window\.\w+\s*=",
22
32
  r"^\s*\$\s*\(",
23
33
  ]
34
+ warnings = []
24
35
  for pat in js_patterns:
25
36
  for m in re.finditer(pat, html_content):
26
37
  in_script = False
@@ -32,14 +43,16 @@ def validate_html(file_path: str) -> str:
32
43
  warnings.append(
33
44
  f"Line {html_content.count(chr(10), 0, m.start())+1}: JavaScript code ('{pat}') found outside <script> tag."
34
45
  )
46
+ return warnings
47
+
48
+
49
+ def _parse_html_and_collect_errors(file_path):
35
50
  lxml_error = None
36
51
  try:
37
- # Parse HTML and collect error log
38
52
  parser = etree.HTMLParser(recover=False)
39
53
  with open(file_path, "rb") as f:
40
54
  etree.parse(f, parser=parser)
41
55
  error_log = parser.error_log
42
- # Look for tag mismatch or unclosed tag errors
43
56
  syntax_errors = []
44
57
  for e in error_log:
45
58
  if (
@@ -52,7 +65,6 @@ def validate_html(file_path: str) -> str:
52
65
  if syntax_errors:
53
66
  lxml_error = tr("Syntax error: {error}", error="; ".join(syntax_errors))
54
67
  elif error_log:
55
- # Other warnings
56
68
  lxml_error = tr(
57
69
  "HTML syntax errors found:\n{errors}",
58
70
  errors="\n".join(str(e) for e in error_log),
@@ -61,6 +73,10 @@ def validate_html(file_path: str) -> str:
61
73
  lxml_error = tr("⚠️ lxml not installed. Cannot validate HTML.")
62
74
  except Exception as e:
63
75
  lxml_error = tr("Syntax error: {error}", error=str(e))
76
+ return lxml_error
77
+
78
+
79
+ def _build_result_message(warnings, lxml_error):
64
80
  msg = ""
65
81
  if warnings:
66
82
  msg += (
@@ -5,62 +5,105 @@ import re
5
5
  def validate_markdown(file_path: str) -> str:
6
6
  with open(file_path, "r", encoding="utf-8") as f:
7
7
  content = f.read()
8
- errors = []
9
8
  lines = content.splitlines()
10
- # Header space check
9
+ errors = []
10
+ errors.extend(_check_header_space(lines))
11
+ errors.extend(_check_unclosed_code_block(content))
12
+ errors.extend(_check_unclosed_links_images(lines))
13
+ errors.extend(_check_list_formatting(lines))
14
+ errors.extend(_check_unclosed_inline_code(content))
15
+ return _build_markdown_result(errors)
16
+
17
+
18
+ def _check_header_space(lines):
19
+ errors = []
11
20
  for i, line in enumerate(lines, 1):
12
21
  if re.match(r"^#+[^ #]", line):
13
22
  errors.append(f"Line {i}: Header missing space after # | {line.strip()}")
14
- # Unclosed code block
23
+ return errors
24
+
25
+
26
+ def _check_unclosed_code_block(content):
27
+ errors = []
15
28
  if content.count("```") % 2 != 0:
16
29
  errors.append("Unclosed code block (```) detected")
17
- # Unclosed link or image
30
+ return errors
31
+
32
+
33
+ def _check_unclosed_links_images(lines):
34
+ errors = []
18
35
  for i, line in enumerate(lines, 1):
19
36
  if re.search(r"\[[^\]]*\]\([^)]+$", line):
20
37
  errors.append(
21
38
  f"Line {i}: Unclosed link or image (missing closing parenthesis) | {line.strip()}"
22
39
  )
23
- # List item formatting and blank line before new list (bulleted and numbered)
40
+ return errors
41
+
42
+
43
+ def _is_table_line(line):
44
+ return line.lstrip().startswith("|")
45
+
46
+
47
+ def _list_item_missing_space(line):
48
+ return re.match(r"^[-*+][^ \n]", line)
49
+
50
+
51
+ def _should_skip_list_item(line):
52
+ stripped = line.strip()
53
+ return stripped.startswith("*") and stripped.endswith("*") and len(stripped) > 2
54
+
55
+
56
+ def _needs_blank_line_before_bullet(lines, i):
57
+ if i <= 1:
58
+ return False
59
+ prev_line = lines[i - 2]
60
+ prev_is_list = bool(re.match(r"^\s*[-*+] ", prev_line))
61
+ return not prev_is_list and prev_line.strip() != ""
62
+
63
+
64
+ def _needs_blank_line_before_numbered(lines, i):
65
+ if i <= 1:
66
+ return False
67
+ prev_line = lines[i - 2]
68
+ prev_is_numbered_list = bool(re.match(r"^\s*\d+\. ", prev_line))
69
+ return not prev_is_numbered_list and prev_line.strip() != ""
70
+
71
+
72
+ def _check_list_formatting(lines):
73
+ errors = []
24
74
  for i, line in enumerate(lines, 1):
25
- # Skip table lines
26
- if line.lstrip().startswith("|"):
75
+ if _is_table_line(line):
27
76
  continue
28
- # List item missing space after bullet
29
- if re.match(r"^[-*+][^ \n]", line):
30
- stripped = line.strip()
31
- if not (
32
- stripped.startswith("*")
33
- and stripped.endswith("*")
34
- and len(stripped) > 2
35
- ):
77
+ if _list_item_missing_space(line):
78
+ if not _should_skip_list_item(line):
36
79
  errors.append(
37
80
  f"Line {i}: List item missing space after bullet | {line.strip()}"
38
81
  )
39
- # Blank line before first item of a new bulleted list
40
82
  if re.match(r"^\s*[-*+] ", line):
41
- if i > 1:
42
- prev_line = lines[i - 2]
43
- prev_is_list = bool(re.match(r"^\s*[-*+] ", prev_line))
44
- if not prev_is_list and prev_line.strip() != "":
45
- errors.append(
46
- f"Line {i}: List should be preceded by a blank line for compatibility with MkDocs and other Markdown parsers | {line.strip()}"
47
- )
48
- # Blank line before first item of a new numbered list
83
+ if _needs_blank_line_before_bullet(lines, i):
84
+ errors.append(
85
+ f"Line {i}: List should be preceded by a blank line for compatibility with MkDocs and other Markdown parsers | {line.strip()}"
86
+ )
49
87
  if re.match(r"^\s*\d+\. ", line):
50
- if i > 1:
51
- prev_line = lines[i - 2]
52
- prev_is_numbered_list = bool(re.match(r"^\s*\d+\. ", prev_line))
53
- if not prev_is_numbered_list and prev_line.strip() != "":
54
- errors.append(
55
- f"Line {i}: Numbered list should be preceded by a blank line for compatibility with MkDocs and other Markdown parsers | {line.strip()}"
56
- )
57
- # Unclosed inline code
88
+ if _needs_blank_line_before_numbered(lines, i):
89
+ errors.append(
90
+ f"Line {i}: Numbered list should be preceded by a blank line for compatibility with MkDocs and other Markdown parsers | {line.strip()}"
91
+ )
92
+ return errors
93
+
94
+
95
+ def _check_unclosed_inline_code(content):
96
+ errors = []
58
97
  if content.count("`") % 2 != 0:
59
98
  errors.append("Unclosed inline code (`) detected")
99
+ return errors
100
+
101
+
102
+ def _build_markdown_result(errors):
60
103
  if errors:
61
104
  msg = tr(
62
- "⚠️ Warning: Markdown syntax issues found:\n{errors}",
105
+ "\u26a0\ufe0f Warning: Markdown syntax issues found:\n{errors}",
63
106
  errors="\n".join(errors),
64
107
  )
65
108
  return msg
66
- return " Syntax valid"
109
+ return "\u2705 Syntax valid"
@@ -0,0 +1,7 @@
1
+ from enum import Enum, auto
2
+
3
+
4
+ class ActionType(Enum):
5
+ READ = auto()
6
+ WRITE = auto()
7
+ EXECUTE = auto()
@@ -1,5 +1,5 @@
1
1
  import os
2
- from .gitignore_utils import filter_ignored
2
+ from .gitignore_utils import GitignoreFilter
3
3
 
4
4
 
5
5
  def walk_dir_with_gitignore(root_dir, max_depth=None):
@@ -11,6 +11,7 @@ def walk_dir_with_gitignore(root_dir, max_depth=None):
11
11
  - If max_depth=1, only the root directory (matches 'find . -maxdepth 1').
12
12
  - If max_depth=N (N>1), yields files in root and up to N-1 levels below root (matches 'find . -maxdepth N').
13
13
  """
14
+ gitignore = GitignoreFilter()
14
15
  for root, dirs, files in os.walk(root_dir):
15
16
  rel_path = os.path.relpath(root, root_dir)
16
17
  depth = 0 if rel_path == "." else rel_path.count(os.sep) + 1
@@ -19,5 +20,5 @@ def walk_dir_with_gitignore(root_dir, max_depth=None):
19
20
  # For max_depth=1, only root (depth=0). For max_depth=2, root and one level below (depth=0,1).
20
21
  if depth > 0:
21
22
  continue
22
- dirs, files = filter_ignored(root, dirs, files)
23
+ dirs, files = gitignore.filter_ignored(root, dirs, files)
23
24
  yield root, dirs, files
@@ -1,23 +1,49 @@
1
- def format_outline_table(outline_items):
2
- if not outline_items:
3
- return "No classes, functions, or variables found."
4
- header = "| Type | Name | Start | End | Parent | Docstring |\n|---------|-------------|-------|-----|----------|--------------------------|"
5
- rows = []
6
- for item in outline_items:
7
- docstring = item.get("docstring", "").replace("\n", " ")
8
- if len(docstring) > 24:
9
- docstring = docstring[:21] + "..."
10
- rows.append(
11
- f"| {item['type']:<7} | {item['name']:<11} | {item['start']:<5} | {item['end']:<3} | {item['parent']:<8} | {docstring:<24} |"
12
- )
13
- return header + "\n" + "\n".join(rows)
1
+ class OutlineFormatter:
2
+ """
3
+ Utility class for formatting code and markdown outlines into human-readable tables.
4
+ """
14
5
 
6
+ @staticmethod
7
+ def format_outline_table(outline_items):
8
+ """
9
+ Format a list of code outline items (classes, functions, variables) into a table.
15
10
 
16
- def format_markdown_outline_table(outline_items):
17
- if not outline_items:
18
- return "No headers found."
19
- header = "| Level | Header | Line |\n|-------|----------------------------------|------|"
20
- rows = []
21
- for item in outline_items:
22
- rows.append(f"| {item['level']:<5} | {item['title']:<32} | {item['line']:<4} |")
23
- return header + "\n" + "\n".join(rows)
11
+ Args:
12
+ outline_items (list of dict): Each dict should contain keys: 'type', 'name', 'start', 'end', 'parent', 'docstring'.
13
+
14
+ Returns:
15
+ str: Formatted table as a string.
16
+ """
17
+ if not outline_items:
18
+ return "No classes, functions, or variables found."
19
+ header = "| Type | Name | Start | End | Parent | Docstring |\n|---------|-------------|-------|-----|----------|--------------------------|"
20
+ rows = []
21
+ for item in outline_items:
22
+ docstring = item.get("docstring", "").replace("\n", " ")
23
+ if len(docstring) > 24:
24
+ docstring = docstring[:21] + "..."
25
+ rows.append(
26
+ f"| {item['type']:<7} | {item['name']:<11} | {item['start']:<5} | {item['end']:<3} | {item['parent']:<8} | {docstring:<24} |"
27
+ )
28
+ return header + "\n" + "\n".join(rows)
29
+
30
+ @staticmethod
31
+ def format_markdown_outline_table(outline_items):
32
+ """
33
+ Format a list of markdown outline items (headers) into a table.
34
+
35
+ Args:
36
+ outline_items (list of dict): Each dict should contain keys: 'level', 'title', 'line'.
37
+
38
+ Returns:
39
+ str: Formatted table as a string.
40
+ """
41
+ if not outline_items:
42
+ return "No headers found."
43
+ header = "| Level | Header | Line |\n|-------|----------------------------------|------|"
44
+ rows = []
45
+ for item in outline_items:
46
+ rows.append(
47
+ f"| {item['level']:<5} | {item['title']:<32} | {item['line']:<4} |"
48
+ )
49
+ return header + "\n" + "\n".join(rows)