pyfrilet 0.6.6 → 0.7.1

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
@@ -388,6 +388,136 @@ Le bouton ▶ (ou `Shift+Entrée`) interrompt le programme en cours et relance l
388
388
 
389
389
  Cliquer plusieurs fois rapidement sur ▶ est sans danger : seul le **dernier** clic produit une exécution, les intermédiaires sont ignorés.
390
390
 
391
+ ---
392
+
393
+ ## Mode sans éditeur
394
+
395
+ Deux attributs permettent d'intégrer pyfrilet dans un contexte où l'éditeur de code n'est pas souhaité.
396
+
397
+ ### `data-no-editor`
398
+
399
+ Cache entièrement le tiroir éditeur. Le sketch tourne en pleine page, sans interface visible. Les erreurs s'affichent en overlay semi-transparent au bas du canvas plutôt que dans le tiroir.
400
+
401
+ ```html
402
+ <script src="pyfrilet.js" data-no-editor></script>
403
+ ```
404
+
405
+ `Shift+Entrée` reste actif pour relancer le sketch. Tous les autres raccourcis clavier (Échap, Ctrl+S, Ctrl+R) sont désactivés pour ne pas interférer avec la page hôte.
406
+
407
+ ### `data-pyfrilet-detached`
408
+
409
+ Mode de rendu découplé, conçu pour intégrer un sketch dans une page qui a sa propre mise en page (présentation RevealJS, article interactif…). Implique `data-no-editor`.
410
+
411
+ ```html
412
+ <script src="pyfrilet.js" data-pyfrilet-detached></script>
413
+ ```
414
+
415
+ Différences avec le mode normal :
416
+
417
+ - Le markup pyfrilet est **ajouté** à `document.body` au lieu de le remplacer — la page hôte est préservée.
418
+ - `#pf-sketch` (le conteneur du canvas) est rendu hors-écran et exposé sous `window.__pyfriletSketchEl` pour qu'un plugin externe puisse le déplacer dans n'importe quel élément de la page.
419
+ - Le **localStorage est ignoré** — le code Python est toujours lu depuis le HTML source. Dans un contexte intégré, la page est la source de vérité.
420
+
421
+ ---
422
+
423
+ ## Intégration RevealJS (`reveal-pyfrilet.js`)
424
+
425
+ `reveal-pyfrilet.js` est un plugin RevealJS qui connecte un sketch pyfrilet à une présentation. Le canvas devient un élément comme les autres dans les slides — RevealJS gère entièrement le layout.
426
+
427
+ ### Principe
428
+
429
+ Le plugin crée un conteneur hors-écran (le « garage ») au démarrage. À chaque changement de slide, il déplace `#pf-sketch` dans l'élément `[data-pyfrilet-canvas]` de la slide active, et appelle `window.pyfrilet_on_step(n)` avec le step déclaré sur la section. Le canvas est ainsi partagé entre toutes les slides — l'état Python persiste d'une slide à l'autre.
430
+
431
+ ### Démarrage rapide
432
+
433
+ **1. Déclarer le sketch hors des slides :**
434
+
435
+ ```html
436
+ <script src="pyfrilet.js" data-pyfrilet-detached></script>
437
+
438
+ <script type="text/python">
439
+ from p5 import *
440
+ import js
441
+
442
+ step = 0
443
+
444
+ def setup():
445
+ size(540, 380)
446
+ frame_rate(30)
447
+
448
+ def draw():
449
+ background(20)
450
+ # utilise la variable `step` pour changer d'état visuel
451
+
452
+ def on_step(n, payload=None):
453
+ global step
454
+ step = int(n)
455
+
456
+ js.window.pyfrilet_on_step = on_step
457
+ </script>
458
+ ```
459
+
460
+ **2. Placer `[data-pyfrilet-canvas]` dans les slides qui doivent afficher le canvas :**
461
+
462
+ ```html
463
+ <section data-pyfrilet-step="1">
464
+ <div class="slide-split">
465
+ <div class="text">
466
+ <h2>Mon titre</h2>
467
+ <p>Mon texte...</p>
468
+ </div>
469
+ <div data-pyfrilet-canvas></div>
470
+ </div>
471
+ </section>
472
+ ```
473
+
474
+ Les slides sans `[data-pyfrilet-canvas]` n'affichent pas le canvas — le sketch continue de tourner en arrière-plan.
475
+
476
+ **3. Initialiser RevealJS avec le plugin :**
477
+
478
+ ```html
479
+ <script src="reveal.js"></script>
480
+ <script src="reveal-pyfrilet.js"></script>
481
+ <script>
482
+ Reveal.initialize({
483
+ plugins: [ RevealPyfrilet ],
484
+ });
485
+ </script>
486
+ ```
487
+
488
+ ### Attributs
489
+
490
+ | Attribut | Emplacement | Rôle |
491
+ |---|---|---|
492
+ | `data-pyfrilet-step="n"` | `<section>` | Step envoyé à `on_step(n)` à l'activation de la slide |
493
+ | `data-pyfrilet-step="n"` | `.fragment` | Step envoyé à `on_step(n)` à l'apparition du fragment |
494
+ | `data-pyfrilet-canvas` | n'importe quel élément dans la slide | Emplacement où le canvas est injecté |
495
+
496
+ ### Dimensionnement du canvas
497
+
498
+ Le plugin injecte `max-width:100%; max-height:100%; width:auto; height:auto` sur le canvas. Le slot `[data-pyfrilet-canvas]` doit donc être un conteneur dont la largeur **et** la hauteur sont définies — le canvas s'y inscrit en conservant son ratio (défini par `size(W, H)` en Python), exactement comme `object-fit:contain`. La mise en page à l'intérieur des slides est entièrement libre et ne dépend pas du plugin.
499
+
500
+ ### Relancer le sketch en cours de présentation
501
+
502
+ `Shift+Entrée` relance le sketch (Python repart de zéro). Le plugin écoute l'événement `pyfrilet:ready` déclenché à la fin du re-run et ré-envoie automatiquement le step de la slide courante — le sketch restaure son état visuel sans intervention.
503
+
504
+ ### Hook Python
505
+
506
+ Le seul contrat entre le sketch et le plugin est l'exposition d'un callable JS :
507
+
508
+ ```python
509
+ import js
510
+
511
+ def on_step(n, payload=None):
512
+ global step
513
+ step = int(n)
514
+
515
+ js.window.pyfrilet_on_step = on_step
516
+ ```
517
+
518
+ `payload` est toujours `None` dans cette version — réservé pour des données structurées futures.
519
+
520
+
391
521
  ---
392
522
 
393
523
  ## Interface utilisateur
@@ -552,13 +682,25 @@ Par défaut, pyfrilet charge p5.js, Pyodide et ACE depuis des CDN publics. Pour
552
682
 
553
683
  ### Configuration
554
684
 
555
- `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">`.
685
+ Ces attributs se placent de préférence sur la balise `<script src="pyfrilet.js">` elle-même. Pour la rétrocompatibilité, `data-sources` et `data-vendor` sont également acceptés sur le premier bloc `<script type="text/python">`.
686
+
687
+ | Attribut | Valeurs | Description |
688
+ |---|---|---|
689
+ | `data-sources` | `cdn` (défaut) / `local` | Source des dépendances JS (CDN ou dossier `vendor/`) |
690
+ | `data-vendor` | chemin | Chemin vers le dossier vendor, relatif à la page HTML (défaut : `vendor/`) |
691
+ | `data-no-watchdog` | — | Désactive le watchdog `draw()` (calculs intensifs légitimes) |
692
+ | `data-no-editor` | — | Cache le tiroir éditeur — canvas uniquement, erreurs en overlay |
693
+ | `data-pyfrilet-detached` | — | Mode découplé pour intégration dans une page hôte (implique `data-no-editor`) |
694
+
556
695
  ```html
557
- <!-- Recommandé -->
558
- <script src="pyfrilet.js" data-sources="local" data-vendor="vendor/"></script>
696
+ <!-- CDN, éditeur visible (comportement par défaut) -->
697
+ <script src="pyfrilet.js"></script>
698
+
699
+ <!-- Local, sans éditeur -->
700
+ <script src="pyfrilet.js" data-sources="local" data-vendor="vendor/" data-no-editor></script>
559
701
 
560
- <!-- Avec watchdog désactivé (calculs intensifs) -->
561
- <script src="pyfrilet.js" data-sources="local" data-vendor="vendor/" data-no-watchdog></script>
702
+ <!-- Intégration RevealJS -->
703
+ <script src="pyfrilet.js" data-pyfrilet-detached></script>
562
704
 
563
705
  <!-- Rétrocompat (ancienne syntaxe) -->
564
706
  <script src="pyfrilet.js"></script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyfrilet",
3
- "version": "0.6.6",
3
+ "version": "0.7.1",
4
4
  "type": "module",
5
5
  "main": "pyfrilet.js",
6
6
  "files": [
package/pyfrilet.js CHANGED
@@ -9,8 +9,15 @@
9
9
  * def draw(): background(0)
10
10
  * </script>
11
11
  *
12
- * data-sources : "local" (default) | "cdn"
13
- * data-vendor : path to local vendor folder, default "vendor/"
12
+ * data-sources : "local" (default) | "cdn"
13
+ * data-vendor : path to local vendor folder, default "vendor/"
14
+ * data-no-editor : hide the slide-up editor entirely (canvas-only mode)
15
+ * data-pyfrilet-detached : do NOT replace document.body — append the sketch
16
+ * container as a hidden off-screen element instead.
17
+ * Intended for use with the reveal-pyfrilet.js plugin, which
18
+ * moves window.__pyfriletSketchEl into a visible panel and
19
+ * drives the sketch via window.pyfrilet_on_step(n).
20
+ * Implies data-no-editor.
14
21
  *
15
22
  * Keyboard shortcuts:
16
23
  * Shift+Enter → run code (also closes editor)
@@ -440,6 +447,52 @@ const STYLES = `html, body {
440
447
  #pf-xterm .xterm-screen {
441
448
  height: 100% !important;
442
449
  }
