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