claude-code-workflow 6.3.11 → 6.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/CLAUDE.md +33 -33
- package/.claude/agents/issue-plan-agent.md +77 -5
- package/.claude/agents/issue-queue-agent.md +122 -18
- package/.claude/commands/issue/execute.md +53 -40
- package/.claude/commands/issue/new.md +113 -11
- package/.claude/commands/issue/plan.md +112 -37
- package/.claude/commands/issue/queue.md +28 -18
- package/.claude/skills/software-manual/scripts/assemble_docsify.py +584 -0
- package/.claude/skills/software-manual/templates/css/docsify-base.css +984 -0
- package/.claude/skills/software-manual/templates/docsify-shell.html +466 -0
- package/.claude/workflows/cli-templates/schemas/issues-jsonl-schema.json +141 -168
- package/.claude/workflows/cli-templates/schemas/solution-schema.json +3 -2
- package/.codex/prompts/issue-execute.md +3 -3
- package/.codex/prompts/issue-queue.md +3 -3
- package/ccw/dist/commands/issue.d.ts.map +1 -1
- package/ccw/dist/commands/issue.js +2 -1
- package/ccw/dist/commands/issue.js.map +1 -1
- package/ccw/src/commands/issue.ts +2 -1
- package/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css +580 -467
- package/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js +532 -461
- package/ccw/src/templates/dashboard-js/components/notifications.js +774 -774
- package/ccw/src/templates/dashboard-js/i18n.js +4 -0
- package/ccw/src/templates/dashboard.html +10 -0
- package/ccw/src/tools/claude-cli-tools.ts +388 -388
- package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/config.py +19 -3
- package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/search/ranking.py +15 -4
- package/codex-lens/src/codexlens/semantic/__pycache__/vector_store.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/semantic/vector_store.py +57 -47
- package/codex-lens/src/codexlens/storage/__pycache__/registry.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/storage/registry.py +114 -101
- package/package.json +83 -83
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Docsify-Style HTML Manual Assembly Script Template
|
|
4
|
+
Generates interactive single-file documentation with hierarchical navigation
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
1. Copy this script to your manual output directory
|
|
8
|
+
2. Customize MANUAL_META and NAV_STRUCTURE
|
|
9
|
+
3. Run: python assemble_docsify.py
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import base64
|
|
14
|
+
import re
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Dict, List, Any
|
|
17
|
+
|
|
18
|
+
# Try to import markdown library
|
|
19
|
+
try:
|
|
20
|
+
import markdown
|
|
21
|
+
from markdown.extensions.codehilite import CodeHiliteExtension
|
|
22
|
+
from markdown.extensions.fenced_code import FencedCodeExtension
|
|
23
|
+
from markdown.extensions.tables import TableExtension
|
|
24
|
+
from markdown.extensions.toc import TocExtension
|
|
25
|
+
HAS_MARKDOWN = True
|
|
26
|
+
except ImportError:
|
|
27
|
+
HAS_MARKDOWN = False
|
|
28
|
+
print("Warning: markdown library not found. Install with: pip install markdown pygments")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ============================================================
|
|
32
|
+
# CONFIGURATION - Customize these for your project
|
|
33
|
+
# ============================================================
|
|
34
|
+
|
|
35
|
+
# Paths - Update these paths for your environment
|
|
36
|
+
BASE_DIR = Path(__file__).parent
|
|
37
|
+
SECTIONS_DIR = BASE_DIR / "sections"
|
|
38
|
+
SCREENSHOTS_DIR = BASE_DIR / "screenshots"
|
|
39
|
+
|
|
40
|
+
# Template paths - Point to skill templates directory
|
|
41
|
+
SKILL_DIR = Path(__file__).parent.parent # Adjust based on where script is placed
|
|
42
|
+
TEMPLATE_FILE = SKILL_DIR / "templates" / "docsify-shell.html"
|
|
43
|
+
CSS_BASE_FILE = SKILL_DIR / "templates" / "css" / "docsify-base.css"
|
|
44
|
+
|
|
45
|
+
# Manual metadata - Customize for your software
|
|
46
|
+
MANUAL_META = {
|
|
47
|
+
"title": "Your Software",
|
|
48
|
+
"subtitle": "使用手册",
|
|
49
|
+
"version": "v1.0.0",
|
|
50
|
+
"timestamp": "2025-01-01",
|
|
51
|
+
"language": "zh-CN",
|
|
52
|
+
"logo_icon": "Y" # First letter or emoji
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Output file
|
|
56
|
+
OUTPUT_FILE = BASE_DIR / f"{MANUAL_META['title']}{MANUAL_META['subtitle']}.html"
|
|
57
|
+
|
|
58
|
+
# Hierarchical navigation structure
|
|
59
|
+
# Customize groups and items based on your sections
|
|
60
|
+
NAV_STRUCTURE = [
|
|
61
|
+
{
|
|
62
|
+
"type": "group",
|
|
63
|
+
"title": "入门指南",
|
|
64
|
+
"icon": "📚",
|
|
65
|
+
"expanded": True,
|
|
66
|
+
"items": [
|
|
67
|
+
{"id": "overview", "title": "产品概述", "file": "section-overview.md"},
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"type": "group",
|
|
72
|
+
"title": "使用教程",
|
|
73
|
+
"icon": "🎯",
|
|
74
|
+
"expanded": False,
|
|
75
|
+
"items": [
|
|
76
|
+
{"id": "ui-guide", "title": "UI操作指南", "file": "section-ui-guide.md"},
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"type": "group",
|
|
81
|
+
"title": "API参考",
|
|
82
|
+
"icon": "🔧",
|
|
83
|
+
"expanded": False,
|
|
84
|
+
"items": [
|
|
85
|
+
{"id": "api-reference", "title": "API文档", "file": "section-api-reference.md"},
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"type": "group",
|
|
90
|
+
"title": "配置与部署",
|
|
91
|
+
"icon": "⚙️",
|
|
92
|
+
"expanded": False,
|
|
93
|
+
"items": [
|
|
94
|
+
{"id": "configuration", "title": "配置指南", "file": "section-configuration.md"},
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"type": "group",
|
|
99
|
+
"title": "帮助与支持",
|
|
100
|
+
"icon": "💡",
|
|
101
|
+
"expanded": False,
|
|
102
|
+
"items": [
|
|
103
|
+
{"id": "troubleshooting", "title": "故障排除", "file": "section-troubleshooting.md"},
|
|
104
|
+
{"id": "examples", "title": "代码示例", "file": "section-examples.md"},
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
# Screenshot ID to filename mapping - Customize for your screenshots
|
|
110
|
+
SCREENSHOT_MAPPING = {
|
|
111
|
+
# "截图ID": "filename.png",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ============================================================
|
|
116
|
+
# CORE FUNCTIONS - Generally don't need to modify
|
|
117
|
+
# ============================================================
|
|
118
|
+
|
|
119
|
+
# Global cache for embedded images
|
|
120
|
+
_embedded_images = {}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def read_file(filepath: Path) -> str:
|
|
124
|
+
"""Read file content with UTF-8 encoding"""
|
|
125
|
+
return filepath.read_text(encoding='utf-8')
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ============================================================
|
|
129
|
+
# MERMAID VALIDATION
|
|
130
|
+
# ============================================================
|
|
131
|
+
|
|
132
|
+
# Valid Mermaid diagram types
|
|
133
|
+
MERMAID_DIAGRAM_TYPES = [
|
|
134
|
+
'graph', 'flowchart', 'sequenceDiagram', 'classDiagram',
|
|
135
|
+
'stateDiagram', 'stateDiagram-v2', 'erDiagram', 'journey',
|
|
136
|
+
'gantt', 'pie', 'quadrantChart', 'requirementDiagram',
|
|
137
|
+
'gitGraph', 'mindmap', 'timeline', 'zenuml', 'sankey-beta',
|
|
138
|
+
'xychart-beta', 'block-beta'
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
# Common Mermaid syntax patterns
|
|
142
|
+
MERMAID_PATTERNS = {
|
|
143
|
+
'graph': r'^graph\s+(TB|BT|LR|RL|TD)\s*$',
|
|
144
|
+
'flowchart': r'^flowchart\s+(TB|BT|LR|RL|TD)\s*$',
|
|
145
|
+
'sequenceDiagram': r'^sequenceDiagram\s*$',
|
|
146
|
+
'classDiagram': r'^classDiagram\s*$',
|
|
147
|
+
'stateDiagram': r'^stateDiagram(-v2)?\s*$',
|
|
148
|
+
'erDiagram': r'^erDiagram\s*$',
|
|
149
|
+
'gantt': r'^gantt\s*$',
|
|
150
|
+
'pie': r'^pie\s*(showData|title\s+.*)?\s*$',
|
|
151
|
+
'journey': r'^journey\s*$',
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class MermaidBlock:
|
|
156
|
+
"""Represents a mermaid code block found in markdown"""
|
|
157
|
+
def __init__(self, content: str, file: str, line_num: int, indented: bool = False):
|
|
158
|
+
self.content = content
|
|
159
|
+
self.file = file
|
|
160
|
+
self.line_num = line_num
|
|
161
|
+
self.indented = indented
|
|
162
|
+
self.errors: List[str] = []
|
|
163
|
+
self.warnings: List[str] = []
|
|
164
|
+
self.diagram_type: str = None
|
|
165
|
+
|
|
166
|
+
def __repr__(self):
|
|
167
|
+
return f"MermaidBlock({self.diagram_type}, {self.file}:{self.line_num})"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def extract_mermaid_blocks(markdown_text: str, filename: str) -> List[MermaidBlock]:
|
|
171
|
+
"""Extract all mermaid code blocks from markdown text"""
|
|
172
|
+
blocks = []
|
|
173
|
+
|
|
174
|
+
# More flexible pattern - matches opening fence with optional indent,
|
|
175
|
+
# then captures content until closing fence (with any indent)
|
|
176
|
+
pattern = r'^(\s*)(```|~~~)mermaid\s*\n(.*?)\n\s*\2\s*$'
|
|
177
|
+
|
|
178
|
+
for match in re.finditer(pattern, markdown_text, re.MULTILINE | re.DOTALL):
|
|
179
|
+
indent = match.group(1)
|
|
180
|
+
content = match.group(3)
|
|
181
|
+
# Calculate line number
|
|
182
|
+
line_num = markdown_text[:match.start()].count('\n') + 1
|
|
183
|
+
indented = len(indent) > 0
|
|
184
|
+
|
|
185
|
+
block = MermaidBlock(
|
|
186
|
+
content=content,
|
|
187
|
+
file=filename,
|
|
188
|
+
line_num=line_num,
|
|
189
|
+
indented=indented
|
|
190
|
+
)
|
|
191
|
+
blocks.append(block)
|
|
192
|
+
|
|
193
|
+
return blocks
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def validate_mermaid_block(block: MermaidBlock) -> bool:
|
|
197
|
+
"""Validate a mermaid block and populate errors/warnings"""
|
|
198
|
+
content = block.content.strip()
|
|
199
|
+
lines = content.split('\n')
|
|
200
|
+
|
|
201
|
+
if not lines:
|
|
202
|
+
block.errors.append("Empty mermaid block")
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
first_line = lines[0].strip()
|
|
206
|
+
|
|
207
|
+
# Detect diagram type
|
|
208
|
+
for dtype in MERMAID_DIAGRAM_TYPES:
|
|
209
|
+
if first_line.startswith(dtype):
|
|
210
|
+
block.diagram_type = dtype
|
|
211
|
+
break
|
|
212
|
+
|
|
213
|
+
if not block.diagram_type:
|
|
214
|
+
block.errors.append(f"Unknown diagram type: '{first_line[:30]}...'")
|
|
215
|
+
block.errors.append(f"Valid types: {', '.join(MERMAID_DIAGRAM_TYPES[:8])}...")
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
# Check for balanced brackets/braces
|
|
219
|
+
brackets = {'[': ']', '{': '}', '(': ')'}
|
|
220
|
+
stack = []
|
|
221
|
+
for i, char in enumerate(content):
|
|
222
|
+
if char in brackets:
|
|
223
|
+
stack.append((char, i))
|
|
224
|
+
elif char in brackets.values():
|
|
225
|
+
if not stack:
|
|
226
|
+
block.errors.append(f"Unmatched closing bracket '{char}' at position {i}")
|
|
227
|
+
else:
|
|
228
|
+
open_char, _ = stack.pop()
|
|
229
|
+
if brackets[open_char] != char:
|
|
230
|
+
block.errors.append(f"Mismatched brackets: '{open_char}' and '{char}'")
|
|
231
|
+
|
|
232
|
+
if stack:
|
|
233
|
+
for open_char, pos in stack:
|
|
234
|
+
block.warnings.append(f"Unclosed bracket '{open_char}' at position {pos}")
|
|
235
|
+
|
|
236
|
+
# Check for common graph/flowchart issues
|
|
237
|
+
if block.diagram_type in ['graph', 'flowchart']:
|
|
238
|
+
# Check direction specifier
|
|
239
|
+
if not re.match(r'^(graph|flowchart)\s+(TB|BT|LR|RL|TD)', first_line):
|
|
240
|
+
block.warnings.append("Missing or invalid direction (TB/BT/LR/RL/TD)")
|
|
241
|
+
|
|
242
|
+
# Check for arrow syntax
|
|
243
|
+
arrow_count = content.count('-->') + content.count('---') + content.count('-.->') + content.count('==>')
|
|
244
|
+
if arrow_count == 0 and len(lines) > 1:
|
|
245
|
+
block.warnings.append("No arrows found - graph may be incomplete")
|
|
246
|
+
|
|
247
|
+
# Check for sequenceDiagram issues
|
|
248
|
+
if block.diagram_type == 'sequenceDiagram':
|
|
249
|
+
if '->' not in content and '->>' not in content:
|
|
250
|
+
block.warnings.append("No message arrows found in sequence diagram")
|
|
251
|
+
|
|
252
|
+
# Indentation warning
|
|
253
|
+
if block.indented:
|
|
254
|
+
block.warnings.append("Indented code block - may not render in some markdown parsers")
|
|
255
|
+
|
|
256
|
+
return len(block.errors) == 0
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def validate_all_mermaid(nav_structure: List[Dict], sections_dir: Path) -> Dict[str, Any]:
|
|
260
|
+
"""Validate all mermaid blocks in all section files"""
|
|
261
|
+
report = {
|
|
262
|
+
'total_blocks': 0,
|
|
263
|
+
'valid_blocks': 0,
|
|
264
|
+
'error_blocks': 0,
|
|
265
|
+
'warning_blocks': 0,
|
|
266
|
+
'blocks': [],
|
|
267
|
+
'by_file': {},
|
|
268
|
+
'by_type': {}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
for group in nav_structure:
|
|
272
|
+
for item in group.get("items", []):
|
|
273
|
+
section_file = item.get("file")
|
|
274
|
+
if not section_file:
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
filepath = sections_dir / section_file
|
|
278
|
+
if not filepath.exists():
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
content = read_file(filepath)
|
|
282
|
+
blocks = extract_mermaid_blocks(content, section_file)
|
|
283
|
+
|
|
284
|
+
file_report = {'blocks': [], 'errors': 0, 'warnings': 0}
|
|
285
|
+
|
|
286
|
+
for block in blocks:
|
|
287
|
+
report['total_blocks'] += 1
|
|
288
|
+
is_valid = validate_mermaid_block(block)
|
|
289
|
+
|
|
290
|
+
if is_valid:
|
|
291
|
+
report['valid_blocks'] += 1
|
|
292
|
+
else:
|
|
293
|
+
report['error_blocks'] += 1
|
|
294
|
+
file_report['errors'] += 1
|
|
295
|
+
|
|
296
|
+
if block.warnings:
|
|
297
|
+
report['warning_blocks'] += 1
|
|
298
|
+
file_report['warnings'] += len(block.warnings)
|
|
299
|
+
|
|
300
|
+
# Track by diagram type
|
|
301
|
+
if block.diagram_type:
|
|
302
|
+
if block.diagram_type not in report['by_type']:
|
|
303
|
+
report['by_type'][block.diagram_type] = 0
|
|
304
|
+
report['by_type'][block.diagram_type] += 1
|
|
305
|
+
|
|
306
|
+
report['blocks'].append(block)
|
|
307
|
+
file_report['blocks'].append(block)
|
|
308
|
+
|
|
309
|
+
if blocks:
|
|
310
|
+
report['by_file'][section_file] = file_report
|
|
311
|
+
|
|
312
|
+
return report
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def print_mermaid_report(report: Dict[str, Any]) -> None:
|
|
316
|
+
"""Print mermaid validation report"""
|
|
317
|
+
print("\n" + "=" * 60)
|
|
318
|
+
print("MERMAID DIAGRAM VALIDATION REPORT")
|
|
319
|
+
print("=" * 60)
|
|
320
|
+
|
|
321
|
+
print(f"\nSummary:")
|
|
322
|
+
print(f" Total blocks: {report['total_blocks']}")
|
|
323
|
+
print(f" Valid: {report['valid_blocks']}")
|
|
324
|
+
print(f" With errors: {report['error_blocks']}")
|
|
325
|
+
print(f" With warnings: {report['warning_blocks']}")
|
|
326
|
+
|
|
327
|
+
if report['by_type']:
|
|
328
|
+
print(f"\nDiagram Types:")
|
|
329
|
+
for dtype, count in sorted(report['by_type'].items()):
|
|
330
|
+
print(f" {dtype}: {count}")
|
|
331
|
+
|
|
332
|
+
# Print errors and warnings
|
|
333
|
+
has_issues = False
|
|
334
|
+
for block in report['blocks']:
|
|
335
|
+
if block.errors or block.warnings:
|
|
336
|
+
if not has_issues:
|
|
337
|
+
print(f"\nIssues Found:")
|
|
338
|
+
has_issues = True
|
|
339
|
+
|
|
340
|
+
print(f"\n [{block.file}:{block.line_num}] {block.diagram_type or 'unknown'}")
|
|
341
|
+
for error in block.errors:
|
|
342
|
+
print(f" [ERROR] {error}")
|
|
343
|
+
for warning in block.warnings:
|
|
344
|
+
print(f" [WARN] {warning}")
|
|
345
|
+
|
|
346
|
+
if not has_issues:
|
|
347
|
+
print(f"\n No issues found!")
|
|
348
|
+
|
|
349
|
+
print("=" * 60 + "\n")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def convert_md_to_html(markdown_text: str) -> str:
|
|
353
|
+
"""Convert Markdown to HTML with syntax highlighting"""
|
|
354
|
+
if not HAS_MARKDOWN:
|
|
355
|
+
# Fallback: just escape HTML and wrap in pre
|
|
356
|
+
escaped = markdown_text.replace('&', '&').replace('<', '<').replace('>', '>')
|
|
357
|
+
return f'<pre>{escaped}</pre>'
|
|
358
|
+
|
|
359
|
+
md = markdown.Markdown(
|
|
360
|
+
extensions=[
|
|
361
|
+
FencedCodeExtension(),
|
|
362
|
+
TableExtension(),
|
|
363
|
+
TocExtension(toc_depth=3),
|
|
364
|
+
CodeHiliteExtension(
|
|
365
|
+
css_class='highlight',
|
|
366
|
+
linenums=False,
|
|
367
|
+
guess_lang=True,
|
|
368
|
+
use_pygments=True
|
|
369
|
+
),
|
|
370
|
+
],
|
|
371
|
+
output_format='html5'
|
|
372
|
+
)
|
|
373
|
+
html = md.convert(markdown_text)
|
|
374
|
+
md.reset()
|
|
375
|
+
return html
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def embed_screenshot_base64(screenshot_id: str) -> str:
|
|
379
|
+
"""Embed screenshot as base64, using cache to avoid duplicates"""
|
|
380
|
+
global _embedded_images
|
|
381
|
+
|
|
382
|
+
filename = SCREENSHOT_MAPPING.get(screenshot_id)
|
|
383
|
+
|
|
384
|
+
if not filename:
|
|
385
|
+
return f'<div class="screenshot-placeholder">📷 {screenshot_id}</div>'
|
|
386
|
+
|
|
387
|
+
filepath = SCREENSHOTS_DIR / filename
|
|
388
|
+
|
|
389
|
+
if not filepath.exists():
|
|
390
|
+
return f'<div class="screenshot-placeholder">📷 {screenshot_id}</div>'
|
|
391
|
+
|
|
392
|
+
# Check cache
|
|
393
|
+
if filename not in _embedded_images:
|
|
394
|
+
try:
|
|
395
|
+
with open(filepath, 'rb') as f:
|
|
396
|
+
image_data = base64.b64encode(f.read()).decode('utf-8')
|
|
397
|
+
ext = filepath.suffix[1:].lower()
|
|
398
|
+
_embedded_images[filename] = f"data:image/{ext};base64,{image_data}"
|
|
399
|
+
except Exception as e:
|
|
400
|
+
return f'<div class="screenshot-placeholder">📷 {screenshot_id} (加载失败)</div>'
|
|
401
|
+
|
|
402
|
+
return f'''<figure class="screenshot">
|
|
403
|
+
<img src="{_embedded_images[filename]}" alt="{screenshot_id}" loading="lazy" />
|
|
404
|
+
<figcaption>{screenshot_id}</figcaption>
|
|
405
|
+
</figure>'''
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def process_markdown_screenshots(markdown_text: str) -> str:
|
|
409
|
+
"""Replace [[screenshot:xxx]] placeholders with embedded images"""
|
|
410
|
+
pattern = r'\[\[screenshot:(.*?)\]\]'
|
|
411
|
+
|
|
412
|
+
def replacer(match):
|
|
413
|
+
screenshot_id = match.group(1)
|
|
414
|
+
return embed_screenshot_base64(screenshot_id)
|
|
415
|
+
|
|
416
|
+
return re.sub(pattern, replacer, markdown_text)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def generate_sidebar_nav_html(nav_structure: List[Dict]) -> str:
|
|
420
|
+
"""Generate hierarchical sidebar navigation HTML"""
|
|
421
|
+
html_parts = []
|
|
422
|
+
|
|
423
|
+
for group in nav_structure:
|
|
424
|
+
if group["type"] == "group":
|
|
425
|
+
expanded_class = "expanded" if group.get("expanded", False) else ""
|
|
426
|
+
html_parts.append(f'''
|
|
427
|
+
<div class="nav-group {expanded_class}">
|
|
428
|
+
<div class="nav-group-header">
|
|
429
|
+
<button class="nav-group-toggle" aria-expanded="{str(group.get('expanded', False)).lower()}">
|
|
430
|
+
<svg viewBox="0 0 24 24"><path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6z" fill="currentColor"/></svg>
|
|
431
|
+
</button>
|
|
432
|
+
<span class="nav-group-title">{group.get('icon', '')} {group['title']}</span>
|
|
433
|
+
</div>
|
|
434
|
+
<div class="nav-group-items">''')
|
|
435
|
+
|
|
436
|
+
for item in group.get("items", []):
|
|
437
|
+
html_parts.append(f'''
|
|
438
|
+
<a class="nav-item" href="#/{item['id']}" data-section="{item['id']}">{item['title']}</a>''')
|
|
439
|
+
|
|
440
|
+
html_parts.append('''
|
|
441
|
+
</div>
|
|
442
|
+
</div>''')
|
|
443
|
+
|
|
444
|
+
return '\n'.join(html_parts)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def generate_sections_html(nav_structure: List[Dict]) -> str:
|
|
448
|
+
"""Generate content sections HTML"""
|
|
449
|
+
sections_html = []
|
|
450
|
+
|
|
451
|
+
for group in nav_structure:
|
|
452
|
+
for item in group.get("items", []):
|
|
453
|
+
section_id = item["id"]
|
|
454
|
+
section_title = item["title"]
|
|
455
|
+
section_file = item.get("file")
|
|
456
|
+
|
|
457
|
+
if not section_file:
|
|
458
|
+
continue
|
|
459
|
+
|
|
460
|
+
filepath = SECTIONS_DIR / section_file
|
|
461
|
+
if not filepath.exists():
|
|
462
|
+
print(f"Warning: Section file not found: {filepath}")
|
|
463
|
+
continue
|
|
464
|
+
|
|
465
|
+
# Read and convert markdown
|
|
466
|
+
markdown_content = read_file(filepath)
|
|
467
|
+
markdown_content = process_markdown_screenshots(markdown_content)
|
|
468
|
+
html_content = convert_md_to_html(markdown_content)
|
|
469
|
+
|
|
470
|
+
sections_html.append(f'''
|
|
471
|
+
<section class="content-section" id="section-{section_id}" data-title="{section_title}">
|
|
472
|
+
{html_content}
|
|
473
|
+
</section>''')
|
|
474
|
+
|
|
475
|
+
return '\n'.join(sections_html)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def generate_search_index(nav_structure: List[Dict]) -> str:
|
|
479
|
+
"""Generate search index JSON"""
|
|
480
|
+
search_index = {}
|
|
481
|
+
|
|
482
|
+
for group in nav_structure:
|
|
483
|
+
for item in group.get("items", []):
|
|
484
|
+
section_id = item["id"]
|
|
485
|
+
section_file = item.get("file")
|
|
486
|
+
|
|
487
|
+
if not section_file:
|
|
488
|
+
continue
|
|
489
|
+
|
|
490
|
+
filepath = SECTIONS_DIR / section_file
|
|
491
|
+
if filepath.exists():
|
|
492
|
+
content = read_file(filepath)
|
|
493
|
+
clean_content = re.sub(r'[#*`\[\]()]', '', content)
|
|
494
|
+
clean_content = re.sub(r'\s+', ' ', clean_content)[:1500]
|
|
495
|
+
|
|
496
|
+
search_index[section_id] = {
|
|
497
|
+
"title": item["title"],
|
|
498
|
+
"body": clean_content,
|
|
499
|
+
"group": group["title"]
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return json.dumps(search_index, ensure_ascii=False, indent=2)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def generate_nav_structure_json(nav_structure: List[Dict]) -> str:
|
|
506
|
+
"""Generate navigation structure JSON for client-side"""
|
|
507
|
+
return json.dumps(nav_structure, ensure_ascii=False, indent=2)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def assemble_manual(validate_mermaid: bool = True):
|
|
511
|
+
"""Main assembly function
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
validate_mermaid: Whether to validate mermaid diagrams (default: True)
|
|
515
|
+
"""
|
|
516
|
+
global _embedded_images
|
|
517
|
+
_embedded_images = {}
|
|
518
|
+
|
|
519
|
+
full_title = f"{MANUAL_META['title']} {MANUAL_META['subtitle']}"
|
|
520
|
+
print(f"Assembling Docsify-style manual: {full_title}")
|
|
521
|
+
|
|
522
|
+
# Verify template exists
|
|
523
|
+
if not TEMPLATE_FILE.exists():
|
|
524
|
+
print(f"Error: Template not found at {TEMPLATE_FILE}")
|
|
525
|
+
print("Please update TEMPLATE_FILE path in this script.")
|
|
526
|
+
return None, 0
|
|
527
|
+
|
|
528
|
+
if not CSS_BASE_FILE.exists():
|
|
529
|
+
print(f"Error: CSS not found at {CSS_BASE_FILE}")
|
|
530
|
+
print("Please update CSS_BASE_FILE path in this script.")
|
|
531
|
+
return None, 0
|
|
532
|
+
|
|
533
|
+
# Validate Mermaid diagrams
|
|
534
|
+
mermaid_report = None
|
|
535
|
+
if validate_mermaid:
|
|
536
|
+
print("\nValidating Mermaid diagrams...")
|
|
537
|
+
mermaid_report = validate_all_mermaid(NAV_STRUCTURE, SECTIONS_DIR)
|
|
538
|
+
print_mermaid_report(mermaid_report)
|
|
539
|
+
|
|
540
|
+
# Warn if there are errors (but continue)
|
|
541
|
+
if mermaid_report['error_blocks'] > 0:
|
|
542
|
+
print(f"[WARN] {mermaid_report['error_blocks']} mermaid block(s) have errors!")
|
|
543
|
+
print(" These diagrams may not render correctly.")
|
|
544
|
+
|
|
545
|
+
# Read template and CSS
|
|
546
|
+
template_html = read_file(TEMPLATE_FILE)
|
|
547
|
+
css_content = read_file(CSS_BASE_FILE)
|
|
548
|
+
|
|
549
|
+
# Generate components
|
|
550
|
+
sidebar_nav_html = generate_sidebar_nav_html(NAV_STRUCTURE)
|
|
551
|
+
sections_html = generate_sections_html(NAV_STRUCTURE)
|
|
552
|
+
search_index_json = generate_search_index(NAV_STRUCTURE)
|
|
553
|
+
nav_structure_json = generate_nav_structure_json(NAV_STRUCTURE)
|
|
554
|
+
|
|
555
|
+
# Replace placeholders
|
|
556
|
+
output_html = template_html
|
|
557
|
+
output_html = output_html.replace('{{SOFTWARE_NAME}}', full_title)
|
|
558
|
+
output_html = output_html.replace('{{VERSION}}', MANUAL_META['version'])
|
|
559
|
+
output_html = output_html.replace('{{TIMESTAMP}}', MANUAL_META['timestamp'])
|
|
560
|
+
output_html = output_html.replace('{{LOGO_ICON}}', MANUAL_META['logo_icon'])
|
|
561
|
+
output_html = output_html.replace('{{EMBEDDED_CSS}}', css_content)
|
|
562
|
+
output_html = output_html.replace('{{SIDEBAR_NAV_HTML}}', sidebar_nav_html)
|
|
563
|
+
output_html = output_html.replace('{{SECTIONS_HTML}}', sections_html)
|
|
564
|
+
output_html = output_html.replace('{{SEARCH_INDEX_JSON}}', search_index_json)
|
|
565
|
+
output_html = output_html.replace('{{NAV_STRUCTURE_JSON}}', nav_structure_json)
|
|
566
|
+
|
|
567
|
+
# Write output file
|
|
568
|
+
OUTPUT_FILE.write_text(output_html, encoding='utf-8')
|
|
569
|
+
|
|
570
|
+
file_size = OUTPUT_FILE.stat().st_size
|
|
571
|
+
file_size_mb = file_size / (1024 * 1024)
|
|
572
|
+
section_count = sum(len(g.get("items", [])) for g in NAV_STRUCTURE)
|
|
573
|
+
|
|
574
|
+
print("[OK] Docsify-style manual generated successfully!")
|
|
575
|
+
print(f" Output: {OUTPUT_FILE}")
|
|
576
|
+
print(f" Size: {file_size_mb:.2f} MB ({file_size:,} bytes)")
|
|
577
|
+
print(f" Navigation Groups: {len(NAV_STRUCTURE)}")
|
|
578
|
+
print(f" Sections: {section_count}")
|
|
579
|
+
|
|
580
|
+
return str(OUTPUT_FILE), file_size
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
if __name__ == "__main__":
|
|
584
|
+
output_path, size = assemble_manual()
|