|
// ==UserScript== |
|
// @name ArtiGist |
|
// @version 0.5 |
|
// @description Launch single-page HTML apps from GitHub Gists. |
|
// @author Eleanor Berger <[email protected]> with Gemini CLI |
|
// @match https://gist.github.com/*/* |
|
// @match https://gist.githubusercontent.com/*/* |
|
// @grant GM_xmlhttpRequest |
|
// @grant GM_openInTab |
|
// ==/UserScript== |
|
|
|
/* |
|
* ArtiGist: A UserScript to launch single-page HTML apps from GitHub Gists. |
|
* |
|
* This script enhances GitHub Gists by adding a "Launch Page" button to any Gist |
|
* that contains an HTML file and is tagged with #artigist in its description. |
|
* Clicking the button opens the HTML file in a new tab, with a permissive |
|
* Content Security Policy that allows for fetching remote resources. |
|
* |
|
* To use this script, simply create a Gist with an HTML file and add |
|
* the #artigist tag to the Gist's description. The script will automatically |
|
* detect the HTML file and add the launch button. |
|
* |
|
* To test that the script is working and see an example of a valid artigist, |
|
* see https://gist.github.com/intellectronica/6d8ccc38f617643982619cc277af7bee |
|
*/ |
|
|
|
(function() { |
|
'use strict'; |
|
|
|
const GIST_TAG = '#artigist'; |
|
|
|
function isGistPage() { |
|
return window.location.href.includes('gist.github.com'); |
|
} |
|
|
|
function hasGistTag() { |
|
const descriptionElement = document.querySelector('div[itemprop="about"]'); |
|
return descriptionElement && descriptionElement.textContent.includes(GIST_TAG); |
|
} |
|
|
|
function findHtmlFile() { |
|
for (const link of document.querySelectorAll('a')) { |
|
const href = link.href; |
|
if (href.includes('/raw/') && href.endsWith('.html')) { |
|
return href; |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
function getRawHtmlContent(htmlFileUrl, callback) { |
|
const rawUrl = htmlFileUrl.replace('github.com', 'githubusercontent.com').replace('/blob/', '/'); |
|
GM_xmlhttpRequest({ |
|
method: "GET", |
|
url: rawUrl, |
|
onload: function(response) { |
|
if (response.status === 200) { |
|
callback(response.responseText); |
|
} |
|
}, |
|
onerror: function(error) { |
|
} |
|
}); |
|
} |
|
|
|
function launchHtml(htmlContent) { |
|
// Inject a more permissive Content Security Policy |
|
const csp = ` |
|
default-src 'self' data:; |
|
script-src 'unsafe-inline' 'unsafe-eval' https://cdn.tailwindcss.com; |
|
style-src 'unsafe-inline' https://fonts.googleapis.com; |
|
img-src 'self' data:; |
|
connect-src 'self' https://fonts.googleapis.com https://cdn.tailwindcss.com; |
|
`; |
|
const cspMetaTag = `<meta http-equiv="Content-Security-Policy" content="${csp.replace(/\s+/g, ' ').trim()}">`; |
|
|
|
// Find the head tag and insert the CSP meta tag |
|
const headEndIndex = htmlContent.indexOf('</head>'); |
|
if (headEndIndex !== -1) { |
|
htmlContent = htmlContent.substring(0, headEndIndex) + cspMetaTag + htmlContent.substring(headEndIndex); |
|
} else { |
|
// If no head tag, prepend to the beginning (less ideal but works) |
|
htmlContent = cspMetaTag + htmlContent; |
|
} |
|
|
|
GM_openInTab('data:text/html;charset=utf-8,' + encodeURIComponent(htmlContent), { active: true, insert: true }); |
|
} |
|
|
|
function createLaunchButton(htmlFileUrl) { |
|
const downloadZipButton = document.querySelector('a[data-ga-click*="download zip"]'); |
|
|
|
if (downloadZipButton) { |
|
const launchButton = document.createElement('a'); |
|
launchButton.className = 'btn btn-sm' + ' ml-2'; // Inherit basic button styling and add margin-left |
|
launchButton.textContent = 'Launch Page'; |
|
launchButton.href = '#'; |
|
launchButton.addEventListener('click', (event) => { |
|
event.preventDefault(); |
|
getRawHtmlContent(htmlFileUrl, launchHtml); |
|
}); |
|
|
|
// Insert the new button after the Download ZIP button |
|
downloadZipButton.parentNode.insertBefore(launchButton, downloadZipButton.nextSibling); |
|
} |
|
} |
|
|
|
function main() { |
|
if (isGistPage() && hasGistTag()) { |
|
const htmlFileUrl = findHtmlFile(); |
|
if (htmlFileUrl) { |
|
createLaunchButton(htmlFileUrl); |
|
} |
|
} |
|
} |
|
|
|
// Use a MutationObserver to wait for the page to load dynamically |
|
const observer = new MutationObserver(function(mutations, me) { |
|
const gistHeader = document.querySelector('.pagehead-actions'); |
|
if (gistHeader) { |
|
main(); |
|
me.disconnect(); // stop observing once the element is found |
|
} |
|
}); |
|
|
|
observer.observe(document, { |
|
childList: true, |
|
subtree: true |
|
}); |
|
})(); |