codetool-shell 0.1.1__py3-none-win_amd64.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.
- codetool_shell/__init__.py +11 -0
- codetool_shell/api.py +59 -0
- codetool_shell/bin/windows-x86_64/codetool-shell-rust.exe +0 -0
- codetool_shell/filters/__init__.py +14 -0
- codetool_shell/filters/build_compiler/__init__.py +7 -0
- codetool_shell/filters/build_compiler/detector.py +412 -0
- codetool_shell/filters/build_compiler/reducer.py +166 -0
- codetool_shell/filters/build_compiler/summary.py +617 -0
- codetool_shell/filters/ci_job_log/__init__.py +7 -0
- codetool_shell/filters/ci_job_log/detector.py +64 -0
- codetool_shell/filters/ci_job_log/reducer.py +99 -0
- codetool_shell/filters/ci_job_log/summary.py +243 -0
- codetool_shell/filters/diff/__init__.py +7 -0
- codetool_shell/filters/diff/detector.py +136 -0
- codetool_shell/filters/diff/reducer.py +308 -0
- codetool_shell/filters/generic_log/__init__.py +7 -0
- codetool_shell/filters/generic_log/detector.py +175 -0
- codetool_shell/filters/generic_log/reducer.py +99 -0
- codetool_shell/filters/generic_log/summary.py +161 -0
- codetool_shell/filters/git.py +514 -0
- codetool_shell/filters/html_cleanup/__init__.py +7 -0
- codetool_shell/filters/html_cleanup/detector.py +136 -0
- codetool_shell/filters/html_cleanup/reducer.py +27 -0
- codetool_shell/filters/html_cleanup/summary.py +422 -0
- codetool_shell/filters/json_payload/__init__.py +7 -0
- codetool_shell/filters/json_payload/detector.py +62 -0
- codetool_shell/filters/json_payload/reducer.py +81 -0
- codetool_shell/filters/json_payload/summary.py +233 -0
- codetool_shell/filters/listing/__init__.py +7 -0
- codetool_shell/filters/listing/detector.py +294 -0
- codetool_shell/filters/listing/reducer.py +30 -0
- codetool_shell/filters/log_template/__init__.py +7 -0
- codetool_shell/filters/log_template/constants.py +76 -0
- codetool_shell/filters/log_template/detector.py +331 -0
- codetool_shell/filters/log_template/reducer.py +78 -0
- codetool_shell/filters/log_template/template.py +280 -0
- codetool_shell/filters/log_template/types.py +21 -0
- codetool_shell/filters/opaque_payload/__init__.py +7 -0
- codetool_shell/filters/opaque_payload/detector.py +563 -0
- codetool_shell/filters/opaque_payload/reducer.py +142 -0
- codetool_shell/filters/opaque_payload/summary.py +61 -0
- codetool_shell/filters/package_manager/__init__.py +7 -0
- codetool_shell/filters/package_manager/detector.py +220 -0
- codetool_shell/filters/package_manager/reducer.py +110 -0
- codetool_shell/filters/package_manager/summary.py +172 -0
- codetool_shell/filters/pipeline.py +65 -0
- codetool_shell/filters/rg.py +250 -0
- codetool_shell/filters/system_output/__init__.py +7 -0
- codetool_shell/filters/system_output/detector.py +600 -0
- codetool_shell/filters/system_output/reducer.py +331 -0
- codetool_shell/filters/system_output/summary.py +164 -0
- codetool_shell/filters/table/__init__.py +7 -0
- codetool_shell/filters/table/detector.py +244 -0
- codetool_shell/filters/table/reducer.py +57 -0
- codetool_shell/filters/table/summary.py +37 -0
- codetool_shell/filters/test_runner/__init__.py +7 -0
- codetool_shell/filters/test_runner/ansi.py +80 -0
- codetool_shell/filters/test_runner/detector.py +409 -0
- codetool_shell/filters/test_runner/reducer.py +288 -0
- codetool_shell/filters/test_runner/summary.py +449 -0
- codetool_shell/filters/text.py +38 -0
- codetool_shell/filters/traceback/__init__.py +7 -0
- codetool_shell/filters/traceback/detector.py +209 -0
- codetool_shell/filters/traceback/reducer.py +141 -0
- codetool_shell/filters/traceback/summary.py +122 -0
- codetool_shell/filters/tree.py +59 -0
- codetool_shell/py.typed +0 -0
- codetool_shell/python_backend.py +38 -0
- codetool_shell/rust_backend.py +254 -0
- codetool_shell-0.1.1.dist-info/METADATA +152 -0
- codetool_shell-0.1.1.dist-info/RECORD +72 -0
- codetool_shell-0.1.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Conservative parsers for structured table output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class StructuredTable:
|
|
11
|
+
"""Parsed table lines ready for row reduction."""
|
|
12
|
+
|
|
13
|
+
header_lines: tuple[str, ...]
|
|
14
|
+
rows: tuple[str, ...]
|
|
15
|
+
footer_lines: tuple[str, ...] = ()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_MIN_DATA_ROWS = 9
|
|
19
|
+
_PSQL_FOOTER_RE = re.compile(r"^\(\d+ rows?\)$")
|
|
20
|
+
_PATH_LOCATION_RE = re.compile(r"^(.+?):([1-9]\d*)(?::([1-9]\d*))?:(.*)$")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_structured_table(lines: list[str]) -> StructuredTable | None:
|
|
24
|
+
"""Return a parsed safe table, or ``None`` if the shape is uncertain."""
|
|
25
|
+
|
|
26
|
+
if _has_blocking_context(lines):
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
for parser in (
|
|
30
|
+
_parse_mysql_box_table,
|
|
31
|
+
_parse_psql_table,
|
|
32
|
+
_parse_markdown_pipe_table,
|
|
33
|
+
_parse_sqlite_column_table,
|
|
34
|
+
):
|
|
35
|
+
parsed = parser(lines)
|
|
36
|
+
if parsed is not None and len(parsed.rows) >= _MIN_DATA_ROWS:
|
|
37
|
+
return parsed
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parse_psql_table(lines: list[str]) -> StructuredTable | None:
|
|
42
|
+
if len(lines) < _MIN_DATA_ROWS + 2:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
footer_lines: tuple[str, ...] = ()
|
|
46
|
+
body = lines
|
|
47
|
+
if lines and _PSQL_FOOTER_RE.match(lines[-1].strip()):
|
|
48
|
+
footer_lines = (lines[-1],)
|
|
49
|
+
body = lines[:-1]
|
|
50
|
+
|
|
51
|
+
if len(body) < _MIN_DATA_ROWS + 2:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
header, separator = body[0], body[1]
|
|
55
|
+
if "|" not in header or not _is_psql_separator(separator):
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
column_count = len(separator.strip().split("+"))
|
|
59
|
+
if column_count < 2 or len(header.split("|")) != column_count:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
rows = body[2:]
|
|
63
|
+
if any(row.count("|") != column_count - 1 for row in rows):
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
return StructuredTable((header, separator), tuple(rows), footer_lines)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _parse_sqlite_column_table(lines: list[str]) -> StructuredTable | None:
|
|
70
|
+
if len(lines) < _MIN_DATA_ROWS + 2:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
header, separator = lines[0], lines[1]
|
|
74
|
+
if "|" in header or "|" in separator or "+" in separator:
|
|
75
|
+
return None
|
|
76
|
+
if not _is_sqlite_separator(separator):
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
header_columns = _split_columns(header)
|
|
80
|
+
separator_columns = _split_columns(separator)
|
|
81
|
+
if len(header_columns) != len(separator_columns) or len(header_columns) < 2:
|
|
82
|
+
return None
|
|
83
|
+
if any(not _is_safe_header_cell(column) for column in header_columns):
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
rows = lines[2:]
|
|
87
|
+
valid_rows = 0
|
|
88
|
+
for row in rows:
|
|
89
|
+
if "|" in row or _looks_like_path_location_line(row) or _looks_like_log_line(row):
|
|
90
|
+
return None
|
|
91
|
+
if len(_split_columns(row)) >= 2:
|
|
92
|
+
valid_rows += 1
|
|
93
|
+
|
|
94
|
+
if valid_rows * 4 < len(rows) * 3:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
return StructuredTable((header, separator), tuple(rows))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _parse_mysql_box_table(lines: list[str]) -> StructuredTable | None:
|
|
101
|
+
if len(lines) < _MIN_DATA_ROWS + 4:
|
|
102
|
+
return None
|
|
103
|
+
if not (
|
|
104
|
+
_is_mysql_border(lines[0])
|
|
105
|
+
and _is_mysql_row(lines[1])
|
|
106
|
+
and _is_mysql_border(lines[2])
|
|
107
|
+
):
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
column_count = lines[0].count("+") - 1
|
|
111
|
+
if column_count < 2 or lines[1].count("|") != column_count + 1:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
row_end: int | None = None
|
|
115
|
+
for index in range(3, len(lines)):
|
|
116
|
+
line = lines[index]
|
|
117
|
+
if _is_mysql_border(line):
|
|
118
|
+
row_end = index
|
|
119
|
+
break
|
|
120
|
+
if not _is_mysql_row(line) or line.count("|") != column_count + 1:
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
if row_end is None:
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
rows = lines[3:row_end]
|
|
127
|
+
footer_lines = lines[row_end:]
|
|
128
|
+
if len(footer_lines) > 1 and any(
|
|
129
|
+
not _is_mysql_footer_line(line) for line in footer_lines[1:]
|
|
130
|
+
):
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
return StructuredTable(tuple(lines[:3]), tuple(rows), tuple(footer_lines))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _parse_markdown_pipe_table(lines: list[str]) -> StructuredTable | None:
|
|
137
|
+
if len(lines) < _MIN_DATA_ROWS + 2:
|
|
138
|
+
return None
|
|
139
|
+
if not (_is_markdown_row(lines[0]) and _is_markdown_separator(lines[1])):
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
rows = lines[2:]
|
|
143
|
+
if any(not _is_markdown_row(row) for row in rows):
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
return StructuredTable((lines[0], lines[1]), tuple(rows))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _has_blocking_context(lines: list[str]) -> bool:
|
|
150
|
+
if not lines or any(not line.strip() for line in lines):
|
|
151
|
+
return True
|
|
152
|
+
if any(line.lstrip().startswith(("```", "~~~")) for line in lines):
|
|
153
|
+
return True
|
|
154
|
+
if any(_looks_like_traceback_line(line) for line in lines):
|
|
155
|
+
return True
|
|
156
|
+
return _looks_like_path_location_output(lines)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _is_psql_separator(line: str) -> bool:
|
|
160
|
+
parts = line.strip().split("+")
|
|
161
|
+
return len(parts) >= 2 and all(
|
|
162
|
+
len(part.strip()) >= 2 and set(part.strip()) == {"-"} for part in parts
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _is_sqlite_separator(line: str) -> bool:
|
|
167
|
+
columns = _split_columns(line)
|
|
168
|
+
return len(columns) >= 2 and all(
|
|
169
|
+
len(column) >= 2 and set(column) == {"-"} for column in columns
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _split_columns(line: str) -> list[str]:
|
|
174
|
+
return [column.strip() for column in re.split(r"\s{2,}", line.strip()) if column.strip()]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _is_safe_header_cell(cell: str) -> bool:
|
|
178
|
+
return bool(re.fullmatch(r"[A-Za-z0-9_.#()/ -]{1,80}", cell))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _is_mysql_border(line: str) -> bool:
|
|
182
|
+
return (
|
|
183
|
+
line.startswith("+")
|
|
184
|
+
and line.endswith("+")
|
|
185
|
+
and line.count("+") >= 3
|
|
186
|
+
and all(char in "+-" for char in line)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _is_mysql_row(line: str) -> bool:
|
|
191
|
+
return line.startswith("|") and line.endswith("|") and line.count("|") >= 3
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _is_mysql_footer_line(line: str) -> bool:
|
|
195
|
+
stripped = line.strip()
|
|
196
|
+
return " row in set" in stripped or " rows in set" in stripped
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _is_markdown_row(line: str) -> bool:
|
|
200
|
+
stripped = line.strip()
|
|
201
|
+
return stripped.startswith("|") and stripped.endswith("|") and stripped.count("|") >= 3
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _is_markdown_separator(line: str) -> bool:
|
|
205
|
+
if not _is_markdown_row(line):
|
|
206
|
+
return False
|
|
207
|
+
cells = [cell.strip().replace(" ", "") for cell in line.strip().strip("|").split("|")]
|
|
208
|
+
if len(cells) < 2:
|
|
209
|
+
return False
|
|
210
|
+
for cell in cells:
|
|
211
|
+
if not re.fullmatch(r":?-{3,}:?", cell):
|
|
212
|
+
return False
|
|
213
|
+
return True
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _looks_like_traceback_line(line: str) -> bool:
|
|
217
|
+
stripped = line.lstrip()
|
|
218
|
+
return stripped.startswith("Traceback ") or stripped.startswith('File "')
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _looks_like_log_line(line: str) -> bool:
|
|
222
|
+
stripped = line.lstrip()
|
|
223
|
+
return stripped.startswith(("INFO ", "DEBUG ", "TRACE ", "WARN ", "ERROR "))
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _looks_like_path_location_output(lines: list[str]) -> bool:
|
|
227
|
+
matches = sum(1 for line in lines if _looks_like_path_location_line(line))
|
|
228
|
+
return matches >= 2 and matches * 2 >= len(lines)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _looks_like_path_location_line(line: str) -> bool:
|
|
232
|
+
match = _PATH_LOCATION_RE.match(line)
|
|
233
|
+
if match is None:
|
|
234
|
+
return False
|
|
235
|
+
return _looks_like_path(match.group(1))
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _looks_like_path(value: str) -> bool:
|
|
239
|
+
return (
|
|
240
|
+
"/" in value
|
|
241
|
+
or "\\" in value
|
|
242
|
+
or "." in value.rsplit("/", 1)[-1].rsplit("\\", 1)[-1]
|
|
243
|
+
or value.startswith(("~", "/"))
|
|
244
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Reduce long structured tables while preserving diagnostic rows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..text import join_preserving_final_newline, score, split_preserving_final_newline
|
|
6
|
+
from .detector import StructuredTable, parse_structured_table
|
|
7
|
+
from .summary import is_interesting_row
|
|
8
|
+
|
|
9
|
+
_HEAD_ROWS = 3
|
|
10
|
+
_TAIL_ROWS = 3
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def compress_table_output(text: str) -> str:
|
|
14
|
+
"""Compress clear long table output, otherwise return ``text`` unchanged."""
|
|
15
|
+
|
|
16
|
+
lines, final_newline = split_preserving_final_newline(text)
|
|
17
|
+
parsed = parse_structured_table(lines)
|
|
18
|
+
if parsed is None:
|
|
19
|
+
return text
|
|
20
|
+
|
|
21
|
+
candidate_lines = _reduce_table(parsed)
|
|
22
|
+
candidate = join_preserving_final_newline(candidate_lines, final_newline)
|
|
23
|
+
if score(candidate) < score(text):
|
|
24
|
+
return candidate
|
|
25
|
+
return text
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _reduce_table(table: StructuredTable) -> list[str]:
|
|
29
|
+
selected_indices = _select_row_indices(table.rows)
|
|
30
|
+
selected = [*table.header_lines]
|
|
31
|
+
cursor = 0
|
|
32
|
+
|
|
33
|
+
for index in selected_indices:
|
|
34
|
+
omitted = index - cursor
|
|
35
|
+
if omitted > 0:
|
|
36
|
+
selected.append(_omitted_line(omitted))
|
|
37
|
+
selected.append(table.rows[index])
|
|
38
|
+
cursor = index + 1
|
|
39
|
+
|
|
40
|
+
if cursor < len(table.rows):
|
|
41
|
+
selected.append(_omitted_line(len(table.rows) - cursor))
|
|
42
|
+
|
|
43
|
+
selected.extend(table.footer_lines)
|
|
44
|
+
return selected
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _select_row_indices(rows: tuple[str, ...]) -> list[int]:
|
|
48
|
+
total = len(rows)
|
|
49
|
+
selected = set(range(min(_HEAD_ROWS, total)))
|
|
50
|
+
selected.update(range(max(_HEAD_ROWS, total - _TAIL_ROWS), total))
|
|
51
|
+
selected.update(index for index, row in enumerate(rows) if is_interesting_row(row))
|
|
52
|
+
return sorted(selected)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _omitted_line(count: int) -> str:
|
|
56
|
+
plural = "" if count == 1 else "s"
|
|
57
|
+
return f"… {count} table row{plural} omitted"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Row importance helpers for structured table compression."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
_ALERT_RE = re.compile(
|
|
8
|
+
r"\b(?:error|errors|warn|warning|warnings|fail|failed|failure|fatal|critical|exception|traceback)\b",
|
|
9
|
+
re.IGNORECASE,
|
|
10
|
+
)
|
|
11
|
+
_URL_RE = re.compile(r"\b[A-Za-z][A-Za-z0-9+.-]*://\S+")
|
|
12
|
+
_PATH_RE = re.compile(
|
|
13
|
+
r"(^|[\s|])(?:\.{0,2}/|/|~/|[A-Za-z]:[\\/]|[A-Za-z0-9_.@+-]+/[A-Za-z0-9_.@+/\-]+)"
|
|
14
|
+
)
|
|
15
|
+
_UUID_RE = re.compile(
|
|
16
|
+
r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b"
|
|
17
|
+
)
|
|
18
|
+
_LONG_HEX_RE = re.compile(r"\b[0-9a-fA-F]{12,}\b")
|
|
19
|
+
_ISSUE_ID_RE = re.compile(r"\b[A-Z]{2,}-\d{2,}\b")
|
|
20
|
+
_NAMED_ID_RE = re.compile(r"\b(?:id|uuid|hash|sha)[=: ]+[A-Za-z0-9_-]{8,}\b", re.IGNORECASE)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_interesting_row(row: str) -> bool:
|
|
24
|
+
"""Return whether a row should be preserved even from the middle."""
|
|
25
|
+
|
|
26
|
+
return any(
|
|
27
|
+
pattern.search(row)
|
|
28
|
+
for pattern in (
|
|
29
|
+
_ALERT_RE,
|
|
30
|
+
_URL_RE,
|
|
31
|
+
_PATH_RE,
|
|
32
|
+
_UUID_RE,
|
|
33
|
+
_LONG_HEX_RE,
|
|
34
|
+
_ISSUE_ID_RE,
|
|
35
|
+
_NAMED_ID_RE,
|
|
36
|
+
)
|
|
37
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""ANSI/control decoration stripping for test-runner output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
ESC = "\x1b"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def strip_ansi_control_sequences(text: str) -> str:
|
|
9
|
+
"""Remove known terminal decorations while preserving unknown escapes."""
|
|
10
|
+
|
|
11
|
+
output: list[str] = []
|
|
12
|
+
index = 0
|
|
13
|
+
length = len(text)
|
|
14
|
+
|
|
15
|
+
while index < length:
|
|
16
|
+
char = text[index]
|
|
17
|
+
|
|
18
|
+
if char == ESC:
|
|
19
|
+
sequence_end = _known_escape_sequence_end(text, index)
|
|
20
|
+
if sequence_end is not None:
|
|
21
|
+
index = sequence_end
|
|
22
|
+
continue
|
|
23
|
+
output.append(char)
|
|
24
|
+
index += 1
|
|
25
|
+
continue
|
|
26
|
+
|
|
27
|
+
if _is_removable_control_char(char):
|
|
28
|
+
index += 1
|
|
29
|
+
continue
|
|
30
|
+
|
|
31
|
+
output.append(char)
|
|
32
|
+
index += 1
|
|
33
|
+
|
|
34
|
+
return "".join(output)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _known_escape_sequence_end(text: str, index: int) -> int | None:
|
|
38
|
+
csi_end = _known_csi_sequence_end(text, index)
|
|
39
|
+
if csi_end is not None:
|
|
40
|
+
return csi_end
|
|
41
|
+
|
|
42
|
+
if index + 2 < len(text) and text[index + 1] in {"(", ")", "*", "+"}:
|
|
43
|
+
final = ord(text[index + 2])
|
|
44
|
+
if 0x20 <= final <= 0x7E:
|
|
45
|
+
return index + 3
|
|
46
|
+
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _known_csi_sequence_end(text: str, index: int) -> int | None:
|
|
51
|
+
start = index + 2
|
|
52
|
+
if start > len(text) or text[index + 1 : start] != "[":
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
cursor = start
|
|
56
|
+
while cursor < len(text):
|
|
57
|
+
char = text[cursor]
|
|
58
|
+
codepoint = ord(char)
|
|
59
|
+
if 0x40 <= codepoint <= 0x7E:
|
|
60
|
+
params = text[start:cursor]
|
|
61
|
+
if _should_strip_csi(params, char):
|
|
62
|
+
return cursor + 1
|
|
63
|
+
return None
|
|
64
|
+
if not 0x20 <= codepoint <= 0x3F:
|
|
65
|
+
return None
|
|
66
|
+
cursor += 1
|
|
67
|
+
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _should_strip_csi(params: str, final: str) -> bool:
|
|
72
|
+
if final in {"m", "K", "J", "A", "B", "C", "D", "E", "F", "G"}:
|
|
73
|
+
return True
|
|
74
|
+
return final in {"h", "l"} and params.startswith("?25")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _is_removable_control_char(char: str) -> bool:
|
|
78
|
+
return char not in {"\n", "\t"} and (
|
|
79
|
+
"\x00" <= char <= "\x08" or "\x0b" <= char <= "\x1f" or char == "\x7f"
|
|
80
|
+
)
|