pyfrilet 0.6.4 → 0.6.6

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/README.md CHANGED
@@ -249,18 +249,81 @@ En mode p5, le sketch est arrêté dès la première erreur (y compris les erreu
249
249
 
250
250
  Quand le code Python ne contient **aucun import de `p5`**, pyfrilet bascule automatiquement en **mode terminal** : le canvas est remplacé par un terminal [xterm.js](https://xtermjs.org/) qui reçoit `stdout` et `stderr`.
251
251
 
252
- Ce mode permet d'écrire des programmes Python classiques — algorithmes, structures de données, visualisations texte — sans aucun lien avec p5.
252
+ Ce mode permet d'écrire des programmes Python classiques — algorithmes, structures de données, jeux textuels, visualisations — sans aucun lien avec p5.
253
+
254
+ ### async/await transparent
255
+
256
+ Le mode terminal tourne dans la boucle asyncio de Pyodide. Comme `input()` doit attendre la saisie de l'utilisateur, le moteur d'exécution est fondamentalement asynchrone. pyfrilet s'en charge automatiquement : **le code utilisateur n'a pas besoin de connaître async/await**.
257
+
253
258
  ```python
254
259
  # Pas d'import p5 → mode terminal automatique
260
+ # Ni async, ni await : ça marche simplement
261
+
262
+ def demander_nombre():
263
+ return int(input("Un nombre : "))
264
+
265
+ n = demander_nombre()
266
+ print(f"Le double : {n * 2}")
267
+ ```
268
+
269
+ En coulisses, pyfrilet applique une transformation AST avant l'exécution :
270
+ - toutes les `def` sont converties en `async def`
271
+ - tous les appels de fonctions sont wrappés dans `await` automatiquement
272
+ - `time.sleep(x)` est redirigé vers `asyncio.sleep(x)` pour ne pas geler l'onglet
273
+
274
+ ```python
275
+ import time
276
+
277
+ def compte_a_rebours(n):
278
+ for i in range(n, 0, -1):
279
+ print(i)
280
+ time.sleep(1) # → asyncio.sleep automatiquement
281
+ print("Go !")
282
+
283
+ compte_a_rebours(3)
284
+ ```
285
+
286
+ ### Limites de la transformation automatique
287
+
288
+ La transformation couvre les cas courants mais a des angles morts :
289
+
290
+ **Les méthodes dunder (`__init__`, `__str__`…) restent synchrones.** Python les appelle directement sans passer par le wrapper. Elles ne peuvent donc pas appeler `input()` :
291
+
292
+ ```python
293
+ class Animal:
294
+ def __init__(self, nom):
295
+ self.nom = nom # OK
296
+ # self.nom = input() # ← ne fonctionnerait pas ici
297
+
298
+ def parler(self): # converti en async def automatiquement
299
+ return input(f"{self.nom} dit : ") # OK ici
300
+ ```
301
+
302
+ **Les lambdas restent synchrones** (Python ne supporte pas les lambdas async) — ne pas y appeler `input()`.
303
+
304
+ **Le sleep rebindé n'est pas détecté.** pyfrilet reconnaît `sleep(x)`, `time.sleep(x)` et `asyncio.sleep(x)` et les redirige tous vers un sleep annulable. En revanche `mon_sleep = time.sleep; mon_sleep(x)` ne sera pas intercepté.
305
+
306
+ ### Quand écrire async/await explicitement
307
+
308
+ Pour des programmes plus avancés, on peut écrire `async`/`await` directement — pyfrilet les respecte et ne double-wrappe pas :
309
+
310
+ ```python
255
311
  import asyncio
312
+ from rich.progress import Progress
256
313
 
257
- for i in range(5):
258
- print(f"étape {i + 1}")
259
- await asyncio.sleep(0.5)
314
+ async def charger():
315
+ with Progress(auto_refresh=False) as progress:
316
+ task = progress.add_task("calcul…", total=100)
317
+ for i in range(100):
318
+ await asyncio.sleep(0.03)
319
+ progress.advance(task)
320
+ progress.refresh()
260
321
 
261
- print("terminé !")
322
+ await charger()
262
323
  ```
263
324
 
325
+ En pratique : pour des scripts simples avec `input()` et `time.sleep()`, pas besoin de `async`/`await`. Dès qu'on fait de l'animation fine, des appels réseau, ou qu'on combine plusieurs coroutines, les écrire explicitement est plus sûr et plus lisible.
326
+
264
327
  ### `input()`
265
328
 
266
329
  `input()` est entièrement supporté : le programme se met en attente, le terminal affiche le prompt et accepte la saisie clavier.
@@ -294,20 +357,6 @@ table.add_row("Terre", "12 742")
294
357
  console.print(table)
295
358
  ```
296
359
 
297
- > **Boucle d'événements** : le mode terminal tourne dans la boucle asyncio de Pyodide. Pour obtenir un rendu progressif avec `rich.Progress` ou toute autre animation, remplacer `time.sleep()` par `await asyncio.sleep()` et passer `auto_refresh=False` aux composants rich qui gèrent leur propre thread de rafraîchissement :
298
- >
299
- > ```python
300
- > import asyncio
301
- > from rich.progress import Progress
302
- >
303
- > with Progress(auto_refresh=False) as progress:
304
- > task = progress.add_task("calcul…", total=100)
305
- > for i in range(100):
306
- > await asyncio.sleep(0.03)
307
- > progress.advance(task)
308
- > progress.refresh()
309
- > ```
310
-
311
360
  ### Persistence (IndexedDB)
312
361
 
313
362
  Pour conserver des données entre les rechargements de page, pyfrilet monte automatiquement un répertoire IndexedDB sur `/persist` au démarrage de chaque sketch — aussi bien en mode p5 qu'en mode terminal. Ce répertoire est disponible sans aucune configuration.
@@ -335,7 +384,9 @@ En mode p5, importer avec `from p5 import persist` ou `from p5 import *`. En mod
335
384
 
336
385
  ### Relancer
337
386
 
338
- Le bouton ▶ (ou `Shift+Entrée`) interrompt proprement le programme en cours (y compris si `input()` est en attente) et relance l'exécution depuis le début.
387
+ Le bouton ▶ (ou `Shift+Entrée`) interrompt le programme en cours et relance l'exécution depuis le début. L'interruption est propre même si le programme est en attente de `input()` ou en train de dormir dans un `time.sleep()` ces deux opérations sont annulables sans délai. Si le programme est suspendu sur autre chose (réseau, calcul long…), pyfrilet attend au maximum 3 secondes avant de forcer le nouveau départ.
388
+
389
+ Cliquer plusieurs fois rapidement sur ▶ est sans danger : seul le **dernier** clic produit une exécution, les intermédiaires sont ignorés.
339
390
 
340
391
  ---
341
392
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyfrilet",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "type": "module",
5
5
  "main": "pyfrilet.js",
6
6
  "files": [
package/pyfrilet.js CHANGED
@@ -1011,7 +1011,7 @@ function main(tabs, htmlTabs, SK, URLS, noWatchdog, staleSnapshot) {
1011
1011
  function updateLineOffsets() {
1012
1012
  let offset = 1;
1013
1013
  tabs.filter(t => t.type === 'python').forEach(tab => {
1014
- if (!tab.hidden && !tab.readonly && aceSessions[tab.id]) {
1014
+ if (!tab.hidden && aceSessions[tab.id]) {
1015
1015
  aceSessions[tab.id].setOption('firstLineNumber', offset);
1016
1016
  offset += aceSessions[tab.id].getLength();
1017
1017
  } else {
@@ -1513,16 +1513,100 @@ async def _pf_async_input(prompt=""):
1513
1513
  result = await _pfTerminalInput(str(prompt) if prompt else "")
1514
1514
  return result
1515
1515
 
1516
- async def _pf_run_terminal(source):
1517
- class _InputAwaiter(_ast.NodeTransformer):
1518
- def visit_Call(self, node):
1519
- self.generic_visit(node)
1520
- if isinstance(node.func, _ast.Name) and node.func.id == 'input':
1521
- return _ast.Await(value=node)
1516
+ # Cancellation flag — set by JS before launching a new run
1517
+ _pf_cancel_run = [False]
1518
+
1519
+ async def _pf_async_sleep(seconds):
1520
+ """Cancellable sleep: polls the cancel flag every 50 ms."""
1521
+ import asyncio as _aio
1522
+ deadline = _aio.get_event_loop().time() + seconds
1523
+ while True:
1524
+ if _pf_cancel_run[0]:
1525
+ raise KeyboardInterrupt
1526
+ remaining = deadline - _aio.get_event_loop().time()
1527
+ if remaining <= 0:
1528
+ break
1529
+ await _aio.sleep(min(0.05, remaining))
1530
+
1531
+ async def _pf_maybe_await(val):
1532
+ """Await val if it's a coroutine, otherwise return it as-is.
1533
+ This lets us await-ify every call site without knowing in advance
1534
+ which functions are async."""
1535
+ import asyncio
1536
+ if asyncio.iscoroutine(val):
1537
+ return await val
1538
+ return val
1539
+
1540
+ class _AsyncTransformer(_ast.NodeTransformer):
1541
+ """Transforms user code so that:
1542
+ - every def becomes async def (including nested and class methods)
1543
+ - every call f(...) becomes await _pf_maybe_await(f(...))
1544
+ whether f is a Name, Attribute, subscript, or any other expression
1545
+ - lambda bodies are left untouched (can't be async)
1546
+ """
1547
+ _HELPER = '_pf_maybe_await'
1548
+
1549
+ def visit_FunctionDef(self, node):
1550
+ # Dunder methods (__init__, __str__...) are called by Python internals
1551
+ # without going through _pf_maybe_await — they must stay synchronous.
1552
+ self.generic_visit(node)
1553
+ if node.name.startswith('__') and node.name.endswith('__'):
1554
+ return node
1555
+ return _ast.AsyncFunctionDef(
1556
+ name=node.name,
1557
+ args=node.args,
1558
+ body=node.body,
1559
+ decorator_list=node.decorator_list,
1560
+ returns=node.returns,
1561
+ lineno=node.lineno,
1562
+ col_offset=node.col_offset,
1563
+ end_lineno=node.end_lineno,
1564
+ end_col_offset=node.end_col_offset,
1565
+ )
1566
+
1567
+ # Leave Lambda untouched — can't be async
1568
+ def visit_Lambda(self, node):
1569
+ return node
1570
+
1571
+ def visit_Await(self, node):
1572
+ # User wrote explicit await f() — recurse into f()'s arguments
1573
+ # but don't re-wrap the call itself with _pf_maybe_await.
1574
+ self._skip_next_wrap = True
1575
+ self.generic_visit(node)
1576
+ self._skip_next_wrap = False
1577
+ return node
1578
+
1579
+ def visit_Call(self, node):
1580
+ # Recurse first so nested calls are also transformed
1581
+ self.generic_visit(node)
1582
+ func = node.func
1583
+ # time.sleep(x) → _pf_async_sleep(x) (cancellable)
1584
+ if (isinstance(func, _ast.Attribute)
1585
+ and isinstance(func.value, _ast.Name)
1586
+ and func.value.id == 'time'
1587
+ and func.attr == 'sleep'):
1588
+ node.func = _ast.Name(id='_pf_async_sleep', ctx=_ast.Load())
1589
+ # sleep(x) → _pf_async_sleep(x) (from time import sleep)
1590
+ elif isinstance(func, _ast.Name) and func.id == 'sleep':
1591
+ node.func = _ast.Name(id='_pf_async_sleep', ctx=_ast.Load())
1592
+ # asyncio.sleep(x) → _pf_async_sleep(x) (cancellable)
1593
+ elif (isinstance(func, _ast.Attribute)
1594
+ and isinstance(func.value, _ast.Name)
1595
+ and func.value.id == 'asyncio'
1596
+ and func.attr == 'sleep'):
1597
+ node.func = _ast.Name(id='_pf_async_sleep', ctx=_ast.Load())
1598
+ # If already under a user-written await, don't re-wrap
1599
+ if getattr(self, '_skip_next_wrap', False):
1600
+ self._skip_next_wrap = False
1522
1601
  return node
1602
+ # Wrap: f(...) → await _pf_maybe_await(f(...))
1603
+ helper = _ast.Name(id=self._HELPER, ctx=_ast.Load())
1604
+ wrapped = _ast.Call(func=helper, args=[node], keywords=[])
1605
+ return _ast.Await(value=wrapped)
1523
1606
 
1607
+ async def _pf_run_terminal(source):
1524
1608
  tree = _ast.parse(source)
1525
- tree = _InputAwaiter().visit(tree)
1609
+ tree = _AsyncTransformer().visit(tree)
1526
1610
 
1527
1611
  wrapper = _ast.parse("async def programme(): pass")
1528
1612
  wrapper.body[0].body = tree.body if tree.body else [_ast.Pass()]
@@ -1533,7 +1617,15 @@ async def _pf_run_terminal(source):
1533
1617
  _f.write(source)
1534
1618
  lines = source.splitlines(keepends=True)
1535
1619
  _linecache.cache[_pf_fname] = (len(source), None, lines, _pf_fname)
1536
- _ns = {'input': _pf_async_input, 'persist': _pf_persist}
1620
+ import asyncio as _asyncio
1621
+ _pf_cancel_run[0] = False # reset at start of each run
1622
+ _ns = {
1623
+ 'input': _pf_async_input,
1624
+ 'persist': _pf_persist,
1625
+ '_pf_maybe_await': _pf_maybe_await,
1626
+ 'asyncio': _asyncio,
1627
+ '_pf_async_sleep': _pf_async_sleep,
1628
+ }
1537
1629
  exec(compile(wrapper, _pf_fname, 'exec'), _ns)
1538
1630
  try:
1539
1631
  await _ns['programme']()
@@ -1587,6 +1679,7 @@ async def _pf_run_terminal(source):
1587
1679
  /* ─────────────────── RUN CODE ───────────────── */
1588
1680
  let running = false;
1589
1681
  let _terminalRunning = false; /* true pendant qu'un runTerminalCode est actif */
1682
+ let _terminalGeneration = 0; /* incremented on each new run — stale waiters self-abort */
1590
1683
  let setupProxy = null, drawProxy = null,
1591
1684
  preloadProxy = null,
1592
1685
  mousePressedProxy = null, mouseReleasedProxy = null, mouseDraggedProxy = null,
@@ -1603,6 +1696,28 @@ async def _pf_run_terminal(source):
1603
1696
  }
1604
1697
 
1605
1698
  async function runTerminalCode(code) {
1699
+ /* Increment generation — any previous waiter with an older generation will self-abort */
1700
+ const myGen = ++_terminalGeneration;
1701
+
1702
+ /* If a run is already active, cancel it and wait for it to finish.
1703
+ We rely on _pf_cancel_run (checked in _pf_async_sleep) + hideTerminal
1704
+ (resolves pending input() with null). We do NOT use _pfInterrupt here —
1705
+ the SharedArrayBuffer SIGINT fires at arbitrary bytecodes and can crash
1706
+ inside stdlib (abc, asyncio…). */
1707
+ if (_terminalRunning) {
1708
+ if (pyodide) {
1709
+ try { pyodide.globals.get('_pf_cancel_run')[0] = true; } catch(e) {}
1710
+ }
1711
+ hideTerminal(); /* resolve any pending input() with null */
1712
+ const deadline = Date.now() + 3000;
1713
+ while (_terminalRunning && Date.now() < deadline) {
1714
+ await new Promise(r => setTimeout(r, 20));
1715
+ }
1716
+ }
1717
+
1718
+ /* A newer run was requested while we were waiting — bail out */
1719
+ if (myGen !== _terminalGeneration) return;
1720
+
1606
1721
  _terminalRunning = true;
1607
1722
  showTerminal();
1608
1723
  termClear();
@@ -1807,7 +1922,7 @@ async def _pf_run_terminal(source):
1807
1922
  }
1808
1923
 
1809
1924
  /* ─────────────────── DOWNLOAD ───────────────── */
1810
- const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.6.4/pyfrilet.min.js';
1925
+ const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.6.6/pyfrilet.min.js';
1811
1926
 
1812
1927
  const STANDALONE_TEMPLATE = `<!doctype html>
1813
1928
  <html lang="fr">
package/pyfrilet.min.js CHANGED
@@ -1 +1 @@
1
- !function(){"use strict";const e=document.currentScript;let n=!1;const t="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js",r="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js",a="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js",o="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js",i="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js",s="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js",d="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js",l="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js",c="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css",p="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js",f="https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js",m="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js",u="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css",_="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js",h="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js",y="https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.8.0/lib/addon-unicode11.min.js",b="html, body {\n height: 100%; margin: 0; overflow: hidden;\n background: #111;\n}\n#pf-root {\n position: fixed; inset: 0;\n display: flex; flex-direction: column;\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n}\n\n/* ── app area ── */\n#pf-app:focus { outline: none; }\n#pf-app {\n flex: 1; min-height: 0;\n position: relative;\n background: #111;\n display: flex; align-items: center; justify-content: center;\n overflow: hidden;\n}\n#pf-viewport {\n transform-origin: 50% 50%;\n will-change: transform;\n}\n#pf-viewport canvas {\n display: block;\n outline: none;\n}\n#pf-loader {\n position: absolute; inset: 0;\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n gap: 14px;\n background: #111;\n color: #565f89;\n font-size: 13px;\n z-index: 50;\n pointer-events: none;\n}\n#pf-loader-bar {\n width: 160px; height: 2px;\n background: #2a2c3e;\n border-radius: 2px;\n overflow: hidden;\n}\n#pf-loader-bar::after {\n content: '';\n display: block;\n height: 100%;\n width: 40%;\n background: #7aa2f7;\n border-radius: 2px;\n animation: pf-slide 1.2s ease-in-out infinite;\n}\n@keyframes pf-slide {\n 0% { transform: translateX(-100%); }\n 100% { transform: translateX(350%); }\n}\n\n/* ── drawer (slide-up editor panel) ── */\n#pf-drawer {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n background: #1a1b26;\n height: 32px; /* collapsed = handle only */\n transition: height 0.26s cubic-bezier(.4, 0, .2, 1);\n overflow: hidden;\n /* shadow cast upward onto the app */\n box-shadow: 0 -4px 20px rgba(0,0,0,.55);\n}\n#pf-drawer.pf-open {\n height: var(--pf-drawer-h, 56vh);\n}\n\n/* ── handle bar ── */\n#pf-handle {\n height: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n padding: 0 8px 0 6px;\n background: #24283b;\n border-top: 1px solid #3d4166;\n cursor: ns-resize;\n user-select: none;\n gap: 6px;\n flex-shrink: 0;\n}\n/* grip zone: clickable to toggle, draggable to resize */\n#pf-grip {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 5px 6px;\n flex-shrink: 0;\n opacity: .5;\n border-radius: 4px;\n transition: opacity .15s, background .15s;\n cursor: pointer;\n}\n#pf-grip:hover { opacity: .85; background: rgba(255,255,255,.06); }\n#pf-grip span {\n display: block;\n width: 16px; height: 2px;\n background: #a9b1d6;\n border-radius: 1px;\n}\n#pf-handle-hint {\n flex: 1;\n color: #565f89;\n font-size: 10px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n#pf-handle-btns {\n display: flex;\n gap: 4px;\n flex-shrink: 0;\n}\n.pf-btn {\n height: 26px;\n min-width: 26px;\n padding: 0 5px;\n border: 0; border-radius: 5px;\n cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n font-size: 13px; line-height: 1;\n white-space: nowrap;\n transition: background .15s, transform .1s, opacity .15s;\n outline: none;\n box-sizing: border-box;\n}\n.pf-btn:active { transform: scale(.88); }\n.pf-btn:focus-visible { outline: 2px solid #7aa2f7; outline-offset: 1px; }\n\n#pf-btn-run { background: #1a6b3a; color: #9ece6a; font-size: 11px; }\n#pf-btn-run:hover { background: #1f8447; color: #b9f27a; }\n#pf-btn-run.pf-running { opacity: .5; cursor: not-allowed; }\n\n#pf-btn-code { background: #2a2c3e; color: #7aa2f7; font-size: 14px; }\n#pf-btn-code:hover { background: #3d4166; color: #c0caf5; }\n#pf-btn-code.pf-active { background: #3d4166; color: #e0af68; }\n\n#pf-btn-dl { background: #2a2c3e; color: #9d7cd8; font-size: 14px; }\n#pf-btn-dl:hover { background: #3d4166; color: #bb9af7; }\n\n#pf-btn-rec { background: #2a2c3e; color: #f7768e; font-size: 13px; }\n#pf-btn-rec:hover { background: #3d4166; color: #ff9e9e; }\n#pf-btn-rec.pf-recording { background: #6b1a1a; color: #f7768e; animation: pf-blink .8s step-end infinite; }\n@keyframes pf-blink { 50% { opacity: .4; } }\n\n#pf-btn-reset { background: #2a2c3e; color: #e0af68; font-size: 16px; }\n#pf-btn-reset:hover { background: #3d4166; color: #ffc777; }\n#pf-btn-reset.pf-dirty::after {\n content: '●';\n position: absolute;\n top: 2px; right: 3px;\n font-size: 7px;\n color: #e0af68;\n line-height: 1;\n}\n#pf-btn-reset { position: relative; }\n\n/* ── editor area inside drawer ── */\n#pf-editor-wrap {\n flex: 1;\n min-height: 80px;\n position: relative;\n display: flex;\n flex-direction: column;\n}\n#pf-ace { flex: 1; position: relative; min-height: 0; }\n\n/* ── tab bar ── */\n#pf-tabs {\n display: flex;\n flex-shrink: 0;\n background: #1a1b2e;\n border-bottom: 1px solid #414868;\n overflow-x: auto;\n scrollbar-width: none;\n}\n#pf-tabs:empty { display: none; }\n.pf-tab {\n padding: 5px 14px;\n font-size: 12px;\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n color: #737aa2;\n cursor: pointer;\n white-space: nowrap;\n transition: color .15s, border-color .15s;\n}\n.pf-tab:hover { color: #c0caf5; }\n.pf-tab.pf-tab-active { color: #c0caf5; border-bottom-color: #7aa2f7; }\n.pf-tab.pf-tab-readonly::after { content: ' 🔒'; font-size: 10px; opacity: .6; }\n.pf-tab.pf-tab-markdown::after { content: ' ✎'; font-size: 11px; opacity: .6; }\n\n/* ── markdown view ── */\n@import url('https://fonts.googleapis.com/css2?family=Alegreya+Sans:ital,wght@0,400;0,700;1,400&family=Fira+Code:wght@300..700&display=swap');\n\n#pf-markdown-view {\n flex: 1;\n overflow: auto;\n background: #f4f4f0;\n}\n\n#pf-markdown-view .pf-md-inner {\n width: 100%;\n max-width: 680px;\n margin: 0 auto;\n padding: 48px 48px 72px;\n box-sizing: border-box;\n font-family: 'Alegreya Sans', Georgia, serif;\n font-size: 17px;\n line-height: 1.8;\n color: #1c1c2e;\n}\n\n#pf-markdown-view h1 {\n font-size: 2.1em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 0 0 .3em;\n padding-bottom: .3em;\n border-bottom: 2px solid #d8d8e8;\n line-height: 1.2;\n}\n#pf-markdown-view h2 {\n font-size: 1.4em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 2em 0 .5em;\n padding-bottom: .2em;\n border-bottom: 1px solid #e0e0ec;\n}\n#pf-markdown-view h3 {\n font-size: 1.1em;\n font-weight: 700;\n color: #2a2a4a;\n margin: 1.6em 0 .4em;\n}\n\n#pf-markdown-view p { margin: .75em 0; }\n#pf-markdown-view ul,\n#pf-markdown-view ol { padding-left: 1.6em; margin: .75em 0; }\n#pf-markdown-view li { margin: .3em 0; }\n#pf-markdown-view hr { border: none; border-top: 1px solid #dde; margin: 2em 0; }\n#pf-markdown-view blockquote {\n margin: 1em 0;\n padding: .5em 1em;\n border-left: 3px solid #aab;\n color: #555;\n background: #ededf5;\n border-radius: 0 4px 4px 0;\n}\n\n#pf-markdown-view code {\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n font-size: .84em;\n background: #e8e8f2;\n color: #3a3a6a;\n padding: .15em .45em;\n border-radius: 4px;\n}\n#pf-markdown-view pre {\n background: #1a1b2e;\n border-radius: 8px;\n padding: 1em 1.2em;\n overflow: auto;\n margin: 1.2em 0;\n box-shadow: 0 2px 8px rgba(0,0,0,.12);\n}\n#pf-markdown-view pre code {\n background: transparent;\n color: #c0caf5;\n font-size: .86em;\n padding: 0;\n line-height: 1.6;\n border-radius: 0;\n}\n\n#pf-markdown-view table {\n border-collapse: collapse;\n width: 100%;\n margin: 1.2em 0;\n font-size: .95em;\n}\n#pf-markdown-view th {\n background: #e4e4f0;\n color: #1c1c2e;\n font-weight: 700;\n text-align: left;\n padding: .55em .85em;\n border: 1px solid #d0d0e8;\n}\n#pf-markdown-view td {\n padding: .5em .85em;\n border: 1px solid #e0e0ee;\n vertical-align: top;\n}\n#pf-markdown-view tr:nth-child(even) td { background: #f0f0f8; }\n\n#pf-markdown-view a {\n color: #3a5fc8;\n text-decoration: none;\n border-bottom: 1px solid rgba(58,95,200,.3);\n transition: color .15s, border-color .15s;\n}\n#pf-markdown-view a:hover { color: #1a3fa0; border-bottom-color: #1a3fa0; }\n\n#pf-markdown-view .katex-display {\n overflow-x: auto;\n padding: .5em 0;\n margin: 1.2em 0;\n}\n#pf-markdown-view .mermaid {\n text-align: center;\n margin: 1.5em 0;\n background: #ededf5;\n border-radius: 8px;\n padding: 1em;\n}\n\n/* ── error panel (below editor, never overlaps ACE) ── */\n#pf-err {\n flex-shrink: 0;\n max-height: 120px;\n overflow: auto;\n margin: 0; padding: 8px 13px;\n font-size: 11.5px; line-height: 1.45;\n background: rgba(13, 3, 3, .95);\n color: #f7768e;\n white-space: pre-wrap;\n display: none;\n border-top: 1px solid rgba(247, 118, 142, .35);\n}\n/* ── xterm terminal ── */\n#pf-xterm {\n display: none;\n position: absolute;\n inset: 0;\n padding: 10px 12px;\n box-sizing: border-box;\n background: #000000;\n overflow: hidden;\n}\n\n#pf-xterm.pf-xterm-overlay {\n background: rgba(0, 0, 0, 0.82);\n}\n\n/* Bandeau bas — mode p5 print() */\n#pf-xterm.pf-xterm-bandeau {\n inset: auto 0 0 0;\n height: 200px;\n background: rgba(0, 0, 0, 0.85);\n border-top: 1px solid rgba(255,255,255,0.1);\n padding: 0 12px 10px;\n}\n#pf-xterm.pf-xterm-bandeau.pf-xterm-collapsed {\n height: 24px;\n padding: 0;\n overflow: hidden;\n}\n\n/* Poignée du bandeau */\n#pf-xterm-handle {\n display: none;\n height: 24px;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n color: rgba(255,255,255,0.4);\n font-size: 11px;\n letter-spacing: 2px;\n user-select: none;\n gap: 8px;\n}\n#pf-xterm-handle:hover { color: rgba(255,255,255,0.8); }\n.pf-xterm-bandeau #pf-xterm-handle { display: flex; }\n\n/* xterm interne : prendre toute la hauteur disponible sous la poignée */\n#pf-xterm .xterm {\n height: 100%;\n}\n#pf-xterm.pf-xterm-bandeau .xterm {\n height: calc(100% - 24px);\n}\n#pf-xterm .xterm-screen {\n height: 100% !important;\n}\n",g='<div id="pf-root">\n <div id="pf-app" tabindex="-1">\n <div id="pf-viewport"><div id="pf-sketch"></div></div>\n <div id="pf-xterm"><div id="pf-xterm-handle"><span id="pf-xterm-chevron">∧</span><span id="pf-xterm-handle-label">console</span></div></div>\n <div id="pf-loader">\n <span id="pf-loader-msg">Chargement…</span>\n <div id="pf-loader-bar"></div>\n </div>\n </div>\n <div id="pf-drawer">\n <div id="pf-handle">\n <div id="pf-grip" title="Clic → ouvrir/fermer"><span></span><span></span><span></span></div>\n <span id="pf-handle-hint">Clic ☰ → ouvrir/fermer &nbsp;·&nbsp; Shift+Entrée → relancer</span>\n <div id="pf-handle-btns">\n <button class="pf-btn" id="pf-btn-run" title="Relancer (Shift+Entrée)">&#9654;</button>\n <button class="pf-btn" id="pf-btn-code" title="Éditeur plein écran">&#9999;&#xFE0F;</button>\n <button class="pf-btn" id="pf-btn-dl" title="Télécharger HTML autonome">&#128190;</button>\n <button class="pf-btn" id="pf-btn-rec" title="Enregistrer WebM">⏺</button>\n <button class="pf-btn" id="pf-btn-help" title="Aide">?</button>\n <button class="pf-btn" id="pf-btn-reset" title="Réinitialiser le code (Ctrl+R)">&#8635;</button>\n </div>\n </div>\n <div id="pf-editor-wrap">\n <div id="pf-tabs"></div>\n <div id="pf-markdown-view" style="display:none"></div>\n <div id="pf-ace"></div>\n </div>\n <pre id="pf-err"></pre>\n </div>\n</div>';document.addEventListener("DOMContentLoaded",function(){const w=[...document.querySelectorAll('script[type="text/python"], script[type="text/markdown"], python')];if(0===w.length)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const x=e||w[0],v=(x.getAttribute("data-sources")||x.getAttribute("sources")||"cdn").toLowerCase().trim(),k=(x.getAttribute("data-vendor")||x.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/");n="cdn"===v;const E=w.some(e=>"text/markdown"===e.getAttribute("type")),C=n?{p5:t,pyodide:r,pyodideIndex:null,ace:a,acePython:o,aceMonokai:i,aceLangTools:s,aceSearchbox:d,marked:E?l:null,katexCss:E?c:null,katex:E?p:null,markedKatex:E?f:null,mermaid:E?m:null,xtermCss:u,xterm:_,xtermFit:h,xtermUni:y}:{p5:k+"p5.min.js",pyodide:k+"pyodide/pyodide.js",pyodideIndex:k+"pyodide/",ace:k+"ace.min.js",acePython:k+"mode-python.min.js",aceMonokai:k+"theme-monokai.min.js",aceLangTools:k+"ext-language_tools.min.js",aceSearchbox:k+"ext-searchbox.min.js",marked:E?k+"marked.min.js":null,katexCss:E?k+"katex.min.css":null,katex:E?k+"katex.min.js":null,markedKatex:E?k+"marked-katex-extension.js":null,mermaid:E?k+"mermaid.min.js":null,xtermCss:k+"xterm.min.css",xterm:k+"xterm.min.js",xtermFit:k+"xterm-addon-fit.min.js",xtermUni:k+"addon-unicode11.min.js"},S="pyfrilet:"+location.pathname,j=w.map((e,n)=>{const t="text/markdown"===e.getAttribute("type")?"markdown":"python",r=e.hasAttribute("data-hidden"),a=e.hasAttribute("data-readonly");let o=e.getAttribute("data-tab");null!==o||r||(o=1===w.length?"Code":`Bloc ${n+1}`);const i=e.textContent.replace(/^\n/,"");return{id:"tab-"+n,label:o,hidden:r,readonly:a,type:t,starterCode:i,code:i}}),L=e=>{try{return localStorage.getItem(e)}catch(e){return null}};let I;const z=L(S);let R=null;if(z)try{R=JSON.parse(z)}catch(e){R=null}if(R&&1===R.v&&Array.isArray(R.tabs)&&R.tabs.length>0){const e=e=>`${e.label}|${e.type}|${e.hidden?1:0}|${e.readonly?1:0}`;R.tabs.map(e).join(",")!==j.map(e).join(",")&&(R._stale=!0)}const T=!(!R||!R._stale);I=R&&1===R.v&&Array.isArray(R.tabs)&&R.tabs.length>0?R.tabs.map((e,n)=>{const t=j.find(n=>n.label===e.label&&n.type===e.type)||null;return{id:"tab-"+n,label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,starterCode:t?t.starterCode:e.content,code:e.content}}):j.map((e,n)=>{if(!e.hidden&&!e.readonly&&"python"===e.type){const t=e.label?e.label.replace(/[^a-zA-Z0-9]/g,"_"):String(n);let r=L(S+":"+t);if(r||"Code"!==e.label||1!==j.length||(r=L(S)),r&&r.trim())return{...e,code:r}}return e});const P=x.hasAttribute("data-no-watchdog");!function(e,t,r,a,o,i){e=e.slice();let s=i;const d=document.createElement("style");d.textContent=b,document.head.appendChild(d),document.body.innerHTML=g;const l=document.getElementById("pf-app"),c=document.getElementById("pf-drawer"),p=document.getElementById("pf-handle"),f=document.getElementById("pf-sketch"),m=document.getElementById("pf-viewport"),u=document.getElementById("pf-loader"),_=document.getElementById("pf-loader-msg"),h=document.getElementById("pf-err"),y=document.getElementById("pf-btn-run"),w=document.getElementById("pf-btn-code"),x=document.getElementById("pf-btn-dl"),v=document.getElementById("pf-btn-rec"),k=document.getElementById("pf-btn-reset"),E=document.getElementById("pf-btn-help"),C=document.getElementById("pf-grip"),S=document.getElementById("pf-handle-hint"),j=document.getElementById("pf-tabs"),L=document.getElementById("pf-markdown-view");let I=!1,z=Math.round(.56*window.innerHeight);function R(){document.documentElement.style.setProperty("--pf-drawer-h",z+"px")}function T(){I=!0,c.classList.add("pf-open"),w.classList.add("pf-active"),setTimeout(()=>{J(),X&&X.focus()},280)}function P(){I=!1,c.classList.remove("pf-open"),w.classList.remove("pf-active"),setTimeout(()=>{J();const e=G._p?.canvas;e&&e.removeAttribute("tabindex"),l.focus()},280)}function A(){I?P():T()}R();let M=null;const B=5,O=120,W=document.createElement("div");function F(e){if(e.target.closest(".pf-btn"))return;if(e.target.closest("#pf-grip"))return;const n=e.touches?e.touches[0].clientY:e.clientY;M={y:n,h:I?z:0,moved:!1},W.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function D(e){if(!M)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=M.y-n;if(Math.abs(t)>B&&(M.moved=!0),!M.moved)return;const r=Math.max(0,Math.min(window.innerHeight-50,M.h+t));r<O?(c.style.transition="none",c.style.height="32px"):(z=r,R(),I||T(),c.style.transition="none",c.style.height=z+"px"),J()}function U(e){if(!M)return;const n=M.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??M.y,r=M.y-t,a=M.h+r;M=null,W.style.display="none",document.body.style.userSelect="",c.style.transition="",c.style.height="",n&&(a<O?P():(z=Math.max(O,Math.min(window.innerHeight-50,a)),R(),I||T()),J())}Object.assign(W.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(W),C.addEventListener("click",e=>{e.stopPropagation(),A()}),p.addEventListener("mousedown",F,!0),document.addEventListener("mousemove",D),document.addEventListener("mouseup",U),p.addEventListener("touchstart",F,{passive:!1}),document.addEventListener("touchmove",D,{passive:!0}),document.addEventListener("touchend",U);let N=0,K=0;function $(e){h.textContent=e,h.style.display="block",T()}function H(){h.textContent="",h.style.display="none"}function Y(){if(!G._p||"fit"!==G._mode)return;const e=G._w,n=G._h;if(!e||!n)return;const t=l.clientWidth,r=l.clientHeight,a=Math.min(t/e,r/n);m.style.transform=`scale(${a})`}function J(){if("fullscreen"===G._mode?G.size("max"):Y(),q&&"function"==typeof q.windowResized)try{q.windowResized()}catch(e){$(String(e))}X&&X.resize()}window.addEventListener("mousemove",e=>{N=e.clientX,K=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(N=e.touches[0].clientX,K=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=G._p?G._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=G._w/n.width,r=G._h/n.height;return[(N-n.left)*t,(K-n.top)*r]},window.addEventListener("resize",J);let q=null;const G=new Proxy({_p:null,_mode:"fit",_w:0,_h:0,_setP(e){this._p=e},size(e,n,t){if(!this._p)return;const r=t??void 0;"max"===e||null==e?(this._mode="fullscreen",this._w=l.clientWidth,this._h=l.clientHeight,void 0===r&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,r),m.style.transform="scale(1)"):(this._mode="fit",this._w=Math.max(1,0|e),this._h=Math.max(1,0|n),void 0===r&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,r),Y())},noSmooth(){this._p?.noSmooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="pixelated")},smooth(){this._p?.smooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="auto")},sketchTitle(e){S.textContent=String(e)},getItem(e){try{return localStorage.getItem(e)}catch(e){return null}},storeItem(e,n){try{localStorage.setItem(e,String(n))}catch(e){}},removeItem(e){try{localStorage.removeItem(e)}catch(e){}},clearStorage(){try{localStorage.clear()}catch(e){}}},{get(e,n){if(n in e)return"function"==typeof e[n]?e[n].bind(e):e[n];if(e._p&&n in e._p){const t=e._p[n];return"function"==typeof t?t.bind(e._p):t}},set:(e,n,t)=>n.startsWith("_")?(e[n]=t,!0):(e._p&&(e._p[n]=t),!0)});function V(){if(Fe(),q){try{q.remove()}catch(e){}q=null}f.innerHTML="",G._p=null,G._mode="fit",G._w=0,G._h=0,m.style.transform="scale(1)",S.textContent="Shift+Entrée → relancer  ·  Échap → ouvrir/fermer",be&&(be.destroy(),be=null),he&&(he.destroy(),he=null),ye&&(ye.destroy(),ye=null),ge&&(ge.destroy(),ge=null),we&&(we.destroy(),we=null),xe&&(xe.destroy(),xe=null),ve&&(ve.destroy(),ve=null),ke&&(ke.destroy(),ke=null),Ee&&(Ee.destroy(),Ee=null),Ce&&(Ce.destroy(),Ce=null),Se&&(Se.destroy(),Se=null),je&&(je.destroy(),je=null),Le&&(Le.destroy(),Le=null),Ie&&(Ie.destroy(),Ie=null)}window.p5py=G;let X=null,Z=null;const Q={},ee=new Set;function ne(){j.innerHTML="",Z=null;const n=e.filter(e=>!e.hidden);j.style.display=n.length<=1?"none":"",n.forEach(e=>{const n=document.createElement("button");n.className="pf-tab",n.dataset.tabId=e.id,n.textContent=e.label,e.readonly&&n.classList.add("pf-tab-readonly"),"markdown"===e.type&&n.classList.add("pf-tab-markdown"),n.addEventListener("click",()=>te(e)),j.appendChild(n)}),n.length>0&&te(n[0],!0)}function te(e,n){if(n||Z!==e)if(Z=e,j.querySelectorAll(".pf-tab").forEach(n=>{n.classList.toggle("pf-tab-active",n.dataset.tabId===e.id)}),"markdown"===e.type){if(document.getElementById("pf-ace").style.display="none",L.style.display="block",window.marked){let n=marked.parse(e.starterCode);window.mermaid&&(n=n.replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,(e,n)=>`<div class="mermaid">${n.replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">")}</div>`)),L.innerHTML=`<div class="pf-md-inner">${n}</div>`}else L.innerHTML=`<div class="pf-md-inner"><pre>${e.starterCode}</pre></div>`;window.mermaid&&mermaid.run({nodes:L.querySelectorAll(".mermaid")})}else document.getElementById("pf-ace").style.display="block",L.style.display="none",X&&Q[e.id]&&(X.setSession(Q[e.id]),X.setReadOnly(e.readonly),X.focus())}function re(){let n=1;e.filter(e=>"python"===e.type).forEach(e=>{e.hidden||e.readonly||!Q[e.id]?n+=e.code.split("\n").length:(Q[e.id].setOption("firstLineNumber",n),n+=Q[e.id].getLength())})}function ae(){Object.keys(Q).forEach(e=>delete Q[e]),e.filter(e=>!e.hidden&&"python"===e.type).forEach(e=>{const n=ace.createEditSession(e.code,"ace/mode/python");if(n.setUseWorker(!1),n.setTabSize(4),Q[e.id]=n,!e.readonly){let e=null;n.on("change",()=>{null!==e&&(clearTimeout(e),ee.delete(e)),e=setTimeout(()=>{ee.delete(e),e=null,ie()},350),ee.add(e),re(),de()})}});const n=e.find(e=>!e.hidden&&"python"===e.type);X&&n&&Q[n.id]&&(X.setSession(Q[n.id]),X.setReadOnly(n.readonly),X.renderer.updateFull(!0)),re()}function oe(){!a.ace.startsWith("vendor")&&a.ace.startsWith("http")||ace.config.set("basePath",a.ace.replace(/\/[^/]+$/,"/")),X=ace.edit("pf-ace"),X.setTheme("ace/theme/monokai"),X.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),X.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{X.completer?.popup?.isOpen||Pe()}}),X.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:P}),X.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:se}),X.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&le()}}),ae(),ne(),de()}function ie(){const n={v:1,tabs:e.map(e=>({label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,content:e.hidden||e.readonly||"python"!==e.type||!Q[e.id]?e.code:Q[e.id].getValue()}))};try{localStorage.setItem(r,JSON.stringify(n))}catch(e){}}function se(){ie()}function de(){const n=s||e.some(e=>!e.hidden&&!e.readonly&&"python"===e.type&&Q[e.id]&&Q[e.id].getValue()!==e.starterCode);k.classList.toggle("pf-dirty",n)}function le(){ee.forEach(e=>clearTimeout(e)),ee.clear();try{localStorage.removeItem(r)}catch(e){}e.forEach(e=>{if(e.label)try{localStorage.removeItem(r+":"+e.label.replace(/[^a-zA-Z0-9]/g,"_"))}catch(e){}});try{localStorage.removeItem(r+":Code")}catch(e){}s=!1,e=t.map((e,n)=>({...e,id:"tab-"+n,code:e.starterCode})),ae(),ne(),de(),Pe()}window.addEventListener("beforeunload",se);let ce=null,pe=null;async function fe(){return pe||(pe=(async()=>{const e={};a.pyodideIndex&&(e.indexURL=a.pyodideIndex),ce=await loadPyodide(e),await ce.loadPackage(["rich","pygments"]);try{const e=new Uint8Array(new SharedArrayBuffer(1));ce.setInterruptBuffer(e),window._pfInterrupt=()=>{e[0]=2,setTimeout(()=>{e[0]=0},50)}}catch(e){window._pfInterrupt=null}if(window._pfMountIdbfs=e=>new Promise((n,t)=>{try{ce.FS.mkdirTree(e);try{ce.FS.mount(ce.FS.filesystems.IDBFS,{},e)}catch(e){if(10!==e.errno)return void t(e)}ce.FS.syncfs(!0,e=>e?t(e):n())}catch(e){t(e)}}),window._pfSyncIdbfs=()=>new Promise((e,n)=>{ce.FS.syncfs(!1,t=>t?n(t):e())}),ce.runPython("\nimport sys, types, js\nfrom js import p5py, _pfMouse\nfrom pyodide.ffi import JsProxy\n\n# ── Python builtins that must NOT be shadowed ──────────────────────\n_BLACKLIST = frozenset({\n 'abs','all','any','bin','bool','bytes','callable','chr','compile',\n 'delattr','dict','dir','divmod','enumerate','eval','exec',\n 'filter','float','format','frozenset','getattr','globals','hasattr',\n 'hash','help','hex','id','input','int','isinstance','issubclass',\n 'iter','len','list','locals','map','max','min','next','object',\n 'oct','open','ord','pow','print','property','range','repr',\n 'reversed','round','set','setattr','slice','sorted','staticmethod',\n 'str','sum','super','tuple','type','vars','zip',\n # p5 lifecycle hooks — user defines these, we don't import them\n 'setup','draw','preload',\n})\n\n# ── Introspect a hidden dummy p5 instance ─────────────────────────\n_dummy_node = js.document.createElement('div')\n_dummy = js.p5.new(lambda _: None, _dummy_node)\n\n_p5_functions = set() # names of callable JS members\n_p5_attributes = set() # names of scalar/readable members\n\nfor _n in dir(_dummy):\n if _n.startswith('_') or _n in _BLACKLIST:\n continue\n _v = getattr(_dummy, _n)\n if isinstance(_v, JsProxy):\n if callable(_v):\n _p5_functions.add(_n)\n # non-callable JsProxy (canvas, pixels…) → skip\n else:\n _p5_attributes.add(_n)\n\n# Read real initial values now, while dummy is still alive\n_attr_init = {}\nfor _n in _p5_attributes:\n try:\n _attr_init[_n] = getattr(_dummy, _n)\n except Exception:\n _attr_init[_n] = 0\n\n_dummy.remove()\ndel _dummy, _dummy_node\n\n# ── Build module ───────────────────────────────────────────────────\nm = types.ModuleType(\"p5\")\n\n# Generic function wrapper: delegates to live p5Bridge instance\nclass _FW:\n __slots__ = ('_n',)\n def __init__(self, n): self._n = n\n def __call__(self, *a): return getattr(p5py, self._n)(*a)\n def __repr__(self): return f'<p5 function {self._n}>'\n\nfor _n in _p5_functions:\n setattr(m, _n, _FW(_n))\n\n# ── Special overrides (our bridge has custom behaviour) ────────────\n# smooth/noSmooth exist on a real p5 instance so introspection finds\n# them — but our Proxy overrides them to also toggle CSS image-rendering.\n# size and sketchTitle are pyfrilet-only: NOT on a real p5 instance,\n# so introspection misses them — add them explicitly.\nfor _n in ('sketchTitle',):\n setattr(m, _n, _FW(_n))\n _p5_functions.add(_n) # keep __all__ consistent\n\n# size() calls _pf_refresh after resizing so width/height are immediately\n# correct in setup() — consistent with p5.js JS behaviour.\nclass _SizeWrapper:\n def __call__(self, *a):\n p5py.size(*a)\n _pf_refresh(_ns_ref[0])\n return _GetCanvasWrapper()()\n def __repr__(self): return '<p5 function size>'\nsetattr(m, 'size', _SizeWrapper())\nsetattr(m, 'createCanvas', m.size) # alias — createCanvas(...) == size(...)\n_p5_functions.add('size')\n_p5_functions.add('createCanvas')\n_ns_ref = [{}] # filled in by runCode before each exec\n\n# getCanvas() — returns the p5.Element wrapping the canvas,\n# so the user can call .drop(create_proxy(fn)), .mouseOver(), etc. directly like in JS.\nclass _GetCanvasWrapper:\n def __call__(self):\n p = p5py._p\n if p is None:\n raise RuntimeError('getCanvas() doit être appelé dans setup() ou après')\n p.canvas.id = '__pf_canvas__'\n return p.select('#__pf_canvas__')\n def __repr__(self): return '<p5 function getCanvas>'\nsetattr(m, 'getCanvas', _GetCanvasWrapper())\n_p5_functions.add('getCanvas')\n\n# mouseX / mouseY: override with our accurate coordinate calculator\n# (p5's own values are wrong when a CSS-transformed parent is used)\n_MOUSE_OVERRIDE = frozenset({'mouseX', 'mouseY'})\n\n# Initial values from the dummy instance — constants like WEBGL, DEGREES,\n# LEFT_ARROW… are correct from the very first setup() call.\nfor _n in _p5_attributes:\n if _n in _MOUSE_OVERRIDE:\n setattr(m, _n, 0.0)\n else:\n setattr(m, _n, _attr_init.get(_n, 0))\n\n# Build __all__ for import * — done later, after snake_case aliases are added\n\n# ── _pf_refresh: called before every event callback ───────────────\nimport re as _re\n\n# Pre-compute snake_case alias for each attribute — None if identical\n_attr_snake = {\n _k: (_re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _k) or None)\n for _k in _p5_attributes\n}\n_attr_snake = {_k: (_s if _s != _k else None) for _k, _s in _attr_snake.items()}\n\n# Add snake_case names to _p5_attributes so __all__ and _pf_refresh cover them\nfor _k, _sk in list(_attr_snake.items()):\n if _sk:\n _p5_attributes.add(_sk)\n setattr(m, _sk, getattr(m, _k, 0)) # initial value mirrors camelCase\n _attr_snake[_sk] = None # snake name has no further alias\n\ndef _pf_refresh(ns):\n # accurate mouse coords (bypasses p5's stale CSS-transform offset)\n mx, my = _pfMouse()\n\n # update all known scalar attributes from live instance\n for _k in _p5_attributes:\n _sk = _attr_snake.get(_k)\n if _k in _MOUSE_OVERRIDE:\n _v = mx if _k in ('mouseX', 'mouse_x') else my\n elif _sk is None and _k not in _attr_snake:\n # pure snake_case entry — skip, updated via its camelCase counterpart\n continue\n else:\n try:\n _v = getattr(p5py, _k)\n except Exception:\n continue\n setattr(m, _k, _v)\n if _k in ns:\n ns[_k] = _v\n if _sk:\n setattr(m, _sk, _v)\n if _sk in ns:\n ns[_sk] = _v\n\nsys.modules[\"p5\"] = m\n\n# ── draw() watchdog via sys.settrace ──────────────────────────────\n# Trace is called on every Python line event. We only call time.monotonic()\n# every N events to minimize overhead — a tight loop still triggers within\n# a few microseconds, so detection latency is negligible.\nimport time as _time\n\n_WDOG_CHECK_EVERY = 100\n_wdog_deadline = [0.0]\n_wdog_count = [0]\n\ndef _wdog_trace(frame, event, arg):\n _wdog_count[0] += 1\n if _wdog_count[0] >= _WDOG_CHECK_EVERY:\n _wdog_count[0] = 0\n if _time.monotonic() > _wdog_deadline[0]:\n raise TimeoutError(\"draw() watchdog\")\n return _wdog_trace\n\nclass _PfHandledError(Exception):\n \"\"\"Levée après que rich a déjà affiché le traceback vers xterm.\"\"\"\n pass\n\ndef _pf_safe_call(fn):\n try:\n fn()\n except (_PfHandledError, TimeoutError):\n raise\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and not _tb.tb_frame.f_code.co_filename.startswith(('sketch_', 'programme_')):\n _tb = _tb.tb_next\n if _tb: _e.__traceback__ = _tb\n _pf_rich_console.print_exception(extra_lines=8, show_locals=True)\n from js import _pfShowErrorTerminal\n _pfShowErrorTerminal()\n\ndef _pf_safe_proxy(fn):\n from pyodide.ffi import create_proxy as _cp\n def _wrapped(*args, **kwargs):\n _pf_safe_call(lambda: fn(*args, **kwargs))\n return _cp(_wrapped)\n\nsetattr(m, 'safe_proxy', _pf_safe_proxy)\n_p5_functions.add('safe_proxy')\n\ndef _pf_persist():\n \"\"\"Synchronise /persist vers IndexedDB (fire-and-forget).\n Fonctionne en mode p5 (synchrone) et en mode terminal.\"\"\"\n from js import _pfSyncIdbfs\n _pfSyncIdbfs() # Promise — le navigateur l'exécute dès que la stack JS se libère\n\nsetattr(m, 'persist', _pf_persist)\n_p5_functions.add('persist')\npersist = _pf_persist # accessible aussi hors p5 (mode terminal sans import p5)\n\nimport linecache as _linecache\n_pf_run_counter = [0]\n\ndef _pf_exec_user_code():\n _ns = {}\n _pf_run_counter[0] += 1\n _pf_fname = f'sketch_{_pf_run_counter[0]}'\n with open(_pf_fname, 'w') as _f:\n _f.write(_USER_CODE)\n lines = _USER_CODE.splitlines(keepends=True)\n _linecache.cache[_pf_fname] = (len(_USER_CODE), None, lines, _pf_fname)\n try:\n exec(compile(_USER_CODE, _pf_fname, 'exec'), _ns, _ns)\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and _tb.tb_frame.f_code.co_filename != _pf_fname:\n _tb = _tb.tb_next\n if _tb: _e.__traceback__ = _tb\n _pf_rich_console.print_exception(extra_lines=8, show_locals=True)\n from js import _pfShowErrorTerminal\n _pfShowErrorTerminal()\n return None\n _ns_ref[0] = _ns\n return _ns\n\ndef _pf_draw_watchdog(fn, timeout_ms):\n _wdog_count[0] = 0\n _wdog_deadline[0] = _time.monotonic() + timeout_ms * 0.001\n sys.settrace(_wdog_trace)\n try:\n _pf_safe_call(fn)\n except TimeoutError:\n from js import _pfShowWatchdogError\n _pfShowWatchdogError(timeout_ms)\n finally:\n sys.settrace(None)\n\ndef _pf_draw_direct(fn, timeout_ms):\n _pf_safe_call(fn)\n\ndef _snake_to_camel(name):\n parts = name.split('_')\n return parts[0] + ''.join(p.capitalize() for p in parts[1:])\n\n# Pre-populate snake_case aliases so \"from p5 import no_fill\" works\nfor _camel in list(vars(m).keys()):\n _snake = _re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _camel)\n if _snake != _camel and not hasattr(m, _snake):\n setattr(m, _snake, getattr(m, _camel))\n if _camel in _p5_functions:\n _p5_functions.add(_snake)\n\n# Rebuild __all__ now that snake_case aliases are included\nm.__all__ = sorted(_p5_functions | _p5_attributes)\n\ndef _p5_getattr(name):\n camel = _snake_to_camel(name)\n if camel != name:\n val = getattr(m, camel, None)\n if val is not None:\n return val\n raise AttributeError(f\"module 'p5' has no attribute '{name}'\")\n\nm.__getattr__ = _p5_getattr\n"),ce.runPython("\nimport asyncio as _asyncio, ast as _ast\nimport os as _os, sys as _sys\n_os.environ.setdefault('TERM', 'xterm-256color')\n_os.environ.setdefault('COLORTERM', 'truecolor')\n\n# Wrapper file-like qui écrit directement vers xterm via JS.\n# Rich écrit des strings sur sys.stdout.write() — il faut un vrai objet fichier.\nclass _PfStream:\n def __init__(self, js_fn):\n self._fn = js_fn\n self.encoding = 'utf-8'\n self.errors = 'replace'\n def write(self, s):\n if s:\n self._fn(s)\n return len(s)\n def writelines(self, lines):\n for l in lines: self.write(l)\n def flush(self): pass\n def isatty(self): return True\n @property\n def softspace(self): return 0\n\nfrom js import _pfTermWrite, _pfTermWriteErr\n_sys.stdout = _PfStream(_pfTermWrite)\n_sys.stderr = _PfStream(_pfTermWriteErr)\n\nfrom rich.console import Console as _RichConsole\n_pf_rich_console = _RichConsole(stderr=True)\n\nasync def _pf_async_input(prompt=\"\"):\n from js import _pfTerminalInput\n result = await _pfTerminalInput(str(prompt) if prompt else \"\")\n return result\n\nasync def _pf_run_terminal(source):\n class _InputAwaiter(_ast.NodeTransformer):\n def visit_Call(self, node):\n self.generic_visit(node)\n if isinstance(node.func, _ast.Name) and node.func.id == 'input':\n return _ast.Await(value=node)\n return node\n\n tree = _ast.parse(source)\n tree = _InputAwaiter().visit(tree)\n\n wrapper = _ast.parse(\"async def programme(): pass\")\n wrapper.body[0].body = tree.body if tree.body else [_ast.Pass()]\n _ast.fix_missing_locations(wrapper)\n _pf_run_counter[0] += 1\n _pf_fname = f'programme_{_pf_run_counter[0]}'\n with open(_pf_fname, 'w') as _f:\n _f.write(source)\n lines = source.splitlines(keepends=True)\n _linecache.cache[_pf_fname] = (len(source), None, lines, _pf_fname)\n _ns = {'input': _pf_async_input, 'persist': _pf_persist}\n exec(compile(wrapper, _pf_fname, 'exec'), _ns)\n try:\n await _ns['programme']()\n except (SystemExit, KeyboardInterrupt):\n pass\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and _tb.tb_frame.f_code.co_filename != _pf_fname:\n _tb = _tb.tb_next\n if _tb:\n _e.__traceback__ = _tb\n _pf_rich_console.print_exception(extra_lines=8, show_locals=True)\n"),X){me(ce.runPython("list(m.__all__)").toJs())}})(),pe)}function me(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,r,a,o){o(null,a.length>0?n:[])}},r=ace.require("ace/ext/language_tools");r&&Array.isArray(r.completers)&&(r.completers=r.completers.filter(e=>!0!==e._pyfrilet)),t._pyfrilet=!0,X.completers=[...X.completers||[],t]}let ue=!1,_e=!1,he=null,ye=null,be=null,ge=null,we=null,xe=null,ve=null,ke=null,Ee=null,Ce=null,Se=null,je=null,Le=null,Ie=null;const ze=300;function Re(e){return!/\bfrom\s+p5\s+import\b|\bimport\s+p5\b/.test(e)}async function Te(e){_e=!0,tn(),nn(),H(),window._pfSetP5Mode&&window._pfSetP5Mode(!1),await window._pfMountIdbfs("/persist");try{const n=ce.globals.get("_pf_run_terminal");await n(e)}catch(e){const n=String(e);n.includes("SystemExit")||en(n+"\n")}finally{_e=!1}}async function Pe(){if(ue){if(!_e)return;window._pfInterrupt&&window._pfInterrupt(),an(),ue=!1,y.classList.remove("pf-running"),await new Promise(e=>setTimeout(e,80))}ue=!0,y.classList.add("pf-running"),H(),nn(),V(),ce||(_.textContent="Initialisation de Pyodide…",u.style.display="flex");try{await fe()}catch(e){return u.style.display="none",$("Erreur Pyodide : "+(e.message||String(e))),ue=!1,void y.classList.remove("pf-running")}u.style.display="none";const t=e.filter(e=>"python"===e.type).map(e=>e.hidden||e.readonly||!Q[e.id]?e.code:Q[e.id].getValue()).join("\n");try{_.textContent="Chargement des dépendances…",u.style.display="flex",await ce.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:n})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}if(u.style.display="none",Re(t))return y.classList.remove("pf-running"),await Te(t),void(ue=!1);an(),window._pfSetP5Mode&&window._pfSetP5Mode(!0),await window._pfMountIdbfs("/persist"),ce.globals.set("_USER_CODE",t);const r=ce.globals.get("_pf_exec_user_code");try{if(!r())return ue=!1,void y.classList.remove("pf-running");ce.runPython("_ns = _ns_ref[0]")}catch(e){return rn(e.message||String(e)),ue=!1,void y.classList.remove("pf-running")}let a,i,s,d,l,c,p,m,h,b,g,w,x,v;try{const e=(e,n)=>ce.runPython(`_ns.get('${e}') or _ns.get('${n}')`);l=e("preload","preload"),a=e("setup","setup"),i=e("draw","draw"),s=e("mousePressed","mouse_pressed"),d=e("keyPressed","key_pressed"),c=e("mouseDragged","mouse_dragged"),p=e("mouseReleased","mouse_released"),m=e("mouseMoved","mouse_moved"),h=e("mouseWheel","mouse_wheel"),b=e("doubleClicked","double_clicked"),g=e("keyReleased","key_released"),w=e("touchStarted","touch_started"),x=e("touchMoved","touch_moved"),v=e("touchEnded","touch_ended")}catch(e){return rn(e.message||String(e)),ue=!1,void y.classList.remove("pf-running")}if(!i)return $("Le script doit définir au moins une fonction draw()."),ue=!1,void y.classList.remove("pf-running");const{create_proxy:k}=ce.pyimport("pyodide.ffi"),E=ce.runPython("_ns.get('windowResized')"),C=ce.globals.get("_pf_refresh"),S=ce.globals.get(o?"_pf_draw_direct":"_pf_draw_watchdog"),j=ce.globals.get("_ns"),L=ce.globals.get("_pf_safe_call"),I=e=>e?k(()=>{try{C(j),L(e)}catch(e){rn("")}}):null;be=l?k(()=>{try{L(l)}catch(e){rn("")}}):null,he=a?k(()=>{try{L(a)}catch(e){rn("")}}):null,ye=k(()=>{try{C(j),S(i,ze)}catch(e){V(),rn("")}}),ge=I(s),we=I(p),xe=I(c),ve=I(m),ke=I(h),Ee=I(b),Ce=I(d),Se=I(g),je=I(w),Le=I(x),Ie=I(v);const z=E?k(()=>{try{L(E)}catch(e){rn("")}}):null;let R=!1;q=new p5(e=>{G._setP(e),be&&(e.preload=()=>{be()}),e.setup=()=>{he&&he(),e.canvas||G.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),R=!0},e.draw=()=>{R&&ye()},e.mousePressed=()=>{R&&ge&&ge()},e.mouseReleased=()=>{R&&we&&we()},e.mouseDragged=()=>{R&&xe&&xe()},e.mouseMoved=()=>{R&&ve&&ve()},e.mouseWheel=e=>{R&&ke&&ke()},e.doubleClicked=()=>{R&&Ee&&Ee()},e.keyPressed=()=>{R&&Ce&&Ce()},e.keyReleased=()=>{R&&Se&&Se()},je&&(e.touchStarted=()=>{R&&je()}),Le&&(e.touchMoved=()=>{R&&Le()}),Ie&&(e.touchEnded=()=>{R&&Ie()}),e.windowResized=()=>{"fullscreen"===G._mode?G.size("max"):Y(),z&&z()}},f),ue=!1,y.classList.remove("pf-running")}const Ae='<!doctype html>\n<html lang="fr">\n<head>\n <meta charset="utf-8">\n <meta name="viewport" content="width=device-width, initial-scale=1">\n <title>export</title>\n <script src="https://cdn.jsdelivr.net/npm/pyfrilet@0.6.4/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\nFILLME-SCRIPTS\n\n</body>\n</html>';function Me(){const n=e.map((e,n)=>{let t;t="python"!==e.type||e.hidden||e.readonly||!Q[e.id]?e.code:Q[e.id].getValue();const r=[],a="markdown"===e.type?"text/markdown":"text/python";null!==e.label&&r.push(`data-tab="${e.label.replace(/"/g,"&quot;")}"`),e.hidden&&r.push("data-hidden"),e.readonly&&r.push("data-readonly");return`<script type="${a}"${r.length?" "+r.join(" "):""}>\n${t.replace(/<\/script>/gi,"<\\/script>")}\n<\/script>`}).join("\n\n"),t=Ae.replace("FILLME-SCRIPTS",n),r=new Blob([t],{type:"text/html;charset=utf-8"}),a=URL.createObjectURL(r),o=Object.assign(document.createElement("a"),{href:a,download:"sketch.html"});document.body.appendChild(o),o.click(),document.body.removeChild(o),URL.revokeObjectURL(a)}let Be=null,Oe=[];function We(){const e=G._p?.canvas;if(!e)return;const n=["video/webm;codecs=vp9","video/webm;codecs=vp8","video/webm"].find(e=>MediaRecorder.isTypeSupported(e))||"video/webm",t=e.captureStream();Be=new MediaRecorder(t,{mimeType:n}),Oe=[],Be.ondataavailable=e=>{e.data.size&&Oe.push(e.data)},Be.onstop=()=>{const e=new Blob(Oe,{type:n}),t=URL.createObjectURL(e),r=n.includes("webm")?"webm":"mp4";Object.assign(document.createElement("a"),{href:t,download:`sketch.${r}`}).click(),URL.revokeObjectURL(t),v.textContent="⏺",v.title="Enregistrer WebM",v.classList.remove("pf-recording"),Be=null},Be.start(),v.textContent="⏹",v.title="Arrêter l'enregistrement",v.classList.add("pf-recording")}function Fe(){Be&&"inactive"!==Be.state&&Be.stop()}v.addEventListener("click",()=>{Be?Fe():We()}),y.addEventListener("click",()=>Pe()),w.addEventListener("click",()=>{I?P():(z=window.innerHeight-32,R(),T())}),x.addEventListener("click",Me);const De="https://codeberg.org/nopid/pyfrilet";function Ue(e){return new Promise((n,t)=>{const r=document.createElement("script");r.src=e,r.onload=n,r.onerror=()=>t(new Error("Impossible de charger : "+e)),document.head.appendChild(r)})}E.addEventListener("click",()=>window.open(De,"_blank")),k.addEventListener("click",()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&le()}),window.addEventListener("keydown",e=>{const n=I&&X&&X.isFocused&&X.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void Pe();if("Escape"===e.key){const t=document.querySelector(".ace_search");if(t&&"none"!==t.style.display)return e.preventDefault(),e.stopPropagation(),X.searchBox?X.searchBox.hide():t.style.display="none",void X.focus();if(n){const n=X.completer?.popup?.isOpen;if(n)return;return e.preventDefault(),e.stopPropagation(),void P()}return e.preventDefault(),e.stopPropagation(),void(I?P():T())}if(!n)return"s"!==e.key&&"S"!==e.key||!e.ctrlKey&&!e.metaKey?"r"!==e.key&&"R"!==e.key||!e.ctrlKey&&!e.metaKey||e.altKey?void 0:(e.preventDefault(),void(confirm("Réinitialiser ? Les modifications seront perdues.")&&le())):(e.preventDefault(),void se())}else e.preventDefault()},!0),(async()=>{_.textContent="Chargement des dépendances…",u.style.display="flex";try{if(await Ue(a.p5),a.marked){const e=document.createElement("link");e.rel="stylesheet",e.href=a.katexCss,document.head.appendChild(e),await Ue(a.marked),await Ue(a.katex),await Ue(a.markedKatex),await Ue(a.mermaid),marked.use(markedKatex({throwOnError:!1})),mermaid.initialize({startOnLoad:!1,theme:"neutral"})}await Ue(a.ace),await Ue(a.acePython),await Ue(a.aceMonokai),await Ue(a.aceLangTools),await Ue(a.aceSearchbox),await Ue(a.pyodide);const e=document.createElement("link");e.rel="stylesheet",e.href=a.xtermCss,document.head.appendChild(e),await Ue(a.xterm),await Ue(a.xtermFit),await Ue(a.xtermUni)}catch(e){return _.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}oe(),await Pe(),u.style.display="none"})();const Ne=document.getElementById("pf-xterm");let Ke=null,$e=null,He=null,Ye="";function Je(){if(Ke)return;Ke=new Terminal({theme:{background:"#000000",foreground:"#e8e8e8",cursor:"#ffffff",black:"#2a2a2a",brightBlack:"#555555",red:"#cc4444",brightRed:"#ff6666",green:"#44aa44",brightGreen:"#66cc66",yellow:"#aaaa00",brightYellow:"#dddd44",blue:"#4466cc",brightBlue:"#6688ff",magenta:"#aa44aa",brightMagenta:"#dd66dd",cyan:"#44aaaa",brightCyan:"#66cccc",white:"#cccccc",brightWhite:"#ffffff"},fontFamily:"'Fira Code', 'Consolas', 'Courier New', monospace",fontSize:15,lineHeight:1,letterSpacing:0,cursorBlink:!0,scrollback:2e3,convertEol:!0,allowProposedApi:!0}),$e=new FitAddon.FitAddon,Ke.loadAddon($e);const e=new Unicode11Addon.Unicode11Addon;Ke.loadAddon(e),Ke.unicode.activeVersion="11",Ke.open(Ne),$e.fit(),new ResizeObserver(()=>{Ke&&"none"!==Ne.style.display&&$e.fit()}).observe(Ne),Ke.onData(e=>{if(He)if("\r"===e){const e=Ye;Ye="",Ke.write("\r\n");const n=He;He=null,n(e)}else if(""===e)Ye.length>0&&(Ye=Ye.slice(0,-1),Ke.write("\b \b"));else if(""===e){Ye="",Ke.write("^C\r\n");const e=He;He=null,e(null)}else e.charCodeAt(0)>=32&&(Ye+=e,Ke.write(e))})}window._pfTerminalInput=function(e){return new Promise(n=>{He=n,Ye="",e&&Ke.write(e),Ke.focus()})};let qe=!1;window._pfSetP5Mode=e=>{qe=e};const Ge=document.getElementById("pf-xterm-handle"),Ve=document.getElementById("pf-xterm-chevron");function Xe(){Ne.style.display="block",Ne.classList.remove("pf-xterm-overlay","pf-xterm-collapsed"),Ne.classList.add("pf-xterm-bandeau"),Ve&&(Ve.textContent="∨"),Je(),$e.fit()}function Ze(){Ne.classList.contains("pf-xterm-collapsed")?(Ne.classList.remove("pf-xterm-collapsed"),Ve&&(Ve.textContent="∨"),$e.fit()):(Ne.classList.add("pf-xterm-collapsed"),Ve&&(Ve.textContent="∧"))}Ge&&Ge.addEventListener("click",Ze);function Qe(e){qe&&"none"===Ne.style.display&&Xe(),Ke&&Ke.write(e)}function en(e){Je(),Ke.write(""),Ke.write(e.replace(/\n/g,"\r\n")),Ke.write("")}function nn(){Ke&&Ke.reset(),He=null,Ye="",Ne.classList.remove("pf-xterm-bandeau","pf-xterm-collapsed"),Ve&&(Ve.textContent="∨")}function tn(){m.style.display="none",Ne.style.display="block",Ne.classList.remove("pf-xterm-overlay"),Je(),$e.fit(),Ke.focus()}function rn(e){Ne.style.display="block",Ne.classList.add("pf-xterm-overlay"),Je(),$e.fit(),e&&(nn(),Ke.write("── Erreur ──────────────────────────────────────\r\n\r\n"),Ke.write(e.replace(/\n/g,"\r\n")+"\r\n"))}function an(){if(Ne.style.display="none",Ne.classList.remove("pf-xterm-overlay"),m.style.display="",He){const e=He;He=null,Ye="",e(null)}}window._pfTermWrite=Qe,window._pfTermWriteErr=en,window._pfShowWatchdogError=e=>{V(),$(`draw() a dépassé ${e}ms — sketch arrêté (watchdog).`)},window._pfShowErrorTerminal=()=>{V(),rn("")}}(I,j,S,C,P,T)})}();
1
+ !function(){"use strict";const e=document.currentScript;let n=!1;const t="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js",a="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js",r="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js",o="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js",i="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js",s="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js",d="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js",l="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js",c="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css",p="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js",f="https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js",m="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js",_="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css",u="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js",h="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js",y="https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.8.0/lib/addon-unicode11.min.js",b="html, body {\n height: 100%; margin: 0; overflow: hidden;\n background: #111;\n}\n#pf-root {\n position: fixed; inset: 0;\n display: flex; flex-direction: column;\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n}\n\n/* ── app area ── */\n#pf-app:focus { outline: none; }\n#pf-app {\n flex: 1; min-height: 0;\n position: relative;\n background: #111;\n display: flex; align-items: center; justify-content: center;\n overflow: hidden;\n}\n#pf-viewport {\n transform-origin: 50% 50%;\n will-change: transform;\n}\n#pf-viewport canvas {\n display: block;\n outline: none;\n}\n#pf-loader {\n position: absolute; inset: 0;\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n gap: 14px;\n background: #111;\n color: #565f89;\n font-size: 13px;\n z-index: 50;\n pointer-events: none;\n}\n#pf-loader-bar {\n width: 160px; height: 2px;\n background: #2a2c3e;\n border-radius: 2px;\n overflow: hidden;\n}\n#pf-loader-bar::after {\n content: '';\n display: block;\n height: 100%;\n width: 40%;\n background: #7aa2f7;\n border-radius: 2px;\n animation: pf-slide 1.2s ease-in-out infinite;\n}\n@keyframes pf-slide {\n 0% { transform: translateX(-100%); }\n 100% { transform: translateX(350%); }\n}\n\n/* ── drawer (slide-up editor panel) ── */\n#pf-drawer {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n background: #1a1b26;\n height: 32px; /* collapsed = handle only */\n transition: height 0.26s cubic-bezier(.4, 0, .2, 1);\n overflow: hidden;\n /* shadow cast upward onto the app */\n box-shadow: 0 -4px 20px rgba(0,0,0,.55);\n}\n#pf-drawer.pf-open {\n height: var(--pf-drawer-h, 56vh);\n}\n\n/* ── handle bar ── */\n#pf-handle {\n height: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n padding: 0 8px 0 6px;\n background: #24283b;\n border-top: 1px solid #3d4166;\n cursor: ns-resize;\n user-select: none;\n gap: 6px;\n flex-shrink: 0;\n}\n/* grip zone: clickable to toggle, draggable to resize */\n#pf-grip {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 5px 6px;\n flex-shrink: 0;\n opacity: .5;\n border-radius: 4px;\n transition: opacity .15s, background .15s;\n cursor: pointer;\n}\n#pf-grip:hover { opacity: .85; background: rgba(255,255,255,.06); }\n#pf-grip span {\n display: block;\n width: 16px; height: 2px;\n background: #a9b1d6;\n border-radius: 1px;\n}\n#pf-handle-hint {\n flex: 1;\n color: #565f89;\n font-size: 10px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n#pf-handle-btns {\n display: flex;\n gap: 4px;\n flex-shrink: 0;\n}\n.pf-btn {\n height: 26px;\n min-width: 26px;\n padding: 0 5px;\n border: 0; border-radius: 5px;\n cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n font-size: 13px; line-height: 1;\n white-space: nowrap;\n transition: background .15s, transform .1s, opacity .15s;\n outline: none;\n box-sizing: border-box;\n}\n.pf-btn:active { transform: scale(.88); }\n.pf-btn:focus-visible { outline: 2px solid #7aa2f7; outline-offset: 1px; }\n\n#pf-btn-run { background: #1a6b3a; color: #9ece6a; font-size: 11px; }\n#pf-btn-run:hover { background: #1f8447; color: #b9f27a; }\n#pf-btn-run.pf-running { opacity: .5; cursor: not-allowed; }\n\n#pf-btn-code { background: #2a2c3e; color: #7aa2f7; font-size: 14px; }\n#pf-btn-code:hover { background: #3d4166; color: #c0caf5; }\n#pf-btn-code.pf-active { background: #3d4166; color: #e0af68; }\n\n#pf-btn-dl { background: #2a2c3e; color: #9d7cd8; font-size: 14px; }\n#pf-btn-dl:hover { background: #3d4166; color: #bb9af7; }\n\n#pf-btn-rec { background: #2a2c3e; color: #f7768e; font-size: 13px; }\n#pf-btn-rec:hover { background: #3d4166; color: #ff9e9e; }\n#pf-btn-rec.pf-recording { background: #6b1a1a; color: #f7768e; animation: pf-blink .8s step-end infinite; }\n@keyframes pf-blink { 50% { opacity: .4; } }\n\n#pf-btn-reset { background: #2a2c3e; color: #e0af68; font-size: 16px; }\n#pf-btn-reset:hover { background: #3d4166; color: #ffc777; }\n#pf-btn-reset.pf-dirty::after {\n content: '●';\n position: absolute;\n top: 2px; right: 3px;\n font-size: 7px;\n color: #e0af68;\n line-height: 1;\n}\n#pf-btn-reset { position: relative; }\n\n/* ── editor area inside drawer ── */\n#pf-editor-wrap {\n flex: 1;\n min-height: 80px;\n position: relative;\n display: flex;\n flex-direction: column;\n}\n#pf-ace { flex: 1; position: relative; min-height: 0; }\n\n/* ── tab bar ── */\n#pf-tabs {\n display: flex;\n flex-shrink: 0;\n background: #1a1b2e;\n border-bottom: 1px solid #414868;\n overflow-x: auto;\n scrollbar-width: none;\n}\n#pf-tabs:empty { display: none; }\n.pf-tab {\n padding: 5px 14px;\n font-size: 12px;\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n color: #737aa2;\n cursor: pointer;\n white-space: nowrap;\n transition: color .15s, border-color .15s;\n}\n.pf-tab:hover { color: #c0caf5; }\n.pf-tab.pf-tab-active { color: #c0caf5; border-bottom-color: #7aa2f7; }\n.pf-tab.pf-tab-readonly::after { content: ' 🔒'; font-size: 10px; opacity: .6; }\n.pf-tab.pf-tab-markdown::after { content: ' ✎'; font-size: 11px; opacity: .6; }\n\n/* ── markdown view ── */\n@import url('https://fonts.googleapis.com/css2?family=Alegreya+Sans:ital,wght@0,400;0,700;1,400&family=Fira+Code:wght@300..700&display=swap');\n\n#pf-markdown-view {\n flex: 1;\n overflow: auto;\n background: #f4f4f0;\n}\n\n#pf-markdown-view .pf-md-inner {\n width: 100%;\n max-width: 680px;\n margin: 0 auto;\n padding: 48px 48px 72px;\n box-sizing: border-box;\n font-family: 'Alegreya Sans', Georgia, serif;\n font-size: 17px;\n line-height: 1.8;\n color: #1c1c2e;\n}\n\n#pf-markdown-view h1 {\n font-size: 2.1em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 0 0 .3em;\n padding-bottom: .3em;\n border-bottom: 2px solid #d8d8e8;\n line-height: 1.2;\n}\n#pf-markdown-view h2 {\n font-size: 1.4em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 2em 0 .5em;\n padding-bottom: .2em;\n border-bottom: 1px solid #e0e0ec;\n}\n#pf-markdown-view h3 {\n font-size: 1.1em;\n font-weight: 700;\n color: #2a2a4a;\n margin: 1.6em 0 .4em;\n}\n\n#pf-markdown-view p { margin: .75em 0; }\n#pf-markdown-view ul,\n#pf-markdown-view ol { padding-left: 1.6em; margin: .75em 0; }\n#pf-markdown-view li { margin: .3em 0; }\n#pf-markdown-view hr { border: none; border-top: 1px solid #dde; margin: 2em 0; }\n#pf-markdown-view blockquote {\n margin: 1em 0;\n padding: .5em 1em;\n border-left: 3px solid #aab;\n color: #555;\n background: #ededf5;\n border-radius: 0 4px 4px 0;\n}\n\n#pf-markdown-view code {\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n font-size: .84em;\n background: #e8e8f2;\n color: #3a3a6a;\n padding: .15em .45em;\n border-radius: 4px;\n}\n#pf-markdown-view pre {\n background: #1a1b2e;\n border-radius: 8px;\n padding: 1em 1.2em;\n overflow: auto;\n margin: 1.2em 0;\n box-shadow: 0 2px 8px rgba(0,0,0,.12);\n}\n#pf-markdown-view pre code {\n background: transparent;\n color: #c0caf5;\n font-size: .86em;\n padding: 0;\n line-height: 1.6;\n border-radius: 0;\n}\n\n#pf-markdown-view table {\n border-collapse: collapse;\n width: 100%;\n margin: 1.2em 0;\n font-size: .95em;\n}\n#pf-markdown-view th {\n background: #e4e4f0;\n color: #1c1c2e;\n font-weight: 700;\n text-align: left;\n padding: .55em .85em;\n border: 1px solid #d0d0e8;\n}\n#pf-markdown-view td {\n padding: .5em .85em;\n border: 1px solid #e0e0ee;\n vertical-align: top;\n}\n#pf-markdown-view tr:nth-child(even) td { background: #f0f0f8; }\n\n#pf-markdown-view a {\n color: #3a5fc8;\n text-decoration: none;\n border-bottom: 1px solid rgba(58,95,200,.3);\n transition: color .15s, border-color .15s;\n}\n#pf-markdown-view a:hover { color: #1a3fa0; border-bottom-color: #1a3fa0; }\n\n#pf-markdown-view .katex-display {\n overflow-x: auto;\n padding: .5em 0;\n margin: 1.2em 0;\n}\n#pf-markdown-view .mermaid {\n text-align: center;\n margin: 1.5em 0;\n background: #ededf5;\n border-radius: 8px;\n padding: 1em;\n}\n\n/* ── error panel (below editor, never overlaps ACE) ── */\n#pf-err {\n flex-shrink: 0;\n max-height: 120px;\n overflow: auto;\n margin: 0; padding: 8px 13px;\n font-size: 11.5px; line-height: 1.45;\n background: rgba(13, 3, 3, .95);\n color: #f7768e;\n white-space: pre-wrap;\n display: none;\n border-top: 1px solid rgba(247, 118, 142, .35);\n}\n/* ── xterm terminal ── */\n#pf-xterm {\n display: none;\n position: absolute;\n inset: 0;\n padding: 10px 12px;\n box-sizing: border-box;\n background: #000000;\n overflow: hidden;\n}\n\n#pf-xterm.pf-xterm-overlay {\n background: rgba(0, 0, 0, 0.82);\n}\n\n/* Bandeau bas — mode p5 print() */\n#pf-xterm.pf-xterm-bandeau {\n inset: auto 0 0 0;\n height: 200px;\n background: rgba(0, 0, 0, 0.85);\n border-top: 1px solid rgba(255,255,255,0.1);\n padding: 0 12px 10px;\n}\n#pf-xterm.pf-xterm-bandeau.pf-xterm-collapsed {\n height: 24px;\n padding: 0;\n overflow: hidden;\n}\n\n/* Poignée du bandeau */\n#pf-xterm-handle {\n display: none;\n height: 24px;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n color: rgba(255,255,255,0.4);\n font-size: 11px;\n letter-spacing: 2px;\n user-select: none;\n gap: 8px;\n}\n#pf-xterm-handle:hover { color: rgba(255,255,255,0.8); }\n.pf-xterm-bandeau #pf-xterm-handle { display: flex; }\n\n/* xterm interne : prendre toute la hauteur disponible sous la poignée */\n#pf-xterm .xterm {\n height: 100%;\n}\n#pf-xterm.pf-xterm-bandeau .xterm {\n height: calc(100% - 24px);\n}\n#pf-xterm .xterm-screen {\n height: 100% !important;\n}\n",g='<div id="pf-root">\n <div id="pf-app" tabindex="-1">\n <div id="pf-viewport"><div id="pf-sketch"></div></div>\n <div id="pf-xterm"><div id="pf-xterm-handle"><span id="pf-xterm-chevron">∧</span><span id="pf-xterm-handle-label">console</span></div></div>\n <div id="pf-loader">\n <span id="pf-loader-msg">Chargement…</span>\n <div id="pf-loader-bar"></div>\n </div>\n </div>\n <div id="pf-drawer">\n <div id="pf-handle">\n <div id="pf-grip" title="Clic → ouvrir/fermer"><span></span><span></span><span></span></div>\n <span id="pf-handle-hint">Clic ☰ → ouvrir/fermer &nbsp;·&nbsp; Shift+Entrée → relancer</span>\n <div id="pf-handle-btns">\n <button class="pf-btn" id="pf-btn-run" title="Relancer (Shift+Entrée)">&#9654;</button>\n <button class="pf-btn" id="pf-btn-code" title="Éditeur plein écran">&#9999;&#xFE0F;</button>\n <button class="pf-btn" id="pf-btn-dl" title="Télécharger HTML autonome">&#128190;</button>\n <button class="pf-btn" id="pf-btn-rec" title="Enregistrer WebM">⏺</button>\n <button class="pf-btn" id="pf-btn-help" title="Aide">?</button>\n <button class="pf-btn" id="pf-btn-reset" title="Réinitialiser le code (Ctrl+R)">&#8635;</button>\n </div>\n </div>\n <div id="pf-editor-wrap">\n <div id="pf-tabs"></div>\n <div id="pf-markdown-view" style="display:none"></div>\n <div id="pf-ace"></div>\n </div>\n <pre id="pf-err"></pre>\n </div>\n</div>';document.addEventListener("DOMContentLoaded",function(){const w=[...document.querySelectorAll('script[type="text/python"], script[type="text/markdown"], python')];if(0===w.length)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const x=e||w[0],v=(x.getAttribute("data-sources")||x.getAttribute("sources")||"cdn").toLowerCase().trim(),k=(x.getAttribute("data-vendor")||x.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/");n="cdn"===v;const E=w.some(e=>"text/markdown"===e.getAttribute("type")),C=n?{p5:t,pyodide:a,pyodideIndex:null,ace:r,acePython:o,aceMonokai:i,aceLangTools:s,aceSearchbox:d,marked:E?l:null,katexCss:E?c:null,katex:E?p:null,markedKatex:E?f:null,mermaid:E?m:null,xtermCss:_,xterm:u,xtermFit:h,xtermUni:y}:{p5:k+"p5.min.js",pyodide:k+"pyodide/pyodide.js",pyodideIndex:k+"pyodide/",ace:k+"ace.min.js",acePython:k+"mode-python.min.js",aceMonokai:k+"theme-monokai.min.js",aceLangTools:k+"ext-language_tools.min.js",aceSearchbox:k+"ext-searchbox.min.js",marked:E?k+"marked.min.js":null,katexCss:E?k+"katex.min.css":null,katex:E?k+"katex.min.js":null,markedKatex:E?k+"marked-katex-extension.js":null,mermaid:E?k+"mermaid.min.js":null,xtermCss:k+"xterm.min.css",xterm:k+"xterm.min.js",xtermFit:k+"xterm-addon-fit.min.js",xtermUni:k+"addon-unicode11.min.js"},S="pyfrilet:"+location.pathname,L=w.map((e,n)=>{const t="text/markdown"===e.getAttribute("type")?"markdown":"python",a=e.hasAttribute("data-hidden"),r=e.hasAttribute("data-readonly");let o=e.getAttribute("data-tab");null!==o||a||(o=1===w.length?"Code":`Bloc ${n+1}`);const i=e.textContent.replace(/^\n/,"");return{id:"tab-"+n,label:o,hidden:a,readonly:r,type:t,starterCode:i,code:i}}),j=e=>{try{return localStorage.getItem(e)}catch(e){return null}};let I;const T=j(S);let z=null;if(T)try{z=JSON.parse(T)}catch(e){z=null}if(z&&1===z.v&&Array.isArray(z.tabs)&&z.tabs.length>0){const e=e=>`${e.label}|${e.type}|${e.hidden?1:0}|${e.readonly?1:0}`;z.tabs.map(e).join(",")!==L.map(e).join(",")&&(z._stale=!0)}const R=!(!z||!z._stale);I=z&&1===z.v&&Array.isArray(z.tabs)&&z.tabs.length>0?z.tabs.map((e,n)=>{const t=L.find(n=>n.label===e.label&&n.type===e.type)||null;return{id:"tab-"+n,label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,starterCode:t?t.starterCode:e.content,code:e.content}}):L.map((e,n)=>{if(!e.hidden&&!e.readonly&&"python"===e.type){const t=e.label?e.label.replace(/[^a-zA-Z0-9]/g,"_"):String(n);let a=j(S+":"+t);if(a||"Code"!==e.label||1!==L.length||(a=j(S)),a&&a.trim())return{...e,code:a}}return e});const P=x.hasAttribute("data-no-watchdog");!function(e,t,a,r,o,i){e=e.slice();let s=i;const d=document.createElement("style");d.textContent=b,document.head.appendChild(d),document.body.innerHTML=g;const l=document.getElementById("pf-app"),c=document.getElementById("pf-drawer"),p=document.getElementById("pf-handle"),f=document.getElementById("pf-sketch"),m=document.getElementById("pf-viewport"),_=document.getElementById("pf-loader"),u=document.getElementById("pf-loader-msg"),h=document.getElementById("pf-err"),y=document.getElementById("pf-btn-run"),w=document.getElementById("pf-btn-code"),x=document.getElementById("pf-btn-dl"),v=document.getElementById("pf-btn-rec"),k=document.getElementById("pf-btn-reset"),E=document.getElementById("pf-btn-help"),C=document.getElementById("pf-grip"),S=document.getElementById("pf-handle-hint"),L=document.getElementById("pf-tabs"),j=document.getElementById("pf-markdown-view");let I=!1,T=Math.round(.56*window.innerHeight);function z(){document.documentElement.style.setProperty("--pf-drawer-h",T+"px")}function R(){I=!0,c.classList.add("pf-open"),w.classList.add("pf-active"),setTimeout(()=>{J(),X&&X.focus()},280)}function P(){I=!1,c.classList.remove("pf-open"),w.classList.remove("pf-active"),setTimeout(()=>{J();const e=G._p?.canvas;e&&e.removeAttribute("tabindex"),l.focus()},280)}function A(){I?P():R()}z();let M=null;const B=5,O=120,F=document.createElement("div");function W(e){if(e.target.closest(".pf-btn"))return;if(e.target.closest("#pf-grip"))return;const n=e.touches?e.touches[0].clientY:e.clientY;M={y:n,h:I?T:0,moved:!1},F.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function D(e){if(!M)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=M.y-n;if(Math.abs(t)>B&&(M.moved=!0),!M.moved)return;const a=Math.max(0,Math.min(window.innerHeight-50,M.h+t));a<O?(c.style.transition="none",c.style.height="32px"):(T=a,z(),I||R(),c.style.transition="none",c.style.height=T+"px"),J()}function N(e){if(!M)return;const n=M.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??M.y,a=M.y-t,r=M.h+a;M=null,F.style.display="none",document.body.style.userSelect="",c.style.transition="",c.style.height="",n&&(r<O?P():(T=Math.max(O,Math.min(window.innerHeight-50,r)),z(),I||R()),J())}Object.assign(F.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(F),C.addEventListener("click",e=>{e.stopPropagation(),A()}),p.addEventListener("mousedown",W,!0),document.addEventListener("mousemove",D),document.addEventListener("mouseup",N),p.addEventListener("touchstart",W,{passive:!1}),document.addEventListener("touchmove",D,{passive:!0}),document.addEventListener("touchend",N);let U=0,K=0;function H(e){h.textContent=e,h.style.display="block",R()}function $(){h.textContent="",h.style.display="none"}function Y(){if(!G._p||"fit"!==G._mode)return;const e=G._w,n=G._h;if(!e||!n)return;const t=l.clientWidth,a=l.clientHeight,r=Math.min(t/e,a/n);m.style.transform=`scale(${r})`}function J(){if("fullscreen"===G._mode?G.size("max"):Y(),q&&"function"==typeof q.windowResized)try{q.windowResized()}catch(e){H(String(e))}X&&X.resize()}window.addEventListener("mousemove",e=>{U=e.clientX,K=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(U=e.touches[0].clientX,K=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=G._p?G._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=G._w/n.width,a=G._h/n.height;return[(U-n.left)*t,(K-n.top)*a]},window.addEventListener("resize",J);let q=null;const G=new Proxy({_p:null,_mode:"fit",_w:0,_h:0,_setP(e){this._p=e},size(e,n,t){if(!this._p)return;const a=t??void 0;"max"===e||null==e?(this._mode="fullscreen",this._w=l.clientWidth,this._h=l.clientHeight,void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),m.style.transform="scale(1)"):(this._mode="fit",this._w=Math.max(1,0|e),this._h=Math.max(1,0|n),void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),Y())},noSmooth(){this._p?.noSmooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="pixelated")},smooth(){this._p?.smooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="auto")},sketchTitle(e){S.textContent=String(e)},getItem(e){try{return localStorage.getItem(e)}catch(e){return null}},storeItem(e,n){try{localStorage.setItem(e,String(n))}catch(e){}},removeItem(e){try{localStorage.removeItem(e)}catch(e){}},clearStorage(){try{localStorage.clear()}catch(e){}}},{get(e,n){if(n in e)return"function"==typeof e[n]?e[n].bind(e):e[n];if(e._p&&n in e._p){const t=e._p[n];return"function"==typeof t?t.bind(e._p):t}},set:(e,n,t)=>n.startsWith("_")?(e[n]=t,!0):(e._p&&(e._p[n]=t),!0)});function V(){if(De(),q){try{q.remove()}catch(e){}q=null}f.innerHTML="",G._p=null,G._mode="fit",G._w=0,G._h=0,m.style.transform="scale(1)",S.textContent="Shift+Entrée → relancer  ·  Échap → ouvrir/fermer",ge&&(ge.destroy(),ge=null),ye&&(ye.destroy(),ye=null),be&&(be.destroy(),be=null),we&&(we.destroy(),we=null),xe&&(xe.destroy(),xe=null),ve&&(ve.destroy(),ve=null),ke&&(ke.destroy(),ke=null),Ee&&(Ee.destroy(),Ee=null),Ce&&(Ce.destroy(),Ce=null),Se&&(Se.destroy(),Se=null),Le&&(Le.destroy(),Le=null),je&&(je.destroy(),je=null),Ie&&(Ie.destroy(),Ie=null),Te&&(Te.destroy(),Te=null)}window.p5py=G;let X=null,Z=null;const Q={},ee=new Set;function ne(){L.innerHTML="",Z=null;const n=e.filter(e=>!e.hidden);L.style.display=n.length<=1?"none":"",n.forEach(e=>{const n=document.createElement("button");n.className="pf-tab",n.dataset.tabId=e.id,n.textContent=e.label,e.readonly&&n.classList.add("pf-tab-readonly"),"markdown"===e.type&&n.classList.add("pf-tab-markdown"),n.addEventListener("click",()=>te(e)),L.appendChild(n)}),n.length>0&&te(n[0],!0)}function te(e,n){if(n||Z!==e)if(Z=e,L.querySelectorAll(".pf-tab").forEach(n=>{n.classList.toggle("pf-tab-active",n.dataset.tabId===e.id)}),"markdown"===e.type){if(document.getElementById("pf-ace").style.display="none",j.style.display="block",window.marked){let n=marked.parse(e.starterCode);window.mermaid&&(n=n.replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,(e,n)=>`<div class="mermaid">${n.replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">")}</div>`)),j.innerHTML=`<div class="pf-md-inner">${n}</div>`}else j.innerHTML=`<div class="pf-md-inner"><pre>${e.starterCode}</pre></div>`;window.mermaid&&mermaid.run({nodes:j.querySelectorAll(".mermaid")})}else document.getElementById("pf-ace").style.display="block",j.style.display="none",X&&Q[e.id]&&(X.setSession(Q[e.id]),X.setReadOnly(e.readonly),X.focus())}function ae(){let n=1;e.filter(e=>"python"===e.type).forEach(e=>{!e.hidden&&Q[e.id]?(Q[e.id].setOption("firstLineNumber",n),n+=Q[e.id].getLength()):n+=e.code.split("\n").length})}function re(){Object.keys(Q).forEach(e=>delete Q[e]),e.filter(e=>!e.hidden&&"python"===e.type).forEach(e=>{const n=ace.createEditSession(e.code,"ace/mode/python");if(n.setUseWorker(!1),n.setTabSize(4),Q[e.id]=n,!e.readonly){let e=null;n.on("change",()=>{null!==e&&(clearTimeout(e),ee.delete(e)),e=setTimeout(()=>{ee.delete(e),e=null,ie()},350),ee.add(e),ae(),de()})}});const n=e.find(e=>!e.hidden&&"python"===e.type);X&&n&&Q[n.id]&&(X.setSession(Q[n.id]),X.setReadOnly(n.readonly),X.renderer.updateFull(!0)),ae()}function oe(){!r.ace.startsWith("vendor")&&r.ace.startsWith("http")||ace.config.set("basePath",r.ace.replace(/\/[^/]+$/,"/")),X=ace.edit("pf-ace"),X.setTheme("ace/theme/monokai"),X.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),X.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{X.completer?.popup?.isOpen||Ae()}}),X.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:P}),X.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:se}),X.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&le()}}),re(),ne(),de()}function ie(){const n={v:1,tabs:e.map(e=>({label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,content:e.hidden||e.readonly||"python"!==e.type||!Q[e.id]?e.code:Q[e.id].getValue()}))};try{localStorage.setItem(a,JSON.stringify(n))}catch(e){}}function se(){ie()}function de(){const n=s||e.some(e=>!e.hidden&&!e.readonly&&"python"===e.type&&Q[e.id]&&Q[e.id].getValue()!==e.starterCode);k.classList.toggle("pf-dirty",n)}function le(){ee.forEach(e=>clearTimeout(e)),ee.clear();try{localStorage.removeItem(a)}catch(e){}e.forEach(e=>{if(e.label)try{localStorage.removeItem(a+":"+e.label.replace(/[^a-zA-Z0-9]/g,"_"))}catch(e){}});try{localStorage.removeItem(a+":Code")}catch(e){}s=!1,e=t.map((e,n)=>({...e,id:"tab-"+n,code:e.starterCode})),re(),ne(),de(),Ae()}window.addEventListener("beforeunload",se);let ce=null,pe=null;async function fe(){return pe||(pe=(async()=>{const e={};r.pyodideIndex&&(e.indexURL=r.pyodideIndex),ce=await loadPyodide(e),await ce.loadPackage(["rich","pygments"]);try{const e=new Uint8Array(new SharedArrayBuffer(1));ce.setInterruptBuffer(e),window._pfInterrupt=()=>{e[0]=2,setTimeout(()=>{e[0]=0},50)}}catch(e){window._pfInterrupt=null}if(window._pfMountIdbfs=e=>new Promise((n,t)=>{try{ce.FS.mkdirTree(e);try{ce.FS.mount(ce.FS.filesystems.IDBFS,{},e)}catch(e){if(10!==e.errno)return void t(e)}ce.FS.syncfs(!0,e=>e?t(e):n())}catch(e){t(e)}}),window._pfSyncIdbfs=()=>new Promise((e,n)=>{ce.FS.syncfs(!1,t=>t?n(t):e())}),ce.runPython("\nimport sys, types, js\nfrom js import p5py, _pfMouse\nfrom pyodide.ffi import JsProxy\n\n# ── Python builtins that must NOT be shadowed ──────────────────────\n_BLACKLIST = frozenset({\n 'abs','all','any','bin','bool','bytes','callable','chr','compile',\n 'delattr','dict','dir','divmod','enumerate','eval','exec',\n 'filter','float','format','frozenset','getattr','globals','hasattr',\n 'hash','help','hex','id','input','int','isinstance','issubclass',\n 'iter','len','list','locals','map','max','min','next','object',\n 'oct','open','ord','pow','print','property','range','repr',\n 'reversed','round','set','setattr','slice','sorted','staticmethod',\n 'str','sum','super','tuple','type','vars','zip',\n # p5 lifecycle hooks — user defines these, we don't import them\n 'setup','draw','preload',\n})\n\n# ── Introspect a hidden dummy p5 instance ─────────────────────────\n_dummy_node = js.document.createElement('div')\n_dummy = js.p5.new(lambda _: None, _dummy_node)\n\n_p5_functions = set() # names of callable JS members\n_p5_attributes = set() # names of scalar/readable members\n\nfor _n in dir(_dummy):\n if _n.startswith('_') or _n in _BLACKLIST:\n continue\n _v = getattr(_dummy, _n)\n if isinstance(_v, JsProxy):\n if callable(_v):\n _p5_functions.add(_n)\n # non-callable JsProxy (canvas, pixels…) → skip\n else:\n _p5_attributes.add(_n)\n\n# Read real initial values now, while dummy is still alive\n_attr_init = {}\nfor _n in _p5_attributes:\n try:\n _attr_init[_n] = getattr(_dummy, _n)\n except Exception:\n _attr_init[_n] = 0\n\n_dummy.remove()\ndel _dummy, _dummy_node\n\n# ── Build module ───────────────────────────────────────────────────\nm = types.ModuleType(\"p5\")\n\n# Generic function wrapper: delegates to live p5Bridge instance\nclass _FW:\n __slots__ = ('_n',)\n def __init__(self, n): self._n = n\n def __call__(self, *a): return getattr(p5py, self._n)(*a)\n def __repr__(self): return f'<p5 function {self._n}>'\n\nfor _n in _p5_functions:\n setattr(m, _n, _FW(_n))\n\n# ── Special overrides (our bridge has custom behaviour) ────────────\n# smooth/noSmooth exist on a real p5 instance so introspection finds\n# them — but our Proxy overrides them to also toggle CSS image-rendering.\n# size and sketchTitle are pyfrilet-only: NOT on a real p5 instance,\n# so introspection misses them — add them explicitly.\nfor _n in ('sketchTitle',):\n setattr(m, _n, _FW(_n))\n _p5_functions.add(_n) # keep __all__ consistent\n\n# size() calls _pf_refresh after resizing so width/height are immediately\n# correct in setup() — consistent with p5.js JS behaviour.\nclass _SizeWrapper:\n def __call__(self, *a):\n p5py.size(*a)\n _pf_refresh(_ns_ref[0])\n return _GetCanvasWrapper()()\n def __repr__(self): return '<p5 function size>'\nsetattr(m, 'size', _SizeWrapper())\nsetattr(m, 'createCanvas', m.size) # alias — createCanvas(...) == size(...)\n_p5_functions.add('size')\n_p5_functions.add('createCanvas')\n_ns_ref = [{}] # filled in by runCode before each exec\n\n# getCanvas() — returns the p5.Element wrapping the canvas,\n# so the user can call .drop(create_proxy(fn)), .mouseOver(), etc. directly like in JS.\nclass _GetCanvasWrapper:\n def __call__(self):\n p = p5py._p\n if p is None:\n raise RuntimeError('getCanvas() doit être appelé dans setup() ou après')\n p.canvas.id = '__pf_canvas__'\n return p.select('#__pf_canvas__')\n def __repr__(self): return '<p5 function getCanvas>'\nsetattr(m, 'getCanvas', _GetCanvasWrapper())\n_p5_functions.add('getCanvas')\n\n# mouseX / mouseY: override with our accurate coordinate calculator\n# (p5's own values are wrong when a CSS-transformed parent is used)\n_MOUSE_OVERRIDE = frozenset({'mouseX', 'mouseY'})\n\n# Initial values from the dummy instance — constants like WEBGL, DEGREES,\n# LEFT_ARROW… are correct from the very first setup() call.\nfor _n in _p5_attributes:\n if _n in _MOUSE_OVERRIDE:\n setattr(m, _n, 0.0)\n else:\n setattr(m, _n, _attr_init.get(_n, 0))\n\n# Build __all__ for import * — done later, after snake_case aliases are added\n\n# ── _pf_refresh: called before every event callback ───────────────\nimport re as _re\n\n# Pre-compute snake_case alias for each attribute — None if identical\n_attr_snake = {\n _k: (_re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _k) or None)\n for _k in _p5_attributes\n}\n_attr_snake = {_k: (_s if _s != _k else None) for _k, _s in _attr_snake.items()}\n\n# Add snake_case names to _p5_attributes so __all__ and _pf_refresh cover them\nfor _k, _sk in list(_attr_snake.items()):\n if _sk:\n _p5_attributes.add(_sk)\n setattr(m, _sk, getattr(m, _k, 0)) # initial value mirrors camelCase\n _attr_snake[_sk] = None # snake name has no further alias\n\ndef _pf_refresh(ns):\n # accurate mouse coords (bypasses p5's stale CSS-transform offset)\n mx, my = _pfMouse()\n\n # update all known scalar attributes from live instance\n for _k in _p5_attributes:\n _sk = _attr_snake.get(_k)\n if _k in _MOUSE_OVERRIDE:\n _v = mx if _k in ('mouseX', 'mouse_x') else my\n elif _sk is None and _k not in _attr_snake:\n # pure snake_case entry — skip, updated via its camelCase counterpart\n continue\n else:\n try:\n _v = getattr(p5py, _k)\n except Exception:\n continue\n setattr(m, _k, _v)\n if _k in ns:\n ns[_k] = _v\n if _sk:\n setattr(m, _sk, _v)\n if _sk in ns:\n ns[_sk] = _v\n\nsys.modules[\"p5\"] = m\n\n# ── draw() watchdog via sys.settrace ──────────────────────────────\n# Trace is called on every Python line event. We only call time.monotonic()\n# every N events to minimize overhead — a tight loop still triggers within\n# a few microseconds, so detection latency is negligible.\nimport time as _time\n\n_WDOG_CHECK_EVERY = 100\n_wdog_deadline = [0.0]\n_wdog_count = [0]\n\ndef _wdog_trace(frame, event, arg):\n _wdog_count[0] += 1\n if _wdog_count[0] >= _WDOG_CHECK_EVERY:\n _wdog_count[0] = 0\n if _time.monotonic() > _wdog_deadline[0]:\n raise TimeoutError(\"draw() watchdog\")\n return _wdog_trace\n\nclass _PfHandledError(Exception):\n \"\"\"Levée après que rich a déjà affiché le traceback vers xterm.\"\"\"\n pass\n\ndef _pf_safe_call(fn):\n try:\n fn()\n except (_PfHandledError, TimeoutError):\n raise\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and not _tb.tb_frame.f_code.co_filename.startswith(('sketch_', 'programme_')):\n _tb = _tb.tb_next\n if _tb: _e.__traceback__ = _tb\n _pf_rich_console.print_exception(extra_lines=8, show_locals=True)\n from js import _pfShowErrorTerminal\n _pfShowErrorTerminal()\n\ndef _pf_safe_proxy(fn):\n from pyodide.ffi import create_proxy as _cp\n def _wrapped(*args, **kwargs):\n _pf_safe_call(lambda: fn(*args, **kwargs))\n return _cp(_wrapped)\n\nsetattr(m, 'safe_proxy', _pf_safe_proxy)\n_p5_functions.add('safe_proxy')\n\ndef _pf_persist():\n \"\"\"Synchronise /persist vers IndexedDB (fire-and-forget).\n Fonctionne en mode p5 (synchrone) et en mode terminal.\"\"\"\n from js import _pfSyncIdbfs\n _pfSyncIdbfs() # Promise — le navigateur l'exécute dès que la stack JS se libère\n\nsetattr(m, 'persist', _pf_persist)\n_p5_functions.add('persist')\npersist = _pf_persist # accessible aussi hors p5 (mode terminal sans import p5)\n\nimport linecache as _linecache\n_pf_run_counter = [0]\n\ndef _pf_exec_user_code():\n _ns = {}\n _pf_run_counter[0] += 1\n _pf_fname = f'sketch_{_pf_run_counter[0]}'\n with open(_pf_fname, 'w') as _f:\n _f.write(_USER_CODE)\n lines = _USER_CODE.splitlines(keepends=True)\n _linecache.cache[_pf_fname] = (len(_USER_CODE), None, lines, _pf_fname)\n try:\n exec(compile(_USER_CODE, _pf_fname, 'exec'), _ns, _ns)\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and _tb.tb_frame.f_code.co_filename != _pf_fname:\n _tb = _tb.tb_next\n if _tb: _e.__traceback__ = _tb\n _pf_rich_console.print_exception(extra_lines=8, show_locals=True)\n from js import _pfShowErrorTerminal\n _pfShowErrorTerminal()\n return None\n _ns_ref[0] = _ns\n return _ns\n\ndef _pf_draw_watchdog(fn, timeout_ms):\n _wdog_count[0] = 0\n _wdog_deadline[0] = _time.monotonic() + timeout_ms * 0.001\n sys.settrace(_wdog_trace)\n try:\n _pf_safe_call(fn)\n except TimeoutError:\n from js import _pfShowWatchdogError\n _pfShowWatchdogError(timeout_ms)\n finally:\n sys.settrace(None)\n\ndef _pf_draw_direct(fn, timeout_ms):\n _pf_safe_call(fn)\n\ndef _snake_to_camel(name):\n parts = name.split('_')\n return parts[0] + ''.join(p.capitalize() for p in parts[1:])\n\n# Pre-populate snake_case aliases so \"from p5 import no_fill\" works\nfor _camel in list(vars(m).keys()):\n _snake = _re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _camel)\n if _snake != _camel and not hasattr(m, _snake):\n setattr(m, _snake, getattr(m, _camel))\n if _camel in _p5_functions:\n _p5_functions.add(_snake)\n\n# Rebuild __all__ now that snake_case aliases are included\nm.__all__ = sorted(_p5_functions | _p5_attributes)\n\ndef _p5_getattr(name):\n camel = _snake_to_camel(name)\n if camel != name:\n val = getattr(m, camel, None)\n if val is not None:\n return val\n raise AttributeError(f\"module 'p5' has no attribute '{name}'\")\n\nm.__getattr__ = _p5_getattr\n"),ce.runPython("\nimport asyncio as _asyncio, ast as _ast\nimport os as _os, sys as _sys\n_os.environ.setdefault('TERM', 'xterm-256color')\n_os.environ.setdefault('COLORTERM', 'truecolor')\n\n# Wrapper file-like qui écrit directement vers xterm via JS.\n# Rich écrit des strings sur sys.stdout.write() — il faut un vrai objet fichier.\nclass _PfStream:\n def __init__(self, js_fn):\n self._fn = js_fn\n self.encoding = 'utf-8'\n self.errors = 'replace'\n def write(self, s):\n if s:\n self._fn(s)\n return len(s)\n def writelines(self, lines):\n for l in lines: self.write(l)\n def flush(self): pass\n def isatty(self): return True\n @property\n def softspace(self): return 0\n\nfrom js import _pfTermWrite, _pfTermWriteErr\n_sys.stdout = _PfStream(_pfTermWrite)\n_sys.stderr = _PfStream(_pfTermWriteErr)\n\nfrom rich.console import Console as _RichConsole\n_pf_rich_console = _RichConsole(stderr=True)\n\nasync def _pf_async_input(prompt=\"\"):\n from js import _pfTerminalInput\n result = await _pfTerminalInput(str(prompt) if prompt else \"\")\n return result\n\n# Cancellation flag — set by JS before launching a new run\n_pf_cancel_run = [False]\n\nasync def _pf_async_sleep(seconds):\n \"\"\"Cancellable sleep: polls the cancel flag every 50 ms.\"\"\"\n import asyncio as _aio\n deadline = _aio.get_event_loop().time() + seconds\n while True:\n if _pf_cancel_run[0]:\n raise KeyboardInterrupt\n remaining = deadline - _aio.get_event_loop().time()\n if remaining <= 0:\n break\n await _aio.sleep(min(0.05, remaining))\n\nasync def _pf_maybe_await(val):\n \"\"\"Await val if it's a coroutine, otherwise return it as-is.\n This lets us await-ify every call site without knowing in advance\n which functions are async.\"\"\"\n import asyncio\n if asyncio.iscoroutine(val):\n return await val\n return val\n\nclass _AsyncTransformer(_ast.NodeTransformer):\n \"\"\"Transforms user code so that:\n - every def becomes async def (including nested and class methods)\n - every call f(...) becomes await _pf_maybe_await(f(...))\n whether f is a Name, Attribute, subscript, or any other expression\n - lambda bodies are left untouched (can't be async)\n \"\"\"\n _HELPER = '_pf_maybe_await'\n\n def visit_FunctionDef(self, node):\n # Dunder methods (__init__, __str__...) are called by Python internals\n # without going through _pf_maybe_await — they must stay synchronous.\n self.generic_visit(node)\n if node.name.startswith('__') and node.name.endswith('__'):\n return node\n return _ast.AsyncFunctionDef(\n name=node.name,\n args=node.args,\n body=node.body,\n decorator_list=node.decorator_list,\n returns=node.returns,\n lineno=node.lineno,\n col_offset=node.col_offset,\n end_lineno=node.end_lineno,\n end_col_offset=node.end_col_offset,\n )\n\n # Leave Lambda untouched — can't be async\n def visit_Lambda(self, node):\n return node\n\n def visit_Await(self, node):\n # User wrote explicit await f() — recurse into f()'s arguments\n # but don't re-wrap the call itself with _pf_maybe_await.\n self._skip_next_wrap = True\n self.generic_visit(node)\n self._skip_next_wrap = False\n return node\n\n def visit_Call(self, node):\n # Recurse first so nested calls are also transformed\n self.generic_visit(node)\n func = node.func\n # time.sleep(x) → _pf_async_sleep(x) (cancellable)\n if (isinstance(func, _ast.Attribute)\n and isinstance(func.value, _ast.Name)\n and func.value.id == 'time'\n and func.attr == 'sleep'):\n node.func = _ast.Name(id='_pf_async_sleep', ctx=_ast.Load())\n # sleep(x) → _pf_async_sleep(x) (from time import sleep)\n elif isinstance(func, _ast.Name) and func.id == 'sleep':\n node.func = _ast.Name(id='_pf_async_sleep', ctx=_ast.Load())\n # asyncio.sleep(x) → _pf_async_sleep(x) (cancellable)\n elif (isinstance(func, _ast.Attribute)\n and isinstance(func.value, _ast.Name)\n and func.value.id == 'asyncio'\n and func.attr == 'sleep'):\n node.func = _ast.Name(id='_pf_async_sleep', ctx=_ast.Load())\n # If already under a user-written await, don't re-wrap\n if getattr(self, '_skip_next_wrap', False):\n self._skip_next_wrap = False\n return node\n # Wrap: f(...) → await _pf_maybe_await(f(...))\n helper = _ast.Name(id=self._HELPER, ctx=_ast.Load())\n wrapped = _ast.Call(func=helper, args=[node], keywords=[])\n return _ast.Await(value=wrapped)\n\nasync def _pf_run_terminal(source):\n tree = _ast.parse(source)\n tree = _AsyncTransformer().visit(tree)\n\n wrapper = _ast.parse(\"async def programme(): pass\")\n wrapper.body[0].body = tree.body if tree.body else [_ast.Pass()]\n _ast.fix_missing_locations(wrapper)\n _pf_run_counter[0] += 1\n _pf_fname = f'programme_{_pf_run_counter[0]}'\n with open(_pf_fname, 'w') as _f:\n _f.write(source)\n lines = source.splitlines(keepends=True)\n _linecache.cache[_pf_fname] = (len(source), None, lines, _pf_fname)\n import asyncio as _asyncio\n _pf_cancel_run[0] = False # reset at start of each run\n _ns = {\n 'input': _pf_async_input,\n 'persist': _pf_persist,\n '_pf_maybe_await': _pf_maybe_await,\n 'asyncio': _asyncio,\n '_pf_async_sleep': _pf_async_sleep,\n }\n exec(compile(wrapper, _pf_fname, 'exec'), _ns)\n try:\n await _ns['programme']()\n except (SystemExit, KeyboardInterrupt):\n pass\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and _tb.tb_frame.f_code.co_filename != _pf_fname:\n _tb = _tb.tb_next\n if _tb:\n _e.__traceback__ = _tb\n _pf_rich_console.print_exception(extra_lines=8, show_locals=True)\n"),X){me(ce.runPython("list(m.__all__)").toJs())}})(),pe)}function me(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,a,r,o){o(null,r.length>0?n:[])}},a=ace.require("ace/ext/language_tools");a&&Array.isArray(a.completers)&&(a.completers=a.completers.filter(e=>!0!==e._pyfrilet)),t._pyfrilet=!0,X.completers=[...X.completers||[],t]}let _e=!1,ue=!1,he=0,ye=null,be=null,ge=null,we=null,xe=null,ve=null,ke=null,Ee=null,Ce=null,Se=null,Le=null,je=null,Ie=null,Te=null;const ze=300;function Re(e){return!/\bfrom\s+p5\s+import\b|\bimport\s+p5\b/.test(e)}async function Pe(e){const n=++he;if(ue){if(ce)try{ce.globals.get("_pf_cancel_run")[0]=!0}catch(e){}on();const e=Date.now()+3e3;for(;ue&&Date.now()<e;)await new Promise(e=>setTimeout(e,20))}if(n===he){ue=!0,an(),tn(),$(),window._pfSetP5Mode&&window._pfSetP5Mode(!1),await window._pfMountIdbfs("/persist");try{const n=ce.globals.get("_pf_run_terminal");await n(e)}catch(e){const n=String(e);n.includes("SystemExit")||nn(n+"\n")}finally{ue=!1}}}async function Ae(){if(_e){if(!ue)return;window._pfInterrupt&&window._pfInterrupt(),on(),_e=!1,y.classList.remove("pf-running"),await new Promise(e=>setTimeout(e,80))}_e=!0,y.classList.add("pf-running"),$(),tn(),V(),ce||(u.textContent="Initialisation de Pyodide…",_.style.display="flex");try{await fe()}catch(e){return _.style.display="none",H("Erreur Pyodide : "+(e.message||String(e))),_e=!1,void y.classList.remove("pf-running")}_.style.display="none";const t=e.filter(e=>"python"===e.type).map(e=>e.hidden||e.readonly||!Q[e.id]?e.code:Q[e.id].getValue()).join("\n");try{u.textContent="Chargement des dépendances…",_.style.display="flex",await ce.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:n})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}if(_.style.display="none",Re(t))return y.classList.remove("pf-running"),await Pe(t),void(_e=!1);on(),window._pfSetP5Mode&&window._pfSetP5Mode(!0),await window._pfMountIdbfs("/persist"),ce.globals.set("_USER_CODE",t);const a=ce.globals.get("_pf_exec_user_code");try{if(!a())return _e=!1,void y.classList.remove("pf-running");ce.runPython("_ns = _ns_ref[0]")}catch(e){return rn(e.message||String(e)),_e=!1,void y.classList.remove("pf-running")}let r,i,s,d,l,c,p,m,h,b,g,w,x,v;try{const e=(e,n)=>ce.runPython(`_ns.get('${e}') or _ns.get('${n}')`);l=e("preload","preload"),r=e("setup","setup"),i=e("draw","draw"),s=e("mousePressed","mouse_pressed"),d=e("keyPressed","key_pressed"),c=e("mouseDragged","mouse_dragged"),p=e("mouseReleased","mouse_released"),m=e("mouseMoved","mouse_moved"),h=e("mouseWheel","mouse_wheel"),b=e("doubleClicked","double_clicked"),g=e("keyReleased","key_released"),w=e("touchStarted","touch_started"),x=e("touchMoved","touch_moved"),v=e("touchEnded","touch_ended")}catch(e){return rn(e.message||String(e)),_e=!1,void y.classList.remove("pf-running")}if(!i)return H("Le script doit définir au moins une fonction draw()."),_e=!1,void y.classList.remove("pf-running");const{create_proxy:k}=ce.pyimport("pyodide.ffi"),E=ce.runPython("_ns.get('windowResized')"),C=ce.globals.get("_pf_refresh"),S=ce.globals.get(o?"_pf_draw_direct":"_pf_draw_watchdog"),L=ce.globals.get("_ns"),j=ce.globals.get("_pf_safe_call"),I=e=>e?k(()=>{try{C(L),j(e)}catch(e){rn("")}}):null;ge=l?k(()=>{try{j(l)}catch(e){rn("")}}):null,ye=r?k(()=>{try{j(r)}catch(e){rn("")}}):null,be=k(()=>{try{C(L),S(i,ze)}catch(e){V(),rn("")}}),we=I(s),xe=I(p),ve=I(c),ke=I(m),Ee=I(h),Ce=I(b),Se=I(d),Le=I(g),je=I(w),Ie=I(x),Te=I(v);const T=E?k(()=>{try{j(E)}catch(e){rn("")}}):null;let z=!1;q=new p5(e=>{G._setP(e),ge&&(e.preload=()=>{ge()}),e.setup=()=>{ye&&ye(),e.canvas||G.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),z=!0},e.draw=()=>{z&&be()},e.mousePressed=()=>{z&&we&&we()},e.mouseReleased=()=>{z&&xe&&xe()},e.mouseDragged=()=>{z&&ve&&ve()},e.mouseMoved=()=>{z&&ke&&ke()},e.mouseWheel=e=>{z&&Ee&&Ee()},e.doubleClicked=()=>{z&&Ce&&Ce()},e.keyPressed=()=>{z&&Se&&Se()},e.keyReleased=()=>{z&&Le&&Le()},je&&(e.touchStarted=()=>{z&&je()}),Ie&&(e.touchMoved=()=>{z&&Ie()}),Te&&(e.touchEnded=()=>{z&&Te()}),e.windowResized=()=>{"fullscreen"===G._mode?G.size("max"):Y(),T&&T()}},f),_e=!1,y.classList.remove("pf-running")}const Me='<!doctype html>\n<html lang="fr">\n<head>\n <meta charset="utf-8">\n <meta name="viewport" content="width=device-width, initial-scale=1">\n <title>export</title>\n <script src="https://cdn.jsdelivr.net/npm/pyfrilet@0.6.6/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\nFILLME-SCRIPTS\n\n</body>\n</html>';function Be(){const n=e.map((e,n)=>{let t;t="python"!==e.type||e.hidden||e.readonly||!Q[e.id]?e.code:Q[e.id].getValue();const a=[],r="markdown"===e.type?"text/markdown":"text/python";null!==e.label&&a.push(`data-tab="${e.label.replace(/"/g,"&quot;")}"`),e.hidden&&a.push("data-hidden"),e.readonly&&a.push("data-readonly");return`<script type="${r}"${a.length?" "+a.join(" "):""}>\n${t.replace(/<\/script>/gi,"<\\/script>")}\n<\/script>`}).join("\n\n"),t=Me.replace("FILLME-SCRIPTS",n),a=new Blob([t],{type:"text/html;charset=utf-8"}),r=URL.createObjectURL(a),o=Object.assign(document.createElement("a"),{href:r,download:"sketch.html"});document.body.appendChild(o),o.click(),document.body.removeChild(o),URL.revokeObjectURL(r)}let Oe=null,Fe=[];function We(){const e=G._p?.canvas;if(!e)return;const n=["video/webm;codecs=vp9","video/webm;codecs=vp8","video/webm"].find(e=>MediaRecorder.isTypeSupported(e))||"video/webm",t=e.captureStream();Oe=new MediaRecorder(t,{mimeType:n}),Fe=[],Oe.ondataavailable=e=>{e.data.size&&Fe.push(e.data)},Oe.onstop=()=>{const e=new Blob(Fe,{type:n}),t=URL.createObjectURL(e),a=n.includes("webm")?"webm":"mp4";Object.assign(document.createElement("a"),{href:t,download:`sketch.${a}`}).click(),URL.revokeObjectURL(t),v.textContent="⏺",v.title="Enregistrer WebM",v.classList.remove("pf-recording"),Oe=null},Oe.start(),v.textContent="⏹",v.title="Arrêter l'enregistrement",v.classList.add("pf-recording")}function De(){Oe&&"inactive"!==Oe.state&&Oe.stop()}v.addEventListener("click",()=>{Oe?De():We()}),y.addEventListener("click",()=>Ae()),w.addEventListener("click",()=>{I?P():(T=window.innerHeight-32,z(),R())}),x.addEventListener("click",Be);const Ne="https://codeberg.org/nopid/pyfrilet";function Ue(e){return new Promise((n,t)=>{const a=document.createElement("script");a.src=e,a.onload=n,a.onerror=()=>t(new Error("Impossible de charger : "+e)),document.head.appendChild(a)})}E.addEventListener("click",()=>window.open(Ne,"_blank")),k.addEventListener("click",()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&le()}),window.addEventListener("keydown",e=>{const n=I&&X&&X.isFocused&&X.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void Ae();if("Escape"===e.key){const t=document.querySelector(".ace_search");if(t&&"none"!==t.style.display)return e.preventDefault(),e.stopPropagation(),X.searchBox?X.searchBox.hide():t.style.display="none",void X.focus();if(n){const n=X.completer?.popup?.isOpen;if(n)return;return e.preventDefault(),e.stopPropagation(),void P()}return e.preventDefault(),e.stopPropagation(),void(I?P():R())}if(!n)return"s"!==e.key&&"S"!==e.key||!e.ctrlKey&&!e.metaKey?"r"!==e.key&&"R"!==e.key||!e.ctrlKey&&!e.metaKey||e.altKey?void 0:(e.preventDefault(),void(confirm("Réinitialiser ? Les modifications seront perdues.")&&le())):(e.preventDefault(),void se())}else e.preventDefault()},!0),(async()=>{u.textContent="Chargement des dépendances…",_.style.display="flex";try{if(await Ue(r.p5),r.marked){const e=document.createElement("link");e.rel="stylesheet",e.href=r.katexCss,document.head.appendChild(e),await Ue(r.marked),await Ue(r.katex),await Ue(r.markedKatex),await Ue(r.mermaid),marked.use(markedKatex({throwOnError:!1})),mermaid.initialize({startOnLoad:!1,theme:"neutral"})}await Ue(r.ace),await Ue(r.acePython),await Ue(r.aceMonokai),await Ue(r.aceLangTools),await Ue(r.aceSearchbox),await Ue(r.pyodide);const e=document.createElement("link");e.rel="stylesheet",e.href=r.xtermCss,document.head.appendChild(e),await Ue(r.xterm),await Ue(r.xtermFit),await Ue(r.xtermUni)}catch(e){return u.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}oe(),await Ae(),_.style.display="none"})();const Ke=document.getElementById("pf-xterm");let He=null,$e=null,Ye=null,Je="";function qe(){if(He)return;He=new Terminal({theme:{background:"#000000",foreground:"#e8e8e8",cursor:"#ffffff",black:"#2a2a2a",brightBlack:"#555555",red:"#cc4444",brightRed:"#ff6666",green:"#44aa44",brightGreen:"#66cc66",yellow:"#aaaa00",brightYellow:"#dddd44",blue:"#4466cc",brightBlue:"#6688ff",magenta:"#aa44aa",brightMagenta:"#dd66dd",cyan:"#44aaaa",brightCyan:"#66cccc",white:"#cccccc",brightWhite:"#ffffff"},fontFamily:"'Fira Code', 'Consolas', 'Courier New', monospace",fontSize:15,lineHeight:1,letterSpacing:0,cursorBlink:!0,scrollback:2e3,convertEol:!0,allowProposedApi:!0}),$e=new FitAddon.FitAddon,He.loadAddon($e);const e=new Unicode11Addon.Unicode11Addon;He.loadAddon(e),He.unicode.activeVersion="11",He.open(Ke),$e.fit(),new ResizeObserver(()=>{He&&"none"!==Ke.style.display&&$e.fit()}).observe(Ke),He.onData(e=>{if(Ye)if("\r"===e){const e=Je;Je="",He.write("\r\n");const n=Ye;Ye=null,n(e)}else if(""===e)Je.length>0&&(Je=Je.slice(0,-1),He.write("\b \b"));else if(""===e){Je="",He.write("^C\r\n");const e=Ye;Ye=null,e(null)}else e.charCodeAt(0)>=32&&(Je+=e,He.write(e))})}window._pfTerminalInput=function(e){return new Promise(n=>{Ye=n,Je="",e&&He.write(e),He.focus()})};let Ge=!1;window._pfSetP5Mode=e=>{Ge=e};const Ve=document.getElementById("pf-xterm-handle"),Xe=document.getElementById("pf-xterm-chevron");function Ze(){Ke.style.display="block",Ke.classList.remove("pf-xterm-overlay","pf-xterm-collapsed"),Ke.classList.add("pf-xterm-bandeau"),Xe&&(Xe.textContent="∨"),qe(),$e.fit()}function Qe(){Ke.classList.contains("pf-xterm-collapsed")?(Ke.classList.remove("pf-xterm-collapsed"),Xe&&(Xe.textContent="∨"),$e.fit()):(Ke.classList.add("pf-xterm-collapsed"),Xe&&(Xe.textContent="∧"))}Ve&&Ve.addEventListener("click",Qe);function en(e){Ge&&"none"===Ke.style.display&&Ze(),He&&He.write(e)}function nn(e){qe(),He.write(""),He.write(e.replace(/\n/g,"\r\n")),He.write("")}function tn(){He&&He.reset(),Ye=null,Je="",Ke.classList.remove("pf-xterm-bandeau","pf-xterm-collapsed"),Xe&&(Xe.textContent="∨")}function an(){m.style.display="none",Ke.style.display="block",Ke.classList.remove("pf-xterm-overlay"),qe(),$e.fit(),He.focus()}function rn(e){Ke.style.display="block",Ke.classList.add("pf-xterm-overlay"),qe(),$e.fit(),e&&(tn(),He.write("── Erreur ──────────────────────────────────────\r\n\r\n"),He.write(e.replace(/\n/g,"\r\n")+"\r\n"))}function on(){if(Ke.style.display="none",Ke.classList.remove("pf-xterm-overlay"),m.style.display="",Ye){const e=Ye;Ye=null,Je="",e(null)}}window._pfTermWrite=en,window._pfTermWriteErr=nn,window._pfShowWatchdogError=e=>{V(),H(`draw() a dépassé ${e}ms — sketch arrêté (watchdog).`)},window._pfShowErrorTerminal=()=>{V(),rn("")}}(I,L,S,C,P,R)})}();