1- // At the very top of pytml.js
21fetch ( 'https://pytml.vercel.app/api/count' )
32 . then ( r => r . json ( ) )
4- . then ( data => console . log ( 'Pytml loaded:' , data . message , 'times' ) ) ;
3+ . then ( data => console . log ( 'Pytml loaded:' , data . message , 'times' ) )
4+ . catch ( ( ) => { } ) ;
55( function ( window ) {
66 'use strict' ;
7-
8- // ----------------------------------------------
9- // PYTML – Main Class
10- // ----------------------------------------------
117 class PYTML {
128 constructor ( ) {
139 this . outputContainer = null ;
@@ -16,33 +12,22 @@ fetch('https://pytml.vercel.app/api/count')
1612 this . pendingPromises = [ ] ;
1713 this . init ( ) ;
1814 }
19-
20- // --------------------------------------------------
21- // Initialisation
22- // --------------------------------------------------
2315 async init ( ) {
2416 console . log ( '[PYTML] Initialising...' ) ;
2517 this . createOutputContainer ( ) ;
2618 this . showStatus ( 'Loading Python engine...' ) ;
2719
2820 try {
29- // Load Pyodide script
3021 await this . loadPyodideScript ( ) ;
31-
32- // Initialise Pyodide
3322 const pyodide = await loadPyodide ( {
3423 indexURL : 'https://cdn.jsdelivr.net/pyodide/v0.26.4/full/'
3524 } ) ;
3625 window . pyodide = pyodide ;
37-
38- // Setup Python environment
3926 await this . setupPythonEnvironment ( ) ;
4027
4128 this . isReady = true ;
4229 this . hideStatus ( ) ;
4330 console . log ( '[PYTML] Ready!' ) ;
44-
45- // Execute all Python scripts (inline and external)
4631 await this . runAllPythonScripts ( ) ;
4732
4833 } catch ( error ) {
@@ -51,10 +36,6 @@ fetch('https://pytml.vercel.app/api/count')
5136 console . error ( '[PYTML] Fatal error:' , error ) ;
5237 }
5338 }
54-
55- // --------------------------------------------------
56- // DOM Setup
57- // --------------------------------------------------
5839 createOutputContainer ( ) {
5940 let container = document . getElementById ( 'pytml-output' ) ;
6041 if ( ! container ) {
@@ -196,10 +177,12 @@ fetch('https://pytml.vercel.app/api/count')
196177 async setupPythonEnvironment ( ) {
197178 const pyodide = window . pyodide ;
198179
199- // Override stdout and stderr
180+
181+ // Override stdout and stderr + DEFINE AST TRANSFORMER ONCE
200182 await pyodide . runPythonAsync ( `
201183import sys
202184import js
185+ import ast
203186
204187class OutputRedirect:
205188 def __init__(self, is_error=False):
@@ -215,11 +198,25 @@ sys.stderr = OutputRedirect(True)
215198
216199# Override input – async function that calls JS
217200async def input(prompt=""):
218- # We do NOT print(prompt) here – the JS will show it in the DOM
219201 result = await js.pyodideInstance.getUserInput(str(prompt))
220202 return result
221- ` ) ;
222203
204+ # ---------- AST TRANSFORMER (DEFINED ONCE) ----------
205+ class InputTransformer(ast.NodeTransformer):
206+ def visit_Call(self, node):
207+ if isinstance(node.func, ast.Name) and node.func.id == 'input':
208+ new_node = ast.Await(value=node)
209+ return ast.copy_location(new_node, node)
210+ self.generic_visit(node)
211+ return node
212+
213+ def transform_pytml_code(code):
214+ tree = ast.parse(code)
215+ transformer = InputTransformer()
216+ new_tree = transformer.visit(tree)
217+ ast.fix_missing_locations(new_tree)
218+ return ast.unparse(new_tree)
219+ ` ) ;
223220 // Also expose the instance to Python's js module
224221 window . pyodideInstance = this ;
225222
@@ -286,38 +283,29 @@ async def input(prompt=""):
286283 // --------------------------------------------------
287284 // AST‑Based Code Transformation (safe input replacement)
288285 // --------------------------------------------------
289- async transformPythonCode ( code ) {
290- // We use Python's ast module to rewrite `input()` calls into `await input()`
291- // This ensures we only replace actual function calls, not strings, comments, or variable names.
292- const pyodide = window . pyodide ;
293- const transformScript = `
294- import ast
295-
296- class InputTransformer(ast.NodeTransformer):
297- def visit_Call(self, node):
298- # Check if it's a call to 'input'
299- if isinstance(node.func, ast.Name) and node.func.id == 'input':
300- # Wrap the call in an Await node
301- new_node = ast.Await(value=node)
302- return ast.copy_location(new_node, node)
303- # Recursively transform child nodes
304- self.generic_visit(node)
305- return node
306-
307- def transform(code):
308- tree = ast.parse(code)
309- transformer = InputTransformer()
310- new_tree = transformer.visit(tree)
311- ast.fix_missing_locations(new_tree)
312- return ast.unparse(new_tree)
313- ` ;
314- await pyodide . runPythonAsync ( transformScript ) ;
286+ async transformPythonCode ( code ) {
287+ if ( ! this . isReady ) {
288+ this . addError ( 'Python engine not ready yet.' ) ;
289+ return code ;
290+ }
315291
316- // Call the transform function
317- const result = await pyodide . runPythonAsync ( `
318- transform('''${ code . replace ( / ' / g, "\\'" ) } ''')
292+ try {
293+ // ✅ Escape triple quotes to safely pass code to Python
294+ const escapedCode = code . replace ( / " " " / g, '\\"\\"\\"' ) ;
295+
296+ // Pass code as a Python variable (safe, no interpolation crashes)
297+ await window . pyodide . runPythonAsync ( `_pytml_code = """${ escapedCode } """` ) ;
298+
299+ // Call the pre-defined transformer (defined once in setupPythonEnvironment)
300+ const result = await window . pyodide . runPythonAsync ( `
301+ result = transform_pytml_code(_pytml_code)
302+ result
319303` ) ;
320- return result ;
304+ return result ;
305+ } catch ( error ) {
306+ this . addError ( 'Code transformation error: ' + error . message ) ;
307+ return code ; // Fallback to raw code if transformation fails
308+ }
321309 }
322310
323311 // --------------------------------------------------
@@ -412,7 +400,8 @@ transform('''${code.replace(/'/g, "\\'")}''')
412400
413401 hideStatus ( ) {
414402 if ( this . statusElement ) {
415- this . statusElement . style . display = 'none' ;
403+ this . statusElement . remove ( ) ; // Actually remove from DOM
404+ this . statusElement = null ; // Free memory
416405 }
417406 }
418407 }
0 commit comments