🐛 fix(stats): switch badges to self-hosted svg
Ubuntu System Information / show-system-info (push) Successful in 0s
Ubuntu System Information / show-system-info (push) Successful in 0s
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user