mirror of
https://github.com/ialley-workshop-open/uni-halo.git
synced 2026-09-12 16:40:40 +08:00
refactor: 架构升级
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
#!/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())
|
||||
|
||||
Reference in New Issue
Block a user