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