@capybara-hq/plugin-sdk 0.1.0-rc.6 → 0.1.0-rc.8
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/agent-docs/connector-templates.md +1 -0
- package/agent-docs/plugin-format.md +17 -2
- package/dist/cards.d.ts +7 -0
- package/dist/cards.js +4 -4
- package/dist/channel.d.ts +20 -1
- package/dist/connector-auth.d.ts +2 -0
- package/dist/connector-auth.js +15 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- package/dist/marketplace.d.ts +26 -0
- package/dist/marketplace.js +43 -1
- package/dist/plugin-manifest.d.ts +22 -3
- package/dist/plugin-manifest.js +85 -5
- package/dist/setup-guide.d.ts +94 -0
- package/dist/setup-guide.js +135 -0
- package/dist/theme-assets.d.ts +39 -0
- package/dist/theme-assets.js +449 -0
- package/dist/theme-readability.d.ts +37 -0
- package/dist/theme-readability.js +145 -0
- package/dist/theme-tokens.generated.d.ts +275 -0
- package/dist/theme-tokens.generated.js +167 -0
- package/dist/theme.d.ts +152 -0
- package/dist/theme.js +392 -0
- package/package.json +2 -1
- package/schemas/connector-template.v1.schema.json +3 -0
- package/schemas/marketplace.v1.schema.json +14 -0
- package/schemas/plugin.v1.schema.json +155 -0
- package/schemas/theme.v1.schema.json +225 -0
package/dist/theme.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CATALOG_ITEM_NAME_RE, isSafePackagePath } from "./plugin-package.js";
|
|
3
|
+
import { checkThemeReadability } from "./theme-readability.js";
|
|
4
|
+
import { THEME_OVERRIDABLE, THEME_TOKENS } from "./theme-tokens.generated.js";
|
|
5
|
+
/**
|
|
6
|
+
* A theme file `themes/<id>.json` (themes spec §4): a partial override of the design tokens per
|
|
7
|
+
* scheme, optional fonts and an optional logo. The id is the file's base name
|
|
8
|
+
* ({@link themeIdOf}, `CATALOG_ITEM_NAME_RE`).
|
|
9
|
+
*
|
|
10
|
+
* {@link parseThemeFile} is the CSS-injection boundary: every token value is parsed against the
|
|
11
|
+
* grammar `tokens.json` uses (`#RRGGBB`, `rgba(r,g,b,a)`, shadows `<x> <y> <blur> [<spread>]
|
|
12
|
+
* <color>`) and re-rendered from its parts in canonical form. Asset paths (`fonts[].file`,
|
|
13
|
+
* `logo.*`) are relative to the plugin root (an optional leading `./`) and every segment is limited
|
|
14
|
+
* to `[A-Za-z0-9._-]`, so they need no escaping inside `url(...)`. Font families are limited to
|
|
15
|
+
* letters, digits, spaces and `-`. Free text (`name`, `description`, `logo.wordmark`) carries no
|
|
16
|
+
* control, format, line / paragraph separator or lone surrogate characters; it is text for the UI,
|
|
17
|
+
* never CSS. Whether the files exist and their size / signature / SVG rules are checked by
|
|
18
|
+
* the reader of the package, with {@link THEME_LIMITS}.
|
|
19
|
+
*
|
|
20
|
+
* The committed JSON Schema (`schemas/theme.v1.schema.json`) is generated from
|
|
21
|
+
* {@link ThemeFileSchema} (the shape only) by `pnpm gen:package-schemas`; token names, value
|
|
22
|
+
* grammar, path safety and readability are decided only by {@link parseThemeFile}.
|
|
23
|
+
*/
|
|
24
|
+
/** Limits of theme files and their assets. */
|
|
25
|
+
export const THEME_LIMITS = {
|
|
26
|
+
/** UTF-8 bytes of one theme file. */
|
|
27
|
+
fileBytes: 64 * 1024,
|
|
28
|
+
/** Themes per plugin. */
|
|
29
|
+
perPlugin: 10,
|
|
30
|
+
/** Total bytes of a plugin's theme assets (fonts and logos). */
|
|
31
|
+
assetsBytes: 16 * 1024 * 1024,
|
|
32
|
+
/** Fonts per theme. */
|
|
33
|
+
fonts: 8,
|
|
34
|
+
/** Bytes of one woff2 font. */
|
|
35
|
+
fontBytes: 2 * 1024 * 1024,
|
|
36
|
+
/** Bytes of a logo SVG. */
|
|
37
|
+
svgBytes: 128 * 1024,
|
|
38
|
+
/** Bytes of a logo PNG. */
|
|
39
|
+
pngBytes: 256 * 1024,
|
|
40
|
+
/** Minimum pixel height of a logo PNG. */
|
|
41
|
+
pngMinHeight: 64,
|
|
42
|
+
/** Characters of the wordmark. */
|
|
43
|
+
wordmark: 24,
|
|
44
|
+
};
|
|
45
|
+
/** `themes/night-city.json` → `night-city`. */
|
|
46
|
+
export function themeIdOf(path) {
|
|
47
|
+
return (path.split("/").pop() ?? "").replace(/\.json$/, "");
|
|
48
|
+
}
|
|
49
|
+
const MAX_ERRORS = 50;
|
|
50
|
+
const FAMILY_RE = /^[A-Za-z0-9 -]{1,64}$/;
|
|
51
|
+
/** Control, format, line / paragraph separator and lone surrogate characters. */
|
|
52
|
+
const FORBIDDEN_CHARS_RE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Cs}]/u;
|
|
53
|
+
const PATH_SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
|
|
54
|
+
const FORBIDDEN_CHARS_MESSAGE = "must not contain control, format, line / paragraph separator or lone surrogate characters";
|
|
55
|
+
const IMAGE_RE = /\.(?:svg|png)$/i;
|
|
56
|
+
const MAX_VALUE_CHARS = 256;
|
|
57
|
+
const text = (max, what) => z
|
|
58
|
+
.string()
|
|
59
|
+
.min(1)
|
|
60
|
+
.max(max)
|
|
61
|
+
.regex(/\S/, { message: `${what} must not be blank` })
|
|
62
|
+
.refine((t) => !FORBIDDEN_CHARS_RE.test(t), { message: `${what} ${FORBIDDEN_CHARS_MESSAGE}` });
|
|
63
|
+
/** A plugin-root relative path (optional `./`), each segment `[A-Za-z0-9._-]`, no `.` / `..`. */
|
|
64
|
+
const isPlainAssetPath = (p) => {
|
|
65
|
+
const rest = p.replace(/^\.\//, "");
|
|
66
|
+
return (isSafePackagePath(rest) &&
|
|
67
|
+
rest.split("/").every((seg) => PATH_SEGMENT_RE.test(seg) && seg !== "." && seg !== ".."));
|
|
68
|
+
};
|
|
69
|
+
const assetPath = (what, ext, kind) => z
|
|
70
|
+
.string()
|
|
71
|
+
.max(512)
|
|
72
|
+
.refine((p) => isPlainAssetPath(p) && ext.test(p), {
|
|
73
|
+
message: `${what} must be a relative ${kind} path inside the plugin, each segment of letters, digits, ".", "_" or "-"`,
|
|
74
|
+
});
|
|
75
|
+
const tokenValues = z.record(z.string().max(64), z.string().max(MAX_VALUE_CHARS));
|
|
76
|
+
const radius = z.number().int().min(0).max(16);
|
|
77
|
+
const schemeOverride = z.strictObject({
|
|
78
|
+
color: tokenValues.optional(),
|
|
79
|
+
shadow: tokenValues.optional(),
|
|
80
|
+
radius: z
|
|
81
|
+
.strictObject({ sm: radius.optional(), md: radius.optional(), bubble: radius.optional() })
|
|
82
|
+
.optional(),
|
|
83
|
+
});
|
|
84
|
+
const shape = z.strictObject({
|
|
85
|
+
$schema: z.string().max(512).optional(),
|
|
86
|
+
name: z.strictObject({ zh: text(100, "name.zh"), en: text(100, "name.en") }),
|
|
87
|
+
description: z
|
|
88
|
+
.strictObject({
|
|
89
|
+
en: text(1024, "description.en"),
|
|
90
|
+
zh: text(1024, "description.zh").optional(),
|
|
91
|
+
})
|
|
92
|
+
.optional(),
|
|
93
|
+
schemes: z
|
|
94
|
+
.strictObject({ dark: schemeOverride.optional(), light: schemeOverride.optional() })
|
|
95
|
+
.refine((s) => s.dark !== undefined || s.light !== undefined, {
|
|
96
|
+
message: "schemes must define dark, light or both",
|
|
97
|
+
}),
|
|
98
|
+
fonts: z
|
|
99
|
+
.array(z.strictObject({
|
|
100
|
+
family: z.string().regex(FAMILY_RE, {
|
|
101
|
+
message: "family must be 1-64 letters, digits, spaces or '-'",
|
|
102
|
+
}),
|
|
103
|
+
file: assetPath("file", /\.woff2$/i, ".woff2"),
|
|
104
|
+
weight: z.number().int().min(100).max(900),
|
|
105
|
+
style: z.enum(["normal", "italic"]).optional(),
|
|
106
|
+
role: z.enum(["prose", "mono"]),
|
|
107
|
+
}))
|
|
108
|
+
.max(THEME_LIMITS.fonts)
|
|
109
|
+
.optional(),
|
|
110
|
+
logo: z
|
|
111
|
+
.strictObject({
|
|
112
|
+
mark: assetPath("mark", IMAGE_RE, ".svg or .png").optional(),
|
|
113
|
+
wordmark: z
|
|
114
|
+
.string()
|
|
115
|
+
.regex(/\S/, { message: "wordmark must not be blank" })
|
|
116
|
+
.refine((w) => [...w].length <= THEME_LIMITS.wordmark, {
|
|
117
|
+
message: `wordmark must be at most ${THEME_LIMITS.wordmark} characters`,
|
|
118
|
+
})
|
|
119
|
+
.refine((w) => !FORBIDDEN_CHARS_RE.test(w), {
|
|
120
|
+
message: `wordmark ${FORBIDDEN_CHARS_MESSAGE}`,
|
|
121
|
+
})
|
|
122
|
+
.optional(),
|
|
123
|
+
favicon: assetPath("favicon", IMAGE_RE, ".svg or .png").optional(),
|
|
124
|
+
})
|
|
125
|
+
.optional(),
|
|
126
|
+
});
|
|
127
|
+
/**
|
|
128
|
+
* The zod schema of a theme file's shape (strict objects). Token names and values are only
|
|
129
|
+
* strings here; see {@link parseThemeFile}. The JSON Schema is generated with
|
|
130
|
+
* `z.toJSONSchema(ThemeFileSchema, { io: "input" })`.
|
|
131
|
+
*/
|
|
132
|
+
export const ThemeFileSchema = shape;
|
|
133
|
+
const HEX_RE = /^#([0-9A-Fa-f]{6})$/;
|
|
134
|
+
const RGBA_RE = /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(0|1|0?\.\d{1,4})\s*\)$/;
|
|
135
|
+
const LENGTH_RE = /^(-?)(\d{1,3})(px)?$/;
|
|
136
|
+
/** `#RRGGBB` uppercase or `rgba(r,g,b,a)` rendered from its parts; undefined outside the grammar. */
|
|
137
|
+
function parseColor(v) {
|
|
138
|
+
const hex = HEX_RE.exec(v);
|
|
139
|
+
if (hex)
|
|
140
|
+
return `#${hex[1].toUpperCase()}`;
|
|
141
|
+
const m = RGBA_RE.exec(v);
|
|
142
|
+
if (!m)
|
|
143
|
+
return undefined;
|
|
144
|
+
const ch = [m[1], m[2], m[3]].map(Number);
|
|
145
|
+
if (ch.some((c) => c > 255))
|
|
146
|
+
return undefined;
|
|
147
|
+
return `rgba(${ch.join(",")},${String(Number(m[4]))})`;
|
|
148
|
+
}
|
|
149
|
+
/** A shadow length: `0` or `<n>px` (unitless only for 0), rendered as `0` / `<n>px`. */
|
|
150
|
+
function parseLength(v, allowNegative) {
|
|
151
|
+
const m = LENGTH_RE.exec(v);
|
|
152
|
+
if (!m)
|
|
153
|
+
return undefined;
|
|
154
|
+
const n = Number(m[2]);
|
|
155
|
+
if (n !== 0 && m[3] === undefined)
|
|
156
|
+
return undefined;
|
|
157
|
+
if (n === 0)
|
|
158
|
+
return "0";
|
|
159
|
+
if (m[1] === "-" && !allowNegative)
|
|
160
|
+
return undefined;
|
|
161
|
+
return `${m[1]}${n}px`;
|
|
162
|
+
}
|
|
163
|
+
/** `<x> <y> <blur> [<spread>] <color>`, rendered canonically; undefined outside the grammar. */
|
|
164
|
+
function parseShadow(v) {
|
|
165
|
+
const s = v.trim();
|
|
166
|
+
const at = s.endsWith(")") ? s.lastIndexOf("rgba(") : s.lastIndexOf(" ") + 1;
|
|
167
|
+
if (at <= 0 || !/\s/.test(s.charAt(at - 1)))
|
|
168
|
+
return undefined;
|
|
169
|
+
const color = parseColor(s.slice(at));
|
|
170
|
+
const lengths = s.slice(0, at).trim().split(/\s+/);
|
|
171
|
+
if (color === undefined || lengths.length < 3 || lengths.length > 4)
|
|
172
|
+
return undefined;
|
|
173
|
+
// x, y and spread may be negative; blur may not.
|
|
174
|
+
const parts = lengths.map((l, i) => parseLength(l, i !== 2));
|
|
175
|
+
if (parts.some((p) => p === undefined))
|
|
176
|
+
return undefined;
|
|
177
|
+
return `${parts.join(" ")} ${color}`;
|
|
178
|
+
}
|
|
179
|
+
const SCHEMES = ["dark", "light"];
|
|
180
|
+
const OVERRIDABLE = {
|
|
181
|
+
color: new Set(THEME_OVERRIDABLE.color),
|
|
182
|
+
shadow: new Set(THEME_OVERRIDABLE.shadow),
|
|
183
|
+
};
|
|
184
|
+
const GRAMMAR = {
|
|
185
|
+
color: { parse: parseColor, form: "#RRGGBB or rgba(r,g,b,a)" },
|
|
186
|
+
shadow: {
|
|
187
|
+
parse: parseShadow,
|
|
188
|
+
form: "<x> <y> <blur> [<spread>] <color> with px lengths and a #RRGGBB or rgba(r,g,b,a) color",
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* The canonical form of a token value under the grammar of `group` (the one {@link parseThemeFile}
|
|
193
|
+
* applies), or undefined when `value` is not a string within it. For readers of stored themes that
|
|
194
|
+
* emit values again (the kernel's stylesheet, themes spec §8): a stored value is re-validated
|
|
195
|
+
* before it reaches CSS, so a changed database row cannot inject CSS.
|
|
196
|
+
*/
|
|
197
|
+
export function canonicalThemeValue(group, value) {
|
|
198
|
+
if (typeof value !== "string" || value.length > MAX_VALUE_CHARS)
|
|
199
|
+
return undefined;
|
|
200
|
+
return GRAMMAR[group].parse(value);
|
|
201
|
+
}
|
|
202
|
+
/** Whether `v` is a font family a theme may name: 1-64 letters, digits, spaces or `-`. */
|
|
203
|
+
export function isThemeFontFamily(v) {
|
|
204
|
+
return typeof v === "string" && FAMILY_RE.test(v);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Whether `v` is an asset path a theme may name: plugin-root relative (an optional leading `./`),
|
|
208
|
+
* every segment of `[A-Za-z0-9._-]`, no `.` / `..` segments (the extension is not checked).
|
|
209
|
+
*/
|
|
210
|
+
export function isThemeAssetPath(v) {
|
|
211
|
+
return typeof v === "string" && v.length <= 512 && isPlainAssetPath(v);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Each scheme the theme defines, merged onto the current built-in values (for swatches, the
|
|
215
|
+
* readability check and anything else that needs full values). A stored theme is read leniently:
|
|
216
|
+
* an override whose token name the built-in tokens no longer know (renamed in core), that is not
|
|
217
|
+
* overridable, or whose value falls outside the grammar is skipped, as the stylesheet skips it.
|
|
218
|
+
*/
|
|
219
|
+
export function mergeThemeSchemes(theme) {
|
|
220
|
+
const out = {};
|
|
221
|
+
const schemes = theme?.schemes;
|
|
222
|
+
if (schemes === null || typeof schemes !== "object")
|
|
223
|
+
return out;
|
|
224
|
+
for (const scheme of SCHEMES) {
|
|
225
|
+
const over = schemes[scheme];
|
|
226
|
+
if (over === null || typeof over !== "object")
|
|
227
|
+
continue;
|
|
228
|
+
const m = builtIn(scheme);
|
|
229
|
+
for (const group of ["color", "shadow"]) {
|
|
230
|
+
const values = over[group];
|
|
231
|
+
if (values === null || typeof values !== "object")
|
|
232
|
+
continue;
|
|
233
|
+
for (const [name, raw] of Object.entries(values)) {
|
|
234
|
+
if (!OVERRIDABLE[group].has(name))
|
|
235
|
+
continue;
|
|
236
|
+
const v = canonicalThemeValue(group, raw);
|
|
237
|
+
if (v !== undefined)
|
|
238
|
+
m[group][name] = v;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const radii = over.radius;
|
|
242
|
+
if (radii !== null && typeof radii === "object") {
|
|
243
|
+
for (const key of ["sm", "md", "bubble"]) {
|
|
244
|
+
const v = radii[key];
|
|
245
|
+
if (Number.isInteger(v) && v >= 0 && v <= 16) {
|
|
246
|
+
m.radius[key] = v;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
out[scheme] = m;
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
/** The built-in values of one scheme. */
|
|
255
|
+
function builtIn(scheme) {
|
|
256
|
+
const pick = (group) => Object.fromEntries(Object.entries(group).map(([n, v]) => [n, v[scheme]]));
|
|
257
|
+
return {
|
|
258
|
+
color: pick(THEME_TOKENS.color),
|
|
259
|
+
shadow: pick(THEME_TOKENS.shadow),
|
|
260
|
+
radius: { ...THEME_TOKENS.radius },
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function readJson(json) {
|
|
264
|
+
let text;
|
|
265
|
+
if (typeof json === "string") {
|
|
266
|
+
text = json;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// An already parsed value is measured by its JSON serialization.
|
|
270
|
+
try {
|
|
271
|
+
text = JSON.stringify(json) ?? "";
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return { errors: ["theme file must be a JSON object"] };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (new TextEncoder().encode(text).length > THEME_LIMITS.fileBytes) {
|
|
278
|
+
return { errors: [`theme file is larger than ${THEME_LIMITS.fileBytes} bytes`] };
|
|
279
|
+
}
|
|
280
|
+
if (typeof json !== "string")
|
|
281
|
+
return { value: json };
|
|
282
|
+
try {
|
|
283
|
+
return { value: JSON.parse(json.startsWith("\uFEFF") ? json.slice(1) : json) };
|
|
284
|
+
}
|
|
285
|
+
catch (e) {
|
|
286
|
+
const first = (e instanceof Error ? e.message : String(e)).split("\n")[0];
|
|
287
|
+
return { errors: [`theme file is not valid JSON: ${first}`] };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Parses and validates a theme file: the parsed JSON value or the file text (larger than
|
|
292
|
+
* {@link THEME_LIMITS}`.fileBytes`, measured as text or as the value's JSON, is refused). Checks the id, the shape, token names against
|
|
293
|
+
* the generated allowlist (an unknown or non-overridable name is an error), the value grammar,
|
|
294
|
+
* fonts and logo; then (unless `options.readability` is false) merges every defined scheme onto
|
|
295
|
+
* the built-in tokens ({@link mergeThemeSchemes}) and reports each readability issue as an error
|
|
296
|
+
* starting `unreadable:`. Returned values are canonical (uppercase
|
|
297
|
+
* hex, rgba and shadows re-rendered from their parts). Invalid input returns `{ errors }`
|
|
298
|
+
* (`<path>: <reason>`, English, at most 50); never throws.
|
|
299
|
+
*/
|
|
300
|
+
export function parseThemeFile(id, json, options = {}) {
|
|
301
|
+
if (!CATALOG_ITEM_NAME_RE.test(id)) {
|
|
302
|
+
return { errors: [`theme id "${id}" must match ^[a-z0-9][a-z0-9-]{0,63}$`] };
|
|
303
|
+
}
|
|
304
|
+
const read = readJson(json);
|
|
305
|
+
if ("errors" in read)
|
|
306
|
+
return read;
|
|
307
|
+
const data = read.value;
|
|
308
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
309
|
+
return { errors: ["theme file must be a JSON object"] };
|
|
310
|
+
}
|
|
311
|
+
const r = shape.safeParse(data);
|
|
312
|
+
if (!r.success) {
|
|
313
|
+
return {
|
|
314
|
+
errors: r.error.issues.slice(0, MAX_ERRORS).map((i) => {
|
|
315
|
+
const path = i.path.map(String).join(".");
|
|
316
|
+
return path ? `${path}: ${i.message}` : i.message;
|
|
317
|
+
}),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const file = r.data;
|
|
321
|
+
const errors = [];
|
|
322
|
+
const seenFonts = new Set();
|
|
323
|
+
file.fonts?.forEach((f, i) => {
|
|
324
|
+
const key = `${f.family.toLowerCase()}|${f.weight}|${f.style ?? "normal"}|${f.role}`;
|
|
325
|
+
if (seenFonts.has(key)) {
|
|
326
|
+
errors.push(`fonts.${i}: duplicate font ${f.family} ${f.weight} ${f.style ?? "normal"} (${f.role})`);
|
|
327
|
+
}
|
|
328
|
+
seenFonts.add(key);
|
|
329
|
+
});
|
|
330
|
+
const schemes = {};
|
|
331
|
+
for (const scheme of SCHEMES) {
|
|
332
|
+
const over = file.schemes[scheme];
|
|
333
|
+
if (over === undefined)
|
|
334
|
+
continue;
|
|
335
|
+
const out = {};
|
|
336
|
+
// Token names come from the raw object's own keys: the zod record drops `__proto__`.
|
|
337
|
+
const rawScheme = data.schemes[scheme];
|
|
338
|
+
for (const group of ["color", "shadow"]) {
|
|
339
|
+
const values = over[group];
|
|
340
|
+
if (values === undefined)
|
|
341
|
+
continue;
|
|
342
|
+
const rawGroup = rawScheme?.[group];
|
|
343
|
+
const names = rawGroup !== null && typeof rawGroup === "object" ? Object.keys(rawGroup) : [];
|
|
344
|
+
const canonical = {};
|
|
345
|
+
for (const name of names) {
|
|
346
|
+
const path = `schemes.${scheme}.${group}.${name}`;
|
|
347
|
+
if (!OVERRIDABLE[group].has(name)) {
|
|
348
|
+
errors.push(Object.hasOwn(THEME_TOKENS[group], name)
|
|
349
|
+
? `${path}: token ${group}.${name} is not overridable`
|
|
350
|
+
: `${path}: unknown token ${group}.${name}`);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const raw = values[name];
|
|
354
|
+
const v = raw === undefined ? undefined : GRAMMAR[group].parse(raw);
|
|
355
|
+
if (v === undefined) {
|
|
356
|
+
errors.push(`${path}: must be ${GRAMMAR[group].form}`);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
canonical[name] = v;
|
|
360
|
+
}
|
|
361
|
+
out[group] = canonical;
|
|
362
|
+
}
|
|
363
|
+
if (over.radius !== undefined)
|
|
364
|
+
out.radius = { ...over.radius };
|
|
365
|
+
schemes[scheme] = out;
|
|
366
|
+
}
|
|
367
|
+
if (errors.length > 0)
|
|
368
|
+
return { errors: errors.slice(0, MAX_ERRORS) };
|
|
369
|
+
const merged = options.readability === false ? {} : mergeThemeSchemes({ schemes });
|
|
370
|
+
for (const scheme of SCHEMES) {
|
|
371
|
+
const m = merged[scheme];
|
|
372
|
+
if (m === undefined)
|
|
373
|
+
continue;
|
|
374
|
+
for (const issue of checkThemeReadability(scheme, m)) {
|
|
375
|
+
errors.push(issue.kind === "unevaluable"
|
|
376
|
+
? `unreadable: schemes.${scheme}: ${issue.pair} has a value the check cannot evaluate`
|
|
377
|
+
: issue.kind === "separation-order"
|
|
378
|
+
? `unreadable: schemes.${scheme}: ${issue.pair} must stand further from the surface than hover (${issue.ratio.toFixed(1)} ≤ ${issue.floor.toFixed(1)})`
|
|
379
|
+
: `unreadable: schemes.${scheme}: ${issue.pair} ${issue.kind} ${issue.ratio.toFixed(2)} is below ${issue.floor}${issue.detail === undefined ? "" : ` (${issue.detail})`}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (errors.length > 0)
|
|
383
|
+
return { errors: errors.slice(0, MAX_ERRORS) };
|
|
384
|
+
const defined = SCHEMES.filter((s) => schemes[s] !== undefined);
|
|
385
|
+
const { $schema: _schema, ...rest } = file;
|
|
386
|
+
return {
|
|
387
|
+
...rest,
|
|
388
|
+
schemes,
|
|
389
|
+
id,
|
|
390
|
+
singleScheme: defined.length === 1 ? defined[0] : null,
|
|
391
|
+
};
|
|
392
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capybara-hq/plugin-sdk",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.8",
|
|
4
4
|
"description": "Capybara plugin SDK: the types and pure functions plugins are written against",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"kysely": "^0.29.6",
|
|
32
|
+
"saxes": "6.0.0",
|
|
32
33
|
"semver": "^7.8.5",
|
|
33
34
|
"yaml": "^2.9.1",
|
|
34
35
|
"zod": "^4.6.7"
|
|
@@ -129,6 +129,20 @@
|
|
|
129
129
|
"maxLength": 2000,
|
|
130
130
|
"pattern": "^https:\\/\\/\\S+$"
|
|
131
131
|
},
|
|
132
|
+
"icon": {
|
|
133
|
+
"anyOf": [
|
|
134
|
+
{
|
|
135
|
+
"type": "string",
|
|
136
|
+
"maxLength": 2000,
|
|
137
|
+
"pattern": "^https:\\/\\/[^\\s?#]+\\.(?:[sS][vV][gG]|[pP][nN][gG])(?:\\?[^\\s#]*)?$"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"type": "string",
|
|
141
|
+
"maxLength": 512,
|
|
142
|
+
"pattern": "^\\.\\/[^\\s?#]+\\.(?:[sS][vV][gG]|[pP][nN][gG])$"
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
},
|
|
132
146
|
"deprecated": {
|
|
133
147
|
"anyOf": [
|
|
134
148
|
{
|
|
@@ -194,6 +194,161 @@
|
|
|
194
194
|
"additionalProperties": false
|
|
195
195
|
}
|
|
196
196
|
},
|
|
197
|
+
"themes": {
|
|
198
|
+
"maxItems": 10,
|
|
199
|
+
"type": "array",
|
|
200
|
+
"items": {
|
|
201
|
+
"type": "object",
|
|
202
|
+
"properties": {
|
|
203
|
+
"path": {
|
|
204
|
+
"type": "string",
|
|
205
|
+
"minLength": 3,
|
|
206
|
+
"maxLength": 512,
|
|
207
|
+
"pattern": "^\\.\\/"
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
"required": [
|
|
211
|
+
"path"
|
|
212
|
+
],
|
|
213
|
+
"additionalProperties": false
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
"setupGuides": {
|
|
217
|
+
"maxItems": 8,
|
|
218
|
+
"type": "array",
|
|
219
|
+
"items": {
|
|
220
|
+
"type": "object",
|
|
221
|
+
"properties": {
|
|
222
|
+
"id": {
|
|
223
|
+
"type": "string",
|
|
224
|
+
"pattern": "^[a-z0-9][a-z0-9-]{0,63}$"
|
|
225
|
+
},
|
|
226
|
+
"place": {
|
|
227
|
+
"type": "string",
|
|
228
|
+
"enum": [
|
|
229
|
+
"channel-connection"
|
|
230
|
+
]
|
|
231
|
+
},
|
|
232
|
+
"title": {
|
|
233
|
+
"type": "object",
|
|
234
|
+
"properties": {
|
|
235
|
+
"en": {
|
|
236
|
+
"type": "string",
|
|
237
|
+
"minLength": 1,
|
|
238
|
+
"maxLength": 100,
|
|
239
|
+
"pattern": "\\S"
|
|
240
|
+
},
|
|
241
|
+
"zh": {
|
|
242
|
+
"type": "string",
|
|
243
|
+
"minLength": 1,
|
|
244
|
+
"maxLength": 100,
|
|
245
|
+
"pattern": "\\S"
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
"required": [
|
|
249
|
+
"en"
|
|
250
|
+
],
|
|
251
|
+
"additionalProperties": false
|
|
252
|
+
},
|
|
253
|
+
"steps": {
|
|
254
|
+
"maxItems": 20,
|
|
255
|
+
"type": "array",
|
|
256
|
+
"items": {
|
|
257
|
+
"type": "object",
|
|
258
|
+
"properties": {
|
|
259
|
+
"en": {
|
|
260
|
+
"type": "string",
|
|
261
|
+
"minLength": 1,
|
|
262
|
+
"maxLength": 2000,
|
|
263
|
+
"pattern": "\\S"
|
|
264
|
+
},
|
|
265
|
+
"zh": {
|
|
266
|
+
"type": "string",
|
|
267
|
+
"minLength": 1,
|
|
268
|
+
"maxLength": 2000,
|
|
269
|
+
"pattern": "\\S"
|
|
270
|
+
},
|
|
271
|
+
"mode": {
|
|
272
|
+
"type": "string",
|
|
273
|
+
"maxLength": 64
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
"required": [
|
|
277
|
+
"en"
|
|
278
|
+
],
|
|
279
|
+
"additionalProperties": false
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
"files": {
|
|
283
|
+
"maxItems": 8,
|
|
284
|
+
"type": "array",
|
|
285
|
+
"items": {
|
|
286
|
+
"type": "object",
|
|
287
|
+
"properties": {
|
|
288
|
+
"path": {
|
|
289
|
+
"type": "string",
|
|
290
|
+
"minLength": 3,
|
|
291
|
+
"maxLength": 512,
|
|
292
|
+
"allOf": [
|
|
293
|
+
{
|
|
294
|
+
"pattern": "^\\.\\/"
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
"pattern": "\\.(?:tmpl|ya?ml|json|txt)$"
|
|
298
|
+
}
|
|
299
|
+
]
|
|
300
|
+
},
|
|
301
|
+
"title": {
|
|
302
|
+
"type": "object",
|
|
303
|
+
"properties": {
|
|
304
|
+
"en": {
|
|
305
|
+
"type": "string",
|
|
306
|
+
"minLength": 1,
|
|
307
|
+
"maxLength": 100,
|
|
308
|
+
"pattern": "\\S"
|
|
309
|
+
},
|
|
310
|
+
"zh": {
|
|
311
|
+
"type": "string",
|
|
312
|
+
"minLength": 1,
|
|
313
|
+
"maxLength": 100,
|
|
314
|
+
"pattern": "\\S"
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
"required": [
|
|
318
|
+
"en"
|
|
319
|
+
],
|
|
320
|
+
"additionalProperties": false
|
|
321
|
+
},
|
|
322
|
+
"language": {
|
|
323
|
+
"type": "string",
|
|
324
|
+
"enum": [
|
|
325
|
+
"yaml",
|
|
326
|
+
"json",
|
|
327
|
+
"text"
|
|
328
|
+
]
|
|
329
|
+
},
|
|
330
|
+
"mode": {
|
|
331
|
+
"type": "string",
|
|
332
|
+
"maxLength": 64
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
"required": [
|
|
336
|
+
"path",
|
|
337
|
+
"title",
|
|
338
|
+
"language"
|
|
339
|
+
],
|
|
340
|
+
"additionalProperties": false
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
"required": [
|
|
345
|
+
"id",
|
|
346
|
+
"place",
|
|
347
|
+
"title"
|
|
348
|
+
],
|
|
349
|
+
"additionalProperties": false
|
|
350
|
+
}
|
|
351
|
+
},
|
|
197
352
|
"extension": {
|
|
198
353
|
"type": "object",
|
|
199
354
|
"properties": {
|