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.
Files changed (81) hide show
  1. package/01-tokens/README.md +1 -1
  2. package/01-tokens/behavior/labels.js +44 -0
  3. package/01-tokens/responsive-demo.html +1 -1
  4. package/01-tokens/tokens.core.css +1 -1
  5. package/01-tokens/tokens.css +2 -2
  6. package/02-primitives/combobox/combobox.js +26 -3
  7. package/02-primitives/date-range/date-range.js +28 -6
  8. package/02-primitives/popover/popover.js +22 -2
  9. package/02-primitives/select/select.js +23 -2
  10. package/02-primitives/timeline/demo.html +1 -1
  11. package/02-primitives/upload/upload.js +50 -17
  12. package/03-patterns/content/content.css +13 -1
  13. package/03-patterns/content/demo.html +1 -1
  14. package/03-patterns/drawer/demo.html +1 -1
  15. package/03-patterns/drawer/drawer.js +44 -6
  16. package/03-patterns/dropdown/dropdown.js +21 -1
  17. package/03-patterns/overlay/overlay.css +1 -1
  18. package/03-patterns/overlay/overlay.js +39 -2
  19. package/03-patterns/pagination/pagination.js +37 -10
  20. package/03-patterns/states/states.css +1 -1
  21. package/03-patterns/tooltip/tooltip.js +17 -2
  22. package/03-patterns/tree/tree.js +21 -1
  23. package/04-recipes/analysis-report/demo.html +1 -1
  24. package/04-recipes/app-shell/README.md +1 -1
  25. package/04-recipes/app-shell/demo.html +1 -1
  26. package/04-recipes/data-showcase/demo.html +2 -2
  27. package/04-recipes/longform/content.html +993 -78
  28. package/04-recipes/longform/longform.html +3 -3
  29. package/04-recipes/report/demo.html +3 -3
  30. package/04-recipes/table/README.md +23 -1
  31. package/04-recipes/table/demo.html +7 -7
  32. package/09-assets/bar/bar.css +1 -1
  33. package/09-assets/bar/bar.js +28 -4
  34. package/09-assets/model-viewer/model-viewer.js +45 -13
  35. package/09-assets/scientific-plot/demo.html +6 -6
  36. package/AGENTS.md +3 -1
  37. package/API.md +32 -0
  38. package/CHANGELOG.md +174 -5
  39. package/CONTRIBUTING.md +44 -3
  40. package/README.md +19 -0
  41. package/README.zh-CN.md +18 -0
  42. package/SECURITY.md +16 -4
  43. package/START-HERE.md +25 -18
  44. package/ai/START-HERE.md +40 -6
  45. package/ai/check-html.js +658 -0
  46. package/ai/cli.js +557 -67
  47. package/ai/components.json +9 -4
  48. package/ai/pdf.js +52 -10
  49. package/ai/recipes.json +86 -0
  50. package/ai/scaffold.js +195 -0
  51. package/ai/shot.js +345 -0
  52. package/ai/validate.js +65 -6
  53. package/dist/01-tokens/behavior/labels.js +11 -0
  54. package/dist/02-primitives/combobox/combobox.js +25 -3
  55. package/dist/02-primitives/date-range/date-range.js +26 -5
  56. package/dist/02-primitives/popover/popover.js +14 -1
  57. package/dist/02-primitives/select/select.js +22 -2
  58. package/dist/02-primitives/upload/upload.js +47 -17
  59. package/dist/03-patterns/content/content.css +1 -1
  60. package/dist/03-patterns/drawer/drawer.js +32 -4
  61. package/dist/03-patterns/dropdown/dropdown.js +20 -1
  62. package/dist/03-patterns/overlay/overlay.js +25 -1
  63. package/dist/03-patterns/pagination/pagination.js +34 -10
  64. package/dist/03-patterns/tooltip/tooltip.js +14 -2
  65. package/dist/03-patterns/tree/tree.js +20 -1
  66. package/dist/09-assets/bar/bar.js +23 -4
  67. package/dist/09-assets/model-viewer/model-viewer.js +38 -12
  68. package/dist/COSTS.md +20 -20
  69. package/docs/ACCESSIBILITY.md +3 -3
  70. package/docs/ARCHITECTURE.md +116 -0
  71. package/docs/L10N.md +92 -0
  72. package/docs/LAYER-BASELINE.md +2 -2
  73. package/docs/PRINT.md +3 -3
  74. package/docs/RELEASE-PROCESS.md +59 -3
  75. package/docs/STABILITY.md +1 -1
  76. package/docs/VERIFY-RELEASE.md +24 -2
  77. package/index.html +3 -3
  78. package/llms-full.txt +18 -4
  79. package/llms.txt +26 -5
  80. package/package.json +4 -2
  81. package/types/index.d.ts +218 -13
