Files
2026-07-13 12:29:52 +08:00

259 lines
5.4 KiB
Python

#!/usr/bin/env python3
"""Shared helpers for the software copyright materials skill."""
from __future__ import annotations
import json
import os
import re
from pathlib import Path
from typing import Any, Iterable
EXCLUDE_DIRS = {
".git",
".hg",
".svn",
".idea",
".vscode",
"__pycache__",
"node_modules",
"dist",
"build",
".next",
".nuxt",
".output",
"coverage",
"target",
"vendor",
"软件著作权申请资料",
"software-copyright-materials",
}
CODE_EXTS = {
".vue",
".ts",
".tsx",
".js",
".jsx",
".mjs",
".cjs",
".css",
".scss",
".sass",
".less",
".html",
".svelte",
".astro",
".json",
".md",
}
KNOWN_CONFIG_FILES = {
".babelrc",
".eslintrc",
".eslintrc.json",
".eslintrc.yaml",
".eslintrc.yml",
".prettierrc",
".prettierrc.json",
".prettierrc.yaml",
".prettierrc.yml",
".swcrc",
"angular.json",
"app.json",
"astro.config.mjs",
"astro.config.ts",
"babel.config.js",
"babel.config.json",
"Cargo.lock",
"Cargo.toml",
"composer.json",
"docker-compose.yaml",
"docker-compose.yml",
"eslint.config.cjs",
"eslint.config.js",
"eslint.config.mjs",
"go.mod",
"go.sum",
"jsconfig.json",
"lerna.json",
"manifest.json",
"next.config.js",
"next.config.mjs",
"next.config.ts",
"nuxt.config.js",
"nuxt.config.ts",
"nx.json",
"package-lock.json",
"package.json",
"playwright.config.js",
"playwright.config.ts",
"postcss.config.cjs",
"postcss.config.js",
"prettier.config.cjs",
"prettier.config.js",
"prettier.config.mjs",
"project.json",
"pyproject.toml",
"rollup.config.js",
"rollup.config.mjs",
"rollup.config.ts",
"svelte.config.js",
"stylelintrc.json",
"tailwind.config.js",
"tailwind.config.ts",
"tsconfig.app.json",
"tsconfig.json",
"tsconfig.node.json",
"tslint.json",
"turbo.json",
"vite.config.js",
"vite.config.mjs",
"vite.config.ts",
"vitest.config.js",
"vitest.config.ts",
"webpack.config.js",
"webpack.config.ts",
"workspace.json",
}
FRONTEND_EXTS = {
".vue",
".ts",
".tsx",
".js",
".jsx",
".mjs",
".css",
".scss",
".sass",
".less",
".html",
".svelte",
".astro",
}
SUPPLEMENT_CODE_EXTS = {
".py",
".java",
".go",
".rs",
".cs",
".php",
".rb",
".kt",
".swift",
".sql",
".sh",
".json",
}
COPYRIGHT_CODE_EXTS = FRONTEND_EXTS | SUPPLEMENT_CODE_EXTS
LOCK_FILES = {
"package-lock.json",
"pnpm-lock.yaml",
"yarn.lock",
"bun.lockb",
"bun.lock",
}
def repo_root_from_script() -> Path:
return Path(__file__).resolve().parents[3]
def is_excluded(path: Path) -> bool:
parts = set(path.parts)
if parts & EXCLUDE_DIRS:
return True
name = path.name
if name.startswith(".") and name not in {".env.example"}:
return True
if name in LOCK_FILES:
return True
if name.endswith(".map") or name.endswith(".min.js") or name.endswith(".min.css"):
return True
return False
def iter_project_files(project: Path, exts: set[str] | None = None) -> Iterable[Path]:
project = project.resolve()
for root, dirs, files in os.walk(project):
root_path = Path(root)
dirs[:] = [d for d in dirs if not is_excluded(root_path / d)]
for filename in files:
path = root_path / filename
if is_excluded(path):
continue
if exts is not None and path.suffix.lower() not in exts:
continue
yield path
def rel(path: Path, root: Path) -> str:
return path.resolve().relative_to(root.resolve()).as_posix()
def read_text(path: Path, limit: int | None = None) -> str:
data = path.read_bytes()
if limit is not None:
data = data[:limit]
for encoding in ("utf-8", "utf-8-sig", "gb18030", "latin-1"):
try:
return data.decode(encoding)
except UnicodeDecodeError:
continue
return data.decode("utf-8", errors="replace")
def read_json(path: Path) -> dict[str, Any]:
return json.loads(read_text(path))
def write_json(path: Path, data: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
def count_text_lines(path: Path, skip_blank: bool = True) -> int:
try:
text = read_text(path)
except Exception:
return 0
if not text:
return 0
if skip_blank:
return sum(1 for line in text.splitlines() if line.strip())
return len(text.splitlines())
def is_known_config_file(path: Path) -> bool:
"""Return True for well-known config files that shouldn't count as source code."""
return path.name in KNOWN_CONFIG_FILES
def looks_binary(path: Path) -> bool:
try:
chunk = path.read_bytes()[:4096]
except Exception:
return True
return b"\x00" in chunk
def normalize_title(value: str) -> str:
value = re.sub(r"[-_]+", " ", value).strip()
value = re.sub(r"\s+", " ", value)
return value or "待命名软件"
def safe_filename(value: str) -> str:
value = re.sub(r'[\\/:*?"<>|]+', "_", value).strip()
return value or "软件"
def ensure_dir(path: Path) -> Path:
path.mkdir(parents=True, exist_ok=True)
return path