create-dwy 0.16.0 → 0.17.0
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/CHANGELOG.md +17 -0
- package/bin/index.js +1 -1
- package/package.json +3 -2
- package/src/searchable-select.js +419 -0
- package/src/sync-all.js +313 -65
- package/src/sync-global-skills.js +167 -0
- package/src/sync.js +59 -43
- package/templates/ai-tools/CHANGELOG.md +210 -0
- package/templates/ai-tools/VERSION +1 -0
- package/templates/ai-tools/hook-manifests/hooks.json +7 -0
- package/templates/ai-tools/hooks/Git/pre-git-commit-ai-signature-check.sh +57 -0
- package/templates/ai-tools/rules/Android/dwy-android-core.md +280 -5
- package/templates/ai-tools/rules/Docker/dwy-docker.md +52 -1
- package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-build.md +50 -1
- package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-design.md +139 -0
- package/templates/ai-tools/rules/HarmonyOS/dwy-harmony-pitfalls.md +72 -0
- package/templates/ai-tools/rules/Python/dwy-python-core.md +47 -0
- package/templates/ai-tools/rules/Vue/dwy-vue-core.md +55 -0
- package/templates/ai-tools/rules/iOS/dwy-ios-pitfalls.md +7 -0
- package/templates/ai-tools/rules/iOS/dwy-swift-core.md +46 -0
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-git-commit.md +9 -6
- package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-oss-commercial-license.md +128 -0
- package/templates/ai-tools/skills//345/205/203/345/267/245/345/205/267/dwy-shared/SKILL.md +22 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/SKILL.md +65 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/assets/appstore-submission.yaml +43 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/api-workflow.md +17 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/in-app-purchase-review.md +16 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/screenshot-workflow.md +16 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/scripts/appstore_config.sh +140 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/SKILL.md +11 -8
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/android.md +2 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/backend-deploy.md +2 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/changelog.md +24 -4
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/frontend-deploy.md +2 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/generic-release.md +2 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/git-tag.md +82 -0
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/harmony.md +2 -1
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/ios.md +3 -2
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/monitor-notify.md +2 -2
- package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-semver/SKILL.md +1 -1
- package/templates/ai-tools/skills//345/237/272/347/241/200/345/272/223/dwy-eapi/references/tasks-integration-guide.md +2 -2
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/SKILL.md +1 -1
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/references/checks-runtime.md +2 -2
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_docker.sh +3 -3
- package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_secrets.sh +1 -1
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/README.md +57 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/SKILL.md +147 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/references/api.md +43 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/references/config.example.yaml +33 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/global_config.py +89 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/setup_config.py +175 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/synthesize.py +339 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/SKILL.md +127 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/assets/cover-style-master-v1.png +0 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/references/platform-copy-examples.md +37 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/references/platform-posting-times.md +193 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0005-ideation/SKILL.md +42 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0005-ideation/templates/topic-backlog.template.md +50 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0005-ideation/templates//351/200/211/351/242/230/346/261/240.template.md +16 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0010-product/SKILL.md +106 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0010-product/templates/channel-profile.template.md +52 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0010-product/templates//344/272/247/345/223/201/345/215/241.template.md +132 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/SKILL.md +60 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/scripts/build_facts_html.py +430 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/scripts/generate_html.py +1287 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/scripts/generate_report.py +189 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/templates//344/272/213/345/256/236/350/265/204/346/226/231/345/272/223.template.md +55 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/SKILL.md +36 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates/README.md +10 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates//344/272/213/345/256/236.template.md +82 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates//345/215/225/351/233/206/346/226/207/346/241/243.template.md +175 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates//350/204/232/346/234/254.template.md +69 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates//351/252/250/346/236/266-v1.1.md +174 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0050-design/SKILL.md +55 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0060-assets/SKILL.md +29 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/SKILL.md +42 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/scripts/validate_shots.py +207 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/shots.schema.json +186 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package//344/272/272/345/256/241/345/214/205.template.md +1 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package//344/272/272/345/256/241/350/257/264/346/230/216.md +15 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package//346/210/220/347/211/207/345/214/205.template.md +49 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0080-factory/SKILL.md +58 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-comic-kit/SKILL.md +424 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-comic-kit/evals.md +41 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-flow/CONTRACT.md +123 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-flow/SKILL.md +81 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-flow//346/211/247/350/241/214/346/270/205/345/215/225.template.md +60 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/SKILL.md +78 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/agents/openai.yaml +4 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/knowledge-town-paper-film-v1.json +80 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/AudioTracks.tsx +20 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/CaptionLayer.tsx +18 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/CueTimeline.ts +24 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/KnowledgeTownComposition.tsx +21 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/references/cinematic-direction.md +73 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/references/sticker-asset-production.md +30 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/references/style-guide.md +43 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/scripts/build_cue_timeline.py +75 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/scripts/validate_cue_timeline.py +68 -0
- package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/scripts/validate_style_preset.py +22 -0
- package/templates/ai-tools/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-first/SKILL.md +176 -0
- package/templates/ai-tools/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-first/references/chapter-01-mirrors.md +112 -0
|
@@ -0,0 +1,1287 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate a Markdown report + interactive HTML viewer.
|
|
3
|
+
|
|
4
|
+
Outputs:
|
|
5
|
+
- README.md (clean Markdown, single source of truth)
|
|
6
|
+
- index.html (self-contained viewer: CSS + JS + embedded MD content)
|
|
7
|
+
- sources.json (structured source data)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
import argparse
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from html import escape
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def slugify(text):
|
|
20
|
+
"""Generate a URL-safe slug from heading text."""
|
|
21
|
+
text = re.sub(r'\*\*([^*]+)\*\*', r'\1', text)
|
|
22
|
+
text = re.sub(r'\*([^*]+)\*', r'\1', text)
|
|
23
|
+
text = re.sub(r'`([^`]+)`', r'\1', text)
|
|
24
|
+
text = re.sub(r'[^\w\u4e00-\u9fff]+', '-', text.strip()).lower().strip('-')
|
|
25
|
+
text = re.sub(r'-+', '-', text)[:60]
|
|
26
|
+
return text
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def normalize_source(src):
|
|
30
|
+
"""Normalize source entries to {title, url, type, ...}.
|
|
31
|
+
|
|
32
|
+
Supports alternate shapes like {project, urls:[...]} used by some angle
|
|
33
|
+
research dumps. Missing fields must not break the HTML viewer.
|
|
34
|
+
"""
|
|
35
|
+
if not isinstance(src, dict):
|
|
36
|
+
return {'title': str(src), 'url': '#', 'type': 'community'}
|
|
37
|
+
|
|
38
|
+
urls = src.get('urls') or []
|
|
39
|
+
if not isinstance(urls, list):
|
|
40
|
+
urls = [urls] if urls else []
|
|
41
|
+
|
|
42
|
+
url = src.get('url') or (urls[0] if urls else '#') or '#'
|
|
43
|
+
title = src.get('title') or src.get('project') or url or '未知'
|
|
44
|
+
out = {
|
|
45
|
+
'title': title,
|
|
46
|
+
'url': url,
|
|
47
|
+
'type': src.get('type') or ('primary' if src.get('project') else 'community'),
|
|
48
|
+
}
|
|
49
|
+
if src.get('note'):
|
|
50
|
+
out['note'] = src['note']
|
|
51
|
+
elif len(urls) > 1:
|
|
52
|
+
out['note'] = '相关链接:' + ' · '.join(str(u) for u in urls[1:])
|
|
53
|
+
return out
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def format_project_details(projects):
|
|
57
|
+
"""Turn a list of project license research objects into readable Markdown."""
|
|
58
|
+
lines = []
|
|
59
|
+
for p in projects:
|
|
60
|
+
if not isinstance(p, dict):
|
|
61
|
+
lines.append(f'- {p}')
|
|
62
|
+
continue
|
|
63
|
+
name = p.get('project', '未知项目')
|
|
64
|
+
lines += [f'### {name}', '']
|
|
65
|
+
if p.get('repo_url'):
|
|
66
|
+
lines.append(f'- **仓库**:[{p["repo_url"]}]({p["repo_url"]})')
|
|
67
|
+
if p.get('license_name'):
|
|
68
|
+
lines.append(f'- **协议**:{p["license_name"]}')
|
|
69
|
+
meta = p.get('github_license_metadata') or {}
|
|
70
|
+
if isinstance(meta, dict) and (meta.get('spdx_id') or meta.get('name')):
|
|
71
|
+
spdx = meta.get('spdx_id', '')
|
|
72
|
+
mname = meta.get('name', '')
|
|
73
|
+
note = meta.get('note', '')
|
|
74
|
+
meta_line = f'- **GitHub 元数据**:{mname}'
|
|
75
|
+
if spdx:
|
|
76
|
+
meta_line += f' (`{spdx}`)'
|
|
77
|
+
if note:
|
|
78
|
+
meta_line += f' — {note}'
|
|
79
|
+
lines.append(meta_line)
|
|
80
|
+
if 'is_osi_open_source' in p:
|
|
81
|
+
lines.append(f'- **是否 OSI 开源**:{"是" if p["is_osi_open_source"] else "否"}')
|
|
82
|
+
if p.get('license_file'):
|
|
83
|
+
lines.append(f'- **LICENSE 文件**:[{p["license_file"]}]({p["license_file"]})')
|
|
84
|
+
faq = p.get('official_faq_is_remotion_open_source')
|
|
85
|
+
if isinstance(faq, dict):
|
|
86
|
+
q = faq.get('question', '')
|
|
87
|
+
a = faq.get('answer_verbatim_summary', '')
|
|
88
|
+
lines.append(f'- **官方 FAQ**:{q} → {a}')
|
|
89
|
+
if faq.get('source'):
|
|
90
|
+
lines.append(f' - 来源:[{faq["source"]}]({faq["source"]})')
|
|
91
|
+
if p.get('verification_method'):
|
|
92
|
+
lines.append(f'- **核实方式**:{p["verification_method"]}')
|
|
93
|
+
points = p.get('license_points') or []
|
|
94
|
+
if points:
|
|
95
|
+
lines += ['', '**要点**:']
|
|
96
|
+
for pt in points:
|
|
97
|
+
lines.append(f'- {pt}')
|
|
98
|
+
quotes = p.get('quotes') or []
|
|
99
|
+
if quotes:
|
|
100
|
+
lines += ['', '**原文摘录**:']
|
|
101
|
+
for q in quotes:
|
|
102
|
+
if not isinstance(q, dict):
|
|
103
|
+
lines.append(f'> {q}')
|
|
104
|
+
lines.append('')
|
|
105
|
+
continue
|
|
106
|
+
text = (q.get('text') or '').replace('\n', ' ').strip()
|
|
107
|
+
qurl = q.get('url') or ''
|
|
108
|
+
lines.append(f'> {text}')
|
|
109
|
+
if qurl:
|
|
110
|
+
lines.append('>')
|
|
111
|
+
lines.append(f'> — [{qurl}]({qurl})')
|
|
112
|
+
lines.append('')
|
|
113
|
+
lines.append('')
|
|
114
|
+
return '\n'.join(lines).rstrip() + '\n'
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def details_to_markdown(details):
|
|
118
|
+
"""Coerce angle details (str / list / dict / JSON string) into Markdown."""
|
|
119
|
+
if details is None:
|
|
120
|
+
return '(内容待补充)'
|
|
121
|
+
|
|
122
|
+
if isinstance(details, str):
|
|
123
|
+
s = details.strip()
|
|
124
|
+
if s.startswith('[') or s.startswith('{'):
|
|
125
|
+
try:
|
|
126
|
+
return details_to_markdown(json.loads(s))
|
|
127
|
+
except json.JSONDecodeError:
|
|
128
|
+
return details
|
|
129
|
+
return details
|
|
130
|
+
|
|
131
|
+
if isinstance(details, list):
|
|
132
|
+
if details and isinstance(details[0], dict) and 'project' in details[0]:
|
|
133
|
+
return format_project_details(details)
|
|
134
|
+
parts = []
|
|
135
|
+
for item in details:
|
|
136
|
+
if isinstance(item, str):
|
|
137
|
+
parts.append(item)
|
|
138
|
+
else:
|
|
139
|
+
parts.append(json.dumps(item, ensure_ascii=False, indent=2))
|
|
140
|
+
return '\n\n'.join(parts)
|
|
141
|
+
|
|
142
|
+
if isinstance(details, dict):
|
|
143
|
+
if 'project' in details:
|
|
144
|
+
return format_project_details([details])
|
|
145
|
+
return '```json\n' + json.dumps(details, ensure_ascii=False, indent=2) + '\n```'
|
|
146
|
+
|
|
147
|
+
return str(details)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def generate_markdown(topic, angles_data):
|
|
151
|
+
"""Generate clean Markdown report (single source of truth)."""
|
|
152
|
+
today = datetime.now().strftime('%Y-%m-%d')
|
|
153
|
+
angles_list = '、'.join(a['angle'] for a in angles_data)
|
|
154
|
+
|
|
155
|
+
lines = [
|
|
156
|
+
f'# {topic}',
|
|
157
|
+
'',
|
|
158
|
+
f'> 整理时间:{today}',
|
|
159
|
+
f'> 覆盖角度:{angles_list}',
|
|
160
|
+
'',
|
|
161
|
+
'---',
|
|
162
|
+
'',
|
|
163
|
+
'## 目录',
|
|
164
|
+
'1. [概述](#概述)',
|
|
165
|
+
]
|
|
166
|
+
for i, a in enumerate(angles_data, 2):
|
|
167
|
+
anchor = slugify(a['angle'])
|
|
168
|
+
lines.append(f'{i}. [{a["angle"]}](#{anchor})')
|
|
169
|
+
lines.append(f'{len(angles_data) + 2}. [参考资料](#参考资料)')
|
|
170
|
+
lines += ['', '---', '', '## 概述', '']
|
|
171
|
+
|
|
172
|
+
for a in angles_data:
|
|
173
|
+
lines.append(f'**{a["angle"]}**:{a.get("summary", "")}')
|
|
174
|
+
lines.append('')
|
|
175
|
+
|
|
176
|
+
for i, a in enumerate(angles_data, 1):
|
|
177
|
+
lines += ['', '---', '', f'## {a["angle"]}', '']
|
|
178
|
+
details = details_to_markdown(a.get('details', a.get('summary', '(内容待补充)')))
|
|
179
|
+
# Remove leading H1 if present (it's the angle title)
|
|
180
|
+
details = re.sub(r'^#\s+.+\s*', '', details, count=1, flags=re.MULTILINE)
|
|
181
|
+
lines.append(details)
|
|
182
|
+
lines.append('')
|
|
183
|
+
if a.get('key_points'):
|
|
184
|
+
lines.append('### 关键要点')
|
|
185
|
+
lines.append('')
|
|
186
|
+
for kp in a['key_points']:
|
|
187
|
+
lines.append(f'- {kp}')
|
|
188
|
+
lines.append('')
|
|
189
|
+
|
|
190
|
+
# Sources (normalized so HTML citations never see missing title/url)
|
|
191
|
+
all_sources = []
|
|
192
|
+
for a in angles_data:
|
|
193
|
+
for src in a.get('sources', []):
|
|
194
|
+
all_sources.append(normalize_source(src))
|
|
195
|
+
|
|
196
|
+
lines += ['', '---', '', '## 参考资料', '', '### 来源列表', '']
|
|
197
|
+
for i, src in enumerate(all_sources, 1):
|
|
198
|
+
t = src.get('type', 'community')
|
|
199
|
+
title = src.get('title', src.get('url', '未知'))
|
|
200
|
+
url = src.get('url', '#')
|
|
201
|
+
lines.append(f'[{i}] [{title}]({url}) `[{t}]`')
|
|
202
|
+
lines.append('')
|
|
203
|
+
lines.append('---')
|
|
204
|
+
lines.append('')
|
|
205
|
+
lines.append('*本笔记由 AI 调研生成,所有事实均标注来源。建议独立验证关键信息。*')
|
|
206
|
+
lines.append('')
|
|
207
|
+
|
|
208
|
+
return '\n'.join(lines), all_sources
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
HTML_TEMPLATE = """\
|
|
212
|
+
<!DOCTYPE html>
|
|
213
|
+
<html lang="zh-CN">
|
|
214
|
+
<head>
|
|
215
|
+
<meta charset="UTF-8">
|
|
216
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
217
|
+
<title>{title}</title>
|
|
218
|
+
<style>
|
|
219
|
+
:root {{
|
|
220
|
+
--bg: #fafafa;
|
|
221
|
+
--card-bg: #ffffff;
|
|
222
|
+
--text: #1a1a1a;
|
|
223
|
+
--text-secondary: #666;
|
|
224
|
+
--border: #e5e5e5;
|
|
225
|
+
--accent: #2563eb;
|
|
226
|
+
--accent-light: #dbeafe;
|
|
227
|
+
--cite-bg: #f0f9ff;
|
|
228
|
+
--cite-border: #93c5fd;
|
|
229
|
+
--popup-bg: #1e293b;
|
|
230
|
+
--popup-text: #f8fafc;
|
|
231
|
+
--popup-link: #93c5fd;
|
|
232
|
+
--code-bg: #f1f5f9;
|
|
233
|
+
--table-stripe: #f8fafc;
|
|
234
|
+
--sidebar-width: 240px;
|
|
235
|
+
}}
|
|
236
|
+
|
|
237
|
+
@media (prefers-color-scheme: dark) {{
|
|
238
|
+
:root {{
|
|
239
|
+
--bg: #0f172a;
|
|
240
|
+
--card-bg: #1e293b;
|
|
241
|
+
--text: #e2e8f0;
|
|
242
|
+
--text-secondary: #94a3b8;
|
|
243
|
+
--border: #334155;
|
|
244
|
+
--accent: #60a5fa;
|
|
245
|
+
--accent-light: #1e3a5f;
|
|
246
|
+
--cite-bg: #1e3a5f;
|
|
247
|
+
--cite-border: #3b82f6;
|
|
248
|
+
--popup-bg: #0f172a;
|
|
249
|
+
--popup-text: #f1f5f9;
|
|
250
|
+
--popup-link: #60a5fa;
|
|
251
|
+
--code-bg: #1e293b;
|
|
252
|
+
--table-stripe: #1a2332;
|
|
253
|
+
}}
|
|
254
|
+
}}
|
|
255
|
+
|
|
256
|
+
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
257
|
+
|
|
258
|
+
body {{
|
|
259
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
260
|
+
background: var(--bg);
|
|
261
|
+
color: var(--text);
|
|
262
|
+
line-height: 1.8;
|
|
263
|
+
}}
|
|
264
|
+
|
|
265
|
+
/* Layout: sidebar + main */
|
|
266
|
+
.layout {{
|
|
267
|
+
display: flex;
|
|
268
|
+
max-width: 1120px;
|
|
269
|
+
margin: 0 auto;
|
|
270
|
+
}}
|
|
271
|
+
|
|
272
|
+
.sidebar {{
|
|
273
|
+
width: var(--sidebar-width);
|
|
274
|
+
flex-shrink: 0;
|
|
275
|
+
position: sticky;
|
|
276
|
+
top: 0;
|
|
277
|
+
height: 100vh;
|
|
278
|
+
overflow-y: auto;
|
|
279
|
+
padding: 2rem 1rem 2rem 1.5rem;
|
|
280
|
+
border-right: 1px solid var(--border);
|
|
281
|
+
}}
|
|
282
|
+
|
|
283
|
+
.main-content {{
|
|
284
|
+
flex: 1;
|
|
285
|
+
max-width: 800px;
|
|
286
|
+
padding: 2rem 3rem;
|
|
287
|
+
}}
|
|
288
|
+
|
|
289
|
+
@media (max-width: 900px) {{
|
|
290
|
+
.sidebar {{ display: none; }}
|
|
291
|
+
.main-content {{ padding: 1.5rem; }}
|
|
292
|
+
}}
|
|
293
|
+
|
|
294
|
+
/* Sidebar TOC */
|
|
295
|
+
.sidebar-title {{
|
|
296
|
+
font-size: 0.875rem;
|
|
297
|
+
font-weight: 600;
|
|
298
|
+
text-transform: uppercase;
|
|
299
|
+
letter-spacing: 0.05em;
|
|
300
|
+
color: var(--text-secondary);
|
|
301
|
+
margin-bottom: 1rem;
|
|
302
|
+
}}
|
|
303
|
+
|
|
304
|
+
.toc-nav {{
|
|
305
|
+
list-style: none;
|
|
306
|
+
padding: 0;
|
|
307
|
+
}}
|
|
308
|
+
|
|
309
|
+
.toc-nav li {{
|
|
310
|
+
margin: 0;
|
|
311
|
+
}}
|
|
312
|
+
|
|
313
|
+
.toc-nav a {{
|
|
314
|
+
display: block;
|
|
315
|
+
padding: 0.4rem 0.5rem;
|
|
316
|
+
font-size: 0.9375rem;
|
|
317
|
+
color: var(--text-secondary);
|
|
318
|
+
text-decoration: none;
|
|
319
|
+
border-radius: 4px;
|
|
320
|
+
border-left: 2px solid transparent;
|
|
321
|
+
transition: all 0.15s ease;
|
|
322
|
+
line-height: 1.5;
|
|
323
|
+
}}
|
|
324
|
+
|
|
325
|
+
.toc-nav a:hover {{
|
|
326
|
+
color: var(--text);
|
|
327
|
+
background: var(--accent-light);
|
|
328
|
+
}}
|
|
329
|
+
|
|
330
|
+
.toc-nav a.active {{
|
|
331
|
+
color: var(--accent);
|
|
332
|
+
border-left-color: var(--accent);
|
|
333
|
+
font-weight: 500;
|
|
334
|
+
}}
|
|
335
|
+
|
|
336
|
+
/* TOC indentation levels */
|
|
337
|
+
.toc-level-1 a {{
|
|
338
|
+
font-weight: 600;
|
|
339
|
+
font-size: 1rem;
|
|
340
|
+
padding-left: 0.5rem;
|
|
341
|
+
}}
|
|
342
|
+
|
|
343
|
+
.toc-level-2 a {{
|
|
344
|
+
padding-left: 1.25rem;
|
|
345
|
+
font-size: 0.9375rem;
|
|
346
|
+
}}
|
|
347
|
+
|
|
348
|
+
.toc-level-3 a {{
|
|
349
|
+
padding-left: 2rem;
|
|
350
|
+
font-size: 0.875rem;
|
|
351
|
+
opacity: 0.85;
|
|
352
|
+
}}
|
|
353
|
+
|
|
354
|
+
.toc-level-4 a {{
|
|
355
|
+
padding-left: 2.75rem;
|
|
356
|
+
font-size: 0.8125rem;
|
|
357
|
+
opacity: 0.7;
|
|
358
|
+
}}
|
|
359
|
+
|
|
360
|
+
.toc-nav {{
|
|
361
|
+
list-style: none;
|
|
362
|
+
padding: 0;
|
|
363
|
+
}}
|
|
364
|
+
|
|
365
|
+
.toc-nav li {{
|
|
366
|
+
margin: 0;
|
|
367
|
+
}}
|
|
368
|
+
|
|
369
|
+
.toc-nav a {{
|
|
370
|
+
display: block;
|
|
371
|
+
padding: 0.3rem 0.5rem;
|
|
372
|
+
font-size: 0.8125rem;
|
|
373
|
+
color: var(--text-secondary);
|
|
374
|
+
text-decoration: none;
|
|
375
|
+
border-radius: 4px;
|
|
376
|
+
border-left: 2px solid transparent;
|
|
377
|
+
transition: all 0.15s ease;
|
|
378
|
+
line-height: 1.4;
|
|
379
|
+
}}
|
|
380
|
+
|
|
381
|
+
.toc-nav a:hover {{
|
|
382
|
+
color: var(--text);
|
|
383
|
+
background: var(--accent-light);
|
|
384
|
+
}}
|
|
385
|
+
|
|
386
|
+
.toc-nav a.active {{
|
|
387
|
+
color: var(--accent);
|
|
388
|
+
border-left-color: var(--accent);
|
|
389
|
+
font-weight: 500;
|
|
390
|
+
}}
|
|
391
|
+
|
|
392
|
+
.toc-level-1 a {{
|
|
393
|
+
font-weight: 600;
|
|
394
|
+
font-size: 0.875rem;
|
|
395
|
+
padding-left: 0.5rem;
|
|
396
|
+
}}
|
|
397
|
+
|
|
398
|
+
.toc-level-2 a {{
|
|
399
|
+
padding-left: 1.25rem;
|
|
400
|
+
font-size: 0.8125rem;
|
|
401
|
+
}}
|
|
402
|
+
|
|
403
|
+
.toc-level-3 a {{
|
|
404
|
+
padding-left: 2rem;
|
|
405
|
+
font-size: 0.75rem;
|
|
406
|
+
opacity: 0.85;
|
|
407
|
+
}}
|
|
408
|
+
|
|
409
|
+
.toc-level-4 a {{
|
|
410
|
+
padding-left: 2.75rem;
|
|
411
|
+
font-size: 0.6875rem;
|
|
412
|
+
opacity: 0.7;
|
|
413
|
+
}}
|
|
414
|
+
|
|
415
|
+
/* Typography */
|
|
416
|
+
h1 {{
|
|
417
|
+
font-size: 2rem;
|
|
418
|
+
font-weight: 700;
|
|
419
|
+
margin-bottom: 0.5rem;
|
|
420
|
+
line-height: 1.3;
|
|
421
|
+
}}
|
|
422
|
+
|
|
423
|
+
.meta {{
|
|
424
|
+
color: var(--text-secondary);
|
|
425
|
+
font-size: 0.875rem;
|
|
426
|
+
margin-bottom: 2rem;
|
|
427
|
+
padding-bottom: 1.5rem;
|
|
428
|
+
border-bottom: 1px solid var(--border);
|
|
429
|
+
}}
|
|
430
|
+
|
|
431
|
+
h2 {{
|
|
432
|
+
font-size: 1.5rem;
|
|
433
|
+
font-weight: 600;
|
|
434
|
+
margin: 2.5rem 0 1rem;
|
|
435
|
+
padding-bottom: 0.5rem;
|
|
436
|
+
border-bottom: 2px solid var(--accent-light);
|
|
437
|
+
}}
|
|
438
|
+
|
|
439
|
+
h3 {{
|
|
440
|
+
font-size: 1.125rem;
|
|
441
|
+
font-weight: 600;
|
|
442
|
+
margin: 1.5rem 0 0.75rem;
|
|
443
|
+
}}
|
|
444
|
+
|
|
445
|
+
h4 {{
|
|
446
|
+
font-size: 1rem;
|
|
447
|
+
font-weight: 600;
|
|
448
|
+
margin: 1.25rem 0 0.5rem;
|
|
449
|
+
color: var(--text-secondary);
|
|
450
|
+
}}
|
|
451
|
+
|
|
452
|
+
h5 {{
|
|
453
|
+
font-size: 0.9375rem;
|
|
454
|
+
font-weight: 600;
|
|
455
|
+
margin: 1rem 0 0.5rem;
|
|
456
|
+
color: var(--text-secondary);
|
|
457
|
+
opacity: 0.9;
|
|
458
|
+
}}
|
|
459
|
+
|
|
460
|
+
p {{
|
|
461
|
+
margin-bottom: 1rem;
|
|
462
|
+
}}
|
|
463
|
+
|
|
464
|
+
a {{
|
|
465
|
+
color: var(--accent);
|
|
466
|
+
text-decoration: none;
|
|
467
|
+
}}
|
|
468
|
+
a:hover {{ text-decoration: underline; }}
|
|
469
|
+
|
|
470
|
+
blockquote {{
|
|
471
|
+
border-left: 3px solid var(--accent);
|
|
472
|
+
padding-left: 1rem;
|
|
473
|
+
color: var(--text-secondary);
|
|
474
|
+
margin: 1rem 0;
|
|
475
|
+
}}
|
|
476
|
+
|
|
477
|
+
table {{
|
|
478
|
+
width: 100%;
|
|
479
|
+
border-collapse: collapse;
|
|
480
|
+
margin: 1rem 0;
|
|
481
|
+
font-size: 0.9375rem;
|
|
482
|
+
}}
|
|
483
|
+
|
|
484
|
+
th, td {{
|
|
485
|
+
padding: 0.75rem 1rem;
|
|
486
|
+
border: 1px solid var(--border);
|
|
487
|
+
text-align: left;
|
|
488
|
+
}}
|
|
489
|
+
|
|
490
|
+
th {{
|
|
491
|
+
background: var(--accent-light);
|
|
492
|
+
font-weight: 600;
|
|
493
|
+
}}
|
|
494
|
+
|
|
495
|
+
tr:nth-child(even) td {{
|
|
496
|
+
background: var(--table-stripe);
|
|
497
|
+
}}
|
|
498
|
+
|
|
499
|
+
code {{
|
|
500
|
+
background: var(--code-bg);
|
|
501
|
+
padding: 0.15rem 0.4rem;
|
|
502
|
+
border-radius: 4px;
|
|
503
|
+
font-size: 0.875em;
|
|
504
|
+
font-family: "SF Mono", "Fira Code", "Consolas", monospace;
|
|
505
|
+
}}
|
|
506
|
+
|
|
507
|
+
pre {{
|
|
508
|
+
background: var(--code-bg);
|
|
509
|
+
padding: 1rem 1.25rem;
|
|
510
|
+
border-radius: 8px;
|
|
511
|
+
overflow-x: auto;
|
|
512
|
+
margin: 1rem 0;
|
|
513
|
+
border: 1px solid var(--border);
|
|
514
|
+
}}
|
|
515
|
+
|
|
516
|
+
pre code {{
|
|
517
|
+
background: none;
|
|
518
|
+
padding: 0;
|
|
519
|
+
}}
|
|
520
|
+
|
|
521
|
+
ul, ol {{
|
|
522
|
+
padding-left: 1.5rem;
|
|
523
|
+
margin-bottom: 1rem;
|
|
524
|
+
}}
|
|
525
|
+
|
|
526
|
+
li {{
|
|
527
|
+
margin: 0.35rem 0;
|
|
528
|
+
}}
|
|
529
|
+
|
|
530
|
+
/* Citation link */
|
|
531
|
+
a.cit {{
|
|
532
|
+
display: inline;
|
|
533
|
+
color: var(--accent);
|
|
534
|
+
background: var(--cite-bg);
|
|
535
|
+
border-bottom: 1px dashed var(--cite-border);
|
|
536
|
+
cursor: pointer;
|
|
537
|
+
padding: 0 1px;
|
|
538
|
+
text-decoration: none;
|
|
539
|
+
border-radius: 2px;
|
|
540
|
+
}}
|
|
541
|
+
|
|
542
|
+
a.cit:hover {{
|
|
543
|
+
background: var(--cite-border);
|
|
544
|
+
color: var(--card-bg);
|
|
545
|
+
text-decoration: none;
|
|
546
|
+
}}
|
|
547
|
+
|
|
548
|
+
/* Image zoom */
|
|
549
|
+
.content img {{
|
|
550
|
+
cursor: zoom-in;
|
|
551
|
+
transition: opacity 0.2s ease;
|
|
552
|
+
}}
|
|
553
|
+
.content img:hover {{
|
|
554
|
+
opacity: 0.9;
|
|
555
|
+
}}
|
|
556
|
+
|
|
557
|
+
/* Image lightbox */
|
|
558
|
+
#img-lightbox {{
|
|
559
|
+
display: none;
|
|
560
|
+
position: fixed;
|
|
561
|
+
z-index: 2000;
|
|
562
|
+
top: 0;
|
|
563
|
+
left: 0;
|
|
564
|
+
width: 100%;
|
|
565
|
+
height: 100%;
|
|
566
|
+
background: rgba(0, 0, 0, 0.85);
|
|
567
|
+
justify-content: center;
|
|
568
|
+
align-items: center;
|
|
569
|
+
cursor: zoom-out;
|
|
570
|
+
}}
|
|
571
|
+
#img-lightbox.visible {{
|
|
572
|
+
display: flex;
|
|
573
|
+
}}
|
|
574
|
+
#img-lightbox img {{
|
|
575
|
+
max-width: 90vw;
|
|
576
|
+
max-height: 90vh;
|
|
577
|
+
object-fit: contain;
|
|
578
|
+
border-radius: 4px;
|
|
579
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
|
580
|
+
}}
|
|
581
|
+
|
|
582
|
+
/* Source popup */
|
|
583
|
+
#source-popup {{
|
|
584
|
+
display: none;
|
|
585
|
+
position: fixed;
|
|
586
|
+
z-index: 1000;
|
|
587
|
+
background: var(--popup-bg);
|
|
588
|
+
color: var(--popup-text);
|
|
589
|
+
padding: 0.75rem 1rem;
|
|
590
|
+
border-radius: 8px;
|
|
591
|
+
font-size: 0.8125rem;
|
|
592
|
+
line-height: 1.5;
|
|
593
|
+
max-width: 320px;
|
|
594
|
+
box-shadow: 0 10px 25px rgba(0,0,0,0.3);
|
|
595
|
+
pointer-events: none;
|
|
596
|
+
opacity: 0;
|
|
597
|
+
transition: opacity 0.15s ease;
|
|
598
|
+
}}
|
|
599
|
+
|
|
600
|
+
#source-popup.visible {{
|
|
601
|
+
display: block;
|
|
602
|
+
opacity: 1;
|
|
603
|
+
}}
|
|
604
|
+
|
|
605
|
+
#source-popup.pinned {{
|
|
606
|
+
pointer-events: auto;
|
|
607
|
+
}}
|
|
608
|
+
|
|
609
|
+
#source-popup a {{
|
|
610
|
+
color: var(--popup-link);
|
|
611
|
+
text-decoration: underline;
|
|
612
|
+
}}
|
|
613
|
+
|
|
614
|
+
#source-popup .popup-title {{
|
|
615
|
+
font-weight: 600;
|
|
616
|
+
margin-bottom: 0.25rem;
|
|
617
|
+
}}
|
|
618
|
+
|
|
619
|
+
#source-popup .popup-url {{
|
|
620
|
+
opacity: 0.7;
|
|
621
|
+
font-size: 0.75rem;
|
|
622
|
+
word-break: break-all;
|
|
623
|
+
}}
|
|
624
|
+
|
|
625
|
+
#source-popup .popup-hint {{
|
|
626
|
+
margin-top: 0.35rem;
|
|
627
|
+
opacity: 0.5;
|
|
628
|
+
font-size: 0.6875rem;
|
|
629
|
+
}}
|
|
630
|
+
|
|
631
|
+
/* Sources section */
|
|
632
|
+
.sources-list {{
|
|
633
|
+
padding-left: 1.25rem;
|
|
634
|
+
}}
|
|
635
|
+
|
|
636
|
+
.sources-list li {{
|
|
637
|
+
margin: 0.5rem 0;
|
|
638
|
+
font-size: 0.9375rem;
|
|
639
|
+
}}
|
|
640
|
+
|
|
641
|
+
.source-type {{
|
|
642
|
+
display: inline-block;
|
|
643
|
+
font-size: 0.6875rem;
|
|
644
|
+
padding: 0.1rem 0.5rem;
|
|
645
|
+
border-radius: 10px;
|
|
646
|
+
margin-left: 0.5rem;
|
|
647
|
+
vertical-align: middle;
|
|
648
|
+
opacity: 0.7;
|
|
649
|
+
}}
|
|
650
|
+
|
|
651
|
+
.source-type.official {{ background: #dcfce7; color: #166534; }}
|
|
652
|
+
.source-type.wikipedia {{ background: #dbeafe; color: #1e40af; }}
|
|
653
|
+
.source-type.academic {{ background: #fef3c7; color: #92400e; }}
|
|
654
|
+
.source-type.community {{ background: #f3e8ff; color: #6b21a8; }}
|
|
655
|
+
.source-type.media {{ background: #ffe4e6; color: #9f1239; }}
|
|
656
|
+
|
|
657
|
+
.footer {{
|
|
658
|
+
margin-top: 3rem;
|
|
659
|
+
padding-top: 1.5rem;
|
|
660
|
+
border-top: 1px solid var(--border);
|
|
661
|
+
color: var(--text-secondary);
|
|
662
|
+
font-size: 0.8125rem;
|
|
663
|
+
text-align: center;
|
|
664
|
+
}}
|
|
665
|
+
|
|
666
|
+
::selection {{
|
|
667
|
+
background: var(--accent-light);
|
|
668
|
+
}}
|
|
669
|
+
</style>
|
|
670
|
+
</head>
|
|
671
|
+
<body>
|
|
672
|
+
|
|
673
|
+
<div class="layout">
|
|
674
|
+
|
|
675
|
+
<!-- Sidebar TOC -->
|
|
676
|
+
<aside class="sidebar">
|
|
677
|
+
<div class="sidebar-title">目录</div>
|
|
678
|
+
<ol class="toc-nav">
|
|
679
|
+
{toc_html}
|
|
680
|
+
</ol>
|
|
681
|
+
</aside>
|
|
682
|
+
|
|
683
|
+
<!-- Main content -->
|
|
684
|
+
<main class="main-content" id="content">
|
|
685
|
+
<!-- Content rendered from Markdown -->
|
|
686
|
+
</main>
|
|
687
|
+
|
|
688
|
+
</div>
|
|
689
|
+
|
|
690
|
+
<!-- Source popup -->
|
|
691
|
+
<div id="source-popup">
|
|
692
|
+
<div class="popup-title" id="popup-title"></div>
|
|
693
|
+
<div class="popup-url" id="popup-url"></div>
|
|
694
|
+
<div class="popup-hint">点击链接访问原文</div>
|
|
695
|
+
</div>
|
|
696
|
+
|
|
697
|
+
<!-- Image lightbox -->
|
|
698
|
+
<div id="img-lightbox">
|
|
699
|
+
<img src="" alt="Zoomed image" id="lightbox-img">
|
|
700
|
+
</div>
|
|
701
|
+
|
|
702
|
+
<script>
|
|
703
|
+
// === Embedded Markdown content ===
|
|
704
|
+
const MARKDOWN_CONTENT = {md_content_json};
|
|
705
|
+
|
|
706
|
+
// === Source data ===
|
|
707
|
+
const sources = {sources_json};
|
|
708
|
+
|
|
709
|
+
function sourceTitle(s) {{
|
|
710
|
+
if (!s) return '来源';
|
|
711
|
+
return String(s.title || s.project || s.url || (s.urls && s.urls[0]) || '来源');
|
|
712
|
+
}}
|
|
713
|
+
function sourceUrl(s) {{
|
|
714
|
+
if (!s) return '#';
|
|
715
|
+
return s.url || (s.urls && s.urls[0]) || '#';
|
|
716
|
+
}}
|
|
717
|
+
|
|
718
|
+
// === Markdown parser ===
|
|
719
|
+
function parseMarkdown(md) {{
|
|
720
|
+
const lines = md.split('\\n');
|
|
721
|
+
let html = '';
|
|
722
|
+
let inTable = false;
|
|
723
|
+
let inCode = false;
|
|
724
|
+
let inUl = false;
|
|
725
|
+
let inOl = false;
|
|
726
|
+
|
|
727
|
+
function escapeHtml(text) {{
|
|
728
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
729
|
+
}}
|
|
730
|
+
|
|
731
|
+
function slugify(text) {{
|
|
732
|
+
text = text.replace(/\\*\\*([^*]+)\\*\\*/g, '$1');
|
|
733
|
+
text = text.replace(/\\*([^*]+)\\*/g, '$1');
|
|
734
|
+
text = text.replace(/`([^`]+)`/g, '$1');
|
|
735
|
+
return text.replace(/[^\\w\\u4e00-\\u9fff]+/g, '-').toLowerCase().replace(/^-|-$/g, '').replace(/-+/g, '-').substring(0, 60);
|
|
736
|
+
}}
|
|
737
|
+
|
|
738
|
+
function renderInline(text) {{
|
|
739
|
+
// Code
|
|
740
|
+
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
741
|
+
// Images:  → <img>
|
|
742
|
+
text = text.replace(/!\\[([^\\]]*)\\]\\(([^)]+)\\)/g, function(m, alt, url) {{
|
|
743
|
+
const isLocal = !url.startsWith('http');
|
|
744
|
+
const src = url;
|
|
745
|
+
return '<img src="' + src + '" alt="' + alt + '" style="max-width:100%;border-radius:8px;margin:1rem 0;border:1px solid var(--border)">';
|
|
746
|
+
}});
|
|
747
|
+
// Links
|
|
748
|
+
text = text.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
|
|
749
|
+
// Bold
|
|
750
|
+
text = text.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
|
|
751
|
+
// Italic
|
|
752
|
+
text = text.replace(/\\*([^*]+)\\*/g, '<em>$1</em>');
|
|
753
|
+
return text;
|
|
754
|
+
}}
|
|
755
|
+
|
|
756
|
+
function renderCitations(text) {{
|
|
757
|
+
return text.replace(/\\[(\\d+)\\]/g, function(m, idx) {{
|
|
758
|
+
const i = parseInt(idx) - 1;
|
|
759
|
+
if (i >= 0 && i < sources.length) {{
|
|
760
|
+
const s = sources[i];
|
|
761
|
+
const title = sourceTitle(s).substring(0, 40).replace(/"/g, '"');
|
|
762
|
+
const url = sourceUrl(s);
|
|
763
|
+
return '<a class="cit" href="' + url + '" target="_blank" rel="noopener" data-source-idx="' + i + '" title="' + title + '">[' + idx + ']</a>';
|
|
764
|
+
}}
|
|
765
|
+
return m;
|
|
766
|
+
}});
|
|
767
|
+
}}
|
|
768
|
+
|
|
769
|
+
function closeLists() {{
|
|
770
|
+
if (inUl) {{ html += '</ul>'; inUl = false; }}
|
|
771
|
+
if (inOl) {{ html += '</ol>'; inOl = false; }}
|
|
772
|
+
if (inTable) {{ html += '</table>'; inTable = false; }}
|
|
773
|
+
}}
|
|
774
|
+
|
|
775
|
+
for (let li = 0; li < lines.length; li++) {{
|
|
776
|
+
const line = lines[li];
|
|
777
|
+
const stripped = line.trim();
|
|
778
|
+
|
|
779
|
+
// Code blocks
|
|
780
|
+
if (stripped.startsWith('```')) {{
|
|
781
|
+
closeLists();
|
|
782
|
+
if (inCode) {{
|
|
783
|
+
html += '</code></pre>';
|
|
784
|
+
inCode = false;
|
|
785
|
+
}} else {{
|
|
786
|
+
const lang = stripped.substring(3).trim();
|
|
787
|
+
html += '<pre><code' + (lang ? ' class="language-' + lang + '"' : '') + '>';
|
|
788
|
+
inCode = true;
|
|
789
|
+
}}
|
|
790
|
+
continue;
|
|
791
|
+
}}
|
|
792
|
+
if (inCode) {{
|
|
793
|
+
html += escapeHtml(line) + '\\n';
|
|
794
|
+
continue;
|
|
795
|
+
}}
|
|
796
|
+
|
|
797
|
+
// Empty line
|
|
798
|
+
if (!stripped) {{
|
|
799
|
+
closeLists();
|
|
800
|
+
continue;
|
|
801
|
+
}}
|
|
802
|
+
|
|
803
|
+
// Horizontal rule
|
|
804
|
+
if (stripped === '---') {{
|
|
805
|
+
closeLists();
|
|
806
|
+
html += '<hr style="border:none;border-top:1px solid var(--border);margin:2rem 0">';
|
|
807
|
+
continue;
|
|
808
|
+
}}
|
|
809
|
+
|
|
810
|
+
// Headings
|
|
811
|
+
const h5Match = stripped.match(/^#####\\s+(.+)$/);
|
|
812
|
+
const h4Match = stripped.match(/^####\\s+(.+)$/);
|
|
813
|
+
const h3Match = stripped.match(/^###\\s+(.+)$/);
|
|
814
|
+
const h2Match = stripped.match(/^##\\s+(.+)$/);
|
|
815
|
+
const h1Match = stripped.match(/^#\\s+(.+)$/);
|
|
816
|
+
|
|
817
|
+
if (h1Match) {{
|
|
818
|
+
closeLists();
|
|
819
|
+
const id = slugify(h1Match[1]);
|
|
820
|
+
html += '<h1 id="' + id + '">' + renderCitations(renderInline(h1Match[1])) + '</h1>';
|
|
821
|
+
continue;
|
|
822
|
+
}}
|
|
823
|
+
if (h2Match) {{
|
|
824
|
+
closeLists();
|
|
825
|
+
const id = slugify(h2Match[1]);
|
|
826
|
+
html += '<h2 id="' + id + '">' + renderCitations(renderInline(h2Match[1])) + '</h2>';
|
|
827
|
+
continue;
|
|
828
|
+
}}
|
|
829
|
+
if (h3Match) {{
|
|
830
|
+
closeLists();
|
|
831
|
+
const id = slugify(h3Match[1]);
|
|
832
|
+
html += '<h3 id="' + id + '">' + renderCitations(renderInline(h3Match[1])) + '</h3>';
|
|
833
|
+
continue;
|
|
834
|
+
}}
|
|
835
|
+
if (h4Match) {{
|
|
836
|
+
closeLists();
|
|
837
|
+
const id = slugify(h4Match[1]);
|
|
838
|
+
html += '<h4 id="' + id + '">' + renderCitations(renderInline(h4Match[1])) + '</h4>';
|
|
839
|
+
continue;
|
|
840
|
+
}}
|
|
841
|
+
if (h5Match) {{
|
|
842
|
+
closeLists();
|
|
843
|
+
const id = slugify(h5Match[1]);
|
|
844
|
+
html += '<h5 id="' + id + '">' + renderCitations(renderInline(h5Match[1])) + '</h5>';
|
|
845
|
+
continue;
|
|
846
|
+
}}
|
|
847
|
+
|
|
848
|
+
// Table
|
|
849
|
+
if (stripped.includes('|') && stripped.startsWith('|')) {{
|
|
850
|
+
const cells = stripped.replace(/^\\||\\|$/g, '').split('|').map(c => c.trim());
|
|
851
|
+
if (cells.every(c => /^[-:]+$/.test(c))) continue; // skip separator
|
|
852
|
+
if (!inTable) {{
|
|
853
|
+
html += '<table><thead><tr>';
|
|
854
|
+
cells.forEach(c => {{ html += '<th>' + renderCitations(renderInline(c)) + '</th>'; }});
|
|
855
|
+
html += '</tr></thead><tbody>';
|
|
856
|
+
inTable = true;
|
|
857
|
+
}} else {{
|
|
858
|
+
html += '<tr>';
|
|
859
|
+
cells.forEach(c => {{ html += '<td>' + renderCitations(renderInline(c)) + '</td>'; }});
|
|
860
|
+
html += '</tr>';
|
|
861
|
+
}}
|
|
862
|
+
continue;
|
|
863
|
+
}} else {{
|
|
864
|
+
if (inTable) {{ html += '</table>'; inTable = false; }}
|
|
865
|
+
}}
|
|
866
|
+
|
|
867
|
+
// Unordered list
|
|
868
|
+
if (/^[-*]\\s+/.test(stripped)) {{
|
|
869
|
+
if (!inUl) {{
|
|
870
|
+
if (inOl) {{ html += '</ol>'; inOl = false; }}
|
|
871
|
+
html += '<ul>';
|
|
872
|
+
inUl = true;
|
|
873
|
+
}}
|
|
874
|
+
html += '<li>' + renderCitations(renderInline(stripped.replace(/^[-*]\\s+/, ''))) + '</li>';
|
|
875
|
+
continue;
|
|
876
|
+
}}
|
|
877
|
+
|
|
878
|
+
// Ordered list
|
|
879
|
+
if (/^\\d+\\.\\s+/.test(stripped)) {{
|
|
880
|
+
if (!inOl) {{
|
|
881
|
+
if (inUl) {{ html += '</ul>'; inUl = false; }}
|
|
882
|
+
html += '<ol>';
|
|
883
|
+
inOl = true;
|
|
884
|
+
}}
|
|
885
|
+
html += '<li>' + renderCitations(renderInline(stripped.replace(/^\\d+\\.\\s+/, ''))) + '</li>';
|
|
886
|
+
continue;
|
|
887
|
+
}}
|
|
888
|
+
|
|
889
|
+
// Blockquote
|
|
890
|
+
if (stripped.startsWith('> ')) {{
|
|
891
|
+
closeLists();
|
|
892
|
+
html += '<blockquote>' + renderCitations(renderInline(stripped.substring(2))) + '</blockquote>';
|
|
893
|
+
continue;
|
|
894
|
+
}}
|
|
895
|
+
|
|
896
|
+
// Paragraph
|
|
897
|
+
closeLists();
|
|
898
|
+
html += '<p>' + renderCitations(renderInline(stripped)) + '</p>';
|
|
899
|
+
}}
|
|
900
|
+
closeLists();
|
|
901
|
+
return html;
|
|
902
|
+
}}
|
|
903
|
+
|
|
904
|
+
// === Render content ===
|
|
905
|
+
document.getElementById('content').innerHTML = parseMarkdown(MARKDOWN_CONTENT);
|
|
906
|
+
|
|
907
|
+
// === Image lightbox ===
|
|
908
|
+
(function() {{
|
|
909
|
+
const lightbox = document.getElementById('img-lightbox');
|
|
910
|
+
const lightboxImg = document.getElementById('lightbox-img');
|
|
911
|
+
document.addEventListener('click', function(evt) {{
|
|
912
|
+
const img = evt.target.closest('.content img');
|
|
913
|
+
if (img) {{
|
|
914
|
+
lightboxImg.src = img.src;
|
|
915
|
+
lightboxImg.alt = img.alt;
|
|
916
|
+
lightbox.classList.add('visible');
|
|
917
|
+
}} else if (evt.target.closest('#img-lightbox')) {{
|
|
918
|
+
lightbox.classList.remove('visible');
|
|
919
|
+
lightboxImg.src = '';
|
|
920
|
+
}}
|
|
921
|
+
}});
|
|
922
|
+
document.addEventListener('keydown', function(evt) {{
|
|
923
|
+
if (evt.key === 'Escape') lightbox.classList.remove('visible');
|
|
924
|
+
}});
|
|
925
|
+
}})();
|
|
926
|
+
|
|
927
|
+
// === Popup interaction ===
|
|
928
|
+
(function() {{
|
|
929
|
+
const popup = document.getElementById('source-popup');
|
|
930
|
+
const popupTitle = document.getElementById('popup-title');
|
|
931
|
+
const popupUrl = document.getElementById('popup-url');
|
|
932
|
+
let pinnedSource = null;
|
|
933
|
+
|
|
934
|
+
function showPopup(el) {{
|
|
935
|
+
const idx = parseInt(el.dataset.sourceIdx);
|
|
936
|
+
const s = sources[idx];
|
|
937
|
+
if (!s) return;
|
|
938
|
+
const title = sourceTitle(s);
|
|
939
|
+
const url = sourceUrl(s);
|
|
940
|
+
popupTitle.textContent = title;
|
|
941
|
+
popupUrl.innerHTML = '<a href="' + url + '" target="_blank" rel="noopener">' + url + '</a>';
|
|
942
|
+
const rect = el.getBoundingClientRect();
|
|
943
|
+
let x = rect.left;
|
|
944
|
+
let y = rect.bottom + 8;
|
|
945
|
+
if (x + 320 > window.innerWidth) x = window.innerWidth - 330;
|
|
946
|
+
if (y + 100 > window.innerHeight) y = rect.top - 100;
|
|
947
|
+
popup.style.left = x + 'px';
|
|
948
|
+
popup.style.top = y + 'px';
|
|
949
|
+
popup.classList.add('visible');
|
|
950
|
+
pinnedSource = idx;
|
|
951
|
+
}}
|
|
952
|
+
|
|
953
|
+
function hidePopup() {{
|
|
954
|
+
popup.classList.remove('visible');
|
|
955
|
+
popup.classList.remove('pinned');
|
|
956
|
+
pinnedSource = null;
|
|
957
|
+
}}
|
|
958
|
+
|
|
959
|
+
// Click on citation: toggle pin
|
|
960
|
+
document.addEventListener('click', function(evt) {{
|
|
961
|
+
const cit = evt.target.closest('.cit');
|
|
962
|
+
if (cit) {{
|
|
963
|
+
evt.preventDefault();
|
|
964
|
+
evt.stopPropagation();
|
|
965
|
+
const idx = parseInt(cit.dataset.sourceIdx);
|
|
966
|
+
if (pinnedSource === idx) {{
|
|
967
|
+
hidePopup();
|
|
968
|
+
}} else {{
|
|
969
|
+
showPopup(cit);
|
|
970
|
+
popup.classList.add('pinned');
|
|
971
|
+
}}
|
|
972
|
+
return;
|
|
973
|
+
}}
|
|
974
|
+
// Click inside pinned popup → allow link navigation
|
|
975
|
+
if (evt.target.closest('#source-popup')) return;
|
|
976
|
+
// Click elsewhere → close
|
|
977
|
+
hidePopup();
|
|
978
|
+
}});
|
|
979
|
+
|
|
980
|
+
// Hover: preview (only when not pinned)
|
|
981
|
+
document.addEventListener('mouseover', function(evt) {{
|
|
982
|
+
const cit = evt.target.closest('.cit');
|
|
983
|
+
if (cit && !pinnedSource) showPopup(cit);
|
|
984
|
+
}});
|
|
985
|
+
document.addEventListener('mouseout', function(evt) {{
|
|
986
|
+
const cit = evt.target.closest('.cit');
|
|
987
|
+
if (cit && !pinnedSource) hidePopup();
|
|
988
|
+
}});
|
|
989
|
+
|
|
990
|
+
// Selection: detect citation
|
|
991
|
+
document.addEventListener('mouseup', function(evt) {{
|
|
992
|
+
const sel = window.getSelection();
|
|
993
|
+
if (sel.isCollapsed) return;
|
|
994
|
+
const range = sel.getRangeAt(0);
|
|
995
|
+
let node = range.startContainer;
|
|
996
|
+
if (node.nodeType === 3) node = node.parentNode;
|
|
997
|
+
const cit = node.closest('.cit');
|
|
998
|
+
if (cit && !pinnedSource) showPopup(cit);
|
|
999
|
+
}});
|
|
1000
|
+
|
|
1001
|
+
// Escape closes
|
|
1002
|
+
document.addEventListener('keydown', function(evt) {{
|
|
1003
|
+
if (evt.key === 'Escape') hidePopup();
|
|
1004
|
+
}});
|
|
1005
|
+
}})();
|
|
1006
|
+
|
|
1007
|
+
// === Scroll-spy ===
|
|
1008
|
+
(function() {{
|
|
1009
|
+
const tocLinks = document.querySelectorAll('.toc-nav a');
|
|
1010
|
+
const headingElements = new Map();
|
|
1011
|
+
tocLinks.forEach(function(link) {{
|
|
1012
|
+
const targetId = link.getAttribute('href').slice(1);
|
|
1013
|
+
const el = document.getElementById(targetId);
|
|
1014
|
+
if (el) headingElements.set(targetId, el);
|
|
1015
|
+
}});
|
|
1016
|
+
|
|
1017
|
+
function updateActiveHeading() {{
|
|
1018
|
+
let current = '';
|
|
1019
|
+
const scrollPos = window.scrollY + 100;
|
|
1020
|
+
headingElements.forEach(function(el, id) {{
|
|
1021
|
+
if (el.offsetTop <= scrollPos) current = id;
|
|
1022
|
+
}});
|
|
1023
|
+
tocLinks.forEach(function(link) {{
|
|
1024
|
+
link.classList.toggle('active', link.getAttribute('href') === '#' + current);
|
|
1025
|
+
}});
|
|
1026
|
+
}}
|
|
1027
|
+
|
|
1028
|
+
window.addEventListener('scroll', updateActiveHeading, {{ passive: true }});
|
|
1029
|
+
updateActiveHeading();
|
|
1030
|
+
}})();
|
|
1031
|
+
</script>
|
|
1032
|
+
|
|
1033
|
+
</body>
|
|
1034
|
+
</html>
|
|
1035
|
+
"""
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def extract_headings_for_toc(md_text):
|
|
1039
|
+
"""Extract headings from markdown for hierarchical TOC."""
|
|
1040
|
+
headings = []
|
|
1041
|
+
in_code = False
|
|
1042
|
+
for line in md_text.split('\n'):
|
|
1043
|
+
stripped = line.strip()
|
|
1044
|
+
if stripped.startswith('```'):
|
|
1045
|
+
in_code = not in_code
|
|
1046
|
+
continue
|
|
1047
|
+
if in_code:
|
|
1048
|
+
continue
|
|
1049
|
+
if stripped.startswith('##### '):
|
|
1050
|
+
headings.append((5, stripped[6:].strip()))
|
|
1051
|
+
elif stripped.startswith('#### '):
|
|
1052
|
+
headings.append((4, stripped[5:].strip()))
|
|
1053
|
+
elif stripped.startswith('### '):
|
|
1054
|
+
headings.append((3, stripped[4:].strip()))
|
|
1055
|
+
elif stripped.startswith('## '):
|
|
1056
|
+
headings.append((2, stripped[3:].strip()))
|
|
1057
|
+
elif stripped.startswith('# '):
|
|
1058
|
+
headings.append((1, stripped[2:].strip()))
|
|
1059
|
+
return headings
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
def generate_html(topic, angles_data, md_content=None):
|
|
1063
|
+
"""Generate HTML template from angle data.
|
|
1064
|
+
If md_content is provided, uses it; otherwise generates from angles_data.
|
|
1065
|
+
Returns (html_content, all_sources).
|
|
1066
|
+
"""
|
|
1067
|
+
if md_content is None:
|
|
1068
|
+
md_content, all_sources = generate_markdown(topic, angles_data)
|
|
1069
|
+
else:
|
|
1070
|
+
all_sources = []
|
|
1071
|
+
for a in angles_data:
|
|
1072
|
+
for src in a.get('sources', []):
|
|
1073
|
+
all_sources.append(normalize_source(src))
|
|
1074
|
+
|
|
1075
|
+
# Extract headings for TOC
|
|
1076
|
+
headings = extract_headings_for_toc(md_content)
|
|
1077
|
+
|
|
1078
|
+
# Build hierarchical TOC HTML
|
|
1079
|
+
toc_html = '<li class="toc-level-1"><a href="#概述">概述</a></li>\n'
|
|
1080
|
+
for level, title in headings:
|
|
1081
|
+
hid = slugify(title)
|
|
1082
|
+
toc_level = min(level + 1, 4)
|
|
1083
|
+
toc_html += f'<li class="toc-level-{toc_level}"><a href="#{hid}">{escape(title)}</a></li>\n'
|
|
1084
|
+
|
|
1085
|
+
# Embed markdown as JSON string
|
|
1086
|
+
md_json = json.dumps(md_content, ensure_ascii=False)
|
|
1087
|
+
sources_json_str = json.dumps(all_sources, ensure_ascii=False)
|
|
1088
|
+
|
|
1089
|
+
html = HTML_TEMPLATE.format(
|
|
1090
|
+
title=escape(topic),
|
|
1091
|
+
toc_html=toc_html,
|
|
1092
|
+
md_content_json=md_json,
|
|
1093
|
+
sources_json=sources_json_str,
|
|
1094
|
+
)
|
|
1095
|
+
|
|
1096
|
+
return html, all_sources
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
def main():
|
|
1100
|
+
parser = argparse.ArgumentParser(description='Generate Markdown + interactive HTML report')
|
|
1101
|
+
parser.add_argument('--topic', required=True, help='Research topic')
|
|
1102
|
+
parser.add_argument('--input', required=True, help='Path to JSON file with angle data')
|
|
1103
|
+
parser.add_argument('--output-dir', required=True, help='Output directory')
|
|
1104
|
+
args = parser.parse_args()
|
|
1105
|
+
|
|
1106
|
+
with open(args.input, 'r', encoding='utf-8') as f:
|
|
1107
|
+
angles_data = json.load(f)
|
|
1108
|
+
|
|
1109
|
+
if isinstance(angles_data, dict):
|
|
1110
|
+
angles_data = angles_data.get('angles', angles_data.get('results', []))
|
|
1111
|
+
|
|
1112
|
+
topic = args.topic
|
|
1113
|
+
out_dir = Path(args.output_dir)
|
|
1114
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
1115
|
+
|
|
1116
|
+
# Generate initial Markdown
|
|
1117
|
+
md_content, all_sources = generate_markdown(topic, angles_data)
|
|
1118
|
+
|
|
1119
|
+
# Download images and insert into markdown
|
|
1120
|
+
print('Downloading images...')
|
|
1121
|
+
image_map = collect_and_download_images(angles_data, out_dir)
|
|
1122
|
+
local_count = sum(1 for v in image_map.values() if not v.startswith('http'))
|
|
1123
|
+
remote_count = sum(1 for v in image_map.values() if v.startswith('http'))
|
|
1124
|
+
print(f'Images: {local_count} local, {remote_count} remote')
|
|
1125
|
+
|
|
1126
|
+
md_content = insert_images_into_markdown(md_content, angles_data, image_map, out_dir)
|
|
1127
|
+
|
|
1128
|
+
# Write README.md
|
|
1129
|
+
md_path = out_dir / 'README.md'
|
|
1130
|
+
md_path.write_text(md_content, encoding='utf-8')
|
|
1131
|
+
print(f'Markdown saved to: {md_path}')
|
|
1132
|
+
|
|
1133
|
+
# Generate HTML with updated markdown (including image references)
|
|
1134
|
+
html_content, all_sources = generate_html(topic, angles_data, md_content=md_content)
|
|
1135
|
+
|
|
1136
|
+
# Write index.html
|
|
1137
|
+
html_path = out_dir / 'index.html'
|
|
1138
|
+
html_path.write_text(html_content, encoding='utf-8')
|
|
1139
|
+
print(f'HTML viewer saved to: {html_path}')
|
|
1140
|
+
|
|
1141
|
+
# Write sources.json
|
|
1142
|
+
sources_data = {
|
|
1143
|
+
'generated_at': datetime.now().isoformat(),
|
|
1144
|
+
'sources': all_sources,
|
|
1145
|
+
}
|
|
1146
|
+
sources_path = out_dir / 'sources.json'
|
|
1147
|
+
sources_path.write_text(json.dumps(sources_data, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
1148
|
+
print(f'Sources saved to: {sources_path}')
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
def download_image(url, dest_path, timeout=10):
|
|
1152
|
+
"""Download an image from URL to local path. Returns (success, local_path_or_url)."""
|
|
1153
|
+
try:
|
|
1154
|
+
import urllib.request
|
|
1155
|
+
import urllib.error
|
|
1156
|
+
# Set a user agent to avoid 403 errors
|
|
1157
|
+
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
|
1158
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
1159
|
+
data = response.read()
|
|
1160
|
+
content_type = response.headers.get('Content-Type', '')
|
|
1161
|
+
# Determine extension from content-type or URL
|
|
1162
|
+
ext = '.png'
|
|
1163
|
+
if 'svg' in content_type or url.endswith('.svg'):
|
|
1164
|
+
ext = '.svg'
|
|
1165
|
+
elif 'jpeg' in content_type or 'jpg' in content_type or url.endswith(('.jpg', '.jpeg')):
|
|
1166
|
+
ext = '.jpg'
|
|
1167
|
+
elif 'gif' in content_type or url.endswith('.gif'):
|
|
1168
|
+
ext = '.gif'
|
|
1169
|
+
elif 'webp' in content_type or url.endswith('.webp'):
|
|
1170
|
+
ext = '.webp'
|
|
1171
|
+
elif url.endswith(('.png', '.svg', '.jpg', '.jpeg', '.gif', '.webp')):
|
|
1172
|
+
ext = '.' + url.split('.')[-1].split('?')[0]
|
|
1173
|
+
|
|
1174
|
+
dest_path = dest_path.with_suffix(ext)
|
|
1175
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1176
|
+
dest_path.write_bytes(data)
|
|
1177
|
+
return True, str(dest_path)
|
|
1178
|
+
except Exception as e:
|
|
1179
|
+
return False, url
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
def collect_and_download_images(angles_data, out_dir):
|
|
1183
|
+
"""Collect all image URLs from angles and download them.
|
|
1184
|
+
Returns (image_map, updated_md_insertions) where image_map is {url: local_path}."""
|
|
1185
|
+
images_dir = out_dir / 'images'
|
|
1186
|
+
images_dir.mkdir(exist_ok=True)
|
|
1187
|
+
|
|
1188
|
+
image_map = {} # url -> local_path or original url
|
|
1189
|
+
all_images = [] # list of (index, image_info)
|
|
1190
|
+
|
|
1191
|
+
for a_idx, a in enumerate(angles_data):
|
|
1192
|
+
for img in a.get('images', []):
|
|
1193
|
+
url = img.get('url', '')
|
|
1194
|
+
if not url or url in image_map:
|
|
1195
|
+
continue
|
|
1196
|
+
all_images.append((a_idx, img))
|
|
1197
|
+
|
|
1198
|
+
# Download images
|
|
1199
|
+
for idx, (a_idx, img) in enumerate(all_images):
|
|
1200
|
+
url = img['url']
|
|
1201
|
+
desc = img.get('description', f'Image {idx+1}')
|
|
1202
|
+
|
|
1203
|
+
# SVG files: use original URL (works in browsers and markdown viewers)
|
|
1204
|
+
if url.endswith('.svg') or 'wikipedia.org/wiki/File:' in url:
|
|
1205
|
+
image_map[url] = url
|
|
1206
|
+
continue
|
|
1207
|
+
|
|
1208
|
+
# Generate a filename
|
|
1209
|
+
safe_name = re.sub(r'[^\w]+', '-', desc.lower()).strip('-')[:50]
|
|
1210
|
+
if not safe_name:
|
|
1211
|
+
safe_name = f'image-{idx+1}'
|
|
1212
|
+
dest_path = images_dir / safe_name
|
|
1213
|
+
|
|
1214
|
+
# Check if already downloaded (from a previous run)
|
|
1215
|
+
existing = list(images_dir.glob(f'{safe_name}.*'))
|
|
1216
|
+
if existing:
|
|
1217
|
+
image_map[url] = str(existing[0].relative_to(out_dir)).replace('\\', '/')
|
|
1218
|
+
continue
|
|
1219
|
+
|
|
1220
|
+
success, result = download_image(url, dest_path)
|
|
1221
|
+
if success:
|
|
1222
|
+
image_map[url] = result.replace('\\', '/')
|
|
1223
|
+
print(f' Downloaded: {desc[:40]} -> {result}')
|
|
1224
|
+
else:
|
|
1225
|
+
image_map[url] = url # Fallback to original URL
|
|
1226
|
+
print(f' Failed to download (using original URL)')
|
|
1227
|
+
|
|
1228
|
+
return image_map
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
def insert_images_into_markdown(md_content, angles_data, image_map, out_dir):
|
|
1232
|
+
"""Insert image references into markdown content at appropriate positions.
|
|
1233
|
+
Returns updated markdown content."""
|
|
1234
|
+
# Build a list of images per angle
|
|
1235
|
+
angle_image_refs = {} # angle_index -> list of markdown image refs
|
|
1236
|
+
for a_idx, a in enumerate(angles_data):
|
|
1237
|
+
for img in a.get('images', []):
|
|
1238
|
+
url = img.get('url', '')
|
|
1239
|
+
if not url:
|
|
1240
|
+
continue
|
|
1241
|
+
local_path = image_map.get(url, url)
|
|
1242
|
+
desc = img.get('description', '')
|
|
1243
|
+
source = img.get('source', url)
|
|
1244
|
+
img_md = f'\n\n*来源:[{source})]({source})*'
|
|
1245
|
+
angle_image_refs.setdefault(a_idx, []).append(img_md)
|
|
1246
|
+
|
|
1247
|
+
# Insert images after the first paragraph of each angle section
|
|
1248
|
+
# We'll find each angle's section and insert images after its first paragraph
|
|
1249
|
+
lines = md_content.split('\n')
|
|
1250
|
+
result = []
|
|
1251
|
+
current_angle = -1
|
|
1252
|
+
angle_header_pattern = re.compile(r'^##\s+(.+)')
|
|
1253
|
+
first_para_done = set()
|
|
1254
|
+
|
|
1255
|
+
for i, line in enumerate(lines):
|
|
1256
|
+
result.append(line)
|
|
1257
|
+
|
|
1258
|
+
# Detect angle headers
|
|
1259
|
+
m = angle_header_pattern.match(line)
|
|
1260
|
+
if m:
|
|
1261
|
+
header_text = m.group(1)
|
|
1262
|
+
for a_idx, a in enumerate(angles_data):
|
|
1263
|
+
if a['angle'] == header_text:
|
|
1264
|
+
current_angle = a_idx
|
|
1265
|
+
break
|
|
1266
|
+
|
|
1267
|
+
# After first paragraph in an angle section, insert images
|
|
1268
|
+
if current_angle >= 0 and current_angle not in angle_image_refs:
|
|
1269
|
+
continue
|
|
1270
|
+
if current_angle >= 0 and current_angle not in first_para_done:
|
|
1271
|
+
# Check if this is a paragraph (non-empty, non-header, non-table, non-list)
|
|
1272
|
+
if line and not line.startswith('#') and not line.startswith('|') and not line.startswith(('-', '*', '>', '```')) and not re.match(r'^\d+\.', line):
|
|
1273
|
+
# Check if next line is empty (end of paragraph)
|
|
1274
|
+
next_line = lines[i + 1] if i + 1 < len(lines) else ''
|
|
1275
|
+
if next_line.strip() == '':
|
|
1276
|
+
# Insert images here
|
|
1277
|
+
result.append('')
|
|
1278
|
+
for img_ref in angle_image_refs.get(current_angle, []):
|
|
1279
|
+
result.append(img_ref)
|
|
1280
|
+
result.append('')
|
|
1281
|
+
first_para_done.add(current_angle)
|
|
1282
|
+
|
|
1283
|
+
return '\n'.join(result)
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
if __name__ == '__main__':
|
|
1287
|
+
main()
|