ArtiGist: Turn any GitHub Gist into an "artifact"

4 months ago 1
// ==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 }); })();
Read Entire Article