@gtkx/runtime 1.2.2 → 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.
Files changed (91) hide show
  1. package/README.md +2 -1
  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/fn.d.ts +2 -0
  14. package/dist/fn.d.ts.map +1 -1
  15. package/dist/fn.js +50 -10
  16. package/dist/fn.js.map +1 -1
  17. package/dist/index.d.ts +11 -5
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +10 -4
  20. package/dist/index.js.map +1 -1
  21. package/dist/mixin.d.ts +2 -1
  22. package/dist/mixin.d.ts.map +1 -1
  23. package/dist/mixin.js +1 -1
  24. package/dist/mixin.js.map +1 -1
  25. package/dist/native-value.d.ts +9 -3
  26. package/dist/native-value.d.ts.map +1 -1
  27. package/dist/native-value.js +86 -9
  28. package/dist/native-value.js.map +1 -1
  29. package/dist/object.d.ts +14 -6
  30. package/dist/object.d.ts.map +1 -1
  31. package/dist/object.js +21 -10
  32. package/dist/object.js.map +1 -1
  33. package/dist/param-spec.d.ts +31 -1
  34. package/dist/param-spec.d.ts.map +1 -1
  35. package/dist/param-spec.js +66 -5
  36. package/dist/param-spec.js.map +1 -1
  37. package/dist/promisify.d.ts +13 -1
  38. package/dist/promisify.d.ts.map +1 -1
  39. package/dist/promisify.js +14 -1
  40. package/dist/promisify.js.map +1 -1
  41. package/dist/properties.d.ts +28 -1
  42. package/dist/properties.d.ts.map +1 -1
  43. package/dist/properties.js +139 -8
  44. package/dist/properties.js.map +1 -1
  45. package/dist/regex.d.ts +25 -0
  46. package/dist/regex.d.ts.map +1 -0
  47. package/dist/regex.js +59 -0
  48. package/dist/regex.js.map +1 -0
  49. package/dist/register-class.d.ts +154 -11
  50. package/dist/register-class.d.ts.map +1 -1
  51. package/dist/register-class.js +157 -19
  52. package/dist/register-class.js.map +1 -1
  53. package/dist/registry.d.ts +48 -4
  54. package/dist/registry.d.ts.map +1 -1
  55. package/dist/registry.js +147 -3
  56. package/dist/registry.js.map +1 -1
  57. package/dist/signal.d.ts +8 -1
  58. package/dist/signal.d.ts.map +1 -1
  59. package/dist/signal.js +41 -3
  60. package/dist/signal.js.map +1 -1
  61. package/dist/type.d.ts +2 -1
  62. package/dist/type.d.ts.map +1 -1
  63. package/dist/type.js +5 -1
  64. package/dist/type.js.map +1 -1
  65. package/dist/value.d.ts +48 -4
  66. package/dist/value.d.ts.map +1 -1
  67. package/dist/value.js +172 -16
  68. package/dist/value.js.map +1 -1
  69. package/dist/variant.d.ts +75 -26
  70. package/dist/variant.d.ts.map +1 -1
  71. package/dist/variant.js +44 -29
  72. package/dist/variant.js.map +1 -1
  73. package/package.json +4 -4
  74. package/src/arg.ts +6 -1
  75. package/src/closure.ts +2 -14
  76. package/src/descriptors.ts +74 -21
  77. package/src/fn.ts +73 -17
  78. package/src/index.ts +31 -4
  79. package/src/mixin.ts +1 -1
  80. package/src/native-value.ts +146 -14
  81. package/src/object.ts +23 -11
  82. package/src/param-spec.ts +94 -8
  83. package/src/promisify.ts +27 -1
  84. package/src/properties.ts +184 -5
  85. package/src/regex.ts +92 -0
  86. package/src/register-class.ts +446 -38
  87. package/src/registry.ts +219 -4
  88. package/src/signal.ts +77 -0
  89. package/src/type.ts +7 -0
  90. package/src/value.ts +268 -14
  91. package/src/variant.ts +184 -53
package/src/object.ts CHANGED
@@ -3,9 +3,9 @@ import { type AnyClass, getParentClass } from "@gtkx/utils";
3
3
  import { bind } from "./bind.js";
4
4
  import { objectT, stringT, voidT } from "./descriptors.js";