450
+
451
+ /* ═══════════════════════════════════════════════════════
452
+ data-no-editor : canvas-only mode — no slide-up editor
453
+ ═══════════════════════════════════════════════════════ */
454
+ #pf-root.pf-no-editor #pf-drawer {
455
+ display: none;
456
+ }
457
+ /* Errors shown as floating overlay at the bottom of the canvas */
458
+ #pf-root.pf-no-editor #pf-err {
459
+ display: none;
460
+ position: absolute;
461
+ bottom: 0; left: 0; right: 0;
462
+ max-height: 40%;
463
+ z-index: 20;
464
+ border-top: 1px solid rgba(247, 118, 142, .45);
465
+ }
466
+ #pf-root.pf-no-editor #pf-err.pf-err-visible {
467
+ display: block;
468
+ }
469
+
470
+ /* ═══════════════════════════════════════════════════════
471
+ data-pyfrilet-detached : off-screen render container.
472
+ reveal-pyfrilet.js moves #pf-sketch into a visible panel
473
+ and drives the sketch via window.pyfrilet_on_step(n).
474
+ ═══════════════════════════════════════════════════════ */
475
+ #pf-root.pf-detached {
476
+ position: fixed !important;
477
+ left: -10000px !important;
478
+ top: 0 !important;
479
+ width: var(--pf-detached-w, 560px) !important;
480
+ height: var(--pf-detached-h, 420px) !important;
481
+ pointer-events: none;
482
+ box-shadow: none !important;
483
+ z-index: -1 !important;
484
+ }
485
+ /* Once #pf-sketch is moved into a host slot, it must be a transparent
486
+ pass-through: no background, no intrinsic size of its own.
487
+ display:contents makes it invisible as a box — the canvas becomes a
488
+ direct child of whatever container holds #pf-sketch. */
489
+ #pf-root.pf-detached #pf-sketch {
490
+ display: contents;
491
+ }
492
+ /* Canvas pointer-events re-enabled once #pf-sketch is moved to the host panel */
493
+ #pf-sketch {
494
+ pointer-events: auto;
495
+ }
443
496
  `;
444
497
 
445
498
  /* ═══════════════════════════ MARKUP ═════════════════════════════════ */
@@ -575,73 +628,99 @@ document.addEventListener('DOMContentLoaded', function () {
575
628
  ──────────────────────────────────────────────────────────────────── */
576
629
  const tryLS = (key) => { try { return localStorage.getItem(key); } catch (e) { return null; } };
577
630
 
578
- let tabs;
579
- const raw = tryLS(SK);
580
- let snap = null;
581
- if (raw) {
582
- try { snap = JSON.parse(raw); } catch (e) { snap = null; }
583
- }
631
+ /* In detached mode the sketch is embedded in a host page (e.g. RevealJS).
632
+ The host page IS the source of truth — localStorage edits made in a
633
+ standalone pyfrilet session must not silently override the sketch code
634
+ declared in the HTML. Always use htmlTabs directly. */
635
+ const detached = configTag.hasAttribute('data-pyfrilet-detached');
584
636
 
585
- if (snap && snap.v === 1 && Array.isArray(snap.tabs) && snap.tabs.length > 0) {
586
- /* Compare structural signature: label, type, hidden, readonly for each tab.
587
- If the HTML has changed since the snapshot was saved, mark as stale but
588
- still serve the old content — the user keeps their work until they
589
- explicitly confirm a Reset (which always deletes SK first). */
590
- const sig = t => `${t.label}|${t.type}|${t.hidden ? 1 : 0}|${t.readonly ? 1 : 0}`;
591
- const snapSig = snap.tabs.map(sig).join(',');
592
- const htmlSig = htmlTabs.map(sig).join(',');
593
- if (snapSig !== htmlSig) snap._stale = true;
594
- }
637
+ let tabs;
638
+ let snap = null; /* declared here so staleSnapshot can reference it below */
595
639
 
596
- const staleSnapshot = !!(snap && snap._stale);
597
-
598
- if (snap && snap.v === 1 && Array.isArray(snap.tabs) && snap.tabs.length > 0) {
599
- /* Restore structure and content from snapshot */
600
- tabs = snap.tabs.map((st, i) => {
601
- /* Find current htmlTab with same label+type to get up-to-date starterCode */
602
- const html = htmlTabs.find(h => h.label === st.label && h.type === st.type) || null;
603
- return {
604
- id : 'tab-' + i,
605
- label : st.label,
606
- hidden : st.hidden,
607
- readonly : st.readonly,
608
- type : st.type,
609
- starterCode : html ? html.starterCode : st.content, /* reset target = current file */
610
- code : st.content, /* working content = last visit */
611
- };
612
- });
640
+ if (detached) {
641
+ tabs = htmlTabs;
613
642
  } else {
614
- /* No snapshot: use htmlTabs with retro-compat for old per-tab keys */
615
- tabs = htmlTabs.map((tab, i) => {
616
- if (!tab.hidden && !tab.readonly && tab.type === 'python') {
617
- const safe = tab.label ? tab.label.replace(/[^a-zA-Z0-9]/g, '_') : String(i);
618
- let saved = tryLS(SK + ':' + safe);
619
- /* Retro-compat: single unnamed tab used bare SK as key */
620
- if (!saved && tab.label === 'Code' && htmlTabs.length === 1) saved = tryLS(SK);
621
- if (saved && saved.trim()) return { ...tab, code: saved };
622
- }
623
- return tab;
624
- });
625
- }
643
+ const raw = tryLS(SK);
644
+ if (raw) {
645
+ try { snap = JSON.parse(raw); } catch (e) { snap = null; }
646
+ }
647
+
648
+ if (snap && snap.v === 1 && Array.isArray(snap.tabs) && snap.tabs.length > 0) {
649
+ /* Compare structural signature: label, type, hidden, readonly for each tab.
650
+ If the HTML has changed since the snapshot was saved, mark as stale but
651
+ still serve the old content — the user keeps their work until they
652
+ explicitly confirm a Reset (which always deletes SK first). */
653
+ const sig = t => `${t.label}|${t.type}|${t.hidden ? 1 : 0}|${t.readonly ? 1 : 0}`;
654
+ const snapSig = snap.tabs.map(sig).join(',');
655
+ const htmlSig = htmlTabs.map(sig).join(',');
656
+ if (snapSig !== htmlSig) snap._stale = true;
657
+ }
658
+
659
+ if (snap && snap.v === 1 && Array.isArray(snap.tabs) && snap.tabs.length > 0) {
660
+ /* Restore structure and content from snapshot */
661
+ tabs = snap.tabs.map((st, i) => {
662
+ /* Find current htmlTab with same label+type to get up-to-date starterCode */
663
+ const html = htmlTabs.find(h => h.label === st.label && h.type === st.type) || null;
664
+ return {
665
+ id : 'tab-' + i,
666
+ label : st.label,
667
+ hidden : st.hidden,
668
+ readonly : st.readonly,
669
+ type : st.type,
670
+ starterCode : html ? html.starterCode : st.content, /* reset target = current file */
671
+ code : st.content, /* working content = last visit */
672
+ };
673
+ });
674
+ } else {
675
+ /* No snapshot: use htmlTabs with retro-compat for old per-tab keys */
676
+ tabs = htmlTabs.map((tab, i) => {
677
+ if (!tab.hidden && !tab.readonly && tab.type === 'python') {
678
+ const safe = tab.label ? tab.label.replace(/[^a-zA-Z0-9]/g, '_') : String(i);
679
+ let saved = tryLS(SK + ':' + safe);
680
+ /* Retro-compat: single unnamed tab used bare SK as key */
681
+ if (!saved && tab.label === 'Code' && htmlTabs.length === 1) saved = tryLS(SK);
682
+ if (saved && saved.trim()) return { ...tab, code: saved };
683
+ }
684
+ return tab;
685
+ });
686
+ }
687
+ } /* end !detached */
626
688
 
627
689
  const noWatchdog = configTag.hasAttribute('data-no-watchdog');
690
+ /* detached already declared above, before the localStorage block */
691
+ /* data-no-editor: hide the editor UI; always implied by detached */
692
+ const noEditor = detached || configTag.hasAttribute('data-no-editor');
693
+ /* staleSnapshot is only meaningful in non-detached mode */
694
+ const staleSnapshot = detached ? false : !!(snap && snap._stale);
628
695
 
629
- main(tabs, htmlTabs, SK, URLS, noWatchdog, staleSnapshot);
696
+ main(tabs, htmlTabs, SK, URLS, noWatchdog, staleSnapshot, noEditor, detached);
630
697
  });
631
698
 
632
699
  /* ═══════════════════════════ MAIN ═══════════════════════════════════ */
633
- function main(tabs, htmlTabs, SK, URLS, noWatchdog, staleSnapshot) {
700
+ function main(tabs, htmlTabs, SK, URLS, noWatchdog, staleSnapshot, noEditor, detached) {
634
701
 
635
702
  /* tabs = working state (from snapshot or HTML), may be reassigned on reset
636
703
  htmlTabs = ground truth from current HTML file, never mutated */
637
704
  tabs = tabs.slice(); /* local mutable copy */
638
705
  let _staleSnapshot = staleSnapshot; /* mutable — cleared after reset */
639
706
 
640
- /* ── inject styles + markup ── */
707
+ /* ── inject styles ── */
641
708
  const styleEl = document.createElement('style');
642
709
  styleEl.textContent = STYLES;
643
710
  document.head.appendChild(styleEl);
644
- document.body.innerHTML = MARKUP;
711
+
712
+ /* ── inject markup ──────────────────────────────────────────────────
713
+ Normal mode : replace body entirely (pyfrilet owns the page).
714
+ Detached mode : append to body instead — the existing page content
715
+ (e.g. a RevealJS presentation) must be preserved.
716
+ #pf-root is then positioned off-screen by CSS until
717
+ the reveal-pyfrilet plugin moves the canvas panel.
718
+ ──────────────────────────────────────────────────────────────────── */
719
+ if (detached) {
720
+ document.body.insertAdjacentHTML('beforeend', MARKUP);
721
+ } else {
722
+ document.body.innerHTML = MARKUP;
723
+ }
645
724
 
646
725
  /* ── element refs ── */
647
726
  const appEl = document.getElementById('pf-app');
@@ -662,6 +741,20 @@ function main(tabs, htmlTabs, SK, URLS, noWatchdog, staleSnapshot) {
662
741
  const hintEl = document.getElementById('pf-handle-hint');
663
742
  const tabsEl = document.getElementById('pf-tabs');
664
743
  const markdownEl = document.getElementById('pf-markdown-view');
744
+ const rootEl = document.getElementById('pf-root');
745
+
746
+ /* ── Apply mode classes ── */
747
+ if (noEditor) rootEl.classList.add('pf-no-editor');
748
+ if (detached) rootEl.classList.add('pf-detached');
749
+
750
+ /* ── Detached: expose sketch container for external plugins ── */
751
+ if (detached) {
752
+ /* The reveal-pyfrilet plugin (and any custom integration) can grab
753
+ window.__pyfriletSketchEl and move it into a visible panel.
754
+ Moving the element out of #pf-viewport disconnects the CSS scale
755
+ transform, which is correct: the host panel handles sizing. */
756
+ window.__pyfriletSketchEl = sketchEl;
757
+ }
665
758
 
666
759
  /* ─────────────────── DRAWER ─────────────────── */
667
760
  let drawerOpen = false;
@@ -805,11 +898,30 @@ function main(tabs, htmlTabs, SK, URLS, noWatchdog, staleSnapshot) {
805
898
  ];
806
899
  };
807
900
 
808
- function showError(txt) { errEl.textContent = txt; errEl.style.display = 'block'; openDrawer(); }
809
- function clearError() { errEl.textContent = ''; errEl.style.display = 'none'; }
901
+ function showError(txt) {
902
+ errEl.textContent = txt;
903
+ if (noEditor) {
904
+ /* Drawer is hidden — show error as a floating overlay on the canvas */
905
+ errEl.classList.add('pf-err-visible');
906
+ errEl.style.display = 'block';
907
+ } else {
908
+ errEl.style.display = 'block';
909
+ openDrawer();
910
+ }
911
+ }
912
+ function clearError() {
913
+ errEl.textContent = '';
914
+ errEl.style.display = 'none';
915
+ errEl.classList.remove('pf-err-visible');
916
+ }
810
917
 
811
918
  /* ─────────────────── CANVAS FIT ─────────────── */
812
919
  function fitCanvas() {
920
+ /* In detached mode the sketch element has been moved into an external
921
+ panel managed by reveal-pyfrilet (or a custom host). The host CSS
922
+ is responsible for display scaling; we must not apply a transform
923
+ to #pf-viewport which is now empty and off-screen. */
924
+ if (detached) return;
813
925
  if (!p5Bridge._p || p5Bridge._mode !== 'fit') return;
814
926
  const w = p5Bridge._w, h = p5Bridge._h;
815
927
  if (!w || !h) return;
@@ -1075,10 +1187,23 @@ function main(tabs, htmlTabs, SK, URLS, noWatchdog, staleSnapshot) {
1075
1187
  });
1076
1188
 
1077
1189
  /* Keyboard shortcuts (registered once) */
1190
+ /* Intercept Shift+Enter at the DOM capture phase, before ACE's completer
1191
+ popup gets a chance to accept the current suggestion. */
1192
+ document.getElementById('pf-ace').addEventListener('keydown', (e) => {
1193
+ if (e.shiftKey && e.key === 'Enter') {
1194
+ e.stopImmediatePropagation();
1195
+ e.preventDefault();
1196
+ if (aceInst.completer?.popup?.isOpen) {
1197
+ aceInst.completer.detach();
1198
+ } else {
1199
+ runCode();
1200
+ }
1201
+ }
1202
+ }, true /* capture */);
1078
1203
  aceInst.commands.addCommand({
1079
1204
  name: 'pfRun',
1080
1205
  bindKey: { win: 'Shift-Enter', mac: 'Shift-Enter' },
1081
- exec: () => { if (aceInst.completer?.popup?.isOpen) return; runCode(); },
1206
+ exec: () => runCode(),
1082
1207
  });
1083
1208
  aceInst.commands.addCommand({
1084
1209
  name: 'pfClose',
@@ -1660,7 +1785,10 @@ async def _pf_run_terminal(source):
1660
1785
  }));
1661
1786
  const completer = {
1662
1787
  getCompletions(editor, session, pos, prefix, callback) {
1663
- callback(null, prefix.length > 0 ? completions : []);
1788
+ /* Only suggest if the prefix contains at least one letter
1789
+ avoids completing numeric constants when the user types a digit */
1790
+ const hasLetter = prefix.length > 0 && /[a-zA-Z_]/.test(prefix);
1791
+ callback(null, hasLetter ? completions : []);
1664
1792
  },
1665
1793
  };
1666
1794
  /* ACE stores completers on the language tools module */
@@ -1919,10 +2047,15 @@ async def _pf_run_terminal(source):
1919
2047
 
1920
2048
  running = false;
1921
2049
  btnRun.classList.remove('pf-running');
2050
+
2051
+ /* Notify external integrations (e.g. reveal-pyfrilet) that the sketch
2052
+ has been re-run and Python globals have been reset.
2053
+ Listeners can use this to re-send the current step. */
2054
+ window.dispatchEvent(new CustomEvent('pyfrilet:ready'));
1922
2055
  }
1923
2056
 
1924
2057
  /* ─────────────────── DOWNLOAD ───────────────── */
1925
- const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.6.6/pyfrilet.min.js';
2058
+ const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.7.1/pyfrilet.min.js';
1926
2059
 
1927
2060
  const STANDALONE_TEMPLATE = `<!doctype html>
1928
2061
  <html lang="fr">
@@ -2038,6 +2171,17 @@ FILLME-SCRIPTS
2038
2171
 
2039
2172
  /* ─────────────────── GLOBAL KEYBOARD (capture phase = before p5 & ACE) ── */
