HTML Preview for GitHub/GitLab

Rendered preview for HTML files shown in GitHub/GitLab repos (JS + UTF-8) with auto-refresh

Versione datata 06/08/2026. Vedi la nuova versione l'ultima versione.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         HTML Preview for GitHub/GitLab
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Rendered preview for HTML files shown in GitHub/GitLab repos (JS + UTF-8) with auto-refresh
// @author       sanchomuzax
// @match        *://github.com/*/blob/*
// @match        *://gitlab.com/*/-/blob/*
// @match        *://glab.p24.hu/*/-/blob/*
// @grant        GM_xmlhttpRequest
// @grant        GM_openInTab
// @connect      *
// @run-at       document-idle
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    const REFRESH_MINUTES = 5;

    function isHtmlFile() {
        const cleanPath = location.pathname.split('?')[0];
        return /\.(html?|xhtml)$/i.test(cleanPath);
    }

    function getRawUrl() {
        const host = location.hostname;
        // GitHub -> use github.com /raw/ (auth via cookies, works for private repos too)
        if (host === 'github.com') {
            const m = location.pathname.match(/^\/([^/]+)\/([^/]+)\/blob\/(.+)$/);
            if (m) return `${location.origin}/${m[1]}/${m[2]}/raw/${m[3]}`;
            return null;
        }
        // GitLab (gitlab.com and self-hosted) -> /-/raw/ path
        if (location.pathname.includes('/-/blob/')) {
            return location.origin +
                   location.pathname.replace('/-/blob/', '/-/raw/') +
                   location.search;
        }
        return null;
    }

    function openPreview(rawUrl) {
        GM_xmlhttpRequest({
            method: 'GET',
            url: rawUrl,
            overrideMimeType: 'text/html; charset=utf-8',
            onload: function (res) {
                if (res.status >= 200 && res.status < 300) {
                    showPreview(res.responseText);
                } else {
                    alert('Failed to load HTML. HTTP ' + res.status);
                }
            },
            onerror: () => alert('Error while loading HTML.')
        });
    }

    // Open the fetched HTML in a NEW top-level tab via a blob: URL.
    // A new tab has its own (empty) CSP, so GitHub's CSP can't block it,
    // and the page's own JavaScript runs normally (needed for dashboards).
    function showPreview(rawHtml) {
        let html = rawHtml;
        if (!/<meta[^>]+charset/i.test(html)) {
            const metaTag = '<meta charset="utf-8">';
            if (/<head[^>]*>/i.test(html)) {
                html = html.replace(/<head[^>]*>/i, m => m + metaTag);
            } else if (/<html[^>]*>/i.test(html)) {
                html = html.replace(/<html[^>]*>/i, m => m + '<head>' + metaTag + '</head>');
            } else {
                html = metaTag + html;
            }
        }
        const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
        const blobUrl = URL.createObjectURL(blob);
        GM_openInTab(blobUrl, { active: true, setParent: true });
        setTimeout(() => URL.revokeObjectURL(blobUrl), 60000);
    }

    let countdownTimer = null;

    function addButton() {
        if (document.getElementById('tm-html-preview-btn')) return;
        if (!isHtmlFile()) return;
        const rawUrl = getRawUrl();
        if (!rawUrl) return;
        if (!document.body) return;

        // Container for the button + countdown
        const container = document.createElement('div');
        container.id = 'tm-html-preview-btn';
        Object.assign(container.style, {
            position: 'fixed', bottom: '20px', right: '20px', zIndex: '2147483646',
            display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: '6px',
            fontFamily: 'sans-serif'
        });

        const btn = document.createElement('button');
        btn.textContent = '👁 Preview HTML';
        Object.assign(btn.style, {
            background: '#1f75cb', color: '#fff', border: 'none',
            padding: '10px 16px', borderRadius: '6px', cursor: 'pointer',
            fontSize: '14px',
            boxShadow: '0 2px 8px rgba(0,0,0,0.3)'
        });
        btn.onclick = () => openPreview(rawUrl);

        const counter = document.createElement('div');
        counter.id = 'tm-html-preview-counter';
        Object.assign(counter.style, {
            background: 'rgba(0,0,0,0.65)', color: '#fff',
            padding: '4px 10px', borderRadius: '6px', fontSize: '12px'
        });

        container.appendChild(counter);
        container.appendChild(btn);
        document.body.appendChild(container);

        startCountdown(counter);
    }

    function startCountdown(counterEl) {
        if (countdownTimer) clearInterval(countdownTimer);
        let remaining = REFRESH_MINUTES * 60; // seconds

        function render() {
            const m = Math.floor(remaining / 60);
            const s = remaining % 60;
            counterEl.textContent = `⟳ refresh: ${m}:${String(s).padStart(2, '0')}`;
        }

        render();
        countdownTimer = setInterval(() => {
            remaining--;
            if (remaining <= 0) {
                clearInterval(countdownTimer);
                location.reload();
                return;
            }
            render();
        }, 1000);
    }

    // --- Robust initialization for GitHub's SPA navigation ---

    function ensureButton() {
        const exists = document.getElementById('tm-html-preview-btn');
        if (isHtmlFile()) {
            if (!exists) addButton();
        } else {
            if (exists) {
                exists.remove();
                if (countdownTimer) clearInterval(countdownTimer);
            }
        }
    }

    // Initial attempts (document-idle can still fire before GitHub finishes
    // rendering the file view, so we retry a few times).
    ensureButton();
    let tries = 0;
    const initInterval = setInterval(() => {
        ensureButton();
        tries++;
        if (document.getElementById('tm-html-preview-btn') || tries > 20) {
            clearInterval(initInterval);
        }
    }, 300);

    // GitHub fires a 'pjax:end' / 'turbo:load' event on SPA navigation.
    ['pjax:end', 'turbo:load', 'turbo:render', 'pageshow'].forEach(evt => {
        document.addEventListener(evt, () => setTimeout(ensureButton, 300));
        window.addEventListener(evt, () => setTimeout(ensureButton, 300));
    });

    // Fallback: watch for URL changes (SPA) and re-check.
    let lastUrl = location.href;
    new MutationObserver(() => {
        if (location.href !== lastUrl) {
            lastUrl = location.href;
            const old = document.getElementById('tm-html-preview-btn');
            if (old) old.remove();
            if (countdownTimer) clearInterval(countdownTimer);
            setTimeout(ensureButton, 500);
        }
    }).observe(document.documentElement, { childList: true, subtree: true });

})();