HTML Preview for GitHub/GitLab

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

Versione datata 24/07/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 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.4
// @description  Rendered preview for HTML files shown in GitHub/GitLab repos (JS + UTF-8)
// @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 cleanPath = location.pathname.split('?')[0];
    if (!/\.(html?|xhtml)$/i.test(cleanPath)) return;

    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);
    }

    function addButton() {
        if (document.getElementById('tm-html-preview-btn')) return;
        const rawUrl = getRawUrl();
        if (!rawUrl) return;
        const btn = document.createElement('button');
        btn.id = 'tm-html-preview-btn';
        btn.textContent = '👁 Preview HTML';
        Object.assign(btn.style, {
            position: 'fixed', bottom: '20px', right: '20px', zIndex: '2147483646',
            background: '#1f75cb', color: '#fff', border: 'none',
            padding: '10px 16px', borderRadius: '6px', cursor: 'pointer',
            fontFamily: 'sans-serif', fontSize: '14px',
            boxShadow: '0 2px 8px rgba(0,0,0,0.3)'
        });
        btn.onclick = () => openPreview(rawUrl);
        document.body.appendChild(btn);
    }

    addButton();

    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();
            setTimeout(addButton, 500);
        }
    }).observe(document.body, { childList: true, subtree: true });
})();