@workbench-kit/monaco 0.0.2-prototype.0.2.10 → 0.0.2-prototype.0.2.11

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workbench-kit/monaco",
3
- "version": "0.0.2-prototype.0.2.10",
3
+ "version": "0.0.2-prototype.0.2.11",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
package/src/index.ts CHANGED
@@ -14,15 +14,28 @@ export {
14
14
  export {
15
15
  MONACO_DARK_THEME_ID,
16
16
  MONACO_LIGHT_THEME_ID,
17
+ buildDefaultMonacoTokenRules,
17
18
  buildMonacoThemeColors,
18
19
  defineMonacoWorkbenchTheme,
20
+ defineOrUpdateWorkbenchMonacoTheme,
21
+ getWorkbenchMonacoTokenRules,
19
22
  getWorkbenchThemeAppearanceSignature,
23
+ mergeMonacoTokenRules,
24
+ monacoRulesFromTokenColors,
20
25
  monacoThemeForWorkspaceTheme,
21
26
  readWorkbenchThemeColors,
22
27
  resolveMonacoThemeRoot,
28
+ setWorkbenchMonacoTokenRules,
29
+ toMonacoTokenColor,
23
30
  withAlpha,
31
+ buildWorkbenchMonacoThemeInput,
32
+ type DefineMonacoWorkbenchThemeOptions,
33
+ type MonacoTokenRule,
24
34
  type MonacoWorkbenchResolvedTheme,
35
+ type MonacoWorkbenchThemeBase,
36
+ type WorkbenchMonacoThemeInput,
25
37
  type WorkbenchThemeCssColors,
38
+ type WorkbenchTokenColorSetting,
26
39
  } from './monacoWorkbenchTheme.js';
27
40
  export { useMonacoWorkbenchThemeSync } from './useMonacoWorkbenchThemeSync.js';
28
41
  export { configureWorkspaceEditorTypeScriptDiagnostics } from './workspaceTypeScriptDiagnostics.js';
@@ -5,6 +5,35 @@ export const MONACO_LIGHT_THEME_ID = 'workbench-kit-light';
5
5
 
6
6
  export type MonacoWorkbenchResolvedTheme = 'dark' | 'light';
7
7
 
8
+ export type MonacoWorkbenchThemeBase = 'vs' | 'vs-dark' | 'hc-black';
9
+
10
+ /** Monaco `editor.defineTheme` token rule (syntax highlighting). */
11
+ export interface MonacoTokenRule {
12
+ readonly token: string;
13
+ readonly foreground?: string;
14
+ readonly background?: string;
15
+ readonly fontStyle?: string;
16
+ }
17
+
18
+ /**
19
+ * VS Code–compatible `tokenColors` entry (TextMate scope settings).
20
+ * Hosts may pass theme JSON `tokenColors` through `monacoRulesFromTokenColors`.
21
+ */
22
+ export interface WorkbenchTokenColorSetting {
23
+ readonly scope?: string | readonly string[];
24
+ readonly settings?: {
25
+ readonly foreground?: string;
26
+ readonly background?: string;
27
+ readonly fontStyle?: string;
28
+ };
29
+ }
30
+
31
+ export interface WorkbenchMonacoThemeInput {
32
+ readonly base: MonacoWorkbenchThemeBase;
33
+ readonly colors?: Readonly<Record<string, string>>;
34
+ readonly rules?: readonly MonacoTokenRule[];
35
+ }
36
+
8
37
  export interface WorkbenchThemeCssColors {
9
38
  accent: string;
10
39
  bg: string;
@@ -22,6 +51,17 @@ export interface WorkbenchThemeCssColors {
22
51
  textSubtle: string;
23
52
  }
24
53
 
54
+ /** Optional host-provided syntax rules merged on every workbench theme define. */
55
+ let activeHostTokenRules: readonly MonacoTokenRule[] | undefined;
56
+
57
+ export function setWorkbenchMonacoTokenRules(rules: readonly MonacoTokenRule[] | undefined): void {
58
+ activeHostTokenRules = rules;
59
+ }
60
+
61
+ export function getWorkbenchMonacoTokenRules(): readonly MonacoTokenRule[] | undefined {
62
+ return activeHostTokenRules;
63
+ }
64
+
25
65
  function readCssVariable(root: HTMLElement, variableName: string): string {
26
66
  return getComputedStyle(root).getPropertyValue(variableName).trim();
27
67
  }
@@ -132,6 +172,187 @@ export function buildMonacoThemeColors(colors: WorkbenchThemeCssColors): monaco.
132
172
  };
133
173
  }
