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.
Files changed (52) hide show
  1. package/README.md +842 -0
  2. package/dist/index.cjs +7613 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.js +7530 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/mrmd-js.iife.js +7618 -0
  7. package/dist/mrmd-js.iife.js.map +1 -0
  8. package/package.json +47 -0
  9. package/src/analysis/format.js +371 -0
  10. package/src/analysis/index.js +18 -0
  11. package/src/analysis/is-complete.js +394 -0
  12. package/src/constants.js +44 -0
  13. package/src/execute/css.js +205 -0
  14. package/src/execute/html.js +162 -0
  15. package/src/execute/index.js +41 -0
  16. package/src/execute/interface.js +144 -0
  17. package/src/execute/javascript.js +197 -0
  18. package/src/execute/registry.js +245 -0
  19. package/src/index.js +136 -0
  20. package/src/lsp/complete.js +353 -0
  21. package/src/lsp/format.js +310 -0
  22. package/src/lsp/hover.js +126 -0
  23. package/src/lsp/index.js +55 -0
  24. package/src/lsp/inspect.js +466 -0
  25. package/src/lsp/parse.js +455 -0
  26. package/src/lsp/variables.js +283 -0
  27. package/src/runtime.js +518 -0
  28. package/src/session/console-capture.js +181 -0
  29. package/src/session/context/iframe.js +407 -0
  30. package/src/session/context/index.js +12 -0
  31. package/src/session/context/interface.js +38 -0
  32. package/src/session/context/main.js +357 -0
  33. package/src/session/index.js +16 -0
  34. package/src/session/manager.js +327 -0
  35. package/src/session/session.js +678 -0
  36. package/src/transform/async.js +133 -0
  37. package/src/transform/extract.js +251 -0
  38. package/src/transform/index.js +10 -0
  39. package/src/transform/persistence.js +176 -0
  40. package/src/types/analysis.js +24 -0
  41. package/src/types/capabilities.js +44 -0
  42. package/src/types/completion.js +47 -0
  43. package/src/types/execution.js +62 -0
  44. package/src/types/index.js +16 -0
  45. package/src/types/inspection.js +39 -0
  46. package/src/types/session.js +32 -0
  47. package/src/types/streaming.js +74 -0
  48. package/src/types/variables.js +54 -0
  49. package/src/utils/ansi-renderer.js +301 -0
  50. package/src/utils/css-applicator.js +149 -0
  51. package/src/utils/html-renderer.js +355 -0
  52. package/src/utils/index.js +25 -0
@@ -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
+ }