pyfrilet 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/pyfrilet.js +20 -8
- package/pyfrilet.min.js +1 -1
package/package.json
CHANGED
package/pyfrilet.js
CHANGED
|
@@ -513,6 +513,19 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
513
513
|
try { snap = JSON.parse(raw); } catch (e) { snap = null; }
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
+
if (snap && snap.v === 1 && Array.isArray(snap.tabs) && snap.tabs.length > 0) {
|
|
517
|
+
/* Compare structural signature: label, type, hidden, readonly for each tab.
|
|
518
|
+
If the HTML has changed since the snapshot was saved, mark as stale but
|
|
519
|
+
still serve the old content — the user keeps their work until they
|
|
520
|
+
explicitly confirm a Reset (which always deletes SK first). */
|
|
521
|
+
const sig = t => `${t.label}|${t.type}|${t.hidden ? 1 : 0}|${t.readonly ? 1 : 0}`;
|
|
522
|
+
const snapSig = snap.tabs.map(sig).join(',');
|
|
523
|
+
const htmlSig = htmlTabs.map(sig).join(',');
|
|
524
|
+
if (snapSig !== htmlSig) snap._stale = true;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const staleSnapshot = !!(snap && snap._stale);
|
|
528
|
+
|
|
516
529
|
if (snap && snap.v === 1 && Array.isArray(snap.tabs) && snap.tabs.length > 0) {
|
|
517
530
|
/* Restore structure and content from snapshot */
|
|
518
531
|
tabs = snap.tabs.map((st, i) => {
|
|
@@ -544,15 +557,16 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
544
557
|
|
|
545
558
|
const noWatchdog = configTag.hasAttribute('data-no-watchdog');
|
|
546
559
|
|
|
547
|
-
main(tabs, htmlTabs, SK, URLS, noWatchdog);
|
|
560
|
+
main(tabs, htmlTabs, SK, URLS, noWatchdog, staleSnapshot);
|
|
548
561
|
});
|
|
549
562
|
|
|
550
563
|
/* ═══════════════════════════ MAIN ═══════════════════════════════════ */
|
|
551
|
-
function main(tabs, htmlTabs, SK, URLS, noWatchdog) {
|
|
564
|
+
function main(tabs, htmlTabs, SK, URLS, noWatchdog, staleSnapshot) {
|
|
552
565
|
|
|
553
566
|
/* tabs = working state (from snapshot or HTML), may be reassigned on reset
|
|
554
567
|
htmlTabs = ground truth from current HTML file, never mutated */
|
|
555
568
|
tabs = tabs.slice(); /* local mutable copy */
|
|
569
|
+
let _staleSnapshot = staleSnapshot; /* mutable — cleared after reset */
|
|
556
570
|
|
|
557
571
|
/* ── inject styles + markup ── */
|
|
558
572
|
const styleEl = document.createElement('style');
|
|
@@ -1041,7 +1055,7 @@ function main(tabs, htmlTabs, SK, URLS, noWatchdog) {
|
|
|
1041
1055
|
|
|
1042
1056
|
/* ── Dirty indicator ──────────────────────────────────── */
|
|
1043
1057
|
function refreshDirty() {
|
|
1044
|
-
const dirty = tabs.some(tab =>
|
|
1058
|
+
const dirty = _staleSnapshot || tabs.some(tab =>
|
|
1045
1059
|
!tab.hidden && !tab.readonly && tab.type === 'python' &&
|
|
1046
1060
|
aceSessions[tab.id] &&
|
|
1047
1061
|
aceSessions[tab.id].getValue() !== tab.starterCode
|
|
@@ -1051,11 +1065,9 @@ function main(tabs, htmlTabs, SK, URLS, noWatchdog) {
|
|
|
1051
1065
|
|
|
1052
1066
|
/* ── Reset: restore file structure + content, no reload ─ */
|
|
1053
1067
|
function resetAllTabs() {
|
|
1054
|
-
/* Cancel all pending saves first */
|
|
1055
1068
|
saveTimers.forEach(t => clearTimeout(t));
|
|
1056
1069
|
saveTimers.clear();
|
|
1057
1070
|
|
|
1058
|
-
/* Erase snapshot and any legacy per-tab keys */
|
|
1059
1071
|
try { localStorage.removeItem(SK); } catch (e) {}
|
|
1060
1072
|
tabs.forEach(tab => {
|
|
1061
1073
|
if (tab.label) {
|
|
@@ -1064,10 +1076,10 @@ function main(tabs, htmlTabs, SK, URLS, noWatchdog) {
|
|
|
1064
1076
|
});
|
|
1065
1077
|
try { localStorage.removeItem(SK + ':Code'); } catch (e) {}
|
|
1066
1078
|
|
|
1067
|
-
|
|
1079
|
+
_staleSnapshot = false;
|
|
1080
|
+
|
|
1068
1081
|
tabs = htmlTabs.map((ht, i) => ({ ...ht, id: 'tab-' + i, code: ht.starterCode }));
|
|
1069
1082
|
|
|
1070
|
-
/* Rebuild sessions and tab bar in-memory */
|
|
1071
1083
|
buildSessions();
|
|
1072
1084
|
buildTabBar();
|
|
1073
1085
|
refreshDirty();
|
|
@@ -1505,7 +1517,7 @@ m.__getattr__ = _p5_getattr
|
|
|
1505
1517
|
}
|
|
1506
1518
|
|
|
1507
1519
|
/* ─────────────────── DOWNLOAD ───────────────── */
|
|
1508
|
-
const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.5.
|
|
1520
|
+
const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.5.3/pyfrilet.min.js';
|
|
1509
1521
|
|
|
1510
1522
|
const STANDALONE_TEMPLATE = `<!doctype html>
|
|
1511
1523
|
<html lang="fr">
|
package/pyfrilet.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(){"use strict";const e=document.currentScript;let n=!1;const t="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js",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],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(/&/g,"&").replace(/</g,"<").replace(/>/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,""")}"`),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)})}();
|
|
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],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}if(L&&1===L.v&&Array.isArray(L.tabs)&&L.tabs.length>0){const e=e=>`${e.label}|${e.type}|${e.hidden?1:0}|${e.readonly?1:0}`;L.tabs.map(e).join(",")!==k.map(e).join(",")&&(L._stale=!0)}const j=!(!L||!L._stale);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 z=y.hasAttribute("data-no-watchdog");!function(e,t,a,o,r,i){e=e.slice();let s=i;const d=document.createElement("style");d.textContent=u,document.head.appendChild(d),document.body.innerHTML=h;const l=document.getElementById("pf-app"),c=document.getElementById("pf-drawer"),p=document.getElementById("pf-handle"),m=document.getElementById("pf-sketch"),f=document.getElementById("pf-viewport"),_=document.getElementById("pf-loader"),y=document.getElementById("pf-loader-msg"),g=document.getElementById("pf-err"),b=document.getElementById("pf-btn-run"),v=document.getElementById("pf-btn-code"),w=document.getElementById("pf-btn-dl"),x=document.getElementById("pf-btn-rec"),k=document.getElementById("pf-btn-reset"),E=document.getElementById("pf-btn-help"),C=document.getElementById("pf-grip"),S=document.getElementById("pf-handle-hint"),L=document.getElementById("pf-tabs"),j=document.getElementById("pf-markdown-view");let z=!1,I=Math.round(.56*window.innerHeight);function R(){document.documentElement.style.setProperty("--pf-drawer-h",I+"px")}function A(){z=!0,c.classList.add("pf-open"),v.classList.add("pf-active"),setTimeout(()=>{X(),q&&q.focus()},280)}function P(){z=!1,c.classList.remove("pf-open"),v.classList.remove("pf-active"),setTimeout(()=>{X();const e=J._p?.canvas;e&&e.removeAttribute("tabindex"),l.focus()},280)}function M(){z?P():A()}R();let T=null;const B=5,O=120,W=document.createElement("div");function D(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;T={y:n,h:z?I:0,moved:!1},W.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function K(e){if(!T)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=T.y-n;if(Math.abs(t)>B&&(T.moved=!0),!T.moved)return;const a=Math.max(0,Math.min(window.innerHeight-50,T.h+t));a<O?(c.style.transition="none",c.style.height="32px"):(I=a,R(),z||A(),c.style.transition="none",c.style.height=I+"px"),X()}function $(e){if(!T)return;const n=T.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??T.y,a=T.y-t,o=T.h+a;T=null,W.style.display="none",document.body.style.userSelect="",c.style.transition="",c.style.height="",n&&(o<O?P():(I=Math.max(O,Math.min(window.innerHeight-50,o)),R(),z||A()),X())}Object.assign(W.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(W),C.addEventListener("click",e=>{e.stopPropagation(),M()}),p.addEventListener("mousedown",D,!0),document.addEventListener("mousemove",K),document.addEventListener("mouseup",$),p.addEventListener("touchstart",D,{passive:!1}),document.addEventListener("touchmove",K,{passive:!0}),document.addEventListener("touchend",$);let N=0,U=0;function F(e){g.textContent=e,g.style.display="block",A()}function H(){g.textContent="",g.style.display="none"}function Y(){if(!J._p||"fit"!==J._mode)return;const e=J._w,n=J._h;if(!e||!n)return;const t=l.clientWidth,a=l.clientHeight,o=Math.min(t/e,a/n);f.style.transform=`scale(${o})`}function X(){if("fullscreen"===J._mode?J.size("max"):Y(),G&&"function"==typeof G.windowResized)try{G.windowResized()}catch(e){F(String(e))}q&&q.resize()}window.addEventListener("mousemove",e=>{N=e.clientX,U=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(N=e.touches[0].clientX,U=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=J._p?J._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=J._w/n.width,a=J._h/n.height;return[(N-n.left)*t,(U-n.top)*a]},window.addEventListener("resize",X);let G=null;const J=new Proxy({_p:null,_mode:"fit",_w:0,_h:0,_setP(e){this._p=e},size(e,n,t){if(!this._p)return;const a=t??void 0;"max"===e||null==e?(this._mode="fullscreen",this._w=l.clientWidth,this._h=l.clientHeight,void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),f.style.transform="scale(1)"):(this._mode="fit",this._w=Math.max(1,0|e),this._h=Math.max(1,0|n),void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),Y())},noSmooth(){this._p?.noSmooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="pixelated")},smooth(){this._p?.smooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="auto")},sketchTitle(e){S.textContent=String(e)},getItem(e){try{return localStorage.getItem(e)}catch(e){return null}},storeItem(e,n){try{localStorage.setItem(e,String(n))}catch(e){}},removeItem(e){try{localStorage.removeItem(e)}catch(e){}},clearStorage(){try{localStorage.clear()}catch(e){}}},{get(e,n){if(n in e)return"function"==typeof e[n]?e[n].bind(e):e[n];if(e._p&&n in e._p){const t=e._p[n];return"function"==typeof t?t.bind(e._p):t}},set:(e,n,t)=>n.startsWith("_")?(e[n]=t,!0):(e._p&&(e._p[n]=t),!0)});function V(){if(Be(),G){try{G.remove()}catch(e){}G=null}m.innerHTML="",J._p=null,J._mode="fit",J._w=0,J._h=0,f.style.transform="scale(1)",S.textContent="Shift+Entrée → relancer · Échap → ouvrir/fermer",ye&&(ye.destroy(),ye=null),he&&(he.destroy(),he=null),_e&&(_e.destroy(),_e=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),Le&&(Le.destroy(),Le=null),je&&(je.destroy(),je=null)}window.p5py=J;let q=null,Z=null;const Q={},ee=new Set;function ne(){L.innerHTML="",Z=null;const n=e.filter(e=>!e.hidden);L.style.display=n.length<=1?"none":"",n.forEach(e=>{const n=document.createElement("button");n.className="pf-tab",n.dataset.tabId=e.id,n.textContent=e.label,e.readonly&&n.classList.add("pf-tab-readonly"),"markdown"===e.type&&n.classList.add("pf-tab-markdown"),n.addEventListener("click",()=>te(e)),L.appendChild(n)}),n.length>0&&te(n[0],!0)}function te(e,n){if(n||Z!==e)if(Z=e,L.querySelectorAll(".pf-tab").forEach(n=>{n.classList.toggle("pf-tab-active",n.dataset.tabId===e.id)}),"markdown"===e.type){if(document.getElementById("pf-ace").style.display="none",j.style.display="block",window.marked){let n=marked.parse(e.starterCode);window.mermaid&&(n=n.replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,(e,n)=>`<div class="mermaid">${n.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}</div>`)),j.innerHTML=`<div class="pf-md-inner">${n}</div>`}else j.innerHTML=`<div class="pf-md-inner"><pre>${e.starterCode}</pre></div>`;window.mermaid&&mermaid.run({nodes:j.querySelectorAll(".mermaid")})}else document.getElementById("pf-ace").style.display="block",j.style.display="none",q&&Q[e.id]&&(q.setSession(Q[e.id]),q.setReadOnly(e.readonly),q.focus())}function ae(){let n=1;e.filter(e=>"python"===e.type).forEach(e=>{e.hidden||e.readonly||!Q[e.id]?n+=e.code.split("\n").length:(Q[e.id].setOption("firstLineNumber",n),n+=Q[e.id].getLength())})}function oe(){Object.keys(Q).forEach(e=>delete Q[e]),e.filter(e=>!e.hidden&&"python"===e.type).forEach(e=>{const n=ace.createEditSession(e.code,"ace/mode/python");if(n.setUseWorker(!1),n.setTabSize(4),Q[e.id]=n,!e.readonly){let e=null;n.on("change",()=>{null!==e&&(clearTimeout(e),ee.delete(e)),e=setTimeout(()=>{ee.delete(e),e=null,ie()},350),ee.add(e),ae(),de()})}});const n=e.find(e=>!e.hidden&&"python"===e.type);q&&n&&Q[n.id]&&(q.setSession(Q[n.id]),q.setReadOnly(n.readonly),q.renderer.updateFull(!0)),ae()}function re(){!o.ace.startsWith("vendor")&&o.ace.startsWith("http")||ace.config.set("basePath",o.ace.replace(/\/[^/]+$/,"/")),q=ace.edit("pf-ace"),q.setTheme("ace/theme/monokai"),q.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),q.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{q.completer?.popup?.isOpen||Ie()}}),q.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:P}),q.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:se}),q.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&le()}}),oe(),ne(),de()}function ie(){const n={v:1,tabs:e.map(e=>({label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,content:e.hidden||e.readonly||"python"!==e.type||!Q[e.id]?e.code:Q[e.id].getValue()}))};try{localStorage.setItem(a,JSON.stringify(n))}catch(e){}}function se(){ie()}function de(){const n=s||e.some(e=>!e.hidden&&!e.readonly&&"python"===e.type&&Q[e.id]&&Q[e.id].getValue()!==e.starterCode);k.classList.toggle("pf-dirty",n)}function le(){ee.forEach(e=>clearTimeout(e)),ee.clear();try{localStorage.removeItem(a)}catch(e){}e.forEach(e=>{if(e.label)try{localStorage.removeItem(a+":"+e.label.replace(/[^a-zA-Z0-9]/g,"_"))}catch(e){}});try{localStorage.removeItem(a+":Code")}catch(e){}s=!1,e=t.map((e,n)=>({...e,id:"tab-"+n,code:e.starterCode})),oe(),ne(),de(),Ie()}window.addEventListener("beforeunload",se);let ce=null,pe=null;async function me(){return pe||(pe=(async()=>{const e={};if(o.pyodideIndex&&(e.indexURL=o.pyodideIndex),ce=await loadPyodide(e),ce.runPython("\nimport sys, types, js\nfrom js import p5py, _pfMouse\nfrom pyodide.ffi import JsProxy\n\n# ── Python builtins that must NOT be shadowed ──────────────────────\n_BLACKLIST = frozenset({\n 'abs','all','any','bin','bool','bytes','callable','chr','compile',\n 'delattr','dict','dir','divmod','enumerate','eval','exec',\n 'filter','float','format','frozenset','getattr','globals','hasattr',\n 'hash','help','hex','id','input','int','isinstance','issubclass',\n 'iter','len','list','locals','map','max','min','next','object',\n 'oct','open','ord','pow','print','property','range','repr',\n 'reversed','round','set','setattr','slice','sorted','staticmethod',\n 'str','sum','super','tuple','type','vars','zip',\n # p5 lifecycle hooks — user defines these, we don't import them\n 'setup','draw','preload',\n})\n\n# ── Introspect a hidden dummy p5 instance ─────────────────────────\n_dummy_node = js.document.createElement('div')\n_dummy = js.p5.new(lambda _: None, _dummy_node)\n\n_p5_functions = set() # names of callable JS members\n_p5_attributes = set() # names of scalar/readable members\n\nfor _n in dir(_dummy):\n if _n.startswith('_') or _n in _BLACKLIST:\n continue\n _v = getattr(_dummy, _n)\n if isinstance(_v, JsProxy):\n if callable(_v):\n _p5_functions.add(_n)\n # non-callable JsProxy (canvas, pixels…) → skip\n else:\n _p5_attributes.add(_n)\n\n# Read real initial values now, while dummy is still alive\n_attr_init = {}\nfor _n in _p5_attributes:\n try:\n _attr_init[_n] = getattr(_dummy, _n)\n except Exception:\n _attr_init[_n] = 0\n\n_dummy.remove()\ndel _dummy, _dummy_node\n\n# ── Build module ───────────────────────────────────────────────────\nm = types.ModuleType(\"p5\")\n\n# Generic function wrapper: delegates to live p5Bridge instance\nclass _FW:\n __slots__ = ('_n',)\n def __init__(self, n): self._n = n\n def __call__(self, *a): return getattr(p5py, self._n)(*a)\n def __repr__(self): return f'<p5 function {self._n}>'\n\nfor _n in _p5_functions:\n setattr(m, _n, _FW(_n))\n\n# ── Special overrides (our bridge has custom behaviour) ────────────\n# smooth/noSmooth exist on a real p5 instance so introspection finds\n# them — but our Proxy overrides them to also toggle CSS image-rendering.\n# size and sketchTitle are pyfrilet-only: NOT on a real p5 instance,\n# so introspection misses them — add them explicitly.\nfor _n in ('sketchTitle',):\n setattr(m, _n, _FW(_n))\n _p5_functions.add(_n) # keep __all__ consistent\n\n# size() calls _pf_refresh after resizing so width/height are immediately\n# correct in setup() — consistent with p5.js JS behaviour.\nclass _SizeWrapper:\n def __call__(self, *a):\n p5py.size(*a)\n _pf_refresh(_ns_ref[0])\n return _GetCanvasWrapper()()\n def __repr__(self): return '<p5 function size>'\nsetattr(m, 'size', _SizeWrapper())\nsetattr(m, 'createCanvas', m.size) # alias — createCanvas(...) == size(...)\n_p5_functions.add('size')\n_p5_functions.add('createCanvas')\n_ns_ref = [{}] # filled in by runCode before each exec\n\n# getCanvas() — returns the p5.Element wrapping the canvas,\n# so the user can call .drop(create_proxy(fn)), .mouseOver(), etc. directly like in JS.\nclass _GetCanvasWrapper:\n def __call__(self):\n p = p5py._p\n if p is None:\n raise RuntimeError('getCanvas() doit être appelé dans setup() ou après')\n p.canvas.id = '__pf_canvas__'\n return p.select('#__pf_canvas__')\n def __repr__(self): return '<p5 function getCanvas>'\nsetattr(m, 'getCanvas', _GetCanvasWrapper())\n_p5_functions.add('getCanvas')\n\n# mouseX / mouseY: override with our accurate coordinate calculator\n# (p5's own values are wrong when a CSS-transformed parent is used)\n_MOUSE_OVERRIDE = frozenset({'mouseX', 'mouseY'})\n\n# Initial values from the dummy instance — constants like WEBGL, DEGREES,\n# LEFT_ARROW… are correct from the very first setup() call.\nfor _n in _p5_attributes:\n if _n in _MOUSE_OVERRIDE:\n setattr(m, _n, 0.0)\n else:\n setattr(m, _n, _attr_init.get(_n, 0))\n\n# Build __all__ for import * — done later, after snake_case aliases are added\n\n# ── _pf_refresh: called before every event callback ───────────────\nimport re as _re\n\n# Pre-compute snake_case alias for each attribute — None if identical\n_attr_snake = {\n _k: (_re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _k) or None)\n for _k in _p5_attributes\n}\n_attr_snake = {_k: (_s if _s != _k else None) for _k, _s in _attr_snake.items()}\n\n# Add snake_case names to _p5_attributes so __all__ and _pf_refresh cover them\nfor _k, _sk in list(_attr_snake.items()):\n if _sk:\n _p5_attributes.add(_sk)\n setattr(m, _sk, getattr(m, _k, 0)) # initial value mirrors camelCase\n _attr_snake[_sk] = None # snake name has no further alias\n\ndef _pf_refresh(ns):\n # accurate mouse coords (bypasses p5's stale CSS-transform offset)\n mx, my = _pfMouse()\n\n # update all known scalar attributes from live instance\n for _k in _p5_attributes:\n _sk = _attr_snake.get(_k)\n if _k in _MOUSE_OVERRIDE:\n _v = mx if _k in ('mouseX', 'mouse_x') else my\n elif _sk is None and _k not in _attr_snake:\n # pure snake_case entry — skip, updated via its camelCase counterpart\n continue\n else:\n try:\n _v = getattr(p5py, _k)\n except Exception:\n continue\n setattr(m, _k, _v)\n if _k in ns:\n ns[_k] = _v\n if _sk:\n setattr(m, _sk, _v)\n if _sk in ns:\n ns[_sk] = _v\n\nsys.modules[\"p5\"] = m\n\n# ── draw() watchdog via sys.settrace ──────────────────────────────\n# Trace is called on every Python line event. We only call time.monotonic()\n# every N events to minimize overhead — a tight loop still triggers within\n# a few microseconds, so detection latency is negligible.\nimport time as _time\n\n_WDOG_CHECK_EVERY = 100\n_wdog_deadline = [0.0]\n_wdog_count = [0]\n\ndef _wdog_trace(frame, event, arg):\n _wdog_count[0] += 1\n if _wdog_count[0] >= _WDOG_CHECK_EVERY:\n _wdog_count[0] = 0\n if _time.monotonic() > _wdog_deadline[0]:\n raise TimeoutError(\"draw() watchdog\")\n return _wdog_trace\n\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"),q){fe(ce.runPython("list(m.__all__)").toJs())}})(),pe)}function fe(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,a,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,q.completers=[...q.completers||[],t]}let ue=!1,he=null,_e=null,ye=null,ge=null,be=null,ve=null,we=null,xe=null,ke=null,Ee=null,Ce=null,Se=null,Le=null,je=null;const ze=300;async function Ie(){if(ue)return;ue=!0,b.classList.add("pf-running"),H(),V(),ce||(y.textContent="Initialisation de Pyodide…",_.style.display="flex");try{await me()}catch(e){return _.style.display="none",F("Erreur Pyodide : "+e),ue=!1,void b.classList.remove("pf-running")}_.style.display="none";const t=e.filter(e=>"python"===e.type).map(e=>e.hidden||e.readonly||!Q[e.id]?e.code:Q[e.id].getValue()).join("\n");try{y.textContent="Chargement des dépendances…",_.style.display="flex",await ce.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:n})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}_.style.display="none",ce.globals.set("_USER_CODE",t);try{ce.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)"),ce.runPython("_ns_ref[0] = _ns")}catch(e){return F(String(e)),ue=!1,void b.classList.remove("pf-running")}let a,o,i,s,d,l,c,p,f,u,h,g,v,w;try{const e=(e,n)=>ce.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"),c=e("mouseReleased","mouse_released"),p=e("mouseMoved","mouse_moved"),f=e("mouseWheel","mouse_wheel"),u=e("doubleClicked","double_clicked"),h=e("keyReleased","key_released"),g=e("touchStarted","touch_started"),v=e("touchMoved","touch_moved"),w=e("touchEnded","touch_ended")}catch(e){return F(String(e)),ue=!1,void b.classList.remove("pf-running")}if(!o)return F("Le script doit définir au moins une fonction draw()."),ue=!1,void b.classList.remove("pf-running");const{create_proxy:x}=ce.pyimport("pyodide.ffi"),k=ce.runPython("_ns.get('windowResized')"),E=ce.globals.get("_pf_refresh"),C=ce.globals.get(r?"_pf_draw_direct":"_pf_draw_watchdog"),S=ce.globals.get("_ns"),L=e=>e?x(()=>{try{E(S),e()}catch(e){F(String(e))}}):null;ye=d?x(()=>{try{d()}catch(e){F(String(e))}}):null,he=a?x(()=>{try{a()}catch(e){F(String(e))}}):null,_e=x(()=>{try{E(S),C(o,ze)}catch(e){const n=String(e);V(),n.includes("TimeoutError")||n.includes("watchdog")?F(`draw() a dépassé ${ze}ms — sketch arrêté (watchdog).`):F(n)}}),ge=L(i),be=L(c),ve=L(l),we=L(p),xe=L(f),ke=L(u),Ee=L(s),Ce=L(h),Se=L(g),Le=L(v),je=L(w);const j=k?x(()=>{try{k()}catch(e){F(String(e))}}):null;let z=!1;G=new p5(e=>{J._setP(e),ye&&(e.preload=()=>{ye()}),e.setup=()=>{he&&he(),e.canvas||J.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),z=!0},e.draw=()=>{z&&_e()},e.mousePressed=()=>{z&&ge&&ge()},e.mouseReleased=()=>{z&&be&&be()},e.mouseDragged=()=>{z&&ve&&ve()},e.mouseMoved=()=>{z&&we&&we()},e.mouseWheel=e=>{z&&xe&&xe()},e.doubleClicked=()=>{z&&ke&&ke()},e.keyPressed=()=>{z&&Ee&&Ee()},e.keyReleased=()=>{z&&Ce&&Ce()},Se&&(e.touchStarted=()=>{z&&Se()}),Le&&(e.touchMoved=()=>{z&&Le()}),je&&(e.touchEnded=()=>{z&&je()}),e.windowResized=()=>{"fullscreen"===J._mode?J.size("max"):Y(),j&&j()}},m),ue=!1,b.classList.remove("pf-running")}const Re='<!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.3/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\nFILLME-SCRIPTS\n\n</body>\n</html>';function Ae(){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,""")}"`),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=Re.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 Pe=null,Me=[];function Te(){const e=J._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();Pe=new MediaRecorder(t,{mimeType:n}),Me=[],Pe.ondataavailable=e=>{e.data.size&&Me.push(e.data)},Pe.onstop=()=>{const e=new Blob(Me,{type:n}),t=URL.createObjectURL(e),a=n.includes("webm")?"webm":"mp4";Object.assign(document.createElement("a"),{href:t,download:`sketch.${a}`}).click(),URL.revokeObjectURL(t),x.textContent="⏺",x.title="Enregistrer WebM",x.classList.remove("pf-recording"),Pe=null},Pe.start(),x.textContent="⏹",x.title="Arrêter l'enregistrement",x.classList.add("pf-recording")}function Be(){Pe&&"inactive"!==Pe.state&&Pe.stop()}x.addEventListener("click",()=>{Pe?Be():Te()}),b.addEventListener("click",()=>Ie()),v.addEventListener("click",()=>{z?P():(I=window.innerHeight-32,R(),A())}),w.addEventListener("click",Ae);const Oe="https://codeberg.org/nopid/pyfrilet";function We(e){return new Promise((n,t)=>{const a=document.createElement("script");a.src=e,a.onload=n,a.onerror=()=>t(new Error("Impossible de charger : "+e)),document.head.appendChild(a)})}E.addEventListener("click",()=>window.open(Oe,"_blank")),k.addEventListener("click",()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&le()}),window.addEventListener("keydown",e=>{const n=z&&q&&q.isFocused&&q.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void Ie();if("Escape"===e.key){const t=document.querySelector(".ace_search");if(t&&"none"!==t.style.display)return e.preventDefault(),e.stopPropagation(),q.searchBox?q.searchBox.hide():t.style.display="none",void q.focus();if(n){const n=q.completer?.popup?.isOpen;if(n)return;return e.preventDefault(),e.stopPropagation(),void P()}return e.preventDefault(),e.stopPropagation(),void(z?P():A())}if(!n)return"s"!==e.key&&"S"!==e.key||!e.ctrlKey&&!e.metaKey?"r"!==e.key&&"R"!==e.key||!e.ctrlKey&&!e.metaKey||e.altKey?void 0:(e.preventDefault(),void(confirm("Réinitialiser ? Les modifications seront perdues.")&&le())):(e.preventDefault(),void se())}else e.preventDefault()},!0),(async()=>{y.textContent="Chargement des dépendances…",_.style.display="flex";try{if(await We(o.p5),o.marked){const e=document.createElement("link");e.rel="stylesheet",e.href=o.katexCss,document.head.appendChild(e),await We(o.marked),await We(o.katex),await We(o.markedKatex),await We(o.mermaid),marked.use(markedKatex({throwOnError:!1})),mermaid.initialize({startOnLoad:!1,theme:"neutral"})}await We(o.ace),await We(o.acePython),await We(o.aceMonokai),await We(o.aceLangTools),await We(o.aceSearchbox),await We(o.pyodide)}catch(e){return y.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}re(),await Ie(),_.style.display="none"})()}(C,k,x,w,z,j)})}();
|