querysub 0.176.0 → 0.178.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.
package/index.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ /// <reference path="./node_modules/socket-function/index.d.ts" />
2
+
3
+ export { };
4
+
5
+ declare global {
6
+ interface SerializedModule {
7
+ originalSource?: string;
8
+ }
9
+ namespace NodeJS {
10
+ interface Module {
11
+ /** Needed if you are using extractType. */
12
+ sendFullSource?: boolean;
13
+ }
14
+ }
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.176.0",
3
+ "version": "0.178.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -24,7 +24,7 @@
24
24
  "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
25
25
  "pako": "^2.1.0",
26
26
  "preact": "^10.11.3",
27
- "socket-function": "^0.122.0",
27
+ "socket-function": "^0.124.0",
28
28
  "terser": "^5.31.0",
29
29
  "typesafecss": "^0.15.0",
30
30
  "yaml": "^2.5.0",
@@ -96,6 +96,9 @@ export namespace qreact {
96
96
  export type Element = preact.JSX.Element;
97
97
  export type HTMLAttributes<T extends EventTarget = EventTarget> = preact.JSX.HTMLAttributes<T>;
98
98
  }
99
+ export namespace JSXInternal {
100
+ export type TargetedInputEvent<T extends EventTarget = EventTarget> = preact.JSX.TargetedInputEvent<T>;
101
+ }
99
102
  }
100
103
 
101
104
 
@@ -911,7 +914,7 @@ class QRenderClass {
911
914
  }
912
915
 
913
916
  function insertInAnchor(anchor: Anchor, node: DOMNode) {
914
- let prev = document.activeElement;
917
+ //let prevFocused = document.activeElement;
915
918
  if (!anchor) {
916
919
  nextRootDOMNodes.push(node);
917
920
  return;
@@ -958,9 +961,9 @@ class QRenderClass {
958
961
  }
959
962
 
960
963
  anchor.prev.after(node);
961
- if (prev !== document.activeElement) {
962
- debugger;
963
- }
964
+ // if (prevFocused !== document.activeElement) {
965
+ // debugger;
966
+ // }
964
967
  }
965
968
 
966
969
  let isParentSVG = !!atomic(self.data().isParentSVG);
@@ -1859,9 +1862,11 @@ function updateDOMNodeFields(domNode: DOMNode, vNode: VirtualDOM, prevVNode: Vir
1859
1862
  updateField(key);
1860
1863
  }
1861
1864
  } else {
1862
- // NOTE: I'm not sure if updating textContent is better or worse than creating a new Text node?
1863
- // As in, should we use `prevNode.replaceWith(document.createTextNode(result))`?
1865
+ // NOTE: By setting textContent we don't create a new Text node. This helps
1866
+ // contentEditable usages distinguish between our internal nodes, and ones
1867
+ // generated by contentEditable.
1864
1868
  (domNode as any as Text).textContent = String(vNode);
1869
+ //domNode.replaceWith(document.createTextNode(String(vNode)));
1865
1870
  }
1866
1871
  }
1867
1872
 
@@ -10,7 +10,7 @@ import { isNode, isNodeTrue, timeInMinute } from "socket-function/src/misc";
10
10
 
11
11
  import { SocketFunction } from "socket-function/SocketFunction";
12
12
  import { isHotReloading, watchFilesAndTriggerHotReloading } from "socket-function/hot/HotReloadController";
13
- import { RequireController, SerializedModule, setRequireBootRequire } from "socket-function/require/RequireController";
13
+ import { RequireController, setRequireBootRequire } from "socket-function/require/RequireController";
14
14
  import { cache, cacheLimited, lazy } from "socket-function/src/caching";
15
15
  import { getOwnMachineId, getThreadKeyCert, verifyMachineIdForPublicKey } from "../-a-auth/certs";
16
16
  import { getSNICerts, publishMachineARecords } from "../-e-certs/EdgeCertController";
