#!/usr/bin/env python3 """ Generate block.md-style documentation for uni-app built-in components. Why this exists: - The skill aims to reduce token usage by keeping key component knowledge local. - The `mermaid/examples/block.md` format uses: Instructions → Syntax → Examples → Reference. - Most built-in component docs were placeholders. This script fetches official pages and generates consistent, detailed local docs under `references/components/built-in/`. Usage: python3 scripts/generate-builtin-block-docs.py python3 scripts/generate-builtin-block-docs.py --only view,button,input python3 scripts/generate-builtin-block-docs.py --dry-run """ 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: """Component page parsed result.""" name: str url: str title: str intro_paragraphs: list[str] properties_table_md: Optional[str] events_table_md: Optional[str] slots_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.""" s = re.sub(r"\s+", " ", s or "").strip() return s def _looks_like_single_token_label(s: str) -> bool: """ Heuristic filter for navigation/platform labels accidentally captured as paragraphs. Examples: 'HarmonyOS', 'HBuilderX', 'App', 'H5' """ if not s: return False if 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, defaulting to vue.""" 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" if "language-css" in classes or "language-scss" in classes: return "css" return "vue" def _table_to_grid(table: Tag) -> Optional[list[list[str]]]: """ Convert a HTML table into a 2D grid (rows x cols). Notes: - Handles simple
/
. intro: list[str] = [] if h1: cur = h1 for _ in range(50): cur = cur.find_next() if cur is None: break if isinstance(cur, Tag) and cur.name in ("h2", "h3"): break if isinstance(cur, Tag) and cur.name == "p": txt = _clean_text(cur.get_text(" ", strip=True)) if txt: intro.append(txt) if len(intro) >= 4: break if not intro: for p in content.find_all("p")[:10]: 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 # Tables (prefer section-based, fallback to header-based). props_table = _find_section_table(content, keywords=("属性", "properties", "Props", "prop")) if props_table is None: props_table = _find_table_by_header_keywords(content, header_keywords=("属性名", "属性", "默认值", "类型")) events_table = _find_section_table(content, keywords=("事件", "events", "Event")) if events_table is None: events_table = _find_table_by_header_keywords(content, header_keywords=("事件名", "事件名称", "回调", "参数")) slots_table = _find_section_table(content, keywords=("插槽", "slot", "slots")) platform_table = _find_platform_table(content) # Prefer explicit events table; if missing, try to split mixed props/events table. props_md = None events_md = None if props_table: grid = _table_to_grid(props_table) if grid: props_grid, embedded_events_grid = _split_props_and_events_grid(grid) props_md = _grid_to_markdown(props_grid) if props_grid else None if embedded_events_grid and events_table is None: events_md = _grid_to_markdown(embedded_events_grid) else: props_md = _table_to_markdown(props_table) if events_table: events_md = _table_to_markdown(events_table) or events_md slots_md = _table_to_markdown(slots_table) if slots_table else None platform_md = _table_to_markdown(platform_table) if platform_table else None platform_notes = _extract_platform_notes(content) if not platform_md else [] # Examples: collect up to 6 non-trivial code blocks. examples: list[tuple[str, str]] = [] for code in content.select("pre code"): # Important: avoid adding separators between syntax highlight token spans. # Using separator "" keeps original newlines but doesn't inject newlines # between adjacent text nodes. txt = code.get_text("", strip=False).strip() if len(txt) < 40: continue # Remove trailing "复制代码" if present in text nodes txt = re.sub(r"\n?复制代码\s*$", "", txt).rstrip() lang = _guess_code_lang(code) examples.append((lang, 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, slots_table_md=slots_md, platform_table_md=platform_md, platform_notes=platform_notes, example_blocks=examples, ) def render_block_style_md(page: ComponentPage) -> str: """ Render markdown roughly aligned with `mermaid/examples/block.md` structure: - Instructions - Syntax (with properties/events/platform tables) - Examples (multiple) - Reference """ title = page.title or page.name lines: list[str] = [] lines.append(f"# {title}") lines.append("") lines.append("## Instructions") lines.append("") if page.intro_paragraphs: for p in page.intro_paragraphs[:4]: lines.append(p) lines.append("") else: lines.append(f"`{page.name}` 是 uni-app 内置组件。") lines.append("") lines.append("### Syntax") lines.append("") lines.append(f"- 使用 `<{page.name} />`(或 `<{page.name}>{page.name}>`,当需要包裹子节点时)。") lines.append("- 遇到平台差异时,建议使用条件编译(`#ifdef / #endif`)显式处理。") lines.append("") if page.properties_table_md: lines.append("#### Properties") lines.append("") lines.append(page.properties_table_md) lines.append("") else: lines.append("#### Properties") lines.append("") lines.append(f"See official docs for full properties list: `{page.url}`") lines.append("") if page.events_table_md: lines.append("#### Events") lines.append("") lines.append(page.events_table_md) lines.append("") else: lines.append("#### Events") lines.append("") lines.append(f"See official docs for full events list: `{page.url}`") lines.append("") if page.slots_table_md: lines.append("#### Slots") lines.append("") lines.append(page.slots_table_md) lines.append("") if page.platform_table_md: lines.append("#### Platform Compatibility") lines.append("") lines.append(page.platform_table_md) lines.append("") else: lines.append("#### Platform Compatibility") lines.append("") if page.platform_notes: for n in page.platform_notes: lines.append(f"- {n}") else: lines.append(f"See official docs for platform support table: `{page.url}`") lines.append("") lines.append("### Examples") lines.append("") if not page.example_blocks: lines.append(f"Examples are available in the official docs: `{page.url}`") lines.append("") else: 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("") lines.append(f"Reference: [Official Documentation]({page.url})") lines.append("") return "\n".join(lines) def _parse_only_arg(s: str) -> list[str]: """Parse --only 'a,b,c' argument.""" items = [] for part in (s or "").split(","): part = part.strip() if part: items.append(part) return items def _parse_skip_arg(s: str) -> set[str]: """Parse --skip 'a,b,c' argument.""" return set(_parse_only_arg(s)) def main() -> int: """CLI entrypoint.""" parser = argparse.ArgumentParser(description="Generate built-in 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; just print what would happen.") 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" / "built-in" if not out_dir.exists(): raise SystemExit(f"Output dir not found: {out_dir}") only = set(_parse_only_arg(args.only)) skip = _parse_skip_arg(args.skip) targets = sorted(p.stem for p in out_dir.glob("*.md")) 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_component_page(name) md = render_block_style_md(page) 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())