5
5
  import { LIB, VALUE_T } from "./library.js";
6
- import { type ConstructProperty, constructPropertyFor } from "./properties.js";
6
+ import { coercePropertyValue, type ConstructProperty, constructPropertyFor } from "./properties.js";
7
7
  import { getHandle, registerWrapper } from "./registry.js";
8
- import { fromValue, newValueForDescriptor, toValue } from "./value.js";
8
+ import { fromValueForDescriptor, newValueForDescriptor, toValue } from "./value.js";
9
9
 
10
10
  /**
11
11
  * One construct property a wrapper class accepts: the canonical `GObject` name it is set under,
@@ -110,7 +110,7 @@ function constructPropertyForEntry(
110
110
  return constructPropertyFor(source.gtype, name, value, source.wrapper);
111
111
  }
112
112
 
113
- return { name: binding[0], value: toValue(binding[1], value) };
113
+ return { name: binding[0], value: toValue(binding[1], coercePropertyValue(source.gtype, binding[0], value)) };
114
114
  }
115
115
 
116
116
  /**
@@ -123,17 +123,24 @@ function constructPropertyForEntry(
123
123
  * for a read-only property and for a value of a type the property cannot hold, and
124
124
  * a `RangeError` for a value the ParamSpec rejects. A value marshalled through a
125
125
  * declared descriptor is converted rather than checked, so a `null` handed to a
126
- * numeric one of those lands 0 rather than being refused.
126
+ * numeric one of those lands 0 rather than being refused, and a number is fitted to
127
+ * the property first, as `coerceObjectProperty` does: truncated toward zero for a
128
+ * whole-number property and clamped to the range its `GObject.ParamSpec` allows.
127
129
  * Properties whose value is `undefined` are skipped. A type registered with
128
130
  * `registerClass` binds the wrapper before its `constructed` slot runs, so an
129
131
  * override of that slot already sees a usable instance.
132
+ * When construction returns an object that already has a wrapper — it reached
133
+ * JavaScript and was wrapped before `g_object_new` returned, for example a
134
+ * `Gtk.Window` observed through the toplevels list — that existing wrapper is
135
+ * returned instead of binding `wrapper`, so both references stay one object.
130
136
  *
131
137
  * @param gtype The GType of the object to construct.
132
138
  * @param props Property names mapped to the values to set them to.
133
139
  * @param wrapper The wrapper instance to bind to the new object.
134
- * @returns The handle of the newly created object.
140
+ * @returns The wrapper bound to the constructed object: `wrapper` itself, or
141
+ * the wrapper the object already had.
135
142
  */
136
- function newObjectWithProperties(gtype: bigint, props: object, wrapper: object): ExternalObject<Handle> {
143
+ function newObjectWithProperties<T extends object>(gtype: bigint, props: object, wrapper: T): T {
137
144
  const names: string[] = [];
138
145
  const values: ExternalObject<Handle>[] = [];
139
146
  const bindings = constructBindingsFor(wrapper.constructor as AnyClass | undefined);
@@ -148,9 +155,13 @@ function newObjectWithProperties(gtype: bigint, props: object, wrapper: object):
148
155
  }
149
156
  }
150
157
 
151
- newObject(gtype, names, values, wrapper, registerWrapper);
158
+ const existing = newObject(gtype, names, values, wrapper, registerWrapper);
152
159
 
153
- return getHandle(wrapper);
160
+ if (existing !== null) {
161
+ return existing as T;
162
+ }
163
+
164
+ return wrapper;
154
165
  }
155
166
 
