pyfrilet 0.5.0 → 0.5.2

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
@@ -100,7 +100,11 @@ Ces fonctions sont définies par l'utilisateur et appelées automatiquement par
100
100
  | `touchMoved()` | Contact tactile déplacé. Utile pour le multi-touch (pinch zoom…). |
101
101
  | `touchEnded()` | Fin de contact tactile. |
102
102
 
103
- > **Limite de temps sur `draw()`** : si une exécution de `draw()` dépasse 200 ms, pyfrilet arrête le sketch et affiche une erreur. Cela protège le navigateur contre les boucles accidentellement bloquantes. Pour des calculs lourds, effectuer le travail en dehors de `draw()` (dans `setup()` ou via un état mis à jour progressivement).
103
+ > **Watchdog `draw()`** : pyfrilet surveille le temps d'exécution de `draw()` via `sys.settrace`. Si une exécution dépasse **300 ms**, le sketch est arrêté et une erreur est affichée — cela protège le navigateur contre les boucles infinies accidentelles (`while True: pass`, récursion sans fin…). La trace est vérifiée toutes les 100 lignes Python pour minimiser l'overhead. Pour désactiver ce comportement (calculs intensifs légitimes hors de `draw()`), ajouter `data-no-watchdog` sur la balise `<script src="pyfrilet.js">` :
104
+ >
105
+ > ```html
106
+ > <script src="pyfrilet.js" data-no-watchdog></script>
107
+ > ```
104
108
 
105
109
  #### Fonctions pyfrilet (hors p5.js standard)
106
110
 
@@ -247,7 +251,7 @@ La barre de contrôle est collée en bas de l'écran.
247
251
  | ✏️ | Ouvre l'éditeur en **plein écran** ; referme si déjà ouvert |
248
252
  | 💾 | Télécharge la page en HTML autonome (voir ci-dessous) |
249
253
  | ⏺ | Démarre l'enregistrement WebM du canvas ; devient ⏹ pendant l'enregistrement — cliquer pour arrêter et télécharger |
250
- | ↻ | Réinitialise le code de l'onglet actif à la version d'origine (confirmation demandée). Un point orange apparaît sur le bouton quand le code a été modifié. |
254
+ | ↻ | Réinitialise **tous les onglets** à la version du fichier source (confirmation demandée). Un point orange apparaît sur le bouton dès qu'un onglet a été modifié par rapport à sa version d'origine. |
251
255
 
252
256
  ### Raccourcis clavier
253
257
 
@@ -256,11 +260,11 @@ La barre de contrôle est collée en bas de l'écran.
256
260
  | `Shift+Entrée` | Relance l'exécution (depuis l'éditeur ou depuis le sketch) |
257
261
  | `Échap` | Ouvre le tiroir si fermé, ferme si ouvert |
258
262
  | `Ctrl+S` | Sauvegarde le code dans le localStorage |
259
- | `Ctrl+R` | Réinitialise l'onglet actif à la version d'origine (confirmation demandée) |
263
+ | `Ctrl+R` | Réinitialise **tous les onglets** à la version d'origine (confirmation demandée) |
260
264
 
261
265
  Quand le tiroir se ferme, le focus clavier est automatiquement donné au canvas — les événements `keyPressed()` du sketch fonctionnent sans clic préalable.
262
266
 
263
- Le code est **sauvegardé automatiquement** dans le `localStorage` à chaque modification (par onglet). Il est restauré à l'ouverture de la page.
267
+ Le code est **sauvegardé automatiquement** dans le `localStorage` à chaque modification. pyfrilet enregistre un snapshot JSON complet de tous les onglets (structure, types, contenus — y compris les blocs cachés et Markdown). À la prochaine visite, la page s'affiche exactement telle qu'elle a été laissée, avec le même nombre d'onglets et les mêmes contenus. Cliquer sur ↻ efface le snapshot et restaure la structure et le contenu du fichier HTML source.
264
268
 
265
269
  ### Télécharger
266
270
 
@@ -334,7 +338,11 @@ def draw():
334
338
  </script>
335
339
  ```
336
340
 
337
- ### Onglets Markdown
341
+ ### Rendu des onglets Markdown
342
+
343
+ Les onglets Markdown sont affichés sur fond clair avec une mise en page soignée : largeur de lecture limitée à 680 px, centrage automatique sur grand écran, police [Alegreya Sans](https://fonts.google.com/specimen/Alegreya+Sans) (chargée depuis Google Fonts en mode CDN). Les blocs de code sont rendus sur fond sombre pour un bon contraste. Les diagrammes Mermaid utilisent le thème neutre (clair).
344
+
345
+ En mode déploiement local sans accès internet, la fonte se replie sur Georgia.
338
346
 
339
347
  Le contenu d'un onglet `type="text/markdown"` est rendu avec [marked](https://marked.js.org/) et supporte les formules mathématiques via [KaTeX](https://katex.org/) et les diagrammes via [Mermaid](https://mermaid.js.org/).
340
348
 
@@ -390,6 +398,8 @@ graph LR
390
398
 
391
399
  Les blocs Python sont concaténés dans l'ordre du DOM avant exécution : blocs cachés et visibles sont traités ensemble, dans l'ordre où ils apparaissent dans le HTML. L'onglet actif dans l'éditeur n'influence pas l'exécution.
392
400
 
401
+ > **Numéros de ligne** : les numéros affichés par l'éditeur ACE dans chaque onglet correspondent aux numéros réels dans le code Python exécuté. Un message d'erreur mentionnant la ligne 142 pointe donc directement vers la bonne ligne dans le bon onglet.
402
+
393
403
  ---
394
404
 
395
405
  ## Déploiement local (mode `vendor/`)
@@ -404,6 +414,9 @@ Par défaut, pyfrilet charge p5.js, Pyodide et ACE depuis des CDN publics. Pour
404
414
  <!-- Recommandé -->
405
415
  <script src="pyfrilet.js" data-sources="local" data-vendor="vendor/"></script>
406
416
 
417
+ <!-- Avec watchdog désactivé (calculs intensifs) -->
418
+ <script src="pyfrilet.js" data-sources="local" data-vendor="vendor/" data-no-watchdog></script>
419
+
407
420
  <!-- Rétrocompat (ancienne syntaxe) -->
408
421
  <script src="pyfrilet.js"></script>
409
422
  <script type="text/python" data-sources="local" data-vendor="vendor/">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyfrilet",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "main": "pyfrilet.js",
6
6
  "files": [
package/pyfrilet.js CHANGED
@@ -243,18 +243,130 @@ const STYLES = `html, body {
243
243
  .pf-tab.pf-tab-markdown::after { content: ' ✎'; font-size: 11px; opacity: .6; }
244
244
 
245
245
  /* ── markdown view ── */
246
+ @import url('https://fonts.googleapis.com/css2?family=Alegreya+Sans:ital,wght@0,400;0,700;1,400&display=swap');
247
+
246
248
  #pf-markdown-view {
247
249
  flex: 1;
248
250
  overflow: auto;
249
- padding: 14px 18px;
251
+ background: #f4f4f0;
252
+ }
253
+
254
+ #pf-markdown-view .pf-md-inner {
255
+ width: 100%;
256
+ max-width: 680px;
257
+ margin: 0 auto;
258
+ padding: 48px 48px 72px;
259
+ box-sizing: border-box;
260
+ font-family: 'Alegreya Sans', Georgia, serif;
261
+ font-size: 17px;
262
+ line-height: 1.8;
263
+ color: #1c1c2e;
264
+ }
265
+
266
+ #pf-markdown-view h1 {
267
+ font-size: 2.1em;
268
+ font-weight: 700;
269
+ color: #1c1c2e;
270
+ margin: 0 0 .3em;
271
+ padding-bottom: .3em;
272
+ border-bottom: 2px solid #d8d8e8;
273
+ line-height: 1.2;
274
+ }
275
+ #pf-markdown-view h2 {
276
+ font-size: 1.4em;
277
+ font-weight: 700;
278
+ color: #1c1c2e;
279
+ margin: 2em 0 .5em;
280
+ padding-bottom: .2em;
281
+ border-bottom: 1px solid #e0e0ec;
282
+ }
283
+ #pf-markdown-view h3 {
284
+ font-size: 1.1em;
285
+ font-weight: 700;
286
+ color: #2a2a4a;
287
+ margin: 1.6em 0 .4em;
288
+ }
289
+
290
+ #pf-markdown-view p { margin: .75em 0; }
291
+ #pf-markdown-view ul,
292
+ #pf-markdown-view ol { padding-left: 1.6em; margin: .75em 0; }
293
+ #pf-markdown-view li { margin: .3em 0; }
294
+ #pf-markdown-view hr { border: none; border-top: 1px solid #dde; margin: 2em 0; }
295
+ #pf-markdown-view blockquote {
296
+ margin: 1em 0;
297
+ padding: .5em 1em;
298
+ border-left: 3px solid #aab;
299
+ color: #555;
300
+ background: #ededf5;
301
+ border-radius: 0 4px 4px 0;
302
+ }
303
+
304
+ #pf-markdown-view code {
305
+ font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;
306
+ font-size: .84em;
307
+ background: #e8e8f2;
308
+ color: #3a3a6a;
309
+ padding: .15em .45em;
310
+ border-radius: 4px;
311
+ }
312
+ #pf-markdown-view pre {
250
313
  background: #1a1b2e;