134
174
 
175
+ /**
176
+ * Monaco token `foreground` / `background` values omit `#`.
177
+ * Invalid or empty colors are skipped by callers.
178
+ */
179
+ export function toMonacoTokenColor(color: string | undefined): string | undefined {
180
+ if (!color) {
181
+ return undefined;
182
+ }
183
+ const trimmed = color.trim();
184
+ if (!trimmed) {
185
+ return undefined;
186
+ }
187
+ const withoutHash = trimmed.startsWith('#') ? trimmed.slice(1) : trimmed;
188
+ // Drop alpha channel when present (#RRGGBBAA → RRGGBB) for token rules.
189
+ if (/^[0-9a-fA-F]{8}$/.test(withoutHash)) {
190
+ return withoutHash.slice(0, 6);
191
+ }
192
+ if (/^[0-9a-fA-F]{6}$/.test(withoutHash) || /^[0-9a-fA-F]{3}$/.test(withoutHash)) {
193
+ return withoutHash;
194
+ }
195
+ // Non-hex (rgb/hsl/var) — Monaco rules require hex; skip safely.
196
+ return undefined;
197
+ }
198
+
199
+ /**
200
+ * Default syntax rules derived from chrome CSS tokens so built-in Monaco
201
+ * languages track the active workbench palette. Hosts that load TextMate /
202
+ * grammar packs should supply richer rules via `setWorkbenchMonacoTokenRules`
203
+ * or `defineOrUpdateWorkbenchMonacoTheme`.
204
+ */
205
+ export function buildDefaultMonacoTokenRules(colors: WorkbenchThemeCssColors): MonacoTokenRule[] {
206
+ const text = toMonacoTokenColor(colors.text);
207
+ const muted = toMonacoTokenColor(colors.textMuted);
208
+ const subtle = toMonacoTokenColor(colors.textSubtle);
209
+ const accent = toMonacoTokenColor(colors.accent);
210
+ const danger = toMonacoTokenColor(colors.danger);
211
+
212
+ const rules: MonacoTokenRule[] = [];
213
+ const push = (token: string, foreground: string | undefined, fontStyle?: string) => {
214
+ if (!foreground) {
215
+ return;
216
+ }
217
+ rules.push(fontStyle ? { token, foreground, fontStyle } : { token, foreground });
218
+ };
219
+
220
+ push('comment', subtle, 'italic');
221
+ push('string', accent);
222
+ push('string.escape', muted);
223
+ push('keyword', accent);
224
+ push('keyword.flow', accent);
225
+ push('number', danger);
226
+ push('regexp', danger);
227
+ push('type', accent);
228
+ push('class', accent);
229
+ push('function', text);
230
+ push('variable', text);
231
+ push('variable.predefined', muted);
232
+ push('constant', muted);
233
+ push('delimiter', muted);
234
+ push('delimiter.html', muted);
235
+ push('tag', accent);
236
+ push('metatag', muted);
237
+ push('attribute.name', muted);
238
+ push('attribute.value', accent);
239
+ push('invalid', danger);
240
+
241
+ return rules;
242
+ }
243
+
244
+ /**
245
+ * Best-effort map from VS Code `tokenColors` to Monaco rules.
246
+ * Uses the first scope segment as the Monaco token name; unknown / invalid
247
+ * entries are skipped (safe fallback to defaults / inherit).
248
+ */
249
+ export function monacoRulesFromTokenColors(
250
+ tokenColors: readonly WorkbenchTokenColorSetting[],
251
+ ): MonacoTokenRule[] {
252
+ const rules: MonacoTokenRule[] = [];
253
+
254
+ for (const entry of tokenColors) {
255
+ const scopes = normalizeTokenScopes(entry.scope);
256
+ if (scopes.length === 0) {
257
+ continue;
258
+ }
259
+ const foreground = toMonacoTokenColor(entry.settings?.foreground);
260
+ const background = toMonacoTokenColor(entry.settings?.background);
261
+ const fontStyle = entry.settings?.fontStyle?.trim() || undefined;
262
+ if (!foreground && !background && !fontStyle) {
263
+ continue;
264
+ }
265
+
266
+ for (const scope of scopes) {
267
+ const token = scopeToMonacoToken(scope);
268
+ if (!token) {
269
+ continue;
270
+ }
271
+ rules.push({
272
+ token,
273
+ ...(foreground ? { foreground } : {}),
274
+ ...(background ? { background } : {}),
275
+ ...(fontStyle ? { fontStyle } : {}),
276
+ });
277
+ }
278
+ }
279
+
280
+ return rules;
281
+ }
282
+
283
+ function normalizeTokenScopes(scope: WorkbenchTokenColorSetting['scope']): string[] {
284
+ if (!scope) {
285
+ return [];
286
+ }
287
+ if (typeof scope === 'string') {
288
+ return scope
289
+ .split(',')
290
+ .map((part) => part.trim())
291
+ .filter(Boolean);
292
+ }
293
+ return scope.map((part) => part.trim()).filter(Boolean);
294
+ }
295
+
296
+ function scopeToMonacoToken(scope: string): string | undefined {
297
+ const trimmed = scope.trim();
298
+ if (!trimmed) {
299
+ return undefined;
300
+ }
301
+ // Prefer the leaf-most useful segment for Monaco's flatter token names.
302
+ const parts = trimmed.split('.');
303
+ if (parts[0] === 'comment') {
304
+ return 'comment';
305
+ }
306
+ if (parts[0] === 'string') {
307
+ return parts[1] === 'escape' ? 'string.escape' : 'string';
308
+ }
309
+ if (parts[0] === 'keyword') {
310
+ return parts[1] === 'control' || parts[1] === 'flow' ? 'keyword.flow' : 'keyword';
311
+ }
312
+ if (parts[0] === 'constant' && parts[1] === 'numeric') {
313
+ return 'number';
314
+ }
315
+ if (parts[0] === 'constant' && parts[1] === 'regexp') {
316
+ return 'regexp';
317
+ }
318
+ if (parts[0] === 'entity' && parts[1] === 'name' && parts[2] === 'type') {
319
+ return 'type';
320
+ }
321
+ if (parts[0] === 'entity' && parts[1] === 'name' && parts[2] === 'function') {
322
+ return 'function';
323
+ }
324
+ if (parts[0] === 'entity' && parts[1] === 'name' && parts[2] === 'tag') {
325
+ return 'tag';
326
+ }
327
+ if (parts[0] === 'entity' && parts[1] === 'other' && parts[2] === 'attribute-name') {
328
+ return 'attribute.name';
329
+ }
330
+ if (parts[0] === 'variable') {
331
+ return 'variable';
332
+ }
333
+ if (parts[0] === 'invalid') {
334
+ return 'invalid';
335
+ }
336
+ // Fall back to the raw scope so hosts using matching tokenizer names still work.
337
+ return trimmed;
338
+ }
339
+
340
+ /** Later lists win on duplicate `token` keys. */
341
+ export function mergeMonacoTokenRules(
342
+ ...groups: Array<readonly MonacoTokenRule[] | undefined>
343
+ ): MonacoTokenRule[] {
344
+ const byToken = new Map<string, MonacoTokenRule>();
345
+ for (const group of groups) {
346
+ if (!group) {
347
+ continue;
348
+ }
349
+ for (const rule of group) {
350
+ byToken.set(rule.token, rule);
351
+ }
352
+ }
353
+ return [...byToken.values()];
354
+ }
355
+
135
356
  export function resolveMonacoThemeRoot(root?: HTMLElement): HTMLElement | null {
136
357
  if (root) {
137
358
  return root;
@@ -144,24 +365,64 @@ export function resolveMonacoThemeRoot(root?: HTMLElement): HTMLElement | null {
144
365
  return document.documentElement;
145
366
  }
146
367
 
368
+ /**
369
+ * Low-level `editor.defineTheme` wrapper. Safe to call on theme switches
370
+ * without remounting editors that already use `themeId`.
371
+ */
372
+ export function defineOrUpdateWorkbenchMonacoTheme(
373
+ monacoInstance: typeof monaco,
374
+ themeId: string,
375
+ input: WorkbenchMonacoThemeInput,
376
+ ): void {
377
+ monacoInstance.editor.defineTheme(themeId, {
378
+ base: input.base,
379
+ inherit: true,
380
+ rules: [...(input.rules ?? [])],
381
+ colors: { ...(input.colors ?? {}) },
382
+ });
383
+ }
384
+
385
+ export interface DefineMonacoWorkbenchThemeOptions {
386
+ /** Extra rules merged after defaults + host registry (wins on duplicate tokens). */
387
+ readonly rules?: readonly MonacoTokenRule[];
388
+ }
389
+
390
+ /**
391
+ * Define the kit dark/light Monaco theme from live chrome CSS variables and
392
+ * optional host tokenColors rules. Re-callable when `data-theme` / preset changes.
393
+ */
394
+ export function buildWorkbenchMonacoThemeInput(
395
+ resolvedTheme: MonacoWorkbenchResolvedTheme,
396
+ cssColors: WorkbenchThemeCssColors,
397
+ options?: DefineMonacoWorkbenchThemeOptions,
398
+ ): WorkbenchMonacoThemeInput {
399
+ return {
400
+ base: resolvedTheme === 'light' ? 'vs' : 'vs-dark',
401
+ colors: buildMonacoThemeColors(cssColors) as Record<string, string>,
402
+ rules: mergeMonacoTokenRules(
403
+ buildDefaultMonacoTokenRules(cssColors),
404
+ getWorkbenchMonacoTokenRules(),
405
+ options?.rules,
406
+ ),
407
+ };
408
+ }
409
+
147
410
  export function defineMonacoWorkbenchTheme(
148
411
  monacoInstance: typeof monaco,
149
412
  resolvedTheme: MonacoWorkbenchResolvedTheme,
150
413
  root?: HTMLElement,
151
- ) {
414
+ options?: DefineMonacoWorkbenchThemeOptions,
415
+ ): void {
152
416
  const themeRoot = resolveMonacoThemeRoot(root);
153
417
  if (!themeRoot) {
154
418
  return;
155
419
  }
156
420
 
157
- const themeId = resolvedTheme === 'light' ? MONACO_LIGHT_THEME_ID : MONACO_DARK_THEME_ID;
158
-
159
- monacoInstance.editor.defineTheme(themeId, {
160
- base: resolvedTheme === 'light' ? 'vs' : 'vs-dark',
161
- inherit: true,
162
- rules: [],
163
- colors: buildMonacoThemeColors(readWorkbenchThemeColors(themeRoot)),
164
- });
421
+ defineOrUpdateWorkbenchMonacoTheme(
422
+ monacoInstance,
423
+ monacoThemeForWorkspaceTheme(resolvedTheme),
424
+ buildWorkbenchMonacoThemeInput(resolvedTheme, readWorkbenchThemeColors(themeRoot), options),
425
+ );
165
426
  }
166
427
 
167
428
  export function monacoThemeForWorkspaceTheme(theme: MonacoWorkbenchResolvedTheme) {
@@ -28,6 +28,13 @@ export function WorkbenchMonacoEditor({
28
28
  export const useMonacoWorkbenchThemeSync = () => undefined;
29
29
  export const prepareMonacoWorkbenchEditor = () => undefined;
30
30
  export const defineMonacoWorkbenchTheme = () => undefined;
31
+ export const defineOrUpdateWorkbenchMonacoTheme = () => undefined;
32
+ export const setWorkbenchMonacoTokenRules = () => undefined;
33
+ export const getWorkbenchMonacoTokenRules = () => undefined;
34
+ export const buildDefaultMonacoTokenRules = () => [];
35
+ export const monacoRulesFromTokenColors = () => [];
36
+ export const mergeMonacoTokenRules = (...groups: unknown[]) => groups.flat();
37
+ export const toMonacoTokenColor = (color: string) => color;
31
38
  export const configureWorkspaceEditorTypeScriptDiagnostics = () => undefined;
32
39
  export const monacoThemeForWorkspaceTheme = (theme: string) =>
33
40
  theme === 'light' ? MONACO_LIGHT_THEME_ID : MONACO_DARK_THEME_ID;