package/ai/shot.js ADDED
@@ -0,0 +1,345 @@
1
+ /*
2
+ * shot.js — 把页面截成 PNG(给定宽度、可选整页)
3
+ *
4
+ * ============================================================================
5
+ * 🔴 为什么不用 `chrome --screenshot --window-size=390,844`
6
+ * ----------------------------------------------------------------------------
7
+ * 因为它**会撒谎**。Windows 上窗口有最小宽度,`--window-size=390` 拿到的
8
+ * 实际视口是 **500px**(实测 innerWidth=500),而 `--screenshot` 只按 390 裁一刀。
9
+ * 结果是一张"看起来是 390 宽"的图:文字在右边缘被切掉、媒体查询按 500 生效 ——
10
+ * 你据此判断"窄屏没问题",其实从没看过 390。
11
+ *
12
+ * 这类失败最贵的地方在于**它不报错**:图是好的、能打开、只是内容不对。
13
+ * 所以这里走 Chrome 的调试管道(`--remote-debugging-pipe`,零依赖、不开端口),
14
+ * 用 `Emulation.setDeviceMetricsOverride` 把视口**设成**要的那个数,再截。
15
+ *
16
+ * ============================================================================
17
+ * 为什么是管道而不是 `--remote-debugging-port`
18
+ * ----------------------------------------------------------------------------
19
+ * 端口版要处理:端口占用、防火墙弹窗(Windows 首次监听会弹)、启动竞态
20
+ * (等调试端口文件出现的时机)、以及同一个 profile 被复用时的锁。
21
+ * 管道版把这些全绕开:读写走子进程的 fd 3/4,消息以 `\0` 分隔的 JSON。
22
+ * 代价是自己拼十几行帧处理 —— 比处理上面四件事便宜得多。
23
+ *
24
+ * ============================================================================
25
+ * 它**不是**视觉基线
26
+ * ----------------------------------------------------------------------------
27
+ * 基线(逐字节比对)由 `05-audit/shot-baseline.py` 管,那套要考虑字体、
28
+ * 双平台产物与指纹。这里的产物是给人/给 Agent **看一眼**的:
29
+ * 同一台机器、同样参数,两次逐字节一致(门禁验的就是这条);
30
+ * 换一台机器不保证 —— 字体光栅化不一样,别拿它当跨机器判据。
31
+ */
32
+ 'use strict';
33
+
34
+ var fs = require('fs');
35
+ var os = require('os');
36
+ var path = require('path');
37
+ var { spawn } = require('child_process');
38
+ var { pathToFileURL } = require('url');
39
+ var pdf = require('./pdf.js');
40
+
41
+ var DEFAULT_WIDTHS = [1440];
42
+ var DEFAULT_HEIGHT = 900;
43
+ var DEFAULT_WAIT = 800;
44
+ /* 整页截图的高度上限。PNG 面积是 W×H×scale²,超了内存与耗时都会失控;
45
+ 截断**不静默**:结果里带 truncatedAt,人/Agent 能看见。 */
46
+ var MAX_FULL_HEIGHT = 10000;
47
+
48
+ /** 极简 CDP 客户端:一个子进程、两个 fd、以 \0 分隔的 JSON。 */
49
+ function connect(chromePath, profile) {
50
+ var child = spawn(chromePath, [
51
+ '--headless=new',
52
+ '--disable-gpu',
53
+ '--disable-extensions',
54
+ '--no-first-run',
55
+ '--no-default-browser-check',
56
+ /* 滚动条会占掉 15px 视口,且它本身不是页面内容 */
57
+ '--hide-scrollbars',
58
+ '--remote-debugging-pipe',
59
+ '--user-data-dir=' + profile,
60
+ 'about:blank'
61
+ ], {
62
+ /* fd 0/1/2 给普通输出,3 = 命令(Chrome 读),4 = 事件(Chrome 写) */
63
+ stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'],
64
+ windowsHide: true
65
+ });
66
+
67
+ var buf = Buffer.alloc(0);
68
+ var pending = Object.create(null);
69
+ var waiters = [];
70
+ var listeners = [];
71
+ var nextId = 0;
72
+ var dead = null;
73
+
74
+ function dispatch(msg) {
75
+ if (msg.id !== undefined && pending[msg.id]) {
76
+ var p = pending[msg.id];
77
+ delete pending[msg.id];
78
+ clearTimeout(p.timer);
79
+ if (msg.error) p.reject(new Error(p.method + ':' + (msg.error.message || JSON.stringify(msg.error))));
80
+ else p.resolve(msg.result);
81
+ return;
82
+ }
83
+ if (!msg.method) return;
84
+ for (var L = 0; L < listeners.length; L++) {
85
+ if (listeners[L].method === msg.method) listeners[L].fn(msg.params, msg.sessionId);
86
+ }
87
+ for (var i = waiters.length - 1; i >= 0; i--) {
88
+ var w = waiters[i];
89
+ if (w.method === msg.method && (!w.sessionId || w.sessionId === msg.sessionId)) {
90
+ waiters.splice(i, 1);
91
+ clearTimeout(w.timer);
92
+ w.resolve(msg.params);
93
+ }
94
+ }
95
+ }
96
+
97
+ child.stdio[4].on('data', function (chunk) {
98
+ buf = Buffer.concat([buf, chunk]);
99
+ var i;
100
+ while ((i = buf.indexOf(0)) >= 0) {
101
+ var raw = buf.subarray(0, i).toString('utf8');
102
+ buf = buf.subarray(i + 1);
103
+ var msg = null;
104
+ try { msg = JSON.parse(raw); } catch (e) { continue; }
105
+ dispatch(msg);
106
+ }
107
+ });
108
+ child.stdio[1].on('data', function () { /* 吞掉:Chrome 的正常输出没有信息量 */ });
109
+ child.stdio[2].on('data', function () { /* 同上;错误通过 CDP 的 error 字段回来 */ });
110
+ child.on('error', function (e) { dead = e; });
111
+ child.on('exit', function (code) {
112
+ dead = dead || new Error('Chrome 提前退出(退出码 ' + code + ')');
113
+ Object.keys(pending).forEach(function (k) {
114
+ clearTimeout(pending[k].timer);
115
+ pending[k].reject(dead);
116
+ });
117
+ pending = Object.create(null);
118
+ waiters.splice(0).forEach(function (w) { clearTimeout(w.timer); w.reject(dead); });
119
+ });
120
+
121
+ function send(method, params, sessionId, timeoutMs) {
122
+ return new Promise(function (resolve, reject) {
123
+ if (dead) return reject(dead);
124
+ var id = ++nextId;
125
+ var timer = setTimeout(function () {
126
+ delete pending[id];
127
+ reject(new Error(method + ' 超时(' + (timeoutMs || 15000) + 'ms 没有回应)'));
128
+ }, timeoutMs || 15000);
129
+ pending[id] = { resolve: resolve, reject: reject, timer: timer, method: method };
130
+ child.stdio[3].write(JSON.stringify({
131
+ id: id, method: method, params: params || {}, sessionId: sessionId
132
+ }) + '\0');
133
+ });
134
+ }
135
+
136
+ function waitEvent(method, sessionId, timeoutMs) {
137
+ return new Promise(function (resolve, reject) {
138
+ var timer = setTimeout(function () {
139
+ var at = waiters.findIndex(function (w) { return w.method === method && w.sessionId === sessionId; });
140
+ if (at >= 0) waiters.splice(at, 1);
141
+ reject(new Error('等 ' + method + ' 超时(' + (timeoutMs || 15000) + 'ms)'));
142
+ }, timeoutMs || 15000);
143
+ waiters.push({ method: method, sessionId: sessionId, resolve: resolve, reject: reject, timer: timer });
144
+ });
145
+ }
146
+
147
+ return {
148
+ send: send, waitEvent: waitEvent,
149
+ /* 常驻监听:异常/控制台消息可能在任意时刻到达,用一次性 waiter 会漏掉第二条起的所有 */
150
+ on: function (method, fn) { listeners.push({ method: method, fn: fn }); },
151
+ close: function () { try { child.kill(); } catch (e) { /* 已退出 */ } },
152
+ child: child
153
+ };
154
+ }
155
+
156
+ /** 页面在某个宽度下的量:溢出多少、谁溢出、有多高。 */
157
+ var PROBE_JS = [
158
+ '(function(){',
159
+ ' var d = document.documentElement;',
160
+ ' var vw = d.clientWidth;',
161
+ ' var over = [];',
162
+ ' var all = document.querySelectorAll("body *");',
163
+ ' for (var i = 0; i < all.length && over.length < 40; i++) {',
164
+ ' var e = all[i], r = e.getBoundingClientRect();',
165
+ ' if (r.width === 0 && r.height === 0) continue;',
166
+ ' if (r.right > vw + 1 || r.left < -1) {',
167
+ ' var cls = typeof e.className === "string" ? e.className : "";',
168
+ ' over.push(e.tagName.toLowerCase() + (cls ? "." + cls.trim().split(/\\s+/).join(".") : "")',
169
+ ' + " @" + Math.round(r.right));',
170
+ ' }',
171
+ ' }',
172
+ ' return JSON.stringify({',
173
+ ' viewport: vw, scrollWidth: d.scrollWidth, scrollHeight: d.scrollHeight,',
174
+ ' overflow: over.slice(0, 6), overflowCount: over.length',
175
+ ' });',
176
+ '})()'
177
+ ].join('\n');
178
+
179
+ function slugWidth(w) { return String(w); }
180
+
181
+ /**
182
+ * @param {object} o
183
+ * @param {string} o.src 本地 HTML 路径或 http(s) URL
184
+ * @param {string} o.out 输出文件;多宽度时须含 `{width}`
185
+ * @param {number[]} [o.widths] 视口宽度,默认 [1440]
186
+ * @param {number} [o.height] 视口高度,默认 900
187
+ * @param {boolean} [o.full] 整页(否则只截首屏)
188
+ * @param {number} [o.scale] 设备像素比,默认 1
189
+ * @param {number} [o.wait] 加载后额外等待(毫秒),默认 800
190
+ * @param {string} [o.chrome]
191
+ * @param {number} [o.timeoutMs]
192
+ * @returns {Promise<object>}
193
+ */
194
+ async function shoot(o) {
195
+ o = o || {};
196
+ var t0 = Date.now();
197
+ if (!o.src) return { ok: false, code: 'usage', message: '缺少 src' };
198
+ if (!o.out) return { ok: false, code: 'usage', message: '缺少 out' };
199
+
200
+ var widths = o.widths && o.widths.length ? o.widths : DEFAULT_WIDTHS;
201
+ for (var i = 0; i < widths.length; i++) {
202
+ if (!Number.isInteger(widths[i]) || widths[i] < 200 || widths[i] > 4000) {
203
+ return { ok: false, code: 'usage',
204
+ message: '宽度必须是 200–4000 的整数,收到 ' + JSON.stringify(widths[i]) };
205
+ }
206
+ }
207
+ var height = o.height || DEFAULT_HEIGHT;
208
+ if (!Number.isInteger(height) || height < 200 || height > 4000) {
209
+ return { ok: false, code: 'usage',
210
+ message: '高度必须是 200–4000 的整数,收到 ' + JSON.stringify(o.height) };
211
+ }
212
+ var scale = o.scale === undefined ? 1 : Number(o.scale);
213
+ if (!(scale > 0 && scale <= 3)) {
214
+ return { ok: false, code: 'usage', message: '--scale 必须在 (0, 3],收到 ' + JSON.stringify(o.scale) };
215
+ }
216
+
217
+ var remote = /^https?:\/\//i.test(o.src);
218
+ if (!remote) {
219
+ var srcAbs = path.resolve(o.src);
220
+ if (!fs.existsSync(srcAbs)) return { ok: false, code: 'usage', message: '找不到输入文件:' + srcAbs };
221
+ o.src = srcAbs;
222
+ }
223
+
224
+ var outTpl = String(o.out);
225
+ if (widths.length > 1 && outTpl.indexOf('{width}') < 0) {
226
+ return { ok: false, code: 'usage',
227
+ message: '截多个宽度时 --out 必须含 {width}(例:shot-{width}.png),'
228
+ + '否则 ' + widths.length + ' 张图会写进同一个文件,最后只剩一张' };
229
+ }
230
+
231
+ var chrome = pdf.resolveChrome(o.chrome);
232
+ if (chrome.error) return { ok: false, code: 'no-chrome', message: chrome.error };
233
+
234
+ var profile = fs.mkdtempSync(path.join(os.tmpdir(), 'agentcss-shot-'));
235
+ var cdp = connect(chrome.path, profile);
236
+ var shots = [];
237
+ var consoleErrors = [];
238
+ var budget = o.timeoutMs || 90000;
239
+ var deadline = Date.now() + budget;
240
+
241
+ try {
242
+ var target = await cdp.send('Target.createTarget', { url: 'about:blank' });
243
+ var attached = await cdp.send('Target.attachToTarget', { targetId: target.targetId, flatten: true });
244
+ var sid = attached.sessionId;
245
+
246
+ await cdp.send('Page.enable', {}, sid);
247
+ await cdp.send('Runtime.enable', {}, sid);
248
+ /* JS 异常要收集:截出一张"布局正确但内容没渲染"的图是最坏的产物,
249
+ 而它通常就是一行 TypeError 造成的。 */
250
+ cdp.on('Runtime.exceptionThrown', function (p) {
251
+ if (consoleErrors.length >= 20) return;
252
+ var d = (p && p.exceptionDetails) || {};
253
+ var txt = (d.exception && d.exception.description) || d.text || '未捕获异常';
254
+ consoleErrors.push(String(txt).split('\n')[0]);
255
+ });
256
+ cdp.on('Runtime.consoleAPICalled', function (p) {
257
+ if (consoleErrors.length >= 20) return;
258
+ if (!p || p.type !== 'error') return;
259
+ var parts = (p.args || []).map(function (a) {
260
+ return a.value === undefined ? (a.description || a.type) : String(a.value);
261
+ });
262
+ consoleErrors.push('console.error: ' + parts.join(' '));
263
+ });
264
+
265
+ for (var w = 0; w < widths.length; w++) {
266
+ if (Date.now() > deadline) {
267
+ return { ok: false, code: 'timeout', message: '超出时间预算 ' + budget + 'ms', chrome: chrome.path, shots: shots };
268
+ }
269
+ var width = widths[w];
270
+ var w0 = Date.now();
271
+
272
+ await cdp.send('Emulation.setDeviceMetricsOverride', {
273
+ width: width, height: height, deviceScaleFactor: scale, mobile: false
274
+ }, sid);
275
+
276
+ var loaded = cdp.waitEvent('Page.loadEventFired', sid, 30000);
277
+ var nav = await cdp.send('Page.navigate', { url: remote ? o.src : pathToFileURL(o.src).href }, sid);
278
+ if (nav && nav.errorText) {
279
+ return { ok: false, code: 'render',
280
+ message: '页面加载失败(' + nav.errorText + '):' + o.src, chrome: chrome.path, shots: shots };
281
+ }
282
+ await loaded;
283
+ if (o.wait) await new Promise(function (r) { setTimeout(r, o.wait); });
284
+
285
+ var probe = await cdp.send('Runtime.evaluate', {
286
+ expression: PROBE_JS, returnByValue: true
287
+ }, sid);
288
+ var info = {};
289
+ try { info = JSON.parse(probe.result.value); } catch (e) { info = {}; }
290
+
291
+ var clipH = height;
292
+ var truncated = null;
293
+ if (o.full) {
294
+ var want = info.scrollHeight || height;
295
+ clipH = Math.min(want, MAX_FULL_HEIGHT);
296
+ if (want > MAX_FULL_HEIGHT) truncated = want;
297
+ }
298
+
299
+ var shot = await cdp.send('Page.captureScreenshot', {
300
+ format: 'png',
301
+ captureBeyondViewport: !!o.full,
302
+ clip: { x: 0, y: 0, width: width, height: clipH, scale: 1 }
303
+ }, sid);
304
+ var png = Buffer.from(shot.data, 'base64');
305
+ if (png.length < 8 || png.readUInt32BE(0) !== 0x89504e47) {
306
+ return { ok: false, code: 'render', message: '产出的不是 PNG(' + png.length + ' 字节)',
307
+ chrome: chrome.path, shots: shots };
308
+ }
309
+
310
+ var outFile = path.resolve(outTpl.replace(/\{width\}/g, slugWidth(width)));
311
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
312
+ fs.writeFileSync(outFile, png);
313
+
314
+ shots.push({
315
+ width: width, out: outFile, bytes: png.length, ms: Date.now() - w0,
316
+ png: { width: png.readUInt32BE(16), height: png.readUInt32BE(20) },
317
+ full: !!o.full,
318
+ truncatedAt: truncated,
319
+ viewport: info.viewport === undefined ? null : info.viewport,
320
+ scrollWidth: info.scrollWidth === undefined ? null : info.scrollWidth,
321
+ scrollHeight: info.scrollHeight === undefined ? null : info.scrollHeight,
322
+ overflow: info.overflow || [],
323
+ overflowCount: info.overflowCount || 0
324
+ });
325
+ }
326
+
327
+ return {
328
+ ok: true, code: null, message: null, chrome: chrome.path,
329
+ scale: scale, wait: o.wait === undefined ? DEFAULT_WAIT : o.wait,
330
+ shots: shots, consoleErrors: consoleErrors, ms: Date.now() - t0
331
+ };
332
+ } catch (e) {
333
+ return { ok: false, code: 'render', message: String(e && e.message || e), chrome: chrome.path, shots: shots };
334
+ } finally {
335
+ cdp.close();
336
+ /* 清理必须**在进程退出前**做完:CLI 随后就 exit,挂 setTimeout 等于每截一张图
337
+ 就在系统临时目录里留一个 Chrome profile(pdf.js 那边实测漏过 6 个)。
338
+ 先等一下再删:Windows 上子进程刚 kill 时还握着文件句柄。 */
339
+ await new Promise(function (r) { setTimeout(r, 120); });
340
+ try { fs.rmSync(profile, { recursive: true, force: true, maxRetries: 3 }); }
341
+ catch (e) { /* 偶发 EBUSY,交给系统清理 */ }
342
+ }
343
+ }
344
+
345
+ module.exports = { shoot: shoot, DEFAULT_WIDTHS: DEFAULT_WIDTHS, DEFAULT_HEIGHT: DEFAULT_HEIGHT, DEFAULT_WAIT: DEFAULT_WAIT };
package/ai/validate.js CHANGED
@@ -32,7 +32,11 @@ var path = require('path');
32
32
  var AI_DIR = __dirname;