@@ -390,8 +390,9 @@ export class Querysub {
390
390
 
391
391
  public static afterAllRendersFinished(callback: () => void) {
392
392
  // onCommitFinished prevents duplicates, as well as only running when we are actually done
393
- Querysub.onCommitFinished(() => {
394
- void clientWatcher.waitForTriggerFinished()?.finally(callback);
393
+ Querysub.onCommitFinished(async () => {
394
+ await clientWatcher.waitForTriggerFinished();
395
+ callback();
395
396
  });
396
397
  }
397
398
 
@@ -599,6 +600,20 @@ export class Querysub {
599
600
 
600
601
  await this.addSourceMapCheck(config);
601
602
 
603
+ const fs = await import("fs");
604
+
605
+ RequireController.addMapGetModules(async (result, args) => {
606
+ await Promise.all(Object.values(result.modules).map(async (mod) => {
607
+ if (!require.cache[mod.filename]?.sendFullSource) return;
608
+ // NOTE: This is used for extractType... which is very useful. And... giving the source
609
+ // isn't that big of a deal anyways...
610
+ try {
611
+ mod.originalSource = await fs.promises.readFile(mod.filename, "utf8");
612
+ } catch { }
613
+ }));
614
+ return result;
615
+ });
616
+
602
617
  SocketFunction.expose(RequireController);
603
618
  setRequireBootRequire(config.rootPath);
604
619
 
@@ -16,6 +16,15 @@ let cborxInstance = new cborx.Encoder({ structuredClone: true });
16
16
 
17
17
  const MAX_UNDO_STACK = 200;
18
18
 
19
+ /** Can only data created with t.undoRegion, as in:
20
+ values2: t.undoRegion(key => key === getUserId()).lookup(
21
+ {
22
+ key: t.string,
23
+ value: t.string("version10"),
24
+ neverSet: t.stringOptional,
25
+ }
26
+ ),
27
+ */
19
28
  export class UndoWatch extends qreact.Component<{
20
29
  // Tracks changes to this data, and undoes it when ctrl+z is pressed
21
30
  // - Depends on undo regions being defined in the schema, via t.undoRegion
@@ -69,106 +69,109 @@ type Watcher = {
69
69
  let registeredWatches = new Set<Watcher>();
70
70
 
71
71
  if (!isNode()) {
72
- let insideAnims = new Set<Watcher>();
73
- let keyUpListener = new Set<() => void>();
74
72
  document.addEventListener("keyup", e => {
75
73
  for (let listener of keyUpListener) {
76
74
  listener();
77
75
  }
78
76
  });
79
77
  document.addEventListener("keydown", e => {
80
- let isAmbientEvent = (
81
- e.target === document.body
82
- // Some elements are commonly selected, but shouldn't handling key events
83
- || (e.target as HTMLElement).tagName === "BUTTON"
84
- || (e.target as HTMLElement).tagName === "A"
85
- || (e.target as HTMLElement).tagName === "INPUT" && (e.target as HTMLInputElement).type === "checkbox"
86
- //|| (e.target as HTMLElement).tagName === "INPUT" && (e.target as HTMLInputElement).type === "number" && Number.isNaN(+e.key) && !e.key.includes("Arrow")
87
- //|| ["INPUT", "TEXTAREA"].includes((e.target as HTMLElement).tagName)
88
- );
89
- if (e.key === "Escape") {
90
- (document.activeElement as any)?.blur();
91
- }
92
- let keyFull = e.code;
93
- if (keyFull.startsWith("Key")) {
94
- keyFull = keyFull.slice("Key".length);
95
- }
96
- if (e.metaKey) keyFull = "meta+" + keyFull;
97
- if (e.altKey) keyFull = "alt+" + keyFull;
98
- if (e.shiftKey) keyFull = "shift+" + keyFull;
99
- if (e.ctrlKey || e.metaKey) keyFull = "ctrl+" + keyFull;
100
- if (!isAmbientEvent) {
101
- trigger("global+" + keyFull);
102
- } else {
103
- trigger("global+" + keyFull);
104
- trigger(keyFull);
105
- }
78
+ triggerKeyDown(e, false);
79
+ });
80
+ }
81
+ let insideAnims = new Set<Watcher>();
82
+ let keyUpListener = new Set<() => void>();
83
+ export function triggerKeyDown(e: KeyboardEvent, forceAmbient = true) {
84
+ let isAmbientEvent = (
85
+ e.target === document.body
86
+ // Some elements are commonly selected, but shouldn't handling key events
87
+ || (e.target as HTMLElement).tagName === "BUTTON"
88
+ || (e.target as HTMLElement).tagName === "A"
89
+ || (e.target as HTMLElement).tagName === "INPUT" && (e.target as HTMLInputElement).type === "checkbox"
90
+ //|| (e.target as HTMLElement).tagName === "INPUT" && (e.target as HTMLInputElement).type === "number" && Number.isNaN(+e.key) && !e.key.includes("Arrow")
91
+ //|| ["INPUT", "TEXTAREA"].includes((e.target as HTMLElement).tagName)
92
+ ) || forceAmbient;
93
+ if (e.key === "Escape") {
94
+ (document.activeElement as any)?.blur();
95
+ }
96
+ let keyFull = e.code;
97
+ if (keyFull.startsWith("Key")) {
98
+ keyFull = keyFull.slice("Key".length);
99
+ }
100
+ if (e.metaKey) keyFull = "meta+" + keyFull;
101
+ if (e.altKey) keyFull = "alt+" + keyFull;
102
+ if (e.shiftKey) keyFull = "shift+" + keyFull;
103
+ if (e.ctrlKey || e.metaKey) keyFull = "ctrl+" + keyFull;
104
+ if (!isAmbientEvent) {
105
+ trigger("global+" + keyFull);
106
+ } else {
107
+ trigger("global+" + keyFull);
108
+ trigger(keyFull);
109
+ }
106
110
 
107
- function trigger(keyFull: string) {
108
- keyFull = keyFull.toLowerCase();
109
- let listeners = Array.from(registeredWatches).map(x => {
110
- let props = Querysub.localRead(() => ({ ...x.props }), { allowProxyResults: true });
111
- let match = props.hotkeys?.find(x => {
112
- let hotkey = x.toLowerCase();
113
- if (hotkey.endsWith("+toggle")) {
114
- hotkey = hotkey.slice(0, -"+toggle".length);
115
- }
116
- return hotkey === keyFull;
117
- }) || "";
118
- let hotkeyPriority = match && (
119
- props.hotkeyPriority
120
- ?? Querysub.localRead(() => qreact.getAncestorProps(ButtonHotkeyRegion, x)?.hotkeyPriority, { allowProxyResults: true })
121
- ?? Number.MIN_SAFE_INTEGER
122
- ) || Number.MIN_SAFE_INTEGER;
123
- return { match, listener: x, props, hotkeyPriority };
124
- }).filter(x => x.match);
125
- if (!listeners.length) return;
126
- console.log(`Triggering hotkey ${JSON.stringify(keyFull)} for ${listeners.length} listeners`);
127
- e.preventDefault();
128
- e.stopPropagation();
129
- let maxPriority = Array.from(listeners).reduce((a, b) => Math.max(a, b.hotkeyPriority), Number.MIN_SAFE_INTEGER);
130
- listeners = listeners.filter(x => x.hotkeyPriority === maxPriority);
131
- if (listeners.length === 0) return;
132
- listeners = listeners.filter((x, index) => x.props.callEvenIfRedundant || index === listeners.length - 1);
133
- for (let { match, listener, props } of listeners) {
134
- if (insideAnims.has(listener)) continue;
135
- listener.element?.click();
136
- if (!keyFull.includes("global+")) {
137
- listener.element?.focus();
111
+ function trigger(keyFull: string) {
112
+ keyFull = keyFull.toLowerCase();
113
+ let listeners = Array.from(registeredWatches).map(x => {
114
+ let props = Querysub.localRead(() => ({ ...x.props }), { allowProxyResults: true });
115
+ let match = props.hotkeys?.find(x => {
116
+ let hotkey = x.toLowerCase();
117
+ if (hotkey.endsWith("+toggle")) {
118
+ hotkey = hotkey.slice(0, -"+toggle".length);
138
119
  }
139
- if (match.includes("+toggle")) {
140
- function onKeyUp() {
141
- keyUpListener.delete(onKeyUp);
142
- listener.element?.click();
143
- }
144
- keyUpListener.add(onKeyUp);
120
+ return hotkey === keyFull;
121
+ }) || "";
122
+ let hotkeyPriority = match && (
123
+ props.hotkeyPriority
124
+ ?? Querysub.localRead(() => qreact.getAncestorProps(ButtonHotkeyRegion, x)?.hotkeyPriority, { allowProxyResults: true })
125
+ ?? Number.MIN_SAFE_INTEGER
126
+ ) || Number.MIN_SAFE_INTEGER;
127
+ return { match, listener: x, props, hotkeyPriority };
128
+ }).filter(x => x.match);
129
+ if (!listeners.length) return;
130
+ console.log(`Triggering hotkey ${JSON.stringify(keyFull)} for ${listeners.length} listeners`);
131
+ e.preventDefault();
132
+ e.stopPropagation();
133
+ let maxPriority = Array.from(listeners).reduce((a, b) => Math.max(a, b.hotkeyPriority), Number.MIN_SAFE_INTEGER);
134
+ listeners = listeners.filter(x => x.hotkeyPriority === maxPriority);
135
+ if (listeners.length === 0) return;
136
+ listeners = listeners.filter((x, index) => x.props.callEvenIfRedundant || index === listeners.length - 1);
137
+ for (let { match, listener, props } of listeners) {
138
+ if (insideAnims.has(listener)) continue;
139
+ listener.element?.click();
140
+ if (!keyFull.includes("global+")) {
141
+ listener.element?.focus();
142
+ }
143
+ if (match.includes("+toggle")) {
144
+ function onKeyUp() {
145
+ keyUpListener.delete(onKeyUp);
146
+ listener.element?.click();
145
147
  }
146
- if (Querysub.localRead(() => props.immediateRepeat)) {
147
- insideAnims.add(listener);
148
- let dead = false;
149
- function onKeyUp() {
150
- keyUpListener.delete(onKeyUp);
151
- insideAnims.delete(listener);
152
- dead = true;
153
- }
154
- keyUpListener.add(onKeyUp);
155
- let prevFrame = Date.now();
156
- function onFrame() {
157
- if (dead) {
158
- animationDuration = 0;
159
- return;
160
- }
161
- let now = Date.now();
162
- animationDuration = now - prevFrame;
163
- prevFrame = now;
164
- listener.element?.click();
165
- requestAnimationFrame(onFrame);
148
+ keyUpListener.add(onKeyUp);
149
+ }
150
+ if (Querysub.localRead(() => props.immediateRepeat)) {
151
+ insideAnims.add(listener);
152
+ let dead = false;
153
+ function onKeyUp() {
154
+ keyUpListener.delete(onKeyUp);
155
+ insideAnims.delete(listener);
156
+ dead = true;
157
+ }
158
+ keyUpListener.add(onKeyUp);
159
+ let prevFrame = Date.now();
160
+ function onFrame() {
161
+ if (dead) {
162
+ animationDuration = 0;
163
+ return;
166
164
  }
165
+ let now = Date.now();
166
+ animationDuration = now - prevFrame;
167
+ prevFrame = now;
168
+ listener.element?.click();
167
169
  requestAnimationFrame(onFrame);
168
170
  }
171
+ requestAnimationFrame(onFrame);
169
172
  }
170
173
  }
171
- });
174
+ }
172
175
  }
173
176
 
174
177
  let animationDuration = 0;
@@ -244,6 +244,9 @@ export function parseSearchString(search: string): { [key: string]: unknown } {
244
244
  let parts = search.split("&");
245
245
  let output: { [key: string]: string | undefined } = Object.create(null);
246
246
  for (let part of parts) {
247
+ if (part.endsWith("=")) {
248
+ part = part.slice(0, -1);
249
+ }
247
250
  let equalIndex = part.indexOf("=");
248
251
  if (equalIndex === -1) {
249
252
  output[decodeURIComponent(part)] = undefined;
@@ -0,0 +1,53 @@
1
+ import * as fs from "fs";
2
+ import { isNode } from "typesafecss";
3
+ import debugbreak from "debugbreak";
4
+ import { parseBraceGroupEndPos, parseParenGroupEndPos } from "./simpleParsing";
5
+ /** Extract the body of a root type from a module.
6
+ * Ex:
7
+ * interface TestType {
8
+ * x: number;
9
+ * }
10
+ * extractType(module, "TestType") === `{ x: number; }`
11
+ *
12
+ * IMPORTANT! You must also set: `module.sendFullSource = true`, serverside, statically on the module.
13
+ */
14
+
15
+ export function extractType(module: NodeJS.Module, typeName: string): string {
16
+ // TODO: Use moduleContents?
17
+ //assertDefined2(module.moduleContents, "module.moduleContents not initialized?");
18
+ let code = module.original.originalSource || fs.readFileSync(module.filename).toString().replaceAll("\r", "");
19
+ // NOTE: This is a hack, but look for the first instance of typeName, followed by a space. That is probably the type we want.
20
+ // (the space helps ensure it is the full symbol, and not just the text in a different symbol)
21
+ let typeStart = code.indexOf(typeName + "<");
22
+ if (typeStart < 0) {
23
+ typeStart = indexOfAsserted(code, typeName + " ");
24
+ }
25
+ typeStart = indexOfAsserted(code, "=", typeStart);
26
+ typeStart++;
27
+
28
+ function indexOfEnd(char: string) {
29
+ let index = code.indexOf(char, typeStart);
30
+ if (index < 0) return code.length;
31
+ return index;
32
+ }
33
+ let end = indexOfEnd(";");
34
+ // Skip any brace or paren groups
35
+ if (indexOfEnd("{") < end && indexOfEnd("{") < indexOfEnd("(")) {
36
+ // We have braces or parens
37
+ end = parseBraceGroupEndPos(code, typeStart)?.endPos || typeStart;
38
+ end = indexOfAsserted(code, ";", end);
39
+ } else if (indexOfEnd("(") < end) {
40
+ // We have braces or parens
41
+ end = parseParenGroupEndPos(code, typeStart)?.endPos || typeStart;
42
+ end = indexOfAsserted(code, ";", end);
43
+ }
44
+
45
+ console.log(code.slice(typeStart, end));
46
+
47
+ return code.slice(typeStart, end);
48
+ }
49
+ export function indexOfAsserted(text: string, search: string, startIndex?: number) {
50
+ let index = text.indexOf(search, startIndex);
51
+ if (index < 0) throw new Error(`Cannot find indexOf text "${search}" in "${text}", starting at index ${startIndex}`);
52
+ return index;
53
+ }
@@ -46,3 +46,27 @@ export function niceNumberStringify(valueIn: number) {
46
46
  }
47
47
  return value;
48
48
  }
49
+
50
+
51
+ export function ellipsize(text: string, maxLength: number) {
52
+ if (text.length <= maxLength) {
53
+ return text;
54
+ }
55
+ return text.slice(0, maxLength) + "...";
56
+ }
57
+
58
+ export function ellipsizeStart(text: string, maxLength: number) {
59
+ if (text.length <= maxLength) {
60
+ return text;
61
+ }
62
+ return "..." + text.slice(-maxLength);
63
+ }
64
+
65
+ export function ellipsizeMiddle(text: string, maxLength: number) {
66
+ if (text.length <= maxLength) {
67
+ return text;
68
+ }
69
+ let halfs = Math.floor((maxLength - 3) / 2);
70
+ if (halfs < 1) halfs = 1;
71
+ return text.slice(0, halfs) + "..." + text.slice(-halfs);
72
+ }
@@ -0,0 +1,310 @@
1
+ import { red } from "socket-function/src/formatting/logColors";
2
+ import fs from "fs";
3
+ import { ellipsizeStart } from "./format2";
4
+
5
+ export function parseParenGroup(text: string, startPos: number): string | undefined {
6
+ const result = parseParenGroupEndPos(text, startPos);
7
+ if (!result) {
8
+ return undefined;
9
+ }
10
+ return text.slice(result.startPos, result.endPos);
11
+ }
12
+ export function parseParenGroupEndPos(text: string, startPos: number): { startPos: number; endPos: number } | undefined {
13
+ startPos = text.indexOf("(", startPos);
14
+ if (startPos < 0) {
15
+ console.warn(red(`Could not find opening parenthesis for paren group starting at ${startPos}, text ${JSON.stringify(ellipsizeStart(text, 100))}`));
16
+ return undefined;
17
+ }
18
+ let parensOpen = 1;
19
+ let endPos = startPos + 1;
20
+ let inLineComment = false;
21
+ let inBlockComment = false;
22
+ while (endPos < text.length) {
23
+ let char = text[endPos++];
24
+
25
+ if (inBlockComment) {
26
+ if (char === "*" && text[endPos] === "/") {
27
+ inBlockComment = false;
28
+ endPos++;
29
+ }
30
+ continue;
31
+ }
32
+
33
+ if (inLineComment) {
34
+ if (char === "\n") {
35
+ inLineComment = false;
36
+ }
37
+ continue;
38
+ }
39
+
40
+ if (char === "/" && text[endPos] === "/") {
41
+ inLineComment = true;
42
+ endPos++;
43
+ continue;
44
+ }
45
+
46
+ if (char === "/" && text[endPos] === "*") {
47
+ inBlockComment = true;
48
+ endPos++;
49
+ continue;
50
+ }
51
+
52
+ if (char === "(") {
53
+ parensOpen++;
54
+ continue;
55
+ }
56
+
57
+ if (char === ")") {
58
+ parensOpen--;
59
+ if (parensOpen === 0) {
60
+ break;
61
+ }
62
+ continue;
63
+ }
64
+ }
65
+ if (endPos >= text.length) {
66
+ console.warn(red(`Could not find closing parenthesis for paren group starting at ${startPos}, text ${JSON.stringify(ellipsizeStart(text, 100))}`));
67
+ return undefined;
68
+ }
69
+ return { startPos, endPos };
70
+ }
71
+ export function parseBraceGroup(text: string, startPos: number): string | undefined {
72
+ const result = parseBraceGroupEndPos(text, startPos);
73
+ if (!result) {
74
+ return undefined;
75
+ }
76
+ return text.slice(result.startPos, result.endPos);
77
+ }
78
+
79
+ export function parseBraceGroupEndPos(text: string, startPos: number): { startPos: number; endPos: number } | undefined {
80
+ startPos = text.indexOf("{", startPos);
81
+ if (startPos < 0) {
82
+ console.warn(red(`Could not find opening brace for brace group starting at ${startPos}, text ${JSON.stringify(ellipsizeStart(text, 100))}`));
83
+ return undefined;
84
+ }
85
+ let bracesOpen = 1;
86
+ let endPos = startPos + 1;
87
+ let inLineComment = false;
88
+ let inBlockComment = false;
89
+ while (endPos < text.length) {
90
+ let char = text[endPos++];
91
+
92
+ if (inBlockComment) {
93
+ if (char === "*" && text[endPos] === "/") {
94
+ inBlockComment = false;
95
+ endPos++;
96
+ }
97
+ continue;
98
+ }
99
+
100
+ if (inLineComment) {
101
+ if (char === "\n") {
102
+ inLineComment = false;
103
+ }
104
+ continue;
105
+ }
106
+
107
+ if (char === "/" && text[endPos] === "/") {
108
+ inLineComment = true;
109
+ endPos++;
110
+ continue;
111
+ }
112
+
113
+ if (char === "/" && text[endPos] === "*") {
114
+ inBlockComment = true;
115
+ endPos++;
116
+ continue;
117
+ }
118
+
119
+ if (char === "{") {
120
+ bracesOpen++;
121
+ continue;
122
+ }
123
+
124
+ if (char === "}") {
125
+ bracesOpen--;
126
+ if (bracesOpen === 0) {
127
+ break;
128
+ }
129
+ continue;
130
+ }
131
+ }
132
+ if (endPos >= text.length) {
133
+ console.warn(red(`Could not find closing brace for brace group starting at ${startPos}, text ${JSON.stringify(ellipsizeStart(text, 100))}`));
134
+ return undefined;
135
+ }
136
+ return { startPos, endPos };
137
+ }
138
+
139
+ export type ArgumentRegion = {
140
+ type: "comment";
141
+ text: string;
142
+ } | {
143
+ type: "param";
144
+ name: string;
145
+ };
146
+
147
+ export type ParserContext = {
148
+ text: string;
149
+ pos: number;
150
+ };
151
+
152
+ export function parseArgumentRegions(text: string): ArgumentRegion[] {
153
+ const regions: ArgumentRegion[] = [];
154
+ const ctx: ParserContext = { text, pos: 0 };
155
+
156
+ while (ctx.pos < ctx.text.length) {
157
+ skipWhitespace(ctx);
158
+
159
+ if (ctx.pos >= ctx.text.length) {
160
+ break;
161
+ }
162
+
163
+ const blockComment = parseBlockComment(ctx);
164
+ if (blockComment) {
165
+ regions.push(blockComment);
166
+ continue;
167
+ }
168
+
169
+ const lineComments = parseLineComments(ctx);
170
+ if (lineComments) {
171
+ regions.push(lineComments);
172
+ continue;
173
+ }
174
+
175
+ const parameter = parseParameter(ctx);
176
+ if (parameter) {
177
+ regions.push(parameter);
178
+ continue;
179
+ }
180
+
181
+ // Skip unknown character
182
+ ctx.pos++;
183
+ }
184
+
185
+ return regions;
186
+ }
187
+
188
+ function skipWhitespace(ctx: ParserContext) {
189
+ while (ctx.pos < ctx.text.length && /\s/.test(ctx.text[ctx.pos])) {
190
+ ctx.pos++;
191
+ }
192
+ }
193
+
194
+ function parseBlockComment(ctx: ParserContext): ArgumentRegion | undefined {
195
+ if (ctx.text[ctx.pos] !== "/" || ctx.text[ctx.pos + 1] !== "*") {
196
+ return undefined;
197
+ }
198
+
199
+ const start = ctx.pos;
200
+ ctx.pos += 2;
201
+ while (ctx.pos < ctx.text.length && !(ctx.text[ctx.pos] === "*" && ctx.text[ctx.pos + 1] === "/")) {
202
+ ctx.pos++;
203
+ }
204
+
205
+ if (ctx.pos >= ctx.text.length) {
206
+ return undefined;
207
+ }
208
+
209
+ ctx.pos += 2;
210
+ return {
211
+ type: "comment",
212
+ text: ctx.text.slice(start + 2, ctx.pos - 2).trim()
213
+ };
214
+ }
215
+
216
+ function parseLineComments(ctx: ParserContext): ArgumentRegion | undefined {
217
+ if (ctx.text[ctx.pos] !== "/" || ctx.text[ctx.pos + 1] !== "/") {
218
+ return undefined;
219
+ }
220
+
221
+ let commentText = "";
222
+ while (ctx.pos < ctx.text.length) {
223
+ // Find start of comment
224
+ while (ctx.pos < ctx.text.length && ctx.text[ctx.pos] !== "/") {
225
+ ctx.pos++;
226
+ }
227
+ if (ctx.pos >= ctx.text.length || ctx.text[ctx.pos + 1] !== "/") {
228
+ break;
229
+ }
230
+
231
+ // Get comment content
232
+ ctx.pos += 2;
233
+ const lineStart = ctx.pos;
234
+ while (ctx.pos < ctx.text.length && ctx.text[ctx.pos] !== "\n") {
235
+ ctx.pos++;
236
+ }
237
+ commentText += (commentText ? "\n" : "") + ctx.text.slice(lineStart, ctx.pos).trim();
238
+
239
+ // Skip newline
240
+ if (ctx.pos < ctx.text.length) {
241
+ ctx.pos++;
242
+ }
243
+
244
+ // Check next line - if it has content before //, break
245
+ let nextLinePos = ctx.pos;
246
+ while (nextLinePos < ctx.text.length && /\s/.test(ctx.text[nextLinePos])) {
247
+ nextLinePos++;
248
+ }
249
+ if (nextLinePos >= ctx.text.length || ctx.text[nextLinePos] !== "/" || ctx.text[nextLinePos + 1] !== "/") {
250
+ break;
251
+ }
252
+ ctx.pos = nextLinePos;
253
+ }
254
+
255
+ if (!commentText) {
256
+ return undefined;
257
+ }
258
+
259
+ return {
260
+ type: "comment",
261
+ text: commentText
262
+ };
263
+ }
264
+
265
+ export function parseParameter(ctx: ParserContext): ArgumentRegion | undefined {
266
+ if (!/[a-zA-Z_]/.test(ctx.text[ctx.pos])) {
267
+ return undefined;
268
+ }
269
+
270
+ const nameStart = ctx.pos;
271
+ while (ctx.pos < ctx.text.length && /[a-zA-Z0-9_]/.test(ctx.text[ctx.pos])) {
272
+ ctx.pos++;
273
+ }
274
+ const name = ctx.text.slice(nameStart, ctx.pos);
275
+
276
+ // Skip whitespace and colon
277
+ skipWhitespace(ctx);
278
+ if (ctx.pos >= ctx.text.length || ctx.text[ctx.pos] !== ":") {
279
+ return undefined;
280
+ }
281
+
282
+ ctx.pos++;
283
+ skipWhitespace(ctx);
284
+
285
+ // Get description until semicolon, handling nested parens
286
+ const descStart = ctx.pos;
287
+ while (ctx.pos < ctx.text.length && ctx.text[ctx.pos] !== ";") {
288
+ if (ctx.text[ctx.pos] === "(") {
289
+ const parenGroup = parseParenGroup(ctx.text, ctx.pos);
290
+ if (!parenGroup) {
291
+ break;
292
+ }
293
+ ctx.pos += parenGroup.length;
294
+ continue;
295
+ }
296
+ ctx.pos++;
297
+ }
298
+
299
+ if (ctx.pos >= ctx.text.length) {
300
+ return undefined;
301
+ }
302
+
303
+ const result = {
304
+ type: "param" as const,
305
+ name,
306
+ description: ctx.text.slice(descStart, ctx.pos).trim()
307
+ };
308
+ ctx.pos++; // Skip semicolon
309
+ return result;
310
+ }