HTML Preview for GitHub/GitLab

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

2026/07/24のページです。最新版はこちら

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         HTML Preview for GitHub/GitLab
// @namespace    http://tampermonkey.net/
// @version      1.3
// @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
// @connect      *
// @run-at       document-idle
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    // Only handle HTML files
    const cleanPath = location.pathname.split('?')[0];
    if (!/\.(html?|xhtml)$/i.test(cleanPath)) return;

    // -----------------------------------------------------------------
    // Compute the "raw" URL depending on the platform
    // -----------------------------------------------------------------
    function getRawUrl() {
        const host = location.hostname;

        // GitHub -> raw.githubusercontent.com
        if (host === 'github.com') {
            const m = location.pathname.match(/^\/([^/]+)\/([^/]+)\/blob\/(.+)$/);
            if (m) return `https://raw.githubusercontent.com/${m[1]}/${m[2]}/${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;
    }

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

    function showOverlay(rawHtml, rawUrl) {
        // Inject charset meta if missing (otherwise the iframe defaults to Latin-1)
        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;
            }
        }

        // Overlay backdrop
        const overlay = document.createElement('div');
        Object.assign(overlay.style, {
            position: 'fixed', inset: '0', zIndex: '2147483647',
            background: 'rgba(0,0,0,0.6)', display: 'flex',
            flexDirection: 'column', padding: '2vh 2vw', boxSizing: 'border-box'
        });

        // Header bar
        const bar = document.createElement('div');
        Object.assign(bar.style, {
            display: 'flex', alignItems: 'center', gap: '10px',
            background: '#1f2328', color: '#fff', padding: '8px 12px',
            borderRadius: '6px 6px 0 0', fontFamily: 'sans-serif', fontSize: '14px'
        });
        bar.innerHTML = '<strong>HTML Preview</strong>';

        // JS toggle
        const jsLabel = document.createElement('label');
        Object.assign(jsLabel.style, { display: 'flex', alignItems: 'center', gap: '5px', cursor: 'pointer', fontSize: '13px' });
        const jsChk = document.createElement('input');
        jsChk.type = 'checkbox';
        jsChk.checked = true;                       // on by default (needed for dashboards)
        jsLabel.append(jsChk, document.createTextNode('Run JavaScript'));

        const spacer = document.createElement('span');
        spacer.style.flex = '1';

        const openTab = document.createElement('a');
        openTab.textContent = 'Open in new tab (raw)';
        openTab.href = rawUrl; openTab.target = '_blank';
        Object.assign(openTab.style, { color: '#58a6ff', textDecoration: 'none' });

        const closeBtn = document.createElement('button');
        closeBtn.textContent = '✕ Close';
        Object.assign(closeBtn.style, {
            background: '#da3633', color: '#fff', border: 'none',
            padding: '6px 12px', borderRadius: '4px', cursor: 'pointer'
        });
        closeBtn.onclick = () => { cleanup(); overlay.remove(); };

        bar.append(jsLabel, spacer, openTab, closeBtn);

        // Iframe container
        const iframeWrap = document.createElement('div');
        Object.assign(iframeWrap.style, { flex: '1', display: 'flex', minHeight: '0' });

        let blobUrl = null;
        function cleanup() { if (blobUrl) { URL.revokeObjectURL(blobUrl); blobUrl = null; } }

        function buildIframe() {
            cleanup();
            iframeWrap.innerHTML = '';
            const iframe = document.createElement('iframe');
            Object.assign(iframe.style, {
                flex: '1', width: '100%', border: 'none',
                background: '#fff', borderRadius: '0 0 6px 6px'
            });

            if (jsChk.checked) {
                // JS enabled: blob: URL on a separate origin -> allow-scripts is safe,
                // because the iframe cannot reach the parent page (no allow-same-origin).
                iframe.setAttribute('sandbox', 'allow-scripts allow-forms allow-popups allow-modals');
                const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
                blobUrl = URL.createObjectURL(blob);
                iframe.src = blobUrl;
            } else {
                // Static rendering only
                iframe.setAttribute('sandbox', 'allow-same-origin');
                iframe.srcdoc = html;
            }
            iframeWrap.appendChild(iframe);
        }

        jsChk.addEventListener('change', buildIframe);
        buildIframe();

        overlay.append(bar, iframeWrap);
        overlay.addEventListener('click', (e) => { if (e.target === overlay) { cleanup(); overlay.remove(); } });
        document.body.appendChild(overlay);
    }

    // -----------------------------------------------------------------
    // Create the button
    // -----------------------------------------------------------------
    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();

    // Watch for GitHub/GitLab SPA navigation
    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 });
})();