2040
2173
  window.addEventListener('keydown', (ev) => {
2174
+ /* In no-editor mode only Shift+Enter (re-run) is kept; all other
2175
+ editor-specific shortcuts (Escape, Ctrl+S, Ctrl+R) are ignored so
2176
+ the host page (e.g. RevealJS) can handle them normally. */
2177
+ if (noEditor) {
2178
+ if (ev.key === 'Enter' && ev.shiftKey) {
2179
+ ev.preventDefault();
2180
+ runCode();
2181
+ }
2182
+ return;
2183
+ }
2184
+
2041
2185
  /* Detect whether ACE currently has keyboard focus */
2042
2186
  const aceHasFocus = drawerOpen && aceInst &&
2043
2187
  aceInst.isFocused && aceInst.isFocused();
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",f="https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js",m="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js",_="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css",u="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js",h="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js",y="https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.8.0/lib/addon-unicode11.min.js",b="html, body {\n height: 100%; margin: 0; overflow: hidden;\n background: #111;\n}\n#pf-root {\n position: fixed; inset: 0;\n display: flex; flex-direction: column;\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n}\n\n/* ── app area ── */\n#pf-app:focus { outline: none; }\n#pf-app {\n flex: 1; min-height: 0;\n position: relative;\n background: #111;\n display: flex; align-items: center; justify-content: center;\n overflow: hidden;\n}\n#pf-viewport {\n transform-origin: 50% 50%;\n will-change: transform;\n}\n#pf-viewport canvas {\n display: block;\n outline: none;\n}\n#pf-loader {\n position: absolute; inset: 0;\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n gap: 14px;\n background: #111;\n color: #565f89;\n font-size: 13px;\n z-index: 50;\n pointer-events: none;\n}\n#pf-loader-bar {\n width: 160px; height: 2px;\n background: #2a2c3e;\n border-radius: 2px;\n overflow: hidden;\n}\n#pf-loader-bar::after {\n content: '';\n display: block;\n height: 100%;\n width: 40%;\n background: #7aa2f7;\n border-radius: 2px;\n animation: pf-slide 1.2s ease-in-out infinite;\n}\n@keyframes pf-slide {\n 0% { transform: translateX(-100%); }\n 100% { transform: translateX(350%); }\n}\n\n/* ── drawer (slide-up editor panel) ── */\n#pf-drawer {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n background: #1a1b26;\n height: 32px; /* collapsed = handle only */\n transition: height 0.26s cubic-bezier(.4, 0, .2, 1);\n overflow: hidden;\n /* shadow cast upward onto the app */\n box-shadow: 0 -4px 20px rgba(0,0,0,.55);\n}\n#pf-drawer.pf-open {\n height: var(--pf-drawer-h, 56vh);\n}\n\n/* ── handle bar ── */\n#pf-handle {\n height: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n padding: 0 8px 0 6px;\n background: #24283b;\n border-top: 1px solid #3d4166;\n cursor: ns-resize;\n user-select: none;\n gap: 6px;\n flex-shrink: 0;\n}\n/* grip zone: clickable to toggle, draggable to resize */\n#pf-grip {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 5px 6px;\n flex-shrink: 0;\n opacity: .5;\n border-radius: 4px;\n transition: opacity .15s, background .15s;\n cursor: pointer;\n}\n#pf-grip:hover { opacity: .85; background: rgba(255,255,255,.06); }\n#pf-grip span {\n display: block;\n width: 16px; height: 2px;\n background: #a9b1d6;\n border-radius: 1px;\n}\n#pf-handle-hint {\n flex: 1;\n color: #565f89;\n font-size: 10px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n#pf-handle-btns {\n display: flex;\n gap: 4px;\n flex-shrink: 0;\n}\n.pf-btn {\n height: 26px;\n min-width: 26px;\n padding: 0 5px;\n border: 0; border-radius: 5px;\n cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n font-size: 13px; line-height: 1;\n white-space: nowrap;\n transition: background .15s, transform .1s, opacity .15s;\n outline: none;\n box-sizing: border-box;\n}\n.pf-btn:active { transform: scale(.88); }\n.pf-btn:focus-visible { outline: 2px solid #7aa2f7; outline-offset: 1px; }\n\n#pf-btn-run { background: #1a6b3a; color: #9ece6a; font-size: 11px; }\n#pf-btn-run:hover { background: #1f8447; color: #b9f27a; }\n#pf-btn-run.pf-running { opacity: .5; cursor: not-allowed; }\n\n#pf-btn-code { background: #2a2c3e; color: #7aa2f7; font-size: 14px; }\n#pf-btn-code:hover { background: #3d4166; color: #c0caf5; }\n#pf-btn-code.pf-active { background: #3d4166; color: #e0af68; }\n\n#pf-btn-dl { background: #2a2c3e; color: #9d7cd8; font-size: 14px; }\n#pf-btn-dl:hover { background: #3d4166; color: #bb9af7; }\n\n#pf-btn-rec { background: #2a2c3e; color: #f7768e; font-size: 13px; }\n#pf-btn-rec:hover { background: #3d4166; color: #ff9e9e; }\n#pf-btn-rec.pf-recording { background: #6b1a1a; color: #f7768e; animation: pf-blink .8s step-end infinite; }\n@keyframes pf-blink { 50% { opacity: .4; } }\n\n#pf-btn-reset { background: #2a2c3e; color: #e0af68; font-size: 16px; }\n#pf-btn-reset:hover { background: #3d4166; color: #ffc777; }\n#pf-btn-reset.pf-dirty::after {\n content: '●';\n position: absolute;\n top: 2px; right: 3px;\n font-size: 7px;\n color: #e0af68;\n line-height: 1;\n}\n#pf-btn-reset { position: relative; }\n\n/* ── editor area inside drawer ── */\n#pf-editor-wrap {\n flex: 1;\n min-height: 80px;\n position: relative;\n display: flex;\n flex-direction: column;\n}\n#pf-ace { flex: 1; position: relative; min-height: 0; }\n\n/* ── tab bar ── */\n#pf-tabs {\n display: flex;\n flex-shrink: 0;\n background: #1a1b2e;\n border-bottom: 1px solid #414868;\n overflow-x: auto;\n scrollbar-width: none;\n}\n#pf-tabs:empty { display: none; }\n.pf-tab {\n padding: 5px 14px;\n font-size: 12px;\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n color: #737aa2;\n cursor: pointer;\n white-space: nowrap;\n transition: color .15s, border-color .15s;\n}\n.pf-tab:hover { color: #c0caf5; }\n.pf-tab.pf-tab-active { color: #c0caf5; border-bottom-color: #7aa2f7; }\n.pf-tab.pf-tab-readonly::after { content: ' 🔒'; font-size: 10px; opacity: .6; }\n.pf-tab.pf-tab-markdown::after { content: ' ✎'; font-size: 11px; opacity: .6; }\n\n/* ── markdown view ── */\n@import url('https://fonts.googleapis.com/css2?family=Alegreya+Sans:ital,wght@0,400;0,700;1,400&family=Fira+Code:wght@300..700&display=swap');\n\n#pf-markdown-view {\n flex: 1;\n overflow: auto;\n background: #f4f4f0;\n}\n\n#pf-markdown-view .pf-md-inner {\n width: 100%;\n max-width: 680px;\n margin: 0 auto;\n padding: 48px 48px 72px;\n box-sizing: border-box;\n font-family: 'Alegreya Sans', Georgia, serif;\n font-size: 17px;\n line-height: 1.8;\n color: #1c1c2e;\n}\n\n#pf-markdown-view h1 {\n font-size: 2.1em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 0 0 .3em;\n padding-bottom: .3em;\n border-bottom: 2px solid #d8d8e8;\n line-height: 1.2;\n}\n#pf-markdown-view h2 {\n font-size: 1.4em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 2em 0 .5em;\n padding-bottom: .2em;\n border-bottom: 1px solid #e0e0ec;\n}\n#pf-markdown-view h3 {\n font-size: 1.1em;\n font-weight: 700;\n color: #2a2a4a;\n margin: 1.6em 0 .4em;\n}\n\n#pf-markdown-view p { margin: .75em 0; }\n#pf-markdown-view ul,\n#pf-markdown-view ol { padding-left: 1.6em; margin: .75em 0; }\n#pf-markdown-view li { margin: .3em 0; }\n#pf-markdown-view hr { border: none; border-top: 1px solid #dde; margin: 2em 0; }\n#pf-markdown-view blockquote {\n margin: 1em 0;\n padding: .5em 1em;\n border-left: 3px solid #aab;\n color: #555;\n background: #ededf5;\n border-radius: 0 4px 4px 0;\n}\n\n#pf-markdown-view code {\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n font-size: .84em;\n background: #e8e8f2;\n color: #3a3a6a;\n padding: .15em .45em;\n border-radius: 4px;\n}\n#pf-markdown-view pre {\n background: #1a1b2e;\n border-radius: 8px;\n padding: 1em 1.2em;\n overflow: auto;\n margin: 1.2em 0;\n box-shadow: 0 2px 8px rgba(0,0,0,.12);\n}\n#pf-markdown-view pre code {\n background: transparent;\n color: #c0caf5;\n font-size: .86em;\n padding: 0;\n line-height: 1.6;\n border-radius: 0;\n}\n\n#pf-markdown-view table {\n border-collapse: collapse;\n width: 100%;\n margin: 1.2em 0;\n font-size: .95em;\n}\n#pf-markdown-view th {\n background: #e4e4f0;\n color: #1c1c2e;\n font-weight: 700;\n text-align: left;\n padding: .55em .85em;\n border: 1px solid #d0d0e8;\n}\n#pf-markdown-view td {\n padding: .5em .85em;\n border: 1px solid #e0e0ee;\n vertical-align: top;\n}\n#pf-markdown-view tr:nth-child(even) td { background: #f0f0f8; }\n\n#pf-markdown-view a {\n color: #3a5fc8;\n text-decoration: none;\n border-bottom: 1px solid rgba(58,95,200,.3);\n transition: color .15s, border-color .15s;\n}\n#pf-markdown-view a:hover { color: #1a3fa0; border-bottom-color: #1a3fa0; }\n\n#pf-markdown-view .katex-display {\n overflow-x: auto;\n padding: .5em 0;\n margin: 1.2em 0;\n}\n#pf-markdown-view .mermaid {\n text-align: center;\n margin: 1.5em 0;\n background: #ededf5;\n border-radius: 8px;\n padding: 1em;\n}\n\n/* ── error panel (below editor, never overlaps ACE) ── */\n#pf-err {\n flex-shrink: 0;\n max-height: 120px;\n overflow: auto;\n margin: 0; padding: 8px 13px;\n font-size: 11.5px; line-height: 1.45;\n background: rgba(13, 3, 3, .95);\n color: #f7768e;\n white-space: pre-wrap;\n display: none;\n border-top: 1px solid rgba(247, 118, 142, .35);\n}\n/* ── xterm terminal ── */\n#pf-xterm {\n display: none;\n position: absolute;\n inset: 0;\n padding: 10px 12px;\n box-sizing: border-box;\n background: #000000;\n overflow: hidden;\n}\n\n#pf-xterm.pf-xterm-overlay {\n background: rgba(0, 0, 0, 0.82);\n}\n\n/* Bandeau bas — mode p5 print() */\n#pf-xterm.pf-xterm-bandeau {\n inset: auto 0 0 0;\n height: 200px;\n background: rgba(0, 0, 0, 0.85);\n border-top: 1px solid rgba(255,255,255,0.1);\n padding: 0 12px 10px;\n}\n#pf-xterm.pf-xterm-bandeau.pf-xterm-collapsed {\n height: 24px;\n padding: 0;\n overflow: hidden;\n}\n\n/* Poignée du bandeau */\n#pf-xterm-handle {\n display: none;\n height: 24px;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n color: rgba(255,255,255,0.4);\n font-size: 11px;\n letter-spacing: 2px;\n user-select: none;\n gap: 8px;\n}\n#pf-xterm-handle:hover { color: rgba(255,255,255,0.8); }\n.pf-xterm-bandeau #pf-xterm-handle { display: flex; }\n\n/* xterm interne : prendre toute la hauteur disponible sous la poignée */\n#pf-xterm .xterm {\n height: 100%;\n}\n#pf-xterm.pf-xterm-bandeau .xterm {\n height: calc(100% - 24px);\n}\n#pf-xterm .xterm-screen {\n height: 100% !important;\n}\n",g='<div id="pf-root">\n <div id="pf-app" tabindex="-1">\n <div id="pf-viewport"><div id="pf-sketch"></div></div>\n <div id="pf-xterm"><div id="pf-xterm-handle"><span id="pf-xterm-chevron">∧</span><span id="pf-xterm-handle-label">console</span></div></div>\n <div id="pf-loader">\n <span id="pf-loader-msg">Chargement…</span>\n <div id="pf-loader-bar"></div>\n </div>\n </div>\n <div id="pf-drawer">\n <div id="pf-handle">\n <div id="pf-grip" title="Clic → ouvrir/fermer"><span></span><span></span><span></span></div>\n <span id="pf-handle-hint">Clic ☰ → ouvrir/fermer &nbsp;·&nbsp; Shift+Entrée → relancer</span>\n <div id="pf-handle-btns">\n <button class="pf-btn" id="pf-btn-run" title="Relancer (Shift+Entrée)">&#9654;</button>\n <button class="pf-btn" id="pf-btn-code" title="Éditeur plein écran">&#9999;&#xFE0F;</button>\n <button class="pf-btn" id="pf-btn-dl" title="Télécharger HTML autonome">&#128190;</button>\n <button class="pf-btn" id="pf-btn-rec" title="Enregistrer WebM">⏺</button>\n <button class="pf-btn" id="pf-btn-help" title="Aide">?</button>\n <button class="pf-btn" id="pf-btn-reset" title="Réinitialiser le code (Ctrl+R)">&#8635;</button>\n </div>\n </div>\n <div id="pf-editor-wrap">\n <div id="pf-tabs"></div>\n <div id="pf-markdown-view" style="display:none"></div>\n <div id="pf-ace"></div>\n </div>\n <pre id="pf-err"></pre>\n </div>\n</div>';document.addEventListener("DOMContentLoaded",function(){const w=[...document.querySelectorAll('script[type="text/python"], script[type="text/markdown"], python')];if(0===w.length)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const x=e||w[0],v=(x.getAttribute("data-sources")||x.getAttribute("sources")||"cdn").toLowerCase().trim(),k=(x.getAttribute("data-vendor")||x.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/");n="cdn"===v;const E=w.some(e=>"text/markdown"===e.getAttribute("type")),C=n?{p5:t,pyodide:a,pyodideIndex:null,ace:r,acePython:o,aceMonokai:i,aceLangTools:s,aceSearchbox:d,marked:E?l:null,katexCss:E?c:null,katex:E?p:null,markedKatex:E?f:null,mermaid:E?m:null,xtermCss:_,xterm:u,xtermFit:h,xtermUni:y}:{p5:k+"p5.min.js",pyodide:k+"pyodide/pyodide.js",pyodideIndex:k+"pyodide/",ace:k+"ace.min.js",acePython:k+"mode-python.min.js",aceMonokai:k+"theme-monokai.min.js",aceLangTools:k+"ext-language_tools.min.js",aceSearchbox:k+"ext-searchbox.min.js",marked:E?k+"marked.min.js":null,katexCss:E?k+"katex.min.css":null,katex:E?k+"katex.min.js":null,markedKatex:E?k+"marked-katex-extension.js":null,mermaid:E?k+"mermaid.min.js":null,xtermCss:k+"xterm.min.css",xterm:k+"xterm.min.js",xtermFit:k+"xterm-addon-fit.min.js",xtermUni:k+"addon-unicode11.min.js"},S="pyfrilet:"+location.pathname,L=w.map((e,n)=>{const t="text/markdown"===e.getAttribute("type")?"markdown":"python",a=e.hasAttribute("data-hidden"),r=e.hasAttribute("data-readonly");let o=e.getAttribute("data-tab");null!==o||a||(o=1===w.length?"Code":`Bloc ${n+1}`);const i=e.textContent.replace(/^\n/,"");return{id:"tab-"+n,label:o,hidden:a,readonly:r,type:t,starterCode:i,code:i}}),j=e=>{try{return localStorage.getItem(e)}catch(e){return null}};let I;const T=j(S);let z=null;if(T)try{z=JSON.parse(T)}catch(e){z=null}if(z&&1===z.v&&Array.isArray(z.tabs)&&z.tabs.length>0){const e=e=>`${e.label}|${e.type}|${e.hidden?1:0}|${e.readonly?1:0}`;z.tabs.map(e).join(",")!==L.map(e).join(",")&&(z._stale=!0)}const R=!(!z||!z._stale);I=z&&1===z.v&&Array.isArray(z.tabs)&&z.tabs.length>0?z.tabs.map((e,n)=>{const t=L.find(n=>n.label===e.label&&n.type===e.type)||null;return{id:"tab-"+n,label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,starterCode:t?t.starterCode:e.content,code:e.content}}):L.map((e,n)=>{if(!e.hidden&&!e.readonly&&"python"===e.type){const t=e.label?e.label.replace(/[^a-zA-Z0-9]/g,"_"):String(n);let a=j(S+":"+t);if(a||"Code"!==e.label||1!==L.length||(a=j(S)),a&&a.trim())return{...e,code:a}}return e});const P=x.hasAttribute("data-no-watchdog");!function(e,t,a,r,o,i){e=e.slice();let s=i;const d=document.createElement("style");d.textContent=b,document.head.appendChild(d),document.body.innerHTML=g;const l=document.getElementById("pf-app"),c=document.getElementById("pf-drawer"),p=document.getElementById("pf-handle"),f=document.getElementById("pf-sketch"),m=document.getElementById("pf-viewport"),_=document.getElementById("pf-loader"),u=document.getElementById("pf-loader-msg"),h=document.getElementById("pf-err"),y=document.getElementById("pf-btn-run"),w=document.getElementById("pf-btn-code"),x=document.getElementById("pf-btn-dl"),v=document.getElementById("pf-btn-rec"),k=document.getElementById("pf-btn-reset"),E=document.getElementById("pf-btn-help"),C=document.getElementById("pf-grip"),S=document.getElementById("pf-handle-hint"),L=document.getElementById("pf-tabs"),j=document.getElementById("pf-markdown-view");let I=!1,T=Math.round(.56*window.innerHeight);function z(){document.documentElement.style.setProperty("--pf-drawer-h",T+"px")}function R(){I=!0,c.classList.add("pf-open"),w.classList.add("pf-active"),setTimeout(()=>{J(),X&&X.focus()},280)}function P(){I=!1,c.classList.remove("pf-open"),w.classList.remove("pf-active"),setTimeout(()=>{J();const e=G._p?.canvas;e&&e.removeAttribute("tabindex"),l.focus()},280)}function A(){I?P():R()}z();let M=null;const B=5,O=120,F=document.createElement("div");function W(e){if(e.target.closest(".pf-btn"))return;if(e.target.closest("#pf-grip"))return;const n=e.touches?e.touches[0].clientY:e.clientY;M={y:n,h:I?T:0,moved:!1},F.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function D(e){if(!M)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=M.y-n;if(Math.abs(t)>B&&(M.moved=!0),!M.moved)return;const a=Math.max(0,Math.min(window.innerHeight-50,M.h+t));a<O?(c.style.transition="none",c.style.height="32px"):(T=a,z(),I||R(),c.style.transition="none",c.style.height=T+"px"),J()}function N(e){if(!M)return;const n=M.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??M.y,a=M.y-t,r=M.h+a;M=null,F.style.display="none",document.body.style.userSelect="",c.style.transition="",c.style.height="",n&&(r<O?P():(T=Math.max(O,Math.min(window.innerHeight-50,r)),z(),I||R()),J())}Object.assign(F.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(F),C.addEventListener("click",e=>{e.stopPropagation(),A()}),p.addEventListener("mousedown",W,!0),document.addEventListener("mousemove",D),document.addEventListener("mouseup",N),p.addEventListener("touchstart",W,{passive:!1}),document.addEventListener("touchmove",D,{passive:!0}),document.addEventListener("touchend",N);let U=0,K=0;function H(e){h.textContent=e,h.style.display="block",R()}function $(){h.textContent="",h.style.display="none"}function Y(){if(!G._p||"fit"!==G._mode)return;const e=G._w,n=G._h;if(!e||!n)return;const t=l.clientWidth,a=l.clientHeight,r=Math.min(t/e,a/n);m.style.transform=`scale(${r})`}function J(){if("fullscreen"===G._mode?G.size("max"):Y(),q&&"function"==typeof q.windowResized)try{q.windowResized()}catch(e){H(String(e))}X&&X.resize()}window.addEventListener("mousemove",e=>{U=e.clientX,K=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(U=e.touches[0].clientX,K=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=G._p?G._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=G._w/n.width,a=G._h/n.height;return[(U-n.left)*t,(K-n.top)*a]},window.addEventListener("resize",J);let q=null;const G=new Proxy({_p:null,_mode:"fit",_w:0,_h:0,_setP(e){this._p=e},size(e,n,t){if(!this._p)return;const a=t??void 0;"max"===e||null==e?(this._mode="fullscreen",this._w=l.clientWidth,this._h=l.clientHeight,void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),m.style.transform="scale(1)"):(this._mode="fit",this._w=Math.max(1,0|e),this._h=Math.max(1,0|n),void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),Y())},noSmooth(){this._p?.noSmooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="pixelated")},smooth(){this._p?.smooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="auto")},sketchTitle(e){S.textContent=String(e)},getItem(e){try{return localStorage.getItem(e)}catch(e){return null}},storeItem(e,n){try{localStorage.setItem(e,String(n))}catch(e){}},removeItem(e){try{localStorage.removeItem(e)}catch(e){}},clearStorage(){try{localStorage.clear()}catch(e){}}},{get(e,n){if(n in e)return"function"==typeof e[n]?e[n].bind(e):e[n];if(e._p&&n in e._p){const t=e._p[n];return"function"==typeof t?t.bind(e._p):t}},set:(e,n,t)=>n.startsWith("_")?(e[n]=t,!0):(e._p&&(e._p[n]=t),!0)});function V(){if(De(),q){try{q.remove()}catch(e){}q=null}f.innerHTML="",G._p=null,G._mode="fit",G._w=0,G._h=0,m.style.transform="scale(1)",S.textContent="Shift+Entrée → relancer  ·  Échap → ouvrir/fermer",ge&&(ge.destroy(),ge=null),ye&&(ye.destroy(),ye=null),be&&(be.destroy(),be=null),we&&(we.destroy(),we=null),xe&&(xe.destroy(),xe=null),ve&&(ve.destroy(),ve=null),ke&&(ke.destroy(),ke=null),Ee&&(Ee.destroy(),Ee=null),Ce&&(Ce.destroy(),Ce=null),Se&&(Se.destroy(),Se=null),Le&&(Le.destroy(),Le=null),je&&(je.destroy(),je=null),Ie&&(Ie.destroy(),Ie=null),Te&&(Te.destroy(),Te=null)}window.p5py=G;let X=null,Z=null;const Q={},ee=new Set;function ne(){L.innerHTML="",Z=null;const n=e.filter(e=>!e.hidden);L.style.display=n.length<=1?"none":"",n.forEach(e=>{const n=document.createElement("button");n.className="pf-tab",n.dataset.tabId=e.id,n.textContent=e.label,e.readonly&&n.classList.add("pf-tab-readonly"),"markdown"===e.type&&n.classList.add("pf-tab-markdown"),n.addEventListener("click",()=>te(e)),L.appendChild(n)}),n.length>0&&te(n[0],!0)}function te(e,n){if(n||Z!==e)if(Z=e,L.querySelectorAll(".pf-tab").forEach(n=>{n.classList.toggle("pf-tab-active",n.dataset.tabId===e.id)}),"markdown"===e.type){if(document.getElementById("pf-ace").style.display="none",j.style.display="block",window.marked){let n=marked.parse(e.starterCode);window.mermaid&&(n=n.replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,(e,n)=>`<div class="mermaid">${n.replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">")}</div>`)),j.innerHTML=`<div class="pf-md-inner">${n}</div>`}else j.innerHTML=`<div class="pf-md-inner"><pre>${e.starterCode}</pre></div>`;window.mermaid&&mermaid.run({nodes:j.querySelectorAll(".mermaid")})}else document.getElementById("pf-ace").style.display="block",j.style.display="none",X&&Q[e.id]&&(X.setSession(Q[e.id]),X.setReadOnly(e.readonly),X.focus())}function ae(){let n=1;e.filter(e=>"python"===e.type).forEach(e=>{!e.hidden&&Q[e.id]?(Q[e.id].setOption("firstLineNumber",n),n+=Q[e.id].getLength()):n+=e.code.split("\n").length})}function re(){Object.keys(Q).forEach(e=>delete Q[e]),e.filter(e=>!e.hidden&&"python"===e.type).forEach(e=>{const n=ace.createEditSession(e.code,"ace/mode/python");if(n.setUseWorker(!1),n.setTabSize(4),Q[e.id]=n,!e.readonly){let e=null;n.on("change",()=>{null!==e&&(clearTimeout(e),ee.delete(e)),e=setTimeout(()=>{ee.delete(e),e=null,ie()},350),ee.add(e),ae(),de()})}});const n=e.find(e=>!e.hidden&&"python"===e.type);X&&n&&Q[n.id]&&(X.setSession(Q[n.id]),X.setReadOnly(n.readonly),X.renderer.updateFull(!0)),ae()}function oe(){!r.ace.startsWith("vendor")&&r.ace.startsWith("http")||ace.config.set("basePath",r.ace.replace(/\/[^/]+$/,"/")),X=ace.edit("pf-ace"),X.setTheme("ace/theme/monokai"),X.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),X.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{X.completer?.popup?.isOpen||Ae()}}),X.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:P}),X.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:se}),X.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&le()}}),re(),ne(),de()}function ie(){const n={v:1,tabs:e.map(e=>({label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,content:e.hidden||e.readonly||"python"!==e.type||!Q[e.id]?e.code:Q[e.id].getValue()}))};try{localStorage.setItem(a,JSON.stringify(n))}catch(e){}}function se(){ie()}function de(){const n=s||e.some(e=>!e.hidden&&!e.readonly&&"python"===e.type&&Q[e.id]&&Q[e.id].getValue()!==e.starterCode);k.classList.toggle("pf-dirty",n)}function le(){ee.forEach(e=>clearTimeout(e)),ee.clear();try{localStorage.removeItem(a)}catch(e){}e.forEach(e=>{if(e.label)try{localStorage.removeItem(a+":"+e.label.replace(/[^a-zA-Z0-9]/g,"_"))}catch(e){}});try{localStorage.removeItem(a+":Code")}catch(e){}s=!1,e=t.map((e,n)=>({...e,id:"tab-"+n,code:e.starterCode})),re(),ne(),de(),Ae()}window.addEventListener("beforeunload",se);let ce=null,pe=null;async function fe(){return pe||(pe=(async()=>{const e={};r.pyodideIndex&&(e.indexURL=r.pyodideIndex),ce=await loadPyodide(e),await ce.loadPackage(["rich","pygments"]);try{const e=new Uint8Array(new SharedArrayBuffer(1));ce.setInterruptBuffer(e),window._pfInterrupt=()=>{e[0]=2,setTimeout(()=>{e[0]=0},50)}}catch(e){window._pfInterrupt=null}if(window._pfMountIdbfs=e=>new Promise((n,t)=>{try{ce.FS.mkdirTree(e);try{ce.FS.mount(ce.FS.filesystems.IDBFS,{},e)}catch(e){if(10!==e.errno)return void t(e)}ce.FS.syncfs(!0,e=>e?t(e):n())}catch(e){t(e)}}),window._pfSyncIdbfs=()=>new Promise((e,n)=>{ce.FS.syncfs(!1,t=>t?n(t):e())}),ce.runPython("\nimport sys, types, js\nfrom js import p5py, _pfMouse\nfrom pyodide.ffi import JsProxy\n\n# ── Python builtins that must NOT be shadowed ──────────────────────\n_BLACKLIST = frozenset({\n 'abs','all','any','bin','bool','bytes','callable','chr','compile',\n 'delattr','dict','dir','divmod','enumerate','eval','exec',\n 'filter','float','format','frozenset','getattr','globals','hasattr',\n 'hash','help','hex','id','input','int','isinstance','issubclass',\n 'iter','len','list','locals','map','max','min','next','object',\n 'oct','open','ord','pow','print','property','range','repr',\n 'reversed','round','set','setattr','slice','sorted','staticmethod',\n 'str','sum','super','tuple','type','vars','zip',\n # p5 lifecycle hooks — user defines these, we don't import them\n 'setup','draw','preload',\n})\n\n# ── Introspect a hidden dummy p5 instance ─────────────────────────\n_dummy_node = js.document.createElement('div')\n_dummy = js.p5.new(lambda _: None, _dummy_node)\n\n_p5_functions = set() # names of callable JS members\n_p5_attributes = set() # names of scalar/readable members\n\nfor _n in dir(_dummy):\n if _n.startswith('_') or _n in _BLACKLIST:\n continue\n _v = getattr(_dummy, _n)\n if isinstance(_v, JsProxy):\n if callable(_v):\n _p5_functions.add(_n)\n # non-callable JsProxy (canvas, pixels…) → skip\n else:\n _p5_attributes.add(_n)\n\n# Read real initial values now, while dummy is still alive\n_attr_init = {}\nfor _n in _p5_attributes:\n try:\n _attr_init[_n] = getattr(_dummy, _n)\n except Exception:\n _attr_init[_n] = 0\n\n_dummy.remove()\ndel _dummy, _dummy_node\n\n# ── Build module ───────────────────────────────────────────────────\nm = types.ModuleType(\"p5\")\n\n# Generic function wrapper: delegates to live p5Bridge instance\nclass _FW:\n __slots__ = ('_n',)\n def __init__(self, n): self._n = n\n def __call__(self, *a): return getattr(p5py, self._n)(*a)\n def __repr__(self): return f'<p5 function {self._n}>'\n\nfor _n in _p5_functions:\n setattr(m, _n, _FW(_n))\n\n# ── Special overrides (our bridge has custom behaviour) ────────────\n# smooth/noSmooth exist on a real p5 instance so introspection finds\n# them — but our Proxy overrides them to also toggle CSS image-rendering.\n# size and sketchTitle are pyfrilet-only: NOT on a real p5 instance,\n# so introspection misses them — add them explicitly.\nfor _n in ('sketchTitle',):\n setattr(m, _n, _FW(_n))\n _p5_functions.add(_n) # keep __all__ consistent\n\n# size() calls _pf_refresh after resizing so width/height are immediately\n# correct in setup() — consistent with p5.js JS behaviour.\nclass _SizeWrapper:\n def __call__(self, *a):\n p5py.size(*a)\n _pf_refresh(_ns_ref[0])\n return _GetCanvasWrapper()()\n def __repr__(self): return '<p5 function size>'\nsetattr(m, 'size', _SizeWrapper())\nsetattr(m, 'createCanvas', m.size) # alias — createCanvas(...) == size(...)\n_p5_functions.add('size')\n_p5_functions.add('createCanvas')\n_ns_ref = [{}] # filled in by runCode before each exec\n\n# getCanvas() — returns the p5.Element wrapping the canvas,\n# so the user can call .drop(create_proxy(fn)), .mouseOver(), etc. directly like in JS.\nclass _GetCanvasWrapper:\n def __call__(self):\n p = p5py._p\n if p is None:\n raise RuntimeError('getCanvas() doit être appelé dans setup() ou après')\n p.canvas.id = '__pf_canvas__'\n return p.select('#__pf_canvas__')\n def __repr__(self): return '<p5 function getCanvas>'\nsetattr(m, 'getCanvas', _GetCanvasWrapper())\n_p5_functions.add('getCanvas')\n\n# mouseX / mouseY: override with our accurate coordinate calculator\n# (p5's own values are wrong when a CSS-transformed parent is used)\n_MOUSE_OVERRIDE = frozenset({'mouseX', 'mouseY'})\n\n# Initial values from the dummy instance — constants like WEBGL, DEGREES,\n# LEFT_ARROW… are correct from the very first setup() call.\nfor _n in _p5_attributes:\n if _n in _MOUSE_OVERRIDE:\n setattr(m, _n, 0.0)\n else:\n setattr(m, _n, _attr_init.get(_n, 0))\n\n# Build __all__ for import * — done later, after snake_case aliases are added\n\n# ── _pf_refresh: called before every event callback ───────────────\nimport re as _re\n\n# Pre-compute snake_case alias for each attribute — None if identical\n_attr_snake = {\n _k: (_re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _k) or None)\n for _k in _p5_attributes\n}\n_attr_snake = {_k: (_s if _s != _k else None) for _k, _s in _attr_snake.items()}\n\n# Add snake_case names to _p5_attributes so __all__ and _pf_refresh cover them\nfor _k, _sk in list(_attr_snake.items()):\n if _sk:\n _p5_attributes.add(_sk)\n setattr(m, _sk, getattr(m, _k, 0)) # initial value mirrors camelCase\n _attr_snake[_sk] = None # snake name has no further alias\n\ndef _pf_refresh(ns):\n # accurate mouse coords (bypasses p5's stale CSS-transform offset)\n mx, my = _pfMouse()\n\n # update all known scalar attributes from live instance\n for _k in _p5_attributes:\n _sk = _attr_snake.get(_k)\n if _k in _MOUSE_OVERRIDE:\n _v = mx if _k in ('mouseX', 'mouse_x') else my\n elif _sk is None and _k not in _attr_snake:\n # pure snake_case entry — skip, updated via its camelCase counterpart\n continue\n else:\n try:\n _v = getattr(p5py, _k)\n except Exception:\n continue\n setattr(m, _k, _v)\n if _k in ns:\n ns[_k] = _v\n if _sk:\n setattr(m, _sk, _v)\n if _sk in ns:\n ns[_sk] = _v\n\nsys.modules[\"p5\"] = m\n\n# ── draw() watchdog via sys.settrace ──────────────────────────────\n# Trace is called on every Python line event. We only call time.monotonic()\n# every N events to minimize overhead — a tight loop still triggers within\n# a few microseconds, so detection latency is negligible.\nimport time as _time\n\n_WDOG_CHECK_EVERY = 100\n_wdog_deadline = [0.0]\n_wdog_count = [0]\n\ndef _wdog_trace(frame, event, arg):\n _wdog_count[0] += 1\n if _wdog_count[0] >= _WDOG_CHECK_EVERY:\n _wdog_count[0] = 0\n if _time.monotonic() > _wdog_deadline[0]:\n raise TimeoutError(\"draw() watchdog\")\n return _wdog_trace\n\nclass _PfHandledError(Exception):\n \"\"\"Levée après que rich a déjà affiché le traceback vers xterm.\"\"\"\n pass\n\ndef _pf_safe_call(fn):\n try:\n fn()\n except (_PfHandledError, TimeoutError):\n raise\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and not _tb.tb_frame.f_code.co_filename.startswith(('sketch_', 'programme_')):\n _tb = _tb.tb_next\n if _tb: _e.__traceback__ = _tb\n _pf_rich_console.print_exception(extra_lines=8, show_locals=True)\n from js import _pfShowErrorTerminal\n _pfShowErrorTerminal()\n\ndef _pf_safe_proxy(fn):\n from pyodide.ffi import create_proxy as _cp\n def _wrapped(*args, **kwargs):\n _pf_safe_call(lambda: fn(*args, **kwargs))\n return _cp(_wrapped)\n\nsetattr(m, 'safe_proxy', _pf_safe_proxy)\n_p5_functions.add('safe_proxy')\n\ndef _pf_persist():\n \"\"\"Synchronise /persist vers IndexedDB (fire-and-forget).\n Fonctionne en mode p5 (synchrone) et en mode terminal.\"\"\"\n from js import _pfSyncIdbfs\n _pfSyncIdbfs() # Promise — le navigateur l'exécute dès que la stack JS se libère\n\nsetattr(m, 'persist', _pf_persist)\n_p5_functions.add('persist')\npersist = _pf_persist # accessible aussi hors p5 (mode terminal sans import p5)\n\nimport linecache as _linecache\n_pf_run_counter = [0]\n\ndef _pf_exec_user_code():\n _ns = {}\n _pf_run_counter[0] += 1\n _pf_fname = f'sketch_{_pf_run_counter[0]}'\n with open(_pf_fname, 'w') as _f:\n _f.write(_USER_CODE)\n lines = _USER_CODE.splitlines(keepends=True)\n _linecache.cache[_pf_fname] = (len(_USER_CODE), None, lines, _pf_fname)\n try:\n exec(compile(_USER_CODE, _pf_fname, 'exec'), _ns, _ns)\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and _tb.tb_frame.f_code.co_filename != _pf_fname:\n _tb = _tb.tb_next\n if _tb: _e.__traceback__ = _tb\n _pf_rich_console.print_exception(extra_lines=8, show_locals=True)\n from js import _pfShowErrorTerminal\n _pfShowErrorTerminal()\n return None\n _ns_ref[0] = _ns\n return _ns\n\ndef _pf_draw_watchdog(fn, timeout_ms):\n _wdog_count[0] = 0\n _wdog_deadline[0] = _time.monotonic() + timeout_ms * 0.001\n sys.settrace(_wdog_trace)\n try:\n _pf_safe_call(fn)\n except TimeoutError:\n from js import _pfShowWatchdogError\n _pfShowWatchdogError(timeout_ms)\n finally:\n sys.settrace(None)\n\ndef _pf_draw_direct(fn, timeout_ms):\n _pf_safe_call(fn)\n\ndef _snake_to_camel(name):\n parts = name.split('_')\n return parts[0] + ''.join(p.capitalize() for p in parts[1:])\n\n# Pre-populate snake_case aliases so \"from p5 import no_fill\" works\nfor _camel in list(vars(m).keys()):\n _snake = _re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _camel)\n if _snake != _camel and not hasattr(m, _snake):\n setattr(m, _snake, getattr(m, _camel))\n if _camel in _p5_functions:\n _p5_functions.add(_snake)\n\n# Rebuild __all__ now that snake_case aliases are included\nm.__all__ = sorted(_p5_functions | _p5_attributes)\n\ndef _p5_getattr(name):\n camel = _snake_to_camel(name)\n if camel != name:\n val = getattr(m, camel, None)\n if val is not None:\n return val\n raise AttributeError(f\"module 'p5' has no attribute '{name}'\")\n\nm.__getattr__ = _p5_getattr\n"),ce.runPython("\nimport asyncio as _asyncio, ast as _ast\nimport os as _os, sys as _sys\n_os.environ.setdefault('TERM', 'xterm-256color')\n_os.environ.setdefault('COLORTERM', 'truecolor')\n\n# Wrapper file-like qui écrit directement vers xterm via JS.\n# Rich écrit des strings sur sys.stdout.write() — il faut un vrai objet fichier.\nclass _PfStream:\n def __init__(self, js_fn):\n self._fn = js_fn\n self.encoding = 'utf-8'\n self.errors = 'replace'\n def write(self, s):\n if s:\n self._fn(s)\n return len(s)\n def writelines(self, lines):\n for l in lines: self.write(l)\n def flush(self): pass\n def isatty(self): return True\n @property\n def softspace(self): return 0\n\nfrom js import _pfTermWrite, _pfTermWriteErr\n_sys.stdout = _PfStream(_pfTermWrite)\n_sys.stderr = _PfStream(_pfTermWriteErr)\n\nfrom rich.console import Console as _RichConsole\n_pf_rich_console = _RichConsole(stderr=True)\n\nasync def _pf_async_input(prompt=\"\"):\n from js import _pfTerminalInput\n result = await _pfTerminalInput(str(prompt) if prompt else \"\")\n return result\n\n# Cancellation flag — set by JS before launching a new run\n_pf_cancel_run = [False]\n\nasync def _pf_async_sleep(seconds):\n \"\"\"Cancellable sleep: polls the cancel flag every 50 ms.\"\"\"\n import asyncio as _aio\n deadline = _aio.get_event_loop().time() + seconds\n while True:\n if _pf_cancel_run[0]:\n raise KeyboardInterrupt\n remaining = deadline - _aio.get_event_loop().time()\n if remaining <= 0:\n break\n await _aio.sleep(min(0.05, remaining))\n\nasync def _pf_maybe_await(val):\n \"\"\"Await val if it's a coroutine, otherwise return it as-is.\n This lets us await-ify every call site without knowing in advance\n which functions are async.\"\"\"\n import asyncio\n if asyncio.iscoroutine(val):\n return await val\n return val\n\nclass _AsyncTransformer(_ast.NodeTransformer):\n \"\"\"Transforms user code so that:\n - every def becomes async def (including nested and class methods)\n - every call f(...) becomes await _pf_maybe_await(f(...))\n whether f is a Name, Attribute, subscript, or any other expression\n - lambda bodies are left untouched (can't be async)\n \"\"\"\n _HELPER = '_pf_maybe_await'\n\n def visit_FunctionDef(self, node):\n # Dunder methods (__init__, __str__...) are called by Python internals\n # without going through _pf_maybe_await — they must stay synchronous.\n self.generic_visit(node)\n if node.name.startswith('__') and node.name.endswith('__'):\n return node\n return _ast.AsyncFunctionDef(\n name=node.name,\n args=node.args,\n body=node.body,\n decorator_list=node.decorator_list,\n returns=node.returns,\n lineno=node.lineno,\n col_offset=node.col_offset,\n end_lineno=node.end_lineno,\n end_col_offset=node.end_col_offset,\n )\n\n # Leave Lambda untouched — can't be async\n def visit_Lambda(self, node):\n return node\n\n def visit_Await(self, node):\n # User wrote explicit await f() — recurse into f()'s arguments\n # but don't re-wrap the call itself with _pf_maybe_await.\n self._skip_next_wrap = True\n self.generic_visit(node)\n self._skip_next_wrap = False\n return node\n\n def visit_Call(self, node):\n # Recurse first so nested calls are also transformed\n self.generic_visit(node)\n func = node.func\n # time.sleep(x) → _pf_async_sleep(x) (cancellable)\n if (isinstance(func, _ast.Attribute)\n and isinstance(func.value, _ast.Name)\n and func.value.id == 'time'\n and func.attr == 'sleep'):\n node.func = _ast.Name(id='_pf_async_sleep', ctx=_ast.Load())\n # sleep(x) → _pf_async_sleep(x) (from time import sleep)\n elif isinstance(func, _ast.Name) and func.id == 'sleep':\n node.func = _ast.Name(id='_pf_async_sleep', ctx=_ast.Load())\n # asyncio.sleep(x) → _pf_async_sleep(x) (cancellable)\n elif (isinstance(func, _ast.Attribute)\n and isinstance(func.value, _ast.Name)\n and func.value.id == 'asyncio'\n and func.attr == 'sleep'):\n node.func = _ast.Name(id='_pf_async_sleep', ctx=_ast.Load())\n # If already under a user-written await, don't re-wrap\n if getattr(self, '_skip_next_wrap', False):\n self._skip_next_wrap = False\n return node\n # Wrap: f(...) → await _pf_maybe_await(f(...))\n helper = _ast.Name(id=self._HELPER, ctx=_ast.Load())\n wrapped = _ast.Call(func=helper, args=[node], keywords=[])\n return _ast.Await(value=wrapped)\n\nasync def _pf_run_terminal(source):\n tree = _ast.parse(source)\n tree = _AsyncTransformer().visit(tree)\n\n wrapper = _ast.parse(\"async def programme(): pass\")\n wrapper.body[0].body = tree.body if tree.body else [_ast.Pass()]\n _ast.fix_missing_locations(wrapper)\n _pf_run_counter[0] += 1\n _pf_fname = f'programme_{_pf_run_counter[0]}'\n with open(_pf_fname, 'w') as _f:\n _f.write(source)\n lines = source.splitlines(keepends=True)\n _linecache.cache[_pf_fname] = (len(source), None, lines, _pf_fname)\n import asyncio as _asyncio\n _pf_cancel_run[0] = False # reset at start of each run\n _ns = {\n 'input': _pf_async_input,\n 'persist': _pf_persist,\n '_pf_maybe_await': _pf_maybe_await,\n 'asyncio': _asyncio,\n '_pf_async_sleep': _pf_async_sleep,\n }\n exec(compile(wrapper, _pf_fname, 'exec'), _ns)\n try:\n await _ns['programme']()\n except (SystemExit, KeyboardInterrupt):\n pass\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and _tb.tb_frame.f_code.co_filename != _pf_fname:\n _tb = _tb.tb_next\n if _tb:\n _e.__traceback__ = _tb\n _pf_rich_console.print_exception(extra_lines=8, show_locals=True)\n"),X){me(ce.runPython("list(m.__all__)").toJs())}})(),pe)}function me(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,a,r,o){o(null,r.length>0?n:[])}},a=ace.require("ace/ext/language_tools");a&&Array.isArray(a.completers)&&(a.completers=a.completers.filter(e=>!0!==e._pyfrilet)),t._pyfrilet=!0,X.completers=[...X.completers||[],t]}let _e=!1,ue=!1,he=0,ye=null,be=null,ge=null,we=null,xe=null,ve=null,ke=null,Ee=null,Ce=null,Se=null,Le=null,je=null,Ie=null,Te=null;const ze=300;function Re(e){return!/\bfrom\s+p5\s+import\b|\bimport\s+p5\b/.test(e)}async function Pe(e){const n=++he;if(ue){if(ce)try{ce.globals.get("_pf_cancel_run")[0]=!0}catch(e){}on();const e=Date.now()+3e3;for(;ue&&Date.now()<e;)await new Promise(e=>setTimeout(e,20))}if(n===he){ue=!0,an(),tn(),$(),window._pfSetP5Mode&&window._pfSetP5Mode(!1),await window._pfMountIdbfs("/persist");try{const n=ce.globals.get("_pf_run_terminal");await n(e)}catch(e){const n=String(e);n.includes("SystemExit")||nn(n+"\n")}finally{ue=!1}}}async function Ae(){if(_e){if(!ue)return;window._pfInterrupt&&window._pfInterrupt(),on(),_e=!1,y.classList.remove("pf-running"),await new Promise(e=>setTimeout(e,80))}_e=!0,y.classList.add("pf-running"),$(),tn(),V(),ce||(u.textContent="Initialisation de Pyodide…",_.style.display="flex");try{await fe()}catch(e){return _.style.display="none",H("Erreur Pyodide : "+(e.message||String(e))),_e=!1,void y.classList.remove("pf-running")}_.style.display="none";const t=e.filter(e=>"python"===e.type).map(e=>e.hidden||e.readonly||!Q[e.id]?e.code:Q[e.id].getValue()).join("\n");try{u.textContent="Chargement des dépendances…",_.style.display="flex",await ce.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:n})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}if(_.style.display="none",Re(t))return y.classList.remove("pf-running"),await Pe(t),void(_e=!1);on(),window._pfSetP5Mode&&window._pfSetP5Mode(!0),await window._pfMountIdbfs("/persist"),ce.globals.set("_USER_CODE",t);const a=ce.globals.get("_pf_exec_user_code");try{if(!a())return _e=!1,void y.classList.remove("pf-running");ce.runPython("_ns = _ns_ref[0]")}catch(e){return rn(e.message||String(e)),_e=!1,void y.classList.remove("pf-running")}let r,i,s,d,l,c,p,m,h,b,g,w,x,v;try{const e=(e,n)=>ce.runPython(`_ns.get('${e}') or _ns.get('${n}')`);l=e("preload","preload"),r=e("setup","setup"),i=e("draw","draw"),s=e("mousePressed","mouse_pressed"),d=e("keyPressed","key_pressed"),c=e("mouseDragged","mouse_dragged"),p=e("mouseReleased","mouse_released"),m=e("mouseMoved","mouse_moved"),h=e("mouseWheel","mouse_wheel"),b=e("doubleClicked","double_clicked"),g=e("keyReleased","key_released"),w=e("touchStarted","touch_started"),x=e("touchMoved","touch_moved"),v=e("touchEnded","touch_ended")}catch(e){return rn(e.message||String(e)),_e=!1,void y.classList.remove("pf-running")}if(!i)return H("Le script doit définir au moins une fonction draw()."),_e=!1,void y.classList.remove("pf-running");const{create_proxy:k}=ce.pyimport("pyodide.ffi"),E=ce.runPython("_ns.get('windowResized')"),C=ce.globals.get("_pf_refresh"),S=ce.globals.get(o?"_pf_draw_direct":"_pf_draw_watchdog"),L=ce.globals.get("_ns"),j=ce.globals.get("_pf_safe_call"),I=e=>e?k(()=>{try{C(L),j(e)}catch(e){rn("")}}):null;ge=l?k(()=>{try{j(l)}catch(e){rn("")}}):null,ye=r?k(()=>{try{j(r)}catch(e){rn("")}}):null,be=k(()=>{try{C(L),S(i,ze)}catch(e){V(),rn("")}}),we=I(s),xe=I(p),ve=I(c),ke=I(m),Ee=I(h),Ce=I(b),Se=I(d),Le=I(g),je=I(w),Ie=I(x),Te=I(v);const T=E?k(()=>{try{j(E)}catch(e){rn("")}}):null;let z=!1;q=new p5(e=>{G._setP(e),ge&&(e.preload=()=>{ge()}),e.setup=()=>{ye&&ye(),e.canvas||G.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),z=!0},e.draw=()=>{z&&be()},e.mousePressed=()=>{z&&we&&we()},e.mouseReleased=()=>{z&&xe&&xe()},e.mouseDragged=()=>{z&&ve&&ve()},e.mouseMoved=()=>{z&&ke&&ke()},e.mouseWheel=e=>{z&&Ee&&Ee()},e.doubleClicked=()=>{z&&Ce&&Ce()},e.keyPressed=()=>{z&&Se&&Se()},e.keyReleased=()=>{z&&Le&&Le()},je&&(e.touchStarted=()=>{z&&je()}),Ie&&(e.touchMoved=()=>{z&&Ie()}),Te&&(e.touchEnded=()=>{z&&Te()}),e.windowResized=()=>{"fullscreen"===G._mode?G.size("max"):Y(),T&&T()}},f),_e=!1,y.classList.remove("pf-running")}const Me='<!doctype html>\n<html lang="fr">\n<head>\n <meta charset="utf-8">\n <meta name="viewport" content="width=device-width, initial-scale=1">\n <title>export</title>\n <script src="https://cdn.jsdelivr.net/npm/pyfrilet@0.6.6/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\nFILLME-SCRIPTS\n\n</body>\n</html>';function Be(){const n=e.map((e,n)=>{let t;t="python"!==e.type||e.hidden||e.readonly||!Q[e.id]?e.code:Q[e.id].getValue();const a=[],r="markdown"===e.type?"text/markdown":"text/python";null!==e.label&&a.push(`data-tab="${e.label.replace(/"/g,"&quot;")}"`),e.hidden&&a.push("data-hidden"),e.readonly&&a.push("data-readonly");return`<script type="${r}"${a.length?" "+a.join(" "):""}>\n${t.replace(/<\/script>/gi,"<\\/script>")}\n<\/script>`}).join("\n\n"),t=Me.replace("FILLME-SCRIPTS",n),a=new Blob([t],{type:"text/html;charset=utf-8"}),r=URL.createObjectURL(a),o=Object.assign(document.createElement("a"),{href:r,download:"sketch.html"});document.body.appendChild(o),o.click(),document.body.removeChild(o),URL.revokeObjectURL(r)}let Oe=null,Fe=[];function We(){const e=G._p?.canvas;if(!e)return;const n=["video/webm;codecs=vp9","video/webm;codecs=vp8","video/webm"].find(e=>MediaRecorder.isTypeSupported(e))||"video/webm",t=e.captureStream();Oe=new MediaRecorder(t,{mimeType:n}),Fe=[],Oe.ondataavailable=e=>{e.data.size&&Fe.push(e.data)},Oe.onstop=()=>{const e=new Blob(Fe,{type:n}),t=URL.createObjectURL(e),a=n.includes("webm")?"webm":"mp4";Object.assign(document.createElement("a"),{href:t,download:`sketch.${a}`}).click(),URL.revokeObjectURL(t),v.textContent="⏺",v.title="Enregistrer WebM",v.classList.remove("pf-recording"),Oe=null},Oe.start(),v.textContent="⏹",v.title="Arrêter l'enregistrement",v.classList.add("pf-recording")}function De(){Oe&&"inactive"!==Oe.state&&Oe.stop()}v.addEventListener("click",()=>{Oe?De():We()}),y.addEventListener("click",()=>Ae()),w.addEventListener("click",()=>{I?P():(T=window.innerHeight-32,z(),R())}),x.addEventListener("click",Be);const Ne="https://codeberg.org/nopid/pyfrilet";function Ue(e){return new Promise((n,t)=>{const a=document.createElement("script");a.src=e,a.onload=n,a.onerror=()=>t(new Error("Impossible de charger : "+e)),document.head.appendChild(a)})}E.addEventListener("click",()=>window.open(Ne,"_blank")),k.addEventListener("click",()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&le()}),window.addEventListener("keydown",e=>{const n=I&&X&&X.isFocused&&X.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void Ae();if("Escape"===e.key){const t=document.querySelector(".ace_search");if(t&&"none"!==t.style.display)return e.preventDefault(),e.stopPropagation(),X.searchBox?X.searchBox.hide():t.style.display="none",void X.focus();if(n){const n=X.completer?.popup?.isOpen;if(n)return;return e.preventDefault(),e.stopPropagation(),void P()}return e.preventDefault(),e.stopPropagation(),void(I?P():R())}if(!n)return"s"!==e.key&&"S"!==e.key||!e.ctrlKey&&!e.metaKey?"r"!==e.key&&"R"!==e.key||!e.ctrlKey&&!e.metaKey||e.altKey?void 0:(e.preventDefault(),void(confirm("Réinitialiser ? Les modifications seront perdues.")&&le())):(e.preventDefault(),void se())}else e.preventDefault()},!0),(async()=>{u.textContent="Chargement des dépendances…",_.style.display="flex";try{if(await Ue(r.p5),r.marked){const e=document.createElement("link");e.rel="stylesheet",e.href=r.katexCss,document.head.appendChild(e),await Ue(r.marked),await Ue(r.katex),await Ue(r.markedKatex),await Ue(r.mermaid),marked.use(markedKatex({throwOnError:!1})),mermaid.initialize({startOnLoad:!1,theme:"neutral"})}await Ue(r.ace),await Ue(r.acePython),await Ue(r.aceMonokai),await Ue(r.aceLangTools),await Ue(r.aceSearchbox),await Ue(r.pyodide);const e=document.createElement("link");e.rel="stylesheet",e.href=r.xtermCss,document.head.appendChild(e),await Ue(r.xterm),await Ue(r.xtermFit),await Ue(r.xtermUni)}catch(e){return u.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}oe(),await Ae(),_.style.display="none"})();const Ke=document.getElementById("pf-xterm");let He=null,$e=null,Ye=null,Je="";function qe(){if(He)return;He=new Terminal({theme:{background:"#000000",foreground:"#e8e8e8",cursor:"#ffffff",black:"#2a2a2a",brightBlack:"#555555",red:"#cc4444",brightRed:"#ff6666",green:"#44aa44",brightGreen:"#66cc66",yellow:"#aaaa00",brightYellow:"#dddd44",blue:"#4466cc",brightBlue:"#6688ff",magenta:"#aa44aa",brightMagenta:"#dd66dd",cyan:"#44aaaa",brightCyan:"#66cccc",white:"#cccccc",brightWhite:"#ffffff"},fontFamily:"'Fira Code', 'Consolas', 'Courier New', monospace",fontSize:15,lineHeight:1,letterSpacing:0,cursorBlink:!0,scrollback:2e3,convertEol:!0,allowProposedApi:!0}),$e=new FitAddon.FitAddon,He.loadAddon($e);const e=new Unicode11Addon.Unicode11Addon;He.loadAddon(e),He.unicode.activeVersion="11",He.open(Ke),$e.fit(),new ResizeObserver(()=>{He&&"none"!==Ke.style.display&&$e.fit()}).observe(Ke),He.onData(e=>{if(Ye)if("\r"===e){const e=Je;Je="",He.write("\r\n");const n=Ye;Ye=null,n(e)}else if(""===e)Je.length>0&&(Je=Je.slice(0,-1),He.write("\b \b"));else if(""===e){Je="",He.write("^C\r\n");const e=Ye;Ye=null,e(null)}else e.charCodeAt(0)>=32&&(Je+=e,He.write(e))})}window._pfTerminalInput=function(e){return new Promise(n=>{Ye=n,Je="",e&&He.write(e),He.focus()})};let Ge=!1;window._pfSetP5Mode=e=>{Ge=e};const Ve=document.getElementById("pf-xterm-handle"),Xe=document.getElementById("pf-xterm-chevron");function Ze(){Ke.style.display="block",Ke.classList.remove("pf-xterm-overlay","pf-xterm-collapsed"),Ke.classList.add("pf-xterm-bandeau"),Xe&&(Xe.textContent="∨"),qe(),$e.fit()}function Qe(){Ke.classList.contains("pf-xterm-collapsed")?(Ke.classList.remove("pf-xterm-collapsed"),Xe&&(Xe.textContent="∨"),$e.fit()):(Ke.classList.add("pf-xterm-collapsed"),Xe&&(Xe.textContent="∧"))}Ve&&Ve.addEventListener("click",Qe);function en(e){Ge&&"none"===Ke.style.display&&Ze(),He&&He.write(e)}function nn(e){qe(),He.write(""),He.write(e.replace(/\n/g,"\r\n")),He.write("")}function tn(){He&&He.reset(),Ye=null,Je="",Ke.classList.remove("pf-xterm-bandeau","pf-xterm-collapsed"),Xe&&(Xe.textContent="∨")}function an(){m.style.display="none",Ke.style.display="block",Ke.classList.remove("pf-xterm-overlay"),qe(),$e.fit(),He.focus()}function rn(e){Ke.style.display="block",Ke.classList.add("pf-xterm-overlay"),qe(),$e.fit(),e&&(tn(),He.write("── Erreur ──────────────────────────────────────\r\n\r\n"),He.write(e.replace(/\n/g,"\r\n")+"\r\n"))}function on(){if(Ke.style.display="none",Ke.classList.remove("pf-xterm-overlay"),m.style.display="",Ye){const e=Ye;Ye=null,Je="",e(null)}}window._pfTermWrite=en,window._pfTermWriteErr=nn,window._pfShowWatchdogError=e=>{V(),H(`draw() a dépassé ${e}ms — sketch arrêté (watchdog).`)},window._pfShowErrorTerminal=()=>{V(),rn("")}}(I,L,S,C,P,R)})}();
1
+ !function(){"use strict";const e=document.currentScript;let n=!1;const t="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js",a="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js",r="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js",o="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js",i="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js",s="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js",d="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js",l="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js",c="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css",p="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js",f="https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js",m="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js",_="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css",u="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js",h="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js",y="https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.8.0/lib/addon-unicode11.min.js",b="html, body {\n height: 100%; margin: 0; overflow: hidden;\n background: #111;\n}\n#pf-root {\n position: fixed; inset: 0;\n display: flex; flex-direction: column;\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n}\n\n/* ── app area ── */\n#pf-app:focus { outline: none; }\n#pf-app {\n flex: 1; min-height: 0;\n position: relative;\n background: #111;\n display: flex; align-items: center; justify-content: center;\n overflow: hidden;\n}\n#pf-viewport {\n transform-origin: 50% 50%;\n will-change: transform;\n}\n#pf-viewport canvas {\n display: block;\n outline: none;\n}\n#pf-loader {\n position: absolute; inset: 0;\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n gap: 14px;\n background: #111;\n color: #565f89;\n font-size: 13px;\n z-index: 50;\n pointer-events: none;\n}\n#pf-loader-bar {\n width: 160px; height: 2px;\n background: #2a2c3e;\n border-radius: 2px;\n overflow: hidden;\n}\n#pf-loader-bar::after {\n content: '';\n display: block;\n height: 100%;\n width: 40%;\n background: #7aa2f7;\n border-radius: 2px;\n animation: pf-slide 1.2s ease-in-out infinite;\n}\n@keyframes pf-slide {\n 0% { transform: translateX(-100%); }\n 100% { transform: translateX(350%); }\n}\n\n/* ── drawer (slide-up editor panel) ── */\n#pf-drawer {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n background: #1a1b26;\n height: 32px; /* collapsed = handle only */\n transition: height 0.26s cubic-bezier(.4, 0, .2, 1);\n overflow: hidden;\n /* shadow cast upward onto the app */\n box-shadow: 0 -4px 20px rgba(0,0,0,.55);\n}\n#pf-drawer.pf-open {\n height: var(--pf-drawer-h, 56vh);\n}\n\n/* ── handle bar ── */\n#pf-handle {\n height: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n padding: 0 8px 0 6px;\n background: #24283b;\n border-top: 1px solid #3d4166;\n cursor: ns-resize;\n user-select: none;\n gap: 6px;\n flex-shrink: 0;\n}\n/* grip zone: clickable to toggle, draggable to resize */\n#pf-grip {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 5px 6px;\n flex-shrink: 0;\n opacity: .5;\n border-radius: 4px;\n transition: opacity .15s, background .15s;\n cursor: pointer;\n}\n#pf-grip:hover { opacity: .85; background: rgba(255,255,255,.06); }\n#pf-grip span {\n display: block;\n width: 16px; height: 2px;\n background: #a9b1d6;\n border-radius: 1px;\n}\n#pf-handle-hint {\n flex: 1;\n color: #565f89;\n font-size: 10px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n#pf-handle-btns {\n display: flex;\n gap: 4px;\n flex-shrink: 0;\n}\n.pf-btn {\n height: 26px;\n min-width: 26px;\n padding: 0 5px;\n border: 0; border-radius: 5px;\n cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n font-size: 13px; line-height: 1;\n white-space: nowrap;\n transition: background .15s, transform .1s, opacity .15s;\n outline: none;\n box-sizing: border-box;\n}\n.pf-btn:active { transform: scale(.88); }\n.pf-btn:focus-visible { outline: 2px solid #7aa2f7; outline-offset: 1px; }\n\n#pf-btn-run { background: #1a6b3a; color: #9ece6a; font-size: 11px; }\n#pf-btn-run:hover { background: #1f8447; color: #b9f27a; }\n#pf-btn-run.pf-running { opacity: .5; cursor: not-allowed; }\n\n#pf-btn-code { background: #2a2c3e; color: #7aa2f7; font-size: 14px; }\n#pf-btn-code:hover { background: #3d4166; color: #c0caf5; }\n#pf-btn-code.pf-active { background: #3d4166; color: #e0af68; }\n\n#pf-btn-dl { background: #2a2c3e; color: #9d7cd8; font-size: 14px; }\n#pf-btn-dl:hover { background: #3d4166; color: #bb9af7; }\n\n#pf-btn-rec { background: #2a2c3e; color: #f7768e; font-size: 13px; }\n#pf-btn-rec:hover { background: #3d4166; color: #ff9e9e; }\n#pf-btn-rec.pf-recording { background: #6b1a1a; color: #f7768e; animation: pf-blink .8s step-end infinite; }\n@keyframes pf-blink { 50% { opacity: .4; } }\n\n#pf-btn-reset { background: #2a2c3e; color: #e0af68; font-size: 16px; }\n#pf-btn-reset:hover { background: #3d4166; color: #ffc777; }\n#pf-btn-reset.pf-dirty::after {\n content: '●';\n position: absolute;\n top: 2px; right: 3px;\n font-size: 7px;\n color: #e0af68;\n line-height: 1;\n}\n#pf-btn-reset { position: relative; }\n\n/* ── editor area inside drawer ── */\n#pf-editor-wrap {\n flex: 1;\n min-height: 80px;\n position: relative;\n display: flex;\n flex-direction: column;\n}\n#pf-ace { flex: 1; position: relative; min-height: 0; }\n\n/* ── tab bar ── */\n#pf-tabs {\n display: flex;\n flex-shrink: 0;\n background: #1a1b2e;\n border-bottom: 1px solid #414868;\n overflow-x: auto;\n scrollbar-width: none;\n}\n#pf-tabs:empty { display: none; }\n.pf-tab {\n padding: 5px 14px;\n font-size: 12px;\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n color: #737aa2;\n cursor: pointer;\n white-space: nowrap;\n transition: color .15s, border-color .15s;\n}\n.pf-tab:hover { color: #c0caf5; }\n.pf-tab.pf-tab-active { color: #c0caf5; border-bottom-color: #7aa2f7; }\n.pf-tab.pf-tab-readonly::after { content: ' 🔒'; font-size: 10px; opacity: .6; }\n.pf-tab.pf-tab-markdown::after { content: ' ✎'; font-size: 11px; opacity: .6; }\n\n/* ── markdown view ── */\n@import url('https://fonts.googleapis.com/css2?family=Alegreya+Sans:ital,wght@0,400;0,700;1,400&family=Fira+Code:wght@300..700&display=swap');\n\n#pf-markdown-view {\n flex: 1;\n overflow: auto;\n background: #f4f4f0;\n}\n\n#pf-markdown-view .pf-md-inner {\n width: 100%;\n max-width: 680px;\n margin: 0 auto;\n padding: 48px 48px 72px;\n box-sizing: border-box;\n font-family: 'Alegreya Sans', Georgia, serif;\n font-size: 17px;\n line-height: 1.8;\n color: #1c1c2e;\n}\n\n#pf-markdown-view h1 {\n font-size: 2.1em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 0 0 .3em;\n padding-bottom: .3em;\n border-bottom: 2px solid #d8d8e8;\n line-height: 1.2;\n}\n#pf-markdown-view h2 {\n font-size: 1.4em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 2em 0 .5em;\n padding-bottom: .2em;\n border-bottom: 1px solid #e0e0ec;\n}\n#pf-markdown-view h3 {\n font-size: 1.1em;\n font-weight: 700;\n color: #2a2a4a;\n margin: 1.6em 0 .4em;\n}\n\n#pf-markdown-view p { margin: .75em 0; }\n#pf-markdown-view ul,\n#pf-markdown-view ol { padding-left: 1.6em; margin: .75em 0; }\n#pf-markdown-view li { margin: .3em 0; }\n#pf-markdown-view hr { border: none; border-top: 1px solid #dde; margin: 2em 0; }\n#pf-markdown-view blockquote {\n margin: 1em 0;\n padding: .5em 1em;\n border-left: 3px solid #aab;\n color: #555;\n background: #ededf5;\n border-radius: 0 4px 4px 0;\n}\n\n#pf-markdown-view code {\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n font-size: .84em;\n background: #e8e8f2;\n color: #3a3a6a;\n padding: .15em .45em;\n border-radius: 4px;\n}\n#pf-markdown-view pre {\n background: #1a1b2e;\n border-radius: 8px;\n padding: 1em 1.2em;\n overflow: auto;\n margin: 1.2em 0;\n box-shadow: 0 2px 8px rgba(0,0,0,.12);\n}\n#pf-markdown-view pre code {\n background: transparent;\n color: #c0caf5;\n font-size: .86em;\n padding: 0;\n line-height: 1.6;\n border-radius: 0;\n}\n\n#pf-markdown-view table {\n border-collapse: collapse;\n width: 100%;\n margin: 1.2em 0;\n font-size: .95em;\n}\n#pf-markdown-view th {\n background: #e4e4f0;\n color: #1c1c2e;\n font-weight: 700;\n text-align: left;\n padding: .55em .85em;\n border: 1px solid #d0d0e8;\n}\n#pf-markdown-view td {\n padding: .5em .85em;\n border: 1px solid #e0e0ee;\n vertical-align: top;\n}\n#pf-markdown-view tr:nth-child(even) td { background: #f0f0f8; }\n\n#pf-markdown-view a {\n color: #3a5fc8;\n text-decoration: none;\n border-bottom: 1px solid rgba(58,95,200,.3);\n transition: color .15s, border-color .15s;\n}\n#pf-markdown-view a:hover { color: #1a3fa0; border-bottom-color: #1a3fa0; }\n\n#pf-markdown-view .katex-display {\n overflow-x: auto;\n padding: .5em 0;\n margin: 1.2em 0;\n}\n#pf-markdown-view .mermaid {\n text-align: center;\n margin: 1.5em 0;\n background: #ededf5;\n border-radius: 8px;\n padding: 1em;\n}\n\n/* ── error panel (below editor, never overlaps ACE) ── */\n#pf-err {\n flex-shrink: 0;\n max-height: 120px;\n overflow: auto;\n margin: 0; padding: 8px 13px;\n font-size: 11.5px; line-height: 1.45;\n background: rgba(13, 3, 3, .95);\n color: #f7768e;\n white-space: pre-wrap;\n display: none;\n border-top: 1px solid rgba(247, 118, 142, .35);\n}\n/* ── xterm terminal ── */\n#pf-xterm {\n display: none;\n position: absolute;\n inset: 0;\n padding: 10px 12px;\n box-sizing: border-box;\n background: #000000;\n overflow: hidden;\n}\n\n#pf-xterm.pf-xterm-overlay {\n background: rgba(0, 0, 0, 0.82);\n}\n\n/* Bandeau bas — mode p5 print() */\n#pf-xterm.pf-xterm-bandeau {\n inset: auto 0 0 0;\n height: 200px;\n background: rgba(0, 0, 0, 0.85);\n border-top: 1px solid rgba(255,255,255,0.1);\n padding: 0 12px 10px;\n}\n#pf-xterm.pf-xterm-bandeau.pf-xterm-collapsed {\n height: 24px;\n padding: 0;\n overflow: hidden;\n}\n\n/* Poignée du bandeau */\n#pf-xterm-handle {\n display: none;\n height: 24px;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n color: rgba(255,255,255,0.4);\n font-size: 11px;\n letter-spacing: 2px;\n user-select: none;\n gap: 8px;\n}\n#pf-xterm-handle:hover { color: rgba(255,255,255,0.8); }\n.pf-xterm-bandeau #pf-xterm-handle { display: flex; }\n\n/* xterm interne : prendre toute la hauteur disponible sous la poignée */\n#pf-xterm .xterm {\n height: 100%;\n}\n#pf-xterm.pf-xterm-bandeau .xterm {\n height: calc(100% - 24px);\n}\n#pf-xterm .xterm-screen {\n height: 100% !important;\n}\n\n/* ═══════════════════════════════════════════════════════\n data-no-editor : canvas-only mode — no slide-up editor\n ═══════════════════════════════════════════════════════ */\n#pf-root.pf-no-editor #pf-drawer {\n display: none;\n}\n/* Errors shown as floating overlay at the bottom of the canvas */\n#pf-root.pf-no-editor #pf-err {\n display: none;\n position: absolute;\n bottom: 0; left: 0; right: 0;\n max-height: 40%;\n z-index: 20;\n border-top: 1px solid rgba(247, 118, 142, .45);\n}\n#pf-root.pf-no-editor #pf-err.pf-err-visible {\n display: block;\n}\n\n/* ═══════════════════════════════════════════════════════\n data-pyfrilet-detached : off-screen render container.\n reveal-pyfrilet.js moves #pf-sketch into a visible panel\n and drives the sketch via window.pyfrilet_on_step(n).\n ═══════════════════════════════════════════════════════ */\n#pf-root.pf-detached {\n position: fixed !important;\n left: -10000px !important;\n top: 0 !important;\n width: var(--pf-detached-w, 560px) !important;\n height: var(--pf-detached-h, 420px) !important;\n pointer-events: none;\n box-shadow: none !important;\n z-index: -1 !important;\n}\n/* Once #pf-sketch is moved into a host slot, it must be a transparent\n pass-through: no background, no intrinsic size of its own.\n display:contents makes it invisible as a box — the canvas becomes a\n direct child of whatever container holds #pf-sketch. */\n#pf-root.pf-detached #pf-sketch {\n display: contents;\n}\n/* Canvas pointer-events re-enabled once #pf-sketch is moved to the host panel */\n#pf-sketch {\n pointer-events: auto;\n}\n",g='<div id="pf-root">\n <div id="pf-app" tabindex="-1">\n <div id="pf-viewport"><div id="pf-sketch"></div></div>\n <div id="pf-xterm"><div id="pf-xterm-handle"><span id="pf-xterm-chevron">∧</span><span id="pf-xterm-handle-label">console</span></div></div>\n <div id="pf-loader">\n <span id="pf-loader-msg">Chargement…</span>\n <div id="pf-loader-bar"></div>\n </div>\n </div>\n <div id="pf-drawer">\n <div id="pf-handle">\n <div id="pf-grip" title="Clic → ouvrir/fermer"><span></span><span></span><span></span></div>\n <span id="pf-handle-hint">Clic ☰ → ouvrir/fermer &nbsp;·&nbsp; Shift+Entrée → relancer</span>\n <div id="pf-handle-btns">\n <button class="pf-btn" id="pf-btn-run" title="Relancer (Shift+Entrée)">&#9654;</button>\n <button class="pf-btn" id="pf-btn-code" title="Éditeur plein écran">&#9999;&#xFE0F;</button>\n <button class="pf-btn" id="pf-btn-dl" title="Télécharger HTML autonome">&#128190;</button>\n <button class="pf-btn" id="pf-btn-rec" title="Enregistrer WebM">⏺</button>\n <button class="pf-btn" id="pf-btn-help" title="Aide">?</button>\n <button class="pf-btn" id="pf-btn-reset" title="Réinitialiser le code (Ctrl+R)">&#8635;</button>\n </div>\n </div>\n <div id="pf-editor-wrap">\n <div id="pf-tabs"></div>\n <div id="pf-markdown-view" style="display:none"></div>\n <div id="pf-ace"></div>\n </div>\n <pre id="pf-err"></pre>\n </div>\n</div>';document.addEventListener("DOMContentLoaded",function(){const w=[...document.querySelectorAll('script[type="text/python"], script[type="text/markdown"], python')];if(0===w.length)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const 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?f:null,mermaid:E?m:null,xtermCss:_,xterm:u,xtermFit:h,xtermUni:y}:{p5:k+"p5.min.js",pyodide:k+"pyodide/pyodide.js",pyodideIndex:k+"pyodide/",ace:k+"ace.min.js",acePython:k+"mode-python.min.js",aceMonokai:k+"theme-monokai.min.js",aceLangTools:k+"ext-language_tools.min.js",aceSearchbox:k+"ext-searchbox.min.js",marked:E?k+"marked.min.js":null,katexCss:E?k+"katex.min.css":null,katex:E?k+"katex.min.js":null,markedKatex:E?k+"marked-katex-extension.js":null,mermaid:E?k+"mermaid.min.js":null,xtermCss:k+"xterm.min.css",xterm:k+"xterm.min.js",xtermFit:k+"xterm-addon-fit.min.js",xtermUni:k+"addon-unicode11.min.js"},S="pyfrilet:"+location.pathname,L=w.map((e,n)=>{const t="text/markdown"===e.getAttribute("type")?"markdown":"python",a=e.hasAttribute("data-hidden"),r=e.hasAttribute("data-readonly");let o=e.getAttribute("data-tab");null!==o||a||(o=1===w.length?"Code":`Bloc ${n+1}`);const i=e.textContent.replace(/^\n/,"");return{id:"tab-"+n,label:o,hidden:a,readonly:r,type:t,starterCode:i,code:i}}),j=e=>{try{return localStorage.getItem(e)}catch(e){return null}},I=v.hasAttribute("data-pyfrilet-detached");let z,T=null;if(I)z=L;else{const e=j(S);if(e)try{T=JSON.parse(e)}catch(e){T=null}if(T&&1===T.v&&Array.isArray(T.tabs)&&T.tabs.length>0){const e=e=>`${e.label}|${e.type}|${e.hidden?1:0}|${e.readonly?1:0}`;T.tabs.map(e).join(",")!==L.map(e).join(",")&&(T._stale=!0)}z=T&&1===T.v&&Array.isArray(T.tabs)&&T.tabs.length>0?T.tabs.map((e,n)=>{const t=L.find(n=>n.label===e.label&&n.type===e.type)||null;return{id:"tab-"+n,label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,starterCode:t?t.starterCode:e.content,code:e.content}}):L.map((e,n)=>{if(!e.hidden&&!e.readonly&&"python"===e.type){const t=e.label?e.label.replace(/[^a-zA-Z0-9]/g,"_"):String(n);let a=j(S+":"+t);if(a||"Code"!==e.label||1!==L.length||(a=j(S)),a&&a.trim())return{...e,code:a}}return e})}const R=v.hasAttribute("data-no-watchdog"),P=I||v.hasAttribute("data-no-editor"),A=!I&&!(!T||!T._stale);!function(e,t,a,r,o,i,s,d){e=e.slice();let l=i;const c=document.createElement("style");c.textContent=b,document.head.appendChild(c),d?document.body.insertAdjacentHTML("beforeend",g):document.body.innerHTML=g;const p=document.getElementById("pf-app"),f=document.getElementById("pf-drawer"),m=document.getElementById("pf-handle"),_=document.getElementById("pf-sketch"),u=document.getElementById("pf-viewport"),h=document.getElementById("pf-loader"),y=document.getElementById("pf-loader-msg"),w=document.getElementById("pf-err"),v=document.getElementById("pf-btn-run"),x=document.getElementById("pf-btn-code"),k=document.getElementById("pf-btn-dl"),E=document.getElementById("pf-btn-rec"),C=document.getElementById("pf-btn-reset"),S=document.getElementById("pf-btn-help"),L=document.getElementById("pf-grip"),j=document.getElementById("pf-handle-hint"),I=document.getElementById("pf-tabs"),z=document.getElementById("pf-markdown-view"),T=document.getElementById("pf-root");s&&T.classList.add("pf-no-editor");d&&T.classList.add("pf-detached");d&&(window.__pyfriletSketchEl=_);let R=!1,P=Math.round(.56*window.innerHeight);function A(){document.documentElement.style.setProperty("--pf-drawer-h",P+"px")}function M(){R=!0,f.classList.add("pf-open"),x.classList.add("pf-active"),setTimeout(()=>{V(),ee&&ee.focus()},280)}function B(){R=!1,f.classList.remove("pf-open"),x.classList.remove("pf-active"),setTimeout(()=>{V();const e=Z._p?.canvas;e&&e.removeAttribute("tabindex"),p.focus()},280)}function O(){R?B():M()}A();let F=null;const W=5,D=120,N=document.createElement("div");function U(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;F={y:n,h:R?P:0,moved:!1},N.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function K(e){if(!F)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=F.y-n;if(Math.abs(t)>W&&(F.moved=!0),!F.moved)return;const a=Math.max(0,Math.min(window.innerHeight-50,F.h+t));a<D?(f.style.transition="none",f.style.height="32px"):(P=a,A(),R||M(),f.style.transition="none",f.style.height=P+"px"),V()}function H(e){if(!F)return;const n=F.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??F.y,a=F.y-t,r=F.h+a;F=null,N.style.display="none",document.body.style.userSelect="",f.style.transition="",f.style.height="",n&&(r<D?B():(P=Math.max(D,Math.min(window.innerHeight-50,r)),A(),R||M()),V())}Object.assign(N.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(N),L.addEventListener("click",e=>{e.stopPropagation(),O()}),m.addEventListener("mousedown",U,!0),document.addEventListener("mousemove",K),document.addEventListener("mouseup",H),m.addEventListener("touchstart",U,{passive:!1}),document.addEventListener("touchmove",K,{passive:!0}),document.addEventListener("touchend",H);let $=0,Y=0;function J(e){w.textContent=e,s?(w.classList.add("pf-err-visible"),w.style.display="block"):(w.style.display="block",M())}function q(){w.textContent="",w.style.display="none",w.classList.remove("pf-err-visible")}function G(){if(d)return;if(!Z._p||"fit"!==Z._mode)return;const e=Z._w,n=Z._h;if(!e||!n)return;const t=p.clientWidth,a=p.clientHeight,r=Math.min(t/e,a/n);u.style.transform=`scale(${r})`}function V(){if("fullscreen"===Z._mode?Z.size("max"):G(),X&&"function"==typeof X.windowResized)try{X.windowResized()}catch(e){J(String(e))}ee&&ee.resize()}window.addEventListener("mousemove",e=>{$=e.clientX,Y=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&($=e.touches[0].clientX,Y=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=Z._p?Z._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=Z._w/n.width,a=Z._h/n.height;return[($-n.left)*t,(Y-n.top)*a]},window.addEventListener("resize",V);let X=null;const Z=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=p.clientWidth,this._h=p.clientHeight,void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),u.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),G())},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){j.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 Q(){if(Ke(),X){try{X.remove()}catch(e){}X=null}_.innerHTML="",Z._p=null,Z._mode="fit",Z._w=0,Z._h=0,u.style.transform="scale(1)",j.textContent="Shift+Entrée → relancer  ·  Échap → ouvrir/fermer",xe&&(xe.destroy(),xe=null),we&&(we.destroy(),we=null),ve&&(ve.destroy(),ve=null),ke&&(ke.destroy(),ke=null),Ee&&(Ee.destroy(),Ee=null),Ce&&(Ce.destroy(),Ce=null),Se&&(Se.destroy(),Se=null),Le&&(Le.destroy(),Le=null),je&&(je.destroy(),je=null),Ie&&(Ie.destroy(),Ie=null),ze&&(ze.destroy(),ze=null),Te&&(Te.destroy(),Te=null),Re&&(Re.destroy(),Re=null),Pe&&(Pe.destroy(),Pe=null)}window.p5py=Z;let ee=null,ne=null;const te={},ae=new Set;function re(){I.innerHTML="",ne=null;const n=e.filter(e=>!e.hidden);I.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",()=>oe(e)),I.appendChild(n)}),n.length>0&&oe(n[0],!0)}function oe(e,n){if(n||ne!==e)if(ne=e,I.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",z.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>`)),z.innerHTML=`<div class="pf-md-inner">${n}</div>`}else z.innerHTML=`<div class="pf-md-inner"><pre>${e.starterCode}</pre></div>`;window.mermaid&&mermaid.run({nodes:z.querySelectorAll(".mermaid")})}else document.getElementById("pf-ace").style.display="block",z.style.display="none",ee&&te[e.id]&&(ee.setSession(te[e.id]),ee.setReadOnly(e.readonly),ee.focus())}function ie(){let n=1;e.filter(e=>"python"===e.type).forEach(e=>{!e.hidden&&te[e.id]?(te[e.id].setOption("firstLineNumber",n),n+=te[e.id].getLength()):n+=e.code.split("\n").length})}function se(){Object.keys(te).forEach(e=>delete te[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),te[e.id]=n,!e.readonly){let e=null;n.on("change",()=>{null!==e&&(clearTimeout(e),ae.delete(e)),e=setTimeout(()=>{ae.delete(e),e=null,le()},350),ae.add(e),ie(),pe()})}});const n=e.find(e=>!e.hidden&&"python"===e.type);ee&&n&&te[n.id]&&(ee.setSession(te[n.id]),ee.setReadOnly(n.readonly),ee.renderer.updateFull(!0)),ie()}function de(){!r.ace.startsWith("vendor")&&r.ace.startsWith("http")||ace.config.set("basePath",r.ace.replace(/\/[^/]+$/,"/")),ee=ace.edit("pf-ace"),ee.setTheme("ace/theme/monokai"),ee.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),document.getElementById("pf-ace").addEventListener("keydown",e=>{e.shiftKey&&"Enter"===e.key&&(e.stopImmediatePropagation(),e.preventDefault(),ee.completer?.popup?.isOpen?ee.completer.detach():Oe())},!0),ee.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>Oe()}),ee.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:B}),ee.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:ce}),ee.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&fe()}}),se(),re(),pe()}function le(){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||!te[e.id]?e.code:te[e.id].getValue()}))};try{localStorage.setItem(a,JSON.stringify(n))}catch(e){}}function ce(){le()}function pe(){const n=l||e.some(e=>!e.hidden&&!e.readonly&&"python"===e.type&&te[e.id]&&te[e.id].getValue()!==e.starterCode);C.classList.toggle("pf-dirty",n)}function fe(){ae.forEach(e=>clearTimeout(e)),ae.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){}l=!1,e=t.map((e,n)=>({...e,id:"tab-"+n,code:e.starterCode})),se(),re(),pe(),Oe()}window.addEventListener("beforeunload",ce);let me=null,_e=null;async function ue(){return _e||(_e=(async()=>{const e={};r.pyodideIndex&&(e.indexURL=r.pyodideIndex),me=await loadPyodide(e),await me.loadPackage(["rich","pygments"]);try{const e=new Uint8Array(new SharedArrayBuffer(1));me.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{me.FS.mkdirTree(e);try{me.FS.mount(me.FS.filesystems.IDBFS,{},e)}catch(e){if(10!==e.errno)return void t(e)}me.FS.syncfs(!0,e=>e?t(e):n())}catch(e){t(e)}}),window._pfSyncIdbfs=()=>new Promise((e,n)=>{me.FS.syncfs(!1,t=>t?n(t):e())}),me.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"),me.runPython("\nimport asyncio as _asyncio, ast as _ast\nimport os as _os, sys as _sys\n_os.environ.setdefault('TERM', 'xterm-256color')\n_os.environ.setdefault('COLORTERM', 'truecolor')\n\n# Wrapper file-like qui écrit directement vers xterm via JS.\n# Rich écrit des strings sur sys.stdout.write() — il faut un vrai objet fichier.\nclass _PfStream:\n def __init__(self, js_fn):\n self._fn = js_fn\n self.encoding = 'utf-8'\n self.errors = 'replace'\n def write(self, s):\n if s:\n self._fn(s)\n return len(s)\n def writelines(self, lines):\n for l in lines: self.write(l)\n def flush(self): pass\n def isatty(self): return True\n @property\n def softspace(self): return 0\n\nfrom js import _pfTermWrite, _pfTermWriteErr\n_sys.stdout = _PfStream(_pfTermWrite)\n_sys.stderr = _PfStream(_pfTermWriteErr)\n\nfrom rich.console import Console as _RichConsole\n_pf_rich_console = _RichConsole(stderr=True)\n\nasync def _pf_async_input(prompt=\"\"):\n from js import _pfTerminalInput\n result = await _pfTerminalInput(str(prompt) if prompt else \"\")\n return result\n\n# Cancellation flag — set by JS before launching a new run\n_pf_cancel_run = [False]\n\nasync def _pf_async_sleep(seconds):\n \"\"\"Cancellable sleep: polls the cancel flag every 50 ms.\"\"\"\n import asyncio as _aio\n deadline = _aio.get_event_loop().time() + seconds\n while True:\n if _pf_cancel_run[0]:\n raise KeyboardInterrupt\n remaining = deadline - _aio.get_event_loop().time()\n if remaining <= 0:\n break\n await _aio.sleep(min(0.05, remaining))\n\nasync def _pf_maybe_await(val):\n \"\"\"Await val if it's a coroutine, otherwise return it as-is.\n This lets us await-ify every call site without knowing in advance\n which functions are async.\"\"\"\n import asyncio\n if asyncio.iscoroutine(val):\n return await val\n return val\n\nclass _AsyncTransformer(_ast.NodeTransformer):\n \"\"\"Transforms user code so that:\n - every def becomes async def (including nested and class methods)\n - every call f(...) becomes await _pf_maybe_await(f(...))\n whether f is a Name, Attribute, subscript, or any other expression\n - lambda bodies are left untouched (can't be async)\n \"\"\"\n _HELPER = '_pf_maybe_await'\n\n def visit_FunctionDef(self, node):\n # Dunder methods (__init__, __str__...) are called by Python internals\n # without going through _pf_maybe_await — they must stay synchronous.\n self.generic_visit(node)\n if node.name.startswith('__') and node.name.endswith('__'):\n return node\n return _ast.AsyncFunctionDef(\n name=node.name,\n args=node.args,\n body=node.body,\n decorator_list=node.decorator_list,\n returns=node.returns,\n lineno=node.lineno,\n col_offset=node.col_offset,\n end_lineno=node.end_lineno,\n end_col_offset=node.end_col_offset,\n )\n\n # Leave Lambda untouched — can't be async\n def visit_Lambda(self, node):\n return node\n\n def visit_Await(self, node):\n # User wrote explicit await f() — recurse into f()'s arguments\n # but don't re-wrap the call itself with _pf_maybe_await.\n self._skip_next_wrap = True\n self.generic_visit(node)\n self._skip_next_wrap = False\n return node\n\n def visit_Call(self, node):\n # Recurse first so nested calls are also transformed\n self.generic_visit(node)\n func = node.func\n # time.sleep(x) → _pf_async_sleep(x) (cancellable)\n if (isinstance(func, _ast.Attribute)\n and isinstance(func.value, _ast.Name)\n and func.value.id == 'time'\n and func.attr == 'sleep'):\n node.func = _ast.Name(id='_pf_async_sleep', ctx=_ast.Load())\n # sleep(x) → _pf_async_sleep(x) (from time import sleep)\n elif isinstance(func, _ast.Name) and func.id == 'sleep':\n node.func = _ast.Name(id='_pf_async_sleep', ctx=_ast.Load())\n # asyncio.sleep(x) → _pf_async_sleep(x) (cancellable)\n elif (isinstance(func, _ast.Attribute)\n and isinstance(func.value, _ast.Name)\n and func.value.id == 'asyncio'\n and func.attr == 'sleep'):\n node.func = _ast.Name(id='_pf_async_sleep', ctx=_ast.Load())\n # If already under a user-written await, don't re-wrap\n if getattr(self, '_skip_next_wrap', False):\n self._skip_next_wrap = False\n return node\n # Wrap: f(...) → await _pf_maybe_await(f(...))\n helper = _ast.Name(id=self._HELPER, ctx=_ast.Load())\n wrapped = _ast.Call(func=helper, args=[node], keywords=[])\n return _ast.Await(value=wrapped)\n\nasync def _pf_run_terminal(source):\n tree = _ast.parse(source)\n tree = _AsyncTransformer().visit(tree)\n\n wrapper = _ast.parse(\"async def programme(): pass\")\n wrapper.body[0].body = tree.body if tree.body else [_ast.Pass()]\n _ast.fix_missing_locations(wrapper)\n _pf_run_counter[0] += 1\n _pf_fname = f'programme_{_pf_run_counter[0]}'\n with open(_pf_fname, 'w') as _f:\n _f.write(source)\n lines = source.splitlines(keepends=True)\n _linecache.cache[_pf_fname] = (len(source), None, lines, _pf_fname)\n import asyncio as _asyncio\n _pf_cancel_run[0] = False # reset at start of each run\n _ns = {\n 'input': _pf_async_input,\n 'persist': _pf_persist,\n '_pf_maybe_await': _pf_maybe_await,\n 'asyncio': _asyncio,\n '_pf_async_sleep': _pf_async_sleep,\n }\n exec(compile(wrapper, _pf_fname, 'exec'), _ns)\n try:\n await _ns['programme']()\n except (SystemExit, KeyboardInterrupt):\n pass\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and _tb.tb_frame.f_code.co_filename != _pf_fname:\n _tb = _tb.tb_next\n if _tb:\n _e.__traceback__ = _tb\n _pf_rich_console.print_exception(extra_lines=8, show_locals=True)\n"),ee){he(me.runPython("list(m.__all__)").toJs())}})(),_e)}function he(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&&/[a-zA-Z_]/.test(r)?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,ee.completers=[...ee.completers||[],t]}let ye=!1,be=!1,ge=0,we=null,ve=null,xe=null,ke=null,Ee=null,Ce=null,Se=null,Le=null,je=null,Ie=null,ze=null,Te=null,Re=null,Pe=null;const Ae=300;function Me(e){return!/\bfrom\s+p5\s+import\b|\bimport\s+p5\b/.test(e)}async function Be(e){const n=++ge;if(be){if(me)try{me.globals.get("_pf_cancel_run")[0]=!0}catch(e){}ln();const e=Date.now()+3e3;for(;be&&Date.now()<e;)await new Promise(e=>setTimeout(e,20))}if(n===ge){be=!0,sn(),on(),q(),window._pfSetP5Mode&&window._pfSetP5Mode(!1),await window._pfMountIdbfs("/persist");try{const n=me.globals.get("_pf_run_terminal");await n(e)}catch(e){const n=String(e);n.includes("SystemExit")||rn(n+"\n")}finally{be=!1}}}async function Oe(){if(ye){if(!be)return;window._pfInterrupt&&window._pfInterrupt(),ln(),ye=!1,v.classList.remove("pf-running"),await new Promise(e=>setTimeout(e,80))}ye=!0,v.classList.add("pf-running"),q(),on(),Q(),me||(y.textContent="Initialisation de Pyodide…",h.style.display="flex");try{await ue()}catch(e){return h.style.display="none",J("Erreur Pyodide : "+(e.message||String(e))),ye=!1,void v.classList.remove("pf-running")}h.style.display="none";const t=e.filter(e=>"python"===e.type).map(e=>e.hidden||e.readonly||!te[e.id]?e.code:te[e.id].getValue()).join("\n");try{y.textContent="Chargement des dépendances…",h.style.display="flex",await me.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:n})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}if(h.style.display="none",Me(t))return v.classList.remove("pf-running"),await Be(t),void(ye=!1);ln(),window._pfSetP5Mode&&window._pfSetP5Mode(!0),await window._pfMountIdbfs("/persist"),me.globals.set("_USER_CODE",t);const a=me.globals.get("_pf_exec_user_code");try{if(!a())return ye=!1,void v.classList.remove("pf-running");me.runPython("_ns = _ns_ref[0]")}catch(e){return dn(e.message||String(e)),ye=!1,void v.classList.remove("pf-running")}let r,i,s,d,l,c,p,f,m,u,b,g,w,x;try{const e=(e,n)=>me.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"),m=e("mouseWheel","mouse_wheel"),u=e("doubleClicked","double_clicked"),b=e("keyReleased","key_released"),g=e("touchStarted","touch_started"),w=e("touchMoved","touch_moved"),x=e("touchEnded","touch_ended")}catch(e){return dn(e.message||String(e)),ye=!1,void v.classList.remove("pf-running")}if(!i)return J("Le script doit définir au moins une fonction draw()."),ye=!1,void v.classList.remove("pf-running");const{create_proxy:k}=me.pyimport("pyodide.ffi"),E=me.runPython("_ns.get('windowResized')"),C=me.globals.get("_pf_refresh"),S=me.globals.get(o?"_pf_draw_direct":"_pf_draw_watchdog"),L=me.globals.get("_ns"),j=me.globals.get("_pf_safe_call"),I=e=>e?k(()=>{try{C(L),j(e)}catch(e){dn("")}}):null;xe=l?k(()=>{try{j(l)}catch(e){dn("")}}):null,we=r?k(()=>{try{j(r)}catch(e){dn("")}}):null,ve=k(()=>{try{C(L),S(i,Ae)}catch(e){Q(),dn("")}}),ke=I(s),Ee=I(p),Ce=I(c),Se=I(f),Le=I(m),je=I(u),Ie=I(d),ze=I(b),Te=I(g),Re=I(w),Pe=I(x);const z=E?k(()=>{try{j(E)}catch(e){dn("")}}):null;let T=!1;X=new p5(e=>{Z._setP(e),xe&&(e.preload=()=>{xe()}),e.setup=()=>{we&&we(),e.canvas||Z.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),T=!0},e.draw=()=>{T&&ve()},e.mousePressed=()=>{T&&ke&&ke()},e.mouseReleased=()=>{T&&Ee&&Ee()},e.mouseDragged=()=>{T&&Ce&&Ce()},e.mouseMoved=()=>{T&&Se&&Se()},e.mouseWheel=e=>{T&&Le&&Le()},e.doubleClicked=()=>{T&&je&&je()},e.keyPressed=()=>{T&&Ie&&Ie()},e.keyReleased=()=>{T&&ze&&ze()},Te&&(e.touchStarted=()=>{T&&Te()}),Re&&(e.touchMoved=()=>{T&&Re()}),Pe&&(e.touchEnded=()=>{T&&Pe()}),e.windowResized=()=>{"fullscreen"===Z._mode?Z.size("max"):G(),z&&z()}},_),ye=!1,v.classList.remove("pf-running"),window.dispatchEvent(new CustomEvent("pyfrilet:ready"))}const Fe='<!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.7.1/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\nFILLME-SCRIPTS\n\n</body>\n</html>';function We(){const n=e.map((e,n)=>{let t;t="python"!==e.type||e.hidden||e.readonly||!te[e.id]?e.code:te[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=Fe.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 De=null,Ne=[];function Ue(){const e=Z._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();De=new MediaRecorder(t,{mimeType:n}),Ne=[],De.ondataavailable=e=>{e.data.size&&Ne.push(e.data)},De.onstop=()=>{const e=new Blob(Ne,{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),E.textContent="⏺",E.title="Enregistrer WebM",E.classList.remove("pf-recording"),De=null},De.start(),E.textContent="⏹",E.title="Arrêter l'enregistrement",E.classList.add("pf-recording")}function Ke(){De&&"inactive"!==De.state&&De.stop()}E.addEventListener("click",()=>{De?Ke():Ue()}),v.addEventListener("click",()=>Oe()),x.addEventListener("click",()=>{R?B():(P=window.innerHeight-32,A(),M())}),k.addEventListener("click",We);const He="https://codeberg.org/nopid/pyfrilet";function $e(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)})}S.addEventListener("click",()=>window.open(He,"_blank")),C.addEventListener("click",()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&fe()}),window.addEventListener("keydown",e=>{if(s)return void("Enter"===e.key&&e.shiftKey&&(e.preventDefault(),Oe()));const n=R&&ee&&ee.isFocused&&ee.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void Oe();if("Escape"===e.key){const t=document.querySelector(".ace_search");if(t&&"none"!==t.style.display)return e.preventDefault(),e.stopPropagation(),ee.searchBox?ee.searchBox.hide():t.style.display="none",void ee.focus();if(n){const n=ee.completer?.popup?.isOpen;if(n)return;return e.preventDefault(),e.stopPropagation(),void B()}return e.preventDefault(),e.stopPropagation(),void(R?B():M())}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.")&&fe())):(e.preventDefault(),void ce())}else e.preventDefault()},!0),(async()=>{y.textContent="Chargement des dépendances…",h.style.display="flex";try{if(await $e(r.p5),r.marked){const e=document.createElement("link");e.rel="stylesheet",e.href=r.katexCss,document.head.appendChild(e),await $e(r.marked),await $e(r.katex),await $e(r.markedKatex),await $e(r.mermaid),marked.use(markedKatex({throwOnError:!1})),mermaid.initialize({startOnLoad:!1,theme:"neutral"})}await $e(r.ace),await $e(r.acePython),await $e(r.aceMonokai),await $e(r.aceLangTools),await $e(r.aceSearchbox),await $e(r.pyodide);const e=document.createElement("link");e.rel="stylesheet",e.href=r.xtermCss,document.head.appendChild(e),await $e(r.xterm),await $e(r.xtermFit),await $e(r.xtermUni)}catch(e){return y.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}de(),await Oe(),h.style.display="none"})();const Ye=document.getElementById("pf-xterm");let Je=null,qe=null,Ge=null,Ve="";function Xe(){if(Je)return;Je=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}),qe=new FitAddon.FitAddon,Je.loadAddon(qe);const e=new Unicode11Addon.Unicode11Addon;Je.loadAddon(e),Je.unicode.activeVersion="11",Je.open(Ye),qe.fit(),new ResizeObserver(()=>{Je&&"none"!==Ye.style.display&&qe.fit()}).observe(Ye),Je.onData(e=>{if(Ge)if("\r"===e){const e=Ve;Ve="",Je.write("\r\n");const n=Ge;Ge=null,n(e)}else if(""===e)Ve.length>0&&(Ve=Ve.slice(0,-1),Je.write("\b \b"));else if(""===e){Ve="",Je.write("^C\r\n");const e=Ge;Ge=null,e(null)}else e.charCodeAt(0)>=32&&(Ve+=e,Je.write(e))})}window._pfTerminalInput=function(e){return new Promise(n=>{Ge=n,Ve="",e&&Je.write(e),Je.focus()})};let Ze=!1;window._pfSetP5Mode=e=>{Ze=e};const Qe=document.getElementById("pf-xterm-handle"),en=document.getElementById("pf-xterm-chevron");function nn(){Ye.style.display="block",Ye.classList.remove("pf-xterm-overlay","pf-xterm-collapsed"),Ye.classList.add("pf-xterm-bandeau"),en&&(en.textContent="∨"),Xe(),qe.fit()}function tn(){Ye.classList.contains("pf-xterm-collapsed")?(Ye.classList.remove("pf-xterm-collapsed"),en&&(en.textContent="∨"),qe.fit()):(Ye.classList.add("pf-xterm-collapsed"),en&&(en.textContent="∧"))}Qe&&Qe.addEventListener("click",tn);function an(e){Ze&&"none"===Ye.style.display&&nn(),Je&&Je.write(e)}function rn(e){Xe(),Je.write(""),Je.write(e.replace(/\n/g,"\r\n")),Je.write("")}function on(){Je&&Je.reset(),Ge=null,Ve="",Ye.classList.remove("pf-xterm-bandeau","pf-xterm-collapsed"),en&&(en.textContent="∨")}function sn(){u.style.display="none",Ye.style.display="block",Ye.classList.remove("pf-xterm-overlay"),Xe(),qe.fit(),Je.focus()}function dn(e){Ye.style.display="block",Ye.classList.add("pf-xterm-overlay"),Xe(),qe.fit(),e&&(on(),Je.write("── Erreur ──────────────────────────────────────\r\n\r\n"),Je.write(e.replace(/\n/g,"\r\n")+"\r\n"))}function ln(){if(Ye.style.display="none",Ye.classList.remove("pf-xterm-overlay"),u.style.display="",Ge){const e=Ge;Ge=null,Ve="",e(null)}}window._pfTermWrite=an,window._pfTermWriteErr=rn,window._pfShowWatchdogError=e=>{Q(),J(`draw() a dépassé ${e}ms — sketch arrêté (watchdog).`)},window._pfShowErrorTerminal=()=>{Q(),dn("")}}(z,L,S,C,R,A,P,I)})}();