156
167
  /**
@@ -165,15 +176,16 @@ function getObjectProperty(obj: object, propertyName: string, descriptor: Descri
165
176
  const value = newValueForDescriptor(descriptor);
166
177
  gObjectGetProperty(getHandle(obj), propertyName, value);
167
178
 
168
- return fromValue(value);
179
+ return fromValueForDescriptor(descriptor, value);
169
180
  }
170
181
 
171
182
  /**
172
183
  * Writes a JavaScript value to a GObject property, converting it to native form
173
184
  * using the descriptor. The descriptor converts what it is given rather than checking it against
174
185
  * the property's `GObject.ParamSpec`, so `null` and `undefined` written to a numeric or enum
175
- * property land 0, where the same write to a property installed through `registerClass` is
176
- * refused with a `TypeError`.
186
+ * property land 0, and a fractional number written to a whole-number property is truncated toward
187
+ * zero, where the same writes to a property installed through `registerClass` are refused with a
188
+ * `TypeError`.
177
189
  *
178
190
  * @param obj The object to write to.
179
191
  * @param propertyName The property name.
package/src/param-spec.ts CHANGED
@@ -2,7 +2,7 @@ import { type ExternalObject, type Handle, read } from "@gtkx/native";
2
2
  import { bind } from "./bind.js";
3
3
  import { biguint64T, booleanT, stringT, uint32T, voidT } from "./descriptors.js";
4
4
  import { LIB, PARAM_T, VALUE_T } from "./library.js";
5
- import { getInstanceType } from "./registry.js";
5
+ import { getHandle, getInstanceType } from "./registry.js";
6
6
  import {
7
7
  getStrvType,
8
8
  isTypedClass,
@@ -33,15 +33,17 @@ import {
33
33
  } from "./type.js";
34
34
 
35
35
  type ValueGuard = (value: unknown) => boolean;
36
- type ParamLayout = { flags: number; valueType: bigint };
36
+ type ParamLayout = { flags: number; valueType: bigint; ownerType: bigint };
37
37
 
38
38
  const PARAM_READABLE = 1;
39
39
  const PARAM_WRITABLE = 2;
40
40
  const PARAM_CONSTRUCT_ONLY = 8;
41
41
  const PARAM_LAX_VALIDATION = 16;
42
+ const PARAM_EXPLICIT_NOTIFY = 1 << 30;
42
43
  const READ_FLAGS = PARAM_READABLE | PARAM_WRITABLE | PARAM_CONSTRUCT_ONLY | PARAM_LAX_VALIDATION;
43
44
  const FLAGS_BYTE_OFFSET = 16;
44
45
  const VALUE_TYPE_BYTE_OFFSET = 24;
46
+ const OWNER_TYPE_BYTE_OFFSET = 32;
45
47
  const LAYOUT_PROBE_NAME = "gtkx-param-layout";
46
48
  const INT8_MINIMUM = -128;
47
49
  const INT8_MAXIMUM = 127;
@@ -49,9 +51,9 @@ const UINT8_MAXIMUM = 255;
49
51
  const INT32_MINIMUM = -2_147_483_648;
50
52
  const INT32_MAXIMUM = 2_147_483_647;
51
53
  const UINT32_MAXIMUM = 4_294_967_295;
52
- const INT64_MINIMUM = -(2n ** 63n);
53
- const INT64_MAXIMUM = 2n ** 63n - 1n;
54
- const UINT64_MAXIMUM = 2n ** 64n - 1n;
54
+ const INT64_MINIMUM: bigint = -(2n ** 63n);
55
+ const INT64_MAXIMUM: bigint = 2n ** 63n - 1n;
56
+ const UINT64_MAXIMUM: bigint = 2n ** 64n - 1n;
55
57
 
56
58
  const WRAPPED_FUNDAMENTALS: Set<bigint> = new Set([
57
59
  TYPE_BOXED,
@@ -62,6 +64,20 @@ const WRAPPED_FUNDAMENTALS: Set<bigint> = new Set([
62
64
  ]);
63
65
 
64
66
  const isWideUnsignedValue: ValueGuard = wideIntegerGuardFor(0n, UINT64_MAXIMUM);
67
+ const MAX_SAFE: number = Number.MAX_SAFE_INTEGER;
68
+
69
+ const WHOLE_NUMBER_RANGES: Map<bigint, [minimum: number, maximum: number]> = new Map([
70
+ [TYPE_CHAR, [INT8_MINIMUM, INT8_MAXIMUM]],
71
+ [TYPE_UCHAR, [0, UINT8_MAXIMUM]],
72
+ [TYPE_INT, [INT32_MINIMUM, INT32_MAXIMUM]],
73
+ [TYPE_UINT, [0, UINT32_MAXIMUM]],
74
+ [TYPE_ENUM, [INT32_MINIMUM, INT32_MAXIMUM]],
75
+ [TYPE_FLAGS, [0, UINT32_MAXIMUM]],
76
+ [TYPE_LONG, [-MAX_SAFE, MAX_SAFE]],
77
+ [TYPE_ULONG, [0, MAX_SAFE]],
78
+ [TYPE_INT64, [-MAX_SAFE, MAX_SAFE]],
79
+ [TYPE_UINT64, [0, MAX_SAFE]],
80
+ ]);
65
81
 
66
82
  const SCALAR_GUARDS: Map<bigint, ValueGuard> = new Map([
67
83
  [TYPE_BOOLEAN, isBooleanValue],
@@ -98,9 +114,49 @@ const getParamFlags = (pspec: ExternalObject<Handle>): number => read(pspec, uin
98
114
  const getParamValueType = (pspec: ExternalObject<Handle>): bigint =>
99
115
  read(pspec, biguint64T, VALUE_TYPE_BYTE_OFFSET) as bigint;
100
116
 
117
+ const getParamOwnerType = (pspec: ExternalObject<Handle>): bigint =>
118
+ read(pspec, biguint64T, OWNER_TYPE_BYTE_OFFSET) as bigint;
119
+
120
+ /**
121
+ * Reads the `GParamFlags` bitfield of a `GObject.ParamSpec`.
122
+ *
123
+ * @param spec The param spec wrapper to read.
124
+ * @returns The spec's flags.
125
+ */
126
+ const getParamSpecFlags = (spec: object): number => {
127
+ assertParamLayout();
128
+
129
+ return getParamFlags(getHandle(spec));
130
+ };
131
+
132
+ /**
133
+ * Reads the GType of the values a `GObject.ParamSpec` describes.
134
+ *
135
+ * @param spec The param spec wrapper to read.
136
+ * @returns The GType of the spec's values.
137
+ */
138
+ const getParamSpecValueType = (spec: object): bigint => {
139
+ assertParamLayout();
140
+
141
+ return getParamValueType(getHandle(spec));
142
+ };
143
+
144
+ /**
145
+ * Reads the GType a `GObject.ParamSpec` is installed on.
146
+ *
147
+ * @param spec The param spec wrapper to read.
148
+ * @returns The owning GType, or `TYPE_INVALID` while the spec is not installed on any type.
149
+ */
150
+ const getParamSpecOwnerType = (spec: object): bigint => {
151
+ assertParamLayout();
152
+
153
+ return getParamOwnerType(getHandle(spec));
154
+ };
155
+
101
156
  const isParamWritable = (flags: number): boolean => (flags & PARAM_WRITABLE) !== 0;
