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.
- 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/mrmd-js.iife.js
CHANGED
|
@@ -1036,48 +1036,96 @@ var mrmdJs = (function (exports) {
|
|
|
1036
1036
|
*/
|
|
1037
1037
|
|
|
1038
1038
|
/**
|
|
1039
|
-
* Extract
|
|
1040
|
-
* Handles var
|
|
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;
|
|
1047
|
-
* // Returns: ['x', '
|
|
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,
|
|
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
|
-
//
|
|
1057
|
-
|
|
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(
|
|
1062
|
-
const
|
|
1063
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
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)
|