@silverbulletmd/silverbullet 2.4.1

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 (117) hide show
  1. package/LICENSE.md +18 -0
  2. package/README.md +98 -0
  3. package/client/asset_bundle/bundle.ts +95 -0
  4. package/client/data/datastore.ts +85 -0
  5. package/client/data/kv_primitives.ts +25 -0
  6. package/client/markdown_parser/constants.ts +13 -0
  7. package/client/plugos/event.ts +36 -0
  8. package/client/plugos/eventhook.ts +8 -0
  9. package/client/plugos/hooks/code_widget.ts +59 -0
  10. package/client/plugos/hooks/command.ts +104 -0
  11. package/client/plugos/hooks/document_editor.ts +77 -0
  12. package/client/plugos/hooks/event.ts +187 -0
  13. package/client/plugos/hooks/mq.ts +154 -0
  14. package/client/plugos/hooks/plug_namespace.ts +85 -0
  15. package/client/plugos/hooks/slash_command.ts +192 -0
  16. package/client/plugos/hooks/syscall.ts +66 -0
  17. package/client/plugos/manifest_cache.ts +67 -0
  18. package/client/plugos/plug.ts +99 -0
  19. package/client/plugos/plug_compile.ts +202 -0
  20. package/client/plugos/protocol.ts +40 -0
  21. package/client/plugos/proxy_fetch.ts +53 -0
  22. package/client/plugos/sandboxes/deno_worker_sandbox.ts +6 -0
  23. package/client/plugos/sandboxes/sandbox.ts +14 -0
  24. package/client/plugos/sandboxes/web_worker_sandbox.ts +17 -0
  25. package/client/plugos/sandboxes/worker_sandbox.ts +132 -0
  26. package/client/plugos/syscalls/asset.ts +35 -0
  27. package/client/plugos/syscalls/clientStore.ts +21 -0
  28. package/client/plugos/syscalls/client_code_widget.ts +12 -0
  29. package/client/plugos/syscalls/code_widget.ts +24 -0
  30. package/client/plugos/syscalls/config.ts +46 -0
  31. package/client/plugos/syscalls/datastore.ts +89 -0
  32. package/client/plugos/syscalls/editor.ts +673 -0
  33. package/client/plugos/syscalls/event.ts +36 -0
  34. package/client/plugos/syscalls/fetch.ts +128 -0
  35. package/client/plugos/syscalls/index.ts +102 -0
  36. package/client/plugos/syscalls/jsonschema.ts +69 -0
  37. package/client/plugos/syscalls/language.ts +23 -0
  38. package/client/plugos/syscalls/lua.ts +58 -0
  39. package/client/plugos/syscalls/markdown.ts +84 -0
  40. package/client/plugos/syscalls/mq.ts +52 -0
  41. package/client/plugos/syscalls/service_registry.ts +43 -0
  42. package/client/plugos/syscalls/shell.ts +39 -0
  43. package/client/plugos/syscalls/space.ts +139 -0
  44. package/client/plugos/syscalls/sync.ts +77 -0
  45. package/client/plugos/syscalls/system.ts +150 -0
  46. package/client/plugos/system.ts +201 -0
  47. package/client/plugos/types.ts +60 -0
  48. package/client/plugos/util.ts +14 -0
  49. package/client/plugos/worker_runtime.ts +195 -0
  50. package/client/space_lua/ast.ts +328 -0
  51. package/client/space_lua/ast_narrow.ts +81 -0
  52. package/client/space_lua/eval.ts +2478 -0
  53. package/client/space_lua/labels.ts +416 -0
  54. package/client/space_lua/numeric.ts +240 -0
  55. package/client/space_lua/parse.ts +1522 -0
  56. package/client/space_lua/query_collection.ts +232 -0
  57. package/client/space_lua/rp.ts +27 -0
  58. package/client/space_lua/runtime.ts +1702 -0
  59. package/client/space_lua/stdlib/crypto.ts +10 -0
  60. package/client/space_lua/stdlib/encoding.ts +19 -0
  61. package/client/space_lua/stdlib/format.ts +770 -0
  62. package/client/space_lua/stdlib/js.ts +73 -0
  63. package/client/space_lua/stdlib/load.ts +52 -0
  64. package/client/space_lua/stdlib/math.ts +193 -0
  65. package/client/space_lua/stdlib/net.ts +113 -0
  66. package/client/space_lua/stdlib/os.ts +368 -0
  67. package/client/space_lua/stdlib/space_lua.ts +153 -0
  68. package/client/space_lua/stdlib/string.ts +286 -0
  69. package/client/space_lua/stdlib/table.ts +401 -0
  70. package/client/space_lua/stdlib.ts +489 -0
  71. package/client/space_lua/tonumber.ts +501 -0
  72. package/client/space_lua/util.ts +96 -0
  73. package/dist/plug-compile.js +1513 -0
  74. package/package.json +120 -0
  75. package/plug-api/constants.ts +42 -0
  76. package/plug-api/lib/async.ts +162 -0
  77. package/plug-api/lib/crypto.ts +202 -0
  78. package/plug-api/lib/dates.ts +13 -0
  79. package/plug-api/lib/json.ts +136 -0
  80. package/plug-api/lib/limited_map.ts +72 -0
  81. package/plug-api/lib/memory_cache.ts +21 -0
  82. package/plug-api/lib/native_fetch.ts +6 -0
  83. package/plug-api/lib/ref.ts +275 -0
  84. package/plug-api/lib/resolve.ts +90 -0
  85. package/plug-api/lib/tags.ts +15 -0
  86. package/plug-api/lib/transclusion.ts +122 -0
  87. package/plug-api/lib/tree.ts +232 -0
  88. package/plug-api/lib/yaml.ts +284 -0
  89. package/plug-api/syscall.ts +15 -0
  90. package/plug-api/syscalls/asset.ts +36 -0
  91. package/plug-api/syscalls/client_store.ts +33 -0
  92. package/plug-api/syscalls/code_widget.ts +8 -0
  93. package/plug-api/syscalls/config.ts +58 -0
  94. package/plug-api/syscalls/datastore.ts +96 -0
  95. package/plug-api/syscalls/editor.ts +517 -0
  96. package/plug-api/syscalls/event.ts +47 -0
  97. package/plug-api/syscalls/index.ts +77 -0
  98. package/plug-api/syscalls/jsonschema.ts +25 -0
  99. package/plug-api/syscalls/language.ts +23 -0
  100. package/plug-api/syscalls/lua.ts +20 -0
  101. package/plug-api/syscalls/markdown.ts +38 -0
  102. package/plug-api/syscalls/mq.ts +79 -0
  103. package/plug-api/syscalls/shell.ts +14 -0
  104. package/plug-api/syscalls/space.ts +212 -0
  105. package/plug-api/syscalls/sync.ts +28 -0
  106. package/plug-api/syscalls/system.ts +102 -0
  107. package/plug-api/syscalls/yaml.ts +28 -0
  108. package/plug-api/syscalls.ts +21 -0
  109. package/plug-api/system_mock.ts +89 -0
  110. package/plug-api/types/client.ts +116 -0
  111. package/plug-api/types/config.ts +22 -0
  112. package/plug-api/types/datastore.ts +28 -0
  113. package/plug-api/types/event.ts +27 -0
  114. package/plug-api/types/index.ts +56 -0
  115. package/plug-api/types/manifest.ts +98 -0
  116. package/plug-api/types/namespace.ts +6 -0
  117. package/plugs/builtin_plugs.ts +14 -0