314
+ border-radius: 8px;
315
+ padding: 1em 1.2em;
316
+ overflow: auto;
317
+ margin: 1.2em 0;
318
+ box-shadow: 0 2px 8px rgba(0,0,0,.12);
319
+ }
320
+ #pf-markdown-view pre code {
321
+ background: transparent;
251
322
  color: #c0caf5;
252
- font-size: 14px;
323
+ font-size: .86em;
324
+ padding: 0;
253
325
  line-height: 1.6;
326
+ border-radius: 0;
327
+ }
328
+
329
+ #pf-markdown-view table {
330
+ border-collapse: collapse;
331
+ width: 100%;
332
+ margin: 1.2em 0;
333
+ font-size: .95em;
334
+ }
335
+ #pf-markdown-view th {
336
+ background: #e4e4f0;
337
+ color: #1c1c2e;
338
+ font-weight: 700;
339
+ text-align: left;
340
+ padding: .55em .85em;
341
+ border: 1px solid #d0d0e8;
342
+ }
343
+ #pf-markdown-view td {
344
+ padding: .5em .85em;
345
+ border: 1px solid #e0e0ee;
346
+ vertical-align: top;
347
+ }
348
+ #pf-markdown-view tr:nth-child(even) td { background: #f0f0f8; }
349
+
350
+ #pf-markdown-view a {
351
+ color: #3a5fc8;
352
+ text-decoration: none;
353
+ border-bottom: 1px solid rgba(58,95,200,.3);
354
+ transition: color .15s, border-color .15s;
355
+ }
356
+ #pf-markdown-view a:hover { color: #1a3fa0; border-bottom-color: #1a3fa0; }
357
+
358
+ #pf-markdown-view .katex-display {
359
+ overflow-x: auto;
360
+ padding: .5em 0;
361
+ margin: 1.2em 0;
362
+ }
363
+ #pf-markdown-view .mermaid {
364
+ text-align: center;
365
+ margin: 1.5em 0;
366
+ background: #ededf5;
367
+ border-radius: 8px;
368
+ padding: 1em;
254
369
  }
255
- #pf-markdown-view h1,#pf-markdown-view h2,#pf-markdown-view h3 { color: #7aa2f7; }
256
- #pf-markdown-view code { background: #24283b; padding: 1px 5px; border-radius: 3px; font-size: 13px; }
257
- #pf-markdown-view pre code { display: block; padding: 10px; overflow: auto; }
258
370
 
259
371
  /* ── error panel (below editor, never overlaps ACE) ── */
260
372
  #pf-err {
