@gtkx/runtime 1.2.2 → 1.4.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.
Files changed (101) hide show
  1. package/README.md +6 -4
  2. package/dist/arg.d.ts +6 -1
  3. package/dist/arg.d.ts.map +1 -1
  4. package/dist/arg.js +2 -1
  5. package/dist/arg.js.map +1 -1
  6. package/dist/closure.d.ts.map +1 -1
  7. package/dist/closure.js +2 -11
  8. package/dist/closure.js.map +1 -1
  9. package/dist/descriptors.d.ts +21 -3
  10. package/dist/descriptors.d.ts.map +1 -1
  11. package/dist/descriptors.js +50 -21
  12. package/dist/descriptors.js.map +1 -1
  13. package/dist/field.d.ts +42 -0
  14. package/dist/field.d.ts.map +1 -0
  15. package/dist/field.js +41 -0
  16. package/dist/field.js.map +1 -0
  17. package/dist/fn.d.ts +2 -0
  18. package/dist/fn.d.ts.map +1 -1
  19. package/dist/fn.js +50 -10
  20. package/dist/fn.js.map +1 -1
  21. package/dist/index.d.ts +14 -6
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +13 -5
  24. package/dist/index.js.map +1 -1
  25. package/dist/mixin.d.ts +2 -1
  26. package/dist/mixin.d.ts.map +1 -1
  27. package/dist/mixin.js +1 -1
  28. package/dist/mixin.js.map +1 -1
  29. package/dist/native-value.d.ts +9 -3
  30. package/dist/native-value.d.ts.map +1 -1
  31. package/dist/native-value.js +86 -9
  32. package/dist/native-value.js.map +1 -1
  33. package/dist/object.d.ts +14 -6
  34. package/dist/object.d.ts.map +1 -1
  35. package/dist/object.js +21 -10
  36. package/dist/object.js.map +1 -1
  37. package/dist/param-spec.d.ts +32 -1
  38. package/dist/param-spec.d.ts.map +1 -1
  39. package/dist/param-spec.js +67 -5
  40. package/dist/param-spec.js.map +1 -1
  41. package/dist/promisify.d.ts +13 -1
  42. package/dist/promisify.d.ts.map +1 -1
  43. package/dist/promisify.js +14 -1
  44. package/dist/promisify.js.map +1 -1
  45. package/dist/properties.d.ts +48 -5
  46. package/dist/properties.d.ts.map +1 -1
  47. package/dist/properties.js +191 -20
  48. package/dist/properties.js.map +1 -1
  49. package/dist/regex.d.ts +25 -0
  50. package/dist/regex.d.ts.map +1 -0
  51. package/dist/regex.js +59 -0
  52. package/dist/regex.js.map +1 -0
  53. package/dist/register-class.d.ts +165 -15
  54. package/dist/register-class.d.ts.map +1 -1
  55. package/dist/register-class.js +175 -19
  56. package/dist/register-class.js.map +1 -1
  57. package/dist/registry.d.ts +48 -4
  58. package/dist/registry.d.ts.map +1 -1
  59. package/dist/registry.js +147 -3
  60. package/dist/registry.js.map +1 -1
  61. package/dist/signal.d.ts +11 -1
  62. package/dist/signal.d.ts.map +1 -1
  63. package/dist/signal.js +66 -6
  64. package/dist/signal.js.map +1 -1
  65. package/dist/t.d.ts +11 -0
  66. package/dist/t.d.ts.map +1 -1
  67. package/dist/t.js +3 -0
  68. package/dist/t.js.map +1 -1
  69. package/dist/type.d.ts +2 -1
  70. package/dist/type.d.ts.map +1 -1
  71. package/dist/type.js +5 -1
  72. package/dist/type.js.map +1 -1
  73. package/dist/value.d.ts +48 -4
  74. package/dist/value.d.ts.map +1 -1
  75. package/dist/value.js +172 -16
  76. package/dist/value.js.map +1 -1
  77. package/dist/variant.d.ts +75 -26
  78. package/dist/variant.d.ts.map +1 -1
  79. package/dist/variant.js +44 -29
  80. package/dist/variant.js.map +1 -1
  81. package/package.json +5 -5
  82. package/src/arg.ts +6 -1
  83. package/src/closure.ts +2 -14
  84. package/src/descriptors.ts +74 -21
  85. package/src/field.ts +76 -0
  86. package/src/fn.ts +73 -17
  87. package/src/index.ts +46 -5
  88. package/src/mixin.ts +1 -1
  89. package/src/native-value.ts +146 -14
  90. package/src/object.ts +23 -11
  91. package/src/param-spec.ts +96 -8
  92. package/src/promisify.ts +27 -1
  93. package/src/properties.ts +280 -32
  94. package/src/regex.ts +92 -0
  95. package/src/register-class.ts +485 -42
  96. package/src/registry.ts +219 -4
  97. package/src/signal.ts +117 -4
  98. package/src/t.ts +13 -0
  99. package/src/type.ts +7 -0
  100. package/src/value.ts +268 -14
  101. package/src/variant.ts +184 -53
