create-dwy 0.16.1 → 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 +9 -0
- package/bin/index.js +1 -1
- package/package.json +1 -1
- package/src/sync-all.js +303 -59
- package/src/sync-global-skills.js +167 -0
- package/src/sync.js +21 -11
- package/templates/ai-tools/CHANGELOG.md +134 -0
- package/templates/ai-tools/VERSION +1 -1
- package/templates/ai-tools/rules/Android/dwy-android-core.md +65 -1
- 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-swift-core.md +46 -0
- 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/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//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/unitcircle/uc-deploy-audit/SKILL.md +0 -511
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-data.md +0 -126
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-network.md +0 -88
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-runtime.md +0 -195
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/security-doc-search.md +0 -179
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_base.sh +0 -39
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_capacity.sh +0 -278
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_db.sh +0 -147
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_docker.sh +0 -230
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_env.sh +0 -64
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_https.sh +0 -60
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_logs.sh +0 -209
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_nginx.sh +0 -100
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_resilience.sh +0 -157
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_secrets.sh +0 -265
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_services.sh +0 -94
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_ssh.sh +0 -56
- package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/run_all.sh +0 -105
|
@@ -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()
|