@silverbulletmd/silverbullet 2.4.2 → 2.6.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 (97) hide show
  1. package/README.md +19 -4
  2. package/client/asset_bundle/bundle.ts +3 -9
  3. package/client/data/datastore.ts +4 -5
  4. package/client/markdown_parser/constants.ts +5 -4
  5. package/client/plugos/hooks/code_widget.ts +3 -8
  6. package/client/plugos/hooks/command.ts +8 -8
  7. package/client/plugos/hooks/document_editor.ts +10 -15
  8. package/client/plugos/hooks/event.ts +33 -36
  9. package/client/plugos/hooks/mq.ts +17 -17
  10. package/client/plugos/hooks/plug_namespace.ts +3 -8
  11. package/client/plugos/hooks/slash_command.ts +13 -28
  12. package/client/plugos/hooks/syscall.ts +3 -3
  13. package/client/plugos/manifest_cache.ts +22 -15
  14. package/client/plugos/plug.ts +2 -6
  15. package/client/plugos/plug_compile.ts +79 -78
  16. package/client/plugos/protocol.ts +28 -28
  17. package/client/plugos/proxy_fetch.ts +7 -6
  18. package/client/plugos/sandboxes/web_worker_sandbox.ts +1 -1
  19. package/client/plugos/sandboxes/worker_sandbox.ts +18 -18
  20. package/client/plugos/syscalls/asset.ts +1 -3
  21. package/client/plugos/syscalls/code_widget.ts +1 -3
  22. package/client/plugos/syscalls/config.ts +1 -5
  23. package/client/plugos/syscalls/datastore.ts +1 -1
  24. package/client/plugos/syscalls/editor.ts +72 -69
  25. package/client/plugos/syscalls/event.ts +9 -12
  26. package/client/plugos/syscalls/fetch.ts +31 -23
  27. package/client/plugos/syscalls/index.ts +10 -1
  28. package/client/plugos/syscalls/jsonschema.ts +72 -32
  29. package/client/plugos/syscalls/language.ts +9 -5
  30. package/client/plugos/syscalls/markdown.ts +29 -7
  31. package/client/plugos/syscalls/mq.ts +4 -12
  32. package/client/plugos/syscalls/service_registry.ts +1 -4
  33. package/client/plugos/syscalls/shell.ts +2 -5
  34. package/client/plugos/syscalls/space.ts +1 -1
  35. package/client/plugos/syscalls/sync.ts +69 -60
  36. package/client/plugos/syscalls/system.ts +2 -3
  37. package/client/plugos/system.ts +6 -12
  38. package/client/plugos/worker_runtime.ts +12 -33
  39. package/client/space_lua/aggregates.ts +782 -0
  40. package/client/space_lua/ast.ts +42 -8
  41. package/client/space_lua/ast_narrow.ts +4 -2
  42. package/client/space_lua/eval.ts +886 -575
  43. package/client/space_lua/labels.ts +7 -12
  44. package/client/space_lua/liq_null.ts +6 -0
  45. package/client/space_lua/numeric.ts +5 -8
  46. package/client/space_lua/parse.ts +346 -120
  47. package/client/space_lua/query_collection.ts +926 -82
  48. package/client/space_lua/query_env.ts +26 -0
  49. package/client/space_lua/render_lua_markdown.ts +369 -0
  50. package/client/space_lua/rp.ts +5 -4
  51. package/client/space_lua/runtime.ts +288 -155
  52. package/client/space_lua/stdlib/format.ts +53 -39
  53. package/client/space_lua/stdlib/js.ts +3 -7
  54. package/client/space_lua/stdlib/load.ts +1 -3
  55. package/client/space_lua/stdlib/math.ts +84 -58
  56. package/client/space_lua/stdlib/net.ts +27 -17
  57. package/client/space_lua/stdlib/os.ts +81 -85
  58. package/client/space_lua/stdlib/pattern.ts +695 -0
  59. package/client/space_lua/stdlib/prng.ts +148 -0
  60. package/client/space_lua/stdlib/space_lua.ts +17 -23
  61. package/client/space_lua/stdlib/string.ts +102 -190
  62. package/client/space_lua/stdlib/string_pack.ts +490 -0
  63. package/client/space_lua/stdlib/table.ts +76 -16
  64. package/client/space_lua/stdlib.ts +53 -39
  65. package/client/space_lua/tonumber.ts +82 -42
  66. package/client/space_lua/util.ts +53 -15
  67. package/dist/plug-compile.js +55 -98
  68. package/package.json +27 -20
  69. package/plug-api/constants.ts +0 -32
  70. package/plug-api/lib/async.ts +20 -7
  71. package/plug-api/lib/crypto.ts +16 -17
  72. package/plug-api/lib/dates.ts +15 -7
  73. package/plug-api/lib/json.ts +11 -5
  74. package/plug-api/lib/limited_map.ts +1 -1
  75. package/plug-api/lib/native_fetch.ts +2 -0
  76. package/plug-api/lib/ref.ts +23 -23
  77. package/plug-api/lib/resolve.ts +7 -11
  78. package/plug-api/lib/tags.ts +13 -4
  79. package/plug-api/lib/transclusion.ts +10 -21
  80. package/plug-api/lib/tree.ts +165 -45
  81. package/plug-api/lib/yaml.ts +35 -25
  82. package/plug-api/syscalls/asset.ts +1 -1
  83. package/plug-api/syscalls/config.ts +1 -4
  84. package/plug-api/syscalls/editor.ts +15 -15
  85. package/plug-api/syscalls/jsonschema.ts +1 -3
  86. package/plug-api/syscalls/lua.ts +3 -9
  87. package/plug-api/syscalls/mq.ts +1 -4
  88. package/plug-api/syscalls/shell.ts +4 -1
  89. package/plug-api/syscalls/space.ts +3 -10
  90. package/plug-api/syscalls/system.ts +1 -4
  91. package/plug-api/syscalls/yaml.ts +2 -6
  92. package/plug-api/system_mock.ts +0 -1
  93. package/plug-api/types/client.ts +16 -1
  94. package/plug-api/types/event.ts +6 -4
  95. package/plug-api/types/manifest.ts +8 -9
  96. package/plugs/builtin_plugs.ts +2 -2
  97. package/client/plugos/sandboxes/deno_worker_sandbox.ts +0 -6
