JVCode

Améliore la balise codesur JVC : design moderne, Piston API, persistence preview, live preview fix, execution boutton.

21.12.2025 itibariyledir. En son verisyonu görün.

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

You will need to install an extension such as Tampermonkey to install this script.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==UserScript==
// @name         JVCode
// @namespace    http://tampermonkey.net/
// @version      1.0.3
// @description  Améliore la balise codesur JVC : design moderne, Piston API, persistence preview, live preview fix, execution boutton.
// @author       FaceDePet
// @match        https://www.jeuxvideo.com/forums/*
// @match        https://www.jeuxvideo.com/messages-prives/*
// @match        https://www.jeuxvideo.com/news/*
// @match        https://www.jeuxvideo.com/wikis-soluce-astuces/*
// @icon         https://image.noelshack.com/fichiers/2017/04/1485268586-hackeur-v1.png
// @require      https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js
// @resource     HLJS_CSS https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css
// @connect      emkc.org
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        GM_setClipboard
// @grant        GM_xmlhttpRequest
// @license      MIT
// @run-at       document-end
// ==/UserScript==


(function() {
    'use strict';

    const hljsCss = GM_getResourceText("HLJS_CSS");
    GM_addStyle(hljsCss);

    // --- CSS ---
    const customCss = `
    /* SVG Data URIs */
    :root {
        --jv-icon-play: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='14' height='14' stroke='%23abb2bf' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='5 3 19 12 5 21 5 3'%3E%3C/polygon%3E%3C/svg%3E");
        --jv-icon-stop: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='14' height='14' stroke='%23e06c75' stroke-width='2' fill='%23e06c75' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='6' y='6' width='12' height='12'%3E%3C/rect%3E%3C/svg%3E");
        --jv-icon-loader: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='14' height='14' stroke='%23e06c75' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 12a9 9 0 1 1-6.219-8.56'%3E%3C/path%3E%3C/svg%3E");
        --jv-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='16' height='16' stroke='%23abb2bf' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='9' y='9' width='13' height='13' rx='2' ry='2'%3E%3C/rect%3E%3Cpath d='M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1'%3E%3C/path%3E%3C/svg%3E");
        --jv-icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='16' height='16' stroke='%2398c379' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
        --jv-icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='20' height='20' stroke='%23abb2bf' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");
    }

    .jv-enhanced-code {
        background-color: #282c34; border-radius: 8px; margin: 10px 0; overflow: hidden;
        font-family: 'Fira Code', 'Consolas', monospace; font-size: 13px;
        box-shadow: 0 4px 6px rgba(0,0,0,0.3); border: 1px solid #3e4451;
        display: flex; flex-direction: column; max-width: 100%;
    }
    .jv-code-header {
        display: flex; justify-content: space-between; align-items: center;
        background-color: #21252b; padding: 5px 10px 5px 15px;
        border-bottom: 1px solid #181a1f; height: 32px;
    }
    .jv-lang-select {
        background-color: #2c313a; color: #61afef; border: 1px solid #3e4451;
        border-radius: 4px; padding: 2px 5px; font-size: 11px; font-weight: bold;
        cursor: pointer; outline: none;
        max-width: 150px;
        text-overflow: ellipsis; white-space: nowrap; overflow: hidden;
    }
    .jv-code-actions { display: flex; gap: 6px; align-items: center; flex-shrink: 0; }

    .jv-icon-btn {
        background: transparent; border: 1px solid transparent; color: #abb2bf;
        cursor: pointer; padding: 4px; border-radius: 4px; display: flex;
        align-items: center; justify-content: center; height: 26px; min-width: 26px;
    }
    .jv-icon-btn:hover { background-color: #3e4451; color: white; }

    .jv-run-btn { border: 1px solid #3e4451; padding: 0 8px; gap: 5px; font-size: 11px; font-weight: bold; transition: all 0.2s;}
    .jv-run-btn:hover { border-color: #98c379; color: #98c379; }

    .jv-icon {
        display: inline-block; width: 16px; height: 16px;
        background-position: center; background-repeat: no-repeat; background-size: contain;
        vertical-align: middle;
    }
    .jv-icon-play { background-image: var(--jv-icon-play); }
    .jv-icon-stop { background-image: var(--jv-icon-stop); }
    .jv-icon-loader { background-image: var(--jv-icon-loader); }
    .jv-icon-copy { background-image: var(--jv-icon-copy); }
    .jv-icon-check { background-image: var(--jv-icon-check); }
    .jv-icon-close { background-image: var(--jv-icon-close); width: 20px; height: 20px;}

    .jv-run-btn.is-running { border-color: #e06c75; color: #e06c75; }
    .jv-run-btn.is-running:hover { background-color: rgba(224, 108, 117, 0.1); }

    @keyframes jv-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
    .jv-spin-anim { animation: jv-spin 1s linear infinite; }

    .jv-code-body { display: flex; width: 100%; min-width: 0; }
    .jv-line-numbers {
        text-align: right; padding: 10px 10px 10px 15px; background-color: #282c34;
        color: #495162; border-right: 1px solid #3e4451; user-select: none; flex-shrink: 0;
    }
    .jv-code-content {
        padding: 10px 15px; background-color: #282c34; color: #abb2bf; line-height: 1.5;
        tab-size: 4; flex-grow: 1; overflow-x: auto; min-width: 0;
    }
    .jv-code-content pre { margin: 0 !important; padding: 0 !important; border: none !important; background: none !important; }
    .jv-code-content code.hljs { padding: 0; background: transparent; white-space: pre; overflow-x: visible; }
    .jv-code-output {
        background-color: #1e2127; border-top: 1px solid #3e4451; padding: 10px;
        color: #e5c07b; font-family: 'Consolas', monospace; font-size: 12px;
        white-space: pre-wrap; display: none; max-height: 300px; overflow-y: auto;
    }
    .jv-code-output.error { color: #e06c75; }
    .jv-code-output.warning { color: #d19a66; }
    .jv-code-output.loading { color: #abb2bf; font-style: italic; }
    .jv-code-content::-webkit-scrollbar, .jv-code-output::-webkit-scrollbar { height: 8px; width: 8px; background-color: #282c34; }
    .jv-code-content::-webkit-scrollbar-thumb, .jv-code-output::-webkit-scrollbar-thumb { background-color: #4b5363; border-radius: 4px; }

    .jv-modal-overlay {
        position: fixed; top: 0; left: 0; width: 100%; height: 100%;
        background: rgba(0, 0, 0, 0.7);
        z-index: 9999999;
        display: flex; justify-content: center; align-items: center;
        opacity: 0; visibility: hidden; transition: all 0.2s;
    }
    .jv-modal-overlay.active { opacity: 1; visibility: visible; }
    .jv-modal {
        background: #21252b; border: 1px solid #3e4451; border-radius: 8px;
        width: 400px; max-width: 90%; max-height: 80vh; display: flex; flex-direction: column;
        box-shadow: 0 10px 25px rgba(0,0,0,0.5);
    }
    .jv-modal-header { padding: 15px; border-bottom: 1px solid #3e4451; display: flex; justify-content: space-between; align-items: center; color: #abb2bf; font-weight: bold; }
    .jv-modal-close { cursor: pointer; color: #abb2bf; background: none; border: none; font-size: 16px;}
    .jv-modal-search { padding: 10px; border-bottom: 1px solid #3e4451; }
    .jv-modal-search input { width: 100%; background: #282c34; border: 1px solid #3e4451; color: white; padding: 8px; border-radius: 4px; outline: none; }
    .jv-modal-list { overflow-y: auto; flex-grow: 1; padding: 5px; }
    .jv-modal-item { padding: 8px 10px; cursor: pointer; color: #abb2bf; border-radius: 4px; display: flex; justify-content: space-between; }
    .jv-modal-item:hover { background-color: #3e4451; color: white; }
    .jv-loader { text-align: center; padding: 20px; color: #61afef; }

    @media (max-width: 600px) {
        .jv-lang-select { max-width: 100px; font-size: 10px; }
        .jv-code-header { padding: 5px; }
        .jv-run-btn span:not(.jv-icon) { display: none; }
        .jv-run-btn { padding: 0 5px; }
    }
    `;
    GM_addStyle(customCss);

    // --- DATA ---
    const initialLangs = [
        { name: 'Texte', piston: null, hljs: 'text' },
        { name: 'Python', piston: 'python', hljs: 'python' },
        { name: 'JavaScript', piston: 'javascript', hljs: 'javascript' },
        { name: 'TypeScript', piston: 'typescript', hljs: 'typescript' },
        { name: 'Java', piston: 'java', hljs: 'java' },
        { name: 'C', piston: 'c', hljs: 'c' },
        { name: 'C++', piston: 'cpp', hljs: 'cpp' },
        { name: 'C#', piston: 'csharp', hljs: 'csharp' },
        { name: 'Go', piston: 'go', hljs: 'go' },
        { name: 'Rust', piston: 'rust', hljs: 'rust' },
        { name: 'PHP', piston: 'php', hljs: 'php' },
        { name: 'Lua', piston: 'lua', hljs: 'lua' },
        { name: 'Bash', piston: 'bash', hljs: 'bash' },
        { name: 'SQL', piston: 'sqlite3', hljs: 'sql' }
    ];

    const fallbackRuntimes = [
        { language: 'swift', version: 'Offline fallback', aliases: [] },
        { language: 'ruby', version: 'Offline fallback', aliases: [] },
        { language: 'kotlin', version: 'Offline fallback', aliases: [] },
        { language: 'scala', version: 'Offline fallback', aliases: [] },
        { language: 'perl', version: 'Offline fallback', aliases: [] },
        { language: 'haskell', version: 'Offline fallback', aliases: [] },
        { language: 'clojure', version: 'Offline fallback', aliases: [] },
        { language: 'elixir', version: 'Offline fallback', aliases: [] },
        { language: 'dart', version: 'Offline fallback', aliases: [] }
    ];

    let cachedRuntimes = null;
    let currentSelectElement = null;
    let modalOverlay = null;
    const previewState = {};

    // --- FONCTIONS ---

    function fetchAllRuntimes(callback) {
        if (cachedRuntimes && cachedRuntimes.length > 0) { callback(cachedRuntimes); return; }

        GM_xmlhttpRequest({
            method: "GET",
            url: "https://emkc.org/api/v2/piston/runtimes",
            timeout: 5000,
            onload: function(response) {
                if (response.status === 200) {
                    try {
                        const data = JSON.parse(response.responseText);
                        cachedRuntimes = data.sort((a, b) => a.language.localeCompare(b.language));
                        callback(cachedRuntimes);
                    } catch (e) { callback(fallbackRuntimes); }
                } else { callback(fallbackRuntimes); }
            },
            onerror: function(err) { callback(fallbackRuntimes); },
            ontimeout: function() { callback(fallbackRuntimes); }
        });
    }

    function createModal() {
        if (document.querySelector('.jv-modal-overlay')) return;
        modalOverlay = document.createElement('div');
        modalOverlay.className = 'jv-modal-overlay';
        modalOverlay.innerHTML = `<div class="jv-modal"> <div class="jv-modal-header"><span>Sélectionner un langage</span><button class="jv-modal-close" type="button"><span class="jv-icon jv-icon-close"></span></button></div> <div class="jv-modal-search"><input type="text" placeholder="Rechercher..." id="jv-lang-search"></div> <div class="jv-modal-list" id="jv-lang-list"><div class="jv-loader">Chargement...</div></div> </div>`;
        document.body.appendChild(modalOverlay);

        const closeBtn = modalOverlay.querySelector('.jv-modal-close');
        const searchInput = modalOverlay.querySelector('#jv-lang-search');
        const close = () => {
            modalOverlay.classList.remove('active');
            if(currentSelectElement && currentSelectElement.value === 'LOAD_MORE') {
                currentSelectElement.value = currentSelectElement.getAttribute('data-prev-val') || 'text';
            }
            currentSelectElement = null;
        };
        closeBtn.addEventListener('click', close);
        modalOverlay.addEventListener('click', (e) => { if (e.target === modalOverlay) close(); });
        searchInput.addEventListener('input', (e) => { renderModalList(e.target.value); });
    }

    function renderModalList(filter = '') {
        const listContainer = document.getElementById('jv-lang-list');
        listContainer.innerHTML = '';
        const data = cachedRuntimes || fallbackRuntimes;
        if (data === fallbackRuntimes) {
            const warning = document.createElement('div');
            warning.style.cssText = "padding:5px 10px; background:#3e2e2e; color:#e06c75; font-size:11px; text-align:center;";
            warning.textContent = "⚠ API indisponible. Liste réduite (Mode hors-ligne).";
            listContainer.appendChild(warning);
        }
        const lowerFilter = filter.toLowerCase();
        let count = 0;
        data.forEach(rt => {
            if (rt.language.toLowerCase().includes(lowerFilter) || (rt.aliases && rt.aliases.some(a => a.toLowerCase().includes(lowerFilter)))) {
                const item = document.createElement('div');
                item.className = 'jv-modal-item';
                item.innerHTML = `<span>${rt.language}</span><span class="jv-modal-item-ver">${rt.version}</span>`;
                item.addEventListener('click', () => { selectLanguageFromModal(rt); });
                listContainer.appendChild(item);
                count++;
            }
        });
        if (count === 0) {
            const noRes = document.createElement('div');
            noRes.style.padding = "10px"; noRes.style.color = "#aaa"; noRes.textContent = "Aucun résultat.";
            listContainer.appendChild(noRes);
        }
    }

    function openLangModal(targetSelect) {
        if (!modalOverlay) createModal();
        currentSelectElement = targetSelect;
        modalOverlay.classList.add('active');
        document.getElementById('jv-lang-search').value = '';
        document.getElementById('jv-lang-search').focus();
        if (!cachedRuntimes) { fetchAllRuntimes(() => renderModalList()); } else { renderModalList(); }
    }

    function selectLanguageFromModal(runtime) {
        if (!currentSelectElement) return;
        let exists = false;
        for (let i = 0; i < currentSelectElement.options.length; i++) {
            if (currentSelectElement.options[i].value === runtime.language) { exists = true; break; }
        }
        if (!exists) {
            const option = document.createElement('option');
            option.value = runtime.language;
            option.textContent = runtime.language.charAt(0).toUpperCase() + runtime.language.slice(1);
            option.setAttribute('data-hljs', runtime.language);
            currentSelectElement.add(option, currentSelectElement.options.length - 1);
        }
        currentSelectElement.value = runtime.language;
        currentSelectElement.dispatchEvent(new Event('change'));
        modalOverlay.classList.remove('active');
        currentSelectElement = null;
    }

    function resetRunButton(btn) {
        if (!btn) return;
        const icon = btn.querySelector('.jv-icon');
        const text = btn.querySelector('span:not(.jv-icon)');

        icon.className = 'jv-icon jv-icon-play';
        icon.classList.remove('jv-spin-anim');
        text.textContent = 'Run';

        btn.classList.remove('is-running');
        btn.removeAttribute('data-running');
    }

    function runCode(code, pistonLang, outputElement, runBtn) {
        if (!pistonLang || pistonLang === 'null') {
            outputElement.style.display = 'block'; outputElement.textContent = "Langage non exécutable.";
            outputElement.className = 'jv-code-output error'; return null;
        }

        outputElement.style.display = 'block';
        outputElement.textContent = "Exécution en cours...";
        outputElement.className = 'jv-code-output loading';

        // UI en mode STOP
        const icon = runBtn.querySelector('.jv-icon');
        const text = runBtn.querySelector('span:not(.jv-icon)');

        icon.className = 'jv-icon jv-icon-loader jv-spin-anim';
        text.textContent = 'Stop';

        runBtn.classList.add('is-running');
        runBtn.setAttribute('data-running', 'true');

        const slowResponseTimer = setTimeout(() => {
            if (runBtn.getAttribute('data-running') === 'true') {
                outputElement.className = 'jv-code-output warning';
                outputElement.innerHTML = "⏳ <b>Attente prolongée...</b><br>L'API Piston met du temps à répondre.<br>On patiente encore un peu...";
            }
        }, 3000);

        return GM_xmlhttpRequest({
            method: "POST",
            url: "https://emkc.org/api/v2/piston/execute",
            headers: { "Content-Type": "application/json" },
            data: JSON.stringify({ language: pistonLang, version: "*", files: [{ content: code }] }),
            onload: function(response) {
                clearTimeout(slowResponseTimer);
                if (runBtn.getAttribute('data-running') !== 'true') return;

                resetRunButton(runBtn);
                if (response.status === 0) {
                    outputElement.className = 'jv-code-output error';
                    outputElement.innerHTML = "⚠️ <b>Erreur (Status 0)</b><br>Requête bloquée ou échouée.";
                    return;
                }
                if (response.status !== 200) {
                    outputElement.className = 'jv-code-output error';
                    outputElement.textContent = `Erreur API HTTP ${response.status}.`;
                    return;
                }
                try {
                    const res = JSON.parse(response.responseText);
                    let output = (res.run ? (res.run.stdout + res.run.stderr) : (res.message ? `Error: ${res.message}` : "Erreur inconnue"));
                    if (!output.trim()) output = "<Aucune sortie>";
                    outputElement.className = res.run && res.run.stderr ? 'jv-code-output error' : 'jv-code-output';
                    outputElement.textContent = output;
                } catch (e) {
                    outputElement.className = 'jv-code-output error'; outputElement.textContent = "Erreur parsing API.";
                }
            },
            onerror: function(err) {
                clearTimeout(slowResponseTimer);
                if (runBtn.getAttribute('data-running') !== 'true') return;
                resetRunButton(runBtn);
                outputElement.className = 'jv-code-output error';
                outputElement.textContent = "Erreur réseau.";
            },
            onabort: function() {
                clearTimeout(slowResponseTimer);
                resetRunButton(runBtn);
            },
            ontimeout: function() {
                clearTimeout(slowResponseTimer);
                if (runBtn.getAttribute('data-running') !== 'true') return;
                resetRunButton(runBtn);
                outputElement.className = 'jv-code-output error';
                outputElement.textContent = "Délai d'attente dépassé.";
            }
        });
    }

    function enhanceCodeBlocks(container = document) {
        const selectors = 'code.code-jv:not([data-processed="true"]), code.message__code:not([data-processed="true"])';
        const codeElements = container.querySelectorAll(selectors);

        codeElements.forEach(codeElement => {
            codeElement.setAttribute('data-processed', 'true');

            let targetToReplace = codeElement;
            const parent = codeElement.parentElement;
            if (parent && parent.tagName === 'PRE' && (parent.classList.contains('pre-jv') || parent.classList.contains('message__pre'))) {
                targetToReplace = parent;
                parent.setAttribute('data-processed', 'true');
            }

            let rawCode = codeElement.textContent.replace(/^\s*\n/g, '').replace(/\n\s*$/g, '');

            const isPreview = codeElement.closest('.messageEditor__containerPreview');
            let detectedHljsLang = null;
            let forcedPistonLang = null;

            if (isPreview) {
                const allRawCodesInPreview = Array.from(document.querySelectorAll('.messageEditor__containerPreview code.message__code, .messageEditor__containerPreview .code-jv'));
                const previewIndex = allRawCodesInPreview.indexOf(codeElement);
                if (previewIndex !== -1 && previewState[previewIndex]) {
                    forcedPistonLang = previewState[previewIndex].piston;
                    detectedHljsLang = previewState[previewIndex].hljs;
                }
            }

            if (!detectedHljsLang) {
                const highlightResult = hljs.highlightAuto(rawCode);
                detectedHljsLang = highlightResult.language || 'text';
            }

            let safeHighlightedCode;
            try { safeHighlightedCode = hljs.highlight(rawCode, { language: detectedHljsLang }).value; }
            catch (e) { safeHighlightedCode = hljs.highlightAuto(rawCode).value; }

            const lineCount = rawCode.split(/\r\n|\r|\n/).length;
            let lineNumbersHtml = '';
            for (let i = 1; i <= lineCount; i++) { lineNumbersHtml += `${i}\n`; }

            let optionsHtml = '';
            initialLangs.forEach(lang => {
                let isSelected = false;
                if (forcedPistonLang) {
                    if (lang.piston === forcedPistonLang) isSelected = true;
                } else if (!forcedPistonLang && lang.hljs === detectedHljsLang) {
                    isSelected = true;
                }
                optionsHtml += `<option value="${lang.piston || ''}" data-hljs="${lang.hljs}" ${isSelected ? 'selected' : ''}>${lang.name}</option>`;
            });

            if (forcedPistonLang && !initialLangs.some(l => l.piston === forcedPistonLang)) {
                optionsHtml += `<option value="${forcedPistonLang}" data-hljs="${detectedHljsLang}" selected>${forcedPistonLang}</option>`;
            }
            optionsHtml += `<option value="LOAD_MORE" style="font-weight:bold; color:#61afef;">➕ Charger plus...</option>`;

            const wrapper = document.createElement('div');
            wrapper.className = 'jv-enhanced-code';

            wrapper.innerHTML = `
                <div class="jv-code-header">
                    <select class="jv-lang-select" title="Changer le langage">${optionsHtml}</select>
                    <div class="jv-code-actions">
                        <button type="button" class="jv-icon-btn jv-run-btn" title="Exécuter (Piston API)"><span class="jv-icon jv-icon-play"></span> <span>Run</span></button>
                        <button type="button" class="jv-icon-btn jv-copy-btn" title="Copier le code"><span class="jv-icon jv-icon-copy"></span></button>
                    </div>
                </div>
                <div class="jv-code-body">
                    <div class="jv-line-numbers"><pre>${lineNumbersHtml}</pre></div>
                    <div class="jv-code-content"><pre><code class="hljs ${detectedHljsLang}">${safeHighlightedCode}</code></pre></div>
                </div>
                <div class="jv-code-output"></div>
            `;

            const selectInfo = wrapper.querySelector('.jv-lang-select');
            const codeContentBlock = wrapper.querySelector('.jv-code-content code');
            const lineNumbersBlock = wrapper.querySelector('.jv-line-numbers pre');
            selectInfo.setAttribute('data-prev-val', selectInfo.value);

            selectInfo.addEventListener('change', (e) => {
                const val = e.target.value;
                if (val === 'LOAD_MORE') { openLangModal(selectInfo); return; }

                selectInfo.setAttribute('data-prev-val', val);
                const selectedOption = e.target.options[e.target.selectedIndex];
                const newHljsClass = selectedOption.getAttribute('data-hljs') || val;

                if (isPreview) {
                    const allRawCodesInPreview = Array.from(document.querySelectorAll('.messageEditor__containerPreview code.message__code, .messageEditor__containerPreview .code-jv'));
                    const currentIndex = allRawCodesInPreview.indexOf(codeElement);
                    if (currentIndex !== -1) {
                        previewState[currentIndex] = { piston: val, hljs: newHljsClass };
                    }
                }
                codeContentBlock.className = `hljs ${newHljsClass}`;
                try {
                    codeContentBlock.innerHTML = hljs.highlight(rawCode, { language: newHljsClass }).value;
                } catch(e) {
                    codeContentBlock.innerHTML = hljs.highlightAuto(rawCode).value;
                }
            });

            const copyBtn = wrapper.querySelector('.jv-copy-btn');
            const copyIcon = copyBtn.querySelector('.jv-icon');

            copyBtn.addEventListener('click', (e) => {
                e.preventDefault(); e.stopPropagation();
                navigator.clipboard.writeText(rawCode).then(() => {
                    copyIcon.className = 'jv-icon jv-icon-check';
                    copyBtn.style.color = "#98c379";
                    setTimeout(() => {
                        copyIcon.className = 'jv-icon jv-icon-copy';
                        copyBtn.style.color = "";
                    }, 2000);
                });
            });

            const runBtn = wrapper.querySelector('.jv-run-btn');
            const outputDiv = wrapper.querySelector('.jv-code-output');

            runBtn.addEventListener('click', (e) => {
                e.preventDefault(); e.stopPropagation();

                if (runBtn.getAttribute('data-running') === 'true') {
                    if (runBtn._currentRequest && typeof runBtn._currentRequest.abort === 'function') {
                        runBtn._currentRequest.abort();
                    }
                    resetRunButton(runBtn);
                    outputDiv.className = 'jv-code-output warning';
                    outputDiv.textContent = "Arrêt forcé par l'utilisateur.";
                    return;
                }
                runBtn._currentRequest = runCode(rawCode, selectInfo.value, outputDiv, runBtn);
            });

            if (targetToReplace.parentNode) {
                const removalObserver = new MutationObserver((mutations) => {
                    for (const mutation of mutations) {
                        if (mutation.removedNodes) {
                            mutation.removedNodes.forEach((removedNode) => {
                                if (removedNode === targetToReplace) {
                                    wrapper.remove();
                                    removalObserver.disconnect();
                                }
                            });
                        }
                    }
                });
                removalObserver.observe(targetToReplace.parentNode, { childList: true });
            }

            if (isPreview) {
                const observerConfig = { characterData: true, childList: true, subtree: true };
                const liveObserver = new MutationObserver((mutations) => {
                    // Si le wrapper n'est plus dans le DOM (supprimé par removalObserver), on arrête tout
                    if (!document.body.contains(wrapper)) {
                        liveObserver.disconnect();
                        return;
                    }

                    const newText = codeElement.textContent.replace(/^\s*\n/g, '').replace(/\n\s*$/g, '');
                    if (newText === rawCode) return;
                    rawCode = newText;
                    const newLineCount = rawCode.split(/\r\n|\r|\n/).length;
                    let newLineHtml = '';
                    for (let i = 1; i <= newLineCount; i++) newLineHtml += `${i}\n`;
                    lineNumbersBlock.textContent = newLineHtml;
                    const currentHljsClass = selectInfo.options[selectInfo.selectedIndex]?.getAttribute('data-hljs') || 'text';
                    let newHighlightedHtml;
                    try {
                        newHighlightedHtml = hljs.highlight(rawCode, { language: currentHljsClass }).value;
                    } catch (e) {
                        newHighlightedHtml = hljs.highlightAuto(rawCode).value;
                    }
                    codeContentBlock.innerHTML = newHighlightedHtml;
                });
                liveObserver.observe(codeElement, observerConfig);
            }

            targetToReplace.style.display = 'none';
            targetToReplace.parentNode.insertBefore(wrapper, targetToReplace);
        });
    }

    enhanceCodeBlocks();

    const observer = new MutationObserver((mutations) => {
        let shouldEnhanceGlobal = false;
        let previewUpdateDetected = false;

        for (const mutation of mutations) {
            if (mutation.addedNodes.length > 0) {
                const targetNode = mutation.target;
                const isPreview = targetNode.closest && targetNode.closest('.messageEditor__containerPreview');
                if (isPreview) {
                    previewUpdateDetected = true;
                } else {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === 1) {
                            if (node.querySelector && node.querySelector('.code-jv, .pre-jv')) shouldEnhanceGlobal = true;
                            if (node.matches && node.matches('.code-jv, .pre-jv')) shouldEnhanceGlobal = true;
                        }
                    });
                }
            }
        }

        if (previewUpdateDetected) {
            const previewContainer = document.querySelector('.messageEditor__containerPreview');
            if (previewContainer) enhanceCodeBlocks(previewContainer);
        } else if (shouldEnhanceGlobal) {
            enhanceCodeBlocks(document.body);
        }
    });

    const targetNode = document.getElementById('page-messages-forum') || document.body;
    observer.observe(targetNode, { childList: true, subtree: true });

})();