253 lines
6.0 KiB
Python
253 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
import ast
|
|
import io
|
|
import os
|
|
import re
|
|
import tokenize
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
OUT = ROOT.parent / "исходный_код_WESP.txt"
|
|
|
|
SKIP_DIRS = {
|
|
"__pycache__",
|
|
".git",
|
|
".pytest_cache",
|
|
".cursor",
|
|
"tests",
|
|
"migrations",
|
|
"vendor",
|
|
"logs",
|
|
"legacy",
|
|
}
|
|
SKIP_FILES = {
|
|
"proga.py",
|
|
}
|
|
EXTS = {".py", ".js", ".html", ".css"}
|
|
|
|
|
|
def should_skip_file(path: Path) -> bool:
|
|
if path.name in SKIP_FILES:
|
|
return True
|
|
if "legacy" in path.name.lower():
|
|
return True
|
|
return False
|
|
|
|
|
|
def should_skip_dir(name: str) -> bool:
|
|
return name in SKIP_DIRS
|
|
|
|
|
|
def iter_source_files():
|
|
for dirpath, dirnames, filenames in os.walk(ROOT):
|
|
dirnames[:] = [d for d in dirnames if not should_skip_dir(d)]
|
|
rel = Path(dirpath).relative_to(ROOT)
|
|
if any(part in SKIP_DIRS for part in rel.parts):
|
|
continue
|
|
for name in sorted(filenames):
|
|
path = Path(dirpath) / name
|
|
if path.suffix.lower() in EXTS and not should_skip_file(path):
|
|
yield path
|
|
|
|
|
|
def _drop_docstring(body):
|
|
if not body:
|
|
return body
|
|
first = body[0]
|
|
if not isinstance(first, ast.Expr):
|
|
return body
|
|
val = first.value
|
|
if isinstance(val, ast.Constant) and isinstance(val.value, str):
|
|
return body[1:]
|
|
return body
|
|
|
|
|
|
class _DocstringStripper(ast.NodeTransformer):
|
|
def visit_FunctionDef(self, node):
|
|
self.generic_visit(node)
|
|
node.body = _drop_docstring(node.body)
|
|
return node
|
|
|
|
def visit_AsyncFunctionDef(self, node):
|
|
self.generic_visit(node)
|
|
node.body = _drop_docstring(node.body)
|
|
return node
|
|
|
|
def visit_ClassDef(self, node):
|
|
self.generic_visit(node)
|
|
node.body = _drop_docstring(node.body)
|
|
return node
|
|
|
|
def visit_Module(self, node):
|
|
self.generic_visit(node)
|
|
node.body = _drop_docstring(node.body)
|
|
return node
|
|
|
|
|
|
def strip_python_comments(source: str) -> str:
|
|
out = []
|
|
try:
|
|
for tok in tokenize.generate_tokens(io.StringIO(source).readline):
|
|
if tok.type == tokenize.COMMENT:
|
|
continue
|
|
out.append(tok)
|
|
return tokenize.untokenize(out)
|
|
except (tokenize.TokenError, SyntaxError):
|
|
return source
|
|
|
|
|
|
def strip_python(source: str) -> str:
|
|
try:
|
|
tree = ast.parse(source)
|
|
tree = _DocstringStripper().visit(tree)
|
|
ast.fix_missing_locations(tree)
|
|
source = ast.unparse(tree)
|
|
except SyntaxError:
|
|
pass
|
|
return strip_python_comments(source)
|
|
|
|
|
|
def strip_js(source: str) -> str:
|
|
result = []
|
|
i = 0
|
|
n = len(source)
|
|
in_single = False
|
|
in_double = False
|
|
in_template = False
|
|
in_line_comment = False
|
|
in_block_comment = False
|
|
escape = False
|
|
|
|
while i < n:
|
|
ch = source[i]
|
|
nxt = source[i + 1] if i + 1 < n else ""
|
|
|
|
if in_line_comment:
|
|
if ch == "\n":
|
|
in_line_comment = False
|
|
result.append(ch)
|
|
i += 1
|
|
continue
|
|
|
|
if in_block_comment:
|
|
if ch == "*" and nxt == "/":
|
|
in_block_comment = False
|
|
i += 2
|
|
continue
|
|
i += 1
|
|
continue
|
|
|
|
if in_single:
|
|
result.append(ch)
|
|
if escape:
|
|
escape = False
|
|
elif ch == "\\":
|
|
escape = True
|
|
elif ch == "'":
|
|
in_single = False
|
|
i += 1
|
|
continue
|
|
|
|
if in_double:
|
|
result.append(ch)
|
|
if escape:
|
|
escape = False
|
|
elif ch == "\\":
|
|
escape = True
|
|
elif ch == '"':
|
|
in_double = False
|
|
i += 1
|
|
continue
|
|
|
|
if in_template:
|
|
result.append(ch)
|
|
if escape:
|
|
escape = False
|
|
elif ch == "\\":
|
|
escape = True
|
|
elif ch == "`":
|
|
in_template = False
|
|
i += 1
|
|
continue
|
|
|
|
if ch == "/" and nxt == "/":
|
|
in_line_comment = True
|
|
i += 2
|
|
continue
|
|
if ch == "/" and nxt == "*":
|
|
in_block_comment = True
|
|
i += 2
|
|
continue
|
|
if ch == "'":
|
|
in_single = True
|
|
result.append(ch)
|
|
i += 1
|
|
continue
|
|
if ch == '"':
|
|
in_double = True
|
|
result.append(ch)
|
|
i += 1
|
|
continue
|
|
if ch == "`":
|
|
in_template = True
|
|
result.append(ch)
|
|
i += 1
|
|
continue
|
|
|
|
result.append(ch)
|
|
i += 1
|
|
|
|
return "".join(result)
|
|
|
|
|
|
def strip_html_css(source: str) -> str:
|
|
source = re.sub(r"<!--[\s\S]*?-->", "", source)
|
|
source = strip_js(source)
|
|
return source
|
|
|
|
|
|
def collapse_blank_lines(text: str) -> str:
|
|
lines = [ln.rstrip() for ln in text.splitlines()]
|
|
cleaned = []
|
|
blank_run = 0
|
|
for ln in lines:
|
|
if not ln.strip():
|
|
blank_run += 1
|
|
if blank_run <= 2:
|
|
cleaned.append("")
|
|
continue
|
|
blank_run = 0
|
|
cleaned.append(ln)
|
|
return "\n".join(cleaned).strip("\n")
|
|
|
|
|
|
def strip_file(path: Path) -> str:
|
|
text = path.read_text(encoding="utf-8", errors="replace")
|
|
ext = path.suffix.lower()
|
|
if ext == ".py":
|
|
body = strip_python(text)
|
|
elif ext == ".js":
|
|
body = strip_js(text)
|
|
else:
|
|
body = strip_html_css(text)
|
|
return collapse_blank_lines(body)
|
|
|
|
|
|
def main():
|
|
chunks = []
|
|
for path in iter_source_files():
|
|
body = strip_file(path)
|
|
if body:
|
|
chunks.append(body)
|
|
content = "\n\n".join(chunks)
|
|
OUT.write_text(content + "\n", encoding="utf-8")
|
|
lines = content.count("\n") + (1 if content else 0)
|
|
print(f"Written: {OUT}")
|
|
print(f"Files: {len(chunks)}")
|
|
print(f"Lines: {lines}")
|
|
print(f"Size: {OUT.stat().st_size} bytes")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|