@@ -0,0 +1,148 @@
1
+ // PRNG based on xoshiro256** for Space Lua
2
+
3
+ export class LuaPRNG {
4
+ private state: BigUint64Array;
5
+
6
+ constructor() {
7
+ this.state = new BigUint64Array(4);
8
+ this.autoSeed();
9
+ }
10
+
11
+ private rotl(x: bigint, k: number): bigint {
12
+ k = k & 63;
13
+ return ((x << BigInt(k)) | (x >> BigInt(64 - k))) & 0xffffffffffffffffn;
14
+ }
15
+
16
+ private nextrand(): bigint {
17
+ const s = this.state;
18
+ const s0 = s[0];
19
+ const s1 = s[1];
20
+ const s2 = s[2];
21
+ const s3 = s[3];
22
+
23
+ const res =
24
+ (this.rotl((s1 * 5n) & 0xffffffffffffffffn, 7) * 9n) &
25
+ 0xffffffffffffffffn;
26
+
27
+ const t = (s1 << 17n) & 0xffffffffffffffffn;
28
+ s[2] = s2 ^ s0;
29
+ s[3] = s3 ^ s1;
30
+ s[1] = s1 ^ s[2];
31
+ s[0] = s0 ^ s[3];
32
+ s[2] = s[2] ^ t;
33
+ s[3] = this.rotl(s[3], 45);
34
+
35
+ return res;
36
+ }
37
+
38
+ public setSeed(seed1: bigint, seed2: bigint = 0n): [bigint, bigint] {
39
+ const MASK = 0xffffffffffffffffn;
40
+ const s = this.state;
41
+
42
+ const sm64 = (x: bigint): bigint => {
43
+ x = ((x ^ (x >> 30n)) * 0xbf58476d1ce4e5b9n) & MASK;
44
+ x = ((x ^ (x >> 27n)) * 0x94d049bb133111ebn) & MASK;
45
+ return (x ^ (x >> 31n)) & MASK;
46
+ };
47
+
48
+ s[0] = sm64(seed1 & MASK);
49
+ s[1] = sm64((seed1 & MASK) | 0xffn);
50
+ s[2] = sm64(seed2 & MASK);
51
+ s[3] = sm64(0n);
52
+
53
+ for (let i = 0; i < 16; i++) {
54
+ this.nextrand();
55
+ }
56
+
57
+ return [seed1, seed2];
58
+ }
59
+
60
+ private autoSeed(): [bigint, bigint] {
61
+ const t = BigInt(Date.now());
62
+ const entropy = BigInt(Math.floor(performance.now() * 1000));
63
+ return this.setSeed(t, entropy);
64
+ }
65
+
66
+ private project(ran: bigint, n: bigint): bigint {
67
+ if (n === 0n) return 0n;
68
+
69
+ let lim = n;
70
+ lim |= lim >> 1n;
71
+ lim |= lim >> 2n;
72
+ lim |= lim >> 4n;
73
+ lim |= lim >> 8n;
74
+ lim |= lim >> 16n;
75
+ lim |= lim >> 32n;
76
+
77
+ while (true) {
78
+ ran &= lim;
79
+ if (ran <= n) return ran;
80
+ ran = this.nextrand();
81
+ }
82
+ }
83
+
84
+ // `random()` yields float in [0, 1)
85
+ // `random(0)` yields raw 64-bit signed integer (all bits random)
86
+ // `random(n)` yields integer in [1, n]
87
+ // `random(m, n)` yields integer in [m, n]
88
+ public random(arg1?: number, arg2?: number): number | bigint {
89
+ const rv = this.nextrand();
90
+
91
+ if (arg1 === undefined) {
92
+ // Top 53 bits for full double precision
93
+ return Number(rv >> 11n) * (1.0 / 9007199254740992.0);
94
+ }
95
+
96
+ if (!Number.isFinite(arg1) || !Number.isInteger(arg1)) {
97
+ throw new Error(
98
+ "bad argument #1 to 'random' (number has no integer representation)",
99
+ );
100
+ }
101
+
102
+ if (arg2 === undefined) {
103
+ if (arg1 === 0) {
104
+ // Raw 64-bit as signed bigint
105
+ const signed =
106
+ rv > 0x7fffffffffffffffn ? rv - 0x10000000000000000n : rv;
107
+ return signed;
108
+ }
109
+ if (arg1 < 1) {
110
+ throw new Error("bad argument #1 to 'random' (interval is empty)");
111
+ }
112
+ return Number(this.project(rv, BigInt(arg1) - 1n) + 1n);
113
+ }
114
+
115
+ if (!Number.isFinite(arg2) || !Number.isInteger(arg2)) {
116
+ throw new Error(
117
+ "bad argument #2 to 'random' (number has no integer representation)",
118
+ );
119
+ }
120
+ if (arg2 < arg1) {
121
+ throw new Error("bad argument #2 to 'random' (interval is empty)");
122
+ }
123
+ return Number(this.project(rv, BigInt(arg2) - BigInt(arg1)) + BigInt(arg1));
124
+ }
125
+
126
+ // Returns [seed1, seed2]
127
+ public randomseed(arg1?: number, arg2?: number): [bigint, bigint] {
128
+ if (arg1 === undefined) {
129
+ return this.autoSeed();
130
+ }
131
+ if (!Number.isFinite(arg1) || !Number.isInteger(arg1)) {
132
+ throw new Error(
133
+ "bad argument #1 to 'randomseed' (number has no integer representation)",
134
+ );
135
+ }
136
+ if (
137
+ arg2 !== undefined &&
138
+ (!Number.isFinite(arg2) || !Number.isInteger(arg2))
139
+ ) {
140
+ throw new Error(
141
+ "bad argument #2 to 'randomseed' (number has no integer representation)",
142
+ );
143
+ }
144
+ const s1 = BigInt(Math.trunc(arg1));
145
+ const s2 = arg2 !== undefined ? BigInt(Math.trunc(arg2)) : 0n;
146
+ return this.setSeed(s1, s2);
147
+ }
148
+ }
@@ -2,6 +2,7 @@ import { parseExpressionString } from "../parse.ts";
2
2
  import type { LuaExpression } from "../ast.ts";
