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