mrmd-js 2.0.0 → 2.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Maxime Rivest
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.cjs CHANGED
@@ -56,6 +56,15 @@ const DEFAULT_FEATURES = {
56
56
  * @typedef {import('./context/interface.js').LogEntry} LogEntry
57
57
  */
58
58
 
59
+ /**
60
+ * Check if a value is a Promise
61
+ * @param {*} value
62
+ * @returns {boolean}
63
+ */
64
+ function isPromise(value) {
65
+ return value && typeof value === 'object' && typeof value.then === 'function';
66
+ }
67
+
59
68
  /**
60
69
  * Format arguments for logging
61
70
  * @param {Array<*>} args
@@ -78,6 +87,21 @@ function formatArgs(args) {
78
87
  .join(' ');
79
88
  }
80
89
 
90
+ /**
91
+ * Check args for Promises and return warning message if found
92
+ * @param {Array<*>} args
93
+ * @returns {string | null}
94
+ */
95
+ function checkForPromises(args) {
96
+ for (const arg of args) {
97
+ if (isPromise(arg)) {
98
+ return '⚠️ Promise detected - this value needs to be awaited. ' +
99
+ 'Add "await" before the async call, or assign it to get the resolved value.';
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+
81
105
  /**
82
106
  * Create a console capture for a window context
83
107
  */
@@ -120,11 +144,23 @@ class ConsoleCapture {
120
144
  // Intercept methods
121
145
  console.log = (...args) => {
122
146
  this.#queue.push({ type: 'log', args, timestamp: Date.now() });
147
+ // Check for unresolved Promises and warn
148
+ const promiseWarning = checkForPromises(args);
149
+ if (promiseWarning) {
150
+ this.#queue.push({ type: 'warn', args: [promiseWarning], timestamp: Date.now() });
151
+ this.#originalConsole?.warn?.(promiseWarning);
152
+ }
123
153
  this.#originalConsole?.log?.(...args);
124
154
  };
125
155
 
126
156
  console.info = (...args) => {
127
157
  this.#queue.push({ type: 'info', args, timestamp: Date.now() });
158
+ // Check for unresolved Promises and warn
159
+ const promiseWarning = checkForPromises(args);
160
+ if (promiseWarning) {
161
+ this.#queue.push({ type: 'warn', args: [promiseWarning], timestamp: Date.now() });
162
+ this.#originalConsole?.warn?.(promiseWarning);
163
+ }
128
164
  this.#originalConsole?.info?.(...args);
129
165
  };
130
166
 
@@ -1510,12 +1546,12 @@ function transformForPersistence(code) {
1510
1546
 
1511
1547
  // Check for const/let keywords
1512
1548
  if (isWordBoundary(code, i)) {
1513
- if (code.slice(i, i + 5) === 'const' && isWordBoundary(code, i + 5)) {
1549
+ if (code.slice(i, i + 5) === 'const' && isWordBoundaryAfter(code, i + 5)) {
1514
1550
  result += 'var';
1515
1551
  i += 5;
1516
1552
  continue;
1517
1553
  }
1518
- if (code.slice(i, i + 3) === 'let' && isWordBoundary(code, i + 3)) {
1554
+ if (code.slice(i, i + 3) === 'let' && isWordBoundaryAfter(code, i + 3)) {
1519
1555
  result += 'var';
1520
1556
  i += 3;
1521
1557
  continue;
@@ -1552,15 +1588,124 @@ function isWordBoundary(code, pos) {
1552
1588
  return true;
1553
1589
  }
1554
1590
 
1591
+ /**
1592
+ * Check if position after keyword is a word boundary
1593
+ * @param {string} code
1594
+ * @param {number} pos - Position after the keyword
1595
+ * @returns {boolean}
1596
+ */
1597
+ function isWordBoundaryAfter(code, pos) {
1598
+ if (pos >= code.length) return true;
1599
+ return !/[a-zA-Z0-9_$]/.test(code[pos]);
1600
+ }
1601
+
1555
1602
  /**
1556
1603
  * Async Transform
1557
1604
  *
1558
- * Wraps code to support top-level await.
1605
+ * Wraps code to support top-level await and auto-awaits common async patterns.
1606
+ * This makes JavaScript feel more linear like Python/R/Julia.
1559
1607
  * @module transform/async
1560
1608
  */
1561
1609
 
1610
+
1562
1611
  /**
1563
- * Check if code contains top-level await
1612
+ * Auto-insert await before common async function calls
1613
+ * This makes JavaScript feel more linear like Python/R
1614
+ *
1615
+ * @param {string} code - Source code
1616
+ * @returns {string} Code with auto-awaits inserted
1617
+ */
1618
+ function autoInsertAwaits(code) {
1619
+ // Don't process if code already uses await extensively
1620
+ // (user knows what they're doing)
1621
+ const awaitCount = (code.match(/\bawait\b/g) || []).length;
1622
+ const lines = code.split('\n').length;
1623
+ if (awaitCount > lines / 2) {
1624
+ return code;
1625
+ }
1626
+
1627
+ let result = code;
1628
+
1629
+ // Track positions to avoid double-processing
1630
+ // We need to be careful not to add await before already-awaited expressions
1631
+
1632
+ // First, temporarily replace existing awaits to protect them
1633
+ const awaitPlaceholders = [];
1634
+ result = result.replace(/\bawait\s+/g, (match) => {
1635
+ const placeholder = `__AWAIT_PLACEHOLDER_${awaitPlaceholders.length}__`;
1636
+ awaitPlaceholders.push(match);
1637
+ return placeholder;
1638
+ });
1639
+
1640
+ // Also protect strings, comments, and template literals
1641
+ const protectedStrings = [];
1642
+
1643
+ // Protect comments FIRST (before strings) to handle apostrophes in comments like "doesn't"
1644
+ result = result.replace(/\/\/[^\n]*/g, (match) => {
1645
+ const placeholder = `__PROTECTED_${protectedStrings.length}__`;
1646
+ protectedStrings.push(match);
1647
+ return placeholder;
1648
+ });
1649
+ result = result.replace(/\/\*[\s\S]*?\*\//g, (match) => {
1650
+ const placeholder = `__PROTECTED_${protectedStrings.length}__`;
1651
+ protectedStrings.push(match);
1652
+ return placeholder;
1653
+ });
1654
+
1655
+ // Protect template literals
1656
+ result = result.replace(/`(?:[^`\\]|\\.)*`/g, (match) => {
1657
+ const placeholder = `__PROTECTED_${protectedStrings.length}__`;
1658
+ protectedStrings.push(match);
1659
+ return placeholder;
1660
+ });
1661
+
1662
+ // Protect strings (after comments, so apostrophes in comments don't interfere)
1663
+ result = result.replace(/"(?:[^"\\]|\\.)*"/g, (match) => {
1664
+ const placeholder = `__PROTECTED_${protectedStrings.length}__`;
1665
+ protectedStrings.push(match);
1666
+ return placeholder;
1667
+ });
1668
+ result = result.replace(/'(?:[^'\\]|\\.)*'/g, (match) => {
1669
+ const placeholder = `__PROTECTED_${protectedStrings.length}__`;
1670
+ protectedStrings.push(match);
1671
+ return placeholder;
1672
+ });
1673
+
1674
+ // Now auto-insert awaits for common patterns
1675
+ // fetch(...) -> await fetch(...)
1676
+ result = result.replace(/\bfetch\s*\(/g, 'await fetch(');
1677
+
1678
+ // import(...) -> await import(...)
1679
+ // But not "import x from" statements
1680
+ result = result.replace(/(?<![.\w])import\s*\(/g, 'await import(');
1681
+
1682
+ // .json() .text() .blob() etc on response objects
1683
+ // These methods also return Promises, so we need to await both:
1684
+ // response.json() -> await (await response).json()
1685
+ result = result.replace(/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\.\s*json\s*\(\s*\)/g, 'await (await $1).json()');
1686
+ result = result.replace(/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\.\s*text\s*\(\s*\)/g, 'await (await $1).text()');
1687
+ result = result.replace(/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\.\s*blob\s*\(\s*\)/g, 'await (await $1).blob()');
1688
+ result = result.replace(/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\.\s*arrayBuffer\s*\(\s*\)/g, 'await (await $1).arrayBuffer()');
1689
+ result = result.replace(/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\.\s*formData\s*\(\s*\)/g, 'await (await $1).formData()');
1690
+
1691
+ // Restore protected content
1692
+ for (let i = protectedStrings.length - 1; i >= 0; i--) {
1693
+ result = result.replace(`__PROTECTED_${i}__`, protectedStrings[i]);
1694
+ }
1695
+
1696
+ // Restore existing awaits
1697
+ for (let i = awaitPlaceholders.length - 1; i >= 0; i--) {
1698
+ result = result.replace(`__AWAIT_PLACEHOLDER_${i}__`, awaitPlaceholders[i]);
1699
+ }
1700
+
1701
+ // Clean up any double awaits we might have introduced
1702
+ result = result.replace(/\bawait\s+await\b/g, 'await');
1703
+
1704
+ return result;
1705
+ }
1706
+
1707
+ /**
1708
+ * Check if code contains top-level await (or will after auto-insertion)
1564
1709
  * @param {string} code
1565
1710
  * @returns {boolean}
1566
1711
  */
@@ -1569,16 +1714,17 @@ function hasTopLevelAwait(code) {
1569
1714
  // This is a heuristic; a proper check would need AST parsing
1570
1715
 
1571
1716
  // Remove strings, comments, and regex to avoid false positives
1717
+ // IMPORTANT: Remove comments FIRST to handle apostrophes in comments like "doesn't"
1572
1718
  const cleaned = code
1573
- // Remove template literals (simple version)
1574
- .replace(/`[^`]*`/g, '')
1575
- // Remove strings
1576
- .replace(/"(?:[^"\\]|\\.)*"/g, '')
1577
- .replace(/'(?:[^'\\]|\\.)*'/g, '')
1578
- // Remove single-line comments
1719
+ // Remove single-line comments FIRST (before strings)
1579
1720
  .replace(/\/\/[^\n]*/g, '')
1580
1721
  // Remove multi-line comments
1581
- .replace(/\/\*[\s\S]*?\*\//g, '');
1722
+ .replace(/\/\*[\s\S]*?\*\//g, '')
1723
+ // Remove template literals
1724
+ .replace(/`[^`]*`/g, '')
1725
+ // Remove strings (after comments, so apostrophes in comments don't interfere)
1726
+ .replace(/"(?:[^"\\]|\\.)*"/g, '')
1727
+ .replace(/'(?:[^'\\]|\\.)*'/g, '');
1582
1728
  let i = 0;
1583
1729
 
1584
1730
  while (i < cleaned.length) {
@@ -1650,19 +1796,50 @@ ${code}
1650
1796
  * @returns {string} Wrapped code that returns last expression
1651
1797
  */
1652
1798
  function wrapWithLastExpression(code) {
1653
- const needsAsync = hasTopLevelAwait(code);
1799
+ // Auto-insert awaits for common async patterns (fetch, import, .json(), etc.)
1800
+ // This makes JavaScript feel more linear like Python/R/Julia
1801
+ const autoAwaitedCode = autoInsertAwaits(code);
1654
1802
 
1655
- // Find the last expression and make it a return value
1656
- // This is tricky without AST - we use eval trick instead
1803
+ // Check if code needs async (either explicit await or auto-inserted)
1804
+ const needsAsync = hasTopLevelAwait(autoAwaitedCode);
1805
+
1806
+ if (needsAsync) {
1807
+ // For code with await, wrap in async IIFE
1808
+ // This allows await to work at the "top level" of the user's code
1809
+ const asyncWrappedCode = `(async () => {\n${autoAwaitedCode}\n})()`;
1810
+
1811
+ // Now wrap to capture the result
1812
+ // Use indirect eval (0, eval)() to run in global scope so var declarations persist
1813
+ const wrapped = `
1814
+ ;(async function() {
1815
+ let __result__;
1816
+ try {
1817
+ __result__ = await (0, eval)(${JSON.stringify(asyncWrappedCode)});
1818
+ } catch (e) {
1819
+ if (e instanceof SyntaxError) {
1820
+ await (0, eval)(${JSON.stringify(asyncWrappedCode)});
1821
+ __result__ = undefined;
1822
+ } else {
1823
+ throw e;
1824
+ }
1825
+ }
1826
+ return __result__;
1827
+ })()`;
1828
+ return wrapped.trim();
1829
+ }
1830
+
1831
+ // No async needed - use simpler synchronous wrapper
1832
+ // Use indirect eval (0, eval)() to run in global scope so var declarations persist
1833
+ // Note: Still use autoAwaitedCode in case auto-awaits were added but hasTopLevelAwait missed them
1657
1834
  const wrapped = `
1658
- ;(${needsAsync ? 'async ' : ''}function() {
1835
+ ;(function() {
1659
1836
  let __result__;
1660
1837
  try {
1661
- __result__ = eval(${JSON.stringify(code)});
1838
+ __result__ = (0, eval)(${JSON.stringify(autoAwaitedCode)});
1662
1839
  } catch (e) {
1663
1840
  if (e instanceof SyntaxError) {
1664
1841
  // Code might be statements, not expression
1665
- eval(${JSON.stringify(code)});
1842
+ (0, eval)(${JSON.stringify(autoAwaitedCode)});
1666
1843
  __result__ = undefined;
1667
1844
  } else {
1668
1845
  throw e;
@@ -1760,7 +1937,19 @@ class JavaScriptExecutor extends BaseExecutor {
1760
1937
 
1761
1938
  try {
1762
1939
  // Execute in context (pass execId for input() support)
1763
- const rawResult = await context.execute(wrapped, { execId: options.execId });
1940
+ let rawResult = await context.execute(wrapped, { execId: options.execId });
1941
+
1942
+ // Auto-await if result is a Promise (catch cases not handled by auto-await transform)
1943
+ if (rawResult.result && typeof rawResult.result === 'object' && typeof rawResult.result.then === 'function') {
1944
+ try {
1945
+ rawResult.result = await rawResult.result;
1946
+ } catch (promiseError) {
1947
+ // Promise rejected - treat as error
1948
+ rawResult.error = promiseError instanceof Error ? promiseError : new Error(String(promiseError));
1949
+ rawResult.result = undefined;
1950
+ }
1951
+ }
1952
+
1764
1953
  const duration = performance.now() - startTime;
1765
1954
 
1766
1955
  // Format result
@@ -6198,6 +6387,143 @@ function createCssExecutor() {
6198
6387
  return new CssExecutor();
6199
6388
  }
6200
6389
 
6390
+ /**
6391
+ * Mermaid Executor
6392
+ *
6393
+ * Executes Mermaid diagram cells by rendering them to SVG.
6394
+ * Loads mermaid from CDN on first use and returns HTML displayData.
6395
+ *
6396
+ * @module execute/mermaid
6397
+ */
6398
+
6399
+
6400
+ /**
6401
+ * @typedef {import('../session/context/interface.js').ExecutionContext} ExecutionContext
6402
+ * @typedef {import('../types/execution.js').ExecuteOptions} ExecuteOptions
6403
+ * @typedef {import('../types/execution.js').ExecutionResult} ExecutionResult
6404
+ * @typedef {import('../types/execution.js').DisplayData} DisplayData
6405
+ */
6406
+
6407
+ /** CDN URL for mermaid */
6408
+ const MERMAID_CDN = 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js';
6409
+
6410
+ /** Counter for unique diagram IDs */
6411
+ let diagramCounter = 0;
6412
+
6413
+ /**
6414
+ * Load mermaid from CDN if not already loaded
6415
+ * @returns {Promise<void>}
6416
+ */
6417
+ async function ensureMermaidLoaded() {
6418
+ // Check if already loaded
6419
+ if (typeof window !== 'undefined' && window.mermaid) {
6420
+ return;
6421
+ }
6422
+
6423
+ // Load from CDN
6424
+ return new Promise((resolve, reject) => {
6425
+ const script = document.createElement('script');
6426
+ script.src = MERMAID_CDN;
6427
+ script.onload = () => {
6428
+ // Initialize mermaid with safe defaults
6429
+ window.mermaid.initialize({
6430
+ startOnLoad: false,
6431
+ theme: 'default',
6432
+ securityLevel: 'loose', // Allow clicks/links in diagrams
6433
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
6434
+ });
6435
+ resolve();
6436
+ };
6437
+ script.onerror = () => reject(new Error('Failed to load mermaid from CDN'));
6438
+ document.head.appendChild(script);
6439
+ });
6440
+ }
6441
+
6442
+ /**
6443
+ * Mermaid executor - renders Mermaid diagrams to SVG
6444
+ */
6445
+ class MermaidExecutor extends BaseExecutor {
6446
+ /** @type {readonly string[]} */
6447
+ languages = ['mermaid'];
6448
+
6449
+ /**
6450
+ * Execute Mermaid diagram cell
6451
+ * @param {string} code - Mermaid diagram definition
6452
+ * @param {ExecutionContext} context - Execution context
6453
+ * @param {ExecuteOptions} [options] - Execution options
6454
+ * @returns {Promise<ExecutionResult>}
6455
+ */
6456
+ async execute(code, context, options = {}) {
6457
+ const startTime = performance.now();
6458
+
6459
+ try {
6460
+ // Ensure mermaid is loaded
6461
+ await ensureMermaidLoaded();
6462
+
6463
+ // Generate unique ID for this diagram
6464
+ const diagramId = `mermaid-diagram-${++diagramCounter}`;
6465
+
6466
+ // Render the diagram
6467
+ const { svg } = await window.mermaid.render(diagramId, code.trim());
6468
+
6469
+ const duration = performance.now() - startTime;
6470
+
6471
+ // Build display data with the rendered SVG
6472
+ /** @type {DisplayData[]} */
6473
+ const displayData = [
6474
+ {
6475
+ data: {
6476
+ 'text/html': svg,
6477
+ 'text/plain': `[Mermaid diagram rendered]`,
6478
+ },
6479
+ metadata: {
6480
+ mermaid: true,
6481
+ diagramId,
6482
+ },
6483
+ },
6484
+ ];
6485
+
6486
+ return {
6487
+ success: true,
6488
+ stdout: '',
6489
+ stderr: '',
6490
+ result: undefined,
6491
+ displayData,
6492
+ assets: [],
6493
+ executionCount: 0,
6494
+ duration,
6495
+ };
6496
+ } catch (error) {
6497
+ const duration = performance.now() - startTime;
6498
+ const errorMessage = error instanceof Error ? error.message : String(error);
6499
+
6500
+ // Return error as stderr with helpful message
6501
+ return {
6502
+ success: false,
6503
+ stdout: '',
6504
+ stderr: `Mermaid rendering error: ${errorMessage}`,
6505
+ result: undefined,
6506
+ error: {
6507
+ type: 'MermaidError',
6508
+ message: errorMessage,
6509
+ },
6510
+ displayData: [],
6511
+ assets: [],
6512
+ executionCount: 0,
6513
+ duration,
6514
+ };
6515
+ }
6516
+ }
6517
+ }
6518
+
6519
+ /**
6520
+ * Create a Mermaid executor
6521
+ * @returns {MermaidExecutor}
6522
+ */
6523
+ function createMermaidExecutor() {
6524
+ return new MermaidExecutor();
6525
+ }
6526
+
6201
6527
  /**
6202
6528
  * Execute Module
6203
6529
  *
@@ -6216,6 +6542,7 @@ function createDefaultExecutorRegistry() {
6216
6542
  registry.register(new JavaScriptExecutor());
6217
6543
  registry.register(new HtmlExecutor());
6218
6544
  registry.register(new CssExecutor());
6545
+ registry.register(new MermaidExecutor());
6219
6546
  return registry;
6220
6547
  }
6221
6548
 
@@ -7542,6 +7869,7 @@ exports.HtmlRenderer = HtmlRenderer;
7542
7869
  exports.IframeContext = IframeContext;
7543
7870
  exports.JavaScriptExecutor = JavaScriptExecutor;
7544
7871
  exports.MainContext = MainContext;
7872
+ exports.MermaidExecutor = MermaidExecutor;
7545
7873
  exports.MrpRuntime = MrpRuntime;
7546
7874
  exports.RUNTIME_NAME = RUNTIME_NAME;
7547
7875
  exports.RUNTIME_VERSION = RUNTIME_VERSION;
@@ -7561,6 +7889,7 @@ exports.createHtmlRenderer = createHtmlRenderer;
7561
7889
  exports.createIframeContext = createIframeContext;
7562
7890
  exports.createJavaScriptExecutor = createJavaScriptExecutor;
7563
7891
  exports.createMainContext = createMainContext;
7892
+ exports.createMermaidExecutor = createMermaidExecutor;
7564
7893
  exports.createRuntime = createRuntime;
7565
7894
  exports.createSession = createSession;
7566
7895
  exports.createSessionManager = createSessionManager;