3
3
  import { evalExpression } from "../eval.ts";
4
4
  import {
5
+ jsToLuaValue,
5
6
  LuaBuiltinFunction,
6
7
  LuaEnv,
7
8
  LuaRuntimeError,
@@ -11,6 +12,7 @@ import {
11
12
  luaValueToJS,
12
13
  singleResult,
13
14
  } from "../runtime.ts";
15
+ import { isSqlNull } from "../liq_null.ts";
14
16
 
15
17
  /**
16
18
  * These are Space Lua specific functions that are available to all scripts, but are not part of the standard Lua language.
@@ -31,7 +33,8 @@ function createAugmentedEnv(
31
33
  if (envAugmentation) {
32
34
  env.setLocal("_", envAugmentation);
33
35
  for (const key of envAugmentation.keys()) {
34
- env.setLocal(key, envAugmentation.rawGet(key));
36
+ const v = envAugmentation.rawGet(key);
37
+ env.setLocal(key, isSqlNull(v) ? null : v);
35
38
  }
36
39
  }
37
40
  return env;
@@ -89,10 +92,7 @@ export async function interpolateLuaString(
89
92
  const luaResult = singleResult(await evalExpression(parsedExpr, env, sf));
90
93
  result += await luaToString(luaResult);
91
94
  } catch (e: any) {
92
- throw new LuaRuntimeError(
93
- `Error evaluating "${expr}": ${e.message}`,
94
- sf,
95
- );
95
+ throw new LuaRuntimeError(`Error evaluating "${expr}": ${e.message}`, sf);
96
96
  }
97
97
 
98
98
  currentIndex = endIndex + 1;
@@ -109,11 +109,9 @@ export const spaceluaApi = new LuaTable({
109
109
  * @param luaExpression - The lua expression to parse.
110
110
  * @returns The parsed expression.
111
111
  */
112
- parseExpression: new LuaBuiltinFunction(
113
- (_sf, luaExpression: string) => {
114
- return parseExpressionString(luaExpression);
115
- },
116
- ),
112
+ parseExpression: new LuaBuiltinFunction((_sf, luaExpression: string) => {
113
+ return parseExpressionString(luaExpression);
114
+ }),
117
115
  /**
118
116
  * Evaluates a parsed lua expression and returns the result.
119
117
  *
@@ -132,22 +130,18 @@ export const spaceluaApi = new LuaTable({
132
130
  * Interpolates a string with lua expressions and returns the result.
133
131
  */
134
132
  interpolate: new LuaBuiltinFunction(
135
- (sf, template: string, envAugmentation?: LuaTable) => {
133
+ (sf, template: string, envAugmentation?: LuaTable | any) => {
134
+ if (envAugmentation && !(envAugmentation instanceof LuaTable)) {
135
+ envAugmentation = jsToLuaValue(envAugmentation);
136
+ }
136
137
  return interpolateLuaString(sf, template, envAugmentation);
137
138
  },
138
139
  ),
139
140
  /**
140
- * Returns your SilverBullet instance's base URL, or `undefined` when run on the server
141
+ * Returns your SilverBullet instance's base URL
141
142
  */
142
- baseUrl: new LuaBuiltinFunction(
143
- () => {
144
- // Deal with Deno
145
- if (typeof location === "undefined") {
146
- return null;
147
- } else {
148
- //NOTE: Removing trailing slash to stay compatible with original code: `location.protocol + "//" + location.host;`
149
- return document.baseURI.replace(/\/*$/, "");
150
- }
151
- },
152
- ),
143
+ baseUrl: new LuaBuiltinFunction(() => {
144
+ //NOTE: Removing trailing slash to stay compatible with original code: `location.protocol + "//" + location.host;`
145
+ return document.baseURI.replace(/\/*$/, "");
146
+ }),
153
147
  });
@@ -6,79 +6,33 @@ import {
6
6
  LuaTable,
7
7
  luaToString,
8
8
  } from "../runtime.ts";
9
- import { untagNumber } from "../numeric.ts";
9
+ import { isTaggedFloat, untagNumber } from "../numeric.ts";
10
10
  import { luaFormat } from "./format.ts";
11
-
12
- // Bits and pieces borrowed from https://github.com/paulcuth/starlight/blob/master/src/runtime/lib/string.js
13
-
14
- const ROSETTA_STONE = {
15
- "([^a-zA-Z0-9%(])-": "$1*?",
16
- "([^%])-([^a-zA-Z0-9?])": "$1*?$2",
17
- "([^%])-$": "$1*?",
18
- "%a": "[a-zA-Z]",
19
- "%A": "[^a-zA-Z]",
20
- "%c": "[\x00-\x1f]",
21
- "%C": "[^\x00-\x1f]",
22
- "%d": "\\d",
23
- "%D": "[^\d]",
24
- "%l": "[a-z]",
25
- "%L": "[^a-z]",
26
- "%p": "[\.\,\"'\?\!\;\:\#\$\%\&\(\)\*\+\-\/\<\>\=\@\\[\\]\\\\^\_\{\}\|\~]",
27
- "%P": "[^\.\,\"'\?\!\;\:\#\$\%\&\(\)\*\+\-\/\<\>\=\@\\[\\]\\\\^\_\{\}\|\~]",
28
- "%s": "[ \\t\\n\\f\\v\\r]",
29
- "%S": "[^ \t\n\f\v\r]",
30
- "%u": "[A-Z]",
31
- "%U": "[^A-Z]",
32
- "%w": "[a-zA-Z0-9]",
33
- "%W": "[^a-zA-Z0-9]",
34
- "%x": "[a-fA-F0-9]",
35
- "%X": "[^a-fA-F0-9]",
36
- "%([^a-zA-Z])": "\\$1",
37
- };
38
-
39
- function translatePattern(pattern: string): string {
40
- pattern = "" + pattern;
41
-
42
- // Replace single backslash with double backslashes
43
- pattern = pattern.replace(new RegExp("\\\\", "g"), "\\\\");
44
- pattern = pattern.replace(new RegExp("\\|", "g"), "\\|");
45
-
46
- for (const [key, value] of Object.entries(ROSETTA_STONE)) {
47
- pattern = pattern.replace(new RegExp(key, "g"), value);
48
- }
49
-
50
- let l = pattern.length;
51
- let n = 0;
52
-
53
- for (let i = 0; i < l; i++) {
54
- const character = pattern.slice(i, 1);
55
- if (i && pattern.slice(i - 1, 1) == "\\") {
56
- continue;
57
- }
58
-
59
- let addSlash = false;
60
-
61
- if (character == "[") {
62
- if (n) addSlash = true;
63
- n++;
64
- } else if (character == "]" && pattern.slice(i - 1, 1) !== "\\") {
65
- n--;
66
- if (n) addSlash = true;
67
- }
68
-
69
- if (addSlash) {
70
- pattern = pattern.slice(0, i) + pattern.slice(i++ + 1);
71
- l++;
72
- }
11
+ import {
12
+ type CaptureResult,
13
+ type GsubCallbacks,
14
+ patternFind,
15
+ patternGmatch,
16
+ patternGsub,
17
+ patternMatch,
18
+ } from "./pattern.ts";
19
+ import { strPackFn, strPackSizeFn, strUnpackFn } from "./string_pack.ts";
20
+
21
+ function capturesToLua(caps: CaptureResult[]): any {
22
+ if (caps.length === 0) return null;
23
+ if (caps.length === 1) {
24
+ const c = caps[0];
25
+ return "s" in c ? c.s : c.position;
73
26
  }
74
-
75
- return pattern;
27
+ return new LuaMultiRes(caps.map((c) => ("s" in c ? c.s : c.position)));
76
28
  }
77
29
 
78
30
  export const stringApi = new LuaTable({
79
31
  byte: new LuaBuiltinFunction((_sf, s: string, i?: number, j?: number) => {
80
32
  i = i ?? 1;
81
33
  j = j ?? i;
34
+ if (j > s.length) j = s.length;
35
+ if (i < 1) i = 1;
82
36
  const result = [];
83
37
  for (let k = i; k <= j; k++) {
84
38
  result.push(s.charCodeAt(k - 1));
@@ -90,119 +44,64 @@ export const stringApi = new LuaTable({
90
44
  }),
91
45
  find: new LuaBuiltinFunction(
92
46
  (_sf, s: string, pattern: string, init = 1, plain = false) => {
93
- // Regex
94
- if (!plain) {
95
- pattern = translatePattern(pattern);
96
- const reg = new RegExp(pattern);
97
- const index = s.slice(init - 1).search(reg);
98
-
99
- if (index < 0) return null;
100
-
101
- const match = s.slice(init - 1).match(reg);
102
- const result = [index + init, index + init + match![0].length - 1];
103
-
104
- match!.shift();
105
- return new LuaMultiRes(result.concat(match));
47
+ const r = patternFind(s, pattern, init, plain);
48
+ if (!r) return null;
49
+ const result: any[] = [r.start, r.end];
50
+ for (const c of r.captures) {
51
+ result.push("s" in c ? c.s : c.position);
106
52
  }
107
-
108
- // Plain
109
- const index = s.indexOf(pattern, init - 1);
110
- return (index === -1)
111
- ? null
112
- : new LuaMultiRes([index + 1, index + pattern.length]);
53
+ return new LuaMultiRes(result);
113
54
  },
114
55
  ),
115
56
  format: new LuaBuiltinFunction((_sf, format: string, ...args: any[]) => {
116
- // Unwrap tagged floats so luaFormat sees plain numbers
117
57
  for (let i = 0; i < args.length; i++) {
118
58
  args[i] = untagNumber(args[i]);
119
59
  }
120
60
  return luaFormat(format, ...args);
121
61
  }),
122
- gmatch: new LuaBuiltinFunction((_sf, s: string, pattern: string) => {
123
- pattern = translatePattern(pattern);
124
- const reg = new RegExp(pattern, "g"),
125
- matches = s.match(reg);
126
- return () => {
127
- if (!matches) {
128
- return;
129
- }
130
- const match = matches.shift();
131
- if (!match) {
132
- return;
133
- }
134
- const groups = new RegExp(pattern).exec(match) || [];
135
-
136
- groups.shift();
137
- return groups.length ? new LuaMultiRes(groups) : match;
138
- };
139
- }),
62
+ gmatch: new LuaBuiltinFunction(
63
+ (_sf, s: string, pattern: string, init = 1) => {
64
+ const iter = patternGmatch(s, pattern, init);
65
+ return () => {
66
+ const caps = iter();
67
+ if (!caps) return;
68
+ return capturesToLua(caps);
69
+ };
70
+ },
71
+ ),
140
72
  gsub: new LuaBuiltinFunction(
141
- async (
142
- sf,
143
- s: string,
144
- pattern: string,
145
- repl: any, // string or LuaFunction
146
- n = Infinity,
147
- ) => {
148
- pattern = translatePattern(pattern);
149
- const replIsFunction = repl.call;
150
-
151
- let count = 0,
152
- result = "",
153
- str,
154
- prefix,
155
- match: any,
156
- lastMatch;
157
-
158
- while (
159
- count < n &&
160
- s &&
161
- (match = s.match(pattern))
162
- ) {
163
- if (replIsFunction) {
164
- // If no captures, pass in the whole match
165
- if (match[1] === undefined) {
166
- str = await repl.call(sf, match[0]);
167
- } else {
168
- // Else pass in the captures
169
- str = await repl.call(sf, ...match.slice(1));
170
- }
171
- if (str instanceof LuaMultiRes) {
172
- str = str.values[0];
173
- }
174
- if (str === undefined || str === null) {
175
- str = match[0];
73
+ async (sf, s: string, pattern: string, repl: any, n?: number) => {
74
+ const callbacks: GsubCallbacks = {};
75
+ if (typeof repl === "string") {
76
+ callbacks.replString = repl;
77
+ } else if (repl instanceof LuaTable) {
78
+ callbacks.replTable = (key: string) => {
79
+ const v = repl.get(key);
80
+ if (v === null || v === undefined || v === false) return null;
81
+ return typeof v === "number"
82
+ ? String(v)
83
+ : String(isTaggedFloat(v) ? v.value : v);
84
+ };
85
+ } else if (repl.call) {
86
+ callbacks.replFunction = async (...caps: CaptureResult[]) => {
87
+ const args = caps.map((c) => ("s" in c ? c.s : c.position));
88
+ let result = await repl.call(sf, ...args);
89
+ if (result instanceof LuaMultiRes) {
90
+ result = result.values[0];
176
91
  }
177
- } else if (repl instanceof LuaTable) {
178
- str = repl.get(match[0]);
179
- } else if (typeof repl === "string") {
180
- str = repl.replaceAll(/%([0-9]+)/g, (_, i) => match[i]);
181
- } else {
182
- throw new LuaRuntimeError(
183
- "string.gsub replacement argument should be a function, table or string",
184
- sf,
185
- );
186
- }
187
-
188
- if (match[0].length === 0) {
189
- if (lastMatch === void 0) {
190
- prefix = "";
191
- } else {
192
- prefix = s.slice(0, 1);
92
+ if (result === null || result === undefined || result === false) {
93
+ return null;
193
94
  }
194
- } else {
195
- prefix = s.slice(0, match.index);
196
- }
197
-
198
- lastMatch = match[0];
199
- result += `${prefix}${str}`;
200
- s = s.slice(`${prefix}${lastMatch}`.length);
201
-
202
- count++;
95
+ return luaToString(result);
96
+ };
97
+ } else {
98
+ throw new LuaRuntimeError(
99
+ "string.gsub replacement argument should be a function, table or string",
100
+ sf,
101
+ );
203
102
  }
204
-
205
- return new LuaMultiRes([`${result}${s}`, count]);
103
+ const [result, count] = await patternGsub(s, pattern, callbacks, n);
104
+ return new LuaMultiRes([result, count]);
206
105
  },
207
106
  ),
208
107
  len: new LuaBuiltinFunction((_sf, s: string) => {
@@ -214,45 +113,58 @@ export const stringApi = new LuaTable({
214
113
  upper: new LuaBuiltinFunction((_sf, s: string) => {
215
114
  return luaToString(s.toUpperCase());
216
115
  }),
217
- match: new LuaBuiltinFunction(
218
- (_sf, s: string, pattern: string, init = 1) => {
219
- s = s.slice(init - 1);
220
- const matches = s.match(new RegExp(translatePattern(pattern)));
221
-
222
- if (!matches) {
223
- return null;
224
- }
225
- if (matches[1] === undefined) {
226
- // No captures
227
- return matches[0];
228
- }
229
-
230
- matches.shift();
231
- return new LuaMultiRes(matches);
232
- },
233
- ),
116
+ match: new LuaBuiltinFunction((_sf, s: string, pattern: string, init = 1) => {
117
+ const caps = patternMatch(s, pattern, init);
118
+ if (!caps) return null;
119
+ return capturesToLua(caps);
120
+ }),
234
121
  rep: new LuaBuiltinFunction((_sf, s: string, n: number, sep?: string) => {
122
+ if (n <= 0) return "";
235
123
  sep = sep ?? "";
236
- return s.repeat(n) + sep;
124
+ const parts: string[] = [];
125
+ for (let i = 0; i < n; i++) {
126
+ parts.push(s);
127
+ }
128
+ return parts.join(sep);
237
129
  }),
238
130
  reverse: new LuaBuiltinFunction((_sf, s: string) => {
239
131
  return s.split("").reverse().join("");
240
132
  }),
241
133
  sub: new LuaBuiltinFunction((_sf, s: string, i: number, j?: number) => {
242
- j = j ?? s.length;
243
- if (i < 0) {
244
- i = s.length + i + 1;
134
+ const len = s.length;
135
+ let start: number;
136
+ if (i > 0) {
137
+ start = i;
138
+ } else if (i < -len) {
139
+ start = 1;
140
+ } else {
141
+ start = i === 0 ? 1 : len + i + 1;
245
142
  }
246
- if (j < 0) {
247
- j = s.length + j + 1;
143
+ let end: number;
144
+ if (j === undefined || j === null || j > len) {
145
+ end = len;
146
+ } else if (j >= 0) {
147
+ end = j;
148
+ } else if (j < -len) {
149
+ end = 0;
150
+ } else {
151
+ end = len + j + 1;
248
152
  }
249
- return s.slice(i - 1, j);
153
+ if (start <= end) {
154
+ return s.substring(start - 1, end);
155
+ }
156
+ return "";
250
157
  }),
158
+
251
159
  split: new LuaBuiltinFunction((_sf, s: string, sep: string) => {
252
160
  return s.split(sep);
253
161
  }),
254
162
 
255
- // Non-standard
163
+ pack: strPackFn,
164
+ unpack: strUnpackFn,
165
+ packsize: strPackSizeFn,
166
+
167
+ // Non-standard extensions
256
168
  startsWith: new LuaBuiltinFunction((_sf, s: string, prefix: string) => {
257
169
  return s.startsWith(prefix);
258
170
  }),