@opentabs-dev/plugin-tools 0.0.16 → 0.0.18
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/cli.js +2 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/build.d.ts +32 -2
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +212 -32
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/inspect.d.ts +29 -0
- package/dist/commands/inspect.d.ts.map +1 -0
- package/dist/commands/inspect.js +165 -0
- package/dist/commands/inspect.js.map +1 -0
- package/dist/validate-icon.d.ts +34 -0
- package/dist/validate-icon.d.ts.map +1 -0
- package/dist/validate-icon.js +405 -0
- package/dist/validate-icon.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `opentabs-plugin inspect` command — pretty-prints the built plugin manifest.
|
|
3
|
+
* Reads dist/tools.json and package.json from the current directory and displays
|
|
4
|
+
* a human-readable summary of tools, resources, and prompts.
|
|
5
|
+
*/
|
|
6
|
+
import type { ManifestPrompt, ManifestResource, ManifestTool } from '@opentabs-dev/shared';
|
|
7
|
+
import type { Command } from 'commander';
|
|
8
|
+
/** Shape of dist/tools.json as written by `opentabs-plugin build` */
|
|
9
|
+
interface ToolsJsonManifest {
|
|
10
|
+
sdkVersion?: string;
|
|
11
|
+
tools: ManifestTool[];
|
|
12
|
+
resources?: ManifestResource[];
|
|
13
|
+
prompts?: ManifestPrompt[];
|
|
14
|
+
}
|
|
15
|
+
/** Extract field names and types from a JSON Schema object */
|
|
16
|
+
declare const extractFields: (schema: Record<string, unknown>) => Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
type: string;
|
|
19
|
+
required: boolean;
|
|
20
|
+
}>;
|
|
21
|
+
/** Truncate a string to maxLen, appending "..." if truncated */
|
|
22
|
+
declare const truncate: (s: string, maxLen: number) => string;
|
|
23
|
+
declare const handleInspect: (options: {
|
|
24
|
+
json?: boolean;
|
|
25
|
+
}, projectDir?: string) => Promise<void>;
|
|
26
|
+
declare const registerInspectCommand: (program: Command) => void;
|
|
27
|
+
export { extractFields, handleInspect, registerInspectCommand, truncate };
|
|
28
|
+
export type { ToolsJsonManifest };
|
|
29
|
+
//# sourceMappingURL=inspect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../src/commands/inspect.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAC3F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,qEAAqE;AACrE,UAAU,iBAAiB;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,SAAS,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC/B,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;CAC5B;AAED,8DAA8D;AAC9D,QAAA,MAAM,aAAa,GAAI,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAkB/G,CAAC;AAEF,gEAAgE;AAChE,QAAA,MAAM,QAAQ,GAAI,GAAG,MAAM,EAAE,QAAQ,MAAM,KAAG,MAAkE,CAAC;AAEjH,QAAA,MAAM,aAAa,GAAU,SAAS;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,EAAE,aAAY,MAAsB,KAAG,OAAO,CAAC,IAAI,CAqI1G,CAAC;AAEF,QAAA,MAAM,sBAAsB,GAAI,SAAS,OAAO,KAAG,IAalD,CAAC;AAEF,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,sBAAsB,EAAE,QAAQ,EAAE,CAAC;AAC1E,YAAY,EAAE,iBAAiB,EAAE,CAAC"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `opentabs-plugin inspect` command — pretty-prints the built plugin manifest.
|
|
3
|
+
* Reads dist/tools.json and package.json from the current directory and displays
|
|
4
|
+
* a human-readable summary of tools, resources, and prompts.
|
|
5
|
+
*/
|
|
6
|
+
import { parsePluginPackageJson } from '@opentabs-dev/shared';
|
|
7
|
+
import pc from 'picocolors';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
/** Extract field names and types from a JSON Schema object */
|
|
10
|
+
const extractFields = (schema) => {
|
|
11
|
+
const properties = schema.properties;
|
|
12
|
+
if (!properties)
|
|
13
|
+
return [];
|
|
14
|
+
const requiredSet = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
15
|
+
return Object.entries(properties).map(([name, prop]) => {
|
|
16
|
+
let type = 'unknown';
|
|
17
|
+
if (typeof prop.type === 'string') {
|
|
18
|
+
type = prop.type;
|
|
19
|
+
}
|
|
20
|
+
else if (Array.isArray(prop.anyOf)) {
|
|
21
|
+
const types = prop.anyOf
|
|
22
|
+
.map(t => (typeof t.type === 'string' ? t.type : '?'))
|
|
23
|
+
.join(' | ');
|
|
24
|
+
type = types;
|
|
25
|
+
}
|
|
26
|
+
return { name, type, required: requiredSet.has(name) };
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
/** Truncate a string to maxLen, appending "..." if truncated */
|
|
30
|
+
const truncate = (s, maxLen) => (s.length > maxLen ? s.slice(0, maxLen - 3) + '...' : s);
|
|
31
|
+
const handleInspect = async (options, projectDir = process.cwd()) => {
|
|
32
|
+
// Read dist/tools.json
|
|
33
|
+
const toolsJsonPath = join(projectDir, 'dist', 'tools.json');
|
|
34
|
+
const toolsJsonFile = Bun.file(toolsJsonPath);
|
|
35
|
+
if (!(await toolsJsonFile.exists())) {
|
|
36
|
+
console.error(pc.red('No manifest found. Run opentabs-plugin build first.'));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
let manifest;
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(await toolsJsonFile.text());
|
|
42
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
43
|
+
throw new Error('not an object');
|
|
44
|
+
}
|
|
45
|
+
const obj = parsed;
|
|
46
|
+
// Support legacy format (plain array) and current format (object with tools key)
|
|
47
|
+
if (Array.isArray(obj.tools)) {
|
|
48
|
+
manifest = obj;
|
|
49
|
+
}
|
|
50
|
+
else if (Array.isArray(parsed)) {
|
|
51
|
+
manifest = { tools: parsed };
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
throw new Error('unexpected format');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
console.error(pc.red('Failed to parse dist/tools.json. The file may be corrupted — rebuild with opentabs-plugin build.'));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
// --json mode: output raw JSON and exit
|
|
62
|
+
if (options.json) {
|
|
63
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Read package.json for plugin metadata
|
|
67
|
+
let pluginName = '(unknown)';
|
|
68
|
+
let pluginVersion = '(unknown)';
|
|
69
|
+
let displayName;
|
|
70
|
+
const pkgJsonFile = Bun.file(join(projectDir, 'package.json'));
|
|
71
|
+
if (await pkgJsonFile.exists()) {
|
|
72
|
+
try {
|
|
73
|
+
const pkgJsonRaw = JSON.parse(await pkgJsonFile.text());
|
|
74
|
+
const result = parsePluginPackageJson(pkgJsonRaw, projectDir);
|
|
75
|
+
if (result.ok) {
|
|
76
|
+
pluginName = result.value.name;
|
|
77
|
+
pluginVersion = result.value.version;
|
|
78
|
+
displayName = result.value.opentabs.displayName;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Non-fatal — we can still show the manifest
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const tools = manifest.tools;
|
|
86
|
+
const resources = manifest.resources ?? [];
|
|
87
|
+
const prompts = manifest.prompts ?? [];
|
|
88
|
+
// Header
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log(pc.bold(displayName ?? pluginName) + pc.dim(` v${pluginVersion}`));
|
|
91
|
+
if (manifest.sdkVersion) {
|
|
92
|
+
console.log(pc.dim(`SDK version: ${manifest.sdkVersion}`));
|
|
93
|
+
}
|
|
94
|
+
// Summary counts
|
|
95
|
+
const parts = [];
|
|
96
|
+
parts.push(`${tools.length} tool${tools.length === 1 ? '' : 's'}`);
|
|
97
|
+
parts.push(`${resources.length} resource${resources.length === 1 ? '' : 's'}`);
|
|
98
|
+
parts.push(`${prompts.length} prompt${prompts.length === 1 ? '' : 's'}`);
|
|
99
|
+
console.log(pc.dim(parts.join(' · ')));
|
|
100
|
+
console.log('');
|
|
101
|
+
// Tools
|
|
102
|
+
if (tools.length > 0) {
|
|
103
|
+
console.log(pc.bold('Tools'));
|
|
104
|
+
console.log('');
|
|
105
|
+
for (const tool of tools) {
|
|
106
|
+
console.log(` ${pc.cyan(tool.icon)} ${pc.bold(tool.name)} ${pc.dim(tool.displayName)}`);
|
|
107
|
+
console.log(` ${truncate(tool.description, 80)}`);
|
|
108
|
+
const inputFields = extractFields(tool.input_schema);
|
|
109
|
+
if (inputFields.length > 0) {
|
|
110
|
+
const fieldStrs = inputFields.map(f => `${f.name}: ${f.type}${f.required ? '' : '?'}`);
|
|
111
|
+
console.log(` ${pc.dim('Input:')} ${fieldStrs.join(', ')}`);
|
|
112
|
+
}
|
|
113
|
+
const outputFields = extractFields(tool.output_schema);
|
|
114
|
+
if (outputFields.length > 0) {
|
|
115
|
+
const fieldStrs = outputFields.map(f => `${f.name}: ${f.type}${f.required ? '' : '?'}`);
|
|
116
|
+
console.log(` ${pc.dim('Output:')} ${fieldStrs.join(', ')}`);
|
|
117
|
+
}
|
|
118
|
+
console.log('');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Resources
|
|
122
|
+
if (resources.length > 0) {
|
|
123
|
+
console.log(pc.bold('Resources'));
|
|
124
|
+
console.log('');
|
|
125
|
+
for (const resource of resources) {
|
|
126
|
+
console.log(` ${pc.cyan(resource.uri)} ${pc.bold(resource.name)}`);
|
|
127
|
+
if (resource.description) {
|
|
128
|
+
console.log(` ${resource.description}`);
|
|
129
|
+
}
|
|
130
|
+
if (resource.mimeType) {
|
|
131
|
+
console.log(` ${pc.dim(`MIME: ${resource.mimeType}`)}`);
|
|
132
|
+
}
|
|
133
|
+
console.log('');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Prompts
|
|
137
|
+
if (prompts.length > 0) {
|
|
138
|
+
console.log(pc.bold('Prompts'));
|
|
139
|
+
console.log('');
|
|
140
|
+
for (const prompt of prompts) {
|
|
141
|
+
console.log(` ${pc.bold(prompt.name)}`);
|
|
142
|
+
if (prompt.description) {
|
|
143
|
+
console.log(` ${prompt.description}`);
|
|
144
|
+
}
|
|
145
|
+
if (prompt.arguments && prompt.arguments.length > 0) {
|
|
146
|
+
const argStrs = prompt.arguments.map(a => `${a.name}${a.required ? '' : '?'}${a.description ? ` — ${a.description}` : ''}`);
|
|
147
|
+
console.log(` ${pc.dim('Args:')} ${argStrs.join(', ')}`);
|
|
148
|
+
}
|
|
149
|
+
console.log('');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const registerInspectCommand = (program) => {
|
|
154
|
+
program
|
|
155
|
+
.command('inspect')
|
|
156
|
+
.description('Pretty-print the built plugin manifest (dist/tools.json)')
|
|
157
|
+
.option('--json', 'Output raw JSON instead of formatted summary')
|
|
158
|
+
.addHelpText('after', `
|
|
159
|
+
Examples:
|
|
160
|
+
$ opentabs-plugin inspect
|
|
161
|
+
$ opentabs-plugin inspect --json`)
|
|
162
|
+
.action((options) => handleInspect(options));
|
|
163
|
+
};
|
|
164
|
+
export { extractFields, handleInspect, registerInspectCommand, truncate };
|
|
165
|
+
//# sourceMappingURL=inspect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inspect.js","sourceRoot":"","sources":["../../src/commands/inspect.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG icon validation and auto-grayscale generation for plugin icons.
|
|
3
|
+
*
|
|
4
|
+
* Three exports:
|
|
5
|
+
* - validateIconSvg — structural validation (size, viewBox, forbidden elements)
|
|
6
|
+
* - validateInactiveIconColors — ensures only achromatic colors are present
|
|
7
|
+
* - generateInactiveIcon — converts all color values to luminance-equivalent grays
|
|
8
|
+
*/
|
|
9
|
+
declare const MAX_ICON_SIZE: number;
|
|
10
|
+
type ValidationResult = {
|
|
11
|
+
valid: true;
|
|
12
|
+
} | {
|
|
13
|
+
valid: false;
|
|
14
|
+
errors: string[];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Validate an SVG string for use as a plugin icon.
|
|
18
|
+
* Checks: size <= 8KB, viewBox present and square, no <image>/<script>,
|
|
19
|
+
* no event handler attributes.
|
|
20
|
+
*/
|
|
21
|
+
declare const validateIconSvg: (content: string, _filename: string) => ValidationResult;
|
|
22
|
+
/**
|
|
23
|
+
* Validate that an SVG contains only achromatic colors.
|
|
24
|
+
* Checks fill, stroke, stop-color, and flood-color attributes and inline styles.
|
|
25
|
+
*/
|
|
26
|
+
declare const validateInactiveIconColors: (content: string) => ValidationResult;
|
|
27
|
+
/**
|
|
28
|
+
* Convert all color values in an SVG to luminance-equivalent grays.
|
|
29
|
+
* Processes fill, stroke, stop-color, and flood-color in both attributes and inline styles.
|
|
30
|
+
* Uses ITU-R BT.709: gray = 0.2126*R + 0.7152*G + 0.0722*B
|
|
31
|
+
*/
|
|
32
|
+
declare const generateInactiveIcon: (svgContent: string) => string;
|
|
33
|
+
export { generateInactiveIcon, MAX_ICON_SIZE, validateIconSvg, validateInactiveIconColors };
|
|
34
|
+
//# sourceMappingURL=validate-icon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-icon.d.ts","sourceRoot":"","sources":["../src/validate-icon.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,QAAA,MAAM,aAAa,QAAW,CAAC;AA+O/B,KAAK,gBAAgB,GAAG;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAE7E;;;;GAIG;AACH,QAAA,MAAM,eAAe,GAAI,SAAS,MAAM,EAAE,WAAW,MAAM,KAAG,gBA2C7D,CAAC;AAMF;;;GAGG;AACH,QAAA,MAAM,0BAA0B,GAAI,SAAS,MAAM,KAAG,gBAgCrD,CAAC;AAuEF;;;;GAIG;AACH,QAAA,MAAM,oBAAoB,GAAI,YAAY,MAAM,KAAG,MAyBlD,CAAC;AAEF,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,eAAe,EAAE,0BAA0B,EAAE,CAAC"}
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG icon validation and auto-grayscale generation for plugin icons.
|
|
3
|
+
*
|
|
4
|
+
* Three exports:
|
|
5
|
+
* - validateIconSvg — structural validation (size, viewBox, forbidden elements)
|
|
6
|
+
* - validateInactiveIconColors — ensures only achromatic colors are present
|
|
7
|
+
* - generateInactiveIcon — converts all color values to luminance-equivalent grays
|
|
8
|
+
*/
|
|
9
|
+
const MAX_ICON_SIZE = 8 * 1024; // 8 KB
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Named color lookup table (CSS2.1 + common extended names)
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
const NAMED_COLORS = {
|
|
14
|
+
aqua: [0, 255, 255],
|
|
15
|
+
black: [0, 0, 0],
|
|
16
|
+
blue: [0, 0, 255],
|
|
17
|
+
fuchsia: [255, 0, 255],
|
|
18
|
+
gray: [128, 128, 128],
|
|
19
|
+
grey: [128, 128, 128],
|
|
20
|
+
green: [0, 128, 0],
|
|
21
|
+
lime: [0, 255, 0],
|
|
22
|
+
maroon: [128, 0, 0],
|
|
23
|
+
navy: [0, 0, 128],
|
|
24
|
+
olive: [128, 128, 0],
|
|
25
|
+
orange: [255, 165, 0],
|
|
26
|
+
purple: [128, 0, 128],
|
|
27
|
+
red: [255, 0, 0],
|
|
28
|
+
silver: [192, 192, 192],
|
|
29
|
+
teal: [0, 128, 128],
|
|
30
|
+
white: [255, 255, 255],
|
|
31
|
+
yellow: [255, 255, 0],
|
|
32
|
+
gold: [255, 215, 0],
|
|
33
|
+
indigo: [75, 0, 130],
|
|
34
|
+
coral: [255, 127, 80],
|
|
35
|
+
crimson: [220, 20, 60],
|
|
36
|
+
tomato: [255, 99, 71],
|
|
37
|
+
salmon: [250, 128, 114],
|
|
38
|
+
orchid: [218, 112, 214],
|
|
39
|
+
plum: [221, 160, 221],
|
|
40
|
+
chocolate: [210, 105, 30],
|
|
41
|
+
tan: [210, 180, 140],
|
|
42
|
+
peru: [205, 133, 63],
|
|
43
|
+
sienna: [160, 82, 45],
|
|
44
|
+
firebrick: [178, 34, 34],
|
|
45
|
+
darkred: [139, 0, 0],
|
|
46
|
+
darkgreen: [0, 100, 0],
|
|
47
|
+
darkblue: [0, 0, 139],
|
|
48
|
+
darkcyan: [0, 139, 139],
|
|
49
|
+
darkmagenta: [139, 0, 139],
|
|
50
|
+
darkviolet: [148, 0, 211],
|
|
51
|
+
deeppink: [255, 20, 147],
|
|
52
|
+
deepskyblue: [0, 191, 255],
|
|
53
|
+
dodgerblue: [30, 144, 255],
|
|
54
|
+
hotpink: [255, 105, 180],
|
|
55
|
+
lawngreen: [124, 252, 0],
|
|
56
|
+
limegreen: [50, 205, 50],
|
|
57
|
+
mediumblue: [0, 0, 205],
|
|
58
|
+
mediumorchid: [186, 85, 211],
|
|
59
|
+
mediumpurple: [147, 111, 219],
|
|
60
|
+
mediumseagreen: [60, 179, 113],
|
|
61
|
+
mediumslateblue: [123, 104, 238],
|
|
62
|
+
mediumspringgreen: [0, 250, 154],
|
|
63
|
+
mediumturquoise: [72, 209, 204],
|
|
64
|
+
mediumvioletred: [199, 21, 133],
|
|
65
|
+
midnightblue: [25, 25, 112],
|
|
66
|
+
orangered: [255, 69, 0],
|
|
67
|
+
palegreen: [152, 251, 152],
|
|
68
|
+
palevioletred: [219, 112, 147],
|
|
69
|
+
royalblue: [65, 105, 225],
|
|
70
|
+
saddlebrown: [139, 69, 19],
|
|
71
|
+
seagreen: [46, 139, 87],
|
|
72
|
+
skyblue: [135, 206, 235],
|
|
73
|
+
slateblue: [106, 90, 205],
|
|
74
|
+
springgreen: [0, 255, 127],
|
|
75
|
+
steelblue: [70, 130, 180],
|
|
76
|
+
turquoise: [64, 224, 208],
|
|
77
|
+
violet: [238, 130, 238],
|
|
78
|
+
wheat: [245, 222, 179],
|
|
79
|
+
yellowgreen: [154, 205, 50],
|
|
80
|
+
// Achromatic named colors
|
|
81
|
+
dimgray: [105, 105, 105],
|
|
82
|
+
dimgrey: [105, 105, 105],
|
|
83
|
+
darkgray: [169, 169, 169],
|
|
84
|
+
darkgrey: [169, 169, 169],
|
|
85
|
+
lightgray: [211, 211, 211],
|
|
86
|
+
lightgrey: [211, 211, 211],
|
|
87
|
+
gainsboro: [220, 220, 220],
|
|
88
|
+
whitesmoke: [245, 245, 245],
|
|
89
|
+
};
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Achromatic color names (allowed in inactive icons)
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
const ACHROMATIC_NAMES = new Set([
|
|
94
|
+
'black',
|
|
95
|
+
'white',
|
|
96
|
+
'gray',
|
|
97
|
+
'grey',
|
|
98
|
+
'silver',
|
|
99
|
+
'dimgray',
|
|
100
|
+
'dimgrey',
|
|
101
|
+
'darkgray',
|
|
102
|
+
'darkgrey',
|
|
103
|
+
'lightgray',
|
|
104
|
+
'lightgrey',
|
|
105
|
+
'gainsboro',
|
|
106
|
+
'whitesmoke',
|
|
107
|
+
]);
|
|
108
|
+
// Values that are not actual colors and should be passed through unchanged
|
|
109
|
+
const PASSTHROUGH_VALUES = new Set(['none', 'currentcolor', 'transparent', 'inherit', 'unset', 'initial']);
|
|
110
|
+
// Color-carrying attributes in SVG
|
|
111
|
+
const COLOR_ATTRS = ['fill', 'stroke', 'stop-color', 'flood-color'];
|
|
112
|
+
// Event handler attributes to reject
|
|
113
|
+
const EVENT_HANDLER_RE = /\bon(?:abort|activate|afterprint|beforeprint|beforeunload|blur|cancel|canplay|canplaythrough|change|click|close|contextmenu|copy|cuechange|cut|dblclick|drag|dragend|dragenter|dragleave|dragover|dragstart|drop|durationchange|emptied|ended|error|focus|focusin|focusout|formdata|fullscreenchange|fullscreenerror|hashchange|input|invalid|keydown|keypress|keyup|load|loadeddata|loadedmetadata|loadstart|message|messageerror|mousedown|mouseenter|mouseleave|mousemove|mouseout|mouseover|mouseup|offline|online|open|pagehide|pageshow|paste|pause|play|playing|pointercancel|pointerdown|pointerenter|pointerleave|pointermove|pointerout|pointerover|pointerup|popstate|progress|ratechange|reset|resize|scroll|securitypolicyviolation|seeked|seeking|select|slotchange|stalled|storage|submit|suspend|timeupdate|toggle|touchcancel|touchend|touchmove|touchstart|transitioncancel|transitionend|transitionrun|transitionstart|unhandledrejection|unload|volumechange|waiting|wheel)\s*=/i;
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Color parsing utilities
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
/** Parse a hex color (#RGB or #RRGGBB) to [R, G, B] */
|
|
118
|
+
const parseHex = (hex) => {
|
|
119
|
+
const trimmedHex = hex.trim();
|
|
120
|
+
if (trimmedHex.length === 4) {
|
|
121
|
+
const c1 = trimmedHex[1] ?? '0';
|
|
122
|
+
const c2 = trimmedHex[2] ?? '0';
|
|
123
|
+
const c3 = trimmedHex[3] ?? '0';
|
|
124
|
+
const red = parseInt(c1 + c1, 16);
|
|
125
|
+
const green = parseInt(c2 + c2, 16);
|
|
126
|
+
const blue = parseInt(c3 + c3, 16);
|
|
127
|
+
if (Number.isNaN(red) || Number.isNaN(green) || Number.isNaN(blue))
|
|
128
|
+
return null;
|
|
129
|
+
return [red, green, blue];
|
|
130
|
+
}
|
|
131
|
+
if (trimmedHex.length === 7) {
|
|
132
|
+
const red = parseInt(trimmedHex.slice(1, 3), 16);
|
|
133
|
+
const green = parseInt(trimmedHex.slice(3, 5), 16);
|
|
134
|
+
const blue = parseInt(trimmedHex.slice(5, 7), 16);
|
|
135
|
+
if (Number.isNaN(red) || Number.isNaN(green) || Number.isNaN(blue))
|
|
136
|
+
return null;
|
|
137
|
+
return [red, green, blue];
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
};
|
|
141
|
+
/** Convert HSL to RGB. hue is 0-360, saturation/lightness are 0-100 percentages. */
|
|
142
|
+
const hslToRgb = (hue, saturation, lightness) => {
|
|
143
|
+
const saturationNorm = saturation / 100;
|
|
144
|
+
const lightnessNorm = lightness / 100;
|
|
145
|
+
const chroma = (1 - Math.abs(2 * lightnessNorm - 1)) * saturationNorm;
|
|
146
|
+
const secondaryComponent = chroma * (1 - Math.abs(((hue / 60) % 2) - 1));
|
|
147
|
+
const lightnessMatch = lightnessNorm - chroma / 2;
|
|
148
|
+
let redPrime, greenPrime, bluePrime;
|
|
149
|
+
if (hue < 60) {
|
|
150
|
+
[redPrime, greenPrime, bluePrime] = [chroma, secondaryComponent, 0];
|
|
151
|
+
}
|
|
152
|
+
else if (hue < 120) {
|
|
153
|
+
[redPrime, greenPrime, bluePrime] = [secondaryComponent, chroma, 0];
|
|
154
|
+
}
|
|
155
|
+
else if (hue < 180) {
|
|
156
|
+
[redPrime, greenPrime, bluePrime] = [0, chroma, secondaryComponent];
|
|
157
|
+
}
|
|
158
|
+
else if (hue < 240) {
|
|
159
|
+
[redPrime, greenPrime, bluePrime] = [0, secondaryComponent, chroma];
|
|
160
|
+
}
|
|
161
|
+
else if (hue < 300) {
|
|
162
|
+
[redPrime, greenPrime, bluePrime] = [secondaryComponent, 0, chroma];
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
[redPrime, greenPrime, bluePrime] = [chroma, 0, secondaryComponent];
|
|
166
|
+
}
|
|
167
|
+
return [
|
|
168
|
+
Math.round((redPrime + lightnessMatch) * 255),
|
|
169
|
+
Math.round((greenPrime + lightnessMatch) * 255),
|
|
170
|
+
Math.round((bluePrime + lightnessMatch) * 255),
|
|
171
|
+
];
|
|
172
|
+
};
|
|
173
|
+
/** Compute luminance-equivalent gray value using ITU-R BT.709 */
|
|
174
|
+
const toLuminanceGray = (red, green, blue) => Math.round(0.2126 * red + 0.7152 * green + 0.0722 * blue);
|
|
175
|
+
/** Convert a gray value (0-255) to a two-digit hex string */
|
|
176
|
+
const grayToHex = (gray) => {
|
|
177
|
+
const hexPair = Math.max(0, Math.min(255, gray)).toString(16).padStart(2, '0');
|
|
178
|
+
return `#${hexPair}${hexPair}${hexPair}`;
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Parse a CSS color value into [R, G, B] or null if not a recognizable color.
|
|
182
|
+
* Returns null for passthrough values (none, currentColor, etc.) and url() references.
|
|
183
|
+
*/
|
|
184
|
+
const parseColor = (value) => {
|
|
185
|
+
const trimmedValue = value.trim();
|
|
186
|
+
const lowerValue = trimmedValue.toLowerCase();
|
|
187
|
+
// Passthrough values
|
|
188
|
+
if (PASSTHROUGH_VALUES.has(lowerValue))
|
|
189
|
+
return null;
|
|
190
|
+
// URL references (e.g., url(#gradient))
|
|
191
|
+
if (lowerValue.startsWith('url('))
|
|
192
|
+
return null;
|
|
193
|
+
// Hex colors
|
|
194
|
+
if (trimmedValue.startsWith('#'))
|
|
195
|
+
return parseHex(trimmedValue);
|
|
196
|
+
// rgb()/rgba()
|
|
197
|
+
const rgbMatch = lowerValue.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
|
198
|
+
if (rgbMatch) {
|
|
199
|
+
return [parseInt(rgbMatch[1] ?? '0', 10), parseInt(rgbMatch[2] ?? '0', 10), parseInt(rgbMatch[3] ?? '0', 10)];
|
|
200
|
+
}
|
|
201
|
+
// hsl()/hsla()
|
|
202
|
+
const hslMatch = lowerValue.match(/^hsla?\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%/);
|
|
203
|
+
if (hslMatch) {
|
|
204
|
+
return hslToRgb(parseFloat(hslMatch[1] ?? '0'), parseFloat(hslMatch[2] ?? '0'), parseFloat(hslMatch[3] ?? '0'));
|
|
205
|
+
}
|
|
206
|
+
// Named colors
|
|
207
|
+
const named = NAMED_COLORS[lowerValue];
|
|
208
|
+
if (named)
|
|
209
|
+
return named;
|
|
210
|
+
return null;
|
|
211
|
+
};
|
|
212
|
+
/** Check if a parsed RGB is achromatic (R === G === B) */
|
|
213
|
+
const isAchromatic = (red, green, blue) => red === green && green === blue;
|
|
214
|
+
/** Check if a CSS color string is achromatic (or a passthrough value) */
|
|
215
|
+
const isAchromaticColor = (value) => {
|
|
216
|
+
const normalizedValue = value.trim().toLowerCase();
|
|
217
|
+
if (PASSTHROUGH_VALUES.has(normalizedValue))
|
|
218
|
+
return true;
|
|
219
|
+
if (normalizedValue.startsWith('url('))
|
|
220
|
+
return true;
|
|
221
|
+
if (ACHROMATIC_NAMES.has(normalizedValue))
|
|
222
|
+
return true;
|
|
223
|
+
// hsl/hsla — check saturation is 0
|
|
224
|
+
const hslMatch = normalizedValue.match(/^hsla?\(\s*[\d.]+\s*,\s*([\d.]+)%/);
|
|
225
|
+
if (hslMatch)
|
|
226
|
+
return parseFloat(hslMatch[1] ?? '0') === 0;
|
|
227
|
+
const rgb = parseColor(normalizedValue);
|
|
228
|
+
if (!rgb)
|
|
229
|
+
return true; // Unrecognized values are considered achromatic
|
|
230
|
+
return isAchromatic(rgb[0], rgb[1], rgb[2]);
|
|
231
|
+
};
|
|
232
|
+
/**
|
|
233
|
+
* Validate an SVG string for use as a plugin icon.
|
|
234
|
+
* Checks: size <= 8KB, viewBox present and square, no <image>/<script>,
|
|
235
|
+
* no event handler attributes.
|
|
236
|
+
*/
|
|
237
|
+
const validateIconSvg = (content, _filename) => {
|
|
238
|
+
const errors = [];
|
|
239
|
+
// Size check (byte count, not string length)
|
|
240
|
+
const byteSize = new TextEncoder().encode(content).byteLength;
|
|
241
|
+
if (byteSize > MAX_ICON_SIZE) {
|
|
242
|
+
errors.push(`SVG size (${byteSize} bytes) exceeds maximum of ${MAX_ICON_SIZE} bytes (8 KB)`);
|
|
243
|
+
}
|
|
244
|
+
// viewBox check
|
|
245
|
+
const viewBoxMatch = content.match(/viewBox\s*=\s*["']([^"']*)["']/);
|
|
246
|
+
if (!viewBoxMatch) {
|
|
247
|
+
errors.push('SVG must have a viewBox attribute');
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
const viewBoxValue = viewBoxMatch[1] ?? '';
|
|
251
|
+
const parts = viewBoxValue.trim().split(/\s+/);
|
|
252
|
+
if (parts.length === 4) {
|
|
253
|
+
const viewBoxWidth = parseFloat(parts[2] ?? '0');
|
|
254
|
+
const viewBoxHeight = parseFloat(parts[3] ?? '0');
|
|
255
|
+
if (viewBoxWidth !== viewBoxHeight) {
|
|
256
|
+
errors.push(`SVG viewBox must be square (got ${viewBoxWidth}x${viewBoxHeight})`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
errors.push('SVG viewBox must have exactly 4 values (min-x min-y width height)');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Forbidden elements: <image>
|
|
264
|
+
if (/<image[\s/>]/i.test(content)) {
|
|
265
|
+
errors.push('SVG must not contain <image> elements');
|
|
266
|
+
}
|
|
267
|
+
// Forbidden elements: <script>
|
|
268
|
+
if (/<script[\s/>]/i.test(content)) {
|
|
269
|
+
errors.push('SVG must not contain <script> elements');
|
|
270
|
+
}
|
|
271
|
+
// Event handler attributes
|
|
272
|
+
if (EVENT_HANDLER_RE.test(content)) {
|
|
273
|
+
errors.push('SVG must not contain event handler attributes (e.g., onclick, onload, onerror)');
|
|
274
|
+
}
|
|
275
|
+
return errors.length === 0 ? { valid: true } : { valid: false, errors };
|
|
276
|
+
};
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
// validateInactiveIconColors
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
/**
|
|
281
|
+
* Validate that an SVG contains only achromatic colors.
|
|
282
|
+
* Checks fill, stroke, stop-color, and flood-color attributes and inline styles.
|
|
283
|
+
*/
|
|
284
|
+
const validateInactiveIconColors = (content) => {
|
|
285
|
+
const errors = [];
|
|
286
|
+
// Check attribute values: (fill|stroke|stop-color|flood-color)="value"
|
|
287
|
+
const attrPattern = new RegExp(`(${COLOR_ATTRS.join('|')})\\s*=\\s*"([^"]*)"`, 'gi');
|
|
288
|
+
let attrMatch;
|
|
289
|
+
while ((attrMatch = attrPattern.exec(content)) !== null) {
|
|
290
|
+
const attr = attrMatch[1] ?? '';
|
|
291
|
+
const value = (attrMatch[2] ?? '').trim();
|
|
292
|
+
if (value && !isAchromaticColor(value)) {
|
|
293
|
+
errors.push(`Attribute ${attr}="${value}" uses a saturated color`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Check inline style property values: style="fill: value; stroke: value"
|
|
297
|
+
const stylePattern = /style\s*=\s*"([^"]*)"/gi;
|
|
298
|
+
let styleMatch;
|
|
299
|
+
while ((styleMatch = stylePattern.exec(content)) !== null) {
|
|
300
|
+
const styleValue = styleMatch[1] ?? '';
|
|
301
|
+
for (const attr of COLOR_ATTRS) {
|
|
302
|
+
const propPattern = new RegExp(`${attr.replace('-', '\\-')}\\s*:\\s*([^;"]+)`, 'gi');
|
|
303
|
+
let propMatch;
|
|
304
|
+
while ((propMatch = propPattern.exec(styleValue)) !== null) {
|
|
305
|
+
const value = (propMatch[1] ?? '').trim();
|
|
306
|
+
if (value && !isAchromaticColor(value)) {
|
|
307
|
+
errors.push(`Style property ${attr}: ${value} uses a saturated color`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return errors.length === 0 ? { valid: true } : { valid: false, errors };
|
|
313
|
+
};
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
// generateInactiveIcon
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
/**
|
|
318
|
+
* Convert a single color value to its grayscale equivalent.
|
|
319
|
+
* Returns the original value for passthrough values (none, currentColor, etc.)
|
|
320
|
+
*/
|
|
321
|
+
const convertColorToGray = (value) => {
|
|
322
|
+
const trimmedValue = value.trim();
|
|
323
|
+
const lowerValue = trimmedValue.toLowerCase();
|
|
324
|
+
if (PASSTHROUGH_VALUES.has(lowerValue))
|
|
325
|
+
return trimmedValue;
|
|
326
|
+
if (lowerValue.startsWith('url('))
|
|
327
|
+
return trimmedValue;
|
|
328
|
+
// hsl/hsla — set saturation to 0, preserve everything else
|
|
329
|
+
const hslaMatch = lowerValue.match(/^(hsla?)\(\s*([\d.]+)\s*,\s*[\d.]+%\s*,\s*([\d.]+%)\s*(?:,\s*([\d.]+))?\s*\)/);
|
|
330
|
+
if (hslaMatch) {
|
|
331
|
+
const fn = hslaMatch[1] ?? 'hsl';
|
|
332
|
+
const hue = hslaMatch[2] ?? '0';
|
|
333
|
+
const lightness = hslaMatch[3] ?? '50%';
|
|
334
|
+
const alpha = hslaMatch[4];
|
|
335
|
+
if (alpha !== undefined) {
|
|
336
|
+
return `${fn}(${hue}, 0%, ${lightness}, ${alpha})`;
|
|
337
|
+
}
|
|
338
|
+
return `${fn}(${hue}, 0%, ${lightness})`;
|
|
339
|
+
}
|
|
340
|
+
// rgba — convert and preserve alpha
|
|
341
|
+
const rgbaMatch = lowerValue.match(/^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)/);
|
|
342
|
+
if (rgbaMatch) {
|
|
343
|
+
const red = parseInt(rgbaMatch[1] ?? '0', 10);
|
|
344
|
+
const green = parseInt(rgbaMatch[2] ?? '0', 10);
|
|
345
|
+
const blue = parseInt(rgbaMatch[3] ?? '0', 10);
|
|
346
|
+
const alpha = rgbaMatch[4] ?? '1';
|
|
347
|
+
const gray = toLuminanceGray(red, green, blue);
|
|
348
|
+
return `rgba(${gray}, ${gray}, ${gray}, ${alpha})`;
|
|
349
|
+
}
|
|
350
|
+
// rgb()
|
|
351
|
+
const rgbMatch = lowerValue.match(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/);
|
|
352
|
+
if (rgbMatch) {
|
|
353
|
+
const red = parseInt(rgbMatch[1] ?? '0', 10);
|
|
354
|
+
const green = parseInt(rgbMatch[2] ?? '0', 10);
|
|
355
|
+
const blue = parseInt(rgbMatch[3] ?? '0', 10);
|
|
356
|
+
const gray = toLuminanceGray(red, green, blue);
|
|
357
|
+
return grayToHex(gray);
|
|
358
|
+
}
|
|
359
|
+
// Hex colors
|
|
360
|
+
if (trimmedValue.startsWith('#')) {
|
|
361
|
+
const rgb = parseHex(trimmedValue);
|
|
362
|
+
if (rgb) {
|
|
363
|
+
const gray = toLuminanceGray(rgb[0], rgb[1], rgb[2]);
|
|
364
|
+
return grayToHex(gray);
|
|
365
|
+
}
|
|
366
|
+
return trimmedValue;
|
|
367
|
+
}
|
|
368
|
+
// Named colors
|
|
369
|
+
const named = NAMED_COLORS[lowerValue];
|
|
370
|
+
if (named) {
|
|
371
|
+
const gray = toLuminanceGray(named[0], named[1], named[2]);
|
|
372
|
+
return grayToHex(gray);
|
|
373
|
+
}
|
|
374
|
+
return trimmedValue;
|
|
375
|
+
};
|
|
376
|
+
/**
|
|
377
|
+
* Convert all color values in an SVG to luminance-equivalent grays.
|
|
378
|
+
* Processes fill, stroke, stop-color, and flood-color in both attributes and inline styles.
|
|
379
|
+
* Uses ITU-R BT.709: gray = 0.2126*R + 0.7152*G + 0.0722*B
|
|
380
|
+
*/
|
|
381
|
+
const generateInactiveIcon = (svgContent) => {
|
|
382
|
+
let result = svgContent;
|
|
383
|
+
// Convert attribute values: (fill|stroke|stop-color|flood-color)="value"
|
|
384
|
+
const attrPattern = new RegExp(`((?:${COLOR_ATTRS.join('|')})\\s*=\\s*")([^"]*)(")`, 'gi');
|
|
385
|
+
result = result.replace(attrPattern, (_match, prefix, value, suffix) => {
|
|
386
|
+
const converted = convertColorToGray(value);
|
|
387
|
+
return `${prefix}${converted}${suffix}`;
|
|
388
|
+
});
|
|
389
|
+
// Convert inline style property values
|
|
390
|
+
const stylePattern = /style\s*=\s*"([^"]*)"/gi;
|
|
391
|
+
result = result.replace(stylePattern, (fullMatch, styleValue) => {
|
|
392
|
+
let newStyle = styleValue;
|
|
393
|
+
for (const attr of COLOR_ATTRS) {
|
|
394
|
+
const propPattern = new RegExp(`(${attr.replace('-', '\\-')}\\s*:\\s*)([^;"]+)`, 'gi');
|
|
395
|
+
newStyle = newStyle.replace(propPattern, (_m, propPrefix, propValue) => {
|
|
396
|
+
const converted = convertColorToGray(propValue);
|
|
397
|
+
return `${propPrefix}${converted}`;
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
return fullMatch.replace(styleValue, newStyle);
|
|
401
|
+
});
|
|
402
|
+
return result;
|
|
403
|
+
};
|
|
404
|
+
export { generateInactiveIcon, MAX_ICON_SIZE, validateIconSvg, validateInactiveIconColors };
|
|
405
|
+
//# sourceMappingURL=validate-icon.js.map
|