244 lines
7.3 KiB
Python
244 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import unicodedata
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import List
|
|
|
|
|
|
LABEL_BG = "#555555"
|
|
DEFAULT_TOTAL_COLOR = "#1f6feb"
|
|
DEFAULT_FILES_COLOR = "#2da44e"
|
|
DEFAULT_LANGUAGE_COUNT_COLOR = "#8250df"
|
|
TEXT_COLOR = "#ffffff"
|
|
|
|
|
|
@dataclass
|
|
class LanguageStat:
|
|
lang_id: str
|
|
display_name: str
|
|
code_lines: int
|
|
formatted_lines: str
|
|
file_count: int
|
|
color: str
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--output-dir", required=True)
|
|
parser.add_argument("--readme-path", required=True)
|
|
parser.add_argument("--repo", required=True)
|
|
parser.add_argument("--generated-by", required=True)
|
|
parser.add_argument("--updated-at", required=True)
|
|
parser.add_argument("--total-color", default=DEFAULT_TOTAL_COLOR)
|
|
parser.add_argument("--files-color", default=DEFAULT_FILES_COLOR)
|
|
parser.add_argument("--language-count-color", default=DEFAULT_LANGUAGE_COUNT_COLOR)
|
|
parser.add_argument("--total-code", required=True, type=int)
|
|
parser.add_argument("--formatted-total-code", required=True)
|
|
parser.add_argument("--total-files", required=True, type=int)
|
|
parser.add_argument("--formatted-total-files", required=True)
|
|
parser.add_argument("--language-count", required=True, type=int)
|
|
parser.add_argument("--exclude-dirs", required=True)
|
|
parser.add_argument("--min-lines-threshold", required=True, type=int)
|
|
parser.add_argument("--lang-summary", required=True)
|
|
return parser.parse_args()
|
|
|
|
|
|
def escape_xml(value: str) -> str:
|
|
return (
|
|
value.replace("&", "&")
|
|
.replace("<", "<")
|
|
.replace(">", ">")
|
|
.replace('"', """)
|
|
.replace("'", "'")
|
|
)
|
|
|
|
|
|
def display_units(value: str) -> int:
|
|
units = 0
|
|
for char in value:
|
|
if unicodedata.east_asian_width(char) in {"F", "W"}:
|
|
units += 2
|
|
else:
|
|
units += 1
|
|
return units
|
|
|
|
|
|
def text_width(value: str) -> int:
|
|
return display_units(value) * 7 + 20
|
|
|
|
|
|
def normalize_color(value: str, fallback: str) -> str:
|
|
if not value:
|
|
return fallback
|
|
return value if value.startswith("#") else f"#{value}"
|
|
|
|
|
|
def parse_language_summary(path: Path) -> List[LanguageStat]:
|
|
stats: List[LanguageStat] = []
|
|
|
|
if not path.exists():
|
|
return stats
|
|
|
|
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
|
line = raw_line.strip()
|
|
if not line:
|
|
continue
|
|
|
|
parts = line.split("|")
|
|
if len(parts) < 6:
|
|
continue
|
|
|
|
while len(parts) < 7:
|
|
parts.append("")
|
|
|
|
lang_id, display_name, code_lines, formatted_lines, file_count, color, _icon = parts[:7]
|
|
stats.append(
|
|
LanguageStat(
|
|
lang_id=lang_id,
|
|
display_name=display_name,
|
|
code_lines=int(code_lines),
|
|
formatted_lines=formatted_lines,
|
|
file_count=int(file_count),
|
|
color=normalize_color(color, DEFAULT_TOTAL_COLOR),
|
|
)
|
|
)
|
|
|
|
return stats
|
|
|
|
|
|
def badge_svg(label: str, message: str, color: str) -> str:
|
|
height = 20
|
|
left_width = max(46, text_width(label))
|
|
right_width = max(46, text_width(message))
|
|
total_width = left_width + right_width
|
|
left_text_x = left_width / 2
|
|
right_text_x = left_width + right_width / 2
|
|
|
|
return f"""<svg xmlns="http://www.w3.org/2000/svg" width="{total_width}" height="{height}" role="img" aria-label="{escape_xml(label)}: {escape_xml(message)}">
|
|
<title>{escape_xml(label)}: {escape_xml(message)}</title>
|
|
<rect width="{left_width}" height="{height}" fill="{LABEL_BG}" />
|
|
<rect x="{left_width}" width="{right_width}" height="{height}" fill="{escape_xml(color)}" />
|
|
<rect width="{total_width}" height="{height}" rx="3" fill="transparent" />
|
|
<g fill="{TEXT_COLOR}" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
|
|
<text x="{left_text_x:.1f}" y="14">{escape_xml(label)}</text>
|
|
<text x="{right_text_x:.1f}" y="14">{escape_xml(message)}</text>
|
|
</g>
|
|
</svg>
|
|
"""
|
|
|
|
|
|
def write_badge(path: Path, label: str, message: str, color: str) -> None:
|
|
path.write_text(badge_svg(label, message, color), encoding="utf-8")
|
|
|
|
|
|
def render_readme(args: argparse.Namespace, languages: List[LanguageStat], badge_dir_name: str) -> str:
|
|
language_rows = []
|
|
|
|
for stat in languages:
|
|
share = (stat.code_lines * 100 / args.total_code) if args.total_code else 0.0
|
|
language_rows.append(
|
|
f"| **{stat.display_name}** | {stat.formatted_lines} | {stat.file_count} | {share:.1f}% | "
|
|
f" |"
|
|
)
|
|
|
|
if language_rows:
|
|
language_table = "\n".join(language_rows)
|
|
else:
|
|
language_table = "| _未找到符合条件的语言_ | - | - | - | - |"
|
|
|
|
return f"""# 📊 代码统计详细报告
|
|
|
|
> 此分支由 {args.generated_by} 自动生成和维护
|
|
>
|
|
> ⚠️ **请勿手动修改此分支的内容!**
|
|
>
|
|
> 🤖 由 {args.generated_by} 自动生成
|
|
>
|
|
> 📅 更新时间: {args.updated_at}
|
|
|
|

|
|

|
|

|
|
|
|
## 📈 总体统计
|
|
|
|
| 统计项 | 数值 | 徽章 |
|
|
|--------|------|------|
|
|
| 💻 总代码行数 | **{args.formatted_total_code}** 行 |  |
|
|
| 📁 总文件数 | **{args.formatted_total_files}** 个 |  |
|
|
| 🌐 语言种类 | **{args.language_count}** 种 |  |
|
|
|
|
## 🌈 语言分布
|
|
|
|
| 语言 | 代码行数 | 文件数 | 占比 | 徽章 |
|
|
|------|----------|--------|------|------|
|
|
{language_table}
|
|
|
|
---
|
|
|
|
## ⚙️ 配置说明
|
|
|
|
### Token 配置
|
|
|
|
- 当前使用: **WORKFLOW**
|
|
- 需要在 Settings -> Actions -> Secrets 中配置 `WORKFLOW`
|
|
|
|
### 排除规则
|
|
|
|
当前排除的目录:
|
|
```
|
|
{args.exclude_dirs}
|
|
```
|
|
|
|
### 阈值设置
|
|
|
|
- 最小代码行数阈值: **{args.min_lines_threshold}** 行
|
|
- 低于此阈值的语言不会生成徽章
|
|
|
|
---
|
|
|
|
*📊 统计仓库:`{args.repo}`*
|
|
"""
|
|
|
|
|
|
def main() -> None:
|
|
args = parse_args()
|
|
output_dir = Path(args.output_dir)
|
|
readme_path = Path(args.readme_path)
|
|
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
readme_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
languages = parse_language_summary(Path(args.lang_summary))
|
|
badge_dir_name = output_dir.name
|
|
|
|
write_badge(
|
|
output_dir / "total-lines.svg",
|
|
"代码",
|
|
f"{args.formatted_total_code} 行",
|
|
normalize_color(args.total_color, DEFAULT_TOTAL_COLOR),
|
|
)
|
|
write_badge(
|
|
output_dir / "total-files.svg",
|
|
"文件",
|
|
f"{args.formatted_total_files} 个",
|
|
normalize_color(args.files_color, DEFAULT_FILES_COLOR),
|
|
)
|
|
write_badge(
|
|
output_dir / "language-count.svg",
|
|
"语言",
|
|
f"{args.language_count} 种",
|
|
normalize_color(args.language_count_color, DEFAULT_LANGUAGE_COUNT_COLOR),
|
|
)
|
|
|
|
for stat in languages:
|
|
write_badge(output_dir / f"{stat.lang_id}-lines.svg", stat.display_name, f"{stat.formatted_lines} 行", stat.color)
|
|
|
|
readme_path.write_text(render_readme(args, languages, badge_dir_name), encoding="utf-8")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|