@lessonkit/themes 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -36,6 +36,7 @@ __export(index_exports, {
36
36
  lightTheme: () => lightTheme,
37
37
  mergeThemes: () => mergeThemes,
38
38
  radiusVarName: () => radiusVarName,
39
+ sanitizeCssCustomPropertyValue: () => sanitizeCssCustomPropertyValue,
39
40
  shadowVarName: () => shadowVarName,
40
41
  spacingVarName: () => spacingVarName,
41
42
  themeToCssDeclarationBlock: () => themeToCssDeclarationBlock,
@@ -68,9 +69,20 @@ var TYPOGRAPHY_KEYS = [
68
69
  ];
69
70
  var RADIUS_KEYS = ["sm", "md", "lg"];
70
71
  var SHADOW_KEYS = ["sm", "md", "lg"];
72
+ var EXTRA_COLOR_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
71
73
  function isNonEmptyString(v) {
72
74
  return typeof v === "string" && v.trim().length > 0;
73
75
  }
76
+ function isSafeCssCustomPropertyValue(value) {
77
+ return !/[;}\r\n]/.test(value) && !value.includes("/*");
78
+ }
79
+ function validateCssTokenValue(path, value, issues) {
80
+ if (!isSafeCssCustomPropertyValue(value)) {
81
+ issues.push({ path, message: "must not contain CSS-breaking characters (;, }, newlines, or comments)" });
82
+ return false;
83
+ }
84
+ return true;
85
+ }
74
86
  function validateRequiredGroup(group, value, keys, issues) {
75
87
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
76
88
  issues.push({ path: group, message: "must be an object" });
@@ -82,7 +94,7 @@ function validateRequiredGroup(group, value, keys, issues) {
82
94
  const v = obj[key];
83
95
  if (!isNonEmptyString(v)) {
84
96
  issues.push({ path: `${group}.${key}`, message: "required non-empty string" });
85
- } else {
97
+ } else if (validateCssTokenValue(`${group}.${key}`, v, issues)) {
86
98
  out[key] = v;
87
99
  }
88
100
  }
@@ -96,9 +108,16 @@ function validateColorsExtra(value, issues) {
96
108
  }
97
109
  const extra = {};
98
110
  for (const [k, v] of Object.entries(value)) {
111
+ if (!EXTRA_COLOR_KEY_PATTERN.test(k)) {
112
+ issues.push({
113
+ path: `colors.extra.${k}`,
114
+ message: "key must match [a-zA-Z][a-zA-Z0-9_-]*"
115
+ });
116
+ continue;
117
+ }
99
118
  if (!isNonEmptyString(v)) {
100
119
  issues.push({ path: `colors.extra.${k}`, message: "must be a non-empty string" });
101
- } else {
120
+ } else if (validateCssTokenValue(`colors.extra.${k}`, v, issues)) {
102
121
  extra[k] = v;
103
122
  }
104
123
  }
@@ -204,6 +223,14 @@ function mergeThemes(base, ...overrides) {
204
223
  }
205
224
 
206
225
  // src/cssVariables.ts
226
+ function sanitizeCssCustomPropertyValue(value) {
227
+ if (/[;}\r\n]/.test(value) || value.includes("/*")) return null;
228
+ return value;
229
+ }
230
+ function assignCssVar(vars, key, value) {
231
+ const safe = sanitizeCssCustomPropertyValue(value);
232
+ if (safe !== null) vars[key] = safe;
233
+ }
207
234
  function tokenKeyToKebab(key) {
208
235
  return key.replace(/([A-Z])/g, "-$1").toLowerCase();
209
236
  }
@@ -230,23 +257,23 @@ function themeToCssVariables(theme) {
230
257
  for (const [key, value] of Object.entries(theme.colors)) {
231
258
  if (key === "extra" && value && typeof value === "object") {
232
259
  for (const [ek, ev] of Object.entries(value)) {
233
- vars[colorExtraVarName(ek)] = ev;
260
+ assignCssVar(vars, colorExtraVarName(ek), ev);
234
261
  }
235
262
  } else if (key !== "extra") {
236
- vars[colorVarName(key)] = value;
263
+ assignCssVar(vars, colorVarName(key), value);
237
264
  }
238
265
  }
239
266
  for (const [key, value] of Object.entries(theme.spacing)) {
240
- vars[spacingVarName(key)] = value;
267
+ assignCssVar(vars, spacingVarName(key), value);
241
268
  }
242
269
  for (const [key, value] of Object.entries(theme.typography)) {
243
- vars[typographyVarName(key)] = value;
270
+ assignCssVar(vars, typographyVarName(key), value);
244
271
  }
245
272
  for (const [key, value] of Object.entries(theme.radius)) {
246
- vars[radiusVarName(key)] = value;
273
+ assignCssVar(vars, radiusVarName(key), value);
247
274
  }
248
275
  for (const [key, value] of Object.entries(theme.shadows)) {
249
- vars[shadowVarName(key)] = value;
276
+ assignCssVar(vars, shadowVarName(key), value);
250
277
  }
251
278
  const sorted = {};
252
279
  for (const key of Object.keys(vars).sort()) {
@@ -492,6 +519,7 @@ function buildThemeCatalog() {
492
519
  lightTheme,
493
520
  mergeThemes,
494
521
  radiusVarName,
522
+ sanitizeCssCustomPropertyValue,
495
523
  shadowVarName,
496
524
  spacingVarName,
497
525
  themeToCssDeclarationBlock,
package/dist/index.d.cts CHANGED
@@ -53,6 +53,8 @@ declare const REQUIRED_SHADOW_KEYS: ThemeShadowKey[];
53
53
  */
54
54
  declare function mergeThemes(base: LessonkitThemeV1, ...overrides: (PartialLessonkitThemeV1 | undefined)[]): LessonkitThemeV1;
55
55
 
56
+ /** Reject values that could break out of a CSS declaration block. */
57
+ declare function sanitizeCssCustomPropertyValue(value: string): string | null;
56
58
  /** Map a token path segment to kebab-case for CSS custom property names. */
57
59
  declare function tokenKeyToKebab(key: string): string;
58
60
  /** CSS custom property name for a color token (required keys). */
@@ -93,4 +95,4 @@ type ThemeCatalogEntry = {
93
95
  /** Enumerable catalog of themeable tokens (v1). */
94
96
  declare function buildThemeCatalog(): ThemeCatalogEntry[];
95
97
 
96
- export { type LessonkitTheme, type LessonkitThemeColors, type LessonkitThemeRadius, type LessonkitThemeShadows, type LessonkitThemeSpacing, type LessonkitThemeTypography, type LessonkitThemeV1, type PartialLessonkitThemeV1, REQUIRED_COLOR_KEYS, REQUIRED_RADIUS_KEYS, REQUIRED_SHADOW_KEYS, REQUIRED_SPACING_KEYS, REQUIRED_TYPOGRAPHY_KEYS, type ThemeCatalogEntry, type ThemeColorKey, type ThemePresetName, type ThemeRadiusKey, type ThemeShadowKey, type ThemeSpacingKey, type ThemeTypographyKey, type ThemeValidationIssue, type ThemeValidationResult, brandTheme, brandThemeOverrides, buildThemeCatalog, colorExtraVarName, colorVarName, darkTheme, defaultTheme, getPresetTheme, lightTheme, mergeThemes, radiusVarName, shadowVarName, spacingVarName, themeToCssDeclarationBlock, themeToCssVariables, tokenKeyToKebab, typographyVarName, validateTheme };
98
+ export { type LessonkitTheme, type LessonkitThemeColors, type LessonkitThemeRadius, type LessonkitThemeShadows, type LessonkitThemeSpacing, type LessonkitThemeTypography, type LessonkitThemeV1, type PartialLessonkitThemeV1, REQUIRED_COLOR_KEYS, REQUIRED_RADIUS_KEYS, REQUIRED_SHADOW_KEYS, REQUIRED_SPACING_KEYS, REQUIRED_TYPOGRAPHY_KEYS, type ThemeCatalogEntry, type ThemeColorKey, type ThemePresetName, type ThemeRadiusKey, type ThemeShadowKey, type ThemeSpacingKey, type ThemeTypographyKey, type ThemeValidationIssue, type ThemeValidationResult, brandTheme, brandThemeOverrides, buildThemeCatalog, colorExtraVarName, colorVarName, darkTheme, defaultTheme, getPresetTheme, lightTheme, mergeThemes, radiusVarName, sanitizeCssCustomPropertyValue, shadowVarName, spacingVarName, themeToCssDeclarationBlock, themeToCssVariables, tokenKeyToKebab, typographyVarName, validateTheme };
package/dist/index.d.ts CHANGED
@@ -53,6 +53,8 @@ declare const REQUIRED_SHADOW_KEYS: ThemeShadowKey[];
53
53
  */
54
54
  declare function mergeThemes(base: LessonkitThemeV1, ...overrides: (PartialLessonkitThemeV1 | undefined)[]): LessonkitThemeV1;
55
55
 
56
+ /** Reject values that could break out of a CSS declaration block. */
57
+ declare function sanitizeCssCustomPropertyValue(value: string): string | null;
56
58
  /** Map a token path segment to kebab-case for CSS custom property names. */
57
59
  declare function tokenKeyToKebab(key: string): string;
58
60
  /** CSS custom property name for a color token (required keys). */
@@ -93,4 +95,4 @@ type ThemeCatalogEntry = {
93
95
  /** Enumerable catalog of themeable tokens (v1). */
94
96
  declare function buildThemeCatalog(): ThemeCatalogEntry[];
95
97
 
96
- export { type LessonkitTheme, type LessonkitThemeColors, type LessonkitThemeRadius, type LessonkitThemeShadows, type LessonkitThemeSpacing, type LessonkitThemeTypography, type LessonkitThemeV1, type PartialLessonkitThemeV1, REQUIRED_COLOR_KEYS, REQUIRED_RADIUS_KEYS, REQUIRED_SHADOW_KEYS, REQUIRED_SPACING_KEYS, REQUIRED_TYPOGRAPHY_KEYS, type ThemeCatalogEntry, type ThemeColorKey, type ThemePresetName, type ThemeRadiusKey, type ThemeShadowKey, type ThemeSpacingKey, type ThemeTypographyKey, type ThemeValidationIssue, type ThemeValidationResult, brandTheme, brandThemeOverrides, buildThemeCatalog, colorExtraVarName, colorVarName, darkTheme, defaultTheme, getPresetTheme, lightTheme, mergeThemes, radiusVarName, shadowVarName, spacingVarName, themeToCssDeclarationBlock, themeToCssVariables, tokenKeyToKebab, typographyVarName, validateTheme };
98
+ export { type LessonkitTheme, type LessonkitThemeColors, type LessonkitThemeRadius, type LessonkitThemeShadows, type LessonkitThemeSpacing, type LessonkitThemeTypography, type LessonkitThemeV1, type PartialLessonkitThemeV1, REQUIRED_COLOR_KEYS, REQUIRED_RADIUS_KEYS, REQUIRED_SHADOW_KEYS, REQUIRED_SPACING_KEYS, REQUIRED_TYPOGRAPHY_KEYS, type ThemeCatalogEntry, type ThemeColorKey, type ThemePresetName, type ThemeRadiusKey, type ThemeShadowKey, type ThemeSpacingKey, type ThemeTypographyKey, type ThemeValidationIssue, type ThemeValidationResult, brandTheme, brandThemeOverrides, buildThemeCatalog, colorExtraVarName, colorVarName, darkTheme, defaultTheme, getPresetTheme, lightTheme, mergeThemes, radiusVarName, sanitizeCssCustomPropertyValue, shadowVarName, spacingVarName, themeToCssDeclarationBlock, themeToCssVariables, tokenKeyToKebab, typographyVarName, validateTheme };
package/dist/index.js CHANGED
@@ -20,9 +20,20 @@ var TYPOGRAPHY_KEYS = [
20
20
  ];
21
21
  var RADIUS_KEYS = ["sm", "md", "lg"];
22
22
  var SHADOW_KEYS = ["sm", "md", "lg"];
23
+ var EXTRA_COLOR_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
23
24
  function isNonEmptyString(v) {
24
25
  return typeof v === "string" && v.trim().length > 0;
25
26
  }
27
+ function isSafeCssCustomPropertyValue(value) {
28
+ return !/[;}\r\n]/.test(value) && !value.includes("/*");
29
+ }
30
+ function validateCssTokenValue(path, value, issues) {
31
+ if (!isSafeCssCustomPropertyValue(value)) {
32
+ issues.push({ path, message: "must not contain CSS-breaking characters (;, }, newlines, or comments)" });
33
+ return false;
34
+ }
35
+ return true;
36
+ }
26
37
  function validateRequiredGroup(group, value, keys, issues) {
27
38
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
28
39
  issues.push({ path: group, message: "must be an object" });
@@ -34,7 +45,7 @@ function validateRequiredGroup(group, value, keys, issues) {
34
45
  const v = obj[key];
35
46
  if (!isNonEmptyString(v)) {
36
47
  issues.push({ path: `${group}.${key}`, message: "required non-empty string" });
37
- } else {
48
+ } else if (validateCssTokenValue(`${group}.${key}`, v, issues)) {
38
49
  out[key] = v;
39
50
  }
40
51
  }
@@ -48,9 +59,16 @@ function validateColorsExtra(value, issues) {
48
59
  }
49
60
  const extra = {};
50
61
  for (const [k, v] of Object.entries(value)) {
62
+ if (!EXTRA_COLOR_KEY_PATTERN.test(k)) {
63
+ issues.push({
64
+ path: `colors.extra.${k}`,
65
+ message: "key must match [a-zA-Z][a-zA-Z0-9_-]*"
66
+ });
67
+ continue;
68
+ }
51
69
  if (!isNonEmptyString(v)) {
52
70
  issues.push({ path: `colors.extra.${k}`, message: "must be a non-empty string" });
53
- } else {
71
+ } else if (validateCssTokenValue(`colors.extra.${k}`, v, issues)) {
54
72
  extra[k] = v;
55
73
  }
56
74
  }
@@ -156,6 +174,14 @@ function mergeThemes(base, ...overrides) {
156
174
  }
157
175
 
158
176
  // src/cssVariables.ts
177
+ function sanitizeCssCustomPropertyValue(value) {
178
+ if (/[;}\r\n]/.test(value) || value.includes("/*")) return null;
179
+ return value;
180
+ }
181
+ function assignCssVar(vars, key, value) {
182
+ const safe = sanitizeCssCustomPropertyValue(value);
183
+ if (safe !== null) vars[key] = safe;
184
+ }
159
185
  function tokenKeyToKebab(key) {
160
186
  return key.replace(/([A-Z])/g, "-$1").toLowerCase();
161
187
  }
@@ -182,23 +208,23 @@ function themeToCssVariables(theme) {
182
208
  for (const [key, value] of Object.entries(theme.colors)) {
183
209
  if (key === "extra" && value && typeof value === "object") {
184
210
  for (const [ek, ev] of Object.entries(value)) {
185
- vars[colorExtraVarName(ek)] = ev;
211
+ assignCssVar(vars, colorExtraVarName(ek), ev);
186
212
  }
187
213
  } else if (key !== "extra") {
188
- vars[colorVarName(key)] = value;
214
+ assignCssVar(vars, colorVarName(key), value);
189
215
  }
190
216
  }
191
217
  for (const [key, value] of Object.entries(theme.spacing)) {
192
- vars[spacingVarName(key)] = value;
218
+ assignCssVar(vars, spacingVarName(key), value);
193
219
  }
194
220
  for (const [key, value] of Object.entries(theme.typography)) {
195
- vars[typographyVarName(key)] = value;
221
+ assignCssVar(vars, typographyVarName(key), value);
196
222
  }
197
223
  for (const [key, value] of Object.entries(theme.radius)) {
198
- vars[radiusVarName(key)] = value;
224
+ assignCssVar(vars, radiusVarName(key), value);
199
225
  }
200
226
  for (const [key, value] of Object.entries(theme.shadows)) {
201
- vars[shadowVarName(key)] = value;
227
+ assignCssVar(vars, shadowVarName(key), value);
202
228
  }
203
229
  const sorted = {};
204
230
  for (const key of Object.keys(vars).sort()) {
@@ -443,6 +469,7 @@ export {
443
469
  lightTheme,
444
470
  mergeThemes,
445
471
  radiusVarName,
472
+ sanitizeCssCustomPropertyValue,
446
473
  shadowVarName,
447
474
  spacingVarName,
448
475
  themeToCssDeclarationBlock,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/themes",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "private": false,
5
5
  "description": "Theme primitives and tokens for LessonKit.",
6
6
  "license": "Apache-2.0",