icuewidget-cli 0.4.45 → 0.4.47
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/package.json +1 -1
- package/src-node/constants.js +16 -1
- package/src-node/templates.js +60 -22
- package/src-node/validation.js +80 -0
package/package.json
CHANGED
package/src-node/constants.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const APP_NAME = "icuewidget";
|
|
2
2
|
export const NPM_PACKAGE_NAME = "icuewidget-cli";
|
|
3
|
-
export const CLI_VERSION = "0.4.
|
|
3
|
+
export const CLI_VERSION = "0.4.47";
|
|
4
4
|
export const DEFAULT_APP_VERSION = "5.45";
|
|
5
5
|
export const DEFAULT_FRAMEWORK_VERSION = "1.0.0";
|
|
6
6
|
export const DEFAULT_PLATFORM = "windows";
|
|
@@ -13,6 +13,21 @@ export const ALLOWED_DEVICE_TYPES = new Set([
|
|
|
13
13
|
"keyboard_lcd",
|
|
14
14
|
]);
|
|
15
15
|
|
|
16
|
+
// Control types accepted by iCUE for x-icue-property meta parameters (data-type).
|
|
17
|
+
// Anything else makes iCUE reject the widget with "Invalid meta parameter data type".
|
|
18
|
+
export const ALLOWED_PROPERTY_TYPES = new Set([
|
|
19
|
+
"slider",
|
|
20
|
+
"switch",
|
|
21
|
+
"color",
|
|
22
|
+
"combobox",
|
|
23
|
+
"search-combobox",
|
|
24
|
+
"tab-buttons",
|
|
25
|
+
"textfield",
|
|
26
|
+
"media-selector",
|
|
27
|
+
"sensors-combobox",
|
|
28
|
+
"sensors-factory",
|
|
29
|
+
]);
|
|
30
|
+
|
|
16
31
|
export const SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
17
32
|
export const APP_VERSION_RE = /^\d+\.\d+(?:\.\d+)?$/;
|
|
18
33
|
export const IDENTIFIER_RE = /^[A-Za-z0-9.-]+$/;
|
package/src-node/templates.js
CHANGED
|
@@ -15,21 +15,34 @@ export function translationTemplate(widgetName) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function indexTemplate(widgetName) {
|
|
18
|
-
|
|
18
|
+
// The widget title and the first property group share the widget name so the group
|
|
19
|
+
// merges with the default settings panel. tr() marks both for translation.
|
|
20
|
+
const titleExpr = `tr('${escapeSingleQuoted(widgetName)}')`;
|
|
21
|
+
const groups = renderGroups([
|
|
22
|
+
{ title: titleExpr, properties: ["headline", "showSubtitle", "subtitle"] },
|
|
23
|
+
{
|
|
24
|
+
title: "tr('Widget Personalization')",
|
|
25
|
+
properties: ["textColor", "accentColor", "backgroundColor", "transparency"],
|
|
26
|
+
},
|
|
27
|
+
]);
|
|
28
|
+
return `<!DOCTYPE html>
|
|
19
29
|
<html lang="en">
|
|
20
30
|
<head>
|
|
21
|
-
<meta charset="utf-8"
|
|
22
|
-
<meta name="viewport" content="width=device-width, initial-scale=1"
|
|
23
|
-
<title>${escapeHtml(
|
|
24
|
-
<link rel="icon" href="resources/icon.svg" type="image/svg+xml"
|
|
25
|
-
<link rel="stylesheet" href="styles/main.css"
|
|
26
|
-
<meta name="x-icue-property" content=
|
|
27
|
-
<meta name="x-icue-property" content=
|
|
28
|
-
<meta name="x-icue-property" content=
|
|
29
|
-
<meta name="x-icue-property" content=
|
|
30
|
-
<meta name="x-icue-property" content=
|
|
31
|
-
<meta name="x-icue-property" content=
|
|
32
|
-
<meta name="x-icue-property" content=
|
|
31
|
+
<meta charset="utf-8" />
|
|
32
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
33
|
+
<title>${escapeHtml(titleExpr)}</title>
|
|
34
|
+
<link rel="icon" href="resources/icon.svg" type="image/svg+xml" />
|
|
35
|
+
<link rel="stylesheet" href="styles/main.css" />
|
|
36
|
+
<meta name="x-icue-property" content="headline" data-label="tr('Headline')" data-type="textfield" data-default="'Hello World'" />
|
|
37
|
+
<meta name="x-icue-property" content="showSubtitle" data-label="tr('Show Subtitle')" data-type="switch" data-default="true" />
|
|
38
|
+
<meta name="x-icue-property" content="subtitle" data-label="tr('Subtitle')" data-type="textfield" data-default="'Built with iCUE Widget CLI'" />
|
|
39
|
+
<meta name="x-icue-property" content="textColor" data-label="tr('Text Color')" data-type="color" data-default="'#ffffff'" />
|
|
40
|
+
<meta name="x-icue-property" content="accentColor" data-label="tr('Accent Color')" data-type="color" data-default="'#59c2ff'" />
|
|
41
|
+
<meta name="x-icue-property" content="backgroundColor" data-label="tr('Background')" data-type="color" data-default="'#111827'" />
|
|
42
|
+
<meta name="x-icue-property" content="transparency" data-label="tr('Transparency')" data-type="slider" data-default="20" data-min="0" data-max="100" data-step="1" data-unit-label="'%'" />
|
|
43
|
+
<script id="x-icue-groups" type="application/json">
|
|
44
|
+
${groups}
|
|
45
|
+
</script>
|
|
33
46
|
</head>
|
|
34
47
|
<body>
|
|
35
48
|
<main id="content">
|
|
@@ -38,6 +51,16 @@ export function indexTemplate(widgetName) {
|
|
|
38
51
|
<p id="subtitle">Built with iCUE Widget CLI</p>
|
|
39
52
|
</main>
|
|
40
53
|
<script src="scripts/main.js"></script>
|
|
54
|
+
<script>
|
|
55
|
+
icueEvents = {
|
|
56
|
+
onICUEInitialized: function(payload) { applySettings(payload && payload.settings); },
|
|
57
|
+
onDataUpdated: function(payload) { applySettings(payload && payload.settings); },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (iCUE_initialized) {
|
|
61
|
+
applySettings();
|
|
62
|
+
}
|
|
63
|
+
</script>
|
|
41
64
|
</body>
|
|
42
65
|
</html>
|
|
43
66
|
`;
|
|
@@ -115,15 +138,6 @@ function applySettings(settings = {}) {
|
|
|
115
138
|
subtitle.hidden = !state.showSubtitle;
|
|
116
139
|
}
|
|
117
140
|
|
|
118
|
-
window.icueEvents = {
|
|
119
|
-
onICUEInitialized(payload) {
|
|
120
|
-
applySettings(payload?.settings);
|
|
121
|
-
},
|
|
122
|
-
onDataUpdated(payload) {
|
|
123
|
-
applySettings(payload?.settings);
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
|
|
127
141
|
applySettings();
|
|
128
142
|
`;
|
|
129
143
|
|
|
@@ -149,3 +163,27 @@ function escapeHtml(value) {
|
|
|
149
163
|
.replaceAll(">", ">")
|
|
150
164
|
.replaceAll('"', """);
|
|
151
165
|
}
|
|
166
|
+
|
|
167
|
+
// Escape a value for embedding inside a single-quoted JavaScript string, e.g. tr('...').
|
|
168
|
+
function escapeSingleQuoted(value) {
|
|
169
|
+
return String(value).replaceAll("\\", "\\\\").replaceAll("'", "\\'");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Render the x-icue-groups JSON body. Each group's title/info are already valid JS
|
|
173
|
+
// expressions (e.g. tr('...')) and are serialized as JSON strings; properties stay inline.
|
|
174
|
+
function renderGroups(groups) {
|
|
175
|
+
const body = groups
|
|
176
|
+
.map((group) => {
|
|
177
|
+
const props = group.properties.map((name) => JSON.stringify(name)).join(", ");
|
|
178
|
+
const lines = [
|
|
179
|
+
" {",
|
|
180
|
+
` "title": ${JSON.stringify(group.title)},`,
|
|
181
|
+
` "properties": [${props}]${group.info ? "," : ""}`,
|
|
182
|
+
];
|
|
183
|
+
if (group.info) lines.push(` "info": ${JSON.stringify(group.info)}`);
|
|
184
|
+
lines.push(" }");
|
|
185
|
+
return lines.join("\n");
|
|
186
|
+
})
|
|
187
|
+
.join(",\n");
|
|
188
|
+
return ` [\n${body}\n ]`;
|
|
189
|
+
}
|
package/src-node/validation.js
CHANGED
|
@@ -4,6 +4,7 @@ import vm from "node:vm";
|
|
|
4
4
|
import {
|
|
5
5
|
ALLOWED_DEVICE_TYPES,
|
|
6
6
|
ALLOWED_PLATFORMS,
|
|
7
|
+
ALLOWED_PROPERTY_TYPES,
|
|
7
8
|
APP_VERSION_RE,
|
|
8
9
|
IDENTIFIER_RE,
|
|
9
10
|
SEMVER_RE,
|
|
@@ -92,6 +93,10 @@ async function validateIndex(root, warnings) {
|
|
|
92
93
|
if (!/rel=["'][^"']*icon/i.test(html)) warnings.push("index.html does not reference a favicon/icon.");
|
|
93
94
|
if (!html.includes("icueEvents")) warnings.push("index.html or scripts do not reference icueEvents.");
|
|
94
95
|
|
|
96
|
+
validateHeadWellFormed(html);
|
|
97
|
+
validatePropertyTypes(html);
|
|
98
|
+
validatePropertyGroups(html);
|
|
99
|
+
|
|
95
100
|
const assetRefs = [...extractAttributes(html, "href"), ...extractAttributes(html, "src")];
|
|
96
101
|
for (const ref of assetRefs) {
|
|
97
102
|
if (isRemote(ref) || ref.startsWith("data:") || ref.startsWith("#")) continue;
|
|
@@ -101,6 +106,81 @@ async function validateIndex(root, warnings) {
|
|
|
101
106
|
}
|
|
102
107
|
}
|
|
103
108
|
|
|
109
|
+
// iCUE imports a widget by parsing <head> with a strict XML parser (Qt QDomDocument).
|
|
110
|
+
// HTML5 void elements written without a self-closing slash (e.g. `<meta charset="utf-8">`)
|
|
111
|
+
// are well-formed HTML but invalid XML: the parser never closes the tag, fails at </head>,
|
|
112
|
+
// and reports "Missing Title Element" because <title> is never reached. Catch it here so
|
|
113
|
+
// the widget fails validation instead of silently producing a package iCUE cannot import.
|
|
114
|
+
const VOID_ELEMENTS = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"];
|
|
115
|
+
|
|
116
|
+
function validateHeadWellFormed(html) {
|
|
117
|
+
const headMatch = /<head\b[^>]*>([\s\S]*?)<\/head>/i.exec(html);
|
|
118
|
+
if (!headMatch) return;
|
|
119
|
+
const head = headMatch[1]
|
|
120
|
+
.replace(/<!--[\s\S]*?-->/g, "")
|
|
121
|
+
.replace(/<script\b[\s\S]*?<\/script>/gi, "")
|
|
122
|
+
.replace(/<style\b[\s\S]*?<\/style>/gi, "");
|
|
123
|
+
const re = new RegExp(`<(${VOID_ELEMENTS.join("|")})\\b[^>]*?>`, "gi");
|
|
124
|
+
let match;
|
|
125
|
+
while ((match = re.exec(head))) {
|
|
126
|
+
if (!match[0].endsWith("/>")) {
|
|
127
|
+
throw new ValidationError(
|
|
128
|
+
`index.html <head> has a non-self-closed <${match[1].toLowerCase()}> element: ${match[0].trim()}. ` +
|
|
129
|
+
`iCUE parses the head as XML on import, so void elements must self-close (for example <${match[1].toLowerCase()} ... />). ` +
|
|
130
|
+
`Otherwise iCUE rejects the widget with "Missing Title Element".`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Each x-icue-property meta declares a control via data-type. iCUE rejects the widget
|
|
137
|
+
// with "Invalid meta parameter data type" if the value is not a known control type, so
|
|
138
|
+
// catch unknown types (e.g. "text" instead of "textfield", "boolean" instead of "switch")
|
|
139
|
+
// at validation time instead of on import.
|
|
140
|
+
function validatePropertyTypes(html) {
|
|
141
|
+
const metaRe = /<meta\b[^>]*\bname=["']x-icue-property["'][^>]*>/gi;
|
|
142
|
+
let meta;
|
|
143
|
+
while ((meta = metaRe.exec(html))) {
|
|
144
|
+
const tag = meta[0];
|
|
145
|
+
const typeMatch = /\bdata-type=["']([^"']*)["']/i.exec(tag);
|
|
146
|
+
if (!typeMatch) {
|
|
147
|
+
throw new ValidationError(`x-icue-property is missing a data-type attribute: ${tag.trim()}`);
|
|
148
|
+
}
|
|
149
|
+
const type = typeMatch[1].trim();
|
|
150
|
+
if (!ALLOWED_PROPERTY_TYPES.has(type)) {
|
|
151
|
+
throw new ValidationError(
|
|
152
|
+
`x-icue-property has an invalid data-type "${type}". ` +
|
|
153
|
+
`iCUE accepts: ${[...ALLOWED_PROPERTY_TYPES].join(", ")}. ` +
|
|
154
|
+
`(For example use "textfield" for text input and "switch" for a boolean toggle.)`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// The x-icue-groups script organizes property controls into settings panels. iCUE
|
|
161
|
+
// JSON-parses it, so malformed JSON or an empty group silently drops the panel. Catch
|
|
162
|
+
// it here. Group title/info are JS expressions (e.g. tr('...')) but are always JSON
|
|
163
|
+
// strings, so the block as a whole stays valid JSON.
|
|
164
|
+
function validatePropertyGroups(html) {
|
|
165
|
+
const match = /<script\b[^>]*\bid=["']x-icue-groups["'][^>]*>([\s\S]*?)<\/script>/i.exec(html);
|
|
166
|
+
if (!match) return;
|
|
167
|
+
let groups;
|
|
168
|
+
try {
|
|
169
|
+
groups = JSON.parse(match[1]);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
throw new ValidationError(`x-icue-groups is not valid JSON: ${error.message}`);
|
|
172
|
+
}
|
|
173
|
+
if (!Array.isArray(groups)) throw new ValidationError("x-icue-groups must be a JSON array of groups.");
|
|
174
|
+
groups.forEach((group, index) => {
|
|
175
|
+
if (!group || typeof group !== "object" || Array.isArray(group)) {
|
|
176
|
+
throw new ValidationError(`x-icue-groups[${index}] must be an object.`);
|
|
177
|
+
}
|
|
178
|
+
if (!Array.isArray(group.properties) || group.properties.length === 0) {
|
|
179
|
+
throw new ValidationError(`x-icue-groups[${index}] must list at least one property.`);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
104
184
|
function* extractAttributes(html, attribute) {
|
|
105
185
|
const re = new RegExp(`${attribute}=["']([^"']+)["']`, "gi");
|
|
106
186
|
let match;
|