mrmd-js 2.2.0 → 2.3.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/dist/index.cjs CHANGED
@@ -1035,48 +1035,96 @@ function createMainContext(options) {
1035
1035
  */
1036
1036
 
1037
1037
  /**
1038
- * Extract all variable names that will be declared by the code.
1039
- * Handles var, let, const, function, and class declarations.
1038
+ * Extract top-level variable names declared by the code.
1039
+ * Handles top-level var/let/const, function, class, and bare assignments.
1040
1040
  *
1041
1041
  * @param {string} code - Source code
1042
1042
  * @returns {string[]} Array of declared variable names
1043
1043
  *
1044
1044
  * @example
1045
- * extractDeclaredVariables('const x = 1; let { a, b } = obj; function foo() {}')
1046
- * // Returns: ['x', 'a', 'b', 'foo']
1045
+ * extractDeclaredVariables('const x = 1; function foo() {}')
1046
+ * // Returns: ['x', 'foo']
1047
1047
  */
1048
1048
  function extractDeclaredVariables(code) {
1049
1049
  const variables = new Set();
1050
1050
 
1051
- // Remove strings, comments to avoid false matches
1051
+ // Remove strings/comments, then mask nested scopes so we only inspect top-level declarations.
1052
1052
  const cleaned = removeStringsAndComments(code);
1053
+ const topLevel = maskNestedScopes(cleaned);
1053
1054
 
1054
- // Match var/let/const declarations
1055
- // Handles: const x = 1, let x = 1, var x = 1
1056
- // Handles: const { a, b } = obj, const [a, b] = arr
1057
- const varPattern = /\b(?:var|let|const)\s+([^=;]+?)(?:\s*=|\s*;|\s*$)/g;
1055
+ // Match top-level var/let/const declarations (simple/comma-separated identifiers).
1056
+ // This intentionally excludes nested function/local declarations.
1057
+ const varPattern = /(?:^|[;\n])\s*(?:var|let|const)\s+([^\n;]+)/gm;
1058
1058
 
1059
1059
  let match;
1060
- while ((match = varPattern.exec(cleaned)) !== null) {
1061
- const declaration = match[1].trim();
1062
- extractNamesFromPattern(declaration, variables);
1060
+ while ((match = varPattern.exec(topLevel)) !== null) {
1061
+ const fullStart = match.index;
1062
+ const fullEnd = fullStart + match[0].length;
1063
+ const originalStatement = cleaned.slice(fullStart, fullEnd);
1064
+ const originalDeclMatch = originalStatement.match(/(?:^|[;\n])\s*(?:var|let|const)\s+([^\n;]+)/m);
1065
+ const declaration = (originalDeclMatch?.[1] || match[1]).trim();
1066
+ const parts = splitByComma(declaration);
1067
+ for (const part of parts) {
1068
+ const candidate = part.split('=')[0].trim();
1069
+ extractNamesFromPattern(candidate, variables);
1070
+ }
1063
1071
  }
1064
1072
 
1065
- // Match function declarations
1073
+ // Match top-level function declarations
1066
1074
  const funcPattern = /\bfunction\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
1067
- while ((match = funcPattern.exec(cleaned)) !== null) {
1075
+ while ((match = funcPattern.exec(topLevel)) !== null) {
1068
1076
  variables.add(match[1]);
1069
1077
  }
1070
1078
 
1071
- // Match class declarations
1079
+ // Match top-level class declarations
1072
1080
  const classPattern = /\bclass\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
1073
- while ((match = classPattern.exec(cleaned)) !== null) {
1081
+ while ((match = classPattern.exec(topLevel)) !== null) {
1074
1082
  variables.add(match[1]);
1075
1083
  }
1076
1084
 
1085
+ // Match top-level bare assignments (e.g., x = 10, data = [...])
1086
+ // These create globals in notebook/REPL contexts
1087
+ const assignPattern = /(?:^|[;\n])\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=(?![=>])/gm;
1088
+ while ((match = assignPattern.exec(topLevel)) !== null) {
1089
+ const name = match[1];
1090
+ // Skip keywords that precede = in other contexts
1091
+ if (!['if', 'else', 'while', 'for', 'return', 'throw', 'typeof', 'delete', 'void', 'new', 'in', 'of', 'switch', 'case', 'default', 'try', 'catch', 'finally', 'do', 'break', 'continue', 'with', 'yield', 'await', 'async', 'import', 'export', 'var', 'let', 'const', 'function', 'class', 'this'].includes(name)) {
1092
+ variables.add(name);
1093
+ }
1094
+ }
1095
+
1077
1096
  return Array.from(variables);
1078
1097
  }
1079
1098
 
1099
+ /**
1100
+ * Keep only top-level text, replacing nested scope contents with spaces.
1101
+ * This avoids tracking local variables declared inside functions/blocks.
1102
+ *
1103
+ * @param {string} code
1104
+ * @returns {string}
1105
+ */
1106
+ function maskNestedScopes(code) {
1107
+ let result = '';
1108
+ let braceDepth = 0;
1109
+ let parenDepth = 0;
1110
+ let bracketDepth = 0;
1111
+
1112
+ for (let i = 0; i < code.length; i++) {
1113
+ const ch = code[i];
1114
+ const isTopLevel = braceDepth === 0 && parenDepth === 0 && bracketDepth === 0;
1115
+ result += isTopLevel ? ch : ' ';
1116
+
1117
+ if (ch === '{') braceDepth++;
1118
+ else if (ch === '}') braceDepth = Math.max(0, braceDepth - 1);
1119
+ else if (ch === '(') parenDepth++;
1120
+ else if (ch === ')') parenDepth = Math.max(0, parenDepth - 1);
1121
+ else if (ch === '[') bracketDepth++;
1122
+ else if (ch === ']') bracketDepth = Math.max(0, bracketDepth - 1);
1123
+ }
1124
+
1125
+ return result;
1126
+ }
1127
+
1080
1128
  /**
1081
1129
  * Extract variable names from a destructuring pattern or simple identifier
1082
1130
  * @param {string} pattern
@@ -1789,24 +1837,204 @@ ${code}
1789
1837
  })()`;
1790
1838
  }
1791
1839
 
1840
+ /**
1841
+ * In notebook/REPL flows, users often type bare object literals:
1842
+ * { "a": 1, b: 2 }
1843
+ * JavaScript parses this as a block statement unless wrapped.
1844
+ * This heuristic wraps likely trailing object literals with parentheses.
1845
+ *
1846
+ * @param {string} code
1847
+ * @returns {string}
1848
+ */
1849
+ const OBJECT_LITERAL_PREFIX_RE = /^\s*(?:[a-zA-Z_$][a-zA-Z0-9_$]*|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\[[^\]]+\])\s*:\s*/s;
1850
+
1851
+ function looksLikeObjectLiteralBlock(blockSource) {
1852
+ if (!blockSource.startsWith('{') || !blockSource.endsWith('}')) {
1853
+ return false;
1854
+ }
1855
+
1856
+ const inner = blockSource.slice(1, -1);
1857
+ const propertyPrefix = inner.match(OBJECT_LITERAL_PREFIX_RE);
1858
+ if (!propertyPrefix) {
1859
+ return false;
1860
+ }
1861
+
1862
+ // Avoid wrapping likely label statements: { foo: while (...) { ... } }
1863
+ const afterColon = inner.slice(propertyPrefix[0].length).trimStart();
1864
+ return !/^(?:while|for|if|switch|try|do|break|continue)\b/.test(afterColon);
1865
+ }
1866
+
1867
+ function findTopLevelBraceRanges(code) {
1868
+ const ranges = [];
1869
+ let depth = 0;
1870
+ let start = -1;
1871
+ let i = 0;
1872
+
1873
+ let inSingle = false;
1874
+ let inDouble = false;
1875
+ let inTemplate = false;
1876
+ let inLineComment = false;
1877
+ let inBlockComment = false;
1878
+
1879
+ while (i < code.length) {
1880
+ const ch = code[i];
1881
+ const next = code[i + 1];
1882
+
1883
+ if (inLineComment) {
1884
+ if (ch === '\n') inLineComment = false;
1885
+ i++;
1886
+ continue;
1887
+ }
1888
+ if (inBlockComment) {
1889
+ if (ch === '*' && next === '/') {
1890
+ inBlockComment = false;
1891
+ i += 2;
1892
+ continue;
1893
+ }
1894
+ i++;
1895
+ continue;
1896
+ }
1897
+ if (inSingle) {
1898
+ if (ch === '\\') {
1899
+ i += 2;
1900
+ continue;
1901
+ }
1902
+ if (ch === '\'') inSingle = false;
1903
+ i++;
1904
+ continue;
1905
+ }
1906
+ if (inDouble) {
1907
+ if (ch === '\\') {
1908
+ i += 2;
1909
+ continue;
1910
+ }
1911
+ if (ch === '"') inDouble = false;
1912
+ i++;
1913
+ continue;
1914
+ }
1915
+ if (inTemplate) {
1916
+ if (ch === '\\') {
1917
+ i += 2;
1918
+ continue;
1919
+ }
1920
+ if (ch === '`') inTemplate = false;
1921
+ i++;
1922
+ continue;
1923
+ }
1924
+
1925
+ if (ch === '/' && next === '/') {
1926
+ inLineComment = true;
1927
+ i += 2;
1928
+ continue;
1929
+ }
1930
+ if (ch === '/' && next === '*') {
1931
+ inBlockComment = true;
1932
+ i += 2;
1933
+ continue;
1934
+ }
1935
+ if (ch === '\'') {
1936
+ inSingle = true;
1937
+ i++;
1938
+ continue;
1939
+ }
1940
+ if (ch === '"') {
1941
+ inDouble = true;
1942
+ i++;
1943
+ continue;
1944
+ }
1945
+ if (ch === '`') {
1946
+ inTemplate = true;
1947
+ i++;
1948
+ continue;
1949
+ }
1950
+
1951
+ if (ch === '{') {
1952
+ if (depth === 0) start = i;
1953
+ depth++;
1954
+ } else if (ch === '}' && depth > 0) {
1955
+ depth--;
1956
+ if (depth === 0 && start >= 0) {
1957
+ ranges.push([start, i + 1]);
1958
+ start = -1;
1959
+ }
1960
+ }
1961
+
1962
+ i++;
1963
+ }
1964
+
1965
+ return ranges;
1966
+ }
1967
+
1968
+ function normalizeStandaloneObjectLiteral(code) {
1969
+ const source = String(code || '');
1970
+ if (!source.trim()) return code;
1971
+
1972
+ // Ignore trailing whitespace and semicolons when locating the final statement.
1973
+ let logicalEnd = source.length;
1974
+ while (logicalEnd > 0 && /\s/.test(source[logicalEnd - 1])) logicalEnd--;
1975
+ while (logicalEnd > 0 && source[logicalEnd - 1] === ';') logicalEnd--;
1976
+ while (logicalEnd > 0 && /\s/.test(source[logicalEnd - 1])) logicalEnd--;
1977
+ if (logicalEnd === 0 || source[logicalEnd - 1] !== '}') {
1978
+ return code;
1979
+ }
1980
+
1981
+ const segment = source.slice(0, logicalEnd);
1982
+ const ranges = findTopLevelBraceRanges(segment);
1983
+ if (ranges.length === 0) {
1984
+ return code;
1985
+ }
1986
+
1987
+ const [blockStart, blockEnd] = ranges[ranges.length - 1];
1988
+ if (blockEnd !== logicalEnd) {
1989
+ return code;
1990
+ }
1991
+
1992
+ // Be conservative when the block follows another token that could require a
1993
+ // statement block (e.g. if (...) { ... }).
1994
+ if (blockStart > 0) {
1995
+ let prev = blockStart - 1;
1996
+ while (prev >= 0 && /\s/.test(source[prev])) prev--;
1997
+ if (prev >= 0 && source[prev] !== ';' && source[prev] !== '}') {
1998
+ return code;
1999
+ }
2000
+ }
2001
+
2002
+ const blockSource = segment.slice(blockStart, blockEnd);
2003
+ if (!looksLikeObjectLiteralBlock(blockSource)) {
2004
+ return code;
2005
+ }
2006
+
2007
+ return `${source.slice(0, blockStart)}(${blockSource})${source.slice(logicalEnd)}`;
2008
+ }
2009
+
1792
2010
  /**
1793
2011
  * Wrap code and capture the last expression value
1794
2012
  *
1795
2013
  * @param {string} code - Source code
2014
+ * @param {string[]} [persistentVars=[]] - Variables to copy to globalThis after async execution
1796
2015
  * @returns {string} Wrapped code that returns last expression
1797
2016
  */
1798
- function wrapWithLastExpression(code) {
2017
+ function wrapWithLastExpression(code, persistentVars = []) {
1799
2018
  // Auto-insert awaits for common async patterns (fetch, import, .json(), etc.)
1800
2019
  // This makes JavaScript feel more linear like Python/R/Julia
1801
2020
  const autoAwaitedCode = autoInsertAwaits(code);
2021
+ const replFriendlyCode = normalizeStandaloneObjectLiteral(autoAwaitedCode);
1802
2022
 
1803
2023
  // Check if code needs async (either explicit await or auto-inserted)
1804
- const needsAsync = hasTopLevelAwait(autoAwaitedCode);
2024
+ const needsAsync = hasTopLevelAwait(replFriendlyCode);
2025
+
2026
+ const persistenceLines = Array.isArray(persistentVars)
2027
+ ? persistentVars
2028
+ .filter((name) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name))
2029
+ .map((name) => `if (typeof ${name} !== 'undefined') globalThis[${JSON.stringify(name)}] = ${name};`)
2030
+ .join('\n')
2031
+ : '';
1805
2032
 
1806
2033
  if (needsAsync) {
1807
2034
  // For code with await, wrap in async IIFE
1808
2035
  // This allows await to work at the "top level" of the user's code
1809
- const asyncWrappedCode = `(async () => {\n${autoAwaitedCode}\n})()`;
2036
+ // Keep user completion value via try/finally, while persisting top-level vars.
2037
+ const asyncWrappedCode = `(async () => {\ntry {\n${replFriendlyCode}\n} finally {\n${persistenceLines}\n}\n})()`;
1810
2038
 
1811
2039
  // Now wrap to capture the result
1812
2040
  // Use indirect eval (0, eval)() to run in global scope so var declarations persist
@@ -1830,16 +2058,16 @@ function wrapWithLastExpression(code) {
1830
2058
 
1831
2059
  // No async needed - use simpler synchronous wrapper
1832
2060
  // 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
2061
+ // Note: use replFriendlyCode so bare object literals return as expressions.
1834
2062
  const wrapped = `
1835
2063
  ;(function() {
1836
2064
  let __result__;
1837
2065
  try {
1838
- __result__ = (0, eval)(${JSON.stringify(autoAwaitedCode)});
2066
+ __result__ = (0, eval)(${JSON.stringify(replFriendlyCode)});
1839
2067
  } catch (e) {
1840
2068
  if (e instanceof SyntaxError) {
1841
2069
  // Code might be statements, not expression
1842
- (0, eval)(${JSON.stringify(autoAwaitedCode)});
2070
+ (0, eval)(${JSON.stringify(replFriendlyCode)});
1843
2071
  __result__ = undefined;
1844
2072
  } else {
1845
2073
  throw e;
@@ -1933,7 +2161,7 @@ class JavaScriptExecutor extends BaseExecutor {
1933
2161
  const transformed = transformForPersistence(code);
1934
2162
 
1935
2163
  // Wrap to capture last expression value and support async
1936
- const wrapped = wrapWithLastExpression(transformed);
2164
+ const wrapped = wrapWithLastExpression(transformed, declaredVars);
1937
2165
 
1938
2166
  try {
1939
2167
  // Execute in context (pass execId for input() support)