102
157
  const isParamConstructOnly = (flags: number): boolean => (flags & PARAM_CONSTRUCT_ONLY) !== 0;
103
158
  const isParamLaxlyValidated = (flags: number): boolean => (flags & PARAM_LAX_VALIDATION) !== 0;
159
+ const isParamExplicitlyNotified = (flags: number): boolean => (flags & PARAM_EXPLICIT_NOTIFY) !== 0;
104
160
 
105
161
  const wasParamValueModified = (pspec: ExternalObject<Handle>, value: ExternalObject<Handle>): boolean =>
106
162
  paramValueValidate(pspec, value) as boolean;
@@ -113,8 +169,18 @@ function isStringValue(value: unknown): boolean {
113
169
  return value == null || typeof value === "string";
114
170
  }
115
171
 
172
+ function isStringArray(value: unknown[]): boolean {
173
+ for (const item of value) {
174
+ if (typeof item !== "string") {
175
+ return false;
176
+ }
177
+ }
178
+
179
+ return true;
180
+ }
181
+
116
182
  function isStrvValue(value: unknown): boolean {
117
- return value == null || (Array.isArray(value) && value.every((item) => typeof item === "string"));
183
+ return value == null || (Array.isArray(value) && isStringArray(value));
118
184
  }
119
185
 
