#!/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 tables. Complex rowspan/colspan is flattened. - Returns None if <2 rows or <2 cols. """ 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]: """Convert a 2D grid (with header row) into a 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 _table_to_markdown(table: Tag) -> Optional[str]: """Backward-compatible helper: HTML table -> Markdown table.""" grid = _table_to_grid(table) return _grid_to_markdown(grid) if grid else None def _find_section_table(soup: BeautifulSoup, keywords: Iterable[str]) -> Optional[Tag]: """ Find the first table under a heading (h2/h3/h4) whose text contains any keyword. """ for heading in soup.find_all(["h2", "h3", "h4"]): title = _clean_text(heading.get_text(" ", strip=True)) if not title: continue if not any(k in title for k in keywords): continue # Walk forward to the next table. 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 # Sometimes a wrapper div contains the table. if isinstance(cur, Tag): table = cur.find("table") if table is not None: return table return None def _table_header_cells(table: Tag) -> list[str]: """Return first-row cell texts for a table.""" tr = table.find("tr") if tr is None: return [] cells = tr.find_all(["th", "td"]) return [_clean_text(c.get_text(" ", strip=True)) for c in cells if _clean_text(c.get_text(" ", strip=True))] def _find_table_by_header_keywords(content: Tag, header_keywords: Iterable[str]) -> Optional[Tag]: """ Find a table by matching keywords against its header row cells. This is a fallback for pages that don't have clear '属性/事件/平台' headings in static HTML. """ kws = tuple(header_keywords) for table in content.find_all("table"): headers = _table_header_cells(table) if not headers: continue joined = " ".join(headers) if any(k in joined for k in kws): return table return None def _is_platform_support_table(headers: list[str]) -> bool: """ Heuristic: detect a platform compatibility/support table. We intentionally reject mixed 'properties' tables that contain '平台差异说明' column. """ if not headers: return False joined = " ".join(headers) # Reject common props table headers if "属性名" in joined or "默认值" in joined or "类型" in joined: return False # Accept platform-like headers if "平台" in joined and ("支持" in joined or "版本" in joined or "说明" in joined): return True return False def _find_platform_table(content: Tag) -> Optional[Tag]: """ Find platform compatibility/support table. Strategy: - Prefer section-based lookup by '平台/兼容' headings. - Fallback: scan all tables and pick the first that looks like a platform table. """ t = _find_section_table(content, keywords=("平台", "兼容", "兼容性", "Platform")) if t is not None: headers = _table_header_cells(t) if _is_platform_support_table(headers): return t for table in content.find_all("table"): headers = _table_header_cells(table) if _is_platform_support_table(headers): return table return None def _is_event_row_name(name: str) -> bool: """ Heuristic: detect event rows embedded inside a 'properties' table. Common patterns in uni-app docs: - '@scroll', '@scrolltoupper' ... - 'bindtap', 'bindgetuserinfo' ... - 'onLoad' ... (rare in component docs, but keep for robustness) """ n = (name or "").strip() if not n: return False return n.startswith("@") or n.startswith("bind") or n.startswith("on") def _split_props_and_events_grid(grid: list[list[str]]) -> tuple[Optional[list[list[str]]], Optional[list[list[str]]]]: """ Split a mixed table into props rows and events rows. If no event-like rows are found, returns (grid, None). """ if not grid or len(grid) < 2: return None, None header = grid[0] body = grid[1:] props_rows = [] event_rows = [] for row in body: first = (row[0] if row else "").strip() if _is_event_row_name(first): event_rows.append(row) else: props_rows.append(row) props_grid = [header] + props_rows if props_rows else None if event_rows: # Reuse header but rename first column to '事件名' for clarity. event_header = list(header) if event_header: event_header[0] = "事件名" events_grid = [event_header] + event_rows else: events_grid = None return props_grid, events_grid def _extract_platform_notes(content: Tag) -> list[str]: """ Extract platform-related notes as bullet points (fallback when no platform table exists). Strategy: - Look for headings containing '平台' / '兼容' and collect nearby

/