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.
- janito/__init__.py +1 -1
- janito/agent/api_exceptions.py +4 -0
- janito/agent/config.py +1 -1
- janito/agent/config_defaults.py +2 -26
- janito/agent/conversation.py +163 -122
- janito/agent/conversation_api.py +246 -168
- janito/agent/conversation_ui.py +1 -1
- janito/agent/{conversation_history.py → llm_conversation_history.py} +30 -1
- janito/agent/openai_client.py +38 -23
- janito/agent/openai_schema_generator.py +162 -129
- janito/agent/platform_discovery.py +134 -77
- janito/agent/profile_manager.py +5 -5
- janito/agent/rich_message_handler.py +80 -31
- janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +20 -4
- janito/agent/test_openai_schema_generator.py +93 -0
- janito/agent/tool_base.py +7 -2
- janito/agent/tool_executor.py +54 -49
- janito/agent/tool_registry.py +5 -2
- janito/agent/tool_use_tracker.py +26 -5
- janito/agent/tools/__init__.py +8 -3
- janito/agent/tools/create_directory.py +3 -1
- janito/agent/tools/create_file.py +7 -1
- janito/agent/tools/fetch_url.py +40 -3
- janito/agent/tools/find_files.py +29 -14
- janito/agent/tools/get_file_outline/core.py +7 -8
- janito/agent/tools/get_file_outline/python_outline.py +139 -95
- janito/agent/tools/get_file_outline/search_outline.py +3 -1
- janito/agent/tools/get_lines.py +98 -64
- janito/agent/tools/move_file.py +59 -31
- janito/agent/tools/open_url.py +31 -0
- janito/agent/tools/present_choices.py +3 -1
- janito/agent/tools/python_command_runner.py +149 -0
- janito/agent/tools/python_file_runner.py +147 -0
- janito/agent/tools/python_stdin_runner.py +153 -0
- janito/agent/tools/remove_directory.py +3 -1
- janito/agent/tools/remove_file.py +5 -1
- janito/agent/tools/replace_file.py +12 -2
- janito/agent/tools/replace_text_in_file.py +195 -149
- janito/agent/tools/run_bash_command.py +30 -69
- janito/agent/tools/run_powershell_command.py +138 -105
- janito/agent/tools/search_text/__init__.py +1 -0
- janito/agent/tools/search_text/core.py +176 -0
- janito/agent/tools/search_text/match_lines.py +58 -0
- janito/agent/tools/search_text/pattern_utils.py +65 -0
- janito/agent/tools/search_text/traverse_directory.py +127 -0
- janito/agent/tools/validate_file_syntax/core.py +43 -30
- janito/agent/tools/validate_file_syntax/html_validator.py +21 -5
- janito/agent/tools/validate_file_syntax/markdown_validator.py +77 -34
- janito/agent/tools_utils/action_type.py +7 -0
- janito/agent/tools_utils/dir_walk_utils.py +3 -2
- janito/agent/tools_utils/formatting.py +47 -21
- janito/agent/tools_utils/gitignore_utils.py +89 -40
- janito/agent/tools_utils/test_gitignore_utils.py +46 -0
- janito/agent/tools_utils/utils.py +7 -1
- janito/cli/_print_config.py +63 -61
- janito/cli/arg_parser.py +13 -12
- janito/cli/cli_main.py +137 -147
- janito/cli/config_commands.py +112 -109
- janito/cli/main.py +152 -174
- janito/cli/one_shot.py +40 -26
- janito/i18n/__init__.py +1 -1
- janito/rich_utils.py +46 -8
- janito/shell/commands/__init__.py +2 -4
- janito/shell/commands/conversation_restart.py +3 -1
- janito/shell/commands/edit.py +3 -0
- janito/shell/commands/history_view.py +3 -3
- janito/shell/commands/lang.py +3 -0
- janito/shell/commands/livelogs.py +5 -3
- janito/shell/commands/prompt.py +6 -0
- janito/shell/commands/session.py +3 -0
- janito/shell/commands/session_control.py +3 -0
- janito/shell/commands/termweb_log.py +8 -0
- janito/shell/commands/tools.py +3 -0
- janito/shell/commands/track.py +36 -0
- janito/shell/commands/utility.py +13 -18
- janito/shell/commands/verbose.py +3 -4
- janito/shell/input_history.py +62 -0
- janito/shell/main.py +160 -181
- janito/shell/session/config.py +83 -75
- janito/shell/session/manager.py +0 -21
- janito/shell/ui/interactive.py +97 -75
- janito/termweb/static/editor.css +32 -33
- janito/termweb/static/editor.css.bak +140 -22
- janito/termweb/static/editor.html +12 -7
- janito/termweb/static/editor.html.bak +16 -11
- janito/termweb/static/editor.js +94 -40
- janito/termweb/static/editor.js.bak +97 -65
- janito/termweb/static/index.html +1 -2
- janito/termweb/static/index.html.bak +1 -1
- janito/termweb/static/termweb.css +1 -22
- janito/termweb/static/termweb.css.bak +6 -4
- janito/termweb/static/termweb.js +0 -6
- janito/termweb/static/termweb.js.bak +1 -2
- janito/tests/test_rich_utils.py +44 -0
- janito/web/app.py +0 -75
- {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/METADATA +61 -42
- janito-1.11.0.dist-info/RECORD +163 -0
- {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/WHEEL +1 -1
- janito/agent/providers.py +0 -77
- janito/agent/tools/run_python_command.py +0 -161
- janito/agent/tools/search_text.py +0 -204
- janito/shell/commands/sum.py +0 -49
- janito-1.9.0.dist-info/RECORD +0 -151
- {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/entry_points.txt +0 -0
- {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
24
|
-
return
|
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("
|
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
|
-
|
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
|
-
- "
|
73
|
-
- "
|
74
|
-
- "
|
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
|
-
|
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("
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
26
|
-
if line.lstrip().startswith("|"):
|
75
|
+
if _is_table_line(line):
|
27
76
|
continue
|
28
|
-
|
29
|
-
|
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
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
"
|
105
|
+
"\u26a0\ufe0f Warning: Markdown syntax issues found:\n{errors}",
|
63
106
|
errors="\n".join(errors),
|
64
107
|
)
|
65
108
|
return msg
|
66
|
-
return "
|
109
|
+
return "\u2705 Syntax valid"
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import os
|
2
|
-
from .gitignore_utils import
|
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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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)
|