pyfrilet 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/pyfrilet.js +307 -92
- package/pyfrilet.min.js +1 -1
package/package.json
CHANGED
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
|
-
|
|
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:
|
|
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
|
-
/*
|
|
320
|
-
const
|
|
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(/\/?$/, '/');
|
|
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,78 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
369
479
|
|
|
370
480
|
const SK = 'pyfrilet:' + location.pathname;
|
|
371
481
|
|
|
372
|
-
/*
|
|
373
|
-
|
|
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
|
-
|
|
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
|
-
|
|
382
|
-
|
|
497
|
+
/* ── Try to restore full snapshot from localStorage ─────────────────
|
|
498
|
+
Snapshot format (v1): { v:1, tabs:[{ label, hidden, readonly, type, content }, ...] }
|
|
383
499
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
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; } };
|
|
393
508
|
|
|
394
|
-
|
|
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
|
+
}
|
|
544
|
+
|
|
545
|
+
main(tabs, htmlTabs, SK, URLS);
|
|
395
546
|
});
|
|
396
547
|
|
|
397
548
|
/* ═══════════════════════════ MAIN ═══════════════════════════════════ */
|
|
398
|
-
function main(tabs, SK, URLS) {
|
|
549
|
+
function main(tabs, htmlTabs, SK, URLS) {
|
|
550
|
+
|
|
551
|
+
/* tabs = working state (from snapshot or HTML), may be reassigned on reset
|
|
552
|
+
htmlTabs = ground truth from current HTML file, never mutated */
|
|
553
|
+
tabs = tabs.slice(); /* local mutable copy */
|
|
399
554
|
|
|
400
555
|
/* ── inject styles + markup ── */
|
|
401
556
|
const styleEl = document.createElement('style');
|
|
@@ -696,18 +851,23 @@ function main(tabs, SK, URLS) {
|
|
|
696
851
|
}
|
|
697
852
|
|
|
698
853
|
/* ─────────────────── ACE EDITOR + TABS ─────────────── */
|
|
699
|
-
let aceInst
|
|
700
|
-
let activeTab
|
|
854
|
+
let aceInst = null;
|
|
855
|
+
let activeTab = null;
|
|
701
856
|
|
|
702
|
-
/* Map tab.id → ACE EditSession
|
|
857
|
+
/* Map tab.id → ACE EditSession */
|
|
703
858
|
const aceSessions = {};
|
|
704
859
|
|
|
705
|
-
/*
|
|
706
|
-
|
|
860
|
+
/* All pending save debounce timers — cancelled before any reset */
|
|
861
|
+
const saveTimers = new Set();
|
|
862
|
+
|
|
863
|
+
/* ── Tab bar ──────────────────────────────────────────── */
|
|
864
|
+
function buildTabBar() {
|
|
865
|
+
/* Clear existing buttons */
|
|
866
|
+
tabsEl.innerHTML = '';
|
|
867
|
+
activeTab = null;
|
|
868
|
+
|
|
707
869
|
const visibleTabs = tabs.filter(t => !t.hidden);
|
|
708
|
-
|
|
709
|
-
tabsEl.style.display = 'none';
|
|
710
|
-
}
|
|
870
|
+
tabsEl.style.display = visibleTabs.length <= 1 ? 'none' : '';
|
|
711
871
|
|
|
712
872
|
visibleTabs.forEach(tab => {
|
|
713
873
|
const btn = document.createElement('button');
|
|
@@ -720,7 +880,6 @@ function main(tabs, SK, URLS) {
|
|
|
720
880
|
tabsEl.appendChild(btn);
|
|
721
881
|
});
|
|
722
882
|
|
|
723
|
-
/* Activate first visible tab */
|
|
724
883
|
if (visibleTabs.length > 0) switchTab(visibleTabs[0], true);
|
|
725
884
|
}
|
|
726
885
|
|
|
@@ -728,19 +887,15 @@ function main(tabs, SK, URLS) {
|
|
|
728
887
|
if (!init && activeTab === tab) return;
|
|
729
888
|
activeTab = tab;
|
|
730
889
|
|
|
731
|
-
/* Update tab button styles */
|
|
732
890
|
tabsEl.querySelectorAll('.pf-tab').forEach(btn => {
|
|
733
891
|
btn.classList.toggle('pf-tab-active', btn.dataset.tabId === tab.id);
|
|
734
892
|
});
|
|
735
893
|
|
|
736
894
|
if (tab.type === 'markdown') {
|
|
737
|
-
/* Show markdown, hide ACE */
|
|
738
895
|
document.getElementById('pf-ace').style.display = 'none';
|
|
739
896
|
markdownEl.style.display = 'block';
|
|
740
897
|
if (window.marked) {
|
|
741
898
|
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
899
|
if (window.mermaid) {
|
|
745
900
|
html = html.replace(
|
|
746
901
|
/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,
|
|
@@ -749,13 +904,12 @@ function main(tabs, SK, URLS) {
|
|
|
749
904
|
}</div>`
|
|
750
905
|
);
|
|
751
906
|
}
|
|
752
|
-
markdownEl.innerHTML = html
|
|
907
|
+
markdownEl.innerHTML = `<div class="pf-md-inner">${html}</div>`;
|
|
753
908
|
} else {
|
|
754
|
-
markdownEl.innerHTML = `<pre>${tab.starterCode}</pre>`;
|
|
909
|
+
markdownEl.innerHTML = `<div class="pf-md-inner"><pre>${tab.starterCode}</pre></div>`;
|
|
755
910
|
}
|
|
756
911
|
if (window.mermaid) mermaid.run({ nodes: markdownEl.querySelectorAll('.mermaid') });
|
|
757
912
|
} else {
|
|
758
|
-
/* Show ACE, hide markdown */
|
|
759
913
|
document.getElementById('pf-ace').style.display = 'block';
|
|
760
914
|
markdownEl.style.display = 'none';
|
|
761
915
|
if (aceInst && aceSessions[tab.id]) {
|
|
@@ -766,8 +920,58 @@ function main(tabs, SK, URLS) {
|
|
|
766
920
|
}
|
|
767
921
|
}
|
|
768
922
|
|
|
923
|
+
/* ── Line number offsets ──────────────────────────────── */
|
|
924
|
+
/* Each visible tab's firstLineNumber is set so ACE line numbers match
|
|
925
|
+
Python traceback line numbers (all python tabs concatenated with join('\n')). */
|
|
926
|
+
function updateLineOffsets() {
|
|
927
|
+
let offset = 1;
|
|
928
|
+
tabs.filter(t => t.type === 'python').forEach(tab => {
|
|
929
|
+
if (!tab.hidden && !tab.readonly && aceSessions[tab.id]) {
|
|
930
|
+
aceSessions[tab.id].setOption('firstLineNumber', offset);
|
|
931
|
+
offset += aceSessions[tab.id].getLength();
|
|
932
|
+
} else {
|
|
933
|
+
offset += tab.code.split('\n').length;
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/* ── ACE sessions ─────────────────────────────────────── */
|
|
939
|
+
function buildSessions() {
|
|
940
|
+
/* Always create fresh sessions — reusing an active session causes ACE
|
|
941
|
+
to skip the display refresh even after setValue(). */
|
|
942
|
+
Object.keys(aceSessions).forEach(id => delete aceSessions[id]);
|
|
943
|
+
|
|
944
|
+
tabs.filter(t => !t.hidden && t.type === 'python').forEach(tab => {
|
|
945
|
+
const session = ace.createEditSession(tab.code, 'ace/mode/python');
|
|
946
|
+
session.setUseWorker(false);
|
|
947
|
+
session.setTabSize(4);
|
|
948
|
+
aceSessions[tab.id] = session;
|
|
949
|
+
|
|
950
|
+
if (!tab.readonly) {
|
|
951
|
+
let saveTimer = null;
|
|
952
|
+
session.on('change', () => {
|
|
953
|
+
if (saveTimer !== null) { clearTimeout(saveTimer); saveTimers.delete(saveTimer); }
|
|
954
|
+
saveTimer = setTimeout(() => { saveTimers.delete(saveTimer); saveTimer = null; saveSnapshot(); }, 350);
|
|
955
|
+
saveTimers.add(saveTimer);
|
|
956
|
+
updateLineOffsets();
|
|
957
|
+
refreshDirty();
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
/* Point ACE instance at first python session and force a full redraw */
|
|
963
|
+
const firstPython = tabs.find(t => !t.hidden && t.type === 'python');
|
|
964
|
+
if (aceInst && firstPython && aceSessions[firstPython.id]) {
|
|
965
|
+
aceInst.setSession(aceSessions[firstPython.id]);
|
|
966
|
+
aceInst.setReadOnly(firstPython.readonly);
|
|
967
|
+
aceInst.renderer.updateFull(true);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
updateLineOffsets();
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/* ── Init ACE (once) ──────────────────────────────────── */
|
|
769
974
|
function initAce() {
|
|
770
|
-
/* In local mode ACE cannot auto-detect where its dynamic modules live */
|
|
771
975
|
if (URLS.ace.startsWith('vendor') || !URLS.ace.startsWith('http')) {
|
|
772
976
|
ace.config.set('basePath', URLS.ace.replace(/\/[^/]+$/, '/'));
|
|
773
977
|
}
|
|
@@ -785,27 +989,7 @@ function main(tabs, SK, URLS) {
|
|
|
785
989
|
enableSnippets : true,
|
|
786
990
|
});
|
|
787
991
|
|
|
788
|
-
/*
|
|
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 */
|
|
992
|
+
/* Keyboard shortcuts (registered once) */
|
|
809
993
|
aceInst.commands.addCommand({
|
|
810
994
|
name: 'pfRun',
|
|
811
995
|
bindKey: { win: 'Shift-Enter', mac: 'Shift-Enter' },
|
|
@@ -825,31 +1009,67 @@ function main(tabs, SK, URLS) {
|
|
|
825
1009
|
name: 'pfReset',
|
|
826
1010
|
bindKey: { win: 'Ctrl-R', mac: 'Command-R' },
|
|
827
1011
|
exec: () => {
|
|
828
|
-
if (
|
|
829
|
-
if (confirm('Réinitialiser cet onglet ? Les modifications seront perdues.')) {
|
|
830
|
-
aceSessions[activeTab.id].setValue(activeTab.starterCode, -1);
|
|
831
|
-
runCode();
|
|
832
|
-
}
|
|
1012
|
+
if (confirm('Réinitialiser ? Les modifications seront perdues.')) resetAllTabs();
|
|
833
1013
|
},
|
|
834
1014
|
});
|
|
835
1015
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
aceInst.setReadOnly(firstPythonTab.readonly);
|
|
841
|
-
}
|
|
1016
|
+
buildSessions();
|
|
1017
|
+
buildTabBar();
|
|
1018
|
+
refreshDirty();
|
|
1019
|
+
}
|
|
842
1020
|
|
|
843
|
-
|
|
1021
|
+
/* ── Persistence ──────────────────────────────────────── */
|
|
1022
|
+
function saveSnapshot() {
|
|
1023
|
+
const snapshot = {
|
|
1024
|
+
v: 1,
|
|
1025
|
+
tabs: tabs.map(tab => ({
|
|
1026
|
+
label : tab.label,
|
|
1027
|
+
hidden : tab.hidden,
|
|
1028
|
+
readonly: tab.readonly,
|
|
1029
|
+
type : tab.type,
|
|
1030
|
+
content : (!tab.hidden && !tab.readonly && tab.type === 'python' && aceSessions[tab.id])
|
|
1031
|
+
? aceSessions[tab.id].getValue()
|
|
1032
|
+
: tab.code,
|
|
1033
|
+
})),
|
|
1034
|
+
};
|
|
1035
|
+
try { localStorage.setItem(SK, JSON.stringify(snapshot)); } catch (e) {}
|
|
844
1036
|
}
|
|
845
1037
|
|
|
846
|
-
function
|
|
847
|
-
|
|
848
|
-
|
|
1038
|
+
function saveCode() { saveSnapshot(); }
|
|
1039
|
+
|
|
1040
|
+
/* ── Dirty indicator ──────────────────────────────────── */
|
|
1041
|
+
function refreshDirty() {
|
|
1042
|
+
const dirty = tabs.some(tab =>
|
|
1043
|
+
!tab.hidden && !tab.readonly && tab.type === 'python' &&
|
|
1044
|
+
aceSessions[tab.id] &&
|
|
1045
|
+
aceSessions[tab.id].getValue() !== tab.starterCode
|
|
1046
|
+
);
|
|
1047
|
+
btnReset.classList.toggle('pf-dirty', dirty);
|
|
849
1048
|
}
|
|
850
1049
|
|
|
851
|
-
|
|
852
|
-
|
|
1050
|
+
/* ── Reset: restore file structure + content, no reload ─ */
|
|
1051
|
+
function resetAllTabs() {
|
|
1052
|
+
/* Cancel all pending saves first */
|
|
1053
|
+
saveTimers.forEach(t => clearTimeout(t));
|
|
1054
|
+
saveTimers.clear();
|
|
1055
|
+
|
|
1056
|
+
/* Erase snapshot and any legacy per-tab keys */
|
|
1057
|
+
try { localStorage.removeItem(SK); } catch (e) {}
|
|
1058
|
+
tabs.forEach(tab => {
|
|
1059
|
+
if (tab.label) {
|
|
1060
|
+
try { localStorage.removeItem(SK + ':' + tab.label.replace(/[^a-zA-Z0-9]/g, '_')); } catch (e) {}
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
try { localStorage.removeItem(SK + ':Code'); } catch (e) {}
|
|
1064
|
+
|
|
1065
|
+
/* Rebuild tabs from htmlTabs (file structure, starterCode as working code) */
|
|
1066
|
+
tabs = htmlTabs.map((ht, i) => ({ ...ht, id: 'tab-' + i, code: ht.starterCode }));
|
|
1067
|
+
|
|
1068
|
+
/* Rebuild sessions and tab bar in-memory */
|
|
1069
|
+
buildSessions();
|
|
1070
|
+
buildTabBar();
|
|
1071
|
+
refreshDirty();
|
|
1072
|
+
runCode();
|
|
853
1073
|
}
|
|
854
1074
|
|
|
855
1075
|
window.addEventListener('beforeunload', saveCode);
|
|
@@ -1248,7 +1468,7 @@ m.__getattr__ = _p5_getattr
|
|
|
1248
1468
|
}
|
|
1249
1469
|
|
|
1250
1470
|
/* ─────────────────── DOWNLOAD ───────────────── */
|
|
1251
|
-
const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.5.
|
|
1471
|
+
const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.5.1/pyfrilet.min.js';
|
|
1252
1472
|
|
|
1253
1473
|
const STANDALONE_TEMPLATE = `<!doctype html>
|
|
1254
1474
|
<html lang="fr">
|
|
@@ -1357,10 +1577,8 @@ FILLME-SCRIPTS
|
|
|
1357
1577
|
btnHelp.addEventListener('click', () => window.open(HELP_URL, '_blank'));
|
|
1358
1578
|
|
|
1359
1579
|
btnReset.addEventListener('click', () => {
|
|
1360
|
-
if (
|
|
1361
|
-
|
|
1362
|
-
aceSessions[activeTab.id].setValue(activeTab.starterCode, -1);
|
|
1363
|
-
runCode();
|
|
1580
|
+
if (confirm('Réinitialiser ? Les modifications seront perdues.')) {
|
|
1581
|
+
resetAllTabs();
|
|
1364
1582
|
}
|
|
1365
1583
|
});
|
|
1366
1584
|
|
|
@@ -1421,11 +1639,8 @@ FILLME-SCRIPTS
|
|
|
1421
1639
|
/* Ctrl/Cmd+R: reset code (prevent browser reload) */
|
|
1422
1640
|
if ((ev.key === 'r' || ev.key === 'R') && (ev.ctrlKey || ev.metaKey) && !ev.altKey) {
|
|
1423
1641
|
ev.preventDefault();
|
|
1424
|
-
if (
|
|
1425
|
-
|
|
1426
|
-
aceSessions[activeTab.id].setValue(activeTab.starterCode, -1);
|
|
1427
|
-
runCode();
|
|
1428
|
-
}
|
|
1642
|
+
if (confirm('Réinitialiser ? Les modifications seront perdues.')) {
|
|
1643
|
+
resetAllTabs();
|
|
1429
1644
|
}
|
|
1430
1645
|
return;
|
|
1431
1646
|
}
|
|
@@ -1462,7 +1677,7 @@ FILLME-SCRIPTS
|
|
|
1462
1677
|
/* Configure marked: KaTeX extension */
|
|
1463
1678
|
marked.use(markedKatex({ throwOnError: false }));
|
|
1464
1679
|
/* Initialize mermaid (startOnLoad:false — we call run() manually) */
|
|
1465
|
-
mermaid.initialize({ startOnLoad: false, theme: '
|
|
1680
|
+
mermaid.initialize({ startOnLoad: false, theme: 'neutral' });
|
|
1466
1681
|
}
|
|
1467
1682
|
await loadScript(URLS.ace);
|
|
1468
1683
|
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 · 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)">▶</button>\n <button class="pf-btn" id="pf-btn-code" title="Éditeur plein écran">✏️</button>\n <button class="pf-btn" id="pf-btn-dl" title="Télécharger HTML autonome">💾</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)">↻</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(/&/g,"&").replace(/</g,"<").replace(/>/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,""")}"`),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 · 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)">▶</button>\n <button class="pf-btn" id="pf-btn-code" title="Éditeur plein écran">✏️</button>\n <button class="pf-btn" id="pf-btn-dl" title="Télécharger HTML autonome">💾</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)">↻</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],b=(y.getAttribute("data-sources")||y.getAttribute("sources")||"cdn").toLowerCase().trim(),g=(y.getAttribute("data-vendor")||y.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/");n="cdn"===b;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:g+"p5.min.js",pyodide:g+"pyodide/pyodide.js",pyodideIndex:g+"pyodide/",ace:g+"ace.min.js",acePython:g+"mode-python.min.js",aceMonokai:g+"theme-monokai.min.js",aceLangTools:g+"ext-language_tools.min.js",aceSearchbox:g+"ext-searchbox.min.js",marked:v?g+"marked.min.js":null,katexCss:v?g+"katex.min.css":null,katex:v?g+"katex.min.js":null,markedKatex:v?g+"marked-katex-extension.js":null,mermaid:v?g+"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}),function(e,t,a,o){e=e.slice();const r=document.createElement("style");r.textContent=u,document.head.appendChild(r),document.body.innerHTML=h;const i=document.getElementById("pf-app"),s=document.getElementById("pf-drawer"),d=document.getElementById("pf-handle"),l=document.getElementById("pf-sketch"),c=document.getElementById("pf-viewport"),p=document.getElementById("pf-loader"),m=document.getElementById("pf-loader-msg"),f=document.getElementById("pf-err"),_=document.getElementById("pf-btn-run"),y=document.getElementById("pf-btn-code"),b=document.getElementById("pf-btn-dl"),g=document.getElementById("pf-btn-rec"),v=document.getElementById("pf-btn-reset"),w=document.getElementById("pf-btn-help"),x=document.getElementById("pf-grip"),k=document.getElementById("pf-handle-hint"),E=document.getElementById("pf-tabs"),C=document.getElementById("pf-markdown-view");let S=!1,L=Math.round(.56*window.innerHeight);function j(){document.documentElement.style.setProperty("--pf-drawer-h",L+"px")}function z(){S=!0,s.classList.add("pf-open"),y.classList.add("pf-active"),setTimeout(()=>{$(),J&&J.focus()},280)}function I(){S=!1,s.classList.remove("pf-open"),y.classList.remove("pf-active"),setTimeout(()=>{$();const e=Y._p?.canvas;e&&e.removeAttribute("tabindex"),i.focus()},280)}function R(){S?I():z()}j();let M=null;const P=5,A=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;M={y:n,h:S?L:0,moved:!1},B.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function O(e){if(!M)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=M.y-n;if(Math.abs(t)>P&&(M.moved=!0),!M.moved)return;const a=Math.max(0,Math.min(window.innerHeight-50,M.h+t));a<A?(s.style.transition="none",s.style.height="32px"):(L=a,j(),S||z(),s.style.transition="none",s.style.height=L+"px"),$()}function W(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="",s.style.transition="",s.style.height="",n&&(o<A?I():(L=Math.max(A,Math.min(window.innerHeight-50,o)),j(),S||z()),$())}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()}),d.addEventListener("mousedown",T,!0),document.addEventListener("mousemove",O),document.addEventListener("mouseup",W),d.addEventListener("touchstart",T,{passive:!1}),document.addEventListener("touchmove",O,{passive:!0}),document.addEventListener("touchend",W);let D=0,K=0;function N(e){f.textContent=e,f.style.display="block",z()}function U(){f.textContent="",f.style.display="none"}function F(){if(!Y._p||"fit"!==Y._mode)return;const e=Y._w,n=Y._h;if(!e||!n)return;const t=i.clientWidth,a=i.clientHeight,o=Math.min(t/e,a/n);c.style.transform=`scale(${o})`}function $(){if("fullscreen"===Y._mode?Y.size("max"):F(),H&&"function"==typeof H.windowResized)try{H.windowResized()}catch(e){N(String(e))}J&&J.resize()}window.addEventListener("mousemove",e=>{D=e.clientX,K=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(D=e.touches[0].clientX,K=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=Y._p?Y._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=Y._w/n.width,a=Y._h/n.height;return[(D-n.left)*t,(K-n.top)*a]},window.addEventListener("resize",$);let H=null;const Y=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),c.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),F())},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 X(){if(Me(),H){try{H.remove()}catch(e){}H=null}l.innerHTML="",Y._p=null,Y._mode="fit",Y._w=0,Y._h=0,c.style.transform="scale(1)",k.textContent="Shift+Entrée → relancer · Échap → ouvrir/fermer",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),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)}window.p5py=Y;let J=null,q=null;const G={},V=new Set;function Z(){E.innerHTML="",q=null;const n=e.filter(e=>!e.hidden);E.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",()=>Q(e)),E.appendChild(n)}),n.length>0&&Q(n[0],!0)}function Q(e,n){if(n||q!==e)if(q=e,E.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",C.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(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}</div>`)),C.innerHTML=`<div class="pf-md-inner">${n}</div>`}else C.innerHTML=`<div class="pf-md-inner"><pre>${e.starterCode}</pre></div>`;window.mermaid&&mermaid.run({nodes:C.querySelectorAll(".mermaid")})}else document.getElementById("pf-ace").style.display="block",C.style.display="none",J&&G[e.id]&&(J.setSession(G[e.id]),J.setReadOnly(e.readonly),J.focus())}function ee(){let n=1;e.filter(e=>"python"===e.type).forEach(e=>{e.hidden||e.readonly||!G[e.id]?n+=e.code.split("\n").length:(G[e.id].setOption("firstLineNumber",n),n+=G[e.id].getLength())})}function ne(){Object.keys(G).forEach(e=>delete G[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),G[e.id]=n,!e.readonly){let e=null;n.on("change",()=>{null!==e&&(clearTimeout(e),V.delete(e)),e=setTimeout(()=>{V.delete(e),e=null,ae()},350),V.add(e),ee(),re()})}});const n=e.find(e=>!e.hidden&&"python"===e.type);J&&n&&G[n.id]&&(J.setSession(G[n.id]),J.setReadOnly(n.readonly),J.renderer.updateFull(!0)),ee()}function te(){!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||Se()}}),J.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:I}),J.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:oe}),J.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&ie()}}),ne(),Z(),re()}function ae(){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||!G[e.id]?e.code:G[e.id].getValue()}))};try{localStorage.setItem(a,JSON.stringify(n))}catch(e){}}function oe(){ae()}function re(){const n=e.some(e=>!e.hidden&&!e.readonly&&"python"===e.type&&G[e.id]&&G[e.id].getValue()!==e.starterCode);v.classList.toggle("pf-dirty",n)}function ie(){V.forEach(e=>clearTimeout(e)),V.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})),ne(),Z(),re(),Se()}window.addEventListener("beforeunload",oe);let se=null,de=null;async function le(){return de||(de=(async()=>{const e={};if(o.pyodideIndex&&(e.indexURL=o.pyodideIndex),se=await loadPyodide(e),se.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"),J){ce(se.runPython("list(m.__all__)").toJs())}})(),de)}function ce(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 pe=!1,me=null,fe=null,ue=null,he=null,_e=null,ye=null,be=null,ge=null,ve=null,we=null,xe=null,ke=null,Ee=null,Ce=null;async function Se(){if(pe)return;pe=!0,_.classList.add("pf-running"),U(),X(),se||(m.textContent="Initialisation de Pyodide…",p.style.display="flex");try{await le()}catch(e){return p.style.display="none",N("Erreur Pyodide : "+e),pe=!1,void _.classList.remove("pf-running")}p.style.display="none";const t=e.filter(e=>"python"===e.type).map(e=>e.hidden||e.readonly||!G[e.id]?e.code:G[e.id].getValue()).join("\n");try{m.textContent="Chargement des dépendances…",p.style.display="flex",await se.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:n})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}p.style.display="none",se.globals.set("_USER_CODE",t);try{se.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)"),se.runPython("_ns_ref[0] = _ns")}catch(e){return N(String(e)),pe=!1,void _.classList.remove("pf-running")}let a,o,r,i,s,d,c,f,u,h,y,b,g,v;try{const e=(e,n)=>se.runPython(`_ns.get('${e}') or _ns.get('${n}')`);s=e("preload","preload"),a=e("setup","setup"),o=e("draw","draw"),r=e("mousePressed","mouse_pressed"),i=e("keyPressed","key_pressed"),d=e("mouseDragged","mouse_dragged"),c=e("mouseReleased","mouse_released"),f=e("mouseMoved","mouse_moved"),u=e("mouseWheel","mouse_wheel"),h=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 N(String(e)),pe=!1,void _.classList.remove("pf-running")}if(!o)return N("Le script doit définir au moins une fonction draw()."),pe=!1,void _.classList.remove("pf-running");const{create_proxy:w}=se.pyimport("pyodide.ffi"),x=se.runPython("_ns.get('windowResized')"),k=se.globals.get("_pf_refresh"),E=se.globals.get("_ns"),C=e=>e?w(()=>{try{k(E),e()}catch(e){N(String(e))}}):null;ue=s?w(()=>{try{s()}catch(e){N(String(e))}}):null,me=a?w(()=>{try{a()}catch(e){N(String(e))}}):null;const S=200;fe=w(()=>{try{k(E);const e=performance.now();o(),performance.now()-e>S&&(X(),N(`draw() a mis plus de ${S} ms — sketch arrêté pour protéger le navigateur.`))}catch(e){N(String(e)),X()}}),he=C(r),_e=C(c),ye=C(d),be=C(f),ge=C(u),ve=C(h),we=C(i),xe=C(y),ke=C(b),Ee=C(g),Ce=C(v);const L=x?w(()=>{try{x()}catch(e){N(String(e))}}):null;let j=!1;H=new p5(e=>{Y._setP(e),ue&&(e.preload=()=>{ue()}),e.setup=()=>{me&&me(),e.canvas||Y.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),j=!0},e.draw=()=>{j&&fe()},e.mousePressed=()=>{j&&he&&he()},e.mouseReleased=()=>{j&&_e&&_e()},e.mouseDragged=()=>{j&&ye&&ye()},e.mouseMoved=()=>{j&&be&&be()},e.mouseWheel=e=>{j&&ge&&ge()},e.doubleClicked=()=>{j&&ve&&ve()},e.keyPressed=()=>{j&&we&&we()},e.keyReleased=()=>{j&&xe&&xe()},ke&&(e.touchStarted=()=>{j&&ke()}),Ee&&(e.touchMoved=()=>{j&&Ee()}),Ce&&(e.touchEnded=()=>{j&&Ce()}),e.windowResized=()=>{"fullscreen"===Y._mode?Y.size("max"):F(),L&&L()}},l),pe=!1,_.classList.remove("pf-running")}const Le='<!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.1/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\nFILLME-SCRIPTS\n\n</body>\n</html>';function je(){const n=e.map((e,n)=>{let t;t="python"!==e.type||e.hidden||e.readonly||!G[e.id]?e.code:G[e.id].getValue();const a=[],o="markdown"===e.type?"text/markdown":"text/python";null!==e.label&&a.push(`data-tab="${e.label.replace(/"/g,""")}"`),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=Le.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 ze=null,Ie=[];function Re(){const e=Y._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();ze=new MediaRecorder(t,{mimeType:n}),Ie=[],ze.ondataavailable=e=>{e.data.size&&Ie.push(e.data)},ze.onstop=()=>{const e=new Blob(Ie,{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),g.textContent="⏺",g.title="Enregistrer WebM",g.classList.remove("pf-recording"),ze=null},ze.start(),g.textContent="⏹",g.title="Arrêter l'enregistrement",g.classList.add("pf-recording")}function Me(){ze&&"inactive"!==ze.state&&ze.stop()}g.addEventListener("click",()=>{ze?Me():Re()}),_.addEventListener("click",()=>Se()),y.addEventListener("click",()=>{S?I():(L=window.innerHeight-32,j(),z())}),b.addEventListener("click",je);const Pe="https://codeberg.org/nopid/pyfrilet";function Ae(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)})}w.addEventListener("click",()=>window.open(Pe,"_blank")),v.addEventListener("click",()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&ie()}),window.addEventListener("keydown",e=>{const n=S&&J&&J.isFocused&&J.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void Se();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 I()}return e.preventDefault(),e.stopPropagation(),void(S?I():z())}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.")&&ie())):(e.preventDefault(),void oe())}else e.preventDefault()},!0),(async()=>{m.textContent="Chargement des dépendances…",p.style.display="flex";try{if(await Ae(o.p5),o.marked){const e=document.createElement("link");e.rel="stylesheet",e.href=o.katexCss,document.head.appendChild(e),await Ae(o.marked),await Ae(o.katex),await Ae(o.markedKatex),await Ae(o.mermaid),marked.use(markedKatex({throwOnError:!1})),mermaid.initialize({startOnLoad:!1,theme:"neutral"})}await Ae(o.ace),await Ae(o.acePython),await Ae(o.aceMonokai),await Ae(o.aceLangTools),await Ae(o.aceSearchbox),await Ae(o.pyodide)}catch(e){return m.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}te(),await Se(),p.style.display="none"})()}(C,k,x,w)})}();
|