glass-easel-devtools-agent 0.9.0 → 0.10.2

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.
@@ -20,7 +20,6 @@ export type AgentRequestKind = {
20
20
  removeGlassEaselStyleSheetProperty: RemoveGlassEaselStyleSheetProperty;
21
21
  replaceGlassEaselStyleSheetProperty: ReplaceGlassEaselStyleSheetProperty;
22
22
  replaceGlassEaselStyleSheetAllProperties: ReplaceGlassEaselStyleSheetAllProperties;
23
- replaceGlassEaselStyleSheetInlineStyle: ReplaceGlassEaselStyleSheetInlineStyle;
24
23
  };
25
24
  export type StyleSheetId = string;
26
25
  export type CSSNameValue = {
@@ -43,7 +42,7 @@ export type CSSStyle = {
43
42
  cssText?: string;
44
43
  };
45
44
  export type CSSRule = {
46
- styleSheetId?: StyleSheetId;
45
+ styleSheetId: StyleSheetId;
47
46
  selectorList: {
48
47
  selectors: {
49
48
  text: string;
@@ -56,6 +55,7 @@ export type CSSRule = {
56
55
  text: string;
57
56
  }[];
58
57
  inactive?: boolean;
58
+ ruleIndex: number;
59
59
  };
60
60
  export type CSSMatchedRule = {
61
61
  rule: CSSRule;
@@ -116,6 +116,7 @@ export interface GetMatchedStylesForNode extends RequestResponse {
116
116
  */
117
117
  export interface AddGlassEaselStyleSheetRule extends RequestResponse {
118
118
  request: {
119
+ nodeId: NodeId;
119
120
  mediaQueryText: string;
120
121
  selector: string;
121
122
  };
@@ -126,15 +127,19 @@ export interface AddGlassEaselStyleSheetRule extends RequestResponse {
126
127
  export interface GetGlassEaselStyleSheetIndexForNewRules extends RequestResponse {
127
128
  request: Record<string, never>;
128
129
  response: {
130
+ nodeId: NodeId;
129
131
  styleSheetId: StyleSheetId;
130
132
  };
131
133
  }
132
134
  /**
133
135
  * Clear a CSS rule.
136
+ *
137
+ * `styleSheetId = undefined` refers to the inline styles.
134
138
  */
135
139
  export interface ResetGlassEaselStyleSheetRule extends RequestResponse {
136
140
  request: {
137
- styleSheetId: StyleSheetId;
141
+ nodeId: NodeId;
142
+ styleSheetId?: StyleSheetId;
138
143
  ruleIndex: number;
139
144
  };
140
145
  }
@@ -143,6 +148,7 @@ export interface ResetGlassEaselStyleSheetRule extends RequestResponse {
143
148
  */
144
149
  export interface ModifyGlassEaselStyleSheetRuleSelector extends RequestResponse {
145
150
  request: {
151
+ nodeId: NodeId;
146
152
  styleSheetId: StyleSheetId;
147
153
  ruleIndex: number;
148
154
  selector: string;
@@ -150,20 +156,26 @@ export interface ModifyGlassEaselStyleSheetRuleSelector extends RequestResponse
150
156
  }
151
157
  /**
152
158
  * Add a new CSS property.
159
+ *
160
+ * `styleSheetId = undefined` refers to the inline styles.
153
161
  */
154
162
  export interface AddGlassEaselStyleSheetProperty extends RequestResponse {
155
163
  request: {
156
- styleSheetId: StyleSheetId;
164
+ nodeId: NodeId;
165
+ styleSheetId?: StyleSheetId;
157
166
  ruleIndex: number;
158
167
  styleText: string;
159
168
  };
160
169
  }
161
170
  /**
162
171
  * Set the disabled status of a new CSS property.
172
+ *
173
+ * `styleSheetId = undefined` refers to the inline styles.
163
174
  */
164
175
  export interface SetGlassEaselStyleSheetPropertyDisabled extends RequestResponse {
165
176
  request: {
166
- styleSheetId: StyleSheetId;
177
+ nodeId: NodeId;
178
+ styleSheetId?: StyleSheetId;
167
179
  ruleIndex: number;
168
180
  propertyIndex: number;
169
181
  disabled: boolean;
@@ -171,20 +183,26 @@ export interface SetGlassEaselStyleSheetPropertyDisabled extends RequestResponse
171
183
  }
172
184
  /**
173
185
  * Remove a CSS property.
186
+ *
187
+ * `styleSheetId = undefined` refers to the inline styles.
174
188
  */
175
189
  export interface RemoveGlassEaselStyleSheetProperty extends RequestResponse {
176
190
  request: {
177
- styleSheetId: StyleSheetId;
191
+ nodeId: NodeId;
192
+ styleSheetId?: StyleSheetId;
178
193
  ruleIndex: number;
179
194
  propertyIndex: number;
180
195
  };
181
196
  }
182
197
  /**
183
198
  * Replace a CSS property.
199
+ *
200
+ * `styleSheetId = undefined` refers to the inline styles.
184
201
  */
185
202
  export interface ReplaceGlassEaselStyleSheetProperty extends RequestResponse {
186
203
  request: {
187
- styleSheetId: StyleSheetId;
204
+ nodeId: NodeId;
205
+ styleSheetId?: StyleSheetId;
188
206
  ruleIndex: number;
189
207
  propertyIndex: number;
190
208
  styleText: string;
@@ -192,20 +210,14 @@ export interface ReplaceGlassEaselStyleSheetProperty extends RequestResponse {
192
210
  }
193
211
  /**
194
212
  * Replace all CSS properties.
213
+ *
214
+ * `styleSheetId = undefined` refers to the inline styles.
195
215
  */
196
216
  export interface ReplaceGlassEaselStyleSheetAllProperties extends RequestResponse {
197
- request: {
198
- styleSheetId: StyleSheetId;
199
- ruleIndex: number;
200
- styleText: string;
201
- };
202
- }
203
- /**
204
- * Replace inline style for a node.
205
- */
206
- export interface ReplaceGlassEaselStyleSheetInlineStyle extends RequestResponse {
207
217
  request: {
208
218
  nodeId: NodeId;
219
+ styleSheetId?: StyleSheetId;
220
+ ruleIndex: number;
209
221
  styleText: string;
210
222
  };
211
223
  }
@@ -16,6 +16,7 @@ export type AgentRequestKind = {
16
16
  pushNodesByBackendIdsToFrontend: PushNodesByBackendIdsToFrontend;
17
17
  removeAttribute: RemoveAttribute;
18
18
  setAttributeValue: SetAttributeValue;
19
+ setGlassEaselClassList: SetGlassEaselClassList;
19
20
  setAttributesAsText: SetAttributesAsText;
20
21
  getAttributes: GetAttributes;
21
22
  getGlassEaselAttributes: GetGlassEaselAttributes;
@@ -161,9 +162,30 @@ export interface SetAttributeValue extends RequestResponse {
161
162
  nodeId: NodeId;
162
163
  name: string;
163
164
  value: string;
165
+ nameType?: 'auto' | 'normal-attribute' | 'property' | 'external-class';
164
166
  };
165
167
  cdpRequestResponse: [Protocol.DOM.SetAttributeValueRequest, unknown];
166
168
  }
169
+ /**
170
+ * Set an attribute.
171
+ */
172
+ export interface SetGlassEaselClassList extends RequestResponse {
173
+ request: {
174
+ nodeId: NodeId;
175
+ externalClass?: string;
176
+ classes: {
177
+ className: string;
178
+ disabled?: boolean;
179
+ }[];
180
+ };
181
+ response: {
182
+ classes: {
183
+ className: string;
184
+ disabled?: boolean;
185
+ }[];
186
+ };
187
+ cdpRequestResponse: [unknown, unknown];
188
+ }
167
189
  /**
168
190
  * Set attributes as text.
169
191
  *
@@ -205,7 +227,10 @@ export interface GetGlassEaselAttributes extends RequestResponse {
205
227
  virtual: boolean;
206
228
  is: string;
207
229
  id: string;
208
- class: string;
230
+ classes: {
231
+ className: string;
232
+ disabled?: boolean;
233
+ }[];
209
234
  slot: string;
210
235
  slotName: string | undefined;
211
236
  slotValues: {
@@ -229,7 +254,10 @@ export interface GetGlassEaselAttributes extends RequestResponse {
229
254
  }[];
230
255
  externalClasses?: {
231
256
  name: string;
232
- value: string;
257
+ value: {
258
+ className: string;
259
+ disabled?: boolean;
260
+ }[];
233
261
  }[];
234
262
  dataset: {
235
263
  name: string;
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "glass-easel-devtools-agent",
3
- "version": "0.9.0",
3
+ "version": "0.10.2",
4
4
  "main": "dist/index.js",
5
5
  "peerDependencies": {
6
- "glass-easel": "^0.9.0"
6
+ "glass-easel": "^0.10.2"
7
7
  },
8
8
  "devDependencies": {
9
9
  "@csstools/css-tokenizer": "^2.4.1",
10
10
  "@csstools/selector-specificity": "^3.1.1",
11
- "@types/node": "^20.14.9",
11
+ "@types/node": "^20.16.5",
12
12
  "devtools-protocol": "^0.0.1319565",
13
- "glass-easel": "^0.9.0",
14
- "glass-easel-template-compiler": "^0.9.0",
15
- "postcss-selector-parser": "^6.1.1"
13
+ "glass-easel": "^0.10.2",
14
+ "glass-easel-template-compiler": "^0.10.2",
15
+ "postcss-selector-parser": "^6.1.2"
16
16
  },
17
17
  "scripts": {
18
18
  "build": "webpack --config webpack.config.js",
package/src/backend.ts CHANGED
@@ -3,7 +3,13 @@
3
3
  import * as glassEasel from 'glass-easel'
4
4
  import parser from 'postcss-selector-parser'
5
5
  import { selectorSpecificity, compare as selectorCompare } from '@csstools/selector-specificity'
6
- import { tokenize, TokenType, stringify } from '@csstools/css-tokenizer'
6
+ import {
7
+ tokenize,
8
+ TokenType,
9
+ stringify,
10
+ type CSSToken,
11
+ type TokenString,
12
+ } from '@csstools/css-tokenizer'
7
13
  import { backendUnsupported } from './utils'
8
14
 
9
15
  export type BoundingClientRect = glassEasel.BoundingClientRect
@@ -141,42 +147,91 @@ export const getBoxModel = (
141
147
  })
142
148
  }
143
149
 
150
+ const expectToken = (
151
+ token: CSSToken | undefined,
152
+ type: TokenType,
153
+ name?: string,
154
+ ): string | null => {
155
+ if (token === undefined) return null
156
+ if (token[0] !== type) return null
157
+ if (name === undefined) return token[1]
158
+ if (name === token[1]) return name
159
+ return null
160
+ }
161
+
162
+ const parseNameValueStr = (cssText: string) => {
163
+ const ret: { name: string; value: string; disabled: boolean }[] = []
164
+ const tokens = tokenize({ css: cssText })
165
+ if (tokens.length === 0) return null
166
+ let nameStart = 0
167
+ for (let i = 0; i < tokens.length; i += 1) {
168
+ const t = tokens[i]
169
+ if (expectToken(t, TokenType.Colon)) {
170
+ const name = stringify(...tokens.slice(nameStart, i)).trim()
171
+ i += 1
172
+ const valueStart = i
173
+ for (; i < tokens.length; i += 1) {
174
+ const t = tokens[i]
175
+ if (expectToken(t, TokenType.Semicolon)) break
176
+ }
177
+ const value = stringify(...tokens.slice(valueStart, i)).trim()
178
+ ret.push({ name, value, disabled: false })
179
+ nameStart = i + 1
180
+ }
181
+ }
182
+ return ret
183
+ }
184
+
144
185
  export const getMatchedRules = (
145
- ctx: glassEasel.GeneralBackendContext,
146
- elem: glassEasel.GeneralBackendElement,
186
+ element: glassEasel.Element,
147
187
  ): Promise<{
148
188
  inline: glassEasel.CSSProperty[]
149
189
  inlineText?: string
150
190
  rules: glassEasel.CSSRule[]
151
191
  crossOriginFailing?: boolean
152
192
  }> => {
153
- const parseNameValueStr = (cssText: string) => {
154
- const ret: { name: string; value: string }[] = []
155
- const tokens = tokenize({ css: cssText })
156
- if (tokens.length === 0) return null
157
- let nameStart = 0
158
- for (let i = 0; i < tokens.length; i += 1) {
159
- const t = tokens[i]
160
- if (t[0] === TokenType.Colon) {
161
- const name = stringify(...tokens.slice(nameStart, i))
162
- i += 1
163
- const valueStart = i
164
- for (; i < tokens.length; i += 1) {
165
- const t = tokens[i]
166
- if (t[0] === TokenType.Semicolon) break
193
+ const convertAdapterGeneratedRules = (rules: glassEasel.CSSRule[]) => {
194
+ rules.forEach((rule) => {
195
+ const tokens = tokenize({ css: rule.selector })
196
+ if (
197
+ expectToken(tokens[0], TokenType.OpenSquare) &&
198
+ expectToken(tokens[1], TokenType.Ident, 'wx-host') &&
199
+ expectToken(tokens[2], TokenType.Delim, '=') &&
200
+ expectToken(tokens[3], TokenType.String) &&
201
+ expectToken(tokens[4], TokenType.CloseSquare) &&
202
+ expectToken(tokens[5], TokenType.EOF) !== null
203
+ ) {
204
+ // convert host rules
205
+ rule.selector = ':host'
206
+ rule.styleScope = (tokens[3] as TokenString)[4].value
207
+ return
208
+ }
209
+ for (let i = 0; i < tokens.length; i += 1) {
210
+ // convert class prefixes
211
+ const t = tokens[i]
212
+ if (expectToken(t, TokenType.Delim, '.')) {
213
+ const peek = expectToken(tokens[i + 1], TokenType.Ident)
214
+ if (peek) {
215
+ i += 1
216
+ const [prefix, name] = peek.split('--', 2)
217
+ if (name !== undefined) {
218
+ rule.styleScope = prefix
219
+ tokens[i][1] = name
220
+ }
221
+ }
167
222
  }
168
- const value = stringify(...tokens.slice(valueStart, i))
169
- ret.push({ name, value })
170
- nameStart = i + 1
171
223
  }
172
- }
173
- return ret
224
+ rule.selector = stringify(...tokens)
225
+ })
174
226
  }
175
- const calcRuleWeight = (rules: glassEasel.CSSRule[]): glassEasel.CSSRule[] => {
227
+
228
+ const calcRuleWeight = (
229
+ ctx: glassEasel.GeneralBackendContext,
230
+ rules: glassEasel.CSSRule[],
231
+ ): glassEasel.CSSRule[] => {
176
232
  const rulesWithSelector = rules.map((rule) => {
177
- if (rule.propertyText) {
178
- rule.properties = parseNameValueStr(rule.propertyText) ?? rule.properties
179
- }
233
+ const edit = styleEditContext.createOrGetRule(ctx, rule, rule.propertyText)
234
+ rule.properties = edit.getProps()
180
235
  const ps = parser().astSync(rule.selector)
181
236
  const specificity = selectorSpecificity(ps)
182
237
  return [specificity, rule] as const
@@ -197,15 +252,22 @@ export const getMatchedRules = (
197
252
  })
198
253
  return rulesWithSelector.map(([_sel, rule]) => rule).reverse()
199
254
  }
255
+
200
256
  return new Promise((resolve) => {
257
+ const ctx = element.getBackendContext()
258
+ const elem = element.getBackendElement()
259
+ if (!ctx || !elem) {
260
+ resolve({ inline: [], rules: [] })
261
+ return
262
+ }
201
263
  if (ctx.mode === glassEasel.BackendMode.Domlike) {
202
264
  if (typeof ctx.getMatchedRules === 'function') {
203
265
  try {
204
266
  ctx.getMatchedRules(elem as glassEasel.domlikeBackend.Element, (ret) => {
205
- ret.rules = calcRuleWeight(ret.rules)
206
- if (ret.inlineText) {
207
- ret.inline = parseNameValueStr(ret.inlineText) ?? ret.inline
208
- }
267
+ convertAdapterGeneratedRules(ret.rules)
268
+ ret.rules = calcRuleWeight(ctx, ret.rules)
269
+ const edit = styleEditContext.createOrGetInline(elem, ret.inline, ret.inlineText)
270
+ ret.inline = edit.getProps()
209
271
  resolve(ret)
210
272
  })
211
273
  } catch (err) {
@@ -222,7 +284,9 @@ export const getMatchedRules = (
222
284
  } else {
223
285
  if ('getMatchedRules' in elem) {
224
286
  ;(elem as glassEasel.backend.Element).getMatchedRules!((ret) => {
225
- ret.rules = calcRuleWeight(ret.rules)
287
+ ret.rules = calcRuleWeight(ctx, ret.rules)
288
+ const edit = styleEditContext.createOrGetInline(elem, ret.inline, ret.inlineText)
289
+ ret.inline = edit.getProps()
226
290
  resolve(ret)
227
291
  })
228
292
  } else {
@@ -231,3 +295,243 @@ export const getMatchedRules = (
231
295
  }
232
296
  })
233
297
  }
298
+
299
+ const filterCssProperties = (props: glassEasel.CSSProperty[]) => {
300
+ return props.map((prop) => ({
301
+ name: prop.name.trim(),
302
+ value: prop.value.trim(),
303
+ disabled: false,
304
+ important: prop.important,
305
+ }))
306
+ }
307
+
308
+ export class StyleRuleEdit {
309
+ private props: { name: string; value: string; disabled: boolean; important?: boolean }[]
310
+
311
+ constructor(props: glassEasel.CSSProperty[]) {
312
+ this.props = filterCssProperties(props)
313
+ }
314
+
315
+ static fromMaybeInlineStyle(props: glassEasel.CSSProperty[], inlineStyle?: string) {
316
+ if (inlineStyle === undefined) return new StyleRuleEdit(props)
317
+ const ret = new StyleRuleEdit([])
318
+ ret.props = parseNameValueStr(inlineStyle) ?? []
319
+ return ret
320
+ }
321
+
322
+ updateWithProps(props: glassEasel.CSSProperty[], inlineStyle?: string) {
323
+ const oldProps = this.props
324
+ this.props =
325
+ inlineStyle === undefined ? filterCssProperties(props) : parseNameValueStr(inlineStyle) ?? []
326
+ let i = 0
327
+ oldProps.forEach((prop) => {
328
+ if (prop.disabled) {
329
+ this.props.splice(i, 0, prop)
330
+ i += 1
331
+ }
332
+ while (i < this.props.length) {
333
+ i += 1
334
+ if (prop.name === this.props[i - 1].name) {
335
+ break
336
+ }
337
+ }
338
+ })
339
+ }
340
+
341
+ getProps() {
342
+ return this.props.slice()
343
+ }
344
+
345
+ clear() {
346
+ this.props.length = 0
347
+ }
348
+
349
+ countProps() {
350
+ return this.props.length
351
+ }
352
+
353
+ append(inlineStyle: string) {
354
+ const list = parseNameValueStr(inlineStyle) ?? []
355
+ this.props.push(...list)
356
+ }
357
+
358
+ setDisabled(index: number, disabled: boolean): boolean {
359
+ if (index >= this.props.length) return false
360
+ this.props[index].disabled = disabled
361
+ return true
362
+ }
363
+
364
+ remove(index: number): boolean {
365
+ if (index >= this.props.length) return false
366
+ this.props.splice(index, 1)
367
+ return true
368
+ }
369
+
370
+ replace(index: number, inlineStyle: string): boolean {
371
+ if (index >= this.props.length) return false
372
+ const list = parseNameValueStr(inlineStyle) ?? []
373
+ this.props.splice(index, 1, ...list)
374
+ return true
375
+ }
376
+
377
+ stringify(): string {
378
+ return this.props
379
+ .map(({ name, value, important, disabled }) => {
380
+ if (disabled) return ''
381
+ if (important) return `${name}: ${value} !important;\n`
382
+ return `${name}: ${value};\n`
383
+ })
384
+ .join('')
385
+ }
386
+ }
387
+
388
+ class StyleEditContext {
389
+ inlineStyleMap = new WeakMap<glassEasel.GeneralBackendElement, StyleRuleEdit>()
390
+ ruleMap = new WeakMap<glassEasel.GeneralBackendContext, Record<string, StyleRuleEdit>>()
391
+
392
+ createOrGetInline(
393
+ elem: glassEasel.GeneralBackendElement,
394
+ properties: glassEasel.CSSProperty[],
395
+ inlineStyle?: string,
396
+ ): StyleRuleEdit {
397
+ const r = this.inlineStyleMap.get(elem)
398
+ if (r) {
399
+ r.updateWithProps(properties, inlineStyle)
400
+ return r
401
+ }
402
+ const edit = StyleRuleEdit.fromMaybeInlineStyle(properties, inlineStyle)
403
+ this.inlineStyleMap.set(elem, edit)
404
+ return edit
405
+ }
406
+
407
+ createOrGetRule(
408
+ ctx: glassEasel.GeneralBackendContext,
409
+ rule: glassEasel.CSSRule,
410
+ inlineStyle?: string,
411
+ ): StyleRuleEdit {
412
+ const key = `${rule.sheetIndex}/${rule.ruleIndex}`
413
+ const map = this.ruleMap.get(ctx)
414
+ if (!map) {
415
+ const map = Object.create(null) as Record<string, StyleRuleEdit>
416
+ map[key] = StyleRuleEdit.fromMaybeInlineStyle(rule.properties, inlineStyle)
417
+ this.ruleMap.set(ctx, map)
418
+ return map[key]
419
+ }
420
+ if (!map[key]) {
421
+ map[key] = StyleRuleEdit.fromMaybeInlineStyle(rule.properties, inlineStyle)
422
+ return map[key]
423
+ }
424
+ map[key].updateWithProps(rule.properties, inlineStyle)
425
+ return map[key]
426
+ }
427
+
428
+ updateInline(
429
+ ctx: glassEasel.GeneralBackendContext,
430
+ elem: glassEasel.GeneralBackendElement,
431
+ f: (edit: StyleRuleEdit) => void,
432
+ ) {
433
+ const edit = this.inlineStyleMap.get(elem)
434
+ if (!edit) return
435
+ f(edit)
436
+ const style = edit.stringify()
437
+ if (ctx.mode === glassEasel.BackendMode.Domlike) {
438
+ ;(elem as glassEasel.domlikeBackend.Element).setAttribute('style', style)
439
+ } else if (ctx.mode === glassEasel.BackendMode.Composed) {
440
+ ;(elem as glassEasel.composedBackend.Element).setStyle(style)
441
+ } else if (ctx.mode === glassEasel.BackendMode.Shadow) {
442
+ ;(elem as glassEasel.backend.Element).setStyle(style)
443
+ }
444
+ }
445
+
446
+ updateRule(
447
+ ctx: glassEasel.GeneralBackendContext,
448
+ sheetIndex: number,
449
+ ruleIndex: number,
450
+ f: (edit: StyleRuleEdit) => void,
451
+ ): Promise<void> {
452
+ const key = `${sheetIndex}/${ruleIndex}`
453
+ const edit = this.ruleMap.get(ctx)?.[key]
454
+ if (!edit) return Promise.resolve()
455
+ f(edit)
456
+ const style = edit.stringify()
457
+ return new Promise((resolve) => {
458
+ ctx.replaceStyleSheetAllProperties?.(sheetIndex, ruleIndex, style, () => {
459
+ resolve()
460
+ })
461
+ })
462
+ }
463
+ }
464
+
465
+ export const styleEditContext = new StyleEditContext()
466
+
467
+ export class ClassListEdit {
468
+ private list: { className: string; disabled: boolean }[] = []
469
+
470
+ update(names: string[]) {
471
+ const oldList = this.list
472
+ this.list = names.map((className) => ({ className, disabled: false }))
473
+ let i = 0
474
+ oldList.forEach((item) => {
475
+ if (item.disabled) {
476
+ this.list.splice(i, 0, item)
477
+ i += 1
478
+ }
479
+ while (i < this.list.length) {
480
+ i += 1
481
+ if (item.className === this.list[i - 1].className) {
482
+ break
483
+ }
484
+ }
485
+ })
486
+ return this
487
+ }
488
+
489
+ setDisabled(className: string, disabled: boolean) {
490
+ const item = this.list.find((x) => x.className === className)
491
+ if (item) item.disabled = disabled
492
+ }
493
+
494
+ getClasses() {
495
+ return this.list.slice()
496
+ }
497
+
498
+ setClasses(list: { className: string; disabled?: boolean }[]) {
499
+ this.list = []
500
+ list.forEach(({ className, disabled }) => {
501
+ className.split(/\s+/g).forEach((className) => {
502
+ if (!className) return
503
+ this.list.push({ className, disabled: !!disabled })
504
+ })
505
+ })
506
+ }
507
+
508
+ stringify() {
509
+ return this.list
510
+ .filter((x) => !x.disabled)
511
+ .map((x) => x.className)
512
+ .join(' ')
513
+ }
514
+
515
+ matches(v: string): boolean {
516
+ return this.stringify() === v
517
+ }
518
+ }
519
+
520
+ export class ClassEditContext {
521
+ map = new WeakMap<glassEasel.Element, Record<string, ClassListEdit>>()
522
+
523
+ createOrGet(external: string, elem: glassEasel.Element): ClassListEdit {
524
+ if (!this.map.get(elem)) {
525
+ const group = Object.create(null) as Record<string, ClassListEdit>
526
+ this.map.set(elem, group)
527
+ }
528
+ const group = this.map.get(elem)!
529
+ const key = external ?? ''
530
+ if (!group[key]) {
531
+ group[key] = new ClassListEdit()
532
+ }
533
+ return group[key]
534
+ }
535
+ }
536
+
537
+ export const classEditContext = new ClassEditContext()