base-paint 0.1.0 → 0.2.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/dist/charts.d.ts +62 -0
- package/dist/charts.d.ts.map +1 -0
- package/dist/charts.js +1677 -0
- package/dist/charts.js.map +1 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +7 -1
- package/dist/contract.js.map +1 -1
- package/dist/controls.d.ts +114 -0
- package/dist/controls.d.ts.map +1 -0
- package/dist/controls.js +724 -0
- package/dist/controls.js.map +1 -0
- package/dist/help.d.ts +24 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +604 -0
- package/dist/help.js.map +1 -0
- package/dist/index.d.ts +21 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -1
- package/dist/index.js.map +1 -1
- package/dist/spec/charts.d.ts +222 -0
- package/dist/spec/charts.d.ts.map +1 -0
- package/dist/spec/charts.js +35 -0
- package/dist/spec/charts.js.map +1 -0
- package/dist/spec/controls.d.ts +329 -0
- package/dist/spec/controls.d.ts.map +1 -0
- package/dist/spec/controls.js +119 -0
- package/dist/spec/controls.js.map +1 -0
- package/dist/spec/help.d.ts +353 -0
- package/dist/spec/help.d.ts.map +1 -0
- package/dist/spec/help.js +167 -0
- package/dist/spec/help.js.map +1 -0
- package/dist/spec/index.d.ts +40 -0
- package/dist/spec/index.d.ts.map +1 -0
- package/dist/spec/index.js +164 -0
- package/dist/spec/index.js.map +1 -0
- package/dist/spec/style.d.ts +46 -0
- package/dist/spec/style.d.ts.map +1 -0
- package/dist/spec/style.js +39 -0
- package/dist/spec/style.js.map +1 -0
- package/dist/spec/template.d.ts +363 -0
- package/dist/spec/template.d.ts.map +1 -0
- package/dist/spec/template.js +252 -0
- package/dist/spec/template.js.map +1 -0
- package/dist/spec/text.d.ts +157 -0
- package/dist/spec/text.d.ts.map +1 -0
- package/dist/spec/text.js +83 -0
- package/dist/spec/text.js.map +1 -0
- package/dist/style.d.ts +19 -0
- package/dist/style.d.ts.map +1 -1
- package/dist/style.js +923 -0
- package/dist/style.js.map +1 -1
- package/dist/template.d.ts +27 -0
- package/dist/template.d.ts.map +1 -0
- package/dist/template.js +362 -0
- package/dist/template.js.map +1 -0
- package/dist/text.d.ts +19 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +448 -0
- package/dist/text.js.map +1 -0
- package/package.json +2 -2
package/dist/help.js
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
/** base-paint/help:HELP 壳渲染(#78 运行时)。
|
|
2
|
+
*
|
|
3
|
+
* 契约正本:`docs/base-paint-contract.md` §3.5.2(scene-data 契约)/§3.5.3(HELP 壳接口)/
|
|
4
|
+
* §6.5(实现指引);冻结面:`packages/base-render/src/spec/help.ts`(本文件**只消费**,不改签名)。
|
|
5
|
+
*
|
|
6
|
+
* 三条红线(逐条对齐契约与架构裁定):
|
|
7
|
+
* 1. **不得自填**(B3/§3.5.3「填充」):壳把「骨架 + 由 sceneData 渲染出的静态 HTML」拼成模板后
|
|
8
|
+
* **一律走 `fillTemplate`**(`src/template.ts`),共享资产/载荷由填充器注入;壳不写 `<script>` 代码、
|
|
9
|
+
* 不读 `document`/`window`/`globalThis`、不引 `node:`、不引第三方。
|
|
10
|
+
* 2. **占位符契约(数据页,R7)**:`<!--INJECT-DATA-->` 恰 1 次且落在**自带容器**
|
|
11
|
+
* `<script id="payload" type="application/json">` 内;`<!--SHARED-CSS-->`/`<!--SHARED-HELPERS-->`
|
|
12
|
+
* 各恰 1 次;`<!--CHARTS-HELPERS-->`/`<!--CONTENT-->` 各 0 次。标记字面量恒读 `TEMPLATE_MARKERS`、
|
|
13
|
+
* 包裹标签恒读 `ASSET_WRAPPERS`(**不写第二份字面量**)。
|
|
14
|
+
* 3. **单一真相**:复制按钮的 `actionId`/文案恒读 `HELP_COPY_ACTIONS`,承载属性恒读 `ACTION_ID_ATTR`,
|
|
15
|
+
* 复制文本承载属性恒读 `DEFAULT_DATA_ATTR`;根 id 恒读 `HELP_SHELL_ID`;类名命名空间恒读
|
|
16
|
+
* `CONTROL_STYLE_SECTIONS` 的 `helpShell` 项(`STYLE_PREFIX` + 区名 kebab)。**不自造**第二套常量。
|
|
17
|
+
*
|
|
18
|
+
* 契约未规定处的取值(本文件显式记账,不留暗猜):
|
|
19
|
+
* - **交互恒为 CSS-only**(裁定 R4):分组 Tab = 每页自带 `:checked` radio + 同级页面体(标签用 `for` 指向),
|
|
20
|
+
* 二级折叠/Sheet = 原生 `<details>`;壳**不产任何可执行 `<script>`**(唯一的 `<script>` 是 payload 数据容器,
|
|
21
|
+
* 由 `fillTemplate` 注入的 helpers 包裹亦为既有约定)。样式规则归 #75 的 `sharedCssText`
|
|
22
|
+
* (`CONTROL_STYLE_SECTIONS.helpShell` 命名空间),本模块**不产 CSS 常量**(否则构成第二份样式真相)。
|
|
23
|
+
* - **逐场景 CLI 形态文本**(Q11/R17/**R32**):**恒等于 `Scene.id` 原文**(逐字,经 `escapeHtml`)。
|
|
24
|
+
* `doc:836` 的 `skill.<combo>.<key>` 描述的是**键的形状**(`Scene.id` 本就等于它),**不是拼接公式**;
|
|
25
|
+
* 该文本用途是「复制去执行」,必须与真实键逐字一致——**不自造**技能专属 CLI 前缀,也**不自造**
|
|
26
|
+
* `Scene` 字段(`cli`/`data_source` 等回补归 #106)。
|
|
27
|
+
* - **Sheet**(R4):`<details>` 内静态渲染 `prompt_template` 全文 + `editable_fields`(label + 当前 value +
|
|
28
|
+
* hint + 必填标记)+ 三个复制按钮;「输入即改预览」的实时替换 JS **降级为静态预览**(模板禁内联脚本)。
|
|
29
|
+
* - **复制按钮**:每张场景卡的三目标按钮的 `actionId`/文案**逐字取** `HELP_COPY_ACTIONS`
|
|
30
|
+
* (`doc:846`/`doc:994`);`params` 文本 = 各 `editable_fields` 的 `label: value` 行(LF 连接),
|
|
31
|
+
* 无字段时取该场景的 CLI 形态文本(旧侧 `buildPrompt` 语义,`help_template.html:311-322`)。
|
|
32
|
+
* - **`contact.copy_all`**(新类型 `string`,R23):渲染为关于 Tab 的备注文本——旧布尔语义的「一键复制」
|
|
33
|
+
* 按钮需要**第二套 actionId**(`doc:993` 禁止),故本票不渲染该按钮,回补归 #88/#106。
|
|
34
|
+
* - **产物形态**:**完整 HTML 文档**(`<!DOCTYPE html>` + `<html lang="zh-CN">` + `<head>`(`<meta charset="utf-8">`
|
|
35
|
+
* + viewport + `<title>`)+ `<body>` + `</html>`;R31)——`<meta charset>` 是含中文的 UTF-8 文件经 `file://`
|
|
36
|
+
* 打开不乱码的前提,速查台是独立页面。载荷容器与 `<!--SHARED-HELPERS-->` 排在 `<body>` 内、`<!--SHARED-CSS-->`
|
|
37
|
+
* 落在 `<head>` 内(对齐旧模板 `help_template.html:194-201` 的槽位语义)。
|
|
38
|
+
* - **`meta_blocks[].html` 原样透传**(`doc:821`);其余一切 sceneData 文本经 `escapeHtml`(AC-14 五字符)。
|
|
39
|
+
* - **`types` 徽章**:字符串元素用默认配色(CSS 类);`{text,bg?,fg?}` 元素把 `bg`/`fg` 写入内联 `style`
|
|
40
|
+
* (旧 `help_template.html:268-281` 同口径,数据驱动配色无法落静态样式表)。
|
|
41
|
+
* - **`scenes[]` 非空**:由 `SCENE_DATA_SCHEMA` 的 `minItems: 1` 承担(裁定 R11,编排者已补);
|
|
42
|
+
* 校验器恒读 schema,不自立第二份规则。
|
|
43
|
+
*/
|
|
44
|
+
import { escapeHtml } from './contract.js';
|
|
45
|
+
import { fillTemplate } from './template.js';
|
|
46
|
+
import { STYLE_PREFIX } from './style.js';
|
|
47
|
+
import { ACTION_ID_ATTR, DEFAULT_DATA_ATTR } from './spec/controls.js';
|
|
48
|
+
import { HELP_COPY_ACTIONS, HELP_SHELL_ID, SCENE_DATA_SCHEMA, SCENE_STATUS, SCENE_TYPE_FIELD, } from './spec/help.js';
|
|
49
|
+
import { CONTROL_STYLE_SECTIONS } from './spec/style.js';
|
|
50
|
+
import { ASSET_WRAPPERS, CONTAINER_CHECK_RULE, TEMPLATE_MARKERS } from './spec/template.js';
|
|
51
|
+
/** 换行(仓库口径:`String.fromCharCode(10)`,不写字面 `\n`)。 */
|
|
52
|
+
const LF = String.fromCharCode(10);
|
|
53
|
+
/** 样式区名 kebab(`helpShell` → `help-shell`):区名 → 类名命名空间的**唯一映射**。 */
|
|
54
|
+
function sectionSlug(section) {
|
|
55
|
+
return section.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase());
|
|
56
|
+
}
|
|
57
|
+
/** 样式区名(`helpShell`):**恒取 `CONTROL_STYLE_SECTIONS` 的闭集成员**——kebab 后与 `HELP_SHELL_ID`
|
|
58
|
+
* 同值者(**不自写字面量**)。闭集漂移即在此 fail-fast,不会静默改用别的命名空间。 */
|
|
59
|
+
const HELP_SECTION = CONTROL_STYLE_SECTIONS.find((section) => STYLE_PREFIX + sectionSlug(section) === HELP_SHELL_ID);
|
|
60
|
+
if (HELP_SECTION === undefined) {
|
|
61
|
+
throw new Error('base-paint/help:CONTROL_STYLE_SECTIONS 闭集缺与 HELP_SHELL_ID 同 kebab 的区名('
|
|
62
|
+
+ HELP_SHELL_ID + ')');
|
|
63
|
+
}
|
|
64
|
+
/** 类名命名空间根(`ilife-help-shell`):由**闭集区名**派生,与根元素 id(`HELP_SHELL_ID`)同值但来源独立。 */
|
|
65
|
+
const HELP_CLASS_ROOT = STYLE_PREFIX + sectionSlug(HELP_SECTION);
|
|
66
|
+
/** 类名拼装(命名空间内,不自写第二份前缀)。 */
|
|
67
|
+
function cls(suffix) {
|
|
68
|
+
return HELP_CLASS_ROOT + '-' + suffix;
|
|
69
|
+
}
|
|
70
|
+
/** 分组 Tab 的 radio `name`(页内唯一分组名,标签用 `for` 跨节点指向)。 */
|
|
71
|
+
const TAB_GROUP_NAME = cls('tab-group');
|
|
72
|
+
/** 分组页 radio 的 id(**索引派生**:数据里的 group.id 可能重复,不得用作 HTML id)。 */
|
|
73
|
+
function tabRadioId(index) {
|
|
74
|
+
return cls('tab-' + index);
|
|
75
|
+
}
|
|
76
|
+
/** 关于 Tab 的 radio id。 */
|
|
77
|
+
const ABOUT_TAB_ID = cls('tab-about');
|
|
78
|
+
/* ── 1. 错误形态 ────────────────────────────────────────────── */
|
|
79
|
+
/** 校验失败(形态逐字对齐冻结的 `HelpSchemaErrorShape`:`{ name, code, path, message }`)。
|
|
80
|
+
*
|
|
81
|
+
* **不从 `src/index.ts` 导出**:冻结面 `SPEC_FROZEN_SURFACE` 无该运行时条目,导出会打破
|
|
82
|
+
* 「新增运行时出口恰好等于清单 implemented 的运行时项」出口面锁(与 `TemplateError`/`ControlsError` 同口径,
|
|
83
|
+
* 裁定 R13)。调用方按 `name`/`code`/`path` 判定。
|
|
84
|
+
*/
|
|
85
|
+
export class HelpSchemaError extends Error {
|
|
86
|
+
name = 'HelpSchemaError';
|
|
87
|
+
code;
|
|
88
|
+
/** JSON 指针式路径(根为 `/`)。 */
|
|
89
|
+
path;
|
|
90
|
+
constructor(code, path, message) {
|
|
91
|
+
super(message);
|
|
92
|
+
this.code = code;
|
|
93
|
+
this.path = path;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function fail(code, path, message) {
|
|
97
|
+
throw new HelpSchemaError(code, path, message);
|
|
98
|
+
}
|
|
99
|
+
function isPlainObject(value) {
|
|
100
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
101
|
+
}
|
|
102
|
+
function hasOwn(value, key) {
|
|
103
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
104
|
+
}
|
|
105
|
+
/** 供 message 辨因的类型描述(不参与判定)。 */
|
|
106
|
+
function describe(value) {
|
|
107
|
+
if (value === null)
|
|
108
|
+
return 'null';
|
|
109
|
+
if (Array.isArray(value))
|
|
110
|
+
return 'array(' + value.length + ')';
|
|
111
|
+
if (typeof value === 'string')
|
|
112
|
+
return 'string(' + JSON.stringify(value) + ')';
|
|
113
|
+
return typeof value;
|
|
114
|
+
}
|
|
115
|
+
/** `type` 关键字判定(子集内的四种类型 + 容错类型;未知 type 不参与判定)。 */
|
|
116
|
+
function typeMatches(type, value) {
|
|
117
|
+
if (type === 'object')
|
|
118
|
+
return isPlainObject(value);
|
|
119
|
+
if (type === 'array')
|
|
120
|
+
return Array.isArray(value);
|
|
121
|
+
if (type === 'string')
|
|
122
|
+
return typeof value === 'string';
|
|
123
|
+
if (type === 'boolean')
|
|
124
|
+
return typeof value === 'boolean';
|
|
125
|
+
if (type === 'number')
|
|
126
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
127
|
+
if (type === 'integer')
|
|
128
|
+
return typeof value === 'number' && Number.isInteger(value);
|
|
129
|
+
if (type === 'null')
|
|
130
|
+
return value === null;
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
/** 路径拼接(根为 `''`,对外展示时映射为 `/`)。 */
|
|
134
|
+
function childPath(path, key) {
|
|
135
|
+
return path + '/' + String(key);
|
|
136
|
+
}
|
|
137
|
+
function showPath(path) {
|
|
138
|
+
return path === '' ? '/' : path;
|
|
139
|
+
}
|
|
140
|
+
/** 支持的关键字(**唯一实现**,恒读 `SCENE_DATA_SCHEMA` 的字段,不自立第二份规则表):
|
|
141
|
+
* `type`/`enum`/`minLength`/`minItems`/`items`/`required`/`additionalProperties:false`/
|
|
142
|
+
* `properties`/`oneOf`。返回**首个**违规(不聚合)。 */
|
|
143
|
+
function firstViolation(schema, value, path) {
|
|
144
|
+
if (!isPlainObject(schema))
|
|
145
|
+
return null;
|
|
146
|
+
const type = schema.type;
|
|
147
|
+
if (typeof type === 'string' && !typeMatches(type, value)) {
|
|
148
|
+
return { path, message: '类型不符:期望 ' + type + ',实际 ' + describe(value) };
|
|
149
|
+
}
|
|
150
|
+
const allowed = schema.enum;
|
|
151
|
+
if (Array.isArray(allowed) && !allowed.some((member) => member === value)) {
|
|
152
|
+
return { path, message: '不在 enum 白名单:' + describe(value) };
|
|
153
|
+
}
|
|
154
|
+
const minLength = schema.minLength;
|
|
155
|
+
if (typeof minLength === 'number' && typeof value === 'string' && value.length < minLength) {
|
|
156
|
+
return { path, message: '长度不足:minLength=' + minLength + ',实际 ' + value.length };
|
|
157
|
+
}
|
|
158
|
+
const minItems = schema.minItems;
|
|
159
|
+
if (typeof minItems === 'number' && Array.isArray(value) && value.length < minItems) {
|
|
160
|
+
return { path, message: '元素不足:minItems=' + minItems + ',实际 ' + value.length };
|
|
161
|
+
}
|
|
162
|
+
if (Array.isArray(value)) {
|
|
163
|
+
const items = schema.items;
|
|
164
|
+
if (items !== undefined) {
|
|
165
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
166
|
+
const violation = firstViolation(items, value[i], childPath(path, i));
|
|
167
|
+
if (violation !== null)
|
|
168
|
+
return violation;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (isPlainObject(value)) {
|
|
173
|
+
const required = schema.required;
|
|
174
|
+
if (Array.isArray(required)) {
|
|
175
|
+
for (const key of required) {
|
|
176
|
+
if (typeof key === 'string' && !hasOwn(value, key)) {
|
|
177
|
+
return { path: childPath(path, key), message: '缺必填字段:' + key };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const properties = isPlainObject(schema.properties) ? schema.properties : null;
|
|
182
|
+
if (schema.additionalProperties === false) {
|
|
183
|
+
for (const key of Object.keys(value)) {
|
|
184
|
+
if (properties === null || !hasOwn(properties, key)) {
|
|
185
|
+
return { path: childPath(path, key), message: '多余字段(additionalProperties:false):' + key };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (properties !== null) {
|
|
190
|
+
for (const key of Object.keys(properties)) {
|
|
191
|
+
if (!hasOwn(value, key))
|
|
192
|
+
continue; // 缺失由 `required` 判定
|
|
193
|
+
const violation = firstViolation(properties[key], value[key], childPath(path, key));
|
|
194
|
+
if (violation !== null)
|
|
195
|
+
return violation;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const oneOf = schema.oneOf;
|
|
200
|
+
if (Array.isArray(oneOf)) {
|
|
201
|
+
let hits = 0;
|
|
202
|
+
for (const branch of oneOf) {
|
|
203
|
+
if (firstViolation(branch, value, path) === null)
|
|
204
|
+
hits += 1;
|
|
205
|
+
}
|
|
206
|
+
if (hits !== 1) {
|
|
207
|
+
return { path, message: 'oneOf 命中 ' + hits + ' 个分支(须恰 1)' };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
/** 遍历 `groups[].subgroups[].scenes[]`(防御式:任一层非数组即止)。 */
|
|
213
|
+
function eachScene(data) {
|
|
214
|
+
const sites = [];
|
|
215
|
+
if (!isPlainObject(data))
|
|
216
|
+
return sites;
|
|
217
|
+
const groups = data.groups;
|
|
218
|
+
if (!Array.isArray(groups))
|
|
219
|
+
return sites;
|
|
220
|
+
groups.forEach((group, gi) => {
|
|
221
|
+
if (!isPlainObject(group))
|
|
222
|
+
return;
|
|
223
|
+
const subgroups = group.subgroups;
|
|
224
|
+
if (!Array.isArray(subgroups))
|
|
225
|
+
return;
|
|
226
|
+
subgroups.forEach((subgroup, si) => {
|
|
227
|
+
if (!isPlainObject(subgroup))
|
|
228
|
+
return;
|
|
229
|
+
const scenes = subgroup.scenes;
|
|
230
|
+
if (!Array.isArray(scenes))
|
|
231
|
+
return;
|
|
232
|
+
scenes.forEach((scene, ci) => {
|
|
233
|
+
if (!isPlainObject(scene))
|
|
234
|
+
return;
|
|
235
|
+
sites.push({ scene, path: '/groups/' + gi + '/subgroups/' + si + '/scenes/' + ci });
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
return sites;
|
|
240
|
+
}
|
|
241
|
+
/** `duplicate-id`:`scenes[].id` **全局唯一**(`doc:820`/R6)。非字符串 id 归 `schema-invalid`。 */
|
|
242
|
+
function findDuplicateId(sites) {
|
|
243
|
+
const seen = new Set();
|
|
244
|
+
for (const site of sites) {
|
|
245
|
+
const id = site.scene.id;
|
|
246
|
+
if (typeof id !== 'string')
|
|
247
|
+
continue;
|
|
248
|
+
if (seen.has(id)) {
|
|
249
|
+
return { path: childPath(site.path, 'id'), message: 'scene.id 重复:' + id };
|
|
250
|
+
}
|
|
251
|
+
seen.add(id);
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
/** `status-invalid`:**出现**的 `status` 不在 `SCENE_STATUS` 白名单(缺失归 `schema-invalid`)。 */
|
|
256
|
+
function findStatusViolation(sites) {
|
|
257
|
+
const allowed = SCENE_STATUS;
|
|
258
|
+
for (const site of sites) {
|
|
259
|
+
if (!hasOwn(site.scene, 'status'))
|
|
260
|
+
continue;
|
|
261
|
+
const status = site.scene.status;
|
|
262
|
+
if (!allowed.includes(status)) {
|
|
263
|
+
return { path: childPath(site.path, 'status'), message: 'status 不在 SCENE_STATUS:' + describe(status) };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
/** `types-invalid`:`types` **元素**既非字符串、也非带字符串 `text` 的对象(R6);
|
|
269
|
+
* `types` 非数组/对象元素的多余键归 `schema-invalid`(oneOf/additionalProperties)。 */
|
|
270
|
+
function findTypesViolation(sites) {
|
|
271
|
+
for (const site of sites) {
|
|
272
|
+
if (!hasOwn(site.scene, SCENE_TYPE_FIELD))
|
|
273
|
+
continue;
|
|
274
|
+
const types = site.scene[SCENE_TYPE_FIELD];
|
|
275
|
+
if (!Array.isArray(types))
|
|
276
|
+
continue;
|
|
277
|
+
for (let i = 0; i < types.length; i += 1) {
|
|
278
|
+
const element = types[i];
|
|
279
|
+
const ok = typeof element === 'string' || (isPlainObject(element) && typeof element.text === 'string');
|
|
280
|
+
if (!ok) {
|
|
281
|
+
return {
|
|
282
|
+
path: childPath(childPath(site.path, SCENE_TYPE_FIELD), i),
|
|
283
|
+
message: 'types 元素须为字符串或 {text}:' + describe(element),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
/** scene-data 校验(**首个命中即抛**,次序恒为 `duplicate-id` → `status-invalid` → `types-invalid`
|
|
291
|
+
* → `schema-invalid`;`SCENE_DATA_SCHEMA` 是唯一机读权威,含 `minItems: 1` 的 `scenes[]` 非空约束)。 */
|
|
292
|
+
function validateSceneData(data) {
|
|
293
|
+
const sites = eachScene(data);
|
|
294
|
+
const duplicated = findDuplicateId(sites);
|
|
295
|
+
if (duplicated !== null) {
|
|
296
|
+
fail('duplicate-id', showPath(duplicated.path), duplicated.message);
|
|
297
|
+
}
|
|
298
|
+
const status = findStatusViolation(sites);
|
|
299
|
+
if (status !== null) {
|
|
300
|
+
fail('status-invalid', showPath(status.path), status.message);
|
|
301
|
+
}
|
|
302
|
+
const types = findTypesViolation(sites);
|
|
303
|
+
if (types !== null) {
|
|
304
|
+
fail('types-invalid', showPath(types.path), types.message);
|
|
305
|
+
}
|
|
306
|
+
const schema = firstViolation(SCENE_DATA_SCHEMA, data, '');
|
|
307
|
+
if (schema !== null) {
|
|
308
|
+
fail('schema-invalid', showPath(schema.path), schema.message);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/* ── 4. 渲染小件 ────────────────────────────────────────────── */
|
|
312
|
+
/** 属性(值一律 `escapeHtml`;属性名由调用方给冻结常量或本模块类名)。 */
|
|
313
|
+
function attr(name, value) {
|
|
314
|
+
return ' ' + name + '="' + escapeHtml(value) + '"';
|
|
315
|
+
}
|
|
316
|
+
/** 文本节点(一律 `escapeHtml`)。 */
|
|
317
|
+
function text(value) {
|
|
318
|
+
return escapeHtml(value);
|
|
319
|
+
}
|
|
320
|
+
/** 复制按钮(`actionId`/文案恒读 `HELP_COPY_ACTIONS`,承载属性恒读冻结常量)。 */
|
|
321
|
+
function renderCopyButton(target, payload, labelOverride) {
|
|
322
|
+
const action = HELP_COPY_ACTIONS[target];
|
|
323
|
+
const label = labelOverride !== undefined && labelOverride !== '' ? labelOverride : action.label;
|
|
324
|
+
return '<button type="button" class="' + cls('btn') + ' ' + cls('btn-' + target) + '"'
|
|
325
|
+
+ attr(ACTION_ID_ATTR, action.actionId)
|
|
326
|
+
+ attr(DEFAULT_DATA_ATTR, payload)
|
|
327
|
+
+ '>' + text(label) + '</button>';
|
|
328
|
+
}
|
|
329
|
+
/** 逐场景 CLI 形态文本 = **`Scene.id` 原文**(逐字,经 `escapeHtml`;Q11/R17/**R32**)。
|
|
330
|
+
*
|
|
331
|
+
* 契约 §3.5.3 的 `skill.<combo>.<key>` 描述的是**键的形状**(`Scene.id` 本就等于它),
|
|
332
|
+
* **不是拼接公式**;该文本的用途是「复制去执行」,必须与真实键逐字一致,base-paint
|
|
333
|
+
* **不得臆造**技能专属 CLI 前缀(更深回补归 #106)。 */
|
|
334
|
+
function cliText(scene) {
|
|
335
|
+
return scene.id;
|
|
336
|
+
}
|
|
337
|
+
/** `params` 复制文本:`label: value` 行(LF 连接,**不 trim/不改写**);无字段时取 CLI 形态文本。 */
|
|
338
|
+
function paramsText(scene, cli) {
|
|
339
|
+
const lines = (scene.editable_fields ?? [])
|
|
340
|
+
.filter((field) => field.value !== '')
|
|
341
|
+
.map((field) => field.label + ': ' + field.value);
|
|
342
|
+
return lines.length > 0 ? lines.join(LF) : cli;
|
|
343
|
+
}
|
|
344
|
+
/** 标题区(`skill_name`/`title`/`subtitle`;`subtitle` 必须渲染,F3 读而不渲染属缺陷)。 */
|
|
345
|
+
function renderHero(data, sceneCount) {
|
|
346
|
+
const parts = ['<header class="' + cls('hero') + '">'];
|
|
347
|
+
parts.push('<p class="' + cls('eyebrow') + '">' + text(data.skill_name) + '</p>');
|
|
348
|
+
parts.push('<h1 class="' + cls('title') + '">' + text(data.title) + '</h1>');
|
|
349
|
+
if (typeof data.subtitle === 'string') {
|
|
350
|
+
parts.push('<p class="' + cls('subtitle') + '">' + text(data.subtitle) + '</p>');
|
|
351
|
+
}
|
|
352
|
+
parts.push('<p class="' + cls('lead') + '">' + text(String(sceneCount) + ' 场景 · 点场景卡看指令全文,一键复制指令') + '</p>');
|
|
353
|
+
parts.push('</header>');
|
|
354
|
+
return parts.join(LF);
|
|
355
|
+
}
|
|
356
|
+
/** 首用引导横幅(`init_banner`;`steps` 取新类型 `readonly string[]`,R23)。 */
|
|
357
|
+
function renderInitBanner(banner) {
|
|
358
|
+
const parts = ['<div class="' + cls('init') + '">'];
|
|
359
|
+
parts.push('<p class="' + cls('init-title') + '">' + text(banner.title) + '</p>');
|
|
360
|
+
if (typeof banner.subtitle === 'string') {
|
|
361
|
+
parts.push('<p class="' + cls('init-subtitle') + '">' + text(banner.subtitle) + '</p>');
|
|
362
|
+
}
|
|
363
|
+
if (typeof banner.prompt === 'string') {
|
|
364
|
+
parts.push('<p class="' + cls('init-prompt') + '">' + text(banner.prompt) + '</p>');
|
|
365
|
+
parts.push(renderCopyButton('prompt', banner.prompt, banner.button_text ?? ''));
|
|
366
|
+
}
|
|
367
|
+
const steps = banner.steps;
|
|
368
|
+
if (Array.isArray(steps) && steps.length > 0) {
|
|
369
|
+
parts.push('<ol class="' + cls('init-steps') + '">'
|
|
370
|
+
+ steps.map((step) => '<li class="' + cls('init-step') + '">' + text(step) + '</li>').join('')
|
|
371
|
+
+ '</ol>');
|
|
372
|
+
}
|
|
373
|
+
parts.push('</div>');
|
|
374
|
+
return parts.join(LF);
|
|
375
|
+
}
|
|
376
|
+
/** 分组 Tab 条(`<label for>` 指向各页自带的 radio;CSS-only,零脚本)。 */
|
|
377
|
+
function renderTabBar(groups) {
|
|
378
|
+
const labels = [];
|
|
379
|
+
groups.forEach((group, index) => {
|
|
380
|
+
labels.push('<label class="' + cls('tab') + '"' + attr('for', tabRadioId(index)) + '>'
|
|
381
|
+
+ (typeof group.icon === 'string' && group.icon !== ''
|
|
382
|
+
? '<span class="' + cls('tab-icon') + '">' + text(group.icon) + '</span>'
|
|
383
|
+
: '')
|
|
384
|
+
+ text(group.label) + '</label>');
|
|
385
|
+
});
|
|
386
|
+
labels.push('<label class="' + cls('tab') + '"' + attr('for', ABOUT_TAB_ID) + '>' + text('关于') + '</label>');
|
|
387
|
+
return '<nav class="' + cls('tab-bar') + '" aria-label="分组导航">' + labels.join('') + '</nav>';
|
|
388
|
+
}
|
|
389
|
+
/** 类型徽章(`types` 元素:字符串走 CSS 默认配色;`{text,bg?,fg?}` 把配色写入内联 style)。 */
|
|
390
|
+
function renderTypeBadge(badge) {
|
|
391
|
+
const element = typeof badge === 'string' ? { text: badge } : badge;
|
|
392
|
+
const style = [];
|
|
393
|
+
if (typeof element.bg === 'string' && element.bg !== '')
|
|
394
|
+
style.push('background:' + element.bg);
|
|
395
|
+
if (typeof element.fg === 'string' && element.fg !== '')
|
|
396
|
+
style.push('color:' + element.fg);
|
|
397
|
+
return '<span class="' + cls('badge') + '"'
|
|
398
|
+
+ (style.length > 0 ? attr('style', style.join(';')) : '')
|
|
399
|
+
+ '>' + text(element.text) + '</span>';
|
|
400
|
+
}
|
|
401
|
+
/** 徽章组(唤醒词 chip + `types` 变体 + `status` 待开发徽章,恒读 `SCENE_STATUS`)。 */
|
|
402
|
+
function renderBadges(scene) {
|
|
403
|
+
const parts = ['<div class="' + cls('card-top') + '">'];
|
|
404
|
+
parts.push('<span class="' + cls('chip') + '">' + text(scene.wake_word) + '</span>');
|
|
405
|
+
for (const badge of scene[SCENE_TYPE_FIELD] ?? [])
|
|
406
|
+
parts.push(renderTypeBadge(badge));
|
|
407
|
+
if (scene.status === SCENE_STATUS[1]) {
|
|
408
|
+
// 徽章文本**恒读**冻结常量(`SCENE_STATUS[1]`),不自写字面量:改 spec 值即随动。
|
|
409
|
+
parts.push('<span class="' + cls('badge') + ' ' + cls('badge-dev') + '">' + text(SCENE_STATUS[1]) + '</span>');
|
|
410
|
+
}
|
|
411
|
+
parts.push('</div>');
|
|
412
|
+
return parts.join('');
|
|
413
|
+
}
|
|
414
|
+
/** `editable_fields` 静态渲染:label + 当前 value + hint + 必填标记(R4:实时替换降级为静态预览)。 */
|
|
415
|
+
function renderFields(fields) {
|
|
416
|
+
const items = fields.map((field) => {
|
|
417
|
+
const parts = ['<li class="' + cls('field') + '"' + attr('data-field', field.name) + '>'];
|
|
418
|
+
parts.push('<span class="' + cls('field-label') + '">' + text(field.label) + '</span>');
|
|
419
|
+
parts.push('<span class="' + cls('field-value') + '">' + text(field.value) + '</span>');
|
|
420
|
+
if (typeof field.hint === 'string' && field.hint !== '') {
|
|
421
|
+
parts.push('<span class="' + cls('field-hint') + '">' + text(field.hint) + '</span>');
|
|
422
|
+
}
|
|
423
|
+
if (field.required === true) {
|
|
424
|
+
parts.push('<span class="' + cls('field-required') + '">' + text('必填') + '</span>');
|
|
425
|
+
}
|
|
426
|
+
parts.push('</li>');
|
|
427
|
+
return parts.join('');
|
|
428
|
+
});
|
|
429
|
+
return '<ul class="' + cls('fields') + '">' + items.join('') + '</ul>';
|
|
430
|
+
}
|
|
431
|
+
/** 场景卡(标题/唤醒词/`types` 徽章/逐场景 CLI 形态文本 + Sheet)。 */
|
|
432
|
+
function renderSceneCard(data, group, scene) {
|
|
433
|
+
const cli = cliText(scene);
|
|
434
|
+
const fields = scene.editable_fields ?? [];
|
|
435
|
+
const sheet = ['<details class="' + cls('sheet') + '">'];
|
|
436
|
+
sheet.push('<summary class="' + cls('sheet-summary') + '">' + text('查看指令') + '</summary>');
|
|
437
|
+
sheet.push('<div class="' + cls('sheet-body') + '">');
|
|
438
|
+
sheet.push('<pre class="' + cls('prompt') + '">' + text(scene.prompt_template) + '</pre>');
|
|
439
|
+
if (fields.length > 0)
|
|
440
|
+
sheet.push(renderFields(fields));
|
|
441
|
+
sheet.push('<div class="' + cls('actions') + '">'
|
|
442
|
+
+ renderCopyButton('prompt', scene.prompt_template)
|
|
443
|
+
+ renderCopyButton('wakeWord', scene.wake_word)
|
|
444
|
+
+ renderCopyButton('params', paramsText(scene, cli))
|
|
445
|
+
+ '</div>');
|
|
446
|
+
sheet.push('</div>');
|
|
447
|
+
sheet.push('</details>');
|
|
448
|
+
return '<article class="' + cls('card') + '"' + attr('data-scene-id', scene.id) + attr('data-group-id', group.id) + '>'
|
|
449
|
+
+ renderBadges(scene)
|
|
450
|
+
+ '<h3 class="' + cls('card-title') + '">' + text(scene.title) + '</h3>'
|
|
451
|
+
+ '<code class="' + cls('cli') + '">' + text(cli) + '</code>'
|
|
452
|
+
+ sheet.join('')
|
|
453
|
+
+ '</article>';
|
|
454
|
+
}
|
|
455
|
+
/** 二级折叠(`<details open>`;子功能名 + 场景数)。 */
|
|
456
|
+
function renderSubgroup(data, group, subgroup) {
|
|
457
|
+
const cards = subgroup.scenes.map((scene) => renderSceneCard(data, group, scene));
|
|
458
|
+
return '<details class="' + cls('subgroup') + '"' + attr('data-subgroup-id', subgroup.id) + ' open>'
|
|
459
|
+
+ '<summary class="' + cls('subgroup-summary') + '">' + text(subgroup.label)
|
|
460
|
+
+ '<span class="' + cls('count') + '">' + text(String(subgroup.scenes.length)) + '</span></summary>'
|
|
461
|
+
+ '<div class="' + cls('subgroup-body') + '">'
|
|
462
|
+
+ '<div class="' + cls('grid') + '">' + cards.join('') + '</div>'
|
|
463
|
+
+ '</div>'
|
|
464
|
+
+ '</details>';
|
|
465
|
+
}
|
|
466
|
+
/** 分组页(自带 `:checked` radio + 同级页面体:CSS-only Tab 的结构前提)。 */
|
|
467
|
+
function renderGroupPage(data, group, index, checked) {
|
|
468
|
+
const subgroups = group.subgroups.map((subgroup) => renderSubgroup(data, group, subgroup));
|
|
469
|
+
return '<section class="' + cls('page') + '"' + attr('data-group-id', group.id) + '>'
|
|
470
|
+
+ '<input class="' + cls('tab-input') + '" type="radio"' + attr('name', TAB_GROUP_NAME) + attr('id', tabRadioId(index))
|
|
471
|
+
+ (checked ? ' checked' : '') + '>'
|
|
472
|
+
+ '<div class="' + cls('page-body') + '">' + subgroups.join('') + '</div>'
|
|
473
|
+
+ '</section>';
|
|
474
|
+
}
|
|
475
|
+
/** 关于 Tab(联系作者 → 版本 → 其他技能,F3 区块序)。 */
|
|
476
|
+
function renderAboutPage(data, checked) {
|
|
477
|
+
const parts = ['<section class="' + cls('page') + '"' + attr('data-page', 'about') + '>'];
|
|
478
|
+
parts.push('<input class="' + cls('tab-input') + '" type="radio"' + attr('name', TAB_GROUP_NAME)
|
|
479
|
+
+ attr('id', ABOUT_TAB_ID) + (checked ? ' checked' : '') + '>');
|
|
480
|
+
parts.push('<div class="' + cls('page-body') + '">');
|
|
481
|
+
const contact = data.contact;
|
|
482
|
+
if (contact !== undefined && contact.items.length > 0) {
|
|
483
|
+
parts.push('<section class="' + cls('about-sec') + '">');
|
|
484
|
+
parts.push('<h2 class="' + cls('about-head') + '">' + text('联系作者') + '</h2>');
|
|
485
|
+
parts.push('<ul class="' + cls('about-list') + '">' + contact.items.map((item) => ('<li class="' + cls('about-row') + '">'
|
|
486
|
+
+ '<span class="' + cls('about-label') + '">' + text(item.label) + '</span>'
|
|
487
|
+
+ '<span class="' + cls('about-value') + '">' + text(item.value) + '</span>'
|
|
488
|
+
+ '</li>')).join('') + '</ul>');
|
|
489
|
+
if (typeof contact.copy_all === 'string' && contact.copy_all !== '') {
|
|
490
|
+
parts.push('<p class="' + cls('about-note') + '">' + text(contact.copy_all) + '</p>');
|
|
491
|
+
}
|
|
492
|
+
parts.push('</section>');
|
|
493
|
+
}
|
|
494
|
+
parts.push('<section class="' + cls('about-sec') + '">');
|
|
495
|
+
parts.push('<h2 class="' + cls('about-head') + '">' + text('版本') + '</h2>');
|
|
496
|
+
parts.push('<p class="' + cls('about-row') + '"><span class="' + cls('about-label') + '">' + text(data.skill_name) + '</span>'
|
|
497
|
+
+ (typeof data.version === 'string' && data.version !== ''
|
|
498
|
+
? '<span class="' + cls('about-value') + '">' + text('v' + data.version) + '</span>'
|
|
499
|
+
: '')
|
|
500
|
+
+ '</p>');
|
|
501
|
+
parts.push('</section>');
|
|
502
|
+
const recommendations = data.recommendations ?? [];
|
|
503
|
+
if (recommendations.length > 0) {
|
|
504
|
+
parts.push('<section class="' + cls('about-sec') + '">');
|
|
505
|
+
parts.push('<h2 class="' + cls('about-head') + '">' + text('其他技能') + '</h2>');
|
|
506
|
+
parts.push('<ul class="' + cls('about-list') + '">' + recommendations.map((item) => ('<li class="' + cls('about-row') + '">'
|
|
507
|
+
+ '<span class="' + cls('about-label') + '">' + text(item.name) + '</span>'
|
|
508
|
+
+ (typeof item.reason === 'string' && item.reason !== ''
|
|
509
|
+
? '<span class="' + cls('about-value') + '">' + text(item.reason) + '</span>'
|
|
510
|
+
: '')
|
|
511
|
+
+ (typeof item.wake_word === 'string' && item.wake_word !== ''
|
|
512
|
+
? '<span class="' + cls('badge') + '">' + text(item.wake_word) + '</span>'
|
|
513
|
+
: '')
|
|
514
|
+
+ '</li>')).join('') + '</ul>');
|
|
515
|
+
parts.push('</section>');
|
|
516
|
+
}
|
|
517
|
+
parts.push('</div>');
|
|
518
|
+
parts.push('</section>');
|
|
519
|
+
return parts.join(LF);
|
|
520
|
+
}
|
|
521
|
+
/** `meta_blocks`:标题转义、`html` **原样透传**(`doc:821`)。 */
|
|
522
|
+
function renderMetaBlocks(blocks) {
|
|
523
|
+
return blocks.map((block) => '<section class="' + cls('meta-block') + '"' + attr('data-meta-id', block.id) + '>'
|
|
524
|
+
+ '<h2 class="' + cls('meta-title') + '">' + text(block.title) + '</h2>'
|
|
525
|
+
+ '<div class="' + cls('meta-html') + '">' + block.html + '</div>'
|
|
526
|
+
+ '</section>').join(LF);
|
|
527
|
+
}
|
|
528
|
+
/* ── 5. 内置壳模板(**非导出**,裁定 R13;可经 `HelpShellInput.template` 覆盖) ── */
|
|
529
|
+
/** payload 容器开标签:标签名恒取 `ASSET_WRAPPERS`,`id`/`type` 恒取 `CONTAINER_CHECK_RULE`
|
|
530
|
+
* (容器校验规则的 `id`/`type` 与 `DEFAULT_DATA_SCRIPT_ID`/`DATA_SCRIPT_TYPE` 同值,不写第二份字面量)。 */
|
|
531
|
+
const DATA_SCRIPT_OPEN = ASSET_WRAPPERS.sharedHelpersJs.openTag.slice(0, -1)
|
|
532
|
+
+ ' id="' + CONTAINER_CHECK_RULE.id + '" type="' + CONTAINER_CHECK_RULE.type + '">';
|
|
533
|
+
const DATA_SCRIPT_CLOSE = ASSET_WRAPPERS.sharedHelpersJs.closeTag;
|
|
534
|
+
/** 文档 `<title>`:`title · skill_name`(两者均经 `escapeHtml`,R31)。
|
|
535
|
+
* `title` 是必填非空字段(`minLength: 1`),`skill_name` 同;拼串使两个字段都有可观察效果。 */
|
|
536
|
+
function documentTitle(data) {
|
|
537
|
+
return data.title + ' · ' + data.skill_name;
|
|
538
|
+
}
|
|
539
|
+
/** 内置壳模板(**完整 HTML 文档**,R31):数据页分型(`<!--INJECT-DATA-->` 恰 1 次且在自带容器内;
|
|
540
|
+
* `<!--SHARED-CSS-->` 落在 `<head>`、`<!--SHARED-HELPERS-->` 落在 `<body>`,各恰 1;
|
|
541
|
+
* `<!--CHARTS-HELPERS-->`/`<!--CONTENT-->` 各 0)。
|
|
542
|
+
*
|
|
543
|
+
* 完整文档而非片段:`<meta charset="utf-8">` 是含中文的 UTF-8 文件经 `file://` 打开不乱码的前提
|
|
544
|
+
* (Windows 尤甚),速查台是独立页面(仓内各技能包的 `templates` 目录与旧
|
|
545
|
+
* `help_template.html` 同为完整文档)。两个共享资产标记仍是**裸标记**(不得预包裹,不变量②),
|
|
546
|
+
* 包裹由 `fillTemplate` 按 `ASSET_WRAPPERS` 完成。 */
|
|
547
|
+
function buildShellTemplate(data) {
|
|
548
|
+
const groups = data.groups;
|
|
549
|
+
const sceneCount = groups.reduce((total, group) => total + group.subgroups.reduce((sub, item) => sub + item.scenes.length, 0), 0);
|
|
550
|
+
const hasGroupPage = groups.length > 0;
|
|
551
|
+
const parts = [];
|
|
552
|
+
parts.push('<!DOCTYPE html>');
|
|
553
|
+
parts.push('<html lang="zh-CN">');
|
|
554
|
+
parts.push('<head>');
|
|
555
|
+
parts.push('<meta charset="utf-8">');
|
|
556
|
+
parts.push('<meta name="viewport" content="width=device-width, initial-scale=1">');
|
|
557
|
+
parts.push('<title>' + text(documentTitle(data)) + '</title>');
|
|
558
|
+
parts.push(TEMPLATE_MARKERS.sharedCss); // 裸标记:填充器包成 <style>(落在 <head> 内)
|
|
559
|
+
parts.push('</head>');
|
|
560
|
+
parts.push('<body>');
|
|
561
|
+
parts.push('<section class="' + HELP_CLASS_ROOT + '"' + attr('id', HELP_SHELL_ID) + '>');
|
|
562
|
+
parts.push(renderHero(data, sceneCount));
|
|
563
|
+
if (data.init_banner !== undefined)
|
|
564
|
+
parts.push(renderInitBanner(data.init_banner));
|
|
565
|
+
parts.push(renderTabBar(groups));
|
|
566
|
+
parts.push('<div class="' + cls('pages') + '">');
|
|
567
|
+
groups.forEach((group, index) => {
|
|
568
|
+
parts.push(renderGroupPage(data, group, index, index === 0));
|
|
569
|
+
});
|
|
570
|
+
parts.push(renderAboutPage(data, !hasGroupPage));
|
|
571
|
+
parts.push('</div>');
|
|
572
|
+
const metaBlocks = data.meta_blocks ?? [];
|
|
573
|
+
if (metaBlocks.length > 0) {
|
|
574
|
+
parts.push('<div class="' + cls('meta') + '">' + renderMetaBlocks(metaBlocks) + '</div>');
|
|
575
|
+
}
|
|
576
|
+
parts.push('</section>');
|
|
577
|
+
// 载荷容器(自带;填充器只替换标记文本,不补写标签)+ 共享 helpers 标记(**裸标记**,不得预包裹)。
|
|
578
|
+
parts.push(DATA_SCRIPT_OPEN + TEMPLATE_MARKERS.injectData + DATA_SCRIPT_CLOSE);
|
|
579
|
+
parts.push(TEMPLATE_MARKERS.sharedHelpers);
|
|
580
|
+
parts.push('</body>');
|
|
581
|
+
parts.push('</html>');
|
|
582
|
+
return parts.join(LF);
|
|
583
|
+
}
|
|
584
|
+
/* ── 6. 对外出口 ────────────────────────────────────────────── */
|
|
585
|
+
/** HELP 壳渲染(冻结签名 `(input: HelpShellInput): FillTemplateOutput`)。
|
|
586
|
+
*
|
|
587
|
+
* 次序:**先校验 `sceneData`**(失败抛 `HelpSchemaError`)→ 再拼内置壳模板(或 `template` 覆盖)
|
|
588
|
+
* → **一律走 `fillTemplate`**(共享资产与 JSON 载荷由填充器注入,壳不得自填)。
|
|
589
|
+
*
|
|
590
|
+
* `strict` 原样透传给 `fillTemplate`(契约 §3.5.3「透传」);`template` 覆盖时按调用方模板填充。
|
|
591
|
+
*/
|
|
592
|
+
export function renderHelpShell(input) {
|
|
593
|
+
const source = input ?? {};
|
|
594
|
+
const sceneData = source.sceneData;
|
|
595
|
+
validateSceneData(sceneData);
|
|
596
|
+
const template = typeof source.template === 'string' ? source.template : buildShellTemplate(sceneData);
|
|
597
|
+
return fillTemplate({
|
|
598
|
+
template,
|
|
599
|
+
assets: source.assets,
|
|
600
|
+
data: sceneData,
|
|
601
|
+
strict: source.strict === true,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
//# sourceMappingURL=help.js.map
|