@@ -316,10 +428,8 @@ document.addEventListener('DOMContentLoaded', function () {
316
428
  return;
317
429
  }
318
430
 
319
- /* Read sources/vendor from the first script tag */
320
- const firstScript = allScripts[0];
321
- /* Config tag: prefer the <script src="pyfrilet.js"> itself, fallback to first python tag (retro-compat) */
322
- const configTag = _pfScriptTag || firstScript;
431
+ /* Config tag: prefer the <script src="pyfrilet.js"> itself, fallback to first python tag */
432
+ const configTag = _pfScriptTag || allScripts[0];
323
433
 
324
434
  const sources = (
325
435
  configTag.getAttribute('data-sources') ||
@@ -332,7 +442,7 @@ document.addEventListener('DOMContentLoaded', function () {
332
442
  configTag.getAttribute('vendor') ||
333
443
  'vendor/'
334
444
  );
335
- const vp = vpRaw.replace(/\/?$/, '/'); /* ensure trailing slash */
445
+ const vp = vpRaw.replace(/\/?$/, '/');
336
446
 
337
447
  isCdn = sources === 'cdn';
338
448
  const hasMarked = allScripts.some(el => el.getAttribute('type') === 'text/markdown');
@@ -369,33 +479,80 @@ document.addEventListener('DOMContentLoaded', function () {
369
479
 
370
480
  const SK = 'pyfrilet:' + location.pathname;
371
481
 
372
- /* Build tabs array from DOM */
373
- const tabs = allScripts.map((el, i) => {
482
+ /* ── Parse HTML as ground truth ─────────────────────────────────────
483
+ htmlTabs = what the current file says, used for:
484
+ - starterCode (reset target)
485
+ - fallback when no snapshot exists
486
+ ──────────────────────────────────────────────────────────────────── */
487
+ const htmlTabs = allScripts.map((el, i) => {
374
488
  const type = el.getAttribute('type') === 'text/markdown' ? 'markdown' : 'python';
375
489
  const hidden = el.hasAttribute('data-hidden');
376
490
  const readonly = el.hasAttribute('data-readonly');
377
- /* label: data-tab value, or null for hidden, or default label for untagged block */
378
- let label = el.getAttribute('data-tab');
491
+ let label = el.getAttribute('data-tab');
379
492
  if (label === null && !hidden) label = allScripts.length === 1 ? 'Code' : `Bloc ${i + 1}`;
493
+ const rawCode = el.textContent.replace(/^\n/, '');
494
+ return { id: 'tab-' + i, label, hidden, readonly, type, starterCode: rawCode, code: rawCode };
495
+ });
380
496
 
381
- const rawCode = el.textContent.replace(/^\n/, ''); /* strip leading blank line */
382
- const tabSK = SK + ':' + i;
497
+ /* ── Try to restore full snapshot from localStorage ─────────────────
498
+ Snapshot format (v1): { v:1, tabs:[{ label, hidden, readonly, type, content }, ...] }
383
499
 
384
- /* Load saved code from localStorage for editable python tabs */
385
- let code = rawCode;
386
- if (type === 'python' && !hidden && !readonly) {
387
- const saved = (() => { try { return localStorage.getItem(tabSK); } catch (e) { return null; } })();
388
- if (saved && saved.trim()) code = saved;
389
- }
500
+ If a valid snapshot exists use it as the working tabs (same structure
501
+ as last visit, including number of tabs and all content).
502
+ The starterCode of each tab is taken from the matching htmlTab (by label+type)
503
+ so that Reset always targets the current file, not the stored version.
390
504
 
391
- return { id: 'tab-' + i, label, hidden, readonly, type, starterCode: rawCode, code, sk: tabSK };
392
- });
505
+ If no snapshot use htmlTabs, with retro-compat fallback on old per-tab keys.
506
+ ──────────────────────────────────────────────────────────────────── */
507
+ const tryLS = (key) => { try { return localStorage.getItem(key); } catch (e) { return null; } };
508
+
509
+ let tabs;
510
+ const raw = tryLS(SK);
511
+ let snap = null;
512
+ if (raw) {
513
+ try { snap = JSON.parse(raw); } catch (e) { snap = null; }
514
+ }
515
+
516
+ if (snap && snap.v === 1 && Array.isArray(snap.tabs) && snap.tabs.length > 0) {
517
+ /* Restore structure and content from snapshot */
518
+ tabs = snap.tabs.map((st, i) => {
519
+ /* Find current htmlTab with same label+type to get up-to-date starterCode */
520
+ const html = htmlTabs.find(h => h.label === st.label && h.type === st.type) || null;
521
+ return {
522
+ id : 'tab-' + i,
523
+ label : st.label,
524
+ hidden : st.hidden,
525
+ readonly : st.readonly,
526
+ type : st.type,
527
+ starterCode : html ? html.starterCode : st.content, /* reset target = current file */
528
+ code : st.content, /* working content = last visit */
529
+ };
530
+ });
531
+ } else {
532
+ /* No snapshot: use htmlTabs with retro-compat for old per-tab keys */
533
+ tabs = htmlTabs.map((tab, i) => {
534
+ if (!tab.hidden && !tab.readonly && tab.type === 'python') {
535
+ const safe = tab.label ? tab.label.replace(/[^a-zA-Z0-9]/g, '_') : String(i);
536
+ let saved = tryLS(SK + ':' + safe);
537
+ /* Retro-compat: single unnamed tab used bare SK as key */
538
+ if (!saved && tab.label === 'Code' && htmlTabs.length === 1) saved = tryLS(SK);
539
+ if (saved && saved.trim()) return { ...tab, code: saved };
540
+ }
541
+ return tab;
542
+ });
543
+ }
393
544
 
394
- main(tabs, SK, URLS);
545
+ const noWatchdog = configTag.hasAttribute('data-no-watchdog');
546
+
547
+ main(tabs, htmlTabs, SK, URLS, noWatchdog);
395
548
  });
396
549
 
397
550
  /* ═══════════════════════════ MAIN ═══════════════════════════════════ */
398
- function main(tabs, SK, URLS) {
551
+ function main(tabs, htmlTabs, SK, URLS, noWatchdog) {
552
+
553
+ /* tabs = working state (from snapshot or HTML), may be reassigned on reset
554
+ htmlTabs = ground truth from current HTML file, never mutated */
555
+ tabs = tabs.slice(); /* local mutable copy */
399
556
 
400
557
  /* ── inject styles + markup ── */
401
558
  const styleEl = document.createElement('style');
@@ -696,18 +853,23 @@ function main(tabs, SK, URLS) {
696
853
  }
697
854
 
698
855
  /* ─────────────────── ACE EDITOR + TABS ─────────────── */
699
- let aceInst = null;
700
- let activeTab = null; /* current tab object */
856
+ let aceInst = null;
857
+ let activeTab = null;
701
858
 
702
- /* Map tab.id → ACE EditSession (only for python editable visible tabs) */
859
+ /* Map tab.id → ACE EditSession */
703
860
  const aceSessions = {};
704
861
 
705
- /* ── Build tab bar ── */
706
- function initTabs() {
862
+ /* All pending save debounce timers — cancelled before any reset */
863
+ const saveTimers = new Set();
864
+
865
+ /* ── Tab bar ──────────────────────────────────────────── */
866
+ function buildTabBar() {
867
+ /* Clear existing buttons */
868
+ tabsEl.innerHTML = '';
869
+ activeTab = null;
870
+
707
871
  const visibleTabs = tabs.filter(t => !t.hidden);
708
- if (visibleTabs.length <= 1) {
709
- tabsEl.style.display = 'none';
710
- }
872
+ tabsEl.style.display = visibleTabs.length <= 1 ? 'none' : '';
711
873
 
712
874
  visibleTabs.forEach(tab => {
713
875
  const btn = document.createElement('button');
@@ -720,7 +882,6 @@ function main(tabs, SK, URLS) {
720
882
  tabsEl.appendChild(btn);
721
883
  });
722
884
 
723
- /* Activate first visible tab */
724
885
  if (visibleTabs.length > 0) switchTab(visibleTabs[0], true);
725
886
  }
726
887
 
@@ -728,19 +889,15 @@ function main(tabs, SK, URLS) {
728
889
  if (!init && activeTab === tab) return;
729
890
  activeTab = tab;
730
891
 
731
- /* Update tab button styles */
732
892
  tabsEl.querySelectorAll('.pf-tab').forEach(btn => {
733
893
  btn.classList.toggle('pf-tab-active', btn.dataset.tabId === tab.id);
734
894
  });
735
895
 
736
896
  if (tab.type === 'markdown') {
737
- /* Show markdown, hide ACE */
738
897
  document.getElementById('pf-ace').style.display = 'none';
739
898
  markdownEl.style.display = 'block';
740
899
  if (window.marked) {
741
900
  let html = marked.parse(tab.starterCode);
742
- /* marked HTML-escapes code block content — unescape mermaid blocks
743
- so mermaid can parse the diagram syntax correctly */
744
901
  if (window.mermaid) {
745
902
  html = html.replace(
746
903
  /<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,
@@ -749,13 +906,12 @@ function main(tabs, SK, URLS) {
749
906
  }</div>`
750
907
  );
751
908
  }
752
- markdownEl.innerHTML = html;
909
+ markdownEl.innerHTML = `<div class="pf-md-inner">${html}</div>`;
753
910
  } else {
754
- markdownEl.innerHTML = `<pre>${tab.starterCode}</pre>`;
911
+ markdownEl.innerHTML = `<div class="pf-md-inner"><pre>${tab.starterCode}</pre></div>`;
755
912
  }
756
913
  if (window.mermaid) mermaid.run({ nodes: markdownEl.querySelectorAll('.mermaid') });
757
914
  } else {
758
- /* Show ACE, hide markdown */
759
915
  document.getElementById('pf-ace').style.display = 'block';
760
916
  markdownEl.style.display = 'none';
761
917
  if (aceInst && aceSessions[tab.id]) {
@@ -766,8 +922,58 @@ function main(tabs, SK, URLS) {
766
922
  }
767
923
  }
768
924
 
925
+ /* ── Line number offsets ──────────────────────────────── */
926
+ /* Each visible tab's firstLineNumber is set so ACE line numbers match
927
+ Python traceback line numbers (all python tabs concatenated with join('\n')). */
928
+ function updateLineOffsets() {
929
+ let offset = 1;
930
+ tabs.filter(t => t.type === 'python').forEach(tab => {
931
+ if (!tab.hidden && !tab.readonly && aceSessions[tab.id]) {
932
+ aceSessions[tab.id].setOption('firstLineNumber', offset);
933
+ offset += aceSessions[tab.id].getLength();
934
+ } else {
935
+ offset += tab.code.split('\n').length;
936
+ }
937
+ });
938
+ }
939
+
940
+ /* ── ACE sessions ─────────────────────────────────────── */
941
+ function buildSessions() {
942
+ /* Always create fresh sessions — reusing an active session causes ACE
943
+ to skip the display refresh even after setValue(). */
944
+ Object.keys(aceSessions).forEach(id => delete aceSessions[id]);
945
+
946
+ tabs.filter(t => !t.hidden && t.type === 'python').forEach(tab => {
947
+ const session = ace.createEditSession(tab.code, 'ace/mode/python');
948
+ session.setUseWorker(false);
949
+ session.setTabSize(4);
950
+ aceSessions[tab.id] = session;
951
+
952
+ if (!tab.readonly) {
953
+ let saveTimer = null;
954
+ session.on('change', () => {
955
+ if (saveTimer !== null) { clearTimeout(saveTimer); saveTimers.delete(saveTimer); }
956
+ saveTimer = setTimeout(() => { saveTimers.delete(saveTimer); saveTimer = null; saveSnapshot(); }, 350);
957
+ saveTimers.add(saveTimer);
958
+ updateLineOffsets();
959
+ refreshDirty();
960
+ });
961
+ }
962
+ });
963
+
964
+ /* Point ACE instance at first python session and force a full redraw */
965
+ const firstPython = tabs.find(t => !t.hidden && t.type === 'python');
966
+ if (aceInst && firstPython && aceSessions[firstPython.id]) {
967
+ aceInst.setSession(aceSessions[firstPython.id]);
968
+ aceInst.setReadOnly(firstPython.readonly);
969
+ aceInst.renderer.updateFull(true);
970
+ }
971
+
972
+ updateLineOffsets();
973
+ }
974
+
975
+ /* ── Init ACE (once) ──────────────────────────────────── */
769
976
  function initAce() {
770
- /* In local mode ACE cannot auto-detect where its dynamic modules live */
771
977
  if (URLS.ace.startsWith('vendor') || !URLS.ace.startsWith('http')) {
772
978
  ace.config.set('basePath', URLS.ace.replace(/\/[^/]+$/, '/'));
773
979
  }
@@ -785,27 +991,7 @@ function main(tabs, SK, URLS) {
785
991
  enableSnippets : true,
786
992
  });
787
993
 
788
- /* Create one ACE session per visible python tab */
789
- tabs.filter(t => !t.hidden && t.type === 'python').forEach(tab => {
790
- const session = ace.createEditSession(tab.code, 'ace/mode/python');
791
- session.setUseWorker(false);
792
- session.setTabSize(4);
793
- aceSessions[tab.id] = session;
794
-
795
- if (!tab.readonly) {
796
- let saveTimer = null;
797
- session.on('change', () => {
798
- clearTimeout(saveTimer);
799
- saveTimer = setTimeout(() => saveTab(tab), 350);
800
- /* dirty indicator only for the active tab */
801
- if (activeTab === tab) {
802
- btnReset.classList.toggle('pf-dirty', session.getValue() !== tab.starterCode);
803
- }
804
- });
805
- }
806
- });
807
-
808
- /* Keyboard shortcuts */
994
+ /* Keyboard shortcuts (registered once) */
809
995
  aceInst.commands.addCommand({
810
996
  name: 'pfRun',
811
997
  bindKey: { win: 'Shift-Enter', mac: 'Shift-Enter' },
@@ -825,31 +1011,67 @@ function main(tabs, SK, URLS) {
825
1011
  name: 'pfReset',
826
1012
  bindKey: { win: 'Ctrl-R', mac: 'Command-R' },
827
1013
  exec: () => {
828
- if (!activeTab || activeTab.readonly || activeTab.type !== 'python') return;
829
- if (confirm('Réinitialiser cet onglet ? Les modifications seront perdues.')) {
830
- aceSessions[activeTab.id].setValue(activeTab.starterCode, -1);
831
- runCode();
832
- }
1014
+ if (confirm('Réinitialiser ? Les modifications seront perdues.')) resetAllTabs();
833
1015
  },
834
1016
  });
835
1017
 
836
- /* Activate first visible python tab in ACE (or first python session if tab is markdown) */
837
- const firstPythonTab = tabs.find(t => !t.hidden && t.type === 'python');
838
- if (firstPythonTab && aceSessions[firstPythonTab.id]) {
839
- aceInst.setSession(aceSessions[firstPythonTab.id]);
840
- aceInst.setReadOnly(firstPythonTab.readonly);
841
- }
1018
+ buildSessions();
1019
+ buildTabBar();
1020
+ refreshDirty();
1021
+ }
842
1022
 
843
- initTabs();
1023
+ /* ── Persistence ──────────────────────────────────────── */
1024
+ function saveSnapshot() {
1025
+ const snapshot = {
1026
+ v: 1,
1027
+ tabs: tabs.map(tab => ({
1028
+ label : tab.label,
1029
+ hidden : tab.hidden,
1030
+ readonly: tab.readonly,
1031
+ type : tab.type,
1032
+ content : (!tab.hidden && !tab.readonly && tab.type === 'python' && aceSessions[tab.id])
1033
+ ? aceSessions[tab.id].getValue()
1034
+ : tab.code,
1035
+ })),
1036
+ };
1037
+ try { localStorage.setItem(SK, JSON.stringify(snapshot)); } catch (e) {}
844
1038
  }
845
1039
 
846
- function saveTab(tab) {
847
- if (!tab || tab.readonly || tab.type !== 'python' || !aceSessions[tab.id]) return;
848
- try { localStorage.setItem(tab.sk, aceSessions[tab.id].getValue()); } catch (e) {}
1040
+ function saveCode() { saveSnapshot(); }
1041
+
1042
+ /* ── Dirty indicator ──────────────────────────────────── */
1043
+ function refreshDirty() {
1044
+ const dirty = tabs.some(tab =>
1045
+ !tab.hidden && !tab.readonly && tab.type === 'python' &&
1046
+ aceSessions[tab.id] &&
1047
+ aceSessions[tab.id].getValue() !== tab.starterCode
1048
+ );
1049
+ btnReset.classList.toggle('pf-dirty', dirty);
849
1050
  }
850
1051
 
851
- function saveCode() {
852
- tabs.forEach(tab => saveTab(tab));
1052
+ /* ── Reset: restore file structure + content, no reload ─ */
1053
+ function resetAllTabs() {
1054
+ /* Cancel all pending saves first */
1055
+ saveTimers.forEach(t => clearTimeout(t));
1056
+ saveTimers.clear();
1057
+
1058
+ /* Erase snapshot and any legacy per-tab keys */
1059
+ try { localStorage.removeItem(SK); } catch (e) {}
1060
+ tabs.forEach(tab => {
1061
+ if (tab.label) {
1062
+ try { localStorage.removeItem(SK + ':' + tab.label.replace(/[^a-zA-Z0-9]/g, '_')); } catch (e) {}
1063
+ }
1064
+ });
1065
+ try { localStorage.removeItem(SK + ':Code'); } catch (e) {}
1066
+
1067
+ /* Rebuild tabs from htmlTabs (file structure, starterCode as working code) */
1068
+ tabs = htmlTabs.map((ht, i) => ({ ...ht, id: 'tab-' + i, code: ht.starterCode }));
1069
+
1070
+ /* Rebuild sessions and tab bar in-memory */
1071
+ buildSessions();
1072
+ buildTabBar();
1073
+ refreshDirty();
1074
+ runCode();
853
1075
  }
854
1076
 
855
1077
  window.addEventListener('beforeunload', saveCode);
@@ -1020,6 +1242,36 @@ def _pf_refresh(ns):
1020
1242
 
1021
1243
  sys.modules["p5"] = m
1022
1244
 
1245
+ # ── draw() watchdog via sys.settrace ──────────────────────────────
1246
+ # Trace is called on every Python line event. We only call time.monotonic()
1247
+ # every N events to minimize overhead — a tight loop still triggers within
1248
+ # a few microseconds, so detection latency is negligible.
1249
+ import time as _time
1250
+
1251
+ _WDOG_CHECK_EVERY = 100
1252
+ _wdog_deadline = [0.0]
1253
+ _wdog_count = [0]
1254
+
1255
+ def _wdog_trace(frame, event, arg):
1256
+ _wdog_count[0] += 1
1257
+ if _wdog_count[0] >= _WDOG_CHECK_EVERY:
1258
+ _wdog_count[0] = 0
1259
+ if _time.monotonic() > _wdog_deadline[0]:
1260
+ raise TimeoutError("draw() watchdog")
1261
+ return _wdog_trace
1262
+
1263
+ def _pf_draw_watchdog(fn, timeout_ms):
1264
+ _wdog_count[0] = 0
1265
+ _wdog_deadline[0] = _time.monotonic() + timeout_ms * 0.001
1266
+ sys.settrace(_wdog_trace)
1267
+ try:
1268
+ fn()
1269
+ finally:
1270
+ sys.settrace(None)
1271
+
1272
+ def _pf_draw_direct(fn, timeout_ms):
1273
+ fn()
1274
+
1023
1275
  def _snake_to_camel(name):
1024
1276
  parts = name.split('_')
1025
1277
  return parts[0] + ''.join(p.capitalize() for p in parts[1:])
@@ -1091,6 +1343,8 @@ m.__getattr__ = _p5_getattr
1091
1343
  keyPressedProxy = null, keyReleasedProxy = null,
1092
1344
  touchStartedProxy = null, touchMovedProxy = null, touchEndedProxy = null;
1093
1345
 
1346
+ const WATCHDOG_MS = 300;
1347
+
1094
1348
  async function runCode() {
1095
1349
  if (running) return;
1096
1350
  running = true;
@@ -1169,9 +1423,10 @@ m.__getattr__ = _p5_getattr
1169
1423
  }
1170
1424
 
1171
1425
  const { create_proxy } = pyodide.pyimport('pyodide.ffi');
1172
- const pyWR = pyodide.runPython("_ns.get('windowResized')");
1173
- const pyRefresh = pyodide.globals.get('_pf_refresh');
1174
- const pyNs = pyodide.globals.get('_ns');
1426
+ const pyWR = pyodide.runPython("_ns.get('windowResized')");
1427
+ const pyRefresh = pyodide.globals.get('_pf_refresh');
1428
+ const pfDrawWatchdog = pyodide.globals.get(noWatchdog ? '_pf_draw_direct' : '_pf_draw_watchdog');
1429
+ const pyNs = pyodide.globals.get('_ns');
1175
1430
 
1176
1431
  const mkProxy = (fn) => fn ? create_proxy(() => {
1177
1432
  try { pyRefresh(pyNs); fn(); } catch (e) { showError(String(e)); }
@@ -1179,17 +1434,19 @@ m.__getattr__ = _p5_getattr
1179
1434
 
1180
1435
  preloadProxy = pyPreload ? create_proxy(() => { try { pyPreload(); } catch (e) { showError(String(e)); } }) : null;
1181
1436
  setupProxy = pySetup ? create_proxy(() => { try { pySetup(); } catch (e) { showError(String(e)); } }) : null;
1182
- const DRAW_TIMEOUT_MS = 200;
1183
1437
  drawProxy = create_proxy(() => {
1184
1438
  try {
1185
1439
  pyRefresh(pyNs);
1186
- const t0 = performance.now();
1187
- pyDraw();
1188
- if (performance.now() - t0 > DRAW_TIMEOUT_MS) {
1189
- stopSketch();
1190
- showError(`draw() a mis plus de ${DRAW_TIMEOUT_MS} ms — sketch arrêté pour protéger le navigateur.`);
1440
+ pfDrawWatchdog(pyDraw, WATCHDOG_MS);
1441
+ } catch (e) {
1442
+ const msg = String(e);
1443
+ stopSketch();
1444
+ if (msg.includes('TimeoutError') || msg.includes('watchdog')) {
1445
+ showError(`draw() a dépassé ${WATCHDOG_MS}ms — sketch arrêté (watchdog).`);
1446
+ } else {
1447
+ showError(msg);
1191
1448
  }
1192
- } catch (e) { showError(String(e)); stopSketch(); }
1449
+ }
1193
1450
  });
1194
1451
  mousePressedProxy = mkProxy(pyMP);
1195
1452
  mouseReleasedProxy = mkProxy(pyMR);
@@ -1248,7 +1505,7 @@ m.__getattr__ = _p5_getattr
1248
1505
  }
1249
1506
 
1250
1507
  /* ─────────────────── DOWNLOAD ───────────────── */
1251
- const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.5.0/pyfrilet.min.js';
1508
+ const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.5.2/pyfrilet.min.js';
1252
1509
 
1253
1510
  const STANDALONE_TEMPLATE = `<!doctype html>
1254
1511
  <html lang="fr">
@@ -1357,10 +1614,8 @@ FILLME-SCRIPTS
1357
1614
  btnHelp.addEventListener('click', () => window.open(HELP_URL, '_blank'));
1358
1615
 
1359
1616
  btnReset.addEventListener('click', () => {
1360
- if (!activeTab || activeTab.readonly || activeTab.type !== 'python') return;
1361
- if (aceSessions[activeTab.id] && confirm('Réinitialiser cet onglet ? Les modifications seront perdues.')) {
1362
- aceSessions[activeTab.id].setValue(activeTab.starterCode, -1);
1363
- runCode();
1617
+ if (confirm('Réinitialiser ? Les modifications seront perdues.')) {
1618
+ resetAllTabs();
1364
1619
  }
1365
1620
  });
1366
1621
 
@@ -1421,11 +1676,8 @@ FILLME-SCRIPTS
1421
1676
  /* Ctrl/Cmd+R: reset code (prevent browser reload) */
1422
1677
  if ((ev.key === 'r' || ev.key === 'R') && (ev.ctrlKey || ev.metaKey) && !ev.altKey) {
1423
1678
  ev.preventDefault();
1424
- if (activeTab && !activeTab.readonly && activeTab.type === 'python' && aceSessions[activeTab.id]) {
1425
- if (confirm('Réinitialiser cet onglet ? Les modifications seront perdues.')) {
1426
- aceSessions[activeTab.id].setValue(activeTab.starterCode, -1);
1427
- runCode();
1428
- }
1679
+ if (confirm('Réinitialiser ? Les modifications seront perdues.')) {
1680
+ resetAllTabs();
1429
1681
  }
1430
1682
  return;
1431
1683
  }
@@ -1462,7 +1714,7 @@ FILLME-SCRIPTS
1462
1714
  /* Configure marked: KaTeX extension */
1463
1715
  marked.use(markedKatex({ throwOnError: false }));
1464
1716
  /* Initialize mermaid (startOnLoad:false — we call run() manually) */
1465
- mermaid.initialize({ startOnLoad: false, theme: 'dark' });
1717
+ mermaid.initialize({ startOnLoad: false, theme: 'neutral' });
1466
1718
  }
1467
1719
  await loadScript(URLS.ace);
1468
1720
  await loadScript(URLS.acePython);
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",o="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js",i="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js",r="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",u="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",f="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#pf-markdown-view {\n flex: 1;\n overflow: auto;\n padding: 14px 18px;\n background: #1a1b2e;\n color: #c0caf5;\n font-size: 14px;\n line-height: 1.6;\n}\n#pf-markdown-view h1,#pf-markdown-view h2,#pf-markdown-view h3 { color: #7aa2f7; }\n#pf-markdown-view code { background: #24283b; padding: 1px 5px; border-radius: 3px; font-size: 13px; }\n#pf-markdown-view pre code { display: block; padding: 10px; overflow: auto; }\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}",h='<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-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 _=[...document.querySelectorAll('script[type="text/python"], script[type="text/markdown"], python')];if(0===_.length)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const y=_[0],b=e||y,g=(b.getAttribute("data-sources")||b.getAttribute("sources")||"cdn").toLowerCase().trim(),v=(b.getAttribute("data-vendor")||b.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/");n="cdn"===g;const x=_.some(e=>"text/markdown"===e.getAttribute("type")),k=n?{p5:t,pyodide:a,pyodideIndex:null,ace:o,acePython:i,aceMonokai:r,aceLangTools:s,aceSearchbox:d,marked:x?l:null,katexCss:x?c:null,katex:x?p:null,markedKatex:x?u:null,mermaid:x?m:null}:{p5:v+"p5.min.js",pyodide:v+"pyodide/pyodide.js",pyodideIndex:v+"pyodide/",ace:v+"ace.min.js",acePython:v+"mode-python.min.js",aceMonokai:v+"theme-monokai.min.js",aceLangTools:v+"ext-language_tools.min.js",aceSearchbox:v+"ext-searchbox.min.js",marked:x?v+"marked.min.js":null,katexCss:x?v+"katex.min.css":null,katex:x?v+"katex.min.js":null,markedKatex:x?v+"marked-katex-extension.js":null,mermaid:x?v+"mermaid.min.js":null},w="pyfrilet:"+location.pathname;!function(e,t,a){const o=document.createElement("style");o.textContent=f,document.head.appendChild(o),document.body.innerHTML=h;const i=document.getElementById("pf-app"),r=document.getElementById("pf-drawer"),s=document.getElementById("pf-handle"),d=document.getElementById("pf-sketch"),l=document.getElementById("pf-viewport"),c=document.getElementById("pf-loader"),p=document.getElementById("pf-loader-msg"),u=document.getElementById("pf-err"),m=document.getElementById("pf-btn-run"),_=document.getElementById("pf-btn-code"),y=document.getElementById("pf-btn-dl"),b=document.getElementById("pf-btn-rec"),g=document.getElementById("pf-btn-reset"),v=document.getElementById("pf-btn-help"),x=document.getElementById("pf-grip"),k=document.getElementById("pf-handle-hint"),w=document.getElementById("pf-tabs"),E=document.getElementById("pf-markdown-view");let C=!1,S=Math.round(.56*window.innerHeight);function L(){document.documentElement.style.setProperty("--pf-drawer-h",S+"px")}function j(){C=!0,r.classList.add("pf-open"),_.classList.add("pf-active"),setTimeout(()=>{F(),V&&V.focus()},280)}function z(){C=!1,r.classList.remove("pf-open"),_.classList.remove("pf-active"),setTimeout(()=>{F();const e=H._p?.canvas;e&&e.removeAttribute("tabindex"),i.focus()},280)}function R(){C?z():j()}L();let I=null;const P=5,M=120,B=document.createElement("div");function T(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;I={y:n,h:C?S:0,moved:!1},B.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function A(e){if(!I)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=I.y-n;if(Math.abs(t)>P&&(I.moved=!0),!I.moved)return;const a=Math.max(0,Math.min(window.innerHeight-50,I.h+t));a<M?(r.style.transition="none",r.style.height="32px"):(S=a,L(),C||j(),r.style.transition="none",r.style.height=S+"px"),F()}function O(e){if(!I)return;const n=I.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??I.y,a=I.y-t,o=I.h+a;I=null,B.style.display="none",document.body.style.userSelect="",r.style.transition="",r.style.height="",n&&(o<M?z():(S=Math.max(M,Math.min(window.innerHeight-50,o)),L(),C||j()),F())}Object.assign(B.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(B),x.addEventListener("click",e=>{e.stopPropagation(),R()}),s.addEventListener("mousedown",T,!0),document.addEventListener("mousemove",A),document.addEventListener("mouseup",O),s.addEventListener("touchstart",T,{passive:!1}),document.addEventListener("touchmove",A,{passive:!0}),document.addEventListener("touchend",O);let W=0,D=0;function K(e){u.textContent=e,u.style.display="block",j()}function U(){u.textContent="",u.style.display="none"}function $(){if(!H._p||"fit"!==H._mode)return;const e=H._w,n=H._h;if(!e||!n)return;const t=i.clientWidth,a=i.clientHeight,o=Math.min(t/e,a/n);l.style.transform=`scale(${o})`}function F(){if("fullscreen"===H._mode?H.size("max"):$(),N&&"function"==typeof N.windowResized)try{N.windowResized()}catch(e){K(String(e))}V&&V.resize()}window.addEventListener("mousemove",e=>{W=e.clientX,D=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(W=e.touches[0].clientX,D=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=H._p?H._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=H._w/n.width,a=H._h/n.height;return[(W-n.left)*t,(D-n.top)*a]},window.addEventListener("resize",F);let N=null;const H=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=i.clientWidth,this._h=i.clientHeight,void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),l.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),$())},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){k.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 Y(){if(Se(),N){try{N.remove()}catch(e){}N=null}d.innerHTML="",H._p=null,H._mode="fit",H._w=0,H._h=0,l.style.transform="scale(1)",k.textContent="Shift+Entrée → relancer  ·  Échap → ouvrir/fermer",de&&(de.destroy(),de=null),re&&(re.destroy(),re=null),se&&(se.destroy(),se=null),le&&(le.destroy(),le=null),ce&&(ce.destroy(),ce=null),pe&&(pe.destroy(),pe=null),ue&&(ue.destroy(),ue=null),me&&(me.destroy(),me=null),fe&&(fe.destroy(),fe=null),he&&(he.destroy(),he=null),_e&&(_e.destroy(),_e=null),ye&&(ye.destroy(),ye=null),be&&(be.destroy(),be=null),ge&&(ge.destroy(),ge=null)}window.p5py=H;let V=null,X=null;const J={};function q(){const n=e.filter(e=>!e.hidden);n.length<=1&&(w.style.display="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",()=>G(e)),w.appendChild(n)}),n.length>0&&G(n[0],!0)}function G(e,n){if(n||X!==e)if(X=e,w.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",E.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>`)),E.innerHTML=n}else E.innerHTML=`<pre>${e.starterCode}</pre>`;window.mermaid&&mermaid.run({nodes:E.querySelectorAll(".mermaid")})}else document.getElementById("pf-ace").style.display="block",E.style.display="none",V&&J[e.id]&&(V.setSession(J[e.id]),V.setReadOnly(e.readonly),V.focus())}function Z(){!a.ace.startsWith("vendor")&&a.ace.startsWith("http")||ace.config.set("basePath",a.ace.replace(/\/[^/]+$/,"/")),V=ace.edit("pf-ace"),V.setTheme("ace/theme/monokai"),V.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),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),J[e.id]=n,!e.readonly){let t=null;n.on("change",()=>{clearTimeout(t),t=setTimeout(()=>Q(e),350),X===e&&g.classList.toggle("pf-dirty",n.getValue()!==e.starterCode)})}}),V.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{V.completer?.popup?.isOpen||ve()}}),V.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:z}),V.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:ee}),V.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{X&&!X.readonly&&"python"===X.type&&confirm("Réinitialiser cet onglet ? Les modifications seront perdues.")&&(J[X.id].setValue(X.starterCode,-1),ve())}});const n=e.find(e=>!e.hidden&&"python"===e.type);n&&J[n.id]&&(V.setSession(J[n.id]),V.setReadOnly(n.readonly)),q()}function Q(e){if(e&&!e.readonly&&"python"===e.type&&J[e.id])try{localStorage.setItem(e.sk,J[e.id].getValue())}catch(e){}}function ee(){e.forEach(e=>Q(e))}window.addEventListener("beforeunload",ee);let ne=null,te=null;async function ae(){return te||(te=(async()=>{const e={};if(a.pyodideIndex&&(e.indexURL=a.pyodideIndex),ne=await loadPyodide(e),ne.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\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"),V){oe(ne.runPython("list(m.__all__)").toJs())}})(),te)}function oe(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,a,o,i){i(null,o.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,V.completers=[...V.completers||[],t]}let ie=!1,re=null,se=null,de=null,le=null,ce=null,pe=null,ue=null,me=null,fe=null,he=null,_e=null,ye=null,be=null,ge=null;async function ve(){if(ie)return;ie=!0,m.classList.add("pf-running"),U(),Y(),ne||(p.textContent="Initialisation de Pyodide…",c.style.display="flex");try{await ae()}catch(e){return c.style.display="none",K("Erreur Pyodide : "+e),ie=!1,void m.classList.remove("pf-running")}c.style.display="none";const t=e.filter(e=>"python"===e.type).map(e=>e.hidden||e.readonly||!J[e.id]?e.code:J[e.id].getValue()).join("\n");try{p.textContent="Chargement des dépendances…",c.style.display="flex",await ne.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:n})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}c.style.display="none",ne.globals.set("_USER_CODE",t);try{ne.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)"),ne.runPython("_ns_ref[0] = _ns")}catch(e){return K(String(e)),ie=!1,void m.classList.remove("pf-running")}let a,o,i,r,s,l,u,f,h,_,y,b,g,v;try{const e=(e,n)=>ne.runPython(`_ns.get('${e}') or _ns.get('${n}')`);s=e("preload","preload"),a=e("setup","setup"),o=e("draw","draw"),i=e("mousePressed","mouse_pressed"),r=e("keyPressed","key_pressed"),l=e("mouseDragged","mouse_dragged"),u=e("mouseReleased","mouse_released"),f=e("mouseMoved","mouse_moved"),h=e("mouseWheel","mouse_wheel"),_=e("doubleClicked","double_clicked"),y=e("keyReleased","key_released"),b=e("touchStarted","touch_started"),g=e("touchMoved","touch_moved"),v=e("touchEnded","touch_ended")}catch(e){return K(String(e)),ie=!1,void m.classList.remove("pf-running")}if(!o)return K("Le script doit définir au moins une fonction draw()."),ie=!1,void m.classList.remove("pf-running");const{create_proxy:x}=ne.pyimport("pyodide.ffi"),k=ne.runPython("_ns.get('windowResized')"),w=ne.globals.get("_pf_refresh"),E=ne.globals.get("_ns"),C=e=>e?x(()=>{try{w(E),e()}catch(e){K(String(e))}}):null;de=s?x(()=>{try{s()}catch(e){K(String(e))}}):null,re=a?x(()=>{try{a()}catch(e){K(String(e))}}):null;const S=200;se=x(()=>{try{w(E);const e=performance.now();o(),performance.now()-e>S&&(Y(),K(`draw() a mis plus de ${S} ms — sketch arrêté pour protéger le navigateur.`))}catch(e){K(String(e)),Y()}}),le=C(i),ce=C(u),pe=C(l),ue=C(f),me=C(h),fe=C(_),he=C(r),_e=C(y),ye=C(b),be=C(g),ge=C(v);const L=k?x(()=>{try{k()}catch(e){K(String(e))}}):null;let j=!1;N=new p5(e=>{H._setP(e),de&&(e.preload=()=>{de()}),e.setup=()=>{re&&re(),e.canvas||H.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),j=!0},e.draw=()=>{j&&se()},e.mousePressed=()=>{j&&le&&le()},e.mouseReleased=()=>{j&&ce&&ce()},e.mouseDragged=()=>{j&&pe&&pe()},e.mouseMoved=()=>{j&&ue&&ue()},e.mouseWheel=e=>{j&&me&&me()},e.doubleClicked=()=>{j&&fe&&fe()},e.keyPressed=()=>{j&&he&&he()},e.keyReleased=()=>{j&&_e&&_e()},ye&&(e.touchStarted=()=>{j&&ye()}),be&&(e.touchMoved=()=>{j&&be()}),ge&&(e.touchEnded=()=>{j&&ge()}),e.windowResized=()=>{"fullscreen"===H._mode?H.size("max"):$(),L&&L()}},d),ie=!1,m.classList.remove("pf-running")}const xe='<!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.5.0/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\nFILLME-SCRIPTS\n\n</body>\n</html>';function ke(){const n=e.map((e,n)=>{let t;t="python"!==e.type||e.hidden||e.readonly||!J[e.id]?e.code:J[e.id].getValue();const a=[],o="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="${o}"${a.length?" "+a.join(" "):""}>\n${t.replace(/<\/script>/gi,"<\\/script>")}\n<\/script>`}).join("\n\n"),t=xe.replace("FILLME-SCRIPTS",n),a=new Blob([t],{type:"text/html;charset=utf-8"}),o=URL.createObjectURL(a),i=Object.assign(document.createElement("a"),{href:o,download:"sketch.html"});document.body.appendChild(i),i.click(),document.body.removeChild(i),URL.revokeObjectURL(o)}let we=null,Ee=[];function Ce(){const e=H._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();we=new MediaRecorder(t,{mimeType:n}),Ee=[],we.ondataavailable=e=>{e.data.size&&Ee.push(e.data)},we.onstop=()=>{const e=new Blob(Ee,{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),b.textContent="⏺",b.title="Enregistrer WebM",b.classList.remove("pf-recording"),we=null},we.start(),b.textContent="⏹",b.title="Arrêter l'enregistrement",b.classList.add("pf-recording")}function Se(){we&&"inactive"!==we.state&&we.stop()}b.addEventListener("click",()=>{we?Se():Ce()}),m.addEventListener("click",()=>ve()),_.addEventListener("click",()=>{C?z():(S=window.innerHeight-32,L(),j())}),y.addEventListener("click",ke);const Le="https://codeberg.org/nopid/pyfrilet";function je(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)})}v.addEventListener("click",()=>window.open(Le,"_blank")),g.addEventListener("click",()=>{X&&!X.readonly&&"python"===X.type&&J[X.id]&&confirm("Réinitialiser cet onglet ? Les modifications seront perdues.")&&(J[X.id].setValue(X.starterCode,-1),ve())}),window.addEventListener("keydown",e=>{const n=C&&V&&V.isFocused&&V.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void ve();if("Escape"===e.key){const t=document.querySelector(".ace_search");if(t&&"none"!==t.style.display)return e.preventDefault(),e.stopPropagation(),V.searchBox?V.searchBox.hide():t.style.display="none",void V.focus();if(n){const n=V.completer?.popup?.isOpen;if(n)return;return e.preventDefault(),e.stopPropagation(),void z()}return e.preventDefault(),e.stopPropagation(),void(C?z():j())}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(X&&!X.readonly&&"python"===X.type&&J[X.id]&&confirm("Réinitialiser cet onglet ? Les modifications seront perdues.")&&(J[X.id].setValue(X.starterCode,-1),ve()))):(e.preventDefault(),void ee())}else e.preventDefault()},!0),(async()=>{p.textContent="Chargement des dépendances…",c.style.display="flex";try{if(await je(a.p5),a.marked){const e=document.createElement("link");e.rel="stylesheet",e.href=a.katexCss,document.head.appendChild(e),await je(a.marked),await je(a.katex),await je(a.markedKatex),await je(a.mermaid),marked.use(markedKatex({throwOnError:!1})),mermaid.initialize({startOnLoad:!1,theme:"dark"})}await je(a.ace),await je(a.acePython),await je(a.aceMonokai),await je(a.aceLangTools),await je(a.aceSearchbox),await je(a.pyodide)}catch(e){return p.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}Z(),await ve(),c.style.display="none"})()}(_.map((e,n)=>{const t="text/markdown"===e.getAttribute("type")?"markdown":"python",a=e.hasAttribute("data-hidden"),o=e.hasAttribute("data-readonly");let i=e.getAttribute("data-tab");null!==i||a||(i=1===_.length?"Code":`Bloc ${n+1}`);const r=e.textContent.replace(/^\n/,""),s=w+":"+n;let d=r;if("python"===t&&!a&&!o){const e=(()=>{try{return localStorage.getItem(s)}catch(e){return null}})();e&&e.trim()&&(d=e)}return{id:"tab-"+n,label:i,hidden:a,readonly:o,type:t,starterCode:r,code:d,sk:s}}),0,k)})}();
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",o="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js",r="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js",i="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js",s="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js",d="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js",l="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js",c="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css",p="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js",m="https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js",f="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js",u="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&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}",h='<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-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 _=[...document.querySelectorAll('script[type="text/python"], script[type="text/markdown"], python')];if(0===_.length)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const y=e||_[0],g=(y.getAttribute("data-sources")||y.getAttribute("sources")||"cdn").toLowerCase().trim(),b=(y.getAttribute("data-vendor")||y.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/");n="cdn"===g;const v=_.some(e=>"text/markdown"===e.getAttribute("type")),w=n?{p5:t,pyodide:a,pyodideIndex:null,ace:o,acePython:r,aceMonokai:i,aceLangTools:s,aceSearchbox:d,marked:v?l:null,katexCss:v?c:null,katex:v?p:null,markedKatex:v?m:null,mermaid:v?f:null}:{p5:b+"p5.min.js",pyodide:b+"pyodide/pyodide.js",pyodideIndex:b+"pyodide/",ace:b+"ace.min.js",acePython:b+"mode-python.min.js",aceMonokai:b+"theme-monokai.min.js",aceLangTools:b+"ext-language_tools.min.js",aceSearchbox:b+"ext-searchbox.min.js",marked:v?b+"marked.min.js":null,katexCss:v?b+"katex.min.css":null,katex:v?b+"katex.min.js":null,markedKatex:v?b+"marked-katex-extension.js":null,mermaid:v?b+"mermaid.min.js":null},x="pyfrilet:"+location.pathname,k=_.map((e,n)=>{const t="text/markdown"===e.getAttribute("type")?"markdown":"python",a=e.hasAttribute("data-hidden"),o=e.hasAttribute("data-readonly");let r=e.getAttribute("data-tab");null!==r||a||(r=1===_.length?"Code":`Bloc ${n+1}`);const i=e.textContent.replace(/^\n/,"");return{id:"tab-"+n,label:r,hidden:a,readonly:o,type:t,starterCode:i,code:i}}),E=e=>{try{return localStorage.getItem(e)}catch(e){return null}};let C;const S=E(x);let L=null;if(S)try{L=JSON.parse(S)}catch(e){L=null}C=L&&1===L.v&&Array.isArray(L.tabs)&&L.tabs.length>0?L.tabs.map((e,n)=>{const t=k.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}}):k.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=E(x+":"+t);if(a||"Code"!==e.label||1!==k.length||(a=E(x)),a&&a.trim())return{...e,code:a}}return e});const j=y.hasAttribute("data-no-watchdog");!function(e,t,a,o,r){e=e.slice();const i=document.createElement("style");i.textContent=u,document.head.appendChild(i),document.body.innerHTML=h;const s=document.getElementById("pf-app"),d=document.getElementById("pf-drawer"),l=document.getElementById("pf-handle"),c=document.getElementById("pf-sketch"),p=document.getElementById("pf-viewport"),m=document.getElementById("pf-loader"),f=document.getElementById("pf-loader-msg"),_=document.getElementById("pf-err"),y=document.getElementById("pf-btn-run"),g=document.getElementById("pf-btn-code"),b=document.getElementById("pf-btn-dl"),v=document.getElementById("pf-btn-rec"),w=document.getElementById("pf-btn-reset"),x=document.getElementById("pf-btn-help"),k=document.getElementById("pf-grip"),E=document.getElementById("pf-handle-hint"),C=document.getElementById("pf-tabs"),S=document.getElementById("pf-markdown-view");let L=!1,j=Math.round(.56*window.innerHeight);function z(){document.documentElement.style.setProperty("--pf-drawer-h",j+"px")}function I(){L=!0,d.classList.add("pf-open"),g.classList.add("pf-active"),setTimeout(()=>{H(),J&&J.focus()},280)}function R(){L=!1,d.classList.remove("pf-open"),g.classList.remove("pf-active"),setTimeout(()=>{H();const e=X._p?.canvas;e&&e.removeAttribute("tabindex"),s.focus()},280)}function P(){L?R():I()}z();let M=null;const A=5,T=120,B=document.createElement("div");function O(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:L?j:0,moved:!1},B.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function W(e){if(!M)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=M.y-n;if(Math.abs(t)>A&&(M.moved=!0),!M.moved)return;const a=Math.max(0,Math.min(window.innerHeight-50,M.h+t));a<T?(d.style.transition="none",d.style.height="32px"):(j=a,z(),L||I(),d.style.transition="none",d.style.height=j+"px"),H()}function D(e){if(!M)return;const n=M.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??M.y,a=M.y-t,o=M.h+a;M=null,B.style.display="none",document.body.style.userSelect="",d.style.transition="",d.style.height="",n&&(o<T?R():(j=Math.max(T,Math.min(window.innerHeight-50,o)),z(),L||I()),H())}Object.assign(B.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(B),k.addEventListener("click",e=>{e.stopPropagation(),P()}),l.addEventListener("mousedown",O,!0),document.addEventListener("mousemove",W),document.addEventListener("mouseup",D),l.addEventListener("touchstart",O,{passive:!1}),document.addEventListener("touchmove",W,{passive:!0}),document.addEventListener("touchend",D);let K=0,N=0;function U(e){_.textContent=e,_.style.display="block",I()}function F(){_.textContent="",_.style.display="none"}function $(){if(!X._p||"fit"!==X._mode)return;const e=X._w,n=X._h;if(!e||!n)return;const t=s.clientWidth,a=s.clientHeight,o=Math.min(t/e,a/n);p.style.transform=`scale(${o})`}function H(){if("fullscreen"===X._mode?X.size("max"):$(),Y&&"function"==typeof Y.windowResized)try{Y.windowResized()}catch(e){U(String(e))}J&&J.resize()}window.addEventListener("mousemove",e=>{K=e.clientX,N=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(K=e.touches[0].clientX,N=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=X._p?X._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=X._w/n.width,a=X._h/n.height;return[(K-n.left)*t,(N-n.top)*a]},window.addEventListener("resize",H);let Y=null;const X=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=s.clientWidth,this._h=s.clientHeight,void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),p.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),$())},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){E.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 G(){if(Ae(),Y){try{Y.remove()}catch(e){}Y=null}c.innerHTML="",X._p=null,X._mode="fit",X._w=0,X._h=0,p.style.transform="scale(1)",E.textContent="Shift+Entrée → relancer  ·  Échap → ouvrir/fermer",he&&(he.destroy(),he=null),fe&&(fe.destroy(),fe=null),ue&&(ue.destroy(),ue=null),_e&&(_e.destroy(),_e=null),ye&&(ye.destroy(),ye=null),ge&&(ge.destroy(),ge=null),be&&(be.destroy(),be=null),ve&&(ve.destroy(),ve=null),we&&(we.destroy(),we=null),xe&&(xe.destroy(),xe=null),ke&&(ke.destroy(),ke=null),Ee&&(Ee.destroy(),Ee=null),Ce&&(Ce.destroy(),Ce=null),Se&&(Se.destroy(),Se=null)}window.p5py=X;let J=null,V=null;const q={},Z=new Set;function Q(){C.innerHTML="",V=null;const n=e.filter(e=>!e.hidden);C.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",()=>ee(e)),C.appendChild(n)}),n.length>0&&ee(n[0],!0)}function ee(e,n){if(n||V!==e)if(V=e,C.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",S.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>`)),S.innerHTML=`<div class="pf-md-inner">${n}</div>`}else S.innerHTML=`<div class="pf-md-inner"><pre>${e.starterCode}</pre></div>`;window.mermaid&&mermaid.run({nodes:S.querySelectorAll(".mermaid")})}else document.getElementById("pf-ace").style.display="block",S.style.display="none",J&&q[e.id]&&(J.setSession(q[e.id]),J.setReadOnly(e.readonly),J.focus())}function ne(){let n=1;e.filter(e=>"python"===e.type).forEach(e=>{e.hidden||e.readonly||!q[e.id]?n+=e.code.split("\n").length:(q[e.id].setOption("firstLineNumber",n),n+=q[e.id].getLength())})}function te(){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),Z.delete(e)),e=setTimeout(()=>{Z.delete(e),e=null,oe()},350),Z.add(e),ne(),ie()})}});const n=e.find(e=>!e.hidden&&"python"===e.type);J&&n&&q[n.id]&&(J.setSession(q[n.id]),J.setReadOnly(n.readonly),J.renderer.updateFull(!0)),ne()}function ae(){!o.ace.startsWith("vendor")&&o.ace.startsWith("http")||ace.config.set("basePath",o.ace.replace(/\/[^/]+$/,"/")),J=ace.edit("pf-ace"),J.setTheme("ace/theme/monokai"),J.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),J.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{J.completer?.popup?.isOpen||je()}}),J.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:R}),J.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:re}),J.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&se()}}),te(),Q(),ie()}function oe(){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 re(){oe()}function ie(){const n=e.some(e=>!e.hidden&&!e.readonly&&"python"===e.type&&q[e.id]&&q[e.id].getValue()!==e.starterCode);w.classList.toggle("pf-dirty",n)}function se(){Z.forEach(e=>clearTimeout(e)),Z.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){}e=t.map((e,n)=>({...e,id:"tab-"+n,code:e.starterCode})),te(),Q(),ie(),je()}window.addEventListener("beforeunload",re);let de=null,le=null;async function ce(){return le||(le=(async()=>{const e={};if(o.pyodideIndex&&(e.indexURL=o.pyodideIndex),de=await loadPyodide(e),de.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\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 fn()\n finally:\n sys.settrace(None)\n\ndef _pf_draw_direct(fn, timeout_ms):\n 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"),J){pe(de.runPython("list(m.__all__)").toJs())}})(),le)}function pe(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,a,o,r){r(null,o.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,J.completers=[...J.completers||[],t]}let me=!1,fe=null,ue=null,he=null,_e=null,ye=null,ge=null,be=null,ve=null,we=null,xe=null,ke=null,Ee=null,Ce=null,Se=null;const Le=300;async function je(){if(me)return;me=!0,y.classList.add("pf-running"),F(),G(),de||(f.textContent="Initialisation de Pyodide…",m.style.display="flex");try{await ce()}catch(e){return m.style.display="none",U("Erreur Pyodide : "+e),me=!1,void y.classList.remove("pf-running")}m.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{f.textContent="Chargement des dépendances…",m.style.display="flex",await de.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:n})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}m.style.display="none",de.globals.set("_USER_CODE",t);try{de.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)"),de.runPython("_ns_ref[0] = _ns")}catch(e){return U(String(e)),me=!1,void y.classList.remove("pf-running")}let a,o,i,s,d,l,p,u,h,_,g,b,v,w;try{const e=(e,n)=>de.runPython(`_ns.get('${e}') or _ns.get('${n}')`);d=e("preload","preload"),a=e("setup","setup"),o=e("draw","draw"),i=e("mousePressed","mouse_pressed"),s=e("keyPressed","key_pressed"),l=e("mouseDragged","mouse_dragged"),p=e("mouseReleased","mouse_released"),u=e("mouseMoved","mouse_moved"),h=e("mouseWheel","mouse_wheel"),_=e("doubleClicked","double_clicked"),g=e("keyReleased","key_released"),b=e("touchStarted","touch_started"),v=e("touchMoved","touch_moved"),w=e("touchEnded","touch_ended")}catch(e){return U(String(e)),me=!1,void y.classList.remove("pf-running")}if(!o)return U("Le script doit définir au moins une fonction draw()."),me=!1,void y.classList.remove("pf-running");const{create_proxy:x}=de.pyimport("pyodide.ffi"),k=de.runPython("_ns.get('windowResized')"),E=de.globals.get("_pf_refresh"),C=de.globals.get(r?"_pf_draw_direct":"_pf_draw_watchdog"),S=de.globals.get("_ns"),L=e=>e?x(()=>{try{E(S),e()}catch(e){U(String(e))}}):null;he=d?x(()=>{try{d()}catch(e){U(String(e))}}):null,fe=a?x(()=>{try{a()}catch(e){U(String(e))}}):null,ue=x(()=>{try{E(S),C(o,Le)}catch(e){const n=String(e);G(),n.includes("TimeoutError")||n.includes("watchdog")?U(`draw() a dépassé ${Le}ms — sketch arrêté (watchdog).`):U(n)}}),_e=L(i),ye=L(p),ge=L(l),be=L(u),ve=L(h),we=L(_),xe=L(s),ke=L(g),Ee=L(b),Ce=L(v),Se=L(w);const j=k?x(()=>{try{k()}catch(e){U(String(e))}}):null;let z=!1;Y=new p5(e=>{X._setP(e),he&&(e.preload=()=>{he()}),e.setup=()=>{fe&&fe(),e.canvas||X.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),z=!0},e.draw=()=>{z&&ue()},e.mousePressed=()=>{z&&_e&&_e()},e.mouseReleased=()=>{z&&ye&&ye()},e.mouseDragged=()=>{z&&ge&&ge()},e.mouseMoved=()=>{z&&be&&be()},e.mouseWheel=e=>{z&&ve&&ve()},e.doubleClicked=()=>{z&&we&&we()},e.keyPressed=()=>{z&&xe&&xe()},e.keyReleased=()=>{z&&ke&&ke()},Ee&&(e.touchStarted=()=>{z&&Ee()}),Ce&&(e.touchMoved=()=>{z&&Ce()}),Se&&(e.touchEnded=()=>{z&&Se()}),e.windowResized=()=>{"fullscreen"===X._mode?X.size("max"):$(),j&&j()}},c),me=!1,y.classList.remove("pf-running")}const ze='<!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.5.2/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\nFILLME-SCRIPTS\n\n</body>\n</html>';function Ie(){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=[],o="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="${o}"${a.length?" "+a.join(" "):""}>\n${t.replace(/<\/script>/gi,"<\\/script>")}\n<\/script>`}).join("\n\n"),t=ze.replace("FILLME-SCRIPTS",n),a=new Blob([t],{type:"text/html;charset=utf-8"}),o=URL.createObjectURL(a),r=Object.assign(document.createElement("a"),{href:o,download:"sketch.html"});document.body.appendChild(r),r.click(),document.body.removeChild(r),URL.revokeObjectURL(o)}let Re=null,Pe=[];function Me(){const e=X._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();Re=new MediaRecorder(t,{mimeType:n}),Pe=[],Re.ondataavailable=e=>{e.data.size&&Pe.push(e.data)},Re.onstop=()=>{const e=new Blob(Pe,{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"),Re=null},Re.start(),v.textContent="⏹",v.title="Arrêter l'enregistrement",v.classList.add("pf-recording")}function Ae(){Re&&"inactive"!==Re.state&&Re.stop()}v.addEventListener("click",()=>{Re?Ae():Me()}),y.addEventListener("click",()=>je()),g.addEventListener("click",()=>{L?R():(j=window.innerHeight-32,z(),I())}),b.addEventListener("click",Ie);const Te="https://codeberg.org/nopid/pyfrilet";function Be(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)})}x.addEventListener("click",()=>window.open(Te,"_blank")),w.addEventListener("click",()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&se()}),window.addEventListener("keydown",e=>{const n=L&&J&&J.isFocused&&J.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void je();if("Escape"===e.key){const t=document.querySelector(".ace_search");if(t&&"none"!==t.style.display)return e.preventDefault(),e.stopPropagation(),J.searchBox?J.searchBox.hide():t.style.display="none",void J.focus();if(n){const n=J.completer?.popup?.isOpen;if(n)return;return e.preventDefault(),e.stopPropagation(),void R()}return e.preventDefault(),e.stopPropagation(),void(L?R():I())}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.")&&se())):(e.preventDefault(),void re())}else e.preventDefault()},!0),(async()=>{f.textContent="Chargement des dépendances…",m.style.display="flex";try{if(await Be(o.p5),o.marked){const e=document.createElement("link");e.rel="stylesheet",e.href=o.katexCss,document.head.appendChild(e),await Be(o.marked),await Be(o.katex),await Be(o.markedKatex),await Be(o.mermaid),marked.use(markedKatex({throwOnError:!1})),mermaid.initialize({startOnLoad:!1,theme:"neutral"})}await Be(o.ace),await Be(o.acePython),await Be(o.aceMonokai),await Be(o.aceLangTools),await Be(o.aceSearchbox),await Be(o.pyodide)}catch(e){return f.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}ae(),await je(),m.style.display="none"})()}(C,k,x,w,j)})}();