pyfrilet 0.6.2 → 0.6.3

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
@@ -6,7 +6,7 @@ Il combine [p5.js](https://p5js.org/) (dessin 2D) et [Pyodide](https://pyodide.o
6
6
  ---
7
7
 
8
8
  ## Démarrage rapide
9
- ````html
9
+ ```html
10
10
  <!doctype html>
11
11
  <html lang="fr">
12
12
  <head>
@@ -31,7 +31,7 @@ def draw():
31
31
  </script>
32
32
  </body>
33
33
  </html>
34
- ````
34
+ ```
35
35
 
36
36
  C'est tout. pyfrilet se charge de démarrer Pyodide, de monter le module `p5`, et de lancer le sketch. Les dépendances (p5.js, Pyodide, ACE) sont chargées depuis des CDN publics par défaut.
37
37
 
@@ -118,22 +118,23 @@ Ces fonctions sont définies par l'utilisateur et appelées automatiquement par
118
118
  | `sketchTitle(s)` | Affiche un texte dans la barre de contrôle. À appeler dans `setup()`. |
119
119
  | `getCanvas()` | Retourne le `p5.Element` wrappant le canvas. Utile pour appeler des méthodes p5.Element comme `.drop()`. À appeler dans `setup()` ou après. |
120
120
  | `safe_proxy(fn)` | Wrape une fonction Python pour l'utiliser comme callback JS (équivalent de `create_proxy`) en capturant les erreurs et en les affichant dans le terminal. À préférer à `create_proxy` pour les callbacks utilisateur (`.drop()`, événements DOM…). |
121
+ | `persist()` | Synchronise `/persist` vers IndexedDB (fire-and-forget). Fonctionne en mode p5 et en mode terminal. Disponible via `from p5 import *` ou directement en mode terminal. |
121
122
 
122
123
  #### Propriétés dynamiques
123
124
 
124
125
  Ces variables sont mises à jour automatiquement avant chaque appel à `draw()`, `keyPressed()` et `mousePressed()`, y compris si elles ont été importées avec `from p5 import *` :
125
- ````python
126
+ ```python
126
127
  mouseX, mouseY # position de la souris dans le repère du canvas
127
128
  width, height # dimensions logiques du canvas
128
129
  frameCount # numéro de la frame courante
129
130
  key # dernière touche appuyée (caractère)
130
131
  keyCode # code numérique de la dernière touche
131
- ````
132
+ ```
132
133
 
133
134
  > **Note sur `keyCode`** : p5.js expose des constantes nommées (`LEFT_ARROW`, `RIGHT_ARROW`, `UP_ARROW`, `DOWN_ARROW`, `ENTER`, `BACKSPACE`…) directement importables avec `from p5 import *`.
134
135
 
135
136
  ### Exemple : plein écran réactif
136
- ````python
137
+ ```python
137
138
  from p5 import *
138
139
 
139
140
  def setup():
@@ -149,10 +150,10 @@ def draw():
149
150
  noStroke()
150
151
  fill('#7aa2f7')
151
152
  circle(width / 2, height / 2, min(width, height) * 0.4)
152
- ````
153
+ ```
153
154
 
154
155
  ### Exemple : navigation clavier
155
- ````python
156
+ ```python
156
157
  from p5 import *
157
158
 
158
159
  page = 0
@@ -171,12 +172,12 @@ def draw():
171
172
  fill('#e0af68')
172
173
  textSize(16)
173
174
  text("page " + str(page), 160, 150)
174
- ````
175
+ ```
175
176
 
176
177
  ### Packages Python tiers
177
178
 
178
179
  pyfrilet détecte automatiquement les imports du sketch et charge les packages disponibles dans la distribution Pyodide avant l'exécution. Il n'y a rien à faire :
179
- ````python
180
+ ```python
180
181
  from p5 import *
181
182
  import numpy as np # chargé automatiquement
182
183
  import networkx as nx # chargé automatiquement
@@ -186,7 +187,7 @@ def setup():
186
187
 
187
188
  def draw():
188
189
  background(20)
189
- ````
190
+ ```
190
191
 
191
192
  Un message "Chargement des dépendances…" s'affiche pendant le téléchargement. Seuls les packages inclus dans la [distribution Pyodide](https://pyodide.org/en/stable/usage/packages-in-pyodide.html) sont supportés (numpy, scipy, pandas, networkx, pillow…). Les packages pip arbitraires ne sont pas supportés.
192
193
 
@@ -195,7 +196,7 @@ Un message "Chargement des dépendances…" s'affiche pendant le téléchargemen
195
196
  ### Glisser-déposer de fichiers
196
197
 
197
198
  Pour recevoir des fichiers glissés sur le canvas, on utilise `getCanvas().drop()` avec `safe_proxy` :
198
- ````python
199
+ ```python
199
200
  from p5 import *
200
201
 
201
202
  img = None
@@ -217,7 +218,7 @@ def draw():
217
218
  background(40)
218
219
  if img:
219
220
  image(img, 0, 0, width, height)
220
- ````
221
+ ```
221
222
 
222
223
  `safe_proxy` est préférable à `create_proxy` (de `pyodide.ffi`) pour les callbacks utilisateur : les erreurs qui surviennent dans le callback sont capturées et affichées proprement dans le terminal au lieu de disparaître silencieusement dans la console du navigateur.
223
224
 
@@ -226,21 +227,21 @@ def draw():
226
227
  ### Note sur `smooth()` / `noSmooth()` et le texte
227
228
 
228
229
  Par défaut le canvas est rendu en mode antialiasé. `noSmooth()` bascule en mode pixel art — formes **et** texte sont pixelisés. Pour mélanger les deux dans le même `draw()` :
229
- ````python
230
+ ```python
230
231
  noSmooth()
231
232
  rect(10, 10, 80, 80) # bords nets
232
233
  smooth()
233
234
  textSize(14)
234
235
  text("lisible", 10, 120) # texte antialiasé
235
- ````
236
+ ```
236
237
 
237
238
  ---
238
239
 
239
240
  ### Gestion des erreurs
240
241
 
241
- Quelle que soit l'origine de l'erreur (mode p5 ou mode terminal), pyfrilet affiche un traceback formaté par [rich](https://rich.readthedocs.io/) dans le terminal intégré : cadre coloré, fichier et numéro de ligne, extrait du code source. Les frames internes de pyfrilet sont automatiquement filtrées — seul le code utilisateur apparaît.
242
+ Quelle que soit l'origine de l'erreur (mode p5 ou mode terminal), pyfrilet affiche un traceback formaté par [rich](https://rich.readthedocs.io/) dans le terminal intégré : cadre coloré, fichier et numéro de ligne, extrait du code source avec quelques lignes de contexte autour de la ligne fautive. Les frames internes de pyfrilet sont automatiquement filtrées — seul le code utilisateur apparaît.
242
243
 
243
- En mode p5, le sketch est arrêté dès la première erreur et le traceback s'affiche en overlay semi-transparent par-dessus le canvas. Corriger le code et relancer suffit à reprendre.
244
+ En mode p5, le sketch est arrêté dès la première erreur (y compris les erreurs au niveau module, hors des fonctions `setup`/`draw`) et le traceback s'affiche en overlay semi-transparent par-dessus le canvas. Corriger le code et relancer suffit à reprendre.
244
245
 
245
246
  ---
246
247
 
@@ -249,7 +250,7 @@ En mode p5, le sketch est arrêté dès la première erreur et le traceback s'af
249
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`.
250
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
- ````python
253
+ ```python
253
254
  # Pas d'import p5 → mode terminal automatique
254
255
  import asyncio
255
256
 
@@ -258,27 +259,27 @@ for i in range(5):
258
259
  await asyncio.sleep(0.5)
259
260
 
260
261
  print("terminé !")
261
- ````
262
+ ```
262
263
 
263
264
  ### `input()`
264
265
 
265
266
  `input()` est entièrement supporté : le programme se met en attente, le terminal affiche le prompt et accepte la saisie clavier.
266
- ````python
267
+ ```python
267
268
  nom = input("Ton prénom : ")
268
269
  print(f"Bonjour, {nom} !")
269
- ````
270
+ ```
270
271
 
271
272
  Ctrl+C interrompt une saisie en cours et retourne `None` — prévoir un guard si nécessaire :
272
- ````python
273
+ ```python
273
274
  val = input("Valeur : ")
274
275
  if val is None:
275
276
  print("annulé")
276
- ````
277
+ ```
277
278
 
278
279
  ### rich
279
280
 
280
281
  `rich` est disponible directement — tableaux, couleurs ANSI, barres de progression :
281
- ````python
282
+ ```python
282
283
  from rich.console import Console
283
284
  from rich.table import Table
284
285
 
@@ -291,7 +292,7 @@ table.add_row("Mercure", "4 879")
291
292
  table.add_row("Vénus", "12 104")
292
293
  table.add_row("Terre", "12 742")
293
294
  console.print(table)
294
- ````
295
+ ```
295
296
 
296
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 :
297
298
  >
@@ -307,6 +308,31 @@ console.print(table)
307
308
  > progress.refresh()
308
309
  > ```
309
310
 
311
+ ### Persistence (IndexedDB)
312
+
313
+ 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.
314
+
315
+ La fonction `persist()` déclenche la synchronisation MEMFS → IndexedDB. Elle fonctionne dans les deux modes sans `await` :
316
+
317
+ ```python
318
+ import json, os
319
+
320
+ DB = '/persist/data.json'
321
+
322
+ # Lire
323
+ data = json.loads(open(DB).read()) if os.path.exists(DB) else {}
324
+
325
+ # Modifier et persister
326
+ data['compteur'] = data.get('compteur', 0) + 1
327
+ with open(DB, 'w') as f:
328
+ json.dump(data, f)
329
+ persist() # fire-and-forget — fonctionne en p5 et en terminal
330
+ ```
331
+
332
+ En mode p5, importer avec `from p5 import persist` ou `from p5 import *`. En mode terminal, `persist` est disponible directement sans import.
333
+
334
+ > La synchronisation est asynchrone côté navigateur : la Promise est lancée dès l'appel et exécutée dès que la stack JS se libère. En mode terminal, on peut aussi `await _pfSyncIdbfs()` depuis `js` pour attendre explicitement la fin de l'écriture.
335
+
310
336
  ### Relancer
311
337
 
312
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.
@@ -362,7 +388,7 @@ Tous les blocs Python partagent le même namespace : les variables et fonctions
362
388
  | `type="text/markdown"` | Le contenu est rendu en Markdown (non exécuté) |
363
389
 
364
390
  ### Exemple : énoncé + code utilitaire + zone élève
365
- ````html
391
+ ```html
366
392
  <script type="text/markdown" data-tab="Énoncé">
367
393
  # Exercice
368
394
 
@@ -386,12 +412,12 @@ def draw():
386
412
  background(20)
387
413
  # à toi de jouer !
388
414
  </script>
389
- ````
415
+ ```
390
416
 
391
417
  ### Bloc caché
392
418
 
393
419
  Un bloc `data-hidden` est concaténé au code exécuté mais invisible dans l'éditeur. Utile pour du code d'initialisation ou de vérification qu'on ne souhaite pas exposer :
394
- ````html
420
+ ```html
395
421
  <script type="text/python" data-hidden>
396
422
  # Code invisible — exécuté en premier
397
423
  PALETTE = ['#7aa2f7', '#e0af68', '#9ece6a']
@@ -408,11 +434,11 @@ def draw():
408
434
  fill(PALETTE[frameCount // 60 % 3])
409
435
  circle(200, 200, 100)
410
436
  </script>
411
- ````
437
+ ```
412
438
 
413
439
  ### Rendu des onglets Markdown
414
440
 
415
- Les onglets Markdown sont affichés sur fond clair avec une mise en page soignée : largeur de lecture limitée à 680 px, centrage automatique sur grand écran, police [Alegreya Sans](https://fonts.google.com/specimen/Alegreya+Sans) (chargée depuis Google Fonts en mode CDN). Les blocs de code sont rendus sur fond sombre pour un bon contraste. Les diagrammes Mermaid utilisent le thème neutre (clair).
441
+ Les onglets Markdown sont affichés sur fond clair avec une mise en page soignée : largeur de lecture limitée à 680 px, centrage automatique sur grand écran, police [Alegreya Sans](https://fonts.google.com/specimen/Alegreya+Sans) (chargée depuis Google Fonts en mode CDN). Les diagrammes Mermaid utilisent le thème neutre (clair).
416
442
 
417
443
  En mode déploiement local sans accès internet, la fonte se replie sur Georgia.
418
444
 
@@ -428,19 +454,19 @@ Le contenu d'un onglet `type="text/markdown"` est rendu avec [marked](https://ma
428
454
  **Diagrammes (Mermaid) :**
429
455
 
430
456
  Un bloc de code avec le langage `mermaid` est rendu comme un diagramme SVG :
431
- ````markdown
432
- ```mermaid
457
+ ```markdown
458
+ ```mermaid
433
459
  graph TD
434
460
  A[Départ] --> B{Condition}
435
461
  B -->|oui| C[Résultat 1]
436
462
  B -->|non| D[Résultat 2]
463
+ ```
437
464
  ```
438
- ````
439
465
 
440
466
  Mermaid supporte de nombreux types de diagrammes : `graph`, `sequenceDiagram`, `classDiagram`, `flowchart`, `gantt`, `pie`… Voir la [documentation Mermaid](https://mermaid.js.org/intro/) pour la syntaxe complète.
441
467
 
442
468
  **Exemple combiné :**
443
- ````html
469
+ ```html
444
470
  <script type="text/markdown" data-tab="Cours">
445
471
  # Algorithme de Dijkstra
446
472
 
@@ -453,15 +479,15 @@ le sommet $u \notin S$ qui minimise :
453
479
  $$d(s, u) = \min_{v \in S} \left( d(s, v) + w(v, u) \right)$$
454
480
 
455
481
  La complexité est $O((V + E) \log V)$ avec un tas binaire.
456
- ```mermaid
482
+ ```mermaid
457
483
  graph LR
458
484
  A((1)) -->|4| B((2))
459
485
  A -->|1| C((3))
460
486
  C -->|2| B
461
487
  B -->|1| D((4))
462
- ```
488
+ ```
463
489
  </script>
464
- ````
490
+ ```
465
491
 
466
492
  Les blocs Python sont concaténés dans l'ordre du DOM avant exécution : blocs cachés et visibles sont traités ensemble, dans l'ordre où ils apparaissent dans le HTML. L'onglet actif dans l'éditeur n'influence pas l'exécution.
467
493
 
@@ -476,7 +502,7 @@ Par défaut, pyfrilet charge p5.js, Pyodide et ACE depuis des CDN publics. Pour
476
502
  ### Configuration
477
503
 
478
504
  `data-sources` et `data-vendor` se placent de préférence sur la balise `<script src="pyfrilet.js">` elle-même. Pour la rétrocompatibilité, ces attributs sont également acceptés sur le premier bloc `<script type="text/python">`.
479
- ````html
505
+ ```html
480
506
  <!-- Recommandé -->
481
507
  <script src="pyfrilet.js" data-sources="local" data-vendor="vendor/"></script>
482
508
 
@@ -488,12 +514,12 @@ Par défaut, pyfrilet charge p5.js, Pyodide et ACE depuis des CDN publics. Pour
488
514
  <script type="text/python" data-sources="local" data-vendor="vendor/">
489
515
 
490
516
  </script>
491
- ````
517
+ ```
492
518
 
493
519
  `data-vendor` indique le chemin vers le dossier `vendor/` **relatif à la page HTML**. La valeur par défaut est `vendor/`.
494
520
 
495
521
  ### Structure de fichiers
496
- ````
522
+ ```
497
523
  mon-projet/
498
524
  ├── pyfrilet.js
499
525
  ├── mon-sketch.html
@@ -518,59 +544,59 @@ mon-projet/
518
544
  ├── pyodide.asm.wasm
519
545
  ├── python_stdlib.zip
520
546
  └── … (autres fichiers Pyodide)
521
- ````
547
+ ```
522
548
 
523
549
  ### Télécharger les dépendances
524
550
 
525
551
  **p5.js**
526
- ````
552
+ ```
527
553
  https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js
528
- ````
554
+ ```
529
555
 
530
556
  **ACE editor** (5 fichiers)
531
- ````
557
+ ```
532
558
  https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js
533
559
  https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js
534
560
  https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js
535
561
  https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js
536
562
  https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js
537
- ````
563
+ ```
538
564
 
539
565
  **xterm.js** (4 fichiers)
540
- ````
566
+ ```
541
567
  https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css
542
568
  https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js
543
569
  https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js
544
570
  https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.8.0/lib/addon-unicode11.min.js
545
- ````
571
+ ```
546
572
  Renommer respectivement en `xterm.min.css`, `xterm.min.js`, `addon-fit.min.js`, `addon-unicode11.min.js`.
547
573
 
548
574
  **marked.js + KaTeX + Mermaid** (uniquement si onglets Markdown)
549
- ````
575
+ ```
550
576
  https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js
551
577
  https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css
552
578
  https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js
553
579
  https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js
554
580
  https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js
555
- ````
581
+ ```
556
582
  Renommer `index.umd.js` en `marked-katex-extension.js` dans le dossier vendor.
557
583
 
558
584
  **Pyodide** — télécharger l'archive complète depuis les releases GitHub :
559
- ````
585
+ ```
560
586
  https://github.com/pyodide/pyodide/releases/tag/0.26.4
561
- ````
587
+ ```
562
588
  Extraire le contenu dans `vendor/pyodide/`.
563
589
 
564
590
  ### Serveur local
565
591
 
566
592
  Les navigateurs bloquent le chargement de fichiers locaux (`file://`) pour des raisons de sécurité. Il faut un serveur HTTP minimal pour tester en local :
567
- ````bash
593
+ ```bash
568
594
  # Python 3
569
595
  python -m http.server 8000
570
596
 
571
597
  # Node.js
572
598
  npx serve .
573
- ````
599
+ ```
574
600
 
575
601
  Puis ouvrir `http://localhost:8000/mon-sketch.html`.
576
602
 
@@ -579,7 +605,7 @@ Puis ouvrir `http://localhost:8000/mon-sketch.html`.
579
605
  ## Build et publication
580
606
 
581
607
  Le build est géré par `build.js` (Node.js + Terser). Il concatène les fragments de `src/`, injecte CSS et HTML, puis génère `pyfrilet.js` (lisible) et `pyfrilet.min.js`.
582
- ````bash
608
+ ```bash
583
609
  npm run build # génère pyfrilet.js + pyfrilet.min.js
584
610
  ````
585
611
 
@@ -592,7 +618,7 @@ npm version patch # (ou minor / major) — modifie package.json, comm
592
618
  npm publish # build automatique puis publication sur npm
593
619
 
594
620
  git push && git push --tags # pousser commits et tag sur Codeberg
595
- ````
621
+ ```
596
622
 
597
623
  ---
598
624
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyfrilet",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "type": "module",
5
5
  "main": "pyfrilet.js",
6
6
  "files": [
package/pyfrilet.js CHANGED
@@ -1135,6 +1135,36 @@ function main(tabs, htmlTabs, SK, URLS, noWatchdog, staleSnapshot) {
1135
1135
  pyodide = await loadPyodide(opts);
1136
1136
  await pyodide.loadPackage(['rich', 'pygments']);
1137
1137
 
1138
+ /* ── Interrupt buffer — nécessite COOP/COEP headers (SharedArrayBuffer) ── */
1139
+ try {
1140
+ const _interruptBuf = new Uint8Array(new SharedArrayBuffer(1));
1141
+ pyodide.setInterruptBuffer(_interruptBuf);
1142
+ window._pfInterrupt = () => {
1143
+ _interruptBuf[0] = 2; /* SIGINT */
1144
+ setTimeout(() => { _interruptBuf[0] = 0; }, 50);
1145
+ };
1146
+ } catch(_e) {
1147
+ /* SharedArrayBuffer indisponible (headers COOP/COEP manquants) — interruption désactivée */
1148
+ window._pfInterrupt = null;
1149
+ }
1150
+
1151
+ /* ── IDBFS helpers — persistence IndexedDB accessible depuis Python ── */
1152
+ window._pfMountIdbfs = (path) => new Promise((resolve, reject) => {
1153
+ try {
1154
+ pyodide.FS.mkdirTree(path);
1155
+ try {
1156
+ pyodide.FS.mount(pyodide.FS.filesystems.IDBFS, {}, path);
1157
+ } catch(e) {
1158
+ if (e.errno !== 10) { reject(e); return; }
1159
+ /* errno 10 = EBUSY : déjà monté, on re-syncfs seulement */
1160
+ }
1161
+ pyodide.FS.syncfs(true, (err) => err ? reject(err) : resolve());
1162
+ } catch(e) { reject(e); }
1163
+ });
1164
+ window._pfSyncIdbfs = () => new Promise((resolve, reject) => {
1165
+ pyodide.FS.syncfs(false, (err) => err ? reject(err) : resolve());
1166
+ });
1167
+
1138
1168
  /* Build the "p5" Python module via dynamic introspection of a dummy instance */
1139
1169
  pyodide.runPython(`
1140
1170
  import sys, types, js
@@ -1309,9 +1339,6 @@ def _wdog_trace(frame, event, arg):
1309
1339
  raise TimeoutError("draw() watchdog")
1310
1340
  return _wdog_trace
1311
1341
 
1312
- from rich.console import Console as _RichConsole
1313
- _pf_rich_console = _RichConsole(stderr=True)
1314
-
1315
1342
  class _PfHandledError(Exception):
1316
1343
  """Levée après que rich a déjà affiché le traceback vers xterm."""
1317
1344
  pass
@@ -1323,10 +1350,10 @@ def _pf_safe_call(fn):
1323
1350
  raise
1324
1351
  except Exception as _e:
1325
1352
  _tb = _e.__traceback__
1326
- while _tb and _tb.tb_frame.f_code.co_filename not in ('<string>', '<pyfrilet>'):
1353
+ while _tb and not _tb.tb_frame.f_code.co_filename.startswith(('sketch_', 'programme_')):
1327
1354
  _tb = _tb.tb_next
1328
1355
  if _tb: _e.__traceback__ = _tb
1329
- _pf_rich_console.print_exception(show_locals=False)
1356
+ _pf_rich_console.print_exception(extra_lines=8, show_locals=True)
1330
1357
  from js import _pfShowErrorTerminal
1331
1358
  _pfShowErrorTerminal()
1332
1359
 
@@ -1339,16 +1366,35 @@ def _pf_safe_proxy(fn):
1339
1366
  setattr(m, 'safe_proxy', _pf_safe_proxy)
1340
1367
  _p5_functions.add('safe_proxy')
1341
1368
 
1369
+ def _pf_persist():
1370
+ """Synchronise /persist vers IndexedDB (fire-and-forget).
1371
+ Fonctionne en mode p5 (synchrone) et en mode terminal."""
1372
+ from js import _pfSyncIdbfs
1373
+ _pfSyncIdbfs() # Promise — le navigateur l'exécute dès que la stack JS se libère
1374
+
1375
+ setattr(m, 'persist', _pf_persist)
1376
+ _p5_functions.add('persist')
1377
+ persist = _pf_persist # accessible aussi hors p5 (mode terminal sans import p5)
1378
+
1379
+ import linecache as _linecache
1380
+ _pf_run_counter = [0]
1381
+
1342
1382
  def _pf_exec_user_code():
1343
1383
  _ns = {}
1384
+ _pf_run_counter[0] += 1
1385
+ _pf_fname = f'sketch_{_pf_run_counter[0]}'
1386
+ with open(_pf_fname, 'w') as _f:
1387
+ _f.write(_USER_CODE)
1388
+ lines = _USER_CODE.splitlines(keepends=True)
1389
+ _linecache.cache[_pf_fname] = (len(_USER_CODE), None, lines, _pf_fname)
1344
1390
  try:
1345
- exec(compile(_USER_CODE, '<string>', 'exec'), _ns, _ns)
1391
+ exec(compile(_USER_CODE, _pf_fname, 'exec'), _ns, _ns)
1346
1392
  except Exception as _e:
1347
1393
  _tb = _e.__traceback__
1348
- while _tb and _tb.tb_frame.f_code.co_filename != '<string>':
1394
+ while _tb and _tb.tb_frame.f_code.co_filename != _pf_fname:
1349
1395
  _tb = _tb.tb_next
1350
1396
  if _tb: _e.__traceback__ = _tb
1351
- _pf_rich_console.print_exception(show_locals=False)
1397
+ _pf_rich_console.print_exception(extra_lines=8, show_locals=True)
1352
1398
  from js import _pfShowErrorTerminal
1353
1399
  _pfShowErrorTerminal()
1354
1400
  return None
@@ -1426,6 +1472,9 @@ from js import _pfTermWrite, _pfTermWriteErr
1426
1472
  _sys.stdout = _PfStream(_pfTermWrite)
1427
1473
  _sys.stderr = _PfStream(_pfTermWriteErr)
1428
1474
 
1475
+ from rich.console import Console as _RichConsole
1476
+ _pf_rich_console = _RichConsole(stderr=True)
1477
+
1429
1478
  async def _pf_async_input(prompt=""):
1430
1479
  from js import _pfTerminalInput
1431
1480
  result = await _pfTerminalInput(str(prompt) if prompt else "")
@@ -1445,20 +1494,25 @@ async def _pf_run_terminal(source):
1445
1494
  wrapper = _ast.parse("async def programme(): pass")
1446
1495
  wrapper.body[0].body = tree.body if tree.body else [_ast.Pass()]
1447
1496
  _ast.fix_missing_locations(wrapper)
1448
-
1449
- _ns = {'input': _pf_async_input}
1450
- exec(compile(wrapper, '<pyfrilet>', 'exec'), _ns)
1497
+ _pf_run_counter[0] += 1
1498
+ _pf_fname = f'programme_{_pf_run_counter[0]}'
1499
+ with open(_pf_fname, 'w') as _f:
1500
+ _f.write(source)
1501
+ lines = source.splitlines(keepends=True)
1502
+ _linecache.cache[_pf_fname] = (len(source), None, lines, _pf_fname)
1503
+ _ns = {'input': _pf_async_input, 'persist': _pf_persist}
1504
+ exec(compile(wrapper, _pf_fname, 'exec'), _ns)
1451
1505
  try:
1452
1506
  await _ns['programme']()
1453
- except SystemExit:
1507
+ except (SystemExit, KeyboardInterrupt):
1454
1508
  pass
1455
1509
  except Exception as _e:
1456
1510
  _tb = _e.__traceback__
1457
- while _tb and _tb.tb_frame.f_code.co_filename != '<pyfrilet>':
1511
+ while _tb and _tb.tb_frame.f_code.co_filename != _pf_fname:
1458
1512
  _tb = _tb.tb_next
1459
1513
  if _tb:
1460
1514
  _e.__traceback__ = _tb
1461
- _pf_rich_console.print_exception(show_locals=False)
1515
+ _pf_rich_console.print_exception(extra_lines=8, show_locals=True)
1462
1516
  `);
1463
1517
 
1464
1518
  /* Inject p5 symbols into ACE autocomplete */
@@ -1521,6 +1575,8 @@ async def _pf_run_terminal(source):
1521
1575
  termClear();
1522
1576
  clearError();
1523
1577
 
1578
+ await window._pfMountIdbfs('/persist');
1579
+
1524
1580
  try {
1525
1581
  const runner = pyodide.globals.get('_pf_run_terminal');
1526
1582
  await runner(code);
@@ -1536,12 +1592,13 @@ async def _pf_run_terminal(source):
1536
1592
  async function runCode() {
1537
1593
  if (running) {
1538
1594
  if (_terminalRunning) {
1539
- /* Terminal en cours (ex: input() en attente) — annuler et relancer */
1540
- hideTerminal(); /* résout le _pfTerminalInput en attente avec '' */
1595
+ /* Terminal en cours envoyer KeyboardInterrupt + résoudre input() en attente */
1596
+ window._pfInterrupt && window._pfInterrupt();
1597
+ hideTerminal(); /* résout le _pfTerminalInput en attente avec null */
1541
1598
  running = false;
1542
1599
  btnRun.classList.remove('pf-running');
1543
- /* Laisser la micro-tâche Python se terminer avant de relancer */
1544
- await new Promise(r => setTimeout(r, 30));
1600
+ /* Laisser Python recevoir et traiter le KeyboardInterrupt */
1601
+ await new Promise(r => setTimeout(r, 80));
1545
1602
  } else {
1546
1603
  return; /* sketch p5 en cours — ignorer */
1547
1604
  }
@@ -1593,6 +1650,8 @@ async def _pf_run_terminal(source):
1593
1650
  }
1594
1651
  hideTerminal(); /* make sure terminal hidden for p5 mode */
1595
1652
 
1653
+ await window._pfMountIdbfs('/persist');
1654
+
1596
1655
  pyodide.globals.set('_USER_CODE', code);
1597
1656
  const pfExecUser = pyodide.globals.get('_pf_exec_user_code');
1598
1657
 
@@ -1713,7 +1772,7 @@ async def _pf_run_terminal(source):
1713
1772
  }
1714
1773
 
1715
1774
  /* ─────────────────── DOWNLOAD ───────────────── */
1716
- const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.6.2/pyfrilet.min.js';
1775
+ const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.6.3/pyfrilet.min.js';
1717
1776
 
1718
1777
  const STANDALONE_TEMPLATE = `<!doctype html>
1719
1778
  <html lang="fr">
@@ -2061,7 +2120,7 @@ FILLME-SCRIPTS
2061
2120
 
2062
2121
  /* ── Clear ── */
2063
2122
  function termClear() {
2064
- if (_xterm) { _xterm.clear(); }
2123
+ if (_xterm) { _xterm.reset(); } /* reset() efface tout y compris le scrollback et repositionne le curseur en (0,0) */
2065
2124
  _inputResolve = null;
2066
2125
  _lineBuffer = '';
2067
2126
  }
@@ -2107,7 +2166,7 @@ FILLME-SCRIPTS
2107
2166
  const res = _inputResolve;
2108
2167
  _inputResolve = null;
2109
2168
  _lineBuffer = '';
2110
- res('');
2169
+ res(null); /* null → None en Python → propage l'annulation proprement */
2111
2170
  }
2112
2171
  }
2113
2172
 
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",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",m="https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js",f="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/* xterm interne : prendre toute la hauteur */\n#pf-xterm .xterm {\n height: 100%;\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>\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 v=e||w[0],x=(v.getAttribute("data-sources")||v.getAttribute("sources")||"cdn").toLowerCase().trim(),k=(v.getAttribute("data-vendor")||v.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/");n="cdn"===x;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?m:null,mermaid:E?f: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",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}}),L=e=>{try{return localStorage.getItem(e)}catch(e){return null}};let z;const R=L(S);let I=null;if(R)try{I=JSON.parse(R)}catch(e){I=null}if(I&&1===I.v&&Array.isArray(I.tabs)&&I.tabs.length>0){const e=e=>`${e.label}|${e.type}|${e.hidden?1:0}|${e.readonly?1:0}`;I.tabs.map(e).join(",")!==j.map(e).join(",")&&(I._stale=!0)}const T=!(!I||!I._stale);z=I&&1===I.v&&Array.isArray(I.tabs)&&I.tabs.length>0?I.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 a=L(S+":"+t);if(a||"Code"!==e.label||1!==j.length||(a=L(S)),a&&a.trim())return{...e,code:a}}return e});const A=v.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"),m=document.getElementById("pf-sketch"),f=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"),v=document.getElementById("pf-btn-dl"),x=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 z=!1,R=Math.round(.56*window.innerHeight);function I(){document.documentElement.style.setProperty("--pf-drawer-h",R+"px")}function T(){z=!0,c.classList.add("pf-open"),w.classList.add("pf-active"),setTimeout(()=>{G(),q&&q.focus()},280)}function A(){z=!1,c.classList.remove("pf-open"),w.classList.remove("pf-active"),setTimeout(()=>{G();const e=V._p?.canvas;e&&e.removeAttribute("tabindex"),l.focus()},280)}function P(){z?A():T()}I();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:z?R:0,moved:!1},W.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function N(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"):(R=a,I(),z||T(),c.style.transition="none",c.style.height=R+"px"),G()}function D(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,W.style.display="none",document.body.style.userSelect="",c.style.transition="",c.style.height="",n&&(r<O?A():(R=Math.max(O,Math.min(window.innerHeight-50,r)),I(),z||T()),G())}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(),P()}),p.addEventListener("mousedown",F,!0),document.addEventListener("mousemove",N),document.addEventListener("mouseup",D),p.addEventListener("touchstart",F,{passive:!1}),document.addEventListener("touchmove",N,{passive:!0}),document.addEventListener("touchend",D);let U=0,K=0;function $(e){h.textContent=e,h.style.display="block",T()}function H(){h.textContent="",h.style.display="none"}function Y(){if(!V._p||"fit"!==V._mode)return;const e=V._w,n=V._h;if(!e||!n)return;const t=l.clientWidth,a=l.clientHeight,r=Math.min(t/e,a/n);f.style.transform=`scale(${r})`}function G(){if("fullscreen"===V._mode?V.size("max"):Y(),J&&"function"==typeof J.windowResized)try{J.windowResized()}catch(e){$(String(e))}q&&q.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=V._p?V._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=V._w/n.width,a=V._h/n.height;return[(U-n.left)*t,(K-n.top)*a]},window.addEventListener("resize",G);let J=null;const V=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),f.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 X(){if(Fe(),J){try{J.remove()}catch(e){}J=null}m.innerHTML="",V._p=null,V._mode="fit",V._w=0,V._h=0,f.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),ve&&(ve.destroy(),ve=null),xe&&(xe.destroy(),xe=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),ze&&(ze.destroy(),ze=null)}window.p5py=V;let q=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",q&&Q[e.id]&&(q.setSession(Q[e.id]),q.setReadOnly(e.readonly),q.focus())}function ae(){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 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);q&&n&&Q[n.id]&&(q.setSession(Q[n.id]),q.setReadOnly(n.readonly),q.renderer.updateFull(!0)),ae()}function oe(){!r.ace.startsWith("vendor")&&r.ace.startsWith("http")||ace.config.set("basePath",r.ace.replace(/\/[^/]+$/,"/")),q=ace.edit("pf-ace"),q.setTheme("ace/theme/monokai"),q.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),q.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{q.completer?.popup?.isOpen||Ae()}}),q.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:A}),q.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:se}),q.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 me(){return pe||(pe=(async()=>{const e={};if(r.pyodideIndex&&(e.indexURL=r.pyodideIndex),ce=await loadPyodide(e),await ce.loadPackage(["rich","pygments"]),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\nfrom rich.console import Console as _RichConsole\n_pf_rich_console = _RichConsole(stderr=True)\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 _tb.tb_frame.f_code.co_filename not in ('<string>', '<pyfrilet>'):\n _tb = _tb.tb_next\n if _tb: _e.__traceback__ = _tb\n _pf_rich_console.print_exception(show_locals=False)\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_exec_user_code():\n _ns = {}\n try:\n exec(compile(_USER_CODE, '<string>', 'exec'), _ns, _ns)\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and _tb.tb_frame.f_code.co_filename != '<string>':\n _tb = _tb.tb_next\n if _tb: _e.__traceback__ = _tb\n _pf_rich_console.print_exception(show_locals=False)\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\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\n _ns = {'input': _pf_async_input}\n exec(compile(wrapper, '<pyfrilet>', 'exec'), _ns)\n try:\n await _ns['programme']()\n except SystemExit:\n pass\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and _tb.tb_frame.f_code.co_filename != '<pyfrilet>':\n _tb = _tb.tb_next\n if _tb:\n _e.__traceback__ = _tb\n _pf_rich_console.print_exception(show_locals=False)\n"),q){fe(ce.runPython("list(m.__all__)").toJs())}})(),pe)}function fe(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,q.completers=[...q.completers||[],t]}let ue=!1,_e=!1,he=null,ye=null,be=null,ge=null,we=null,ve=null,xe=null,ke=null,Ee=null,Ce=null,Se=null,je=null,Le=null,ze=null;const Re=300;function Ie(e){return!/\bfrom\s+p5\s+import\b|\bimport\s+p5\b/.test(e)}async function Te(e){_e=!0,qe(),Xe(),H();try{const n=ce.globals.get("_pf_run_terminal");await n(e)}catch(e){const n=String(e);n.includes("SystemExit")||Ve(n+"\n")}finally{_e=!1}}async function Ae(){if(ue){if(!_e)return;Qe(),ue=!1,y.classList.remove("pf-running"),await new Promise(e=>setTimeout(e,30))}ue=!0,y.classList.add("pf-running"),H(),Xe(),X(),ce||(_.textContent="Initialisation de Pyodide…",u.style.display="flex");try{await me()}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",Ie(t))return y.classList.remove("pf-running"),await Te(t),void(ue=!1);Qe(),ce.globals.set("_USER_CODE",t);const a=ce.globals.get("_pf_exec_user_code");try{if(!a())return ue=!1,void y.classList.remove("pf-running");ce.runPython("_ns = _ns_ref[0]")}catch(e){return Ze(e.message||String(e)),ue=!1,void y.classList.remove("pf-running")}let r,i,s,d,l,c,p,f,h,b,g,w,v,x;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"),f=e("mouseMoved","mouse_moved"),h=e("mouseWheel","mouse_wheel"),b=e("doubleClicked","double_clicked"),g=e("keyReleased","key_released"),w=e("touchStarted","touch_started"),v=e("touchMoved","touch_moved"),x=e("touchEnded","touch_ended")}catch(e){return Ze(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"),z=e=>e?k(()=>{try{C(j),L(e)}catch(e){Ze("")}}):null;be=l?k(()=>{try{L(l)}catch(e){Ze("")}}):null,he=r?k(()=>{try{L(r)}catch(e){Ze("")}}):null,ye=k(()=>{try{C(j),S(i,Re)}catch(e){X(),Ze("")}}),ge=z(s),we=z(p),ve=z(c),xe=z(f),ke=z(h),Ee=z(b),Ce=z(d),Se=z(g),je=z(w),Le=z(v),ze=z(x);const R=E?k(()=>{try{L(E)}catch(e){Ze("")}}):null;let I=!1;J=new p5(e=>{V._setP(e),be&&(e.preload=()=>{be()}),e.setup=()=>{he&&he(),e.canvas||V.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),I=!0},e.draw=()=>{I&&ye()},e.mousePressed=()=>{I&&ge&&ge()},e.mouseReleased=()=>{I&&we&&we()},e.mouseDragged=()=>{I&&ve&&ve()},e.mouseMoved=()=>{I&&xe&&xe()},e.mouseWheel=e=>{I&&ke&&ke()},e.doubleClicked=()=>{I&&Ee&&Ee()},e.keyPressed=()=>{I&&Ce&&Ce()},e.keyReleased=()=>{I&&Se&&Se()},je&&(e.touchStarted=()=>{I&&je()}),Le&&(e.touchMoved=()=>{I&&Le()}),ze&&(e.touchEnded=()=>{I&&ze()}),e.windowResized=()=>{"fullscreen"===V._mode?V.size("max"):Y(),R&&R()}},m),ue=!1,y.classList.remove("pf-running")}const Pe='<!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.2/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 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=Pe.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 Be=null,Oe=[];function We(){const e=V._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),a=n.includes("webm")?"webm":"mp4";Object.assign(document.createElement("a"),{href:t,download:`sketch.${a}`}).click(),URL.revokeObjectURL(t),x.textContent="⏺",x.title="Enregistrer WebM",x.classList.remove("pf-recording"),Be=null},Be.start(),x.textContent="⏹",x.title="Arrêter l'enregistrement",x.classList.add("pf-recording")}function Fe(){Be&&"inactive"!==Be.state&&Be.stop()}x.addEventListener("click",()=>{Be?Fe():We()}),y.addEventListener("click",()=>Ae()),w.addEventListener("click",()=>{z?A():(R=window.innerHeight-32,I(),T())}),v.addEventListener("click",Me);const Ne="https://codeberg.org/nopid/pyfrilet";function De(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=z&&q&&q.isFocused&&q.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(),q.searchBox?q.searchBox.hide():t.style.display="none",void q.focus();if(n){const n=q.completer?.popup?.isOpen;if(n)return;return e.preventDefault(),e.stopPropagation(),void A()}return e.preventDefault(),e.stopPropagation(),void(z?A():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 De(r.p5),r.marked){const e=document.createElement("link");e.rel="stylesheet",e.href=r.katexCss,document.head.appendChild(e),await De(r.marked),await De(r.katex),await De(r.markedKatex),await De(r.mermaid),marked.use(markedKatex({throwOnError:!1})),mermaid.initialize({startOnLoad:!1,theme:"neutral"})}await De(r.ace),await De(r.acePython),await De(r.aceMonokai),await De(r.aceLangTools),await De(r.aceSearchbox),await De(r.pyodide);const e=document.createElement("link");e.rel="stylesheet",e.href=r.xtermCss,document.head.appendChild(e),await De(r.xterm),await De(r.xtermFit),await De(r.xtermUni)}catch(e){return _.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}oe(),await Ae(),u.style.display="none"})();const Ue=document.getElementById("pf-xterm");let Ke=null,$e=null,He=null,Ye="";function Ge(){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(Ue),$e.fit(),new ResizeObserver(()=>{Ke&&"none"!==Ue.style.display&&$e.fit()}).observe(Ue),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))})}function Je(e){Ke&&Ke.write(e)}function Ve(e){Ge(),Ke.write(""),Ke.write(e.replace(/\n/g,"\r\n")),Ke.write("")}function Xe(){Ke&&Ke.clear(),He=null,Ye=""}function qe(){f.style.display="none",Ue.style.display="block",Ue.classList.remove("pf-xterm-overlay"),Ge(),$e.fit(),Ke.focus()}function Ze(e){Ue.style.display="block",Ue.classList.add("pf-xterm-overlay"),Ge(),$e.fit(),e&&(Xe(),Ke.write("── Erreur ──────────────────────────────────────\r\n\r\n"),Ke.write(e.replace(/\n/g,"\r\n")+"\r\n"))}function Qe(){if(Ue.style.display="none",Ue.classList.remove("pf-xterm-overlay"),f.style.display="",He){const e=He;He=null,Ye="",e("")}}window._pfTerminalInput=function(e){return new Promise(n=>{He=n,Ye="",e&&Ke.write(e),Ke.focus()})},window._pfTermWrite=Je,window._pfTermWriteErr=Ve,window._pfShowWatchdogError=e=>{X(),$(`draw() a dépassé ${e}ms — sketch arrêté (watchdog).`)},window._pfShowErrorTerminal=()=>{X(),Ze("")}}(z,j,S,C,A,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",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/* xterm interne : prendre toute la hauteur */\n#pf-xterm .xterm {\n height: 100%;\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>\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 v=e||w[0],x=(v.getAttribute("data-sources")||v.getAttribute("sources")||"cdn").toLowerCase().trim(),k=(v.getAttribute("data-vendor")||v.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/");n="cdn"===x;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 A=v.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"),v=document.getElementById("pf-btn-dl"),x=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 A(){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 P(){I?A():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?A():(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(),P()}),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),ve&&(ve.destroy(),ve=null),xe&&(xe.destroy(),xe=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||Ae()}}),X.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:A}),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(),Ae()}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,ve=null,xe=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,Xe(),Ve(),H(),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")||Ge(n+"\n")}finally{_e=!1}}async function Ae(){if(ue){if(!_e)return;window._pfInterrupt&&window._pfInterrupt(),Qe(),ue=!1,y.classList.remove("pf-running"),await new Promise(e=>setTimeout(e,80))}ue=!0,y.classList.add("pf-running"),H(),Ve(),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);Qe(),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 Ze(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,v,x;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"),v=e("touchMoved","touch_moved"),x=e("touchEnded","touch_ended")}catch(e){return Ze(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){Ze("")}}):null;be=l?k(()=>{try{L(l)}catch(e){Ze("")}}):null,he=a?k(()=>{try{L(a)}catch(e){Ze("")}}):null,ye=k(()=>{try{C(j),S(i,ze)}catch(e){V(),Ze("")}}),ge=I(s),we=I(p),ve=I(c),xe=I(m),ke=I(h),Ee=I(b),Ce=I(d),Se=I(g),je=I(w),Le=I(v),Ie=I(x);const z=E?k(()=>{try{L(E)}catch(e){Ze("")}}):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&&ve&&ve()},e.mouseMoved=()=>{R&&xe&&xe()},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 Pe='<!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.3/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=Pe.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),x.textContent="⏺",x.title="Enregistrer WebM",x.classList.remove("pf-recording"),Be=null},Be.start(),x.textContent="⏹",x.title="Arrêter l'enregistrement",x.classList.add("pf-recording")}function Fe(){Be&&"inactive"!==Be.state&&Be.stop()}x.addEventListener("click",()=>{Be?Fe():We()}),y.addEventListener("click",()=>Ae()),w.addEventListener("click",()=>{I?A():(z=window.innerHeight-32,R(),T())}),v.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 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 A()}return e.preventDefault(),e.stopPropagation(),void(I?A():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 Ae(),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))})}function qe(e){Ke&&Ke.write(e)}function Ge(e){Je(),Ke.write(""),Ke.write(e.replace(/\n/g,"\r\n")),Ke.write("")}function Ve(){Ke&&Ke.reset(),He=null,Ye=""}function Xe(){m.style.display="none",Ne.style.display="block",Ne.classList.remove("pf-xterm-overlay"),Je(),$e.fit(),Ke.focus()}function Ze(e){Ne.style.display="block",Ne.classList.add("pf-xterm-overlay"),Je(),$e.fit(),e&&(Ve(),Ke.write("── Erreur ──────────────────────────────────────\r\n\r\n"),Ke.write(e.replace(/\n/g,"\r\n")+"\r\n"))}function Qe(){if(Ne.style.display="none",Ne.classList.remove("pf-xterm-overlay"),m.style.display="",He){const e=He;He=null,Ye="",e(null)}}window._pfTerminalInput=function(e){return new Promise(n=>{He=n,Ye="",e&&Ke.write(e),Ke.focus()})},window._pfTermWrite=qe,window._pfTermWriteErr=Ge,window._pfShowWatchdogError=e=>{V(),$(`draw() a dépassé ${e}ms — sketch arrêté (watchdog).`)},window._pfShowErrorTerminal=()=>{V(),Ze("")}}(I,j,S,C,A,T)})}();