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.
Files changed (102) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/bin/index.js +1 -1
  3. package/package.json +3 -2
  4. package/src/searchable-select.js +419 -0
  5. package/src/sync-all.js +313 -65
  6. package/src/sync-global-skills.js +167 -0
  7. package/src/sync.js +59 -43
  8. package/templates/ai-tools/CHANGELOG.md +210 -0
  9. package/templates/ai-tools/VERSION +1 -0
  10. package/templates/ai-tools/hook-manifests/hooks.json +7 -0
  11. package/templates/ai-tools/hooks/Git/pre-git-commit-ai-signature-check.sh +57 -0
  12. package/templates/ai-tools/rules/Android/dwy-android-core.md +280 -5
  13. package/templates/ai-tools/rules/Docker/dwy-docker.md +52 -1
  14. package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-build.md +50 -1
  15. package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-design.md +139 -0
  16. package/templates/ai-tools/rules/HarmonyOS/dwy-harmony-pitfalls.md +72 -0
  17. package/templates/ai-tools/rules/Python/dwy-python-core.md +47 -0
  18. package/templates/ai-tools/rules/Vue/dwy-vue-core.md +55 -0
  19. package/templates/ai-tools/rules/iOS/dwy-ios-pitfalls.md +7 -0
  20. package/templates/ai-tools/rules/iOS/dwy-swift-core.md +46 -0
  21. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-git-commit.md +9 -6
  22. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-oss-commercial-license.md +128 -0
  23. package/templates/ai-tools/skills//345/205/203/345/267/245/345/205/267/dwy-shared/SKILL.md +22 -1
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/SKILL.md +11 -8
  31. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/android.md +2 -1
  32. 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
  33. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/changelog.md +24 -4
  34. 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
  35. 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
  36. 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
  37. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/harmony.md +2 -1
  38. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/ios.md +3 -2
  39. 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
  40. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-semver/SKILL.md +1 -1
  41. package/templates/ai-tools/skills//345/237/272/347/241/200/345/272/223/dwy-eapi/references/tasks-integration-guide.md +2 -2
  42. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/SKILL.md +1 -1
  43. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/references/checks-runtime.md +2 -2
  44. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_docker.sh +3 -3
  45. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_secrets.sh +1 -1
  46. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/README.md +57 -0
  47. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/SKILL.md +147 -0
  48. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/references/api.md +43 -0
  49. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/references/config.example.yaml +33 -0
  50. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/global_config.py +89 -0
  51. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/setup_config.py +175 -0
  52. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/dwy-doubao-tts/scripts/synthesize.py +339 -0
  53. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/SKILL.md +127 -0
  54. 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
  55. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/references/platform-copy-examples.md +37 -0
  56. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/media-platform-packaging/references/platform-posting-times.md +193 -0
  57. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0005-ideation/SKILL.md +42 -0
  58. 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
  59. 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
  60. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0010-product/SKILL.md +106 -0
  61. 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
  62. 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
  63. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/SKILL.md +60 -0
  64. 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
  65. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/scripts/generate_html.py +1287 -0
  66. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0030-facts/scripts/generate_report.py +189 -0
  67. 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
  68. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/SKILL.md +36 -0
  69. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0040-script/templates/README.md +10 -0
  70. 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
  71. 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
  72. 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
  73. 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
  74. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0050-design/SKILL.md +55 -0
  75. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0060-assets/SKILL.md +29 -0
  76. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/SKILL.md +42 -0
  77. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/scripts/validate_shots.py +207 -0
  78. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0070-package/shots.schema.json +186 -0
  79. 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
  80. 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
  81. 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
  82. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-0080-factory/SKILL.md +58 -0
  83. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-comic-kit/SKILL.md +424 -0
  84. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-comic-kit/evals.md +41 -0
  85. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-flow/CONTRACT.md +123 -0
  86. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-flow/SKILL.md +81 -0
  87. 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
  88. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/SKILL.md +78 -0
  89. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/agents/openai.yaml +4 -0
  90. 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
  91. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/AudioTracks.tsx +20 -0
  92. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/CaptionLayer.tsx +18 -0
  93. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/CueTimeline.ts +24 -0
  94. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/assets/remotion/KnowledgeTownComposition.tsx +21 -0
  95. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/references/cinematic-direction.md +73 -0
  96. 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
  97. package/templates/ai-tools/skills//350/207/252/345/252/222/344/275/223/uc-media-knowledge-town/references/style-guide.md +43 -0
  98. 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
  99. 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
  100. 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
  101. package/templates/ai-tools/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-first/SKILL.md +176 -0
  102. 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,430 @@
