#!/usr/bin/env python3 """ Docsify-Style HTML Manual Assembly Script Template Generates interactive single-file documentation with hierarchical navigation Usage: 1. Copy this script to your manual output directory 2. Customize MANUAL_META and NAV_STRUCTURE 3. Run: python assemble_docsify.py """ import json import base64 import re from pathlib import Path from typing import Dict, List, Any # Try to import markdown library try: import markdown from markdown.extensions.codehilite import CodeHiliteExtension from markdown.extensions.fenced_code import FencedCodeExtension from markdown.extensions.tables import TableExtension from markdown.extensions.toc import TocExtension HAS_MARKDOWN = True except ImportError: HAS_MARKDOWN = False print("Warning: markdown library not found. Install with: pip install markdown pygments") # ============================================================ # CONFIGURATION - Customize these for your project # ============================================================ # Paths - Update these paths for your environment BASE_DIR = Path(__file__).parent SECTIONS_DIR = BASE_DIR / "sections" SCREENSHOTS_DIR = BASE_DIR / "screenshots" # Template paths - Point to skill templates directory SKILL_DIR = Path(__file__).parent.parent # Adjust based on where script is placed TEMPLATE_FILE = SKILL_DIR / "templates" / "docsify-shell.html" CSS_BASE_FILE = SKILL_DIR / "templates" / "css" / "docsify-base.css" # Manual metadata - Customize for your software MANUAL_META = { "title": "Your Software", "subtitle": "使用手册", "version": "v1.0.0", "timestamp": "2025-01-01", "language": "zh-CN", "logo_icon": "Y" # First letter or emoji } # Output file OUTPUT_FILE = BASE_DIR / f"{MANUAL_META['title']}{MANUAL_META['subtitle']}.html" # Hierarchical navigation structure # Customize groups and items based on your sections NAV_STRUCTURE = [ { "type": "group", "title": "入门指南", "icon": "📚", "expanded": True, "items": [ {"id": "overview", "title": "产品概述", "file": "section-overview.md"}, ] }, { "type": "group", "title": "使用教程", "icon": "🎯", "expanded": False, "items": [ {"id": "ui-guide", "title": "UI操作指南", "file": "section-ui-guide.md"}, ] }, { "type": "group", "title": "API参考", "icon": "🔧", "expanded": False, "items": [ {"id": "api-reference", "title": "API文档", "file": "section-api-reference.md"}, ] }, { "type": "group", "title": "配置与部署", "icon": "⚙️", "expanded": False, "items": [ {"id": "configuration", "title": "配置指南", "file": "section-configuration.md"}, ] }, { "type": "group", "title": "帮助与支持", "icon": "💡", "expanded": False, "items": [ {"id": "troubleshooting", "title": "故障排除", "file": "section-troubleshooting.md"}, {"id": "examples", "title": "代码示例", "file": "section-examples.md"}, ] } ] # Screenshot ID to filename mapping - Customize for your screenshots SCREENSHOT_MAPPING = { # "截图ID": "filename.png", } # ============================================================ # CORE FUNCTIONS - Generally don't need to modify # ============================================================ # Global cache for embedded images _embedded_images = {} def read_file(filepath: Path) -> str: """Read file content with UTF-8 encoding""" return filepath.read_text(encoding='utf-8') # ============================================================ # MERMAID VALIDATION # ============================================================ # Valid Mermaid diagram types MERMAID_DIAGRAM_TYPES = [ 'graph', 'flowchart', 'sequenceDiagram', 'classDiagram', 'stateDiagram', 'stateDiagram-v2', 'erDiagram', 'journey', 'gantt', 'pie', 'quadrantChart', 'requirementDiagram', 'gitGraph', 'mindmap', 'timeline', 'zenuml', 'sankey-beta', 'xychart-beta', 'block-beta' ] # Common Mermaid syntax patterns MERMAID_PATTERNS = { 'graph': r'^graph\s+(TB|BT|LR|RL|TD)\s*$', 'flowchart': r'^flowchart\s+(TB|BT|LR|RL|TD)\s*$', 'sequenceDiagram': r'^sequenceDiagram\s*$', 'classDiagram': r'^classDiagram\s*$', 'stateDiagram': r'^stateDiagram(-v2)?\s*$', 'erDiagram': r'^erDiagram\s*$', 'gantt': r'^gantt\s*$', 'pie': r'^pie\s*(showData|title\s+.*)?\s*$', 'journey': r'^journey\s*$', } class MermaidBlock: """Represents a mermaid code block found in markdown""" def __init__(self, content: str, file: str, line_num: int, indented: bool = False): self.content = content self.file = file self.line_num = line_num self.indented = indented self.errors: List[str] = [] self.warnings: List[str] = [] self.diagram_type: str = None def __repr__(self): return f"MermaidBlock({self.diagram_type}, {self.file}:{self.line_num})" def extract_mermaid_blocks(markdown_text: str, filename: str) -> List[MermaidBlock]: """Extract all mermaid code blocks from markdown text""" blocks = [] # More flexible pattern - matches opening fence with optional indent, # then captures content until closing fence (with any indent) pattern = r'^(\s*)(```|~~~)mermaid\s*\n(.*?)\n\s*\2\s*$' for match in re.finditer(pattern, markdown_text, re.MULTILINE | re.DOTALL): indent = match.group(1) content = match.group(3) # Calculate line number line_num = markdown_text[:match.start()].count('\n') + 1 indented = len(indent) > 0 block = MermaidBlock( content=content, file=filename, line_num=line_num, indented=indented ) blocks.append(block) return blocks def validate_mermaid_block(block: MermaidBlock) -> bool: """Validate a mermaid block and populate errors/warnings""" content = block.content.strip() lines = content.split('\n') if not lines: block.errors.append("Empty mermaid block") return False first_line = lines[0].strip() # Detect diagram type for dtype in MERMAID_DIAGRAM_TYPES: if first_line.startswith(dtype): block.diagram_type = dtype break if not block.diagram_type: block.errors.append(f"Unknown diagram type: '{first_line[:30]}...'") block.errors.append(f"Valid types: {', '.join(MERMAID_DIAGRAM_TYPES[:8])}...") return False # Check for balanced brackets/braces brackets = {'[': ']', '{': '}', '(': ')'} stack = [] for i, char in enumerate(content): if char in brackets: stack.append((char, i)) elif char in brackets.values(): if not stack: block.errors.append(f"Unmatched closing bracket '{char}' at position {i}") else: open_char, _ = stack.pop() if brackets[open_char] != char: block.errors.append(f"Mismatched brackets: '{open_char}' and '{char}'") if stack: for open_char, pos in stack: block.warnings.append(f"Unclosed bracket '{open_char}' at position {pos}") # Check for common graph/flowchart issues if block.diagram_type in ['graph', 'flowchart']: # Check direction specifier if not re.match(r'^(graph|flowchart)\s+(TB|BT|LR|RL|TD)', first_line): block.warnings.append("Missing or invalid direction (TB/BT/LR/RL/TD)") # Check for arrow syntax arrow_count = content.count('-->') + content.count('---') + content.count('-.->') + content.count('==>') if arrow_count == 0 and len(lines) > 1: block.warnings.append("No arrows found - graph may be incomplete") # Check for sequenceDiagram issues if block.diagram_type == 'sequenceDiagram': if '->' not in content and '->>' not in content: block.warnings.append("No message arrows found in sequence diagram") # Indentation warning if block.indented: block.warnings.append("Indented code block - may not render in some markdown parsers") return len(block.errors) == 0 def validate_all_mermaid(nav_structure: List[Dict], sections_dir: Path) -> Dict[str, Any]: """Validate all mermaid blocks in all section files""" report = { 'total_blocks': 0, 'valid_blocks': 0, 'error_blocks': 0, 'warning_blocks': 0, 'blocks': [], 'by_file': {}, 'by_type': {} } for group in nav_structure: for item in group.get("items", []): section_file = item.get("file") if not section_file: continue filepath = sections_dir / section_file if not filepath.exists(): continue content = read_file(filepath) blocks = extract_mermaid_blocks(content, section_file) file_report = {'blocks': [], 'errors': 0, 'warnings': 0} for block in blocks: report['total_blocks'] += 1 is_valid = validate_mermaid_block(block) if is_valid: report['valid_blocks'] += 1 else: report['error_blocks'] += 1 file_report['errors'] += 1 if block.warnings: report['warning_blocks'] += 1 file_report['warnings'] += len(block.warnings) # Track by diagram type if block.diagram_type: if block.diagram_type not in report['by_type']: report['by_type'][block.diagram_type] = 0 report['by_type'][block.diagram_type] += 1 report['blocks'].append(block) file_report['blocks'].append(block) if blocks: report['by_file'][section_file] = file_report return report def print_mermaid_report(report: Dict[str, Any]) -> None: """Print mermaid validation report""" print("\n" + "=" * 60) print("MERMAID DIAGRAM VALIDATION REPORT") print("=" * 60) print(f"\nSummary:") print(f" Total blocks: {report['total_blocks']}") print(f" Valid: {report['valid_blocks']}") print(f" With errors: {report['error_blocks']}") print(f" With warnings: {report['warning_blocks']}") if report['by_type']: print(f"\nDiagram Types:") for dtype, count in sorted(report['by_type'].items()): print(f" {dtype}: {count}") # Print errors and warnings has_issues = False for block in report['blocks']: if block.errors or block.warnings: if not has_issues: print(f"\nIssues Found:") has_issues = True print(f"\n [{block.file}:{block.line_num}] {block.diagram_type or 'unknown'}") for error in block.errors: print(f" [ERROR] {error}") for warning in block.warnings: print(f" [WARN] {warning}") if not has_issues: print(f"\n No issues found!") print("=" * 60 + "\n") def convert_md_to_html(markdown_text: str) -> str: """Convert Markdown to HTML with syntax highlighting""" if not HAS_MARKDOWN: # Fallback: just escape HTML and wrap in pre escaped = markdown_text.replace('&', '&').replace('<', '<').replace('>', '>') return f'
{escaped}'
md = markdown.Markdown(
extensions=[
FencedCodeExtension(),
TableExtension(),
TocExtension(toc_depth=3),
CodeHiliteExtension(
css_class='highlight',
linenums=False,
guess_lang=True,
use_pygments=True
),
],
output_format='html5'
)
html = md.convert(markdown_text)
md.reset()
return html
def embed_screenshot_base64(screenshot_id: str) -> str:
"""Embed screenshot as base64, using cache to avoid duplicates"""
global _embedded_images
filename = SCREENSHOT_MAPPING.get(screenshot_id)
if not filename:
return f'