skill-forge-hub 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +94 -2
- package/article-to-html/SKILL.md +239 -0
- package/article-to-html/design-system.md +358 -0
- package/article-to-html/scripts/download_roboto.py +38 -0
- package/article-to-html/scripts/extract_docx.py +68 -0
- package/article-to-html/scripts/inline_fonts.py +32 -0
- package/article-to-html/scripts/install_sourcehan.py +78 -0
- package/cli.js +326 -0
- package/daily-report/SKILL.md +54 -0
- package/daily-report/resources/content-rules.md +82 -0
- package/daily-report/resources/data-collection.md +40 -0
- package/daily-report/resources/date-parsing.md +21 -0
- package/daily-report/resources/disk-and-html.md +14 -0
- package/daily-report/resources/industry-dictionary.md +19 -0
- package/daily-report/resources/usage-guide.md +65 -0
- package/package.json +32 -4
- package/spec-doc2spec/SKILL.md +53 -0
- package/spec-spec2doc/SKILL.md +52 -0
- package/template/SKILL.md +42 -0
- package/template/resources/README.md +20 -0
- package/weekly-report/SKILL.md +55 -0
- package/weekly-report/resources/content-rules.md +82 -0
- package/weekly-report/resources/data-collection.md +41 -0
- package/weekly-report/resources/date-parsing.md +20 -0
- package/weekly-report/resources/disk-and-html.md +14 -0
- package/weekly-report/resources/industry-dictionary.md +19 -0
- package/weekly-report/resources/usage-guide.md +68 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# Design System Reference
|
|
2
|
+
|
|
3
|
+
本文件提供文章 HTML 的设计系统:CSS 变量 + 组件类名 + 关键样式片段。执行时按需复制到 `<style>` 中,并根据文章内容裁剪不需要的组件。
|
|
4
|
+
|
|
5
|
+
## 0. 风格菜单(构建前按文章气质选一款)
|
|
6
|
+
|
|
7
|
+
六款版式范式,每款给适用气质与关键参数。选定后其余章节的组件按需裁剪。字体白名单锁死为 Roboto + 思源黑体,任何风格都不许引入其他字体。
|
|
8
|
+
|
|
9
|
+
| 风格 | 适用气质 | 关键参数 |
|
|
10
|
+
|:---|:---|:---|
|
|
11
|
+
| **A. 暗色封面长文风**(默认款) | 科技文、产品文、深度技术长文 | 深色渐变 hero + 浅灰 `--bg:#f6f8fb` 正文 + 彩色圆角组件(第 2-5 节全套)。主题色随文章气质定:科技蓝 `#2563eb`、暖棕金 `#b45309/#6b4a2b`、墨绿 `#0f766e` 等,改 `:root` 与 hero 渐变两处即可 |
|
|
12
|
+
| **B. 文艺排版风** | 散文、人物稿、随笔、书评 | 字体仍为思源黑体(**不改宋体**);背景米白 `#faf7f1`;封面极简:居中大标题 + 细线 + 作者名,无深色块;行距 1.9、字号 17px、栏宽 680px;`p:first-of-type::first-letter{font-size:3em;float:left}` 首字下沉;引用用居中大字号灰字,分割用 `1px solid #e8e0d0` 细线;禁彩色卡片与重阴影——文艺感全靠排版营造 |
|
|
13
|
+
| **C. 极简白** | 干货教程、方法论、SOP | `--bg:#ffffff`;封面白底大黑标题 + 蓝字点缀;组件只用白卡细边框 `1px solid var(--line)`,单色点缀 `#2563eb`,不用多彩色系统;行距 1.8 |
|
|
14
|
+
| **D. 终端极客风** | 硬核技术文、命令行主题 | 全页暗色 `--bg:#0d1117`,正文 `#c9d1d9`,强调绿 `#7ee787`/蓝 `#58a6ff`;封面命令行提示符风格(`$` 前缀标题 + ASCII 装饰);组件边框 `#30363d`、圆角 ≤8、阴影弱化;代码块为主导视觉;`--mono` 仍为思源黑体(禁等宽商业字体) |
|
|
15
|
+
| **E. 卡片流** | 清单体、科普、碎片知识 | `--bg:#f5f6f8`;内容全卡片化(白卡圆角 16 + `--shadow-sm` + emoji 图标领衔);封面渐变卡 + 大 emoji;段间距拉大 |
|
|
16
|
+
| **F. 报纸分栏风** | 深度报道、行业观察 | 白底;正文双栏 `column-count:2;column-gap:40px`(小屏单栏);封面特大黑标题 + 摘要 + 细双线;小标题黑体 + 上下细线;表格跨栏 `column-span:all` |
|
|
17
|
+
|
|
18
|
+
选择原则:科技/产品长文默认 A;文艺散文 B;教程干货 C;硬核技术 D;科普清单 E;报道评论 F。拿不准时用 AskUserQuestion 让用户挑(每款附一句话气质描述)。
|
|
19
|
+
|
|
20
|
+
## 1. 字体与设计变量(必用)
|
|
21
|
+
|
|
22
|
+
```css
|
|
23
|
+
@font-face { font-family:'Roboto'; font-style:normal; font-weight:400; font-display:swap; src:url('assets/fonts/roboto-latin-400-normal.woff2') format('woff2'); }
|
|
24
|
+
@font-face { font-family:'Roboto'; font-style:normal; font-weight:500; font-display:swap; src:url('assets/fonts/roboto-latin-500-normal.woff2') format('woff2'); }
|
|
25
|
+
@font-face { font-family:'Roboto'; font-style:normal; font-weight:700; font-display:swap; src:url('assets/fonts/roboto-latin-700-normal.woff2') format('woff2'); }
|
|
26
|
+
@font-face { font-family:'Roboto'; font-style:normal; font-weight:900; font-display:swap; src:url('assets/fonts/roboto-latin-900-normal.woff2') format('woff2'); }
|
|
27
|
+
|
|
28
|
+
:root{
|
|
29
|
+
--blue:#2563eb; --blue-bg:#ebf0ff; --blue-line:#a1bdfa;
|
|
30
|
+
--red:#e24b4a; --red-bg:#fcebeb; --red-line:#f09595; --red-dark:#a32d2d;
|
|
31
|
+
--green:#16a34a; --green-bg:#ecfdf5; --green-line:#86efac; --green-dark:#15803d;
|
|
32
|
+
--purple:#7c3aed; --purple-bg:#f5f0ff; --purple-line:#c4b5fd; --purple-dark:#6d28d9;
|
|
33
|
+
--teal:#0d9488; --teal-bg:#f0fdfa; --teal-line:#5eead4;
|
|
34
|
+
--orange:#ea580c; --orange-bg:#fff7ed; --orange-line:#fdba74;
|
|
35
|
+
--ink:#1f2937; --ink-2:#4b5563; --ink-3:#6b7280; --ink-4:#9ca3af;
|
|
36
|
+
--bg:#f6f8fb; --card:#ffffff; --line:#e5e7eb;
|
|
37
|
+
--shadow-sm:0 1px 3px rgba(17,24,39,.06),0 1px 2px rgba(17,24,39,.04);
|
|
38
|
+
--shadow-md:0 4px 12px rgba(17,24,39,.08),0 2px 4px rgba(17,24,39,.04);
|
|
39
|
+
--shadow-lg:0 12px 32px rgba(17,24,39,.12),0 4px 8px rgba(17,24,39,.06);
|
|
40
|
+
--sans:'Roboto','Source Han Sans SC';
|
|
41
|
+
--mono:'Source Han Sans SC';
|
|
42
|
+
}
|
|
43
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
44
|
+
body{font-family:var(--sans);background:var(--bg);color:var(--ink);line-height:1.75;font-size:16px}
|
|
45
|
+
.wrap{max-width:880px;margin:0 auto;padding:0 28px}
|
|
46
|
+
section{padding:56px 0 8px}
|
|
47
|
+
h1,h2,h3,h4{line-height:1.35;font-weight:700}
|
|
48
|
+
p{margin:0 0 1.1em}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> 字体纯化原则(绝对纯粹):`--sans` 仅 `'Roboto','Source Han Sans SC'`;`--mono` 仅 `'Source Han Sans SC'`。任何别名(Noto Sans SC、思源黑体)与兜底(苹方/雅黑/系统字体/sans-serif)一律禁止——未装思源黑体时宁可缺字,不可静默回退,截图抽检即能发现。SVG 内 font-family 同样仅两字体。
|
|
52
|
+
> 纯白背景版:把 `--bg:#f6f8fb` 改为 `--bg:#ffffff`(深色 hero/ending/term 组件不受影响)。
|
|
53
|
+
|
|
54
|
+
## 2. 封面 hero(深色渐变 + 流程节点)
|
|
55
|
+
|
|
56
|
+
```css
|
|
57
|
+
.hero{position:relative;overflow:hidden;color:#fff;padding:96px 0 88px;text-align:center;
|
|
58
|
+
background:radial-gradient(1200px 600px at 15% -10%,rgba(96,165,250,.35),transparent 55%),
|
|
59
|
+
radial-gradient(1000px 500px at 110% 30%,rgba(167,139,250,.30),transparent 55%),
|
|
60
|
+
radial-gradient(900px 700px at 50% 120%,rgba(45,212,191,.22),transparent 60%),
|
|
61
|
+
linear-gradient(160deg,#0b1220 0%,#0e1a33 45%,#131c3a 100%)}
|
|
62
|
+
.hero::before{content:"";position:absolute;inset:0;background-image:
|
|
63
|
+
linear-gradient(rgba(148,163,184,.05) 1px,transparent 1px),
|
|
64
|
+
linear-gradient(90deg,rgba(148,163,184,.05) 1px,transparent 1px);
|
|
65
|
+
background-size:44px 44px;pointer-events:none}
|
|
66
|
+
.hero .inner{position:relative;z-index:2}
|
|
67
|
+
.hero .kicker{display:inline-flex;align-items:center;gap:8px;font-size:13px;letter-spacing:.14em;color:#93c5fd;
|
|
68
|
+
background:rgba(59,130,246,.14);border:1px solid rgba(96,165,250,.35);padding:7px 18px;border-radius:999px;margin-bottom:30px}
|
|
69
|
+
.hero h1{font-size:44px;font-weight:900;margin-bottom:18px}
|
|
70
|
+
.hero h1 .grad{background:linear-gradient(90deg,#7dd3fc,#a5b4fc,#c4b5fd);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
|
|
71
|
+
.hero .sub{font-size:19px;color:#cbd5e1;margin-bottom:12px}
|
|
72
|
+
.hero .flow{display:flex;align-items:center;justify-content:center;gap:14px;margin-top:44px;flex-wrap:wrap}
|
|
73
|
+
.hero .flow .node{display:flex;align-items:center;gap:8px;background:rgba(30,41,59,.7);border:1px solid rgba(148,163,184,.3);
|
|
74
|
+
padding:9px 18px;border-radius:12px;font-size:14px;color:#e2e8f0}
|
|
75
|
+
.hero .flow .node .n{width:22px;height:22px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;
|
|
76
|
+
font-size:12px;font-weight:700;color:#fff;background:linear-gradient(135deg,#3b82f6,#8b5cf6)}
|
|
77
|
+
.hero .flow .arrow{color:#64748b;font-size:18px}
|
|
78
|
+
.hero .tagline{margin-top:34px;font-size:15px;color:#94a3b8;letter-spacing:.08em}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 3. 章节标题 + 通用卡片
|
|
82
|
+
|
|
83
|
+
```css
|
|
84
|
+
.sec-head{display:flex;align-items:baseline;gap:14px;margin:0 0 26px;padding-bottom:14px;border-bottom:1px solid var(--line)}
|
|
85
|
+
.sec-head .no{font-weight:900;font-size:30px;color:var(--blue);letter-spacing:-.02em;min-width:52px}
|
|
86
|
+
.sec-head h2{font-size:26px}
|
|
87
|
+
.sec-head .en{font-size:12px;letter-spacing:.12em;color:var(--ink-4);text-transform:uppercase;margin-left:auto}
|
|
88
|
+
.lead{font-size:17px;color:var(--ink-2);margin-bottom:28px}
|
|
89
|
+
.h3{font-size:19px;margin:34px 0 14px;display:flex;align-items:center;gap:10px}
|
|
90
|
+
.h3::before{content:"";width:4px;height:18px;border-radius:2px;background:var(--blue)}
|
|
91
|
+
.card{background:var(--card);border-radius:16px;box-shadow:var(--shadow-sm);border:1px solid var(--line)}
|
|
92
|
+
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:18px}
|
|
93
|
+
.grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
|
|
94
|
+
@media(max-width:720px){.grid-2,.grid-3{grid-template-columns:1fr}}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 4. 图表组件映射表
|
|
98
|
+
|
|
99
|
+
| 源文档图表类型 | 使用组件(类名) | 关键样式要点 |
|
|
100
|
+
|:---|:---|:---|
|
|
101
|
+
| 左右对比(问题 vs 方案) | `.vs-panel`(bad/good 两栏) | 红/绿渐变底、虚线分隔、圆点前缀 |
|
|
102
|
+
| 升级/演变图(A→B) | `.upgrade`(vibe/spec 双栏 + mid 箭头) | 底部渐变色带 band、✓/✕ 前缀 |
|
|
103
|
+
| 强调数据/警示 | `.callout`(大数字 + 说明) | 左边框 5px 主题色 |
|
|
104
|
+
| 原则/铁律条目 | `.rules` + `.rule`(编号角标) | hover 上浮 |
|
|
105
|
+
| 数据对比表 | `.tbl-wrap` + `table` | `td{vertical-align:middle}`、Spec 列加蓝底高亮 |
|
|
106
|
+
| 分层架构图 | `.layers` + `.layer`(l1/l2/l3) | 蓝绿紫三层、fdoc 文档卡、sync-label 竖排同步线 |
|
|
107
|
+
| 特性/优势卡片墙 | `.adv` + `.a`(2×3 或 3×3) | 图标色块 ic-b/g/p/o/t/r |
|
|
108
|
+
| 暗色功能面板 | `.dark-panel` + `.ask-card` | 深蓝渐变 + 点阵背景 + 彩色描边卡 |
|
|
109
|
+
| 流程图(多步骤) | `.flowchart` + `.frow` + `.fnode` | 彩色节点 tag、反向行 dashed |
|
|
110
|
+
| 层级递进(1→2→3) | `.modes` + `.mode`(m1/m2/m3) | 圆形数字 badge、箭头注释 |
|
|
111
|
+
| 类型卡片网格 | `.ttypes` + `.tt`(5 列) | mono 类型名 + 文档数角标 |
|
|
112
|
+
| 目录/终端展示 | `.term` / `.dirtree` | 深底 mono、两列注释对齐 |
|
|
113
|
+
| 推荐/不推荐 | `.rec` + `.rc`(good/bad) | ✓/✕ 前缀列表 |
|
|
114
|
+
| 命令安装步骤 | `.cmd-block` | 深底 mono、`<span class="p">` 命令、`.c` 注释 |
|
|
115
|
+
| 对话/聊天记录 | `.chat` + `.msg`(u/a 气泡) | 左右气泡 + who 标签 |
|
|
116
|
+
| 链接/仓库卡片 | `.oss` + `.o` | logo 色块 + mono URL |
|
|
117
|
+
| 结尾升华块 | `.ending` | 深蓝渐变 + 渐变标题 + nospec 大按钮 |
|
|
118
|
+
| 作者介绍 | `.author` | avatar 色块 + role 标签 |
|
|
119
|
+
|
|
120
|
+
## 5. 常用组件样式片段(按需复制)
|
|
121
|
+
|
|
122
|
+
### 对比双卡 .vs-panel
|
|
123
|
+
|
|
124
|
+
```css
|
|
125
|
+
.vs-panel{display:grid;grid-template-columns:1fr 1fr;gap:0;border-radius:18px;overflow:hidden;box-shadow:var(--shadow-md);border:1px solid var(--line);background:#fff}
|
|
126
|
+
.vs-panel .side{padding:28px 30px 24px}
|
|
127
|
+
.vs-panel .side.bad{background:linear-gradient(180deg,#fff 0%,#fff7f5 100%)}
|
|
128
|
+
.vs-panel .side.good{background:linear-gradient(180deg,#fff 0%,#f4fcf7 100%);border-left:1px solid var(--line)}
|
|
129
|
+
.vs-panel h4{font-size:17px;margin-bottom:18px;display:flex;align-items:center;gap:9px}
|
|
130
|
+
.vs-panel .side.bad h4{color:var(--red-dark)}
|
|
131
|
+
.vs-panel .side.good h4{color:var(--green-dark)}
|
|
132
|
+
.vs-panel h4 .ico{width:26px;height:26px;border-radius:8px;display:inline-flex;align-items:center;justify-content:center;font-size:13px}
|
|
133
|
+
.vs-panel .bad h4 .ico{background:var(--red-bg);border:1px solid var(--red-line)}
|
|
134
|
+
.vs-panel .good h4 .ico{background:var(--green-bg);border:1px solid var(--green-line)}
|
|
135
|
+
.vs-panel ul{list-style:none}
|
|
136
|
+
.vs-panel li{display:flex;gap:11px;align-items:flex-start;padding:9px 0;border-bottom:1px dashed var(--line);font-size:14.5px;color:var(--ink-2)}
|
|
137
|
+
.vs-panel li:last-child{border-bottom:none}
|
|
138
|
+
.vs-panel li::before{content:"";flex:none;width:7px;height:7px;border-radius:50%;margin-top:8px}
|
|
139
|
+
.vs-panel .bad li::before{background:var(--red)}
|
|
140
|
+
.vs-panel .good li::before{background:var(--green)}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 表格 .tbl-wrap
|
|
144
|
+
|
|
145
|
+
```css
|
|
146
|
+
.tbl-wrap{overflow-x:auto;border-radius:14px;box-shadow:var(--shadow-sm);border:1px solid var(--line);background:#fff}
|
|
147
|
+
table{border-collapse:collapse;width:100%;font-size:14px;min-width:640px}
|
|
148
|
+
th{background:#f0f4fb;color:#334e7e;font-weight:700;text-align:left;padding:12px 16px;border-bottom:2px solid #dbe4f5;white-space:nowrap;font-size:13.5px}
|
|
149
|
+
td{padding:11px 16px;border-bottom:1px solid var(--line);color:var(--ink-2);vertical-align:middle}
|
|
150
|
+
tr:last-child td{border-bottom:none}
|
|
151
|
+
tbody tr:hover{background:#f8fafc}
|
|
152
|
+
td.ok{color:var(--green);text-align:center;font-size:15px}
|
|
153
|
+
td.no{color:#d1d5db;text-align:center}
|
|
154
|
+
td.hl{font-weight:700;color:var(--ink)}
|
|
155
|
+
.tbl-cap{font-size:13px;color:var(--ink-3);margin:10px 0 0;text-align:center}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### 分层架构 .layers
|
|
159
|
+
|
|
160
|
+
```css
|
|
161
|
+
.layers{position:relative;padding-right:56px}
|
|
162
|
+
.layer{border-radius:14px;margin-bottom:20px;overflow:hidden;border:1px solid}
|
|
163
|
+
.layer .h{color:#fff;padding:10px 20px;font-size:15px;font-weight:700;display:flex;justify-content:space-between;align-items:center;gap:10px}
|
|
164
|
+
.layer .h .d{font-size:12px;font-weight:400;opacity:.85}
|
|
165
|
+
.layer .body{padding:16px;display:grid;gap:12px}
|
|
166
|
+
.layer.l1{border-color:var(--blue-line);background:var(--blue-bg)}
|
|
167
|
+
.layer.l1 .h{background:linear-gradient(90deg,#1d4ed8,#2563eb)}
|
|
168
|
+
.layer.l2{border-color:#6ee7b7;background:var(--green-bg)}
|
|
169
|
+
.layer.l2 .h{background:linear-gradient(90deg,#15803d,#16a34a)}
|
|
170
|
+
.layer.l3{border-color:var(--purple-line);background:var(--purple-bg)}
|
|
171
|
+
.layer.l3 .h{background:linear-gradient(90deg,#6d28d9,#7c3aed)}
|
|
172
|
+
.layer.l1 .body{grid-template-columns:repeat(4,1fr)}
|
|
173
|
+
.layer.l2 .body{grid-template-columns:repeat(3,1fr)}
|
|
174
|
+
.layer.l3 .body{grid-template-columns:repeat(3,1fr)}
|
|
175
|
+
.fdoc{background:#fff;border-radius:10px;padding:12px 10px;text-align:center;box-shadow:var(--shadow-sm)}
|
|
176
|
+
.fdoc .f{font-size:13px;font-weight:700;font-family:var(--mono)}
|
|
177
|
+
.fdoc .d{font-size:11.5px;margin-top:2px}
|
|
178
|
+
.layer.l1 .fdoc{border:1px solid var(--blue-line)}.layer.l1 .fdoc .f{color:#1e4ad6}
|
|
179
|
+
.layer.l2 .fdoc{border:1px solid var(--green-line)}.layer.l2 .fdoc .f{color:#15803d}
|
|
180
|
+
.layer.l3 .fdoc{border:1px solid var(--purple-line)}.layer.l3 .fdoc .f{color:#6d28d9}
|
|
181
|
+
.fdoc.sync{border-style:dashed;background:#ede9fe}
|
|
182
|
+
.layer-arrow{display:flex;justify-content:center;color:var(--ink-3);font-size:18px;margin:-6px 0 14px}
|
|
183
|
+
.sync-label{position:absolute;right:0;top:0;bottom:0;width:52px;display:flex;align-items:center;justify-content:center}
|
|
184
|
+
.sync-label .vline{border-left:2px dashed var(--purple-line);height:88%;display:flex;align-items:center}
|
|
185
|
+
.sync-label span{writing-mode:vertical-rl;font-size:12px;color:#8b5cf6;letter-spacing:.3em;background:var(--bg);padding:8px 0}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 升级图 .upgrade / 铁律 .rules / 警示 .callout
|
|
189
|
+
|
|
190
|
+
```css
|
|
191
|
+
.upgrade{display:grid;grid-template-columns:1fr 64px 1fr;gap:0;align-items:stretch}
|
|
192
|
+
.upgrade .col{border-radius:16px;padding:24px 24px 0;position:relative;padding-bottom:52px}
|
|
193
|
+
.upgrade .col.vibe{background:var(--red-bg);border:1.5px solid var(--red-line)}
|
|
194
|
+
.upgrade .col.spec{background:var(--green-bg);border:1.5px solid var(--green-line)}
|
|
195
|
+
.upgrade .col .ttl{font-size:19px;font-weight:900;margin-bottom:2px}
|
|
196
|
+
.upgrade .col.vibe .ttl{color:var(--red-dark)}
|
|
197
|
+
.upgrade .col.spec .ttl{color:var(--green-dark)}
|
|
198
|
+
.upgrade .col .st{font-size:13px;margin-bottom:16px}
|
|
199
|
+
.upgrade .col .chip{background:#fff;border-radius:10px;padding:11px 14px;font-size:14px;text-align:center;margin-bottom:16px;box-shadow:var(--shadow-sm)}
|
|
200
|
+
.upgrade .col ul{list-style:none;margin:0 0 18px}
|
|
201
|
+
.upgrade .col li{font-size:13.5px;padding:6.5px 0 6.5px 22px;position:relative;color:var(--ink-2)}
|
|
202
|
+
.upgrade .col.vibe li::before{content:"✕";position:absolute;left:0;top:7px;color:var(--red);font-weight:700;font-size:12px}
|
|
203
|
+
.upgrade .col.spec li::before{content:"✓";position:absolute;left:0;top:7px;color:var(--green);font-weight:900;font-size:12px}
|
|
204
|
+
.upgrade .col .band{position:absolute;left:0;right:0;bottom:0;border-radius:0 0 14px 14px;padding:12px;text-align:center;color:#fff;font-size:14.5px;font-weight:700}
|
|
205
|
+
.upgrade .col.vibe .band{background:linear-gradient(90deg,#dc2626,#e24b4a)}
|
|
206
|
+
.upgrade .col.spec .band{background:linear-gradient(90deg,#16a34a,#22c55e)}
|
|
207
|
+
.upgrade .mid{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px}
|
|
208
|
+
.upgrade .mid .arrow{width:40px;height:40px;border-radius:50%;background:#fff;border:1.5px solid var(--line);display:flex;align-items:center;justify-content:center;color:var(--ink-3);font-size:17px;box-shadow:var(--shadow-sm)}
|
|
209
|
+
.upgrade .mid span{font-size:12px;color:var(--ink-3);writing-mode:vertical-lr;letter-spacing:.2em}
|
|
210
|
+
|
|
211
|
+
.callout{display:flex;gap:16px;align-items:center;background:linear-gradient(90deg,#fef2f2,#fff);border:1px solid var(--red-line);border-left:5px solid var(--red);border-radius:14px;padding:20px 26px;margin:26px 0;box-shadow:var(--shadow-sm)}
|
|
212
|
+
.callout .big{font-size:34px;font-weight:900;color:var(--red);white-space:nowrap}
|
|
213
|
+
.callout p{margin:0;font-size:15px;color:var(--ink-2)}
|
|
214
|
+
.callout p b{color:var(--red-dark)}
|
|
215
|
+
|
|
216
|
+
.rules{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
|
|
217
|
+
.rule{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:24px 22px;position:relative;box-shadow:var(--shadow-sm);transition:transform .2s}
|
|
218
|
+
.rule:hover{transform:translateY(-3px);box-shadow:var(--shadow-md)}
|
|
219
|
+
.rule .num{position:absolute;top:16px;right:18px;font-size:34px;font-weight:900;color:var(--line)}
|
|
220
|
+
.rule .en{font-size:16px;font-weight:900;color:var(--blue);margin-bottom:8px}
|
|
221
|
+
.rule p{font-size:13.5px;color:var(--ink-2);margin:0;line-height:1.7}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### 暗色面板 .dark-panel / 流程图 / 模式分级
|
|
225
|
+
|
|
226
|
+
```css
|
|
227
|
+
.dark-panel{background:linear-gradient(160deg,#0f172a,#111c36);border-radius:18px;padding:34px 30px;color:#e2e8f0;box-shadow:var(--shadow-lg);position:relative;overflow:hidden}
|
|
228
|
+
.dark-panel::before{content:"";position:absolute;inset:0;background-image:radial-gradient(rgba(96,165,250,.09) 1px,transparent 1px);background-size:26px 26px;pointer-events:none}
|
|
229
|
+
.dark-panel .dp-title{text-align:center;margin-bottom:6px;font-size:21px;font-weight:700;color:#f1f5f9}
|
|
230
|
+
.dark-panel .dp-sub{text-align:center;font-size:13.5px;color:#94a3b8;margin-bottom:28px}
|
|
231
|
+
.ask-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;position:relative;z-index:1}
|
|
232
|
+
.ask-card{background:rgba(30,41,59,.72);border:1px solid;border-radius:14px;padding:16px 16px 14px}
|
|
233
|
+
.ask-card .ac-t{font-weight:700;font-size:15px;margin-bottom:9px;display:flex;align-items:center;gap:8px}
|
|
234
|
+
.ask-card .ac-t .dot{width:8px;height:8px;border-radius:50%}
|
|
235
|
+
.ask-card .q{font-size:12.5px;color:#94a3b8;margin-bottom:2px}
|
|
236
|
+
.ask-card .q code{color:#cbd5e1;background:rgba(148,163,184,.14);padding:1px 6px;border-radius:5px;font-family:var(--mono);font-size:11.5px}
|
|
237
|
+
.ask-card .feat{font-size:12px;color:#64748b;margin:8px 0 10px}
|
|
238
|
+
.ask-card .btn{display:inline-block;font-size:12px;font-weight:700;color:#fff;padding:4px 12px;border-radius:999px}
|
|
239
|
+
.ask-c1{border-color:rgba(45,212,191,.4)}.ask-c1 .ac-t{color:#5eead4}.ask-c1 .dot{background:#2dd4bf}.ask-c1 .btn{background:linear-gradient(90deg,#0d9488,#14b8a6)}
|
|
240
|
+
.ask-c2{border-color:rgba(251,146,60,.4)}.ask-c2 .ac-t{color:#fdba74}.ask-c2 .dot{background:#fb923c}.ask-c2 .btn{background:linear-gradient(90deg,#c2410c,#ea580c)}
|
|
241
|
+
.ask-c3{border-color:rgba(96,165,250,.4)}.ask-c3 .ac-t{color:#93c5fd}.ask-c3 .dot{background:#60a5fa}.ask-c3 .btn{background:linear-gradient(90deg,#1d4ed8,#3b82f6)}
|
|
242
|
+
.ask-c4{border-color:rgba(167,139,250,.4)}.ask-c4 .ac-t{color:#c4b5fd}.ask-c4 .dot{background:#a78bfa}.ask-c4 .btn{background:linear-gradient(90deg,#6d28d9,#8b5cf6)}
|
|
243
|
+
|
|
244
|
+
.flowchart{background:#fff;border:1px solid var(--line);border-radius:16px;padding:26px;box-shadow:var(--shadow-sm)}
|
|
245
|
+
.flowchart .fc-title{text-align:center;font-weight:700;font-size:17px;margin-bottom:20px}
|
|
246
|
+
.frow{display:flex;align-items:center;gap:10px;justify-content:center;flex-wrap:wrap;margin-bottom:8px}
|
|
247
|
+
.fnode{padding:9px 15px;border-radius:10px;font-size:13px;font-weight:500;border:1.5px solid;white-space:nowrap}
|
|
248
|
+
.fnode.tag{font-weight:700}
|
|
249
|
+
.farr{color:var(--ink-3);font-size:15px}
|
|
250
|
+
.frow.rev .fnode{border-style:dashed}
|
|
251
|
+
.f-tag-f{color:#1d4ed8;border-color:#93c5fd;background:var(--blue-bg)}
|
|
252
|
+
.f-node-b{color:#0369a1;border-color:#7dd3fc;background:#f0f9ff}
|
|
253
|
+
.f-node-p{color:#6d28d9;border-color:var(--purple-line);background:var(--purple-bg)}
|
|
254
|
+
.f-node-g{color:#15803d;border-color:#86efac;background:var(--green-bg)}
|
|
255
|
+
.f-node-y{color:#b45309;border-color:#fcd34d;background:#fefce8}
|
|
256
|
+
.f-node-r{color:#b91c1c;border-color:#fca5a5;background:var(--red-bg)}
|
|
257
|
+
.flow-principle{margin-top:18px;background:linear-gradient(90deg,#eff6ff,#eef2ff);border:1px solid #bfdbfe;border-radius:10px;padding:11px 16px;text-align:center;font-size:13.5px;color:#1e3a8a;font-weight:500}
|
|
258
|
+
|
|
259
|
+
.modes{display:flex;flex-direction:column;gap:0;max-width:680px;margin:0 auto}
|
|
260
|
+
.mode{border-radius:14px;padding:16px 22px 16px 56px;position:relative;border:1px solid;box-shadow:var(--shadow-sm)}
|
|
261
|
+
.mode .badge{position:absolute;left:16px;top:50%;transform:translateY(-50%);width:28px;height:28px;border-radius:50%;color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px}
|
|
262
|
+
.mode .mt{font-weight:700;font-size:15.5px;display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
263
|
+
.mode .mt .lbl{font-size:12px;font-weight:700;color:#fff;padding:3px 14px;border-radius:999px;white-space:nowrap}
|
|
264
|
+
.mode .md{font-size:13px;margin-top:4px}
|
|
265
|
+
.mode.m1{background:var(--blue-bg);border-color:var(--blue-line)}.mode.m1 .badge{background:var(--blue)}.mode.m1 .mt{color:#1e4ad6}
|
|
266
|
+
.mode.m2{background:var(--green-bg);border-color:var(--green-line)}.mode.m2 .badge{background:var(--green)}.mode.m2 .mt{color:var(--green-dark)}
|
|
267
|
+
.mode.m3{background:var(--purple-bg);border-color:var(--purple-line)}.mode.m3 .badge{background:var(--purple)}.mode.m3 .mt{color:var(--purple-dark)}
|
|
268
|
+
.mode-arr{display:flex;align-items:center;justify-content:center;gap:10px;color:var(--ink-4);font-size:11.5px;padding:9px 0}
|
|
269
|
+
.mode-arr::before,.mode-arr::after{content:"↓";font-size:14px}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### 任务卡片 / 目录树 / 推荐 / 命令 / 对话 / 开源 / 结尾 / 作者
|
|
273
|
+
|
|
274
|
+
```css
|
|
275
|
+
.ttypes{display:grid;grid-template-columns:repeat(5,1fr);gap:12px}
|
|
276
|
+
.tt{background:#fff;border:1px solid var(--line);border-radius:12px;padding:14px 12px;box-shadow:var(--shadow-sm)}
|
|
277
|
+
.tt .ttn{font-family:var(--mono);font-weight:700;font-size:13px;color:var(--blue);margin-bottom:5px}
|
|
278
|
+
.tt .tcount{font-size:10.5px;color:var(--ink-4);float:right;font-family:var(--sans)}
|
|
279
|
+
.tt p{font-size:12px;color:var(--ink-3);margin:0;line-height:1.6}
|
|
280
|
+
|
|
281
|
+
.dirtree{background:#0f172a;border-radius:14px;padding:22px 28px;font-family:var(--mono);font-size:13.5px;line-height:2.1;color:#cbd5e1;box-shadow:var(--shadow-md);overflow-x:auto}
|
|
282
|
+
.dt-row{display:grid;grid-template-columns:minmax(320px,1.35fr) 1fr;gap:20px}
|
|
283
|
+
.dt-row .dt-cmt{color:#64748b;font-family:var(--sans);font-size:12.5px}
|
|
284
|
+
.dt-row .dir{color:#7dd3fc;font-weight:700}
|
|
285
|
+
.dt-row .file{color:#e2e8f0}
|
|
286
|
+
.dt-row.root .dir{color:#f472b6}
|
|
287
|
+
.dt-ind1{padding-left:24px}
|
|
288
|
+
.dt-ind2{padding-left:48px}
|
|
289
|
+
|
|
290
|
+
.rec{display:grid;grid-template-columns:1fr 1fr;gap:18px}
|
|
291
|
+
.rec .rc{border-radius:16px;overflow:hidden;border:1px solid;box-shadow:var(--shadow-sm);background:#fff}
|
|
292
|
+
.rec .rc .rh{padding:13px 22px;color:#fff;font-weight:700;font-size:16px}
|
|
293
|
+
.rec .rc.good{border-color:var(--green-line)}.rec .rc.good .rh{background:linear-gradient(90deg,#15803d,#16a34a)}
|
|
294
|
+
.rec .rc.bad{border-color:var(--red-line)}.rec .rc.bad .rh{background:linear-gradient(90deg,#dc2626,#e24b4a)}
|
|
295
|
+
.rec .rc ul{list-style:none;padding:14px 22px 18px}
|
|
296
|
+
.rec .rc li{font-size:14px;color:var(--ink-2);padding:8px 0 8px 22px;position:relative;border-bottom:1px dashed var(--line)}
|
|
297
|
+
.rec .rc li:last-child{border-bottom:none}
|
|
298
|
+
.rec .good li::before{content:"✓";position:absolute;left:0;top:8px;color:var(--green);font-weight:900;font-size:13px}
|
|
299
|
+
.rec .bad li::before{content:"✕";position:absolute;left:0;top:8px;color:var(--red);font-weight:700;font-size:13px}
|
|
300
|
+
|
|
301
|
+
.cmd-block{background:#0f172a;border-radius:14px;padding:18px 24px;font-family:var(--mono);font-size:13.5px;line-height:2.1;color:#e2e8f0;box-shadow:var(--shadow-md);overflow-x:auto}
|
|
302
|
+
.cmd-block .p{color:#f472b6}
|
|
303
|
+
.cmd-block .c{color:#94a3b8}
|
|
304
|
+
.cmd-block .k{color:#7dd3fc}
|
|
305
|
+
|
|
306
|
+
.chat{background:#fff;border:1px solid var(--line);border-radius:16px;padding:24px 26px;box-shadow:var(--shadow-sm)}
|
|
307
|
+
.chat .chat-title{font-weight:700;font-size:16px;margin-bottom:18px}
|
|
308
|
+
.msg{max-width:86%;border-radius:13px;padding:10px 15px;font-size:13.5px;margin-bottom:12px;line-height:1.65}
|
|
309
|
+
.msg .who{font-size:11.5px;font-weight:700;opacity:.65;margin-bottom:2px}
|
|
310
|
+
.msg.u{background:var(--blue-bg);border:1px solid var(--blue-line);margin-left:auto;border-bottom-right-radius:3px;color:#1e3a5f}
|
|
311
|
+
.msg.a{background:#f8fafc;border:1px solid var(--line);border-bottom-left-radius:3px;color:var(--ink-2)}
|
|
312
|
+
|
|
313
|
+
.oss{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
|
314
|
+
.oss .o{display:flex;align-items:center;gap:14px;background:#fff;border:1px solid var(--line);border-radius:14px;padding:18px 20px;box-shadow:var(--shadow-sm);text-decoration:none}
|
|
315
|
+
.oss .o .logo{width:42px;height:42px;border-radius:11px;display:flex;align-items:center;justify-content:center;font-weight:900;font-size:17px;color:#fff;flex:none}
|
|
316
|
+
.oss .o .nm{font-weight:700;color:var(--ink);font-size:15px}
|
|
317
|
+
.oss .o .url{font-size:12.5px;color:var(--ink-3);font-family:var(--mono)}
|
|
318
|
+
|
|
319
|
+
.ending{background:linear-gradient(160deg,#0f172a,#111c36);border-radius:20px;color:#e2e8f0;padding:46px 40px;text-align:center;box-shadow:var(--shadow-lg);position:relative;overflow:hidden}
|
|
320
|
+
.ending .e-t{position:relative;font-size:26px;font-weight:900;margin-bottom:14px}
|
|
321
|
+
.ending .e-t .grad{background:linear-gradient(90deg,#7dd3fc,#a5b4fc,#c4b5fd);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
|
|
322
|
+
.ending p{position:relative;color:#94a3b8;font-size:14.5px;max-width:620px;margin:0 auto 22px}
|
|
323
|
+
.ending .nospec{position:relative;display:inline-block;font-size:21px;font-weight:900;letter-spacing:.08em;color:#fff;background:linear-gradient(90deg,#2563eb,#4f46e5);padding:12px 34px;border-radius:12px;box-shadow:0 8px 26px rgba(37,99,235,.5)}
|
|
324
|
+
|
|
325
|
+
.author{display:flex;gap:20px;align-items:flex-start;background:#fff;border:1px solid var(--line);border-radius:16px;padding:24px 28px;box-shadow:var(--shadow-sm);margin-bottom:60px}
|
|
326
|
+
.author .avatar{width:56px;height:56px;border-radius:14px;flex:none;display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:900;color:#fff;background:linear-gradient(135deg,#3b82f6,#8b5cf6)}
|
|
327
|
+
.author .nm{font-weight:700;font-size:16.5px;margin-bottom:6px}
|
|
328
|
+
.author .nm .role{display:inline-block;font-size:11.5px;font-weight:500;color:var(--blue);background:var(--blue-bg);border:1px solid var(--blue-line);padding:2px 10px;border-radius:999px;margin-left:10px;vertical-align:2px}
|
|
329
|
+
.author p{font-size:13.5px;color:var(--ink-2);margin:0;line-height:1.7}
|
|
330
|
+
|
|
331
|
+
.foot{text-align:center;padding:34px 0 44px;color:var(--ink-4);font-size:12.5px}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## 6. HTML 骨架模板
|
|
335
|
+
|
|
336
|
+
```html
|
|
337
|
+
<!DOCTYPE html>
|
|
338
|
+
<html lang="zh-CN">
|
|
339
|
+
<head>
|
|
340
|
+
<meta charset="UTF-8">
|
|
341
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
342
|
+
<title>文章标题</title>
|
|
343
|
+
<style>/* 按需复制上面的变量与组件样式 */</style>
|
|
344
|
+
</head>
|
|
345
|
+
<body>
|
|
346
|
+
<div class="hero">…封面…</div>
|
|
347
|
+
<div class="wrap">
|
|
348
|
+
<section>…引言…</section>
|
|
349
|
+
<section>
|
|
350
|
+
<div class="sec-head"><span class="no">01</span><h2>章节标题</h2><span class="en">Chapter English</span></div>
|
|
351
|
+
…正文与图表组件…
|
|
352
|
+
</section>
|
|
353
|
+
…更多章节…
|
|
354
|
+
<div class="foot">…页脚…</div>
|
|
355
|
+
</div>
|
|
356
|
+
</body>
|
|
357
|
+
</html>
|
|
358
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Download Roboto woff2 weights (400/500/700/900) into a fonts directory.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python3 download_roboto.py /path/to/assets/fonts
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import urllib.request
|
|
10
|
+
import ssl
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
ssl._create_default_https_context = ssl._create_unverified_context
|
|
14
|
+
except Exception:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
BASE = 'https://cdn.jsdelivr.net/npm/@fontsource/roboto@5.2.6/files/roboto-latin-{w}-normal.woff2'
|
|
18
|
+
WEIGHTS = ['400', '500', '700', '900']
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main(outdir):
|
|
22
|
+
os.makedirs(outdir, exist_ok=True)
|
|
23
|
+
for w in WEIGHTS:
|
|
24
|
+
dst = os.path.join(outdir, f'roboto-latin-{w}-normal.woff2')
|
|
25
|
+
if os.path.exists(dst) and os.path.getsize(dst) > 10000:
|
|
26
|
+
print('skip (exists):', dst)
|
|
27
|
+
continue
|
|
28
|
+
url = BASE.format(w=w)
|
|
29
|
+
print('downloading:', url)
|
|
30
|
+
urllib.request.urlretrieve(url, dst)
|
|
31
|
+
print('saved:', dst, os.path.getsize(dst), 'bytes')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == '__main__':
|
|
35
|
+
if len(sys.argv) < 2:
|
|
36
|
+
print('Usage: python3 download_roboto.py /path/to/assets/fonts', file=sys.stderr)
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
main(sys.argv[1])
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Extract text, tables, and images from a .docx file.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python3 extract_docx.py input.docx [--media-dir /tmp/docx-extract] > /tmp/extracted.json
|
|
6
|
+
|
|
7
|
+
Output JSON:
|
|
8
|
+
{
|
|
9
|
+
"texts": ["段落1", ...], # 正文段落(不含表格内文本)
|
|
10
|
+
"tables": [["单元格", ...], ...], # 表格内容,行优先(每行一个数组)
|
|
11
|
+
"media": ["word/media/image1.png", ...], # 图片在 zip 内的路径
|
|
12
|
+
"media_dir": "/tmp/docx-extract" or null # 图片落盘目录,供 Read 工具查看
|
|
13
|
+
}
|
|
14
|
+
"""
|
|
15
|
+
import sys
|
|
16
|
+
import os
|
|
17
|
+
import json
|
|
18
|
+
import zipfile
|
|
19
|
+
import xml.etree.ElementTree as ET
|
|
20
|
+
|
|
21
|
+
W = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def para_text(p):
|
|
25
|
+
return ''.join(n.text or '' for n in p.iter(W + 't')).strip()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def cell_text(tc):
|
|
29
|
+
return ''.join(n.text or '' for n in tc.iter(W + 't')).strip()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main(path):
|
|
33
|
+
z = zipfile.ZipFile(path)
|
|
34
|
+
tree = ET.fromstring(z.read('word/document.xml'))
|
|
35
|
+
body = tree.find(W + 'body')
|
|
36
|
+
|
|
37
|
+
texts, tables = [], []
|
|
38
|
+
for el in body:
|
|
39
|
+
if el.tag == W + 'p':
|
|
40
|
+
t = para_text(el)
|
|
41
|
+
if t:
|
|
42
|
+
texts.append(t)
|
|
43
|
+
elif el.tag == W + 'tbl':
|
|
44
|
+
rows = []
|
|
45
|
+
for tr in el.findall(W + 'tr'):
|
|
46
|
+
rows.append([cell_text(tc) for tc in tr.findall(W + 'tc')])
|
|
47
|
+
tables.append(rows)
|
|
48
|
+
|
|
49
|
+
media = [n for n in z.namelist() if n.startswith('word/media/') and not n.endswith('/')]
|
|
50
|
+
|
|
51
|
+
media_dir = None
|
|
52
|
+
if '--media-dir' in sys.argv:
|
|
53
|
+
media_dir = sys.argv[sys.argv.index('--media-dir') + 1]
|
|
54
|
+
os.makedirs(media_dir, exist_ok=True)
|
|
55
|
+
for n in media:
|
|
56
|
+
with open(os.path.join(media_dir, os.path.basename(n)), 'wb') as f:
|
|
57
|
+
f.write(z.read(n))
|
|
58
|
+
|
|
59
|
+
print(json.dumps({'texts': texts, 'tables': tables, 'media': media,
|
|
60
|
+
'media_dir': media_dir}, ensure_ascii=False, indent=1))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == '__main__':
|
|
64
|
+
if len(sys.argv) < 2:
|
|
65
|
+
print('Usage: python3 extract_docx.py input.docx [--media-dir /path]',
|
|
66
|
+
file=sys.stderr)
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
main(sys.argv[1])
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Embed Roboto woff2 fonts as base64 data URIs to produce a self-contained HTML.
|
|
3
|
+
|
|
4
|
+
Replaces url('assets/fonts/roboto-latin-{400,500,700,900}-normal.woff2')
|
|
5
|
+
with inline base64 data URIs, so the output HTML needs no external files.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python3 inline_fonts.py input.html output.html /path/to/assets/fonts
|
|
9
|
+
"""
|
|
10
|
+
import sys
|
|
11
|
+
import base64
|
|
12
|
+
|
|
13
|
+
WEIGHTS = ['400', '500', '700', '900']
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main(src, dst, fontdir):
|
|
17
|
+
html = open(src, encoding='utf-8').read()
|
|
18
|
+
for w in WEIGHTS:
|
|
19
|
+
path = f"{fontdir.rstrip('/')}/roboto-latin-{w}-normal.woff2"
|
|
20
|
+
data = 'data:font/woff2;base64,' + base64.b64encode(open(path, 'rb').read()).decode()
|
|
21
|
+
html = html.replace(f"url('assets/fonts/roboto-latin-{w}-normal.woff2')",
|
|
22
|
+
f"url('{data}')")
|
|
23
|
+
open(dst, 'w', encoding='utf-8').write(html)
|
|
24
|
+
print(f'OK: {dst} ({len(html)} bytes, 4 Roboto weights embedded)')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == '__main__':
|
|
28
|
+
if len(sys.argv) < 4:
|
|
29
|
+
print('Usage: python3 inline_fonts.py input.html output.html /path/to/assets/fonts',
|
|
30
|
+
file=sys.stderr)
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
main(sys.argv[1], sys.argv[2], sys.argv[3])
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Detect and install Source Han Sans SC (思源黑体) on macOS/Windows.
|
|
3
|
+
|
|
4
|
+
Checks common font directories first; if missing, downloads the official
|
|
5
|
+
Adobe Fonts release (2.005R, SIL OFL — free for commercial use), verifies
|
|
6
|
+
SHA-256, and installs the OTF weights to the user font directory.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python3 install_sourcehan.py
|
|
10
|
+
"""
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import shutil
|
|
14
|
+
import zipfile
|
|
15
|
+
import hashlib
|
|
16
|
+
import platform
|
|
17
|
+
import tempfile
|
|
18
|
+
import urllib.request
|
|
19
|
+
|
|
20
|
+
OFFICIAL = 'https://github.com/adobe-fonts/source-han-sans/releases/download/2.005R/09_SourceHanSansSC.zip'
|
|
21
|
+
MIRROR = 'https://ghfast.top/' + OFFICIAL # GitHub 直连超时时使用
|
|
22
|
+
SHA256 = 'ef7364f7ac2564be1ae9c1d74276de2653fe38b73449070398c4fc0b7e032ff1'
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def font_dirs():
|
|
26
|
+
if platform.system() == 'Darwin':
|
|
27
|
+
return [os.path.expanduser('~/Library/Fonts'), '/Library/Fonts']
|
|
28
|
+
if platform.system() == 'Windows':
|
|
29
|
+
return [os.path.join(os.environ.get('LOCALAPPDATA', os.path.expanduser('~')),
|
|
30
|
+
'Microsoft', 'Windows', 'Fonts')]
|
|
31
|
+
return [os.path.expanduser('~/.fonts')]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def installed():
|
|
35
|
+
for d in font_dirs():
|
|
36
|
+
if os.path.isdir(d):
|
|
37
|
+
for f in os.listdir(d):
|
|
38
|
+
if f.lower().startswith('sourcehansans'):
|
|
39
|
+
return True
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main():
|
|
44
|
+
if installed():
|
|
45
|
+
print('OK: Source Han Sans SC already installed, nothing to do')
|
|
46
|
+
return
|
|
47
|
+
target = font_dirs()[0]
|
|
48
|
+
os.makedirs(target, exist_ok=True)
|
|
49
|
+
tmp = tempfile.mkdtemp(prefix='sourcehan-')
|
|
50
|
+
zip_path = os.path.join(tmp, 'sourcehan.zip')
|
|
51
|
+
for url in (MIRROR, OFFICIAL):
|
|
52
|
+
try:
|
|
53
|
+
print('downloading:', url)
|
|
54
|
+
urllib.request.urlretrieve(url, zip_path)
|
|
55
|
+
break
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print('download failed:', e)
|
|
58
|
+
else:
|
|
59
|
+
print('ERROR: both sources failed, install manually from', OFFICIAL)
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
h = hashlib.sha256(open(zip_path, 'rb').read()).hexdigest()
|
|
62
|
+
if h != SHA256:
|
|
63
|
+
print('ERROR: SHA-256 mismatch:', h, 'expected', SHA256)
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
with zipfile.ZipFile(zip_path) as z:
|
|
66
|
+
for n in z.namelist():
|
|
67
|
+
if n.lower().endswith('.otf') and 'license' not in n.lower():
|
|
68
|
+
base = os.path.basename(n)
|
|
69
|
+
with z.open(n) as f, open(os.path.join(target, base), 'wb') as out:
|
|
70
|
+
out.write(f.read())
|
|
71
|
+
print('installed:', base)
|
|
72
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
73
|
+
print('DONE: 思源黑体已安装到', target, '— 重启浏览器后生效')
|
|
74
|
+
print('NOTE: 严禁执行 atsutil databases -removeUser(会破坏字体数据库)')
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == '__main__':
|
|
78
|
+
main()
|