@@ -0,0 +1,770 @@
1
+ // Supported specifiers set: [diuoxXaAfeEgGcspq%]
2
+ // Supported flags set: [-+0# ]
3
+ // Width and precision via digits or `*`
4
+
5
+ type FormatSpec = {
6
+ flags: number; // FLAG_*
7
+ width: number;
8
+ hasPrec: boolean;
9
+ prec: number;
10
+ spec: number;
11
+ };
12
+
13
+ const FLAG_MINUS = 1;
14
+ const FLAG_PLUS = 2;
15
+ const FLAG_ZERO = 4;
16
+ const FLAG_HASH = 8;
17
+ const FLAG_SPACE = 16;
18
+
19
+ function isDigit(c: number): boolean {
20
+ return c >= 48 && c <= 57; // '0'..'9'
21
+ }
22
+
23
+ // Parse a format spec starting after '%' and return index of specifier.
24
+ function parseSpec(
25
+ fmt: string,
26
+ start: number,
27
+ ): { spec: FormatSpec; end: number } {
28
+ let i = start;
29
+ const len = fmt.length;
30
+ let flags = 0;
31
+
32
+ outer: while (i < len) {
33
+ switch (fmt.charCodeAt(i)) {
34
+ case 45:
35
+ flags |= FLAG_MINUS;
36
+ i++;
37
+ break; // '-'
38
+ case 43:
39
+ flags |= FLAG_PLUS;
40
+ i++;
41
+ break; // '+'
42
+ case 48:
43
+ flags |= FLAG_ZERO;
44
+ i++;
45
+ break; // '0'
46
+ case 35:
47
+ flags |= FLAG_HASH;
48
+ i++;
49
+ break; // '#'
50
+ case 32:
51
+ flags |= FLAG_SPACE;
52
+ i++;
53
+ break; // ' '
54
+ default:
55
+ break outer;
56
+ }
57
+ }
58
+
59
+ // Parse width
60
+ let width = 0;
61
+ if (i < len && fmt.charCodeAt(i) === 42) { // '*'
62
+ width = -1;
63
+ i++;
64
+ } else {
65
+ while (i < len && isDigit(fmt.charCodeAt(i))) {
66
+ width = width * 10 + (fmt.charCodeAt(i) - 48);
67
+ i++;
68
+ }
69
+ }
70
+
71
+ // Parse precision
72
+ let hasPrec = false;
73
+ let prec = 0;
74
+ if (i < len && fmt.charCodeAt(i) === 46) { // '.'
75
+ hasPrec = true;
76
+ i++;
77
+ if (i < len && fmt.charCodeAt(i) === 42) { // '*'
78
+ prec = -1;
79
+ i++;
80
+ } else {
81
+ while (i < len && isDigit(fmt.charCodeAt(i))) {
82
+ prec = prec * 10 + (fmt.charCodeAt(i) - 48);
83
+ i++;
84
+ }
85
+ }
86
+ }
87
+
88
+ // Skip length modifiers [hlL] ignored in Lua
89
+ while (
90
+ i < len &&
91
+ (fmt.charCodeAt(i) === 104 || // 'h'
92
+ fmt.charCodeAt(i) === 108 || // 'l'
93
+ fmt.charCodeAt(i) === 76) // 'L'
94
+ ) {
95
+ i++;
96
+ }
97
+
98
+ if (i >= len) {
99
+ throw new Error("invalid format (missing specifier)");
100
+ }
101
+
102
+ return {
103
+ spec: { flags, width, hasPrec, prec, spec: fmt.charCodeAt(i) },
104
+ end: i,
105
+ };
106
+ }
107
+
108
+ // pad a string to `width` respecting `FLAG_MINUS` and `FLAG_ZERO`
109
+ function pad(s: string, width: number, flags: number, numPad: boolean): string {
110
+ if (width <= 0 || s.length >= width) return s;
111
+ const n = width - s.length;
112
+ if (numPad && (flags & FLAG_ZERO) && !(flags & FLAG_MINUS)) {
113
+ let signLen = 0;
114
+ if (s.charCodeAt(0) === 45 || s.charCodeAt(0) === 43) { // '-' or '+'
115
+ signLen = 1;
116
+ } else if (
117
+ s.charCodeAt(0) === 48 &&
118
+ (s.charCodeAt(1) === 120 || s.charCodeAt(1) === 88)
119
+ ) {
120
+ signLen = 2; // '0x' or '0X'
121
+ }
122
+ return s.slice(0, signLen) + "0".repeat(n) + s.slice(signLen);
123
+ }
124
+ if (flags & FLAG_MINUS) {
125
+ return s + " ".repeat(n);
126
+ }
127
+ return " ".repeat(n) + s;
128
+ }
129
+
130
+ function addSign(s: string, flags: number): string {
131
+ if (flags & FLAG_PLUS) return "+" + s;
132
+ if (flags & FLAG_SPACE) return " " + s;
133
+ return s;
134
+ }
135
+
136
+ function formatInt(n: number, spec: FormatSpec): string {
137
+ const code = spec.spec;
138
+ const v = Math.trunc(n);
139
+
140
+ let base = 10;
141
+ let unsigned = false;
142
+ let upper = false;
143
+
144
+ switch (code) {
145
+ case 100:
146
+ case 105: // 'd', 'i'
147
+ break;
148
+ case 117: // 'u'
149
+ unsigned = true;
150
+ break;
151
+ case 111: // 'o'
152
+ base = 8;
153
+ unsigned = true;
154
+ break;
155
+ case 120: // 'x'
156
+ base = 16;
157
+ unsigned = true;
158
+ break;
159
+ case 88: // 'X'
160
+ base = 16;
161
+ unsigned = true;
162
+ upper = true;
163
+ break;
164
+ }
165
+
166
+ let neg = false;
167
+ let digits: string;
168
+
169
+ if (unsigned && v < 0) {
170
+ // Reinterpret as 64-bit unsigned
171
+ const bv = BigInt(v) + (1n << 64n);
172
+ digits = bv.toString(base);
173
+ } else if (unsigned) {
174
+ digits = v.toString(base);
175
+ } else {
176
+ neg = v < 0;
177
+ digits = (neg ? -v : v).toString(base);
178
+ }
179
+
180
+ if (upper) digits = digits.toUpperCase();
181
+
182
+ // Precision
183
+ if (spec.hasPrec) {
184
+ if (spec.prec === 0 && v === 0) {
185
+ digits = "";
186
+ } else if (digits.length < spec.prec) {
187
+ digits = "0".repeat(spec.prec - digits.length) + digits;
188
+ }
189
+ }
190
+
191
+ // Alt flag
192
+ let prefix = "";
193
+ if (spec.flags & FLAG_HASH) {
194
+ if (base === 8 && (digits.length === 0 || digits.charCodeAt(0) !== 48)) {
195
+ prefix = "0";
196
+ } else if (base === 16 && v !== 0) {
197
+ prefix = upper ? "0X" : "0x";
198
+ }
199
+ }
200
+
201
+ let result: string;
202
+ if (neg) {
203
+ result = "-" + prefix + digits;
204
+ } else {
205
+ result = addSign(prefix + digits, spec.flags);
206
+ }
207
+
208
+ const numPad = !spec.hasPrec;
209
+ return pad(result, spec.width, spec.flags, numPad);
210
+ }
211
+
212
+ function formatFloat(n: number, spec: FormatSpec): string {
213
+ const code = spec.spec;
214
+ const upper = code === 69 || code === 71 || code === 70;
215
+ // 'E'=69 'G'=71 'F'=70 'e'=101 'g'=103 'f'=102
216
+ const lower = code | 32; // to lowercase
217
+
218
+ // Lua convention
219
+ if (!isFinite(n)) {
220
+ let s: string;
221
+ if (n !== n) {
222
+ s = upper ? "-NAN" : "-nan";
223
+ } else if (n > 0) {
224
+ s = upper ? "INF" : "inf";
225
+ s = addSign(s, spec.flags);
226
+ } else {
227
+ s = upper ? "-INF" : "-inf";
228
+ }
229
+ return pad(s, spec.width, spec.flags, false);
230
+ }
231
+
232
+ const neg = n < 0 || (n === 0 && 1 / n === -Infinity);
233
+ const abs = neg ? -n : n;
234
+ const prec = spec.hasPrec ? spec.prec : 6;
235
+
236
+ let body: string;
237
+
238
+ if (lower === 102) { // 'f'
239
+ body = abs.toFixed(prec);
240
+ } else if (lower === 101) { // 'e'
241
+ body = abs.toExponential(prec);
242
+ // Ensure exponent has at least 2 digits
243
+ body = ensureExpTwoDigits(body);
244
+ } else { // 'g'
245
+ const gPrec = (prec === 0) ? 1 : prec;
246
+ if (abs === 0) {
247
+ body = "0";
248
+ } else {
249
+ // C rule: use 'e' if exponent < -4 or exponent >= precision
250
+ const exp = Math.floor(Math.log10(abs));
251
+ if (exp < -4 || exp >= gPrec) {
252
+ body = abs.toExponential(gPrec - 1);
253
+ body = ensureExpTwoDigits(body);
254
+ } else {
255
+ // Number of decimals = precision - (exponent + 1)
256
+ const decimals = gPrec - (exp + 1);
257
+ body = abs.toFixed(decimals);
258
+ }
259
+ }
260
+ // Strip trailing zeros unless '#' flag
261
+ if (!(spec.flags & FLAG_HASH)) {
262
+ body = stripTrailingZerosG(body);
263
+ }
264
+ }
265
+
266
+ if (upper) {
267
+ body = body.toUpperCase();
268
+ }
269
+
270
+ // Alt flag for 'f'/'e': ensure decimal point exists
271
+ if ((spec.flags & FLAG_HASH) && lower !== 103) {
272
+ if (body.indexOf(".") === -1) {
273
+ // Insert dot before 'e' if present, else append
274
+ const eIdx = body.indexOf("e");
275
+ const EIdx = body.indexOf("E");
276
+ const expIdx = eIdx !== -1 ? eIdx : EIdx;
277
+ if (expIdx !== -1) {
278
+ body = body.slice(0, expIdx) + "." + body.slice(expIdx);
279
+ } else {
280
+ body = body + ".";
281
+ }
282
+ }
283
+ }
284
+
285
+ // Alt flag for 'g': keep trailing zeros but ensure decimal point
286
+ if ((spec.flags & FLAG_HASH) && lower === 103) {
287
+ if (body.indexOf(".") === -1) {
288
+ const expIdx = findExpIndex(body);
289
+ if (expIdx !== -1) {
290
+ body = body.slice(0, expIdx) + "." + body.slice(expIdx);
291
+ } else {
292
+ body = body + ".";
293
+ }
294
+ }
295
+ }
296
+
297
+ let result: string;
298
+ if (neg) {
299
+ result = "-" + body;
300
+ } else {
301
+ result = addSign(body, spec.flags);
302
+ }
303
+
304
+ return pad(result, spec.width, spec.flags, true);
305
+ }
306
+
307
+ function findExpIndex(s: string): number {
308
+ for (let i = 0; i < s.length; i++) {
309
+ const c = s.charCodeAt(i);
310
+ if (c === 101 || c === 69) return i; // 'e' or 'E'
311
+ }
312
+ return -1;
313
+ }
314
+
315
+ // Ensure exponent part has at least 2 digits
316
+ function ensureExpTwoDigits(s: string): string {
317
+ const idx = findExpIndex(s);
318
+ if (idx === -1) return s;
319
+ // idx+1 is sign, idx+2... are digits
320
+ const signIdx = idx + 1;
321
+ if (signIdx >= s.length) return s;
322
+ const digitStart = signIdx + 1;
323
+ const expLen = s.length - digitStart;
324
+ if (expLen < 2) {
325
+ return s.slice(0, digitStart) + "0" + s.slice(digitStart);
326
+ }
327
+ return s;
328
+ }
329
+
330
+ // Strip trailing zeros from '%g' output
331
+ function stripTrailingZerosG(s: string): string {
332
+ const expIdx = findExpIndex(s);
333
+ const mantissa = expIdx !== -1 ? s.slice(0, expIdx) : s;
334
+ const exp = expIdx !== -1 ? s.slice(expIdx) : "";
335
+
336
+ const dotIdx = mantissa.indexOf(".");
337
+ if (dotIdx === -1) return s; // nothing to strip
338
+
339
+ let end = mantissa.length;
340
+ while (end > dotIdx + 1 && mantissa.charCodeAt(end - 1) === 48) { // '0'
341
+ end--;
342
+ }
343
+ // Remove dot if nothing after it
344
+ if (end === dotIdx + 1) {
345
+ end = dotIdx;
346
+ }
347
+
348
+ return mantissa.slice(0, end) + exp;
349
+ }
350
+
351
+ // Format a number as hexadecimal floating-point (%a/%A)
352
+ function formatHexFloat(n: number, spec: FormatSpec): string {
353
+ const code = spec.spec;
354
+ const upper = code === 65; // 'A'
355
+
356
+ if (!isFinite(n)) {
357
+ let s: string;
358
+ if (n !== n) {
359
+ s = upper ? "-NAN" : "-nan";
360
+ } else if (n > 0) {
361
+ s = upper ? "INF" : "inf";
362
+ s = addSign(s, spec.flags);
363
+ } else {
364
+ s = upper ? "-INF" : "-inf";
365
+ }
366
+ return pad(s, spec.width, spec.flags, false);
367
+ }
368
+
369
+ const neg = n < 0 || (n === 0 && 1 / n === -Infinity);
370
+ const abs = neg ? -n : n;
371
+
372
+ let body: string;
373
+ if (abs === 0) {
374
+ const prec = spec.hasPrec ? spec.prec : 0;
375
+ if (prec > 0) {
376
+ body = "0x0." + "0".repeat(prec) + "p+0";
377
+ } else {
378
+ body = "0x0p+0";
379
+ }
380
+ } else {
381
+ body = hexFloatBody(abs, spec);
382
+ }
383
+
384
+ if (upper) body = body.toUpperCase();
385
+
386
+ // Alt flag: ensure decimal point
387
+ if (spec.flags & FLAG_HASH) {
388
+ const pIdx = findPIndex(body);
389
+ if (pIdx !== -1) {
390
+ let hasDot = false;
391
+ for (let k = 0; k < pIdx; k++) {
392
+ if (body.charCodeAt(k) === 46) { // '.'
393
+ hasDot = true;
394
+ break;
395
+ }
396
+ }
397
+ if (!hasDot) {
398
+ body = body.slice(0, pIdx) + "." + body.slice(pIdx);
399
+ }
400
+ }
401
+ }
402
+
403
+ let result: string;
404
+ if (neg) {
405
+ result = "-" + body;
406
+ } else {
407
+ result = addSign(body, spec.flags);
408
+ }
409
+
410
+ return pad(result, spec.width, spec.flags, true);
411
+ }
412
+
413
+ // Find index of 'p' or 'P' in hex float string
414
+ function findPIndex(s: string): number {
415
+ for (let i = 0; i < s.length; i++) {
416
+ const c = s.charCodeAt(i);
417
+ if (c === 112 || c === 80) return i; // 'p' or 'P'
418
+ }
419
+ return -1;
420
+ }
421
+
422
+ // Number of bits needed to represent a positive bigint
423
+ function bitLength(n: bigint): number {
424
+ let bits = 0;
425
+ let v = n;
426
+ while (v > 0n) {
427
+ bits++;
428
+ v >>= 1n;
429
+ }
430
+ return bits;
431
+ }
432
+
433
+ // Decompose a positive non-zero finite float into `0xH.HHHpN` form
434
+ function hexFloatBody(abs: number, spec: FormatSpec): string {
435
+ const buf = new Float64Array(1);
436
+ const view = new DataView(buf.buffer);
437
+ view.setFloat64(0, abs);
438
+ const bits = view.getBigUint64(0);
439
+ const biasedExp = Number((bits >> 52n) & 0x7FFn);
440
+ const frac = bits & 0xFFFFFFFFFFFFFn;
441
+
442
+ let exponent: number;
443
+ let mantBits: bigint;
444
+
445
+ if (biasedExp === 0) {
446
+ // Subnormal
447
+ if (frac === 0n) return "0x0p+0";
448
+ const shift = 52 - bitLength(frac) + 1;
449
+ mantBits = frac << BigInt(shift);
450
+ exponent = -1022 - shift;
451
+ } else {
452
+ // Normal
453
+ exponent = biasedExp - 1023;
454
+ mantBits = frac | (1n << 52n);
455
+ }
456
+
457
+ let firstDigit = Number(mantBits >> 52n);
458
+ const restBits = mantBits & ((1n << 52n) - 1n);
459
+
460
+ // 13 hex digits from 52 bits
461
+ let fracHex = hexDigits52(restBits);
462
+
463
+ if (spec.hasPrec) {
464
+ if (spec.prec < 13) {
465
+ const carry = roundHexInPlace(fracHex, spec.prec);
466
+ if (carry) firstDigit++;
467
+ fracHex = truncHexDigits(fracHex, spec.prec);
468
+ } else {
469
+ fracHex = padHexRight(fracHex, spec.prec);
470
+ }
471
+ } else {
472
+ fracHex = stripHexTrailingZeros(fracHex);
473
+ }
474
+
475
+ const expSign = exponent >= 0 ? "+" : "";
476
+ if (fracHex.length > 0) {
477
+ return "0x" + firstDigit + "." + fracHex + "p" + expSign + exponent;
478
+ }
479
+ return "0x" + firstDigit + "p" + expSign + exponent;
480
+ }
481
+
482
+ // Convert 52-bit value to 13 hex digits, zero-padded
483
+ function hexDigits52(bits: bigint): string {
484
+ const s = bits.toString(16);
485
+ if (s.length >= 13) return s;
486
+ return "0".repeat(13 - s.length) + s;
487
+ }
488
+
489
+ // Parse one hex char to its numeric value
490
+ function hexVal(c: number): number {
491
+ if (c >= 48 && c <= 57) return c - 48; // '0'..'9'
492
+ if (c >= 97 && c <= 102) return c - 87; // 'a'..'f'
493
+ if (c >= 65 && c <= 70) return c - 55; // 'A'..'F'
494
+ return 0;
495
+ }
496
+
497
+ function roundHexInPlace(digits: string, prec: number): boolean {
498
+ if (prec >= digits.length) return false;
499
+
500
+ const nextVal = hexVal(digits.charCodeAt(prec));
501
+ if (nextVal < 8) return false;
502
+
503
+ if (prec === 0) return true;
504
+
505
+ const arr = new Array<number>(prec);
506
+ for (let i = 0; i < prec; i++) {
507
+ arr[i] = hexVal(digits.charCodeAt(i));
508
+ }
509
+
510
+ let carry = 1;
511
+ for (let i = prec - 1; i >= 0 && carry; i--) {
512
+ arr[i] += carry;
513
+ if (arr[i] >= 16) {
514
+ arr[i] = 0;
515
+ carry = 1;
516
+ } else {
517
+ carry = 0;
518
+ }
519
+ }
520
+
521
+ return carry === 1;
522
+ }
523
+
524
+ function truncHexDigits(digits: string, prec: number): string {
525
+ if (prec === 0) return "";
526
+
527
+ const nextVal = hexVal(digits.charCodeAt(prec));
528
+ if (nextVal < 8) return digits.slice(0, prec);
529
+
530
+ const arr = new Array<number>(prec);
531
+ for (let i = 0; i < prec; i++) {
532
+ arr[i] = hexVal(digits.charCodeAt(i));
533
+ }
534
+
535
+ let carry = 1;
536
+ for (let i = prec - 1; i >= 0 && carry; i--) {
537
+ arr[i] += carry;
538
+ if (arr[i] >= 16) {
539
+ arr[i] = 0;
540
+ } else {
541
+ carry = 0;
542
+ }
543
+ }
544
+
545
+ let out = "";
546
+ for (let i = 0; i < prec; i++) {
547
+ out += arr[i].toString(16);
548
+ }
549
+
550
+ return out;
551
+ }
552
+
553
+ function padHexRight(s: string, len: number): string {
554
+ if (s.length >= len) return s;
555
+ return s + "0".repeat(len - s.length);
556
+ }
557
+
558
+ function stripHexTrailingZeros(s: string): string {
559
+ let end = s.length;
560
+ while (end > 0 && s.charCodeAt(end - 1) === 48) { // '0'
561
+ end--;
562
+ }
563
+ if (end === s.length) return s;
564
+ return s.slice(0, end);
565
+ }
566
+
567
+ function quoteString(s: string): string {
568
+ let out = '"';
569
+ for (let i = 0; i < s.length; i++) {
570
+ const c = s.charCodeAt(i);
571
+ if (c === 34 || c === 92 || c === 10) {
572
+ // '"', '\\', '\n': backslash + literal char
573
+ out += "\\";
574
+ out += String.fromCharCode(c);
575
+ } else if (c < 32) {
576
+ const next = i + 1 < s.length ? s.charCodeAt(i + 1) : -1;
577
+ const isNextDigit = next >= 48 && next <= 57;
578
+ if (isNextDigit) {
579
+ const ds = c.toString();
580
+ out += "\\";
581
+ if (ds.length < 3) out += "0".repeat(3 - ds.length);
582
+ out += ds;
583
+ } else {
584
+ out += "\\" + c.toString();
585
+ }
586
+ } else {
587
+ out += String.fromCharCode(c);
588
+ }
589
+ }
590
+ out += '"';
591
+ return out;
592
+ }
593
+
594
+ // Format a float for %q: hex representation preserving full precision
595
+ function quoteFloat(n: number): string {
596
+ if (n !== n) return "(0/0)";
597
+ if (n === Infinity) return "1e9999";
598
+ if (n === -Infinity) return "-1e9999";
599
+
600
+ const spec: FormatSpec = {
601
+ flags: 0,
602
+ width: 0,
603
+ hasPrec: false,
604
+ prec: 0,
605
+ spec: 97, // 'a'
606
+ };
607
+ return formatHexFloat(n, spec);
608
+ }
609
+
610
+ function formatQ(v: unknown): string {
611
+ if (v === null || v === undefined) return "nil";
612
+ if (v === true) return "true";
613
+ if (v === false) return "false";
614
+
615
+ if (typeof v === "number") {
616
+ if (v === 0 && 1 / v === -Infinity) return quoteFloat(v);
617
+ if (Number.isInteger(v) && Number.isFinite(v)) {
618
+ return v.toString();
619
+ }
620
+ return quoteFloat(v);
621
+ }
622
+
623
+ return quoteString(String(v));
624
+ }
625
+
626
+ function formatChar(n: number): string {
627
+ return String.fromCharCode(n & 0x7f);
628
+ }
629
+
630
+ const objectIds = new WeakMap<WeakKey, number>();
631
+ const stringIds = new Map<string, number>();
632
+ let nextId = 1;
633
+
634
+ function toPointer(v: unknown): string {
635
+ if (v === null || v === undefined) return "(null)";
636
+ if (typeof v === "boolean" || typeof v === "number") return "(null)";
637
+
638
+ // Primitives (strings, symbols, etc.) cannot be `WeakMap` keys
639
+ if (typeof v !== "object" && typeof v !== "function") {
640
+ const key = String(v);
641
+ let id = stringIds.get(key);
642
+ if (id === undefined) {
643
+ id = nextId++;
644
+ stringIds.set(key, id);
645
+ }
646
+ return "0x" + id.toString(16).padStart(14, "0");
647
+ }
648
+
649
+ const obj = v as object;
650
+ let id = objectIds.get(obj);
651
+ if (id === undefined) {
652
+ id = nextId++;
653
+ objectIds.set(obj, id);
654
+ }
655
+ return "0x" + id.toString(16).padStart(14, "0");
656
+ }
657
+
658
+ function formatPointer(v: unknown, spec: FormatSpec): string {
659
+ const s = toPointer(v);
660
+ // `%p` only supports width and '-' flag, no precision
661
+ return pad(s, spec.width, spec.flags, false);
662
+ }
663
+
664
+ export function luaFormat(fmt: string, ...args: any[]): string {
665
+ let out = "";
666
+ let ai = 0; // arg index
667
+ const len = fmt.length;
668
+ let i = 0;
669
+
670
+ while (i < len) {
671
+ const c = fmt.charCodeAt(i);
672
+ if (c !== 37) { // not '%'
673
+ // Fast path: scan for next '%' or end
674
+ let j = i + 1;
675
+ while (j < len && fmt.charCodeAt(j) !== 37) j++;
676
+ out += fmt.slice(i, j);
677
+ i = j;
678
+ continue;
679
+ }
680
+
681
+ // '%' found
682
+ i++;
683
+ if (i >= len) {
684
+ throw new Error("invalid format (ends with '%')");
685
+ }
686
+
687
+ // '%%' into literal '%'
688
+ if (fmt.charCodeAt(i) === 37) {
689
+ out += "%";
690
+ i++;
691
+ continue;
692
+ }
693
+
694
+ const { spec, end } = parseSpec(fmt, i);
695
+ i = end + 1;
696
+
697
+ // Resolve `*` width and precision from args
698
+ let width = spec.width;
699
+ if (width === -1) {
700
+ width = Number(args[ai++]) || 0;
701
+ if (width < 0) {
702
+ spec.flags |= FLAG_MINUS;
703
+ width = -width;
704
+ }
705
+ spec.width = width;
706
+ }
707
+ if (spec.prec === -1) {
708
+ let p = Number(args[ai++]) || 0;
709
+ if (p < 0) {
710
+ spec.hasPrec = false;
711
+ p = 0;
712
+ }
713
+ spec.prec = p;
714
+ }
715
+
716
+ const code = spec.spec;
717
+ switch (code) {
718
+ case 97:
719
+ case 65: // 'a', 'A'
720
+ out += formatHexFloat(Number(args[ai++]), spec);
721
+ break;
722
+ case 100:
723
+ case 105:
724
+ case 117: // 'd', 'i', 'u'
725
+ case 111:
726
+ case 120:
727
+ case 88: // 'o', 'x', 'X'
728
+ out += formatInt(Number(args[ai++]), spec);
729
+ break;
730
+ case 102:
731
+ case 101:
732
+ case 69: // 'f', 'e', 'E'
733
+ case 103:
734
+ case 71:
735
+ case 70: // 'g', 'G', 'F'
736
+ out += formatFloat(Number(args[ai++]), spec);
737
+ break;
738
+ case 99: // 'c'
739
+ out += pad(
740
+ formatChar(Number(args[ai++])),
741
+ spec.width,
742
+ spec.flags,
743
+ false,
744
+ );
745
+ break;
746
+ case 112: { // 'p'
747
+ out += formatPointer(args[ai++], spec);
748
+ break;
749
+ }
750
+ case 113: { // 'q'
751
+ out += formatQ(args[ai++]);
752
+ break;
753
+ }
754
+ case 115: { // 's'
755
+ let s = String(args[ai++]);
756
+ if (spec.hasPrec && s.length > spec.prec) {
757
+ s = s.slice(0, spec.prec);
758
+ }
759
+ out += pad(s, spec.width, spec.flags, false);
760
+ break;
761
+ }
762
+ default:
763
+ throw new Error(
764
+ `invalid format specifier '${String.fromCharCode(code)}'`,
765
+ );
766
+ }
767
+ }
768
+
769
+ return out;
770
+ }