120
186
  function isNumberValue(value: unknown): boolean {
@@ -193,7 +259,11 @@ function readProbeLayout(flags: number): ParamLayout {
193
259
  paramSpecRefSink(probe);
194
260
 
195
261
  try {
196
- return { flags: getParamFlags(probe), valueType: getParamValueType(probe) };
262
+ return {
263
+ flags: getParamFlags(probe),
264
+ valueType: getParamValueType(probe),
265
+ ownerType: getParamOwnerType(probe),
266
+ };
197
267
  } finally {
198
268
  paramSpecUnref(probe);
199
269
  }
@@ -202,7 +272,11 @@ function readProbeLayout(flags: number): ParamLayout {
202
272
  function isLayoutIntact(flags: number): boolean {
203
273
  const layout = readProbeLayout(flags);
204
274
 
205
- return (layout.flags & READ_FLAGS) === flags && layout.valueType === TYPE_BOOLEAN;
275
+ return (
276
+ (layout.flags & READ_FLAGS) === flags &&
277
+ layout.valueType === TYPE_BOOLEAN &&
278
+ layout.ownerType === TYPE_INVALID
279
+ );
206
280
  }
207
281
 
208
282
  function assertParamLayout(): void {
@@ -223,11 +297,23 @@ function assertParamLayout(): void {
223
297
  }
224
298
 
225
299
  export {
300
+ WHOLE_NUMBER_RANGES,
226
301
  getParamFlags,
302
+ getParamSpecFlags,
303
+ getParamSpecOwnerType,
304
+ getParamSpecValueType,
227
305
  getParamValueType,
306
+ INT32_MAXIMUM,
307
+ INT32_MINIMUM,
308
+ INT64_MAXIMUM,
309
+ INT64_MINIMUM,
228
310
  isParamConstructOnly,
311
+ isStringArray,
312
+ isParamExplicitlyNotified,
229
313
  isParamLaxlyValidated,
230
314
  isParamWritable,
315
+ resolveGtype,
316
+ UINT64_MAXIMUM,
231
317
  type ValueGuard,
232
318
  valueGuardFor,
233
319
  wasParamValueModified,
package/src/promisify.ts CHANGED
@@ -13,6 +13,13 @@ type Settlement<R extends object, T> = {
13
13
  reject: (reason: Error) => void;
14
14
  };
15
15
 
16
+ /** A finish result with its leading success boolean dropped: bare when one value remains, a tuple otherwise. */
17
+ type TrimmedFinish<T> = T extends readonly [boolean, infer Only]
18
+ ? Only
19
+ : T extends readonly [boolean, ...infer Rest]
20
+ ? Rest
21
+ : never;
22
+
16
23
  const attachCreationStack = (error: unknown, creationStack: Error | undefined): void => {
17
24
  if (creationStack === undefined || !(error instanceof Error)) {
18
25
  return;
@@ -36,6 +43,25 @@ const settle = <R extends object, T>(settlement: Settlement<R, T>, asyncResult:
36
43
  }
37
44
  };
38
45
 
46
+ /**
47
+ * Wraps a finish function whose result leads with the success boolean of a throwing C call,
48
+ * dropping that boolean: failure already surfaces as a thrown error, so the boolean is always
49
+ * `true`. A single remaining value is handed back bare, several stay a tuple. The finish result
50
+ * must carry at least one value beyond the boolean; a boolean-only finish needs no trimming.
51
+ *
52
+ * @param finish Extracts the boolean-led result from the async result passed to the completion callback.
53
+ * @returns A finish function resolving to the result without its leading boolean.
54
+ */
55
+ const trimFinish =
56
+ <R extends object, T extends readonly [boolean, unknown, ...unknown[]]>(
57
+ finish: FinishResult<R, T>,
58
+ ): FinishResult<R, TrimmedFinish<T>> =>
59
+ (result: R): TrimmedFinish<T> => {
60
+ const [, ...rest] = finish(result);
61
+
62
+ return (rest.length === 1 ? rest[0] : rest) as TrimmedFinish<T>;
63
+ };
64
+
39
65
  /**
40
66
  * Wraps a GIO-style asynchronous function that takes a completion callback into a
41
67
  * promise, invoking the finish function to extract the result. Outside production,
@@ -64,4 +90,4 @@ const promisify = <R extends object, T>(
64
90
  });
65
91
  });
66
92
 
67
- export { promisify };
93
+ export { promisify, trimFinish };
package/src/properties.ts CHANGED
@@ -2,23 +2,33 @@ import type { ExternalObject, Handle, RegisterClassProperty } from "@gtkx/native
2
2
  import type { AnyClass } from "@gtkx/utils";
3
3
  import { camelCase, kebabCase } from "@gtkx/utils";
4
4
  import { bind } from "./bind.js";
5
- import { biguint64T, stringT, structT, voidT } from "./descriptors.js";
5
+ import { biguint64T, fundamentalT, stringT, structT, 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,
12
13
  isParamWritable,
13
14
  type ValueGuard,
14
15
  valueGuardFor,
15
16
  wasParamValueModified,
17
+ WHOLE_NUMBER_RANGES,
16
18
  } from "./param-spec.js";
17
- import { getHandle, getInterfaceProperties, instanceClassName, type InterfaceProperty } from "./registry.js";
18
- import { typeName } from "./type.js";
19
+ import {
20
+ getClassType,
21
+ getHandle,
22
+ getInstanceType,
23
+ getInterfaceProperties,
24
+ instanceClassName,
25
+ type InterfaceProperty,
26
+ } from "./registry.js";
27
+ import { TYPE_INTERFACE, TYPE_INVALID, TYPE_OBJECT, typeFundamental, typeIsA, typeName } from "./type.js";
19
28
  import {
20
29
  fromValue,
21
30
  newValueForType,
31
+ valueGuardOverrideFor,
22
32
  type ValueNarrower,
23
33
  valueNarrowerFor,
24
34
  type ValueWriter,
@@ -72,12 +82,23 @@ const SET_PROPERTY_VFUNC = "vfuncSetProperty";
72
82
  const READ_ONLY_REASON = "the property is read-only";
73
83
  const CONSTRUCT_ONLY_REASON = "the property can only be set when the object is constructed";
74
84
  const CLASS_T = structT("borrowed");
85
+
86
+ const OVERRIDE_PARAM_T = fundamentalT(LIB, "g_param_spec_ref_sink", "g_param_spec_unref", {
87
+ ownership: "borrowed",
88
+ typeName: "GParam",
89
+ });
90
+
75
91
  const interfaceStorages: Map<string, symbol> = new Map();
76
92
  const paramSpecDefaultValue = bind(LIB, "g_param_spec_get_default_value", [PARAM_T], VALUE_T);
77
93
  const paramSpecName = bind(LIB, "g_param_spec_get_name", [PARAM_T], stringT("borrowed"));
78
94
  const typeClassRef = bind(LIB, "g_type_class_ref", [biguint64T], CLASS_T);
79
95
  const typeClassUnref = bind(LIB, "g_type_class_unref", [CLASS_T], voidT);
96
+ const coercionChecks: Map<bigint, Map<string, PropertyCheck | null>> = new Map();
80
97
  const classFindProperty = bind(LIB, "g_object_class_find_property", [CLASS_T, stringT("borrowed")], PARAM_T);
98
+ const defaultInterfaceRef = bind(LIB, "g_type_default_interface_ref", [biguint64T], CLASS_T);
99
+ const defaultInterfaceUnref = bind(LIB, "g_type_default_interface_unref", [CLASS_T], voidT);
100
+ const interfaceFindProperty = bind(LIB, "g_object_interface_find_property", [CLASS_T, stringT("borrowed")], PARAM_T);
101
+ const paramSpecOverrideNew = bind(LIB, "g_param_spec_override", [stringT("borrowed"), PARAM_T], OVERRIDE_PARAM_T);
81
102
 
82
103
  const getPropertyName = (pspec: PropertySpec): string => paramSpecName(getHandle(pspec)) as string;
83
104
  const underscoreCase = (name: string): string => name.replaceAll("-", "_");
@@ -102,7 +123,7 @@ function checkFor(handle: ExternalObject<Handle>, name: string): PropertyCheck {
102
123
  handle,
103
124
  flags: getParamFlags(handle),
104
125
  valueType,
105
- canHoldValue: valueGuardFor(valueType),
126
+ canHoldValue: valueGuardOverrideFor(valueType) ?? valueGuardFor(valueType),
106
127
  narrowValue: valueNarrowerFor(valueType),
107
128
  };
108
129
  }
@@ -113,6 +134,62 @@ function findPropertySpec(klass: ExternalObject<Handle>, name: string): External
113
134
  return found ?? (classFindProperty(klass, canonicalCase(name)) as ExternalObject<Handle> | null);
114
135
  }
115
136
 
137
+ const sourceLabel = (source: bigint | AnyClass, gtype: bigint): string =>
138
+ typeName(gtype) ?? (typeof source === "function" ? source.name : String(source));
139
+
140
+ const findInterfaceSpec = (gtype: bigint, name: string): ExternalObject<Handle> | null => {
141
+ const vtable = defaultInterfaceRef(gtype) as ExternalObject<Handle>;
142
+
143
+ try {
144
+ return interfaceFindProperty(vtable, name) as ExternalObject<Handle> | null;
145
+ } finally {
146
+ defaultInterfaceUnref(vtable);
147
+ }
148
+ };
149
+
150
+ const findClassSpec = (gtype: bigint, name: string): ExternalObject<Handle> | null => {
151
+ const klass = typeClassRef(gtype) as ExternalObject<Handle>;
152
+
153
+ try {
154
+ return classFindProperty(klass, name) as ExternalObject<Handle> | null;
155
+ } finally {
156
+ typeClassUnref(klass);
157
+ }
158
+ };
159
+
160
+ const findSourceSpec = (source: bigint | AnyClass, name: string): ExternalObject<Handle> => {
161
+ const gtype = typeof source === "bigint" ? source : getClassType(source);
162
+ const isInterface = typeIsA(gtype, TYPE_INTERFACE);
163
+
164
+ if (!isInterface && !typeIsA(gtype, TYPE_OBJECT)) {
165
+ throw new TypeError(
166
+ `paramSpecOverride: '${sourceLabel(source, gtype)}' is neither a registered object class nor an interface`,
167
+ );
168
+ }
169
+
170
+ const pspec = isInterface ? findInterfaceSpec(gtype, name) : findClassSpec(gtype, name);
171
+
172
+ if (pspec === null) {
173
+ throw new TypeError(`paramSpecOverride: type '${sourceLabel(source, gtype)}' has no property named '${name}'`);
174
+ }
175
+
176
+ return pspec;
177
+ };
178
+
179
+ /**
180
+ * Creates the handle of a `GParamSpec` that redirects to the property `source` declares under
181
+ * `name` — the equivalent of `g_param_spec_override`, which introspection does not expose.
182
+ *
183
+ * Throws when `source` is neither a GType nor a registered class or interface, and when it
184
+ * declares no property under `name`.
185
+ *
186
+ * @param name Canonical name of the property to override.
187
+ * @param source The GType, wrapper class or interface declaring the property.
188
+ * @returns Handle of the override spec.
189
+ */
190
+ const newParamSpecOverride = (name: string, source: bigint | AnyClass): ExternalObject<Handle> =>
191
+ paramSpecOverrideNew(name, findSourceSpec(source, name)) as ExternalObject<Handle>;
192
+
116
193
  function describeValue(value: unknown): string {
117
194
  if (typeof value === "string") {
118
195
  return JSON.stringify(value);
@@ -213,6 +290,97 @@ function constructValueFor(wrapper: object, check: PropertyCheck, value: unknown
213
290
  return { name: check.propertyName, value: checkedValueFor(wrapper, check, value) };
214
291
  }
215
292
 
293
+ function lookupCoercionCheck(gtype: bigint, name: string): PropertyCheck | null {
294
+ const klass = typeClassRef(gtype) as ExternalObject<Handle>;
295
+
296
+ try {
297
+ const pspec = findPropertySpec(klass, name);
298
+
299
+ return pspec === null ? null : checkFor(pspec, name);
300
+ } finally {
301
+ typeClassUnref(klass);
302
+ }
303
+ }
304
+
305
+ function coercionCheckFor(gtype: bigint, name: string): PropertyCheck | null {
306
+ let checks = coercionChecks.get(gtype);
307
+
308
+ if (checks === undefined) {
309
+ checks = new Map();
310
+ coercionChecks.set(gtype, checks);
311
+ }
312
+
313
+ let check = checks.get(name);
314
+
315
+ if (check === undefined) {
316
+ check = lookupCoercionCheck(gtype, name);
317
+ checks.set(name, check);
318
+ }
319
+
320
+ return check;
321
+ }
322
+
323
+ function truncateToWhole(check: PropertyCheck, value: number): number {
324
+ const range = WHOLE_NUMBER_RANGES.get(typeFundamental(check.valueType));
325
+
326
+ if (range === undefined) {
327
+ return value;
328
+ }
329
+
330
+ return Math.min(range[1], Math.max(range[0], Math.trunc(value)));
331
+ }
332
+
333
+ function validatedNumber(check: PropertyCheck, number: number): number {
334
+ check.scratch ??= newValueForType(check.valueType);
335
+
336
+ try {
337
+ writeHeld(check, check.scratch, number);
338
+ } catch {
339
+ return number;
340
+ }
341
+
342
+ return wasParamValueModified(check.handle, check.scratch) ? (fromValue(check.scratch) as number) : number;
343
+ }
344
+
345
+ function coerceNumber(check: PropertyCheck, value: number): number {
346
+ const number = truncateToWhole(check, value);
347
+
348
+ return check.canHoldValue(number) ? validatedNumber(check, number) : value;
349
+ }
350
+
351
+ function coercePropertyValue(gtype: bigint, propertyName: string, value: unknown): unknown {
352
+ if (typeof value !== "number" || !Number.isFinite(value)) {
353
+ return value;
354
+ }
355
+
356
+ const check = coercionCheckFor(gtype, propertyName);
357
+
358
+ return check === null ? value : coerceNumber(check, value);
359
+ }
360
+
361
+ /**
362
+ * Fits a number to what a GObject property accepts, the way an animation writing a value on every
363
+ * frame needs: a fractional number headed for a property that holds whole numbers, such as a
364
+ * `gint`, an enum, or flags, is truncated toward zero, and a number outside the range the
365
+ * property's `GObject.ParamSpec` allows is clamped to it, so the write that follows never trips
366
+ * GObject's range check. Anything that is not a finite number, a number the property cannot hold
367
+ * at all, and a name the object installs no property under come back unchanged.
368
+ *
369
+ * @param obj The object the value is about to be written to.
370
+ * @param propertyName The property name, dashed or camelCased.
371
+ * @param value The value about to be written.
372
+ * @returns The value as the property accepts it.
373
+ */
374
+ function coerceObjectProperty(obj: object, propertyName: string, value: unknown): unknown {
375
+ if (typeof value !== "number") {
376
+ return value;
377
+ }
378
+
379
+ const gtype = getInstanceType(obj);
380
+
381
+ return gtype === TYPE_INVALID ? value : coercePropertyValue(gtype, propertyName, value);
382
+ }
383
+
216
384
  function constructPropertyFor(
217
385
  gtype: bigint,
218
386
  name: string,
@@ -414,6 +582,14 @@ function readCurrent(instance: object, accessor: PropertyAccessor): unknown {
414
582
  : (instance as Record<string, unknown>)[member];
415
583
  }
416
584
 
585
+ function storeCurrent(instance: Record<symbol, unknown>, accessor: PropertyAccessor, value: unknown): void {
586
+ if (isParamExplicitlyNotified(accessor.flags)) {
587
+ writeStored(instance, accessor, value);
588
+ } else {
589
+ storeValue(instance, accessor, value);
590
+ }
591
+ }
592
+
417
593
  function writeCurrent(instance: object, accessor: PropertyAccessor, value: unknown): void {
418
594
  const setter = accessor.delegate?.setter;
419
595
 
@@ -432,7 +608,7 @@ function writeCurrent(instance: object, accessor: PropertyAccessor, value: unkno
432
608
  const member = backingMemberFor(instance, accessor);
433
609
 
434
610
  if (member === undefined) {
435
- storeValue(instance as Record<symbol, unknown>, accessor, value);
611
+ storeCurrent(instance as Record<symbol, unknown>, accessor, value);
436
612
 
437
613
  return;
438
614
  }
@@ -531,10 +707,13 @@ function toNativeProperties(properties: Record<string, PropertySpec>): RegisterC
531
707
 
532
708
  export {
533
709
  buildPropertyDispatch,
710
+ coerceObjectProperty,
711
+ coercePropertyValue,
534
712
  constructPropertyFor,
535
713
  GET_PROPERTY_VFUNC,
536
714
  makeGetProperty,
537
715
  makeSetProperty,
716
+ newParamSpecOverride,
538
717
  SET_PROPERTY_VFUNC,
539
718
  toNativeProperties,
540
719
  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 };