Files
health-record-mp/.agents/skills/uniapp-project/scripts/generate-uniui-block-docs.py
2026-08-20 15:26:19 +08:00

376 lines
12 KiB
Python

#!/usr/bin/env python3
"""
Generate block.md-style documentation for uni-ui components.
Outputs:
references/components/uni-ui/{component}.md
Why:
- Keep key info local to reduce token usage.
- Enforce a consistent structure similar to `mermaid/examples/block.md`:
Instructions → Syntax → Examples → Reference
Usage:
python3 scripts/generate-uniui-block-docs.py
python3 scripts/generate-uniui-block-docs.py --only uni-badge,uni-icons
python3 scripts/generate-uniui-block-docs.py --skip uni-badge
"""
from __future__ import annotations
import argparse
import re
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Optional, Sequence
import requests
from bs4 import BeautifulSoup, Tag
@dataclass(frozen=True)
class ComponentPage:
"""uni-ui component page parsed result."""
name: str
url: str
title: str
intro_paragraphs: list[str]
properties_table_md: Optional[str]
events_table_md: Optional[str]
platform_table_md: Optional[str]
platform_notes: list[str]
example_blocks: list[tuple[str, str]] # (lang, code)
def _clean_text(s: str) -> str:
"""Normalize whitespace for human-readable text blocks."""
return re.sub(r"\s+", " ", s or "").strip()
def _looks_like_single_token_label(s: str) -> bool:
"""Filter nav/platform labels accidentally captured as paragraphs."""
if not s or len(s) > 16:
return False
return re.fullmatch(r"[A-Za-z0-9.+-]+", s) is not None
def _guess_code_lang(code_tag: Tag) -> str:
"""Guess fenced code language from CSS classes."""
classes = " ".join(code_tag.get("class", [])).lower()
if "language-vue" in classes:
return "vue"
if "language-html" in classes:
return "html"
if "language-javascript" in classes or "language-js" in classes:
return "javascript"
if "language-typescript" in classes or "language-ts" in classes:
return "typescript"
return "vue"
def _table_to_grid(table: Tag) -> Optional[list[list[str]]]:
"""HTML table -> normalized 2D grid."""
rows = table.find_all("tr")
grid: list[list[str]] = []
for tr in rows:
cells = tr.find_all(["th", "td"])
if not cells:
continue
grid.append([_clean_text(c.get_text(" ", strip=True)) for c in cells])
if len(grid) < 2:
return None
max_cols = max(len(r) for r in grid)
if max_cols < 2:
return None
return [r + [""] * (max_cols - len(r)) for r in grid]
def _grid_to_markdown(grid: Sequence[Sequence[str]]) -> Optional[str]:
"""2D grid -> markdown table."""
if not grid or len(grid) < 2:
return None
max_cols = max(len(r) for r in grid)
if max_cols < 2:
return None
norm = [list(r) + [""] * (max_cols - len(r)) for r in grid]
header = norm[0]
aligns = ["---"] * max_cols
out = []
out.append("| " + " | ".join(header) + " |")
out.append("| " + " | ".join(aligns) + " |")
for r in norm[1:]:
out.append("| " + " | ".join(r) + " |")
return "\n".join(out)
def _find_section_table(content: Tag, keywords: Iterable[str]) -> Optional[Tag]:
"""Find a table under a heading containing keywords."""
for heading in content.find_all(["h2", "h3", "h4"]):
ht = _clean_text(heading.get_text(" ", strip=True))
if not ht:
continue
if not any(k in ht for k in keywords):
continue
cur = heading
for _ in range(20):
cur = cur.find_next_sibling()
if cur is None:
break
if isinstance(cur, Tag) and cur.name == "table":
return cur
if isinstance(cur, Tag):
t = cur.find("table")
if t is not None:
return t
return None
def _table_header_cells(table: Tag) -> list[str]:
tr = table.find("tr")
if tr is None:
return []
return [_clean_text(c.get_text(" ", strip=True)) for c in tr.find_all(["th", "td"])]
def _is_platform_support_table(headers: list[str]) -> bool:
joined = " ".join(headers)
if "属性名" in joined or "默认值" in joined or "类型" in joined:
return False
if "平台" in joined and ("支持" in joined or "版本" in joined or "说明" in joined):
return True
return False
def _find_platform_table(content: Tag) -> Optional[Tag]:
t = _find_section_table(content, keywords=("平台", "兼容", "兼容性", "Platform"))
if t is not None and _is_platform_support_table(_table_header_cells(t)):
return t
for table in content.find_all("table"):
if _is_platform_support_table(_table_header_cells(table)):
return table
return None
def _extract_platform_notes(content: Tag) -> list[str]:
notes: list[str] = []
for heading in content.find_all(["h2", "h3", "h4"]):
ht = _clean_text(heading.get_text(" ", strip=True))
if ("平台" not in ht) and ("兼容" not in ht):
continue
cur = heading
for _ in range(15):
cur = cur.find_next_sibling()
if cur is None:
break
if isinstance(cur, Tag) and cur.name in ("h2", "h3", "h4"):
break
if isinstance(cur, Tag) and cur.name in ("p", "li"):
txt = _clean_text(cur.get_text(" ", strip=True))
if txt and not _looks_like_single_token_label(txt):
notes.append(txt)
if len(notes) >= 8:
break
seen = set()
uniq = []
for n in notes:
if n in seen:
continue
seen.add(n)
uniq.append(n)
return uniq[:8]
def fetch_uniui_page(name: str, timeout_s: int = 25) -> ComponentPage:
"""Fetch and parse uni-ui component page."""
url = f"https://uniapp.dcloud.net.cn/component/uniui/{name}.html"
resp = requests.get(
url,
headers={
"User-Agent": "uniapp-develop-skills-docgen/1.0 (+local skill generator)",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
},
timeout=timeout_s,
)
resp.raise_for_status()
soup = BeautifulSoup(resp.text, "html.parser")
container = soup.select_one(".theme-default-content, .content__default")
content = container if container is not None else soup
# Title: prefer component name for stability.
title = name
# Intro: first 3 paragraphs from content
intro: list[str] = []
for p in content.find_all("p")[:12]:
txt = _clean_text(p.get_text(" ", strip=True))
if txt and not _looks_like_single_token_label(txt):
intro.append(txt)
if len(intro) >= 3:
break
props_table = _find_section_table(content, keywords=("属性", "Properties", "props", "Props"))
props_md = _grid_to_markdown(_table_to_grid(props_table)) if props_table else None
events_table = _find_section_table(content, keywords=("事件", "Events", "Event"))
events_md = _grid_to_markdown(_table_to_grid(events_table)) if events_table else None
platform_table = _find_platform_table(content)
platform_md = _grid_to_markdown(_table_to_grid(platform_table)) if platform_table else None
platform_notes = _extract_platform_notes(content) if not platform_md else []
# Examples: collect up to 6 code blocks
examples: list[tuple[str, str]] = []
for code in content.select("pre code"):
txt = code.get_text("", strip=False).strip()
if len(txt) < 40:
continue
txt = re.sub(r"\n?复制代码\s*$", "", txt).rstrip()
examples.append((_guess_code_lang(code), txt))
if len(examples) >= 6:
break
return ComponentPage(
name=name,
url=url,
title=title,
intro_paragraphs=intro,
properties_table_md=props_md,
events_table_md=events_md,
platform_table_md=platform_md,
platform_notes=platform_notes,
example_blocks=examples,
)
def render_block_style_md(page: ComponentPage, repo_root: Path) -> str:
"""Render markdown in block.md-like structure."""
name = page.name
url = page.url
# Links
plugin_name = name.replace("uni-", "")
plugin_url = f"https://ext.dcloud.net.cn/plugin?name={plugin_name}"
local_example = repo_root / "examples" / "uni-ui" / f"{name}.vue"
lines: list[str] = []
lines.append(f"# {name}")
lines.append("")
lines.append("## Instructions")
lines.append("")
if page.intro_paragraphs:
for p in page.intro_paragraphs:
lines.append(p)
lines.append("")
else:
lines.append(f"`{name}` 是 uni-ui 扩展组件。")
lines.append("")
lines.append("### Syntax")
lines.append("")
lines.append(f"- 使用 `<{name} />`(或 `<{name}></{name}>`,当需要包裹子节点时)。")
lines.append("- 遇到平台差异时,建议使用条件编译(`#ifdef / #endif`)显式处理。")
lines.append("")
lines.append("#### Properties")
lines.append("")
if page.properties_table_md:
lines.append(page.properties_table_md)
else:
lines.append(f"See official docs for full properties list: `{url}`")
lines.append("")
lines.append("#### Events")
lines.append("")
if page.events_table_md:
lines.append(page.events_table_md)
else:
lines.append(f"See official docs for full events list: `{url}`")
lines.append("")
lines.append("#### Platform Compatibility")
lines.append("")
if page.platform_table_md:
lines.append(page.platform_table_md)
elif page.platform_notes:
for n in page.platform_notes:
lines.append(f"- {n}")
else:
lines.append(f"See official docs for platform support table: `{url}`")
lines.append("")
lines.append("### Examples")
lines.append("")
if page.example_blocks:
for idx, (lang, code) in enumerate(page.example_blocks, start=1):
lines.append(f"### Example (Example {idx})")
lines.append("")
lines.append(f"```{lang}")
lines.append(code.rstrip())
lines.append("```")
lines.append("")
else:
lines.append(f"Examples are available in the official docs: `{url}`")
lines.append("")
lines.append("Reference:")
lines.append(f"- [Official Documentation]({url})")
lines.append(f"- [Plugin Marketplace]({plugin_url})")
if local_example.exists():
rel = local_example.relative_to(repo_root)
lines.append(f"- [Local Example]({rel.as_posix()})")
lines.append("")
return "\n".join(lines)
def _parse_list_arg(s: str) -> list[str]:
return [p.strip() for p in (s or "").split(",") if p.strip()]
def main() -> int:
parser = argparse.ArgumentParser(description="Generate uni-ui component docs in block.md style.")
parser.add_argument("--only", type=str, default="", help="Comma-separated component names to generate.")
parser.add_argument("--skip", type=str, default="", help="Comma-separated component names to skip.")
parser.add_argument("--dry-run", action="store_true", help="Do not write files.")
parser.add_argument("--sleep", type=float, default=0.2, help="Sleep between requests (seconds).")
args = parser.parse_args()
repo_root = Path(__file__).resolve().parents[1]
out_dir = repo_root / "references" / "components" / "uni-ui"
if not out_dir.exists():
raise SystemExit(f"Output dir not found: {out_dir}")
targets = sorted(p.stem for p in out_dir.glob("*.md"))
only = set(_parse_list_arg(args.only))
skip = set(_parse_list_arg(args.skip))
if only:
targets = [t for t in targets if t in only]
if skip:
targets = [t for t in targets if t not in skip]
if not targets:
print("No targets found.")
return 0
for name in targets:
print(f"[fetch] {name}")
page = fetch_uniui_page(name)
md = render_block_style_md(page, repo_root=repo_root)
out_file = out_dir / f"{name}.md"
if args.dry_run:
print(f"[dry-run] would write {out_file} ({len(md)} chars)")
else:
out_file.write_text(md, encoding="utf-8")
print(f"[write] {out_file}")
time.sleep(max(args.sleep, 0.0))
return 0
if __name__ == "__main__":
raise SystemExit(main())