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 +251 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +251 -23
- package/dist/index.js.map +1 -1
- package/dist/mrmd-js.iife.js +251 -23
- package/dist/mrmd-js.iife.js.map +1 -1
- package/package.json +1 -1
- package/src/execute/javascript.js +1 -1
- package/src/transform/async.js +186 -6
- package/src/transform/extract.js +64 -16
package/dist/index.cjs
CHANGED
|
@@ -1035,48 +1035,96 @@ function createMainContext(options) {
|
|
|
1035
1035
|
*/
|
|
1036
1036
|
|
|
1037
1037
|
/**
|
|
1038
|
-
* Extract
|
|
1039
|
-
* Handles var
|
|
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;
|
|
1046
|
-
* // Returns: ['x', '
|
|
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,
|
|
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
|
-
//
|
|
1056
|
-
|
|
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(
|
|
1061
|
-
const
|
|
1062
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
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)
|