package/src/properties.ts CHANGED
@@ -1,24 +1,35 @@
1
1
  import type { ExternalObject, Handle, RegisterClassProperty } from "@gtkx/native";
2
2
  import type { AnyClass } from "@gtkx/utils";
3
- import { camelCase, kebabCase } from "@gtkx/utils";
3
+ import { camelCase, kebabCase, toCamelIdentifier } from "@gtkx/utils";
4
4
  import { bind } from "./bind.js";
5
- import { biguint64T, stringT, structT, voidT } from "./descriptors.js";
5
+ import { biguint64T, fundamentalT, refT, sizedArrayT, stringT, structT, uint32T, voidT } from "./descriptors.js";
6
6
  import { LIB, PARAM_T, VALUE_T } from "./library.js";
7
7
  import {
8
8
  getParamFlags,
9
9
  getParamValueType,
10
10
  isParamConstructOnly,
11
+ isParamExplicitlyNotified,
11
12
  isParamLaxlyValidated,
13
+ isParamReadable,
12
14
  isParamWritable,
13
15
  type ValueGuard,
14
16
  valueGuardFor,
15
17
  wasParamValueModified,
18
+ WHOLE_NUMBER_RANGES,
16
19
  } from "./param-spec.js";
17
- import { getHandle, getInterfaceProperties, instanceClassName, type InterfaceProperty } from "./registry.js";
18
- import { typeName } from "./type.js";
20
+ import {
21
+ getClassType,
22
+ getHandle,
23
+ getInstanceType,
24
+ getInterfaceProperties,
25
+ instanceClassName,
26
+ type InterfaceProperty,
27
+ } from "./registry.js";
28
+ import { TYPE_INTERFACE, TYPE_INVALID, TYPE_OBJECT, typeFundamental, typeIsA, typeName } from "./type.js";
19
29
  import {
20
30
  fromValue,
21
31
  newValueForType,
32
+ valueGuardOverrideFor,
22
33
  type ValueNarrower,
23
34
  valueNarrowerFor,
24
35
  type ValueWriter,
@@ -37,14 +48,19 @@ type PropertyCheck = {
37
48
  scratch?: ExternalObject<Handle>;
38
49
  };
39
50
 
40
- type PropertyAccessor = PropertyCheck & {
41
- memberName: string;
51
+ type AccessorBase = PropertyCheck & {
42
52
  storage: symbol;
43
- hasGeneratedMember: boolean;
44
- isInterfaceProperty?: boolean;
45
53
  delegate?: InterfaceProperty;
46
54
  };
47
55
 
56
+ type DeclaredAccessor = AccessorBase & {
57
+ isInterfaceProperty?: false;
58
+ memberName: string;
59
+ hasGeneratedMember: boolean;
60
+ };
61
+
62
+ type InterfaceAccessor = AccessorBase & { isInterfaceProperty: true };
63
+ type PropertyAccessor = DeclaredAccessor | InterfaceAccessor;
48
64
  /** A `GObject.ParamSpec` wrapper describing one property's name, type, flags and default. */
49
65
  type PropertySpec = object;
50
66
 
@@ -72,13 +88,34 @@ const SET_PROPERTY_VFUNC = "vfuncSetProperty";
72
88
  const READ_ONLY_REASON = "the property is read-only";
73
89
  const CONSTRUCT_ONLY_REASON = "the property can only be set when the object is constructed";
74
90
  const CLASS_T = structT("borrowed");
91
+ const CANONICAL_NAME_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
92
+ const DECLARED_NAMES = Symbol("gtkx:declaredPropertyNames");
93
+
94
+ const OVERRIDE_PARAM_T = fundamentalT(LIB, "g_param_spec_ref_sink", "g_param_spec_unref", {
95
+ ownership: "borrowed",
96
+ typeName: "GParam",
97
+ });
98
+
75
99
  const interfaceStorages: Map<string, symbol> = new Map();
76
100
  const paramSpecDefaultValue = bind(LIB, "g_param_spec_get_default_value", [PARAM_T], VALUE_T);
77
101
  const paramSpecName = bind(LIB, "g_param_spec_get_name", [PARAM_T], stringT("borrowed"));
78
102
  const typeClassRef = bind(LIB, "g_type_class_ref", [biguint64T], CLASS_T);
79
103
  const typeClassUnref = bind(LIB, "g_type_class_unref", [CLASS_T], voidT);
104
+ const coercionChecks: Map<bigint, Map<string, PropertyCheck | null>> = new Map();
80
105
  const classFindProperty = bind(LIB, "g_object_class_find_property", [CLASS_T, stringT("borrowed")], PARAM_T);
81
106
 
107
+ const classListProperties = bind(
108
+ LIB,
109
+ "g_object_class_list_properties",
110
+ [CLASS_T, refT(uint32T)],
111
+ sizedArrayT(PARAM_T, 1, "full"),
112
+ );
113
+
114
+ const defaultInterfaceRef = bind(LIB, "g_type_default_interface_ref", [biguint64T], CLASS_T);
115
+ const defaultInterfaceUnref = bind(LIB, "g_type_default_interface_unref", [CLASS_T], voidT);
116
+ const interfaceFindProperty = bind(LIB, "g_object_interface_find_property", [CLASS_T, stringT("borrowed")], PARAM_T);
117
+ const paramSpecOverrideNew = bind(LIB, "g_param_spec_override", [stringT("borrowed"), PARAM_T], OVERRIDE_PARAM_T);
118
+
82
119
  const getPropertyName = (pspec: PropertySpec): string => paramSpecName(getHandle(pspec)) as string;
83
120
  const underscoreCase = (name: string): string => name.replaceAll("-", "_");
84
121
  const canonicalCase = (name: string): string => kebabCase(name).replaceAll("_", "-");
@@ -102,17 +139,85 @@ function checkFor(handle: ExternalObject<Handle>, name: string): PropertyCheck {
102
139
  handle,
103
140
  flags: getParamFlags(handle),
104
141
  valueType,
105
- canHoldValue: valueGuardFor(valueType),
142
+ canHoldValue: valueGuardOverrideFor(valueType) ?? valueGuardFor(valueType),
106
143
  narrowValue: valueNarrowerFor(valueType),
107
144
  };
108
145
  }
109
146
 
147
+ function accessorSpecs(klass: ExternalObject<Handle>): Map<string, ExternalObject<Handle>> {
148
+ const specs: Map<string, ExternalObject<Handle>> = new Map();
149
+ const countRef = { value: 0 };
150
+
151
+ for (const pspec of classListProperties(klass, countRef) as ExternalObject<Handle>[]) {
152
+ specs.set(toCamelIdentifier(paramSpecName(pspec) as string), pspec);
153
+ }
154
+
155
+ return specs;
156
+ }
157
+
110
158
  function findPropertySpec(klass: ExternalObject<Handle>, name: string): ExternalObject<Handle> | null {
111
- const found = classFindProperty(klass, name) as ExternalObject<Handle> | null;
159
+ const found = (classFindProperty(klass, name) ??
160
+ classFindProperty(klass, canonicalCase(name))) as ExternalObject<Handle> | null;
112
161
 
113
- return found ?? (classFindProperty(klass, canonicalCase(name)) as ExternalObject<Handle> | null);
162
+ return found ?? accessorSpecs(klass).get(toCamelIdentifier(name)) ?? null;
114
163
  }
115
164
 
165
+ const sourceLabel = (source: bigint | AnyClass, gtype: bigint): string =>
166
+ typeName(gtype) ?? (typeof source === "function" ? source.name : String(source));
167
+
168
+ const findInterfaceSpec = (gtype: bigint, name: string): ExternalObject<Handle> | null => {
169
+ const vtable = defaultInterfaceRef(gtype) as ExternalObject<Handle>;
170
+
171
+ try {
172
+ return interfaceFindProperty(vtable, name) as ExternalObject<Handle> | null;
173
+ } finally {
174
+ defaultInterfaceUnref(vtable);
175
+ }
176
+ };
177
+
178
+ const findClassSpec = (gtype: bigint, name: string): ExternalObject<Handle> | null => {
179
+ const klass = typeClassRef(gtype) as ExternalObject<Handle>;
180
+
181
+ try {
182
+ return classFindProperty(klass, name) as ExternalObject<Handle> | null;
183
+ } finally {
184
+ typeClassUnref(klass);
185
+ }
186
+ };
187
+
188
+ const findSourceSpec = (source: bigint | AnyClass, name: string): ExternalObject<Handle> => {
189
+ const gtype = typeof source === "bigint" ? source : getClassType(source);
190
+ const isInterface = typeIsA(gtype, TYPE_INTERFACE);
191
+
192
+ if (!isInterface && !typeIsA(gtype, TYPE_OBJECT)) {
193
+ throw new TypeError(
194
+ `paramSpecOverride: '${sourceLabel(source, gtype)}' is neither a registered object class nor an interface`,
195
+ );
196
+ }
197
+
198
+ const pspec = isInterface ? findInterfaceSpec(gtype, name) : findClassSpec(gtype, name);
199
+
200
+ if (pspec === null) {
201
+ throw new TypeError(`paramSpecOverride: type '${sourceLabel(source, gtype)}' has no property named '${name}'`);
202
+ }
203
+
204
+ return pspec;
205
+ };
206
+
207
+ /**
208
+ * Creates the handle of a `GParamSpec` that redirects to the property `source` declares under
209
+ * `name` — the equivalent of `g_param_spec_override`, which introspection does not expose.
210
+ *
211
+ * Throws when `source` is neither a GType nor a registered class or interface, and when it
212
+ * declares no property under `name`.
213
+ *
214
+ * @param name Canonical name of the property to override.
215
+ * @param source The GType, wrapper class or interface declaring the property.
216
+ * @returns Handle of the override spec.
217
+ */
218
+ const newParamSpecOverride = (name: string, source: bigint | AnyClass): ExternalObject<Handle> =>
219
+ paramSpecOverrideNew(name, findSourceSpec(source, name)) as ExternalObject<Handle>;
220
+
116
221
  function describeValue(value: unknown): string {
117
222
  if (typeof value === "string") {
118
223
  return JSON.stringify(value);
@@ -213,6 +318,113 @@ function constructValueFor(wrapper: object, check: PropertyCheck, value: unknown
213
318
  return { name: check.propertyName, value: checkedValueFor(wrapper, check, value) };
214
319
  }
215
320
 
321
+ /**
322
+ * Whether the type declares a readable GObject property under `propertyName`, asking GObject itself
323
+ * rather than looking for a generated accessor, which codegen omits when a method shares the name.
324
+ */
325
+ function isReadableProperty(gtype: bigint, propertyName: string): boolean {
326
+ const klass = typeClassRef(gtype) as ExternalObject<Handle>;
327
+
328
+ try {
329
+ const pspec = findPropertySpec(klass, propertyName);
330
+
331
+ return pspec !== null && isParamReadable(getParamFlags(pspec));
332
+ } finally {
333
+ typeClassUnref(klass);
334
+ }
335
+ }
336
+
337
+ function lookupCoercionCheck(gtype: bigint, name: string): PropertyCheck | null {
338
+ const klass = typeClassRef(gtype) as ExternalObject<Handle>;
339
+
340
+ try {
341
+ const pspec = findPropertySpec(klass, name);
342
+
343
+ return pspec === null ? null : checkFor(pspec, name);
344
+ } finally {
345
+ typeClassUnref(klass);
346
+ }
347
+ }
348
+
349
+ function coercionCheckFor(gtype: bigint, name: string): PropertyCheck | null {
350
+ let checks = coercionChecks.get(gtype);
351
+
352
+ if (checks === undefined) {
353
+ checks = new Map();
354
+ coercionChecks.set(gtype, checks);
355
+ }
356
+
357
+ let check = checks.get(name);
358
+
359
+ if (check === undefined) {
360
+ check = lookupCoercionCheck(gtype, name);
361
+ checks.set(name, check);
362
+ }
363
+
364
+ return check;
365
+ }
366
+
367
+ function truncateToWhole(check: PropertyCheck, value: number): number {
368
+ const range = WHOLE_NUMBER_RANGES.get(typeFundamental(check.valueType));
369
+
370
+ if (range === undefined) {
371
+ return value;
372
+ }
373
+
374
+ return Math.min(range[1], Math.max(range[0], Math.trunc(value)));
375
+ }
376
+
377
+ function validatedNumber(check: PropertyCheck, number: number): number {
378
+ check.scratch ??= newValueForType(check.valueType);
379
+
380
+ try {
381
+ writeHeld(check, check.scratch, number);
382
+ } catch {
383
+ return number;
384
+ }
385
+
386
+ return wasParamValueModified(check.handle, check.scratch) ? (fromValue(check.scratch) as number) : number;
387
+ }
388
+
389
+ function coerceNumber(check: PropertyCheck, value: number): number {
390
+ const number = truncateToWhole(check, value);
391
+
392
+ return check.canHoldValue(number) ? validatedNumber(check, number) : value;
393
+ }
394
+
395
+ function coercePropertyValue(gtype: bigint, propertyName: string, value: unknown): unknown {
396
+ if (typeof value !== "number" || !Number.isFinite(value)) {
397
+ return value;
398
+ }
399
+
400
+ const check = coercionCheckFor(gtype, propertyName);
401
+
402
+ return check === null ? value : coerceNumber(check, value);
403
+ }
404
+
405
+ /**
406
+ * Fits a number to what a GObject property accepts, the way an animation writing a value on every
407
+ * frame needs: a fractional number headed for a property that holds whole numbers, such as a
408
+ * `gint`, an enum, or flags, is truncated toward zero, and a number outside the range the
409
+ * property's `GObject.ParamSpec` allows is clamped to it, so the write that follows never trips
410
+ * GObject's range check. Anything that is not a finite number, a number the property cannot hold
411
+ * at all, and a name the object installs no property under come back unchanged.
412
+ *
413
+ * @param obj The object the value is about to be written to.
414
+ * @param propertyName The property name, dashed or camelCased.
415
+ * @param value The value about to be written.
416
+ * @returns The value as the property accepts it.
417
+ */
418
+ function coerceObjectProperty(obj: object, propertyName: string, value: unknown): unknown {
419
+ if (typeof value !== "number") {
420
+ return value;
421
+ }
422
+
423
+ const gtype = getInstanceType(obj);
424
+
425
+ return gtype === TYPE_INVALID ? value : coercePropertyValue(gtype, propertyName, value);
426
+ }
427
+
216
428
  function constructPropertyFor(
217
429
  gtype: bigint,
218
430
  name: string,
@@ -268,13 +480,13 @@ function storedGetter(accessor: PropertyAccessor): (this: object) => unknown {
268
480
  };
269
481
  }
270
482
 
271
- function memberGetter(accessor: PropertyAccessor): (this: object) => unknown {
483
+ function memberGetter(accessor: DeclaredAccessor): (this: object) => unknown {
272
484
  return function get(this: object) {
273
485
  return (this as Record<string, unknown>)[accessor.memberName];
274
486
  };
275
487
  }
276
488
 
277
- function memberSetter(accessor: PropertyAccessor): (this: object, value: unknown) => void {
489
+ function memberSetter(accessor: DeclaredAccessor): (this: object, value: unknown) => void {
278
490
  return function set(this: object, value: unknown) {
279
491
  (this as Record<string, unknown>)[accessor.memberName] = value;
280
492
  };
@@ -286,7 +498,7 @@ function refusalFor(accessor: PropertyAccessor, reason: string): (this: object,
286
498
  };
287
499
  }
288
500
 
289
- function checkedSetter(accessor: PropertyAccessor): (this: object, value: unknown) => void {
501
+ function checkedSetter(accessor: DeclaredAccessor): (this: object, value: unknown) => void {
290
502
  if (!isParamWritable(accessor.flags)) {
291
503
  return refusalFor(accessor, READ_ONLY_REASON);
292
504
  }
@@ -312,7 +524,7 @@ function defineAccessor(prototype: object, descriptor: PropertyDescriptor, alias
312
524
  Object.defineProperty(prototype, alias, descriptor);
313
525
  }
314
526
 
315
- function installAccessors(klass: AnyClass, accessor: PropertyAccessor): void {
527
+ function installAccessors(klass: AnyClass, accessor: DeclaredAccessor): void {
316
528
  const prototype = (klass as { prototype: object }).prototype;
317
529
  accessor.hasGeneratedMember = !hasOwnMember(prototype, accessor.memberName);
318
530
 
@@ -341,16 +553,14 @@ function storageFor(propertyName: string): symbol {
341
553
  return created;
342
554
  }
343
555
 
344
- function interfaceAccessorFor(dispatch: PropertyDispatch, pspec: PropertySpec): PropertyAccessor {
556
+ function interfaceAccessorFor(dispatch: PropertyDispatch, pspec: PropertySpec): InterfaceAccessor {
345
557
  const handle = getHandle(pspec);
346
558
  const propertyName = paramSpecName(handle) as string;
347
559
  const delegate = dispatch.delegates.get(propertyName);
348
560
 
349
- const accessor: PropertyAccessor = {
561
+ const accessor: InterfaceAccessor = {
350
562
  ...checkFor(handle, propertyName),
351
- memberName: camelCase(propertyName),
352
563
  storage: storageFor(propertyName),
353
- hasGeneratedMember: false,
354
564
  isInterfaceProperty: true,
355
565
  };
356
566
 
@@ -388,7 +598,7 @@ function callMember(instance: object, member: string, args: unknown[]): unknown
388
598
  return (fn as (...values: unknown[]) => unknown).apply(instance, args);
389
599
  }
390
600
 
391
- function backingMemberFor(instance: object, accessor: PropertyAccessor): string | undefined {
601
+ function backingMemberFor(instance: object, accessor: DeclaredAccessor): string | undefined {
392
602
  if (accessor.hasGeneratedMember && !Object.hasOwn(instance, accessor.memberName)) {
393
603
  return undefined;
394
604
  }
@@ -414,6 +624,14 @@ function readCurrent(instance: object, accessor: PropertyAccessor): unknown {
414
624
  : (instance as Record<string, unknown>)[member];
415
625
  }
416
626
 
627
+ function storeCurrent(instance: Record<symbol, unknown>, accessor: PropertyAccessor, value: unknown): void {
628
+ if (isParamExplicitlyNotified(accessor.flags)) {
629
+ writeStored(instance, accessor, value);
630
+ } else {
631
+ storeValue(instance, accessor, value);
632
+ }
633
+ }
634
+
417
635
  function writeCurrent(instance: object, accessor: PropertyAccessor, value: unknown): void {
418
636
  const setter = accessor.delegate?.setter;
419
637
 
@@ -432,7 +650,7 @@ function writeCurrent(instance: object, accessor: PropertyAccessor, value: unkno
432
650
  const member = backingMemberFor(instance, accessor);
433
651
 
434
652
  if (member === undefined) {
435
- storeValue(instance as Record<symbol, unknown>, accessor, value);
653
+ storeCurrent(instance as Record<symbol, unknown>, accessor, value);
436
654
 
437
655
  return;
438
656
  }
@@ -456,16 +674,17 @@ function makeSetProperty(dispatch: PropertyDispatch) {
456
674
  };
457
675
  }
458
676
 
459
- function assertCanonicalName(klass: AnyClass, name: string, propertyName: string): void {
460
- const canonical = canonicalCase(name);
677
+ const isCanonicalName = (name: string, propertyName: string): boolean =>
678
+ CANONICAL_NAME_PATTERN.test(propertyName) && toCamelIdentifier(propertyName) === toCamelIdentifier(name);
461
679
 
462
- if (propertyName === canonical) {
680
+ function assertCanonicalName(klass: AnyClass, name: string, propertyName: string): void {
681
+ if (isCanonicalName(name, propertyName)) {
463
682
  return;
464
683
  }
465
684
 
466
685
  throw new TypeError(
467
686
  `registerClass: ${klass.name} keys the property '${name}' to a GObject.ParamSpec named ` +
468
- `'${propertyName}', which is the name GObject notifies under; name the ParamSpec '${canonical}'`,
687
+ `'${propertyName}', which is the name GObject notifies under; name the ParamSpec '${canonicalCase(name)}'`,
469
688
  );
470
689
  }
471
690
 
@@ -475,8 +694,8 @@ function assertCanonicalNames(klass: AnyClass, properties: Record<string, Proper
475
694
  }
476
695
  }
477
696
 
478
- function buildAccessor(klass: AnyClass, name: string, pspec: PropertySpec): PropertyAccessor {
479
- const accessor: PropertyAccessor = {
697
+ function buildAccessor(klass: AnyClass, name: string, pspec: PropertySpec): DeclaredAccessor {
698
+ const accessor: DeclaredAccessor = {
480
699
  ...checkFor(getHandle(pspec), name),
481
700
  memberName: camelCase(name),
482
701
  storage: Symbol(`gtkx:property:${name}`),
@@ -488,7 +707,7 @@ function buildAccessor(klass: AnyClass, name: string, pspec: PropertySpec): Prop
488
707
  return accessor;
489
708
  }
490
709
 
491
- function buildAccessors(source: PropertyDispatchSource): PropertyAccessor[] {
710
+ function buildAccessors(source: PropertyDispatchSource): DeclaredAccessor[] {
492
711
  const { klass, properties } = source;
493
712
  assertCanonicalNames(klass, properties);
494
713
 
@@ -515,11 +734,35 @@ function interfaceDelegatesFor(adoptedTypes: bigint[]): Map<string, InterfacePro
515
734
  return delegates;
516
735
  }
517
736
 
737
+ function recordDeclaredNames(klass: AnyClass, accessors: DeclaredAccessor[]): void {
738
+ const proto = klass.prototype as Record<PropertyKey, unknown>;
739
+ const inherited = proto[DECLARED_NAMES] as Record<string, string> | undefined;
740
+ const own: Record<string, string> = {};
741
+
742
+ for (const accessor of accessors) {
743
+ own[accessor.memberName] = accessor.propertyName;
744
+ }
745
+
746
+ Object.defineProperty(proto, DECLARED_NAMES, { value: { ...inherited, ...own }, enumerable: false });
747
+ }
748
+
518
749
  function buildPropertyDispatch(source: PropertyDispatchSource): PropertyDispatch {
519
- return {
520
- accessors: buildAccessors(source),
521
- delegates: interfaceDelegatesFor(source.adoptedTypes),
522
- };
750
+ const declared = buildAccessors(source);
751
+ recordDeclaredNames(source.klass, declared);
752
+
753
+ return { accessors: [...declared], delegates: interfaceDelegatesFor(source.adoptedTypes) };
754
+ }
755
+
756
+ /**
757
+ * The GObject name a class registered with `registerClass` installed an accessor under, or
758
+ * `undefined` when the accessor names no property the class declared itself.
759
+ */
760
+ function getDeclaredPropertyName(object: object, accessor: string): string | undefined {
761
+ const declared = (object as Record<PropertyKey, unknown>)[DECLARED_NAMES] as
762
+ | Record<string, string> |
763
+ undefined;
764
+
765
+ return declared?.[accessor];
523
766
  }
524
767
 
525
768
  function toNativeProperties(properties: Record<string, PropertySpec>): RegisterClassProperty[] {
@@ -531,10 +774,15 @@ function toNativeProperties(properties: Record<string, PropertySpec>): RegisterC
531
774
 
532
775
  export {
533
776
  buildPropertyDispatch,
777
+ coerceObjectProperty,
778
+ getDeclaredPropertyName,
779
+ isReadableProperty,
780
+ coercePropertyValue,
534
781
  constructPropertyFor,
535
782
  GET_PROPERTY_VFUNC,
536
783
  makeGetProperty,
537
784
  makeSetProperty,
785
+ newParamSpecOverride,
538
786
  SET_PROPERTY_VFUNC,
539
787
  toNativeProperties,
540
788
  type ConstructProperty,
package/src/regex.ts ADDED
@@ -0,0 +1,92 @@
1
+ import { type Arg } from "./arg.js";
2
+ import { booleanT, boxedT, bufferT, int32T, int64T, uint32T } from "./descriptors.js";
3
+ import { fn } from "./fn.js";
4
+ import { LIB } from "./library.js";
5
+ import { getHandle } from "./registry.js";
6
+
7
+ type MatchParams = {
8
+ regex: object;
9
+ subject: string | string[];
10
+ startPosition: number;
11
+ matchOptions: number;
12
+ };
13
+
14
+ const MATCH_ARGS: Arg[] = [
15
+ {
16
+ type: boxedT("GRegex", { ownership: "borrowed", sharedLibrary: LIB, getTypeFnName: "g_regex_get_type" }),
17
+ },
18
+ { type: bufferT },
19
+ { type: int64T },
20
+ { type: int32T },
21
+ { type: uint32T },
22
+ {
23
+ type: boxedT("GMatchInfo", { ownership: "full", sharedLibrary: LIB, getTypeFnName: "g_match_info_get_type" }),
24
+ direction: "out",
25
+ },
26
+ ];
27
+
28
+ const gRegexMatchFull = fn(LIB, "g_regex_match_full", { args: MATCH_ARGS, returns: booleanT, canThrow: true });
29
+ const gRegexMatchAllFull = fn(LIB, "g_regex_match_all_full", { args: MATCH_ARGS, returns: booleanT, canThrow: true });
30
+ /* eslint-disable-next-line sonarjs/no-unused-collection -- keeps each subject's bytes alive alongside its match info */
31
+ const subjectBytes: WeakMap<object, Uint8Array> = new WeakMap();
32
+ const encoder = new TextEncoder();
33
+
34
+ const flattenSubject = (subject: string | string[]): string =>
35
+ Array.isArray(subject) ? subject.join("") : subject;
36
+
37
+ const matchWithSubject = <MatchInfo extends object>(
38
+ boundFn: (...inputs: unknown[]) => unknown,
39
+ { regex, subject, startPosition, matchOptions }: MatchParams,
40
+ ): [boolean, MatchInfo] => {
41
+ const bytes = encoder.encode(`${flattenSubject(subject)}\0`);
42
+
43
+ const result = boundFn(getHandle(regex), bytes, bytes.length - 1, startPosition, matchOptions) as [
44
+ boolean,
45
+ MatchInfo,
46
+ ];
47
+
48
+ subjectBytes.set(result[1], bytes);
49
+
50
+ return result;
51
+ };
52
+
53
+ /**
54
+ * Scans for a match of a compiled `GRegex` in the subject string, keeping the subject's bytes
55
+ * alive so the returned match info's fetch methods stay valid for as long as it is reachable.
56
+ *
57
+ * @param regex The compiled regex to scan with.
58
+ * @param subject The string to scan for matches.
59
+ * @param startPosition Starting index of the subject to match, in bytes.
60
+ * @param matchOptions Match options to apply.
61
+ * @returns Whether the subject matched, and the match info describing the match.
62
+ */
63
+ function matchRegex<MatchInfo extends object>(
64
+ regex: object,
65
+ subject: string | string[],
66
+ startPosition: number,
67
+ matchOptions: number,
68
+ ): [boolean, MatchInfo] {
69
+ return matchWithSubject(gRegexMatchFull, { regex, subject, startPosition, matchOptions });
70
+ }
71
+
72
+ /**
73
+ * Scans for all possible matches of a compiled `GRegex` in the subject string using the DFA
74
+ * algorithm, including overlapping matches, keeping the subject's bytes alive so the returned
75
+ * match info's fetch methods stay valid for as long as it is reachable.
76
+ *
77
+ * @param regex The compiled regex to scan with.
78
+ * @param subject The string to scan for matches.
79
+ * @param startPosition Starting index of the subject to match, in bytes.
80
+ * @param matchOptions Match options to apply.
81
+ * @returns Whether the subject matched, and the match info describing the matches.
82
+ */
83
+ function matchAllRegex<MatchInfo extends object>(
84
+ regex: object,
85
+ subject: string | string[],
86
+ startPosition: number,
87
+ matchOptions: number,
88
+ ): [boolean, MatchInfo] {
89
+ return matchWithSubject(gRegexMatchAllFull, { regex, subject, startPosition, matchOptions });
90
+ }
91
+
92
+ export { matchAllRegex, matchRegex };