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 +15 -0
- package/package.json +2 -2
- package/src/4-dom/qreact.tsx +11 -6
- package/src/4-querysub/Querysub.ts +18 -3
- package/src/functional/UndoWatch.tsx +9 -0
- package/src/library-components/Button.tsx +89 -86
- package/src/library-components/URLParam.ts +3 -0
- package/src/misc/extractType.ts +53 -0
- package/src/misc/format2.ts +24 -0
- package/src/misc/simpleParsing.ts +310 -0
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.
|
|
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.
|
|
27
|
+
"socket-function": "^0.124.0",
|
|
28
28
|
"terser": "^5.31.0",
|
|
29
29
|
"typesafecss": "^0.15.0",
|
|
30
30
|
"yaml": "^2.5.0",
|
package/src/4-dom/qreact.tsx
CHANGED
|
@@ -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
|
|
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 (
|
|
962
|
-
|
|
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:
|
|
1863
|
-
//
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
+
}
|
package/src/misc/format2.ts
CHANGED
|
@@ -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
|
+
}
|