mrmd-js 2.0.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/README.md +842 -0
- package/dist/index.cjs +7613 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +7530 -0
- package/dist/index.js.map +1 -0
- package/dist/mrmd-js.iife.js +7618 -0
- package/dist/mrmd-js.iife.js.map +1 -0
- package/package.json +47 -0
- package/src/analysis/format.js +371 -0
- package/src/analysis/index.js +18 -0
- package/src/analysis/is-complete.js +394 -0
- package/src/constants.js +44 -0
- package/src/execute/css.js +205 -0
- package/src/execute/html.js +162 -0
- package/src/execute/index.js +41 -0
- package/src/execute/interface.js +144 -0
- package/src/execute/javascript.js +197 -0
- package/src/execute/registry.js +245 -0
- package/src/index.js +136 -0
- package/src/lsp/complete.js +353 -0
- package/src/lsp/format.js +310 -0
- package/src/lsp/hover.js +126 -0
- package/src/lsp/index.js +55 -0
- package/src/lsp/inspect.js +466 -0
- package/src/lsp/parse.js +455 -0
- package/src/lsp/variables.js +283 -0
- package/src/runtime.js +518 -0
- package/src/session/console-capture.js +181 -0
- package/src/session/context/iframe.js +407 -0
- package/src/session/context/index.js +12 -0
- package/src/session/context/interface.js +38 -0
- package/src/session/context/main.js +357 -0
- package/src/session/index.js +16 -0
- package/src/session/manager.js +327 -0
- package/src/session/session.js +678 -0
- package/src/transform/async.js +133 -0
- package/src/transform/extract.js +251 -0
- package/src/transform/index.js +10 -0
- package/src/transform/persistence.js +176 -0
- package/src/types/analysis.js +24 -0
- package/src/types/capabilities.js +44 -0
- package/src/types/completion.js +47 -0
- package/src/types/execution.js +62 -0
- package/src/types/index.js +16 -0
- package/src/types/inspection.js +39 -0
- package/src/types/session.js +32 -0
- package/src/types/streaming.js +74 -0
- package/src/types/variables.js +54 -0
- package/src/utils/ansi-renderer.js +301 -0
- package/src/utils/css-applicator.js +149 -0
- package/src/utils/html-renderer.js +355 -0
- package/src/utils/index.js +25 -0
package/src/lsp/parse.js
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Parsing Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for parsing JavaScript code to extract identifiers,
|
|
5
|
+
* determine completion context, and find symbol boundaries.
|
|
6
|
+
*
|
|
7
|
+
* @module lsp/parse
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} IdentifierInfo
|
|
12
|
+
* @property {string} name - The identifier name
|
|
13
|
+
* @property {string} full - Full path (e.g., "obj.prop" or "arr[0]")
|
|
14
|
+
* @property {number} start - Start position in code
|
|
15
|
+
* @property {number} end - End position in code
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {'member' | 'global' | 'bracket' | 'string' | 'comment' | 'none'} CompletionContextType
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} CompletionContext
|
|
24
|
+
* @property {CompletionContextType} type - Context type
|
|
25
|
+
* @property {string} prefix - What user has typed
|
|
26
|
+
* @property {string} [object] - Object path for member access
|
|
27
|
+
* @property {number} start - Start of completion region
|
|
28
|
+
* @property {number} end - End of completion region
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
// Characters that can be part of an identifier
|
|
32
|
+
const ID_START = /[$_a-zA-Z]/;
|
|
33
|
+
const ID_CONTINUE = /[$_a-zA-Z0-9]/;
|
|
34
|
+
|
|
35
|
+
// JavaScript keywords
|
|
36
|
+
const KEYWORDS = new Set([
|
|
37
|
+
'await', 'break', 'case', 'catch', 'class', 'const', 'continue',
|
|
38
|
+
'debugger', 'default', 'delete', 'do', 'else', 'enum', 'export',
|
|
39
|
+
'extends', 'false', 'finally', 'for', 'function', 'if', 'import',
|
|
40
|
+
'in', 'instanceof', 'let', 'new', 'null', 'return', 'static',
|
|
41
|
+
'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof',
|
|
42
|
+
'undefined', 'var', 'void', 'while', 'with', 'yield',
|
|
43
|
+
// Future reserved
|
|
44
|
+
'implements', 'interface', 'package', 'private', 'protected', 'public',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
// Common globals to suggest
|
|
48
|
+
const COMMON_GLOBALS = [
|
|
49
|
+
'Array', 'Boolean', 'Date', 'Error', 'Function', 'JSON', 'Map',
|
|
50
|
+
'Math', 'Number', 'Object', 'Promise', 'Proxy', 'Reflect', 'RegExp',
|
|
51
|
+
'Set', 'String', 'Symbol', 'WeakMap', 'WeakSet',
|
|
52
|
+
'console', 'fetch', 'setTimeout', 'setInterval', 'clearTimeout',
|
|
53
|
+
'clearInterval', 'parseInt', 'parseFloat', 'isNaN', 'isFinite',
|
|
54
|
+
'encodeURI', 'decodeURI', 'encodeURIComponent', 'decodeURIComponent',
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if a character is an identifier start
|
|
59
|
+
* @param {string} char
|
|
60
|
+
* @returns {boolean}
|
|
61
|
+
*/
|
|
62
|
+
export function isIdentifierStart(char) {
|
|
63
|
+
return ID_START.test(char);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if a character is an identifier continuation
|
|
68
|
+
* @param {string} char
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
export function isIdentifierPart(char) {
|
|
72
|
+
return ID_CONTINUE.test(char);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if a string is a JavaScript keyword
|
|
77
|
+
* @param {string} str
|
|
78
|
+
* @returns {boolean}
|
|
79
|
+
*/
|
|
80
|
+
export function isKeyword(str) {
|
|
81
|
+
return KEYWORDS.has(str);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get all JavaScript keywords
|
|
86
|
+
* @returns {string[]}
|
|
87
|
+
*/
|
|
88
|
+
export function getKeywords() {
|
|
89
|
+
return Array.from(KEYWORDS);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get common global names
|
|
94
|
+
* @returns {string[]}
|
|
95
|
+
*/
|
|
96
|
+
export function getCommonGlobals() {
|
|
97
|
+
return COMMON_GLOBALS;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Find the identifier at a given position in code
|
|
102
|
+
* @param {string} code
|
|
103
|
+
* @param {number} cursor - Cursor position (0-indexed)
|
|
104
|
+
* @returns {IdentifierInfo | null}
|
|
105
|
+
*/
|
|
106
|
+
export function parseIdentifierAtPosition(code, cursor) {
|
|
107
|
+
if (!code || cursor < 0 || cursor > code.length) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Find the start of the identifier chain (handles obj.prop.sub)
|
|
112
|
+
let start = cursor;
|
|
113
|
+
let parenDepth = 0;
|
|
114
|
+
let bracketDepth = 0;
|
|
115
|
+
|
|
116
|
+
// Walk backwards to find the start
|
|
117
|
+
while (start > 0) {
|
|
118
|
+
const char = code[start - 1];
|
|
119
|
+
|
|
120
|
+
// Handle brackets for array access
|
|
121
|
+
if (char === ']') {
|
|
122
|
+
bracketDepth++;
|
|
123
|
+
start--;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (char === '[') {
|
|
127
|
+
if (bracketDepth > 0) {
|
|
128
|
+
bracketDepth--;
|
|
129
|
+
start--;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Skip over bracket contents
|
|
136
|
+
if (bracketDepth > 0) {
|
|
137
|
+
start--;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Handle dots for member access
|
|
142
|
+
if (char === '.') {
|
|
143
|
+
start--;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Handle identifier characters
|
|
148
|
+
if (isIdentifierPart(char)) {
|
|
149
|
+
start--;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Handle closing paren (for function calls like foo().bar)
|
|
154
|
+
if (char === ')') {
|
|
155
|
+
parenDepth++;
|
|
156
|
+
start--;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (char === '(') {
|
|
160
|
+
if (parenDepth > 0) {
|
|
161
|
+
parenDepth--;
|
|
162
|
+
start--;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Skip over paren contents
|
|
169
|
+
if (parenDepth > 0) {
|
|
170
|
+
start--;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Stop at any other character
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Find the end of the identifier
|
|
179
|
+
let end = cursor;
|
|
180
|
+
while (end < code.length && isIdentifierPart(code[end])) {
|
|
181
|
+
end++;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (start === end) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const full = code.slice(start, end);
|
|
189
|
+
|
|
190
|
+
// Extract just the last identifier name
|
|
191
|
+
const lastDot = full.lastIndexOf('.');
|
|
192
|
+
const name = lastDot >= 0 ? full.slice(lastDot + 1) : full;
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
name,
|
|
196
|
+
full,
|
|
197
|
+
start,
|
|
198
|
+
end,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Determine the completion context at cursor position
|
|
204
|
+
* @param {string} code
|
|
205
|
+
* @param {number} cursor
|
|
206
|
+
* @returns {CompletionContext}
|
|
207
|
+
*/
|
|
208
|
+
export function parseCompletionContext(code, cursor) {
|
|
209
|
+
// Default result
|
|
210
|
+
const defaultResult = {
|
|
211
|
+
type: /** @type {CompletionContextType} */ ('none'),
|
|
212
|
+
prefix: '',
|
|
213
|
+
start: cursor,
|
|
214
|
+
end: cursor,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
if (!code || cursor < 0 || cursor > code.length) {
|
|
218
|
+
return defaultResult;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Check if we're in a string or comment
|
|
222
|
+
const contextType = getStringOrCommentContext(code, cursor);
|
|
223
|
+
if (contextType === 'string' || contextType === 'comment') {
|
|
224
|
+
return { type: contextType, prefix: '', start: cursor, end: cursor };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Find what's immediately before the cursor
|
|
228
|
+
let pos = cursor - 1;
|
|
229
|
+
|
|
230
|
+
// Skip whitespace
|
|
231
|
+
while (pos >= 0 && /\s/.test(code[pos])) {
|
|
232
|
+
pos--;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (pos < 0) {
|
|
236
|
+
return { type: 'global', prefix: '', start: cursor, end: cursor };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Check for member access (dot notation)
|
|
240
|
+
if (code[pos] === '.') {
|
|
241
|
+
// Find the object before the dot
|
|
242
|
+
const objectEnd = pos;
|
|
243
|
+
const objectInfo = parseIdentifierAtPosition(code, objectEnd);
|
|
244
|
+
|
|
245
|
+
if (objectInfo) {
|
|
246
|
+
return {
|
|
247
|
+
type: 'member',
|
|
248
|
+
prefix: '',
|
|
249
|
+
object: objectInfo.full,
|
|
250
|
+
start: cursor,
|
|
251
|
+
end: cursor,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { type: 'member', prefix: '', object: '', start: cursor, end: cursor };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check if we're typing an identifier
|
|
259
|
+
if (isIdentifierPart(code[pos])) {
|
|
260
|
+
// Walk back to find the start
|
|
261
|
+
let start = pos;
|
|
262
|
+
while (start > 0 && isIdentifierPart(code[start - 1])) {
|
|
263
|
+
start--;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const prefix = code.slice(start, cursor);
|
|
267
|
+
|
|
268
|
+
// Check what's before this identifier
|
|
269
|
+
let beforeStart = start - 1;
|
|
270
|
+
while (beforeStart >= 0 && /\s/.test(code[beforeStart])) {
|
|
271
|
+
beforeStart--;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (beforeStart >= 0 && code[beforeStart] === '.') {
|
|
275
|
+
// Member access with partial identifier
|
|
276
|
+
const objectEnd = beforeStart;
|
|
277
|
+
const objectInfo = parseIdentifierAtPosition(code, objectEnd);
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
type: 'member',
|
|
281
|
+
prefix,
|
|
282
|
+
object: objectInfo?.full ?? '',
|
|
283
|
+
start,
|
|
284
|
+
end: cursor,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Global identifier
|
|
289
|
+
return {
|
|
290
|
+
type: 'global',
|
|
291
|
+
prefix,
|
|
292
|
+
start,
|
|
293
|
+
end: cursor,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Check for bracket access
|
|
298
|
+
if (code[pos] === '[') {
|
|
299
|
+
// Find the object before the bracket
|
|
300
|
+
const objectEnd = pos;
|
|
301
|
+
const objectInfo = parseIdentifierAtPosition(code, objectEnd);
|
|
302
|
+
|
|
303
|
+
if (objectInfo) {
|
|
304
|
+
return {
|
|
305
|
+
type: 'bracket',
|
|
306
|
+
prefix: '',
|
|
307
|
+
object: objectInfo.full,
|
|
308
|
+
start: cursor,
|
|
309
|
+
end: cursor,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return { type: 'global', prefix: '', start: cursor, end: cursor };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Determine if cursor is inside a string or comment
|
|
319
|
+
* @param {string} code
|
|
320
|
+
* @param {number} cursor
|
|
321
|
+
* @returns {'string' | 'comment' | null}
|
|
322
|
+
*/
|
|
323
|
+
export function getStringOrCommentContext(code, cursor) {
|
|
324
|
+
let inSingleQuote = false;
|
|
325
|
+
let inDoubleQuote = false;
|
|
326
|
+
let inTemplate = false;
|
|
327
|
+
let inLineComment = false;
|
|
328
|
+
let inBlockComment = false;
|
|
329
|
+
|
|
330
|
+
for (let i = 0; i < cursor && i < code.length; i++) {
|
|
331
|
+
const char = code[i];
|
|
332
|
+
const next = code[i + 1];
|
|
333
|
+
const prev = code[i - 1];
|
|
334
|
+
|
|
335
|
+
// Skip escaped characters in strings
|
|
336
|
+
if ((inSingleQuote || inDoubleQuote || inTemplate) && prev === '\\') {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Line comment
|
|
341
|
+
if (!inSingleQuote && !inDoubleQuote && !inTemplate && !inBlockComment) {
|
|
342
|
+
if (char === '/' && next === '/') {
|
|
343
|
+
inLineComment = true;
|
|
344
|
+
i++; // Skip next char
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Block comment
|
|
350
|
+
if (!inSingleQuote && !inDoubleQuote && !inTemplate && !inLineComment) {
|
|
351
|
+
if (char === '/' && next === '*') {
|
|
352
|
+
inBlockComment = true;
|
|
353
|
+
i++;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (inBlockComment && char === '*' && next === '/') {
|
|
357
|
+
inBlockComment = false;
|
|
358
|
+
i++;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// End line comment at newline
|
|
364
|
+
if (inLineComment && char === '\n') {
|
|
365
|
+
inLineComment = false;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Strings
|
|
370
|
+
if (!inLineComment && !inBlockComment) {
|
|
371
|
+
if (char === "'" && !inDoubleQuote && !inTemplate) {
|
|
372
|
+
inSingleQuote = !inSingleQuote;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (char === '"' && !inSingleQuote && !inTemplate) {
|
|
376
|
+
inDoubleQuote = !inDoubleQuote;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (char === '`' && !inSingleQuote && !inDoubleQuote) {
|
|
380
|
+
inTemplate = !inTemplate;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (inSingleQuote || inDoubleQuote || inTemplate) {
|
|
387
|
+
return 'string';
|
|
388
|
+
}
|
|
389
|
+
if (inLineComment || inBlockComment) {
|
|
390
|
+
return 'comment';
|
|
391
|
+
}
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Extract the word at cursor position (simpler than full identifier)
|
|
397
|
+
* @param {string} code
|
|
398
|
+
* @param {number} cursor
|
|
399
|
+
* @returns {{word: string, start: number, end: number}}
|
|
400
|
+
*/
|
|
401
|
+
export function getWordAtCursor(code, cursor) {
|
|
402
|
+
let start = cursor;
|
|
403
|
+
let end = cursor;
|
|
404
|
+
|
|
405
|
+
// Walk backwards
|
|
406
|
+
while (start > 0 && isIdentifierPart(code[start - 1])) {
|
|
407
|
+
start--;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Walk forwards
|
|
411
|
+
while (end < code.length && isIdentifierPart(code[end])) {
|
|
412
|
+
end++;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
word: code.slice(start, end),
|
|
417
|
+
start,
|
|
418
|
+
end,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Split an object path into parts
|
|
424
|
+
* e.g., "obj.prop[0].name" → ["obj", "prop", "0", "name"]
|
|
425
|
+
* @param {string} path
|
|
426
|
+
* @returns {string[]}
|
|
427
|
+
*/
|
|
428
|
+
export function splitObjectPath(path) {
|
|
429
|
+
const parts = [];
|
|
430
|
+
let current = '';
|
|
431
|
+
let inBracket = false;
|
|
432
|
+
|
|
433
|
+
for (const char of path) {
|
|
434
|
+
if (char === '.' && !inBracket) {
|
|
435
|
+
if (current) parts.push(current);
|
|
436
|
+
current = '';
|
|
437
|
+
} else if (char === '[') {
|
|
438
|
+
if (current) parts.push(current);
|
|
439
|
+
current = '';
|
|
440
|
+
inBracket = true;
|
|
441
|
+
} else if (char === ']') {
|
|
442
|
+
if (current) parts.push(current);
|
|
443
|
+
current = '';
|
|
444
|
+
inBracket = false;
|
|
445
|
+
} else if (char === '"' || char === "'") {
|
|
446
|
+
// Skip quotes in bracket notation
|
|
447
|
+
continue;
|
|
448
|
+
} else {
|
|
449
|
+
current += char;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (current) parts.push(current);
|
|
454
|
+
return parts;
|
|
455
|
+
}
|