1
+ #!/usr/bin/env python3
2
+ """Build a facts-only HTML viewer from 事实-*.md + sources.json.
3
+
4
+ - Renders only factual content (TOC + body + figures + references).
5
+ - Inline [[cite:S1]] / [[cite:S1,S2]] become hover markers showing
6
+ site, authority, and URL from sources.json.
7
+ - Sections titled like 成片裁切备忘 are collapsed by default (author memo).
8
+
9
+ Usage:
10
+ python3 build_facts_html.py \\
11
+ --md path/to/事实-xxx.md \\
12
+ --sources path/to/sources.json \\
13
+ --out path/to/index.html
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import html
20
+ import json
21
+ import re
22
+ from pathlib import Path
23
+ from urllib.parse import urlparse
24
+
25
+
26
+ CITE_RE = re.compile(r"\[\[cite:([A-Za-z0-9_,\s]+)\]\]")
27
+ HEADING_RE = re.compile(r"^(#{1,4})\s+(.+)$", re.M)
28
+ IMG_RE = re.compile(r"!\[([^\]]*)\]\(([^)]+)\)")
29
+ LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
30
+ CODE_FENCE_RE = re.compile(r"```(\w*)\n(.*?)```", re.S)
31
+
32
+
33
+ def slugify(text: str) -> str:
34
+ text = re.sub(r"<[^>]+>", "", text)
35
+ text = re.sub(r"[^\w\u4e00-\u9fff]+", "-", text.strip()).lower().strip("-")
36
+ return re.sub(r"-+", "-", text)[:80] or "section"
37
+
38
+
39
+ def load_sources(path: Path) -> dict[str, dict]:
40
+ data = json.loads(path.read_text(encoding="utf-8"))
41
+ items = data.get("sources", data if isinstance(data, list) else [])
42
+ out = {}
43
+ for s in items:
44
+ sid = s["id"]
45
+ if "site" not in s or not s["site"]:
46
+ host = urlparse(s.get("url", "")).hostname or ""
47
+ s = {**s, "site": host}
48
+ if "authority_label_zh" not in s:
49
+ s = {
50
+ **s,
51
+ "authority_label_zh": AUTHORITY_ZH.get(
52
+ s.get("authority", ""), s.get("authority", "未分级")
53
+ ),
54
+ }
55
+ out[sid] = s
56
+ return out
57
+
58
+
59
+ AUTHORITY_ZH = {
60
+ "primary_official": "官方一手(一级)",
61
+ "primary_academic": "学术论文(一级)",
62
+ "secondary_edu": "教材/高校(二级)",
63
+ "secondary_tech": "技术解读(二级·需交叉验证)",
64
+ "media": "媒体(仅背景)",
65
+ }
66
+
67
+
68
+ def cite_html(ids: list[str], sources: dict[str, dict]) -> str:
69
+ parts = []
70
+ for sid in ids:
71
+ s = sources.get(sid)
72
+ if not s:
73
+ parts.append(
74
+ f'<sup class="cite missing" title="未知来源 {html.escape(sid)}">{html.escape(sid)}</sup>'
75
+ )
76
+ continue
77
+ title = html.escape(s.get("title", sid))
78
+ site = html.escape(s.get("site", ""))
79
+ auth = html.escape(s.get("authority_label_zh", s.get("authority", "")))
80
+ url = html.escape(s.get("url", "#"))
81
+ tip = (
82
+ f'<span class="cite-tip">'
83
+ f"<strong>{title}</strong><br>"
84
+ f"网站:{site}<br>"
85
+ f"权威性:{auth}<br>"
86
+ f'链接:<a href="{url}" target="_blank" rel="noopener">{url}</a>'
87
+ f"</span>"
88
+ )
89
+ parts.append(
90
+ f'<sup class="cite" tabindex="0" data-cite="{html.escape(sid)}">'
91
+ f'<a href="#ref-{html.escape(sid)}">{html.escape(sid)}</a>{tip}</sup>'
92
+ )
93
+ return "".join(parts)
94
+
95
+
96
+ def replace_cites(text: str, sources: dict[str, dict]) -> str:
97
+ def repl(m: re.Match) -> str:
98
+ ids = [x.strip() for x in m.group(1).split(",") if x.strip()]
99
+ return cite_html(ids, sources)
100
+
101
+ return CITE_RE.sub(repl, text)
102
+
103
+
104
+ def inline_format(text: str, sources: dict[str, dict]) -> str:
105
+ text = replace_cites(text, sources)
106
+ # images first
107
+ text = IMG_RE.sub(
108
+ lambda m: (
109
+ f'<figure class="fig"><img src="{html.escape(m.group(2))}" alt="{html.escape(m.group(1))}" loading="lazy"/>'
110
+ f'<figcaption>{html.escape(m.group(1))}</figcaption></figure>'
111
+ ),
112
+ text,
113
+ )
114
+ # links
115
+ text = LINK_RE.sub(
116
+ lambda m: f'<a href="{html.escape(m.group(2))}" target="_blank" rel="noopener">{html.escape(m.group(1))}</a>',
117
+ text,
118
+ )
119
+ # bold / italic / code
120
+ text = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text)
121
+ text = re.sub(r"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)", r"<em>\1</em>", text)
122
+ text = re.sub(r"`([^`]+)`", r"<code>\1</code>", text)
123
+ return text
124
+
125
+
126
+ def is_memo_heading(title: str) -> bool:
127
+ t = title.strip()
128
+ return any(k in t for k in ("成片裁切", "作者备忘", "口播", "给 0040", "给0040"))
129
+
130
+
131
+ def md_to_html(md: str, sources: dict[str, dict]) -> tuple[str, list[dict]]:
132
+ # strip YAML-like status lines we don't need? keep body
133
+ # remove leading blockquote meta if purely pipeline — keep for context lightly
134
+ lines = md.splitlines()
135
+ toc: list[dict] = []
136
+ body: list[str] = []
137
+ i = 0
138
+ in_memo = False
139
+ memo_buf: list[str] = []
140
+
141
+ def flush_para(buf: list[str], sink: list[str]):
142
+ if not buf:
143
+ return
144
+ para = "\n".join(buf).strip()
145
+ buf.clear()
146
+ if not para:
147
+ return
148
+ if para.startswith("|") and "\n|" in para + "\n":
149
+ sink.append(table_html(para, sources))
150
+ elif para.startswith(">"):
151
+ quote = "\n".join(
152
+ re.sub(r"^>\s?", "", ln) for ln in para.splitlines()
153
+ )
154
+ sink.append(f"<blockquote>{inline_format(quote, sources)}</blockquote>")
155
+ else:
156
+ # single newlines -> space for paragraphs
157
+ p = inline_format(para.replace("\n", " "), sources)
158
+ sink.append(f"<p>{p}</p>")
159
+
160
+ para_buf: list[str] = []
161
+ n = len(lines)
162
+ while i < n:
163
+ line = lines[i]
164
+ # code fence
165
+ if line.strip().startswith("```"):
166
+ flush_para(para_buf, memo_buf if in_memo else body)
167
+ lang = line.strip()[3:].strip()
168
+ i += 1
169
+ code_lines = []
170
+ while i < n and not lines[i].strip().startswith("```"):
171
+ code_lines.append(lines[i])
172
+ i += 1
173
+ if i < n:
174
+ i += 1
175
+ code = html.escape("\n".join(code_lines))
176
+ block = f'<pre class="code"><code class="language-{html.escape(lang)}">{code}</code></pre>'
177
+ (memo_buf if in_memo else body).append(block)
178
+ continue
179
+
180
+ hm = re.match(r"^(#{1,4})\s+(.+)$", line)
181
+ if hm:
182
+ flush_para(para_buf, memo_buf if in_memo else body)
183
+ level = len(hm.group(1))
184
+ title = hm.group(2).strip()
185
+ # close previous memo
186
+ if in_memo and level <= 2:
187
+ body.append(wrap_memo(memo_buf))
188
+ memo_buf = []
189
+ in_memo = False
190
+ if is_memo_heading(title):
191
+ in_memo = True
192
+ sid = slugify(title)
193
+ memo_buf.append(f'<h{level} id="{sid}">{inline_format(title, sources)}</h{level}>')
194
+ toc.append({"level": level, "id": sid, "title": re.sub(r"<[^>]+>", "", title), "memo": True})
195
+ i += 1
196
+ continue
197
+ sid = slugify(title)
198
+ title_html = inline_format(title, sources)
199
+ # strip cites from toc text
200
+ toc_title = CITE_RE.sub("", title)
201
+ toc_title = re.sub(r"[#*`]", "", toc_title).strip()
202
+ toc.append({"level": level, "id": sid, "title": toc_title, "memo": False})
203
+ sink = memo_buf if in_memo else body
204
+ sink.append(f'<h{level} id="{sid}">{title_html}</h{level}>')
205
+ i += 1
206
+ continue
207
+
208
+ if line.strip() == "---":
209
+ flush_para(para_buf, memo_buf if in_memo else body)
210
+ (memo_buf if in_memo else body).append("<hr/>")
211
+ i += 1
212
+ continue
213
+
214
+ if line.strip().startswith("- ") or line.strip().startswith("* "):
215
+ flush_para(para_buf, memo_buf if in_memo else body)
216
+ items = []
217
+ while i < n and (lines[i].strip().startswith("- ") or lines[i].strip().startswith("* ")):
218
+ items.append(lines[i].strip()[2:])
219
+ i += 1
220
+ lis = "".join(f"<li>{inline_format(it, sources)}</li>" for it in items)
221
+ (memo_buf if in_memo else body).append(f"<ul>{lis}</ul>")
222
+ continue
223
+
224
+ if not line.strip():
225
+ flush_para(para_buf, memo_buf if in_memo else body)
226
+ i += 1
227
+ continue
228
+
229
+ para_buf.append(line)
230
+ i += 1
231
+
232
+ flush_para(para_buf, memo_buf if in_memo else body)
233
+ if in_memo and memo_buf:
234
+ body.append(wrap_memo(memo_buf))
235
+
236
+ return "\n".join(body), toc
237
+
238
+
239
+ def wrap_memo(parts: list[str]) -> str:
240
+ inner = "\n".join(parts)
241
+ return (
242
+ '<details class="memo"><summary>作者备忘(默认折叠 · 非正文事实主轴)</summary>'
243
+ f'<div class="memo-body">{inner}</div></details>'
244
+ )
245
+
246
+
247
+ def table_html(md_table: str, sources: dict[str, dict]) -> str:
248
+ rows = [r.strip() for r in md_table.strip().splitlines() if r.strip()]
249
+ if len(rows) < 2:
250
+ return f"<p>{inline_format(md_table, sources)}</p>"
251
+ def cells(row: str) -> list[str]:
252
+ row = row.strip().strip("|")
253
+ return [c.strip() for c in row.split("|")]
254
+
255
+ header = cells(rows[0])
256
+ # skip separator
257
+ body_rows = rows[2:] if re.match(r"^\|?[\s:-]+\|", rows[1]) else rows[1:]
258
+ th = "".join(f"<th>{inline_format(h, sources)}</th>" for h in header)
259
+ trs = []
260
+ for r in body_rows:
261
+ cs = cells(r)
262
+ trs.append("<tr>" + "".join(f"<td>{inline_format(c, sources)}</td>" for c in cs) + "</tr>")
263
+ return f'<div class="table-wrap"><table><thead><tr>{th}</tr></thead><tbody>{"".join(trs)}</tbody></table></div>'
264
+
265
+
266
+ def refs_html(sources: dict[str, dict]) -> str:
267
+ items = []
268
+ for sid in sorted(sources.keys(), key=lambda x: (len(x), x)):
269
+ s = sources[sid]
270
+ items.append(
271
+ f'<li id="ref-{html.escape(sid)}"><strong>{html.escape(sid)}</strong> '
272
+ f'{html.escape(s.get("title", ""))} · '
273
+ f'<span class="meta">{html.escape(s.get("site", ""))} · '
274
+ f'{html.escape(s.get("authority_label_zh", ""))}</span><br>'
275
+ f'<a href="{html.escape(s.get("url", "#"))}" target="_blank" rel="noopener">'
276
+ f'{html.escape(s.get("url", ""))}</a></li>'
277
+ )
278
+ return '<section class="refs"><h2 id="references">参考资料</h2><ol class="ref-list">' + "".join(items) + "</ol></section>"
279
+
280
+
281
+ def toc_html(toc: list[dict]) -> str:
282
+ lis = []
283
+ for t in toc:
284
+ if t.get("memo"):
285
+ continue
286
+ if t["level"] > 3:
287
+ continue
288
+ lis.append(
289
+ f'<li class="toc-level-{t["level"]}"><a href="#{html.escape(t["id"])}">{html.escape(t["title"])}</a></li>'
290
+ )
291
+ lis.append('<li class="toc-level-1"><a href="#references">参考资料</a></li>')
292
+ return f'<nav class="sidebar"><div class="sidebar-title">目录</div><ul class="toc-nav">{"".join(lis)}</ul></nav>'
293
+
294
+
295
+ HTML_SHELL = """<!DOCTYPE html>
296
+ <html lang="zh-CN">
297
+ <head>
298
+ <meta charset="UTF-8"/>
299
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
300
+ <title>{title}</title>
301
+ <style>
302
+ :root {{
303
+ --bg: #f7f8fa; --card: #fff; --text: #1a1a1a; --muted: #64748b;
304
+ --border: #e2e8f0; --accent: #2563eb; --accent-soft: #dbeafe;
305
+ --tip-bg: #0f172a; --tip-text: #f8fafc; --tip-link: #93c5fd;
306
+ --code-bg: #f1f5f9; --sidebar-w: 240px;
307
+ }}
308
+ @media (prefers-color-scheme: dark) {{
309
+ :root {{
310
+ --bg: #0b1220; --card: #111827; --text: #e5e7eb; --muted: #94a3b8;
311
+ --border: #1f2937; --accent: #60a5fa; --accent-soft: #1e3a5f;
312
+ --tip-bg: #020617; --tip-text: #f1f5f9; --tip-link: #93c5fd;
313
+ --code-bg: #0f172a;
314
+ }}
315
+ }}
316
+ * {{ box-sizing: border-box; }}
317
+ body {{ margin: 0; font-family: "Segoe UI", "PingFang SC", "Noto Sans SC", system-ui, sans-serif;
318
+ background: var(--bg); color: var(--text); line-height: 1.75; }}
319
+ .layout {{ display: flex; max-width: 1100px; margin: 0 auto; min-height: 100vh; }}
320
+ .sidebar {{ width: var(--sidebar-w); position: sticky; top: 0; height: 100vh; overflow: auto;
321
+ padding: 1.5rem 1rem; border-right: 1px solid var(--border); flex-shrink: 0; }}
322
+ .sidebar-title {{ font-size: .75rem; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); margin-bottom: .75rem; }}
323
+ .toc-nav {{ list-style: none; padding: 0; margin: 0; }}
324
+ .toc-nav a {{ display: block; padding: .35rem .5rem; color: var(--muted); text-decoration: none;
325
+ border-radius: 6px; border-left: 2px solid transparent; font-size: .85rem; }}
326
+ .toc-nav a:hover {{ background: var(--accent-soft); color: var(--text); }}
327
+ .toc-level-1 a {{ font-weight: 600; color: var(--text); }}
328
+ .toc-level-2 a {{ padding-left: 1rem; }}
329
+ .toc-level-3 a {{ padding-left: 1.5rem; font-size: .8rem; }}
330
+ .main {{ flex: 1; padding: 2rem 2.5rem 4rem; max-width: 820px; }}
331
+ h1 {{ font-size: 1.75rem; line-height: 1.3; margin: 0 0 1rem; }}
332
+ h2 {{ font-size: 1.35rem; margin: 2rem 0 .75rem; padding-top: .5rem; border-top: 1px solid var(--border); }}
333
+ h3 {{ font-size: 1.1rem; margin: 1.5rem 0 .5rem; }}
334
+ h4 {{ font-size: 1rem; margin: 1.25rem 0 .4rem; color: var(--muted); }}
335
+ p {{ margin: .65rem 0; }}
336
+ a {{ color: var(--accent); }}
337
+ hr {{ border: 0; border-top: 1px solid var(--border); margin: 1.5rem 0; }}
338
+ ul {{ padding-left: 1.25rem; }}
339
+ code {{ background: var(--code-bg); padding: .1em .35em; border-radius: 4px; font-size: .9em; }}
340
+ pre.code {{ background: var(--code-bg); padding: 1rem; border-radius: 8px; overflow: auto; font-size: .85rem; }}
341
+ .table-wrap {{ overflow-x: auto; margin: 1rem 0; }}
342
+ table {{ border-collapse: collapse; width: 100%; font-size: .9rem; }}
343
+ th, td {{ border: 1px solid var(--border); padding: .5rem .65rem; vertical-align: top; }}
344
+ th {{ background: var(--accent-soft); text-align: left; }}
345
+ blockquote {{ margin: .75rem 0; padding: .5rem 1rem; border-left: 3px solid var(--accent); color: var(--muted); background: var(--card); }}
346
+ figure.fig {{ margin: 1.25rem 0; padding: .75rem; background: var(--card); border: 1px solid var(--border); border-radius: 10px; }}
347
+ figure.fig img {{ max-width: 100%; height: auto; display: block; margin: 0 auto; border-radius: 6px; }}
348
+ figcaption {{ font-size: .85rem; color: var(--muted); margin-top: .5rem; text-align: center; }}
349
+ sup.cite {{ position: relative; display: inline-block; margin: 0 1px; cursor: help; font-weight: 600; }}
350
+ sup.cite > a {{ color: var(--accent); text-decoration: none; font-size: .75em; padding: 0 .2em; border-radius: 3px; background: var(--accent-soft); }}
351
+ sup.cite .cite-tip {{
352
+ display: none; position: absolute; z-index: 50; left: 50%; transform: translateX(-50%);
353
+ bottom: calc(100% + 8px); width: min(320px, 70vw); padding: .75rem .85rem;
354
+ background: var(--tip-bg); color: var(--tip-text); border-radius: 8px; font-size: .8rem;
355
+ line-height: 1.45; box-shadow: 0 8px 24px rgba(0,0,0,.25); font-weight: 400; text-align: left;
356
+ }}
357
+ sup.cite .cite-tip a {{ color: var(--tip-link); word-break: break-all; }}
358
+ sup.cite:hover .cite-tip, sup.cite:focus .cite-tip, sup.cite:focus-within .cite-tip, sup.cite.open .cite-tip {{ display: block; }}
359
+ sup.cite.missing > a {{ background: #fee2e2; color: #b91c1c; }}
360
+ .memo {{ margin: 2rem 0; border: 1px dashed var(--border); border-radius: 8px; padding: .5rem 1rem; color: var(--muted); }}
361
+ .memo summary {{ cursor: pointer; font-weight: 600; }}
362
+ .refs {{ margin-top: 3rem; }}
363
+ .ref-list {{ padding-left: 1.25rem; }}
364
+ .ref-list li {{ margin: .75rem 0; }}
365
+ .ref-list .meta {{ color: var(--muted); font-size: .9em; }}
366
+ .page-note {{ font-size: .85rem; color: var(--muted); margin-bottom: 1.5rem; }}
367
+ @media (max-width: 900px) {{
368
+ .sidebar {{ display: none; }}
369
+ .main {{ padding: 1.25rem; }}
370
+ }}
371
+ </style>
372
+ </head>
373
+ <body>
374
+ <div class="layout">
375
+ {toc}
376
+ <main class="main">
377
+ <p class="page-note">事实资料库 · 仅展示可溯源技术内容 · 悬停引用标记可查看来源网站 / 权威性 / 链接</p>
378
+ {body}
379
+ {refs}
380
+ </main>
381
+ </div>
382
+ <script>
383
+ document.querySelectorAll('sup.cite').forEach(function(el) {{
384
+ el.addEventListener('click', function(e) {{
385
+ if (window.matchMedia('(hover: none)').matches) {{
386
+ e.preventDefault();
387
+ document.querySelectorAll('sup.cite.open').forEach(function(x) {{ if (x !== el) x.classList.remove('open'); }});
388
+ el.classList.toggle('open');
389
+ }}
390
+ }});
391
+ }});
392
+ document.addEventListener('click', function(e) {{
393
+ if (!e.target.closest('sup.cite')) document.querySelectorAll('sup.cite.open').forEach(function(x) {{ x.classList.remove('open'); }});
394
+ }});
395
+ </script>
396
+ </body>
397
+ </html>
398
+ """
399
+
400
+
401
+ def extract_title(md: str) -> str:
402
+ m = re.search(r"^#\s+(.+)$", md, re.M)
403
+ return m.group(1).strip() if m else "事实资料库"
404
+
405
+
406
+ def main() -> None:
407
+ ap = argparse.ArgumentParser()
408
+ ap.add_argument("--md", required=True, type=Path)
409
+ ap.add_argument("--sources", required=True, type=Path)
410
+ ap.add_argument("--out", required=True, type=Path)
411
+ args = ap.parse_args()
412
+
413
+ md = args.md.read_text(encoding="utf-8")
414
+ sources = load_sources(args.sources)
415
+ body, toc = md_to_html(md, sources)
416
+ title = extract_title(md)
417
+ # strip pipeline meta blockquotes at very top from title display — body already has them
418
+ page = HTML_SHELL.format(
419
+ title=html.escape(CITE_RE.sub("", title)),
420
+ toc=toc_html(toc),
421
+ body=body,
422
+ refs=refs_html(sources),
423
+ )
424
+ args.out.parent.mkdir(parents=True, exist_ok=True)
425
+ args.out.write_text(page, encoding="utf-8")
426
+ print(f"wrote {args.out} ({len(page)} bytes, {len(sources)} sources)")
427
+
428
+
429
+ if __name__ == "__main__":
430
+ main()