janito 1.10.0__py3-none-any.whl → 1.11.1__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/conversation_api.py +178 -90
- janito/agent/conversation_ui.py +1 -1
- janito/agent/llm_conversation_history.py +12 -0
- janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +19 -4
- janito/agent/tools/__init__.py +2 -0
- janito/agent/tools/create_directory.py +1 -1
- janito/agent/tools/create_file.py +1 -1
- janito/agent/tools/fetch_url.py +1 -1
- janito/agent/tools/find_files.py +26 -13
- janito/agent/tools/get_file_outline/core.py +1 -1
- janito/agent/tools/get_file_outline/python_outline.py +139 -95
- janito/agent/tools/get_lines.py +92 -63
- janito/agent/tools/move_file.py +58 -32
- janito/agent/tools/open_url.py +31 -0
- janito/agent/tools/python_command_runner.py +85 -86
- janito/agent/tools/python_file_runner.py +85 -86
- janito/agent/tools/python_stdin_runner.py +87 -88
- janito/agent/tools/remove_directory.py +1 -1
- janito/agent/tools/remove_file.py +1 -1
- janito/agent/tools/replace_file.py +2 -2
- janito/agent/tools/replace_text_in_file.py +193 -149
- janito/agent/tools/run_bash_command.py +1 -1
- janito/agent/tools/run_powershell_command.py +4 -0
- 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 +132 -0
- janito/agent/tools/validate_file_syntax/core.py +41 -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/gitignore_utils.py +25 -2
- janito/agent/tools_utils/utils.py +7 -1
- janito/cli/config_commands.py +112 -109
- janito/shell/main.py +51 -8
- janito/shell/session/config.py +83 -75
- janito/shell/ui/interactive.py +97 -73
- janito/termweb/static/editor.css +32 -29
- 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-1.10.0.dist-info → janito-1.11.1.dist-info}/METADATA +1 -1
- {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/RECORD +56 -51
- {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/WHEEL +1 -1
- janito/agent/tools/search_text.py +0 -254
- {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/entry_points.txt +0 -0
- {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/licenses/LICENSE +0 -0
- {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,65 @@
|
|
1
|
+
import re
|
2
|
+
from janito.i18n import tr
|
3
|
+
from janito.agent.tools_utils.utils import pluralize
|
4
|
+
|
5
|
+
|
6
|
+
def prepare_pattern(pattern, is_regex, report_error, report_warning):
|
7
|
+
if not pattern:
|
8
|
+
report_error(tr("Error: Empty search pattern provided. Operation aborted."))
|
9
|
+
return (
|
10
|
+
None,
|
11
|
+
False,
|
12
|
+
tr("Error: Empty search pattern provided. Operation aborted."),
|
13
|
+
)
|
14
|
+
regex = None
|
15
|
+
use_regex = False
|
16
|
+
if is_regex:
|
17
|
+
try:
|
18
|
+
regex = re.compile(pattern)
|
19
|
+
use_regex = True
|
20
|
+
except re.error as e:
|
21
|
+
report_warning(tr("\u26a0\ufe0f Invalid regex pattern."))
|
22
|
+
return (
|
23
|
+
None,
|
24
|
+
False,
|
25
|
+
tr(
|
26
|
+
"Error: Invalid regex pattern: {error}. Operation aborted.", error=e
|
27
|
+
),
|
28
|
+
)
|
29
|
+
else:
|
30
|
+
# Do not compile as regex if is_regex is False; treat as plain text
|
31
|
+
regex = None
|
32
|
+
use_regex = False
|
33
|
+
return regex, use_regex, None
|
34
|
+
|
35
|
+
|
36
|
+
def format_result(
|
37
|
+
pattern, use_regex, output, limit_reached, count_only=False, per_file_counts=None
|
38
|
+
):
|
39
|
+
# Ensure output is always a list for joining
|
40
|
+
if output is None or not isinstance(output, (list, tuple)):
|
41
|
+
output = []
|
42
|
+
if count_only:
|
43
|
+
lines = []
|
44
|
+
total = 0
|
45
|
+
if per_file_counts:
|
46
|
+
for file_path, count in per_file_counts:
|
47
|
+
lines.append(f"{file_path}: {count}")
|
48
|
+
total += count
|
49
|
+
lines.append(f"Total matches: {total}")
|
50
|
+
if limit_reached:
|
51
|
+
lines.append(tr("[Max results reached. Output truncated.]"))
|
52
|
+
return "\n".join(lines)
|
53
|
+
else:
|
54
|
+
if not output:
|
55
|
+
return tr("No matches found.")
|
56
|
+
result = "\n".join(output)
|
57
|
+
if limit_reached:
|
58
|
+
result += tr("\n[Max results reached. Output truncated.]")
|
59
|
+
return result
|
60
|
+
|
61
|
+
|
62
|
+
def summarize_total(all_per_file_counts):
|
63
|
+
total = sum(count for _, count in all_per_file_counts)
|
64
|
+
summary = f"\nGrand total matches: {total}"
|
65
|
+
return summary
|
@@ -0,0 +1,132 @@
|
|
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
|
+
# Always exclude directories named .git, regardless of gitignore
|
19
|
+
return [
|
20
|
+
d
|
21
|
+
for d in dirs
|
22
|
+
if d != ".git" and not gitignore_filter.is_ignored(os.path.join(root, d))
|
23
|
+
]
|
24
|
+
|
25
|
+
|
26
|
+
def process_file_count_only(
|
27
|
+
file_path, per_file_counts, pattern, regex, use_regex, max_results, total_results
|
28
|
+
):
|
29
|
+
match_count, file_limit_reached, _ = read_file_lines(
|
30
|
+
file_path,
|
31
|
+
pattern,
|
32
|
+
regex,
|
33
|
+
use_regex,
|
34
|
+
True,
|
35
|
+
max_results,
|
36
|
+
total_results + sum(count for _, count in per_file_counts),
|
37
|
+
)
|
38
|
+
if match_count > 0:
|
39
|
+
per_file_counts.append((file_path, match_count))
|
40
|
+
return file_limit_reached
|
41
|
+
|
42
|
+
|
43
|
+
def process_file_collect(
|
44
|
+
file_path,
|
45
|
+
dir_output,
|
46
|
+
per_file_counts,
|
47
|
+
pattern,
|
48
|
+
regex,
|
49
|
+
use_regex,
|
50
|
+
max_results,
|
51
|
+
total_results,
|
52
|
+
):
|
53
|
+
actual_match_count, file_limit_reached, file_lines_output = read_file_lines(
|
54
|
+
file_path,
|
55
|
+
pattern,
|
56
|
+
regex,
|
57
|
+
use_regex,
|
58
|
+
False,
|
59
|
+
max_results,
|
60
|
+
total_results + len(dir_output),
|
61
|
+
)
|
62
|
+
dir_output.extend(file_lines_output)
|
63
|
+
if actual_match_count > 0:
|
64
|
+
per_file_counts.append((file_path, actual_match_count))
|
65
|
+
return file_limit_reached
|
66
|
+
|
67
|
+
|
68
|
+
def should_limit_depth(root, search_path, max_depth, dirs):
|
69
|
+
if max_depth > 0:
|
70
|
+
rel_root = os.path.relpath(root, search_path)
|
71
|
+
if rel_root != ".":
|
72
|
+
depth = rel_root.count(os.sep) + 1
|
73
|
+
if depth >= max_depth:
|
74
|
+
del dirs[:]
|
75
|
+
|
76
|
+
|
77
|
+
def traverse_directory(
|
78
|
+
search_path,
|
79
|
+
pattern,
|
80
|
+
regex,
|
81
|
+
use_regex,
|
82
|
+
max_depth,
|
83
|
+
max_results,
|
84
|
+
total_results,
|
85
|
+
count_only,
|
86
|
+
):
|
87
|
+
dir_output = []
|
88
|
+
dir_limit_reached = False
|
89
|
+
per_file_counts = []
|
90
|
+
walker = walk_directory(search_path, max_depth)
|
91
|
+
gitignore_filter = GitignoreFilter(search_path)
|
92
|
+
|
93
|
+
for root, dirs, files in walker:
|
94
|
+
dirs[:] = filter_dirs(dirs, root, gitignore_filter)
|
95
|
+
for file in files:
|
96
|
+
file_path = os.path.join(root, file)
|
97
|
+
if gitignore_filter.is_ignored(file_path):
|
98
|
+
continue
|
99
|
+
if count_only:
|
100
|
+
file_limit_reached = process_file_count_only(
|
101
|
+
file_path,
|
102
|
+
per_file_counts,
|
103
|
+
pattern,
|
104
|
+
regex,
|
105
|
+
use_regex,
|
106
|
+
max_results,
|
107
|
+
total_results,
|
108
|
+
)
|
109
|
+
if file_limit_reached:
|
110
|
+
dir_limit_reached = True
|
111
|
+
break
|
112
|
+
else:
|
113
|
+
file_limit_reached = process_file_collect(
|
114
|
+
file_path,
|
115
|
+
dir_output,
|
116
|
+
per_file_counts,
|
117
|
+
pattern,
|
118
|
+
regex,
|
119
|
+
use_regex,
|
120
|
+
max_results,
|
121
|
+
total_results,
|
122
|
+
)
|
123
|
+
if file_limit_reached:
|
124
|
+
dir_limit_reached = True
|
125
|
+
break
|
126
|
+
if dir_limit_reached:
|
127
|
+
break
|
128
|
+
should_limit_depth(root, search_path, max_depth, dirs)
|
129
|
+
if count_only:
|
130
|
+
return per_file_counts, dir_limit_reached, []
|
131
|
+
else:
|
132
|
+
return dir_output, dir_limit_reached, per_file_counts
|
@@ -16,39 +16,47 @@ from .js_validator import validate_js
|
|
16
16
|
from .css_validator import validate_css
|
17
17
|
|
18
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
|
+
|
19
45
|
def validate_file_syntax(
|
20
46
|
file_path: str, report_info=None, report_warning=None, report_success=None
|
21
47
|
) -> str:
|
22
48
|
ext = os.path.splitext(file_path)[1].lower()
|
49
|
+
validator = _get_validator(ext)
|
23
50
|
try:
|
24
|
-
if
|
25
|
-
return
|
26
|
-
elif ext == ".json":
|
27
|
-
return validate_json(file_path)
|
28
|
-
elif ext in [".yml", ".yaml"]:
|
29
|
-
return validate_yaml(file_path)
|
30
|
-
elif ext == ".ps1":
|
31
|
-
return validate_ps1(file_path)
|
32
|
-
elif ext == ".xml":
|
33
|
-
return validate_xml(file_path)
|
34
|
-
elif ext in (".html", ".htm"):
|
35
|
-
return validate_html(file_path)
|
36
|
-
elif ext == ".md":
|
37
|
-
return validate_markdown(file_path)
|
38
|
-
elif ext == ".js":
|
39
|
-
return validate_js(file_path)
|
40
|
-
elif ext == ".css":
|
41
|
-
return validate_css(file_path)
|
51
|
+
if validator:
|
52
|
+
return validator(file_path)
|
42
53
|
else:
|
43
|
-
msg = tr("
|
54
|
+
msg = tr("\u26a0\ufe0f Warning: Unsupported file extension: {ext}", ext=ext)
|
44
55
|
if report_warning:
|
45
56
|
report_warning(msg)
|
46
57
|
return msg
|
47
58
|
except Exception as e:
|
48
|
-
|
49
|
-
if report_warning:
|
50
|
-
report_warning(msg)
|
51
|
-
return msg
|
59
|
+
return _handle_validation_error(e, report_warning)
|
52
60
|
|
53
61
|
|
54
62
|
@register_tool(name="validate_file_syntax")
|
@@ -70,16 +78,19 @@ class ValidateFileSyntaxTool(ToolBase):
|
|
70
78
|
file_path (str): Path to the file to validate.
|
71
79
|
Returns:
|
72
80
|
str: Validation status message. Example:
|
73
|
-
- "
|
74
|
-
- "
|
75
|
-
- "
|
81
|
+
- "\u2705 Syntax OK"
|
82
|
+
- "\u26a0\ufe0f Warning: Syntax error: <error message>"
|
83
|
+
- "\u26a0\ufe0f Warning: Unsupported file extension: <ext>"
|
76
84
|
"""
|
77
85
|
|
78
86
|
def run(self, file_path: str) -> str:
|
79
87
|
disp_path = display_path(file_path)
|
80
88
|
self.report_info(
|
81
89
|
ActionType.READ,
|
82
|
-
tr(
|
90
|
+
tr(
|
91
|
+
"\U0001f50e Validate syntax for file '{disp_path}' ...",
|
92
|
+
disp_path=disp_path,
|
93
|
+
),
|
83
94
|
)
|
84
95
|
result = validate_file_syntax(
|
85
96
|
file_path,
|
@@ -87,8 +98,8 @@ class ValidateFileSyntaxTool(ToolBase):
|
|
87
98
|
report_warning=self.report_warning,
|
88
99
|
report_success=self.report_success,
|
89
100
|
)
|
90
|
-
if result.startswith("
|
101
|
+
if result.startswith("\u2705"):
|
91
102
|
self.report_success(result)
|
92
|
-
elif result.startswith("
|
93
|
-
self.report_warning(tr("
|
103
|
+
elif result.startswith("\u26a0\ufe0f"):
|
104
|
+
self.report_warning(tr("\u26a0\ufe0f ") + result.lstrip("\u26a0\ufe0f "))
|
94
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"
|
@@ -9,7 +9,7 @@ class GitignoreFilter:
|
|
9
9
|
Methods
|
10
10
|
-------
|
11
11
|
__init__(self, gitignore_path: str = ".gitignore")
|
12
|
-
Loads and parses .gitignore patterns from the specified path.
|
12
|
+
Loads and parses .gitignore patterns from the specified path or finds the nearest .gitignore if a directory is given.
|
13
13
|
|
14
14
|
is_ignored(self, path: str) -> bool
|
15
15
|
Returns True if the given path matches any of the loaded .gitignore patterns.
|
@@ -18,8 +18,31 @@ class GitignoreFilter:
|
|
18
18
|
Filters out ignored directories and files from the provided lists, returning only those not ignored.
|
19
19
|
"""
|
20
20
|
|
21
|
+
@staticmethod
|
22
|
+
def find_nearest_gitignore(start_path):
|
23
|
+
"""
|
24
|
+
Search upward from start_path for the nearest .gitignore file.
|
25
|
+
Returns the path to the found .gitignore, or the default .gitignore in start_path if none found.
|
26
|
+
"""
|
27
|
+
current_dir = os.path.abspath(start_path)
|
28
|
+
if os.path.isfile(current_dir):
|
29
|
+
current_dir = os.path.dirname(current_dir)
|
30
|
+
while True:
|
31
|
+
candidate = os.path.join(current_dir, ".gitignore")
|
32
|
+
if os.path.isfile(candidate):
|
33
|
+
return candidate
|
34
|
+
parent = os.path.dirname(current_dir)
|
35
|
+
if parent == current_dir:
|
36
|
+
# Reached filesystem root, return default .gitignore path (may not exist)
|
37
|
+
return os.path.join(start_path, ".gitignore")
|
38
|
+
current_dir = parent
|
39
|
+
|
21
40
|
def __init__(self, gitignore_path: str = ".gitignore"):
|
22
|
-
|
41
|
+
# If a directory is passed, find the nearest .gitignore up the tree
|
42
|
+
if os.path.isdir(gitignore_path):
|
43
|
+
self.gitignore_path = self.find_nearest_gitignore(gitignore_path)
|
44
|
+
else:
|
45
|
+
self.gitignore_path = os.path.abspath(gitignore_path)
|
23
46
|
self.base_dir = os.path.dirname(self.gitignore_path)
|
24
47
|
lines = []
|
25
48
|
if not os.path.exists(self.gitignore_path):
|
@@ -12,7 +12,13 @@ def display_path(path):
|
|
12
12
|
str: Display path, optionally as an ANSI hyperlink.
|
13
13
|
"""
|
14
14
|
if os.path.isabs(path):
|
15
|
-
|
15
|
+
cwd = os.path.abspath(os.getcwd())
|
16
|
+
abs_path = os.path.abspath(path)
|
17
|
+
# Check if the absolute path is within the current working directory
|
18
|
+
if abs_path.startswith(cwd + os.sep):
|
19
|
+
disp = os.path.relpath(abs_path, cwd)
|
20
|
+
else:
|
21
|
+
disp = path
|
16
22
|
else:
|
17
23
|
disp = os.path.relpath(path)
|
18
24
|
port = runtime_config.get("termweb_port")
|