Rendered preview for HTML files shown in GitHub/GitLab repos (JS + UTF-8)
Fra
// ==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 });
})();