agentcss 0.25.6 → 0.27.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/01-tokens/README.md +1 -1
- package/01-tokens/behavior/labels.js +44 -0
- package/01-tokens/responsive-demo.html +1 -1
- package/01-tokens/tokens.core.css +1 -1
- package/01-tokens/tokens.css +2 -2
- package/02-primitives/combobox/combobox.js +26 -3
- package/02-primitives/date-range/date-range.js +28 -6
- package/02-primitives/popover/popover.js +22 -2
- package/02-primitives/select/select.js +23 -2
- package/02-primitives/timeline/demo.html +1 -1
- package/02-primitives/upload/upload.js +50 -17
- package/03-patterns/content/content.css +13 -1
- package/03-patterns/content/demo.html +1 -1
- package/03-patterns/drawer/demo.html +1 -1
- package/03-patterns/drawer/drawer.js +44 -6
- package/03-patterns/dropdown/dropdown.js +21 -1
- package/03-patterns/overlay/overlay.css +1 -1
- package/03-patterns/overlay/overlay.js +39 -2
- package/03-patterns/pagination/pagination.js +37 -10
- package/03-patterns/states/states.css +1 -1
- package/03-patterns/tooltip/tooltip.js +17 -2
- package/03-patterns/tree/tree.js +21 -1
- package/04-recipes/analysis-report/demo.html +1 -1
- package/04-recipes/app-shell/README.md +1 -1
- package/04-recipes/app-shell/demo.html +1 -1
- package/04-recipes/data-showcase/demo.html +2 -2
- package/04-recipes/longform/content.html +993 -78
- package/04-recipes/longform/longform.html +3 -3
- package/04-recipes/report/demo.html +3 -3
- package/04-recipes/table/README.md +23 -1
- package/04-recipes/table/demo.html +7 -7
- package/09-assets/bar/bar.css +1 -1
- package/09-assets/bar/bar.js +28 -4
- package/09-assets/model-viewer/model-viewer.js +45 -13
- package/09-assets/scientific-plot/demo.html +6 -6
- package/AGENTS.md +3 -1
- package/API.md +32 -0
- package/CHANGELOG.md +174 -5
- package/CONTRIBUTING.md +44 -3
- package/README.md +19 -0
- package/README.zh-CN.md +18 -0
- package/SECURITY.md +16 -4
- package/START-HERE.md +25 -18
- package/ai/START-HERE.md +40 -6
- package/ai/check-html.js +658 -0
- package/ai/cli.js +557 -67
- package/ai/components.json +9 -4
- package/ai/pdf.js +52 -10
- package/ai/recipes.json +86 -0
- package/ai/scaffold.js +195 -0
- package/ai/shot.js +345 -0
- package/ai/validate.js +65 -6
- package/dist/01-tokens/behavior/labels.js +11 -0
- package/dist/02-primitives/combobox/combobox.js +25 -3
- package/dist/02-primitives/date-range/date-range.js +26 -5
- package/dist/02-primitives/popover/popover.js +14 -1
- package/dist/02-primitives/select/select.js +22 -2
- package/dist/02-primitives/upload/upload.js +47 -17
- package/dist/03-patterns/content/content.css +1 -1
- package/dist/03-patterns/drawer/drawer.js +32 -4
- package/dist/03-patterns/dropdown/dropdown.js +20 -1
- package/dist/03-patterns/overlay/overlay.js +25 -1
- package/dist/03-patterns/pagination/pagination.js +34 -10
- package/dist/03-patterns/tooltip/tooltip.js +14 -2
- package/dist/03-patterns/tree/tree.js +20 -1
- package/dist/09-assets/bar/bar.js +23 -4
- package/dist/09-assets/model-viewer/model-viewer.js +38 -12
- package/dist/COSTS.md +20 -20
- package/docs/ACCESSIBILITY.md +3 -3
- package/docs/ARCHITECTURE.md +116 -0
- package/docs/L10N.md +92 -0
- package/docs/LAYER-BASELINE.md +2 -2
- package/docs/PRINT.md +3 -3
- package/docs/RELEASE-PROCESS.md +59 -3
- package/docs/STABILITY.md +1 -1
- package/docs/VERIFY-RELEASE.md +24 -2
- package/index.html +3 -3
- package/llms-full.txt +18 -4
- package/llms.txt +26 -5
- package/package.json +4 -2
- package/types/index.d.ts +218 -13
package/ai/components.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
3
|
"generatedBy": "05-audit/gen-ai-contract.py",
|
|
4
|
-
"libraryVersion": "0.
|
|
4
|
+
"libraryVersion": "0.27.0",
|
|
5
5
|
"$comment": "机器可读的组件契约。slots = BEM 元素名(在本库里元素名就是插槽名),states.enum = 组件会在 DOM 上写出的 data-* 取值,states.legacyClass = 仍未收敛到 data 属性的历史状态类(INVARIANT I-8 未达标)。printPolicy = 这个组件在纸上会发生什么(空数组 = 原样保留);pagination = 内容是否被分页裁过(server 不许与 expand 同时出现,否则展开的只是当前一页)。",
|
|
6
6
|
"maturityCriteria": {
|
|
7
7
|
"alpha": "缺 demo.html 或缺 05-audit 下的专属门禁 ⇒ 还没被机器验证过,不要用",
|
|
@@ -249,11 +249,13 @@
|
|
|
249
249
|
},
|
|
250
250
|
"aria": {
|
|
251
251
|
"roles": [
|
|
252
|
-
"img"
|
|
252
|
+
"img",
|
|
253
|
+
"region"
|
|
253
254
|
],
|
|
254
255
|
"attrs": [
|
|
255
256
|
"hidden",
|
|
256
|
-
"label"
|
|
257
|
+
"label",
|
|
258
|
+
"labelledby"
|
|
257
259
|
]
|
|
258
260
|
},
|
|
259
261
|
"usesOtherNamespaces": [],
|
|
@@ -2411,7 +2413,9 @@
|
|
|
2411
2413
|
"legacyClass": []
|
|
2412
2414
|
},
|
|
2413
2415
|
"aria": {
|
|
2414
|
-
"roles": [
|
|
2416
|
+
"roles": [
|
|
2417
|
+
"region"
|
|
2418
|
+
],
|
|
2415
2419
|
"attrs": [
|
|
2416
2420
|
"hidden",
|
|
2417
2421
|
"label"
|
|
@@ -2475,6 +2479,7 @@
|
|
|
2475
2479
|
"aria": {
|
|
2476
2480
|
"roles": [
|
|
2477
2481
|
"alert",
|
|
2482
|
+
"region",
|
|
2478
2483
|
"status"
|
|
2479
2484
|
],
|
|
2480
2485
|
"attrs": [
|
package/ai/pdf.js
CHANGED
|
@@ -75,6 +75,35 @@ const DEFAULT_MARGIN = '18mm 15mm';
|
|
|
75
75
|
* **找不到就明确报错,不许静默降级**。
|
|
76
76
|
* 漂移由 `05-audit/pdf-export-gate.js` 的 E8 交叉核对(两边都找到时必须是同一个文件)。
|
|
77
77
|
*/
|
|
78
|
+
/* Playwright 实际会用到的可执行文件名。只收这几个 —— 名字表越宽,
|
|
79
|
+
越可能在缓存目录里认错文件(例如 README、脚本)。 */
|
|
80
|
+
const PW_EXE = [
|
|
81
|
+
'chrome', 'chrome.exe',
|
|
82
|
+
'chrome-headless-shell', 'chrome-headless-shell.exe',
|
|
83
|
+
'headless_shell', 'headless_shell.exe',
|
|
84
|
+
'Chromium', 'Google Chrome for Testing', 'Google Chrome for Testing.exe',
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
/** 在 Playwright 的 chromium* 目录里按文件名找可执行文件(有界递归,depth 层) */
|
|
88
|
+
function scanForExe(dir, depth) {
|
|
89
|
+
let names;
|
|
90
|
+
try { names = fs.readdirSync(dir); } catch (e) { return null; }
|
|
91
|
+
const dirs = [];
|
|
92
|
+
for (const n of names) {
|
|
93
|
+
const p = path.join(dir, n);
|
|
94
|
+
let st;
|
|
95
|
+
try { st = fs.statSync(p); } catch (e) { continue; }
|
|
96
|
+
if (st.isDirectory()) { dirs.push(p); continue; }
|
|
97
|
+
if (PW_EXE.indexOf(n) !== -1) return p;
|
|
98
|
+
}
|
|
99
|
+
if (depth <= 0) return null;
|
|
100
|
+
for (const d of dirs) {
|
|
101
|
+
const hit = scanForExe(d, depth - 1);
|
|
102
|
+
if (hit) return hit;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
78
107
|
function chromeCandidates() {
|
|
79
108
|
const out = [];
|
|
80
109
|
if (process.env.AGENTCSS_CHROME) out.push(process.env.AGENTCSS_CHROME);
|
|
@@ -97,18 +126,28 @@ function chromeCandidates() {
|
|
|
97
126
|
out.push('/usr/bin/google-chrome', '/usr/bin/google-chrome-stable',
|
|
98
127
|
'/usr/bin/chromium', '/usr/bin/chromium-browser', '/snap/bin/chromium');
|
|
99
128
|
}
|
|
100
|
-
/* CI 上没有系统 Chrome:Playwright 装的那份也得认,否则"能导出"
|
|
101
|
-
|
|
102
|
-
|
|
129
|
+
/* CI 上没有系统 Chrome:Playwright 装的那份也得认,否则"能导出"只在开发机上成立。
|
|
130
|
+
⚠️ **不写死目录布局**。Playwright 改过两次命名:
|
|
131
|
+
chrome-linux/chrome → chrome-linux64/chrome
|
|
132
|
+
chrome-linux/headless_shell → chrome-headless-shell-linux64/chrome-headless-shell
|
|
133
|
+
写死布局的结果就是"某天起突然 no-chrome"(外部审计 F5 实测:
|
|
134
|
+
干净环境里 `npm ci && npx playwright install chromium` 之后依然找不到)。
|
|
135
|
+
⇒ 在 chromium* 目录下按**可执行文件名**有界递归找,布局怎么变都还能找到。
|
|
136
|
+
⚠️ Windows 的根目录是 %LOCALAPPDATA%\ms-playwright —— `~/.cache` 在 Windows 上不存在。 */
|
|
137
|
+
const roots = [
|
|
138
|
+
process.env.PLAYWRIGHT_BROWSERS_PATH,
|
|
139
|
+
process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, 'ms-playwright'),
|
|
140
|
+
path.join(os.homedir(), '.cache/ms-playwright'),
|
|
141
|
+
path.join(os.homedir(), 'Library/Caches/ms-playwright'),
|
|
142
|
+
].filter(Boolean);
|
|
143
|
+
for (const root of roots) {
|
|
103
144
|
let names;
|
|
104
145
|
try { names = fs.readdirSync(root); } catch (e) { continue; }
|
|
105
146
|
for (const n of names) {
|
|
106
147
|
if (!/^chromium/.test(n)) continue;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
out.push(path.join(root, n, ...rel.split('/')));
|
|
111
|
-
}
|
|
148
|
+
/* 深度 4:macOS 的 .app 包多两层(见 browser.js 同一处注释) */
|
|
149
|
+
const hit = scanForExe(path.join(root, n), 4);
|
|
150
|
+
if (hit) out.push(hit);
|
|
112
151
|
}
|
|
113
152
|
}
|
|
114
153
|
return out.filter(Boolean);
|
|
@@ -163,7 +202,7 @@ function paperSpec(name) {
|
|
|
163
202
|
return PAPERS[k] ? { key: k, ...PAPERS[k] } : null;
|
|
164
203
|
}
|
|
165
204
|
|
|
166
|
-
/** 注入到 `<head>`
|
|
205
|
+
/** 注入到 `<head>` 末尾(**必须在使用者样式之后**,否则同特异性被覆盖)。 */
|
|
167
206
|
function injectSheet(html, css) {
|
|
168
207
|
const style = '<style id="agentcss-paper" data-agentcss="paper">' + css + '</style>';
|
|
169
208
|
if (/<\/head\s*>/i.test(html)) return html.replace(/<\/head\s*>/i, style + '</head>');
|
|
@@ -217,7 +256,7 @@ function exportPdf(o) {
|
|
|
217
256
|
if (!Number.isInteger(b) || b < 1) {
|
|
218
257
|
/* ⚠️ 用 String() 而不是 JSON.stringify:`JSON.stringify(NaN)` 是 `"null"`,
|
|
219
258
|
于是 `--max-pages=abc` 会报「收到 null」—— 使用者据这句话会去查一个
|
|
220
|
-
根本不存在的 null
|
|
259
|
+
根本不存在的 null(实测踩过)。 */
|
|
221
260
|
return { ok: false, code: 'usage',
|
|
222
261
|
message: '--max-pages 必须是 ≥1 的整数,收到 ' + String(o.budget) };
|
|
223
262
|
}
|
|
@@ -351,4 +390,7 @@ function overBudgetMessage(got, budget, spec, margin, out, box, injected) {
|
|
|
351
390
|
module.exports = {
|
|
352
391
|
exportPdf, resolveChrome, normalizeMargin, paperSpec, PAPERS,
|
|
353
392
|
DEFAULT_PAPER, DEFAULT_MARGIN,
|
|
393
|
+
/* 只为可测性导出:浏览器发现逻辑必须在"没有系统 Chrome"的机器上也能被验证,
|
|
394
|
+
否则它只会在 CI 上坏、而开发机永远绿。 */
|
|
395
|
+
chromeCandidates,
|
|
354
396
|
};
|
package/ai/recipes.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1.0.0",
|
|
3
|
+
"$comment": [
|
|
4
|
+
"版式的「必有件」—— 只收**能从标记本身判定**的那几条。",
|
|
5
|
+
"每一条都对应 04-recipes/report/README.md 里「三种版式的取舍」表中的一格;",
|
|
6
|
+
"那张表是这套系统存在的理由:同一份内容换一个阅读目的,该留什么、该放弃什么。",
|
|
7
|
+
"",
|
|
8
|
+
"🔴 为什么有的写在 requires、有的写在 unchecked:",
|
|
9
|
+
" requires = 有标记可查(少一个类就是确定的缺件),因此可以直接判红;",
|
|
10
|
+
" unchecked = 说得清但判不了(「方法要写明」是内容判断),因此**只打印、不判过**。",
|
|
11
|
+
" 把后者塞进 requires 只能靠关键词匹配,那会造出一种「看起来在检查」的假绿 ——",
|
|
12
|
+
" 这套库宁可写「这条没查」,也不写「查过了」。"
|
|
13
|
+
],
|
|
14
|
+
"recipes": {
|
|
15
|
+
"report/finance": {
|
|
16
|
+
"label": "report · 金融研究版",
|
|
17
|
+
"root": ".rep.rep--finance",
|
|
18
|
+
"css": "04-recipes/report/report.css",
|
|
19
|
+
"doc": "04-recipes/report/README.md",
|
|
20
|
+
"why": "给要快速比数字的人看:行长最短、字号小一档,靠关键数字条先给结论。",
|
|
21
|
+
"requires": [
|
|
22
|
+
{
|
|
23
|
+
"selector": ".rep__keyline",
|
|
24
|
+
"label": "关键数字条",
|
|
25
|
+
"why": "这一版的读者先看数、后看论证;没有数字条,标题下面就是一大段文字,等于放弃了它唯一的长处。"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"selector": ".rep__risk",
|
|
29
|
+
"label": "风险披露",
|
|
30
|
+
"why": "金融口径的结论必须带约束条件。少了它,页面读起来像建议,而不是分析。"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"unchecked": [
|
|
34
|
+
{
|
|
35
|
+
"label": "长论证不超过两屏",
|
|
36
|
+
"why": "篇幅是内容判断,没有可查的标记。超过两屏的推理在这一版里会被跳过 —— 那是作者该拆的,不是工具能判的。"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"report/technical": {
|
|
41
|
+
"label": "report · 科研技术版",
|
|
42
|
+
"root": ".rep.rep--technical",
|
|
43
|
+
"css": "04-recipes/report/report.css",
|
|
44
|
+
"doc": "04-recipes/report/README.md",
|
|
45
|
+
"why": "给要逐条核对的人看:行长最窄、行距最松,图与表自动编号。",
|
|
46
|
+
"requires": [
|
|
47
|
+
{
|
|
48
|
+
"selector": ".rep__source",
|
|
49
|
+
"label": "来源脚注",
|
|
50
|
+
"why": "这一版的读者会顺着结论回查原始材料;没有来源,逐条核对就无从开始。"
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"unchecked": [
|
|
54
|
+
{
|
|
55
|
+
"label": "方法写明",
|
|
56
|
+
"why": "「写了方法」没有可查的标记:它可能是一节、也可能是一段。判它需要读懂内容,工具不假装能判。"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
"report/magazine": {
|
|
61
|
+
"label": "report · 杂志编辑版",
|
|
62
|
+
"root": ".rep.rep--magazine",
|
|
63
|
+
"css": "04-recipes/report/report.css",
|
|
64
|
+
"doc": "04-recipes/report/README.md",
|
|
65
|
+
"why": "给通读一遍的人看:行长最宽、不编号,靠版面节奏而不是编号导航。",
|
|
66
|
+
"requires": [
|
|
67
|
+
{
|
|
68
|
+
"selector": ".rep__pull",
|
|
69
|
+
"label": "引语块",
|
|
70
|
+
"why": "这一版靠「停顿」制造节奏;没有引语块,长文会变成一整片同样密度的灰。"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"selector": ".rep__fig--wide",
|
|
74
|
+
"label": "出血宽图",
|
|
75
|
+
"why": "宽图是这一版唯一的视觉重音。全篇只有正文列宽的图,读起来和别的版式没有区别。"
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
"unchecked": [
|
|
79
|
+
{
|
|
80
|
+
"label": "审计级追溯",
|
|
81
|
+
"why": "这一版**有意放弃**了图/表编号(没有编号就引用不了「第几图」)。它不是缺件,是取舍;不作判据。"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
package/ai/scaffold.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* scaffold.js — 三个「最小壳」:正确的骨架,不含内容
|
|
3
|
+
*
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* 🔴 为什么给壳,而不是给模板引擎
|
|
6
|
+
* ----------------------------------------------------------------------------
|
|
7
|
+
* 这一层最容易做歪的方向是"喂一段内容,吐一页报告" —— 那需要解析输入、
|
|
8
|
+
* 决定结构、再选版式,最后产出的东西**没法被断言**(对不对取决于读的人)。
|
|
9
|
+
*
|
|
10
|
+
* 这里只做一件事:把**前置条件**给对。漏一条 `<link>`、少一层 `.rep__grid`、
|
|
11
|
+
* 把文章标题写成 h3,页面不会报错,只会静默地长得不对 —— 而这些都是
|
|
12
|
+
* "知道就不会错、不知道就一定会错"的东西。
|
|
13
|
+
*
|
|
14
|
+
* ⇒ 壳只保证三件事:
|
|
15
|
+
* ① 引对了样式表(少一个 = 那部分完全没样式)
|
|
16
|
+
* ② 骨架层级对(.rep > header > .rep__grid > .rep__body/.rep__aside)
|
|
17
|
+
* ③ 每一版**必有件**的位置留出来了(ai/recipes.json 里那几条)
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ 三个壳都必须能通过 `ai check --html --recipe=<自己>`。
|
|
20
|
+
* 这不是口号:05-audit/html-check-gate.js 每次都会把三个壳跑一遍,
|
|
21
|
+
* 壳和判据一旦对不上就红。壳里少写一个必有件,门禁当场抓到。
|
|
22
|
+
*/
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
var fs = require('fs');
|
|
26
|
+
var path = require('path');
|
|
27
|
+
|
|
28
|
+
var AI = __dirname;
|
|
29
|
+
|
|
30
|
+
/** 壳里出现的 CSS 清单(相对库根)。只列真正用到的,不多引。 */
|
|
31
|
+
var SHEETS = [
|
|
32
|
+
'01-tokens/tokens.css',
|
|
33
|
+
'01-tokens/typography.css',
|
|
34
|
+
'01-tokens/page.css',
|
|
35
|
+
'04-recipes/table/table.css',
|
|
36
|
+
'04-recipes/report/report.css'
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function head(title, cssPrefix) {
|
|
40
|
+
var p = String(cssPrefix || 'node_modules/agentcss').replace(/\/+$/, '');
|
|
41
|
+
var links = SHEETS.map(function (s) {
|
|
42
|
+
return '<link rel="stylesheet" href="' + p + '/' + s + '">';
|
|
43
|
+
}).join('\n');
|
|
44
|
+
return '<!doctype html>\n'
|
|
45
|
+
+ '<html lang="zh-CN">\n'
|
|
46
|
+
+ '<head>\n'
|
|
47
|
+
+ '<meta charset="utf-8">\n'
|
|
48
|
+
+ '<meta name="viewport" content="width=device-width, initial-scale=1">\n'
|
|
49
|
+
+ '<title>' + title + '</title>\n'
|
|
50
|
+
+ links + '\n'
|
|
51
|
+
+ '</head>\n';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* 表格:三个壳共用。caption + th[scope] 是硬要求,壳里就写对,
|
|
55
|
+
免得使用者照着抄的时候把这两条一起漏掉。 */
|
|
56
|
+
function table() {
|
|
57
|
+
return ' <div class="table">\n'
|
|
58
|
+
+ ' <table>\n'
|
|
59
|
+
+ ' <caption>表 1 · 三组对比(写"这张表比的是什么",不是"这是数据")</caption>\n'
|
|
60
|
+
+ ' <thead>\n'
|
|
61
|
+
+ ' <tr><th scope="col">项目</th><th scope="col">A 组</th>'
|
|
62
|
+
+ '<th scope="col">B 组</th><th scope="col">C 组</th></tr>\n'
|
|
63
|
+
+ ' </thead>\n'
|
|
64
|
+
+ ' <tbody>\n'
|
|
65
|
+
+ ' <tr><th scope="row">指标一</th><td>—</td><td>—</td><td>—</td></tr>\n'
|
|
66
|
+
+ ' <tr><th scope="row">指标二</th><td>—</td><td>—</td><td>—</td></tr>\n'
|
|
67
|
+
+ ' </tbody>\n'
|
|
68
|
+
+ ' </table>\n'
|
|
69
|
+
+ ' </div>\n';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function figure(wide) {
|
|
73
|
+
return ' <figure class="rep__fig' + (wide ? ' rep__fig--wide' : '') + '">\n'
|
|
74
|
+
+ ' <!-- 放图:09-assets/scientific-plot 的容器 id,或直接 <img src alt> -->\n'
|
|
75
|
+
+ ' <div id="chart"></div>\n'
|
|
76
|
+
+ ' <figcaption class="rep__cap">图 1 · 图说(说清"看什么",不是"这是什么")</figcaption>\n'
|
|
77
|
+
+ ' </figure>\n';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function section(title, body) {
|
|
81
|
+
return ' <section class="rep__sec">\n'
|
|
82
|
+
+ ' <h3>' + title + '</h3>\n'
|
|
83
|
+
+ body
|
|
84
|
+
+ ' </section>\n';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* 三种版式各自的那一段。顺序刻意与 README 的取舍表一致。 */
|
|
88
|
+
var VARIANTS = {
|
|
89
|
+
'report/finance': {
|
|
90
|
+
label: 'report · 金融研究版',
|
|
91
|
+
rootClass: 'rep rep--finance',
|
|
92
|
+
title: '标题:把结论写在标题里',
|
|
93
|
+
kicker: '行业跟踪 · 金融研究版',
|
|
94
|
+
standfirst: '导语:先说结论与它成立的条件,再说依据。读者读完这一段就该知道要不要往下看。',
|
|
95
|
+
before: ' <!-- 关键数字条:这一版的读者先看数、后看论证(ai/recipes.json: rep__keyline) -->\n'
|
|
96
|
+
+ ' <div class="rep__keyline">\n'
|
|
97
|
+
+ ' <div><b>读数一</b><span>—</span></div>\n'
|
|
98
|
+
+ ' <div><b>读数二</b><span>—</span></div>\n'
|
|
99
|
+
+ ' <div><b>读数三</b><span>—</span></div>\n'
|
|
100
|
+
+ ' </div>\n',
|
|
101
|
+
body: '',
|
|
102
|
+
after: ' <!-- 风险披露:这一版必须有(ai/recipes.json: rep__risk) -->\n'
|
|
103
|
+
+ ' <p class="rep__risk">风险披露:把结论的适用条件、数据口径与不确定性写在这里。'
|
|
104
|
+
+ '没有这一段的金融结论读起来像建议,而不是分析。</p>\n',
|
|
105
|
+
aside: '次要读数放这里。放不下的大图不要塞边注 —— 边注只有 14em。'
|
|
106
|
+
},
|
|
107
|
+
'report/technical': {
|
|
108
|
+
label: 'report · 科研技术版',
|
|
109
|
+
rootClass: 'rep rep--technical',
|
|
110
|
+
title: '标题:说清对象与方法',
|
|
111
|
+
kicker: '实验报告 · 科研技术版',
|
|
112
|
+
standfirst: '导语:一句话说清做了什么、得到什么、结论的置信边界在哪。',
|
|
113
|
+
before: '',
|
|
114
|
+
body: '',
|
|
115
|
+
after: ' <!-- 来源脚注:这一版必须有(ai/recipes.json: rep__source) -->\n'
|
|
116
|
+
+ ' <p class="rep__source">来源:数据出处、采集时间与口径。逐条核对的人会顺着这里回查原始材料。</p>\n',
|
|
117
|
+
aside: '术语与口径写这里。这一版的读者会逐条核对,边注用来放"不影响主线但需要说明"的东西。'
|
|
118
|
+
},
|
|
119
|
+
'report/magazine': {
|
|
120
|
+
label: 'report · 杂志编辑版',
|
|
121
|
+
rootClass: 'rep rep--magazine',
|
|
122
|
+
title: '标题:用一个具体的画面开头',
|
|
123
|
+
kicker: '长报道 · 杂志编辑版',
|
|
124
|
+
standfirst: '导语:把读者带进场景,而不是先给结论。这一版是通读的,不是查数的。',
|
|
125
|
+
before: ' <!-- 引语块:这一版靠停顿制造节奏(ai/recipes.json: rep__pull) -->\n'
|
|
126
|
+
+ ' <blockquote class="rep__pull">一句能被单独引用的话。它应该是全文的转折点,不是摘要。</blockquote>\n',
|
|
127
|
+
body: '',
|
|
128
|
+
after: '',
|
|
129
|
+
aside: '短提醒放这里。这一版没有编号,边注是唯一的"旁白"通道。'
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @param {string} id 版式名(见 ai/recipes.json)
|
|
135
|
+
* @param {object} [opts] `{cssPrefix}` —— 样式表前缀,默认 `node_modules/agentcss`
|
|
136
|
+
* @returns {{ok:true, html:string, id:string, requires:string[]}|{ok:false, message:string}}
|
|
137
|
+
*/
|
|
138
|
+
function render(id, opts) {
|
|
139
|
+
opts = opts || {};
|
|
140
|
+
var v = VARIANTS[id];
|
|
141
|
+
if (!v) {
|
|
142
|
+
return { ok: false, message: '未知版式:' + id + '(可用:' + Object.keys(VARIANTS).join(' / ') + ')' };
|
|
143
|
+
}
|
|
144
|
+
var wide = id === 'report/magazine';
|
|
145
|
+
|
|
146
|
+
var body = ' <section class="rep__sec">\n'
|
|
147
|
+
+ ' <h3>第一节:先给判断,再给依据</h3>\n'
|
|
148
|
+
+ ' <p>正文。这一段的每句话都应该能被上一段的数字或下一段的图支撑;'
|
|
149
|
+
+ '撑不住的删掉。</p>\n'
|
|
150
|
+
+ figure(wide)
|
|
151
|
+
+ table()
|
|
152
|
+
+ ' </section>\n'
|
|
153
|
+
+ section('第二节:把限制写清楚', ' <p>适用的边界、数据口径、以及"这一版有意不做什么"。</p>\n');
|
|
154
|
+
|
|
155
|
+
var html = head(v.title, opts.cssPrefix)
|
|
156
|
+
+ '<body class="page">\n'
|
|
157
|
+
+ '<main class="wrap">\n'
|
|
158
|
+
+ ' <!-- 由 `agentcss ai scaffold` 生成:只给壳,不给内容。\n'
|
|
159
|
+
+ ' 写完跑:agentcss ai check 本文件 --recipe=' + id + ' -->\n'
|
|
160
|
+
+ ' <article class="' + v.rootClass + '">\n'
|
|
161
|
+
+ ' <header>\n'
|
|
162
|
+
+ ' <p class="rep__kicker">' + v.kicker + '</p>\n'
|
|
163
|
+
+ ' <h2 class="rep__title">' + v.title + '</h2>\n'
|
|
164
|
+
+ ' <p class="rep__standfirst">' + v.standfirst + '</p>\n'
|
|
165
|
+
+ ' <ul class="rep__meta"><li>作者</li><li>日期</li><li>版本 v1</li></ul>\n'
|
|
166
|
+
+ ' </header>\n'
|
|
167
|
+
+ v.before
|
|
168
|
+
+ ' <div class="rep__grid">\n'
|
|
169
|
+
+ ' <div class="rep__body">\n'
|
|
170
|
+
+ body
|
|
171
|
+
+ ' </div>\n'
|
|
172
|
+
+ ' <aside class="rep__aside">' + v.aside + '</aside>\n'
|
|
173
|
+
+ ' </div>\n'
|
|
174
|
+
+ v.after
|
|
175
|
+
+ ' </article>\n'
|
|
176
|
+
+ '</main>\n'
|
|
177
|
+
+ '</body>\n'
|
|
178
|
+
+ '</html>\n';
|
|
179
|
+
|
|
180
|
+
var requires = [];
|
|
181
|
+
try {
|
|
182
|
+
var all = JSON.parse(fs.readFileSync(path.join(AI, 'recipes.json'), 'utf8'));
|
|
183
|
+
requires = ((all.recipes[id] || {}).requires || []).map(function (r) { return r.selector; });
|
|
184
|
+
} catch (e) { /* 读不到就不报;门禁会盯 recipes.json */ }
|
|
185
|
+
|
|
186
|
+
return { ok: true, html: html, id: id, requires: requires };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function list() {
|
|
190
|
+
return Object.keys(VARIANTS).map(function (k) {
|
|
191
|
+
return { id: k, label: VARIANTS[k].label };
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = { render: render, list: list, VARIANTS: VARIANTS, SHEETS: SHEETS };
|