33
33
 
34
34
  // ---------------------------------------------------------------- 能力白名单
35
- // ⚠️ 改这个表 = 改工具的能力边界。加关键字 ⇒ 必须同时实现它,否则别加。
35
+ // ⚠️ 改这个表 = 改工具的能力边界。
36
+ // 🔴 表里有的关键字**必须真的实现**:白名单只负责"允许出现",
37
+ // 不负责"校验它"。两者不一致 ⇒ schema 里写了、校验器当没看见 ⇒ **静默假绿**。
38
+ // (本轮审计 F4:`uniqueItems` / `not` 在表里挂了很久但没实现,
39
+ // `validate({type:'array',uniqueItems:true},[1,1])` 返回 ok:true。)
36
40
  var SUPPORTED_KEYWORDS = [
37
41
  '$schema', '$id', '$comment', '$ref', 'definitions', 'title',
38
42
  'type', 'required', 'properties', 'additionalProperties', 'patternProperties',
@@ -57,15 +61,30 @@ function isPlainObject(v) {
57
61
  */
58
62
  // 哪些键的"值"本身还是 schema
59
63
  var SUBSCHEMA_KEYS = [
60
- 'items', 'additionalProperties', 'patternProperties', 'allOf', 'anyOf',
64
+ 'items', 'additionalProperties', 'allOf', 'anyOf',
61
65
  'oneOf', 'not', 'propertyNames', 'contains', 'if', 'then', 'else'
62
66
  ];
63
- // 哪些键是"名字 → schema"的容器(它们的**键**是字段名,不是关键字)
64
- var CONTAINER_KEYS = ['properties', 'definitions', '$defs', 'dependentSchemas'];
67
+ /* 哪些键是"名字 → schema"的容器:它们的**键**不是关键字。
68
+ 🔴 `patternProperties` 必须在这里,不能当普通子 schema ——
69
+ 它的键是**正则**(`^--[a-z][a-z0-9-]*$`),
70
+ 当成关键字检查就会报"用了没实现的关键字",而 schema 本身完全合法。
71
+ 这个错 0.26.0 就有,只是预扫当时只覆盖 4 份文件、碰巧都没用 patternProperties
72
+ ⇒ 直到本轮把预扫扩到全部 8 份才暴露(`tokens.audience` 第一个撞上)。 */
73
+ var CONTAINER_KEYS = ['properties', 'definitions', '$defs', 'dependentSchemas',
74
+ 'patternProperties'];
75
+
76
+ /* 扫哪些文件:**目录里现找**,不手写清单。
77
+ 手写清单必漂 —— 0.26.0 时 `ai/` 下有 8 份 schema,这里只列了 4 份
78
+ (behaviors / deprecations / tokensAudience / tokensTree 从来没被扫过),
79
+ 于是那四份里出现未知关键字也不会有人知道。 */
80
+ function schemaFiles() {
81
+ return fs.readdirSync(AI_DIR).filter(function (f) {
82
+ return /\.schema\.json$/.test(f);
83
+ }).sort();
84
+ }
65
85
 
66
86
  function assertSchemasUseOnlySupported() {
67
- var files = ['contract.schema.json', 'components.schema.json',
68
- 'tokens.schema.json', 'patch.schema.json'];
87
+ var files = schemaFiles();
69
88
  var unknown = [];
70
89
 
71
90
  function checkNode(schema, f, ptr) {
@@ -118,6 +137,27 @@ function typeOk(expected, value) {
118
137
  return true;
119
138
  }
120
139
 
140
+ /* JSON Schema 的"相等"是**深比较**:`{a:1}` 与 `{a:1}` 是两个对象但相等。
141
+ 用 `===` 判重会把重复项全放过,而且是静默的。 */
142
+ function deepEqual(a, b) {
143
+ if (a === b) return true;
144
+ if (Array.isArray(a) || Array.isArray(b)) {
145
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
146
+ for (var i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
147
+ return true;
148
+ }
149
+ if (isPlainObject(a) && isPlainObject(b)) {
150
+ var ka = Object.keys(a), kb = Object.keys(b);
151
+ if (ka.length !== kb.length) return false;
152
+ for (var j = 0; j < ka.length; j++) {
153
+ if (!Object.prototype.hasOwnProperty.call(b, ka[j])) return false;
154
+ if (!deepEqual(a[ka[j]], b[ka[j]])) return false;
155
+ }
156
+ return true;
157
+ }
158
+ return false;
159
+ }
160
+
121
161
  function resolveRef(root, ref) {
122
162
  if (ref.charAt(0) !== '#' || ref.indexOf('#/') !== 0) {
123
163
  throw new Error('只支持文档内 $ref:' + ref);
@@ -178,6 +218,18 @@ function validate(schema, data, root, ptr, errors) {
178
218
  if (schema.maxItems !== undefined && data.length > schema.maxItems) {
179
219
  errors.push({ path: ptr, msg: '元素多于 ' + schema.maxItems + ' 个' });
180
220
  }
221
+ /* `uniqueItems: true` = 元素两两不相等。O(n²) 深比较 —— 这里的数据都是
222
+ 几十个以内,可读性比复杂度重要;真要上千个再换哈希。 */
223
+ if (schema.uniqueItems === true) {
224
+ for (var ui = 0; ui < data.length; ui++) {
225
+ for (var uj = ui + 1; uj < data.length; uj++) {
226
+ if (deepEqual(data[ui], data[uj])) {
227
+ errors.push({ path: ptr + '/' + uj,
228
+ msg: '第 ' + uj + ' 个元素与第 ' + ui + ' 个重复(uniqueItems)' });
229
+ }
230
+ }
231
+ }
232
+ }
181
233
  if (schema.items) {
182
234
  data.forEach(function (v, i) {
183
235
  validate(schema.items, v, root, ptr + '/' + i, errors);
@@ -228,6 +280,13 @@ function validate(schema, data, root, ptr, errors) {
228
280
  errors.push({ path: ptr, msg: '必须恰好满足 oneOf 中的一条,实际满足 ' + n + ' 条' });
229
281
  }
230
282
  }
283
+ /* `not`:**必须不**满足里面的约束。子校验用独立的 errors 数组 ——
284
+ 借用外层数组的话,"不满足 not" 会被记成"满足了"的错,方向正好反了。 */
285
+ if (schema.not !== undefined) {
286
+ if (validate(schema.not, data, root, ptr, []).length === 0) {
287
+ errors.push({ path: ptr, msg: '不应满足 not 里的约束(实际满足了)' });
288
+ }
289
+ }
231
290
  return errors;
232
291
  }
233
292
 
@@ -0,0 +1,11 @@
1
+ var flLabels = function (defaults, src) {
2
+ src = src || {};
3
+ var out = {};
4
+ var k;
5
+ for (k in defaults) {
6
+ if (Object.prototype.hasOwnProperty.call(defaults, k)) {
7
+ out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
8
+ }
9
+ }
10
+ return out;
11
+ };
@@ -119,6 +119,28 @@ return true;
119
119
  var uid = 0;
120
120
 
121
121
  function create(root, opt) {
122
+
123
+ var DEFAULT_LABELS = {
124
+ suggest: '建议',
125
+ input: '输入',
126
+ noMatch: function (q) { return '没有匹配「' + q + '」的选项'; },
127
+ empty: '暂无可选'
128
+ };
129
+
130
+ var flLabels = function (defaults, src) {
131
+ src = src || {};
132
+ var out = {};
133
+ var k;
134
+ for (k in defaults) {
135
+ if (Object.prototype.hasOwnProperty.call(defaults, k)) {
136
+ out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
137
+ }
138
+ }
139
+ return out;
140
+ };
141
+
142
+ var L = flLabels(DEFAULT_LABELS, opt && opt.labels);
143
+
122
144
  opt = opt || {};
123
145
  var input = root.querySelector('[data-combo-input]');
124
146
  var list = root.querySelector('[data-combo-list]');
@@ -147,14 +169,14 @@ var activeIdx = -1;
147
169
  var shown = [];
148
170
 
149
171
  list.setAttribute('role', 'listbox');
150
- list.setAttribute('aria-label', opt.label || '建议');
172
+ list.setAttribute('aria-label', opt.label || L.suggest);
151
173
  input.setAttribute('role', 'combobox');
152
174
  input.setAttribute('aria-autocomplete', 'list');
153
175
  input.setAttribute('aria-expanded', 'false');
154
176
  input.setAttribute('aria-controls', list.id);
155
177
  if (!input.id) input.id = 'combo-input-' + seq;
156
178
  if (!input.getAttribute('aria-label') && !root.querySelector('label[for="' + input.id + '"]')) {
157
- input.setAttribute('aria-label', opt.label || '输入');
179
+ input.setAttribute('aria-label', opt.label || L.input);
158
180
  }
159
181
 
160
182
  function esc(s) {
@@ -190,7 +212,7 @@ return String(s).replace(/[&<>"]/g, function (c) {
190
212
  if (!shown.length) {
191
213
  /* @html-sink escaped — 插值只有 q,且过了 esc() */
192
214
  list.innerHTML = '<li class="combo__empty" role="presentation">' +
193
- (q ? '没有匹配「' + esc(q) + '」的选项' : '暂无可选') + '</li>';
215
+ (q ? L.noMatch(esc(q)) : L.empty) + '</li>';
194
216
  return;
195
217
  }
196
218
  var ql = q.toLowerCase();
@@ -94,6 +94,27 @@ return { from: iso(start), to: iso(end) };
94
94
  }
95
95
 
96
96
  function create(root, opt) {
97
+
98
+ var flLabels = function (defaults, src) {
99
+ src = src || {};
100
+ var out = {};
101
+ var k;
102
+ for (k in defaults) {
103
+ if (Object.prototype.hasOwnProperty.call(defaults, k)) {
104
+ out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
105
+ }
106
+ }
107
+ return out;
108
+ };
109
+
110
+ var DEFAULT_LABELS = {
111
+ requiredEnd: '请填写结束日期。',
112
+ requiredStart: '请填写开始日期。',
113
+ order: function (f, t) { return '开始日期不能晚于结束日期(当前:' + f + ' → ' + t + ')。'; },
114
+ min: function (min) { return '开始日期不能早于 ' + min + '。'; },
115
+ max: function (max) { return '结束日期不能晚于 ' + max + '。'; }
116
+ };
117
+ var L = flLabels(DEFAULT_LABELS, opt && opt.labels);
97
118
  opt = opt || {};
98
119
  var from = root.querySelector('[data-dr-from]');
99
120
  var to = root.querySelector('[data-dr-to]');
@@ -132,14 +153,14 @@ return Math.round((b - a) / 86400000) + 1;
132
153
  function validate() {
133
154
  var f = from.value, t = to.value;
134
155
  if (!f && !t) { setError(''); paintSummary(''); return true; }
135
- if (f && !t) { setError('请填写结束日期。'); return false; }
136
- if (!f && t) { setError('请填写开始日期。'); return false; }
156
+ if (f && !t) { setError(L.requiredEnd); return false; }
157
+ if (!f && t) { setError(L.requiredStart); return false; }
137
158
  if (f > t) {
138
- setError('开始日期不能晚于结束日期(当前:' + f + ' → ' + t + ')。');
159
+ setError(L.order(f, t));
139
160
  return false;
140
161
  }
141
- if (opt.min && f < opt.min) { setError('开始日期不能早于 ' + opt.min + '。'); return false; }
142
- if (opt.max && t > opt.max) { setError('结束日期不能晚于 ' + opt.max + '。'); return false; }
162
+ if (opt.min && f < opt.min) { setError(L.min(opt.min)); return false; }
163
+ if (opt.max && t > opt.max) { setError(L.max(opt.max)); return false; }
143
164
  setError('');
144
165
  paintSummary(f + ' → ' + t + '(' + days(f, t) + ' 天)');
145
166
  return true;
@@ -148,10 +148,11 @@ box.setAttribute('inert', '');
148
148
 
149
149
  box.setAttribute('role', 'dialog');
150
150
  box.setAttribute('aria-label', opts.label || '');
151
+ var bodyHtml = sanitizeHtml(opts.html || '', opts.sanitize);
151
152
 
152
153
  box.innerHTML =
153
154
  '<span class="popover__arrow" aria-hidden="true"></span>'+
154
- '<div class="popover__body">'+ (opts.html || '') + '</div>';
155
+ '<div class="popover__body">'+ bodyHtml + '</div>';
155
156
 
156
157
  positionArrow(box, trigger);
157
158
  anchor.appendChild(box);
@@ -230,6 +231,17 @@ el: box,
230
231
  return api;
231
232
  }
232
233
 
234
+ var SANITIZER = null;
235
+
236
+ function setSanitizer(fn) {
237
+ SANITIZER = (typeof fn === 'function') ? fn : null;
238
+ }
239
+
240
+ function sanitizeHtml(html, perCall) {
241
+ var fn = (typeof perCall === 'function') ? perCall : SANITIZER;
242
+ return fn ? String(fn(String(html))) : String(html);
243
+ }
244
+
233
245
  function positionArrow(box, trigger) {
234
246
  var arrow = box.querySelector('.popover__arrow');
235
247
  if (!arrow) return;
@@ -256,5 +268,6 @@ closeAll: function () { if (current) current.close(); },
256
268
  get current() { return current; },
257
269
  };
258
270
 
271
+ Popover.setSanitizer = setSanitizer;
259
272
  global.Popover = Popover;
260
273
  })(typeof window !== 'undefined' ? window : globalThis);
@@ -213,6 +213,26 @@ return true;
213
213
  var uid = 0;
214
214
 
215
215
  function create(root, opt) {
216
+
217
+ var DEFAULT_LABELS = {
218
+ option: '选项',
219
+ placeholder: '请选择'
220
+ };
221
+
222
+ var flLabels = function (defaults, src) {
223
+ src = src || {};
224
+ var out = {};
225
+ var k;
226
+ for (k in defaults) {
227
+ if (Object.prototype.hasOwnProperty.call(defaults, k)) {
228
+ out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
229
+ }
230
+ }
231
+ return out;
232
+ };
233
+
234
+ var L = flLabels(DEFAULT_LABELS, opt && opt.labels);
235
+
216
236
  opt = opt || {};
217
237
  var btn = root.querySelector('[data-select-btn]');
218
238
  var list = root.querySelector('[data-select-list]');
@@ -253,7 +273,7 @@ seed(opt.value);
253
273
 
254
274
  list.setAttribute('role', 'listbox');
255
275
  if (isMulti) list.setAttribute('aria-multiselectable', 'true');
256
- list.setAttribute('aria-label', opt.label || '选项');
276
+ list.setAttribute('aria-label', opt.label || L.option);
257
277
  btn.setAttribute('aria-haspopup', 'listbox');
258
278
  btn.setAttribute('aria-expanded', 'false');
259
279
  btn.setAttribute('aria-controls', list.id);
@@ -293,7 +313,7 @@ var texts = [];
293
313
  for (var i = 0; i < options.length; i++) {
294
314
  if (hasValue(valueOfOpt(options[i]))) texts.push(optText(options[i]));
295
315
  }
296
- return texts.length ? texts.join('、') : (opt.placeholder || '请选择');
316
+ return texts.length ? texts.join('、') : (opt.placeholder || L.placeholder);
297
317
  }
298
318
 
299
319
  function optText(o) {