restructure Hunter Wednesday, March 18th, 2026 at 5:42 PM
3 on: 4 push: 5 branches: [ main ] - paths: [ 'images/**', 'resources/**' ] # Run when images or resources are added/changed 6 + paths: [ 'resources/**' ] # Run when resources (including images) are added/changed 7 workflow_dispatch: # Allow manual trigger 8 9 jobs: 24 python-version: '3.x' 25 26 - name: Generate resource manifest - run: python generate_resource_manifest.py 27 + run: python resources/scripts/generate_resource_manifest.py 28 29 - name: Check for changes 30 id: verify-changed-files 40 run: | 41 git config --local user.email "action@github.com" 42 git config --local user.name "GitHub Action" - git add resource-manifest.json 43 + git add resources/resource-manifest.json 44 git commit -m "Auto-update resource manifest [skip ci]" 45 git push
2 3 jen-only/ 4 -vendor/node_modules/ -vendor/package-lock.json 5 +resources/vendor/node_modules/ 6 +resources/vendor/package-lock.json
2 3 Craft handmade websites in their natural habitat (your web browser). 4 -<img src="images/pika_construction.gif"> 5 +<img src="resources/images/pika_construction.gif"> 6 7 Web Workshop was born as a teaching tool for web development classes, with the goal of making web publishing more accessible, especially to those who have never written code before. 8 9 By stripping away complexity and focusing on the basics, Web Workshop turns website building into something playful and toylike; a creative playground where you can experiment, break things, and watch your ideas materialize in real-time. 10 11 <a href="https://hunterirving.github.io/web_workshop/"> -<img src="readme_images/picnic.png" width="650"></a> 12 +<img src="resources/readme_images/picnic.png" width="650"></a> 13 <br><br> 14 15 Recently, Web Workshop has become an editor of choice for <a href="https://html.energy/html-day/2025/index.html">HTML Day</a>, an annual celebration of handmade websites and the creative web. 32 A library of stock images is included for your convenience. To browse them, type `<img src="?">` anywhere in the editor pane, then click to select an image from the resulting table. 33 34 <a href="https://hunterirving.github.io/web_workshop/"> -<img src="readme_images/stock_images.png" width="650"></a> 35 +<img src="resources/readme_images/stock_images.png" width="650"></a> 36 <br><br> 37 38 Alternatively, if you know the name of the image you'd like to use, you can add it to your project like so: `<img src="coffee.gif">`. 58 - to toggle <b>line numbers</b>, press <kbd>F1</kbd> (disabled by default) 59 - to toggle <b>line wrapping</b>, press <kbd>F2</kbd> (enabled by default) 60 -<img src="images/hint.gif" width=60px> 61 +<img src="resources/images/hint.gif" width=60px> 62 <i> 63 Wanna make websites on your phone? Try installing Web Workshop as a <a href="https://hunterirving.github.io/web_workshop/pages/pwa">Progressive Web App</a>!</i> 64 67 - [CodeMirror 6](https://codemirror.net/) 68 - [GitHub Dark Theme for CodeMirror](https://github.com/fsegurai/codemirror-themes) 69 -<img src="images/bright_idea.gif"> 70 +<img src="resources/images/bright_idea.gif"> 71 72 ## License 73 74 GPLv3 (see <a href="LICENSE">LICENSE</a> for details). 75 76 <br> -<img src="images/bigN.gif" width=110px> 77 +<img src="resources/images/bigN.gif" width=110px> 78 79 ## Contributing 80 81 Feel free to open issues or submit pull requests for improvements! 82 -<img src="images/gaia.gif" width=180px> 83 +<img src="resources/images/gaia.gif" width=180px>
-#!/usr/bin/env python3 -""" -Generate resource manifest for PWA caching -Scans the images/ and resources/ directories and creates a JSON manifest file -""" - -import os -import json -from pathlib import Path - -def generate_resource_manifest(): - """Generate a JSON manifest of all resources in the images/ and resources/ directories""" - - # Define supported image extensions - image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico', '.bmp'} - - # Get the directory where this script is located - script_dir = Path(__file__).parent - images_dir = script_dir / 'images' - resources_dir = script_dir / 'resources' - - # Collect all image files - image_files = [] - if images_dir.exists(): - for file_path in images_dir.iterdir(): - if file_path.is_file() and file_path.suffix.lower() in image_extensions: - # Create relative path from web root (GitHub Pages subdirectory) - relative_path = f"/web_workshop/images/{file_path.name}" - image_files.append(relative_path) - - # Collect ALL resource files (no extension filtering) - resource_files = [] - if resources_dir.exists(): - for file_path in resources_dir.rglob('*'): - if file_path.is_file(): - # Create relative path from web root (GitHub Pages subdirectory) - relative_path = f"/web_workshop/resources/{file_path.relative_to(resources_dir).as_posix()}" - resource_files.append(relative_path) - - # Sort for consistent output - image_files.sort() - resource_files.sort() - - # Create manifest object - manifest = { - "images": image_files, - "resources": resource_files, - "generated_at": "auto-generated by GitHub Actions" - } - - # Write manifest file - manifest_path = script_dir / 'resource-manifest.json' - with open(manifest_path, 'w', encoding='utf-8') as f: - json.dump(manifest, f, indent=2, sort_keys=True) - - print(f"Generated manifest with {len(image_files)} images and {len(resource_files)} resources:") - for img in image_files: - print(f" - {img}") - for res in resource_files: - print(f" - {res}") - - return manifest - -if __name__ == "__main__": - manifest = generate_resource_manifest() - print(f"\nManifest saved to: resource-manifest.json")
4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"> 6 <title>Web Workshop</title> - <link rel="icon" href="resources/construction.png"> 7 + <link rel="icon" href="resources/icons/construction.png"> 8 <link rel="manifest" href="manifest.json"> - <link rel="stylesheet" href="styles.css"> 9 + <link rel="stylesheet" href="resources/css/styles.css"> 10 <script type="module"> - import {EditorView, keymap, placeholder, lineNumbers, Decoration, EditorState, Compartment, defaultKeymap, indentWithTab, undo, redo, undoDepth, redoDepth, history, historyKeymap, closeBrackets, closeBracketsKeymap, html, githubDark, indentUnit, search, searchKeymap, closeSearchPanel, openSearchPanel} from "./codemirror-bundle.js" 11 + import {EditorView, keymap, placeholder, lineNumbers, Decoration, EditorState, Compartment, defaultKeymap, indentWithTab, undo, redo, undoDepth, redoDepth, history, historyKeymap, closeBrackets, closeBracketsKeymap, html, githubDark, indentUnit, search, searchKeymap, closeSearchPanel, openSearchPanel} from "./resources/js/codemirror-bundle.js" 12 window.CodeMirror = {EditorView, EditorState, Compartment, keymap, defaultKeymap, indentWithTab, html, githubDark, indentUnit, placeholder, undo, redo, undoDepth, redoDepth, history, historyKeymap, closeBrackets, closeBracketsKeymap, search, searchKeymap, closeSearchPanel, openSearchPanel, lineNumbers, Decoration}; 13 </script> 14 </head> 21 <iframe id="preview"></iframe> 22 </div> 23 - <script type="module" src="main.js"></script> 24 + <script type="module" src="resources/js/main.js"></script> 25 </body> 26 </html>
-let updateTimer; -let editorView; -const preview = document.getElementById('preview'); - -// Stock images loaded from manifest - can be referenced by bare filename -let stockImages = new Map(); // lowercase -> original filename - -// Load stock image list from manifest -const stockImagesReady = fetch('resource-manifest.json') - .then(r => r.json()) - .then(manifest => { - manifest.images.forEach(path => { - const filename = path.split('/').pop(); - stockImages.set(filename.toLowerCase(), filename); - }); - }) - .catch(() => console.log('Could not load resource manifest')); - -// Rewrite bare image filenames to use images/ prefix (case-insensitive, uses correct case) -function rewriteBareImageSrcs(html) { - // Rewrite <img src="filename.ext"> - html = html.replace(/(<img\s[^>]*\bsrc\s*=\s*["'])([^"'/:]+\.(gif|png|jpg|jpeg|svg|webp))(["'])/gi, - (match, before, filename, ext, after) => { - const original = stockImages.get(filename.toLowerCase()); - if (original) { - return before + 'images/' + original + after; - } - return match; - }); - // Rewrite url(filename.ext) in CSS - html = html.replace(/(url\(\s*["']?)([^"')/:]+\.(gif|png|jpg|jpeg|svg|webp))(["']?\s*\))/gi, - (match, before, filename, ext, after) => { - const original = stockImages.get(filename.toLowerCase()); - if (original) { - return before + 'images/' + original + after; - } - return match; - }); - return html; -} - -// Expand <img src="?"> tags into gallery table -function expandImagesTag(html) { - return html.replace(/<img\s+src\s*=\s*["']?\?["']?\s*\/?>/gi, () => { - const images = Array.from(stockImages.values()).sort(); - if (images.length === 0) return '<p>No images available</p>'; - const rows = images.map(filename => - `<tr class="stock-image-row" data-filename="${filename}" style="cursor:pointer;user-select:none;"><td>${filename}</td><td style="text-align:center;"><img src="images/${filename}" style="max-width:100%;height:auto;pointer-events:none;"></td></tr>` - ).join(''); - return `<table class="stock-image-table" border="1" cellpadding="8" cellspacing="0" style="max-width:100%;box-sizing:border-box;table-layout:fixed;"><colgroup><col style="width:50%"><col style="width:50%"></colgroup>${rows}</table>`; - }); -} -const editorPane = document.querySelector('.editor-pane'); -const previewPane = document.querySelector('.preview-pane'); -const storageKey = 'html-lab-content'; -let isFullscreen = false; -let showLineNumbers = false; -let enableLineWrapping = false; -let lineNumbersCompartment; -let lineWrappingCompartment; - -function toggleFullscreen() { - isFullscreen = !isFullscreen; - - if (isFullscreen) { - previewPane.classList.add('fullscreen'); - editorPane.classList.add('hidden'); - // Set iframe height to actual visible height (fixes iOS Safari toolbar issue) - preview.style.height = window.innerHeight + 'px'; - } else { - previewPane.classList.remove('fullscreen'); - editorPane.classList.remove('hidden'); - preview.style.height = ''; - } - - // Update button title in iframe - try { - const toggleButton = preview.contentDocument.getElementById('fullscreenToggle'); - if (toggleButton) { - toggleButton.title = isFullscreen ? 'Exit fullscreen' : 'Toggle fullscreen'; - } - } catch (e) { - // Ignore cross-origin errors - } -} - - -// Listen for messages from iframe -window.addEventListener('message', function(event) { - if (event.data === 'toggleFullscreen') { - toggleFullscreen(); - } -}); - -function extractTitleAndFavicon(htmlCode) { - const parser = new DOMParser(); - const doc = parser.parseFromString(htmlCode, 'text/html'); - - // Extract title - const titleElement = doc.querySelector('title'); - const title = titleElement ? titleElement.textContent.trim() : null; - - // Extract favicon - const faviconSelectors = [ - 'link[rel="icon"]', - 'link[rel="shortcut icon"]', - 'link[rel="apple-touch-icon"]', - 'link[rel="mask-icon"]' - ]; - - let favicon = null; - for (const selector of faviconSelectors) { - const faviconElement = doc.querySelector(selector); - if (faviconElement && faviconElement.href) { - favicon = faviconElement.href; - break; - } - } - - return { title, favicon }; -} - -function updateMainPageTitleAndFavicon(title, favicon) { - // Update title - if (title) { - document.title = title; - } else { - document.title = 'Web Workshop'; - } - - // Update favicon - let faviconLink = document.querySelector('link[rel="icon"]'); - if (!faviconLink) { - faviconLink = document.createElement('link'); - faviconLink.rel = 'icon'; - document.head.appendChild(faviconLink); - } - - if (favicon) { - faviconLink.href = favicon; - } else { - faviconLink.href = 'resources/construction.png'; - } -} - -function updatePreview() { - // Skip preview updates while mobile keyboard is open and editor is focused - // This prevents keyboard layer resets on Android - if (isMobileDevice() && isEditorFocused && document.body.classList.contains('mobile-keyboard-open')) { - return; - } - - const code = editorView.state.doc.toString(); - - // Extract and update title and favicon from user's HTML - const { title, favicon } = extractTitleAndFavicon(code); - updateMainPageTitleAndFavicon(title, favicon); - - // Store scroll position before updating - let scrollX = 0, scrollY = 0; - try { - if (preview.contentWindow?.scrollX !== undefined) { - scrollX = preview.contentWindow.scrollX; - scrollY = preview.contentWindow.scrollY; - } - } catch (e) { - // Ignore cross-origin errors - } - - // Use srcdoc to create a completely fresh document context - // Expand <images> tag and rewrite bare stock image filenames - const processedCode = expandImagesTag(rewriteBareImageSrcs(code.trim())) || '<!DOCTYPE html><html><head></head><body></body></html>'; - preview.srcdoc = processedCode; - - // Add our functionality after the iframe loads - const onLoad = () => { - try { - const doc = preview.contentDocument; - if (!doc) return; - - // Add CSS for overscroll and button - const style = doc.createElement('style'); - style.textContent = '* { overscroll-behavior: none !important; } .iframe-fullscreen-toggle { position: fixed; top: 5px; right: 5px; z-index: 10000; background: rgba(0, 0, 0, 0.2); color: white; border: none; border-radius: 4px; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background-color 0.2s; box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); -webkit-tap-highlight-color: transparent; outline: none; user-select: none; } .iframe-fullscreen-toggle svg { filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.3)); opacity: 0.8; transition: opacity 0.2s; } @media (hover: hover) and (pointer: fine) { .iframe-fullscreen-toggle:hover { background: rgba(0, 0, 0, 0.35); } .iframe-fullscreen-toggle:hover svg { opacity: 1; } }'; - doc.head.appendChild(style); - - // Create fullscreen button - const existingButton = doc.getElementById('fullscreenToggle'); - if (existingButton) existingButton.remove(); - - const button = doc.createElement('button'); - button.id = 'fullscreenToggle'; - button.className = 'iframe-fullscreen-toggle'; - button.title = 'Toggle fullscreen'; - - // Use inline SVG to avoid being affected by user's img styles - button.innerHTML = '<svg width="20" height="20" viewBox="0 0 14 14" fill="white"><path d="M 7,14 H 5 v 5 h 5 V 17 H 7 Z M 5,10 H 7 V 7 h 3 V 5 H 5 Z m 12,7 h -3 v 2 h 5 V 14 H 17 Z M 14,5 v 2 h 3 v 3 h 2 V 5 Z" transform="translate(-5,-5)"/></svg>'; - - button.addEventListener('click', function() { - parent.postMessage('toggleFullscreen', '*'); - }); - - if (doc.body) { - doc.body.appendChild(button); - } - - // Add click handlers for stock image table rows - const stockImageRows = doc.querySelectorAll('.stock-image-row'); - stockImageRows.forEach(row => { - row.addEventListener('click', () => { - const filename = row.getAttribute('data-filename'); - if (!filename) return; - - // Find and replace <img src="?"> in the editor - const editorContent = editorView.state.doc.toString(); - const imgPattern = /<img\s+src\s*=\s*["']?\?["']?\s*\/?>/i; - const match = editorContent.match(imgPattern); - - if (match) { - const start = editorContent.indexOf(match[0]); - const end = start + match[0].length; - const replacement = `<img src="${filename}">`; - - editorView.dispatch({ - changes: { from: start, to: end, insert: replacement } - }); - saveToStorage(); - updatePreview(); - } - }); - }); - - // Restore scroll position - setTimeout(() => { - try { - preview.contentWindow?.scrollTo(scrollX, scrollY); - } catch (e) { - // Ignore cross-origin errors - } - }, 10); - } catch (e) { - // Ignore cross-origin errors - } - - preview.removeEventListener('load', onLoad); - }; - preview.addEventListener('load', onLoad); -} - -function saveToStorage() { - try { - localStorage.setItem(storageKey, editorView.state.doc.toString()); - } catch (e) { - console.warn('Could not save to localStorage:', e); - } -} - -function loadFromStorage() { - try { - return localStorage.getItem(storageKey) || ''; - } catch (e) { - console.warn('Could not load from localStorage:', e); - return ''; - } -} - -function loadEditorSettings() { - try { - showLineNumbers = localStorage.getItem('editor-line-numbers') === 'true'; - enableLineWrapping = localStorage.getItem('editor-line-wrapping') !== 'false'; - } catch (e) { - console.warn('Could not load editor settings from localStorage:', e); - showLineNumbers = false; - enableLineWrapping = true; - } -} - -function saveEditorSetting(key, value) { - try { - localStorage.setItem(key, value.toString()); - } catch (e) { - console.warn('Could not save editor setting to localStorage:', e); - } -} - -function toggleLineNumbers() { - const {lineNumbers} = window.CodeMirror; - showLineNumbers = !showLineNumbers; - saveEditorSetting('editor-line-numbers', showLineNumbers); - - editorView.dispatch({ - effects: lineNumbersCompartment.reconfigure(showLineNumbers ? lineNumbers() : []) - }); -} - -function createLineWrappingExtension() { - const {EditorView, Decoration} = window.CodeMirror; - - return [ - EditorView.lineWrapping, - EditorView.decorations.of((view) => { - const decorations = []; - - for (let {from, to} of view.visibleRanges) { - for (let pos = from; pos <= to;) { - const line = view.state.doc.lineAt(pos); - const lineText = line.text; - - // Calculate indentation level (count leading whitespace) - let indentChars = 0; - for (let i = 0; i < lineText.length; i++) { - if (lineText[i] === '\t') { - indentChars += 2; // Convert tab to 2 spaces for calculation - } else if (lineText[i] === ' ') { - indentChars += 1; - } else { - break; - } - } - - // Apply hanging indent if line has indentation - if (indentChars > 0) { - const indentDecoration = Decoration.line({ - attributes: { - style: `text-indent: -${indentChars}ch; padding-left: calc(${indentChars}ch + 6px);` - } - }); - decorations.push(indentDecoration.range(line.from)); - } - - pos = line.to + 1; - } - } - - return decorations.length > 0 ? Decoration.set(decorations) : Decoration.none; - }), - ]; -} - -function toggleLineWrapping() { - enableLineWrapping = !enableLineWrapping; - saveEditorSetting('editor-line-wrapping', enableLineWrapping); - - const lineWrappingExtension = enableLineWrapping ? createLineWrappingExtension() : []; - - editorView.dispatch({ - effects: lineWrappingCompartment.reconfigure(lineWrappingExtension) - }); -} - -// File operations -window.saveFile = function() { - const blob = new Blob([editorView.state.doc.toString()], { type: 'text/html' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'index.html'; - a.click(); - URL.revokeObjectURL(url); -}; - -window.loadFile = function() { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = '.html,.htm'; - input.onchange = function(event) { - const file = event.target.files[0]; - if (!file) return; - - const reader = new FileReader(); - reader.onload = function(e) { - editorView.dispatch({ - changes: { from: 0, to: editorView.state.doc.length, insert: e.target.result } - }); - saveToStorage(); - updatePreview(); - }; - reader.readAsText(file); - }; - input.click(); -}; - -// Wait for CodeMirror to be available -function initializeCodeMirror() { - if (!window.CodeMirror) { - setTimeout(initializeCodeMirror, 100); - return; - } - - const {EditorView, EditorState, Compartment, keymap, defaultKeymap, indentWithTab, html, githubDark, indentUnit, placeholder, undo, redo, history, closeBrackets, search, searchKeymap, closeSearchPanel, openSearchPanel, lineNumbers} = window.CodeMirror; - - // Custom phrase overrides for CodeMirror UI (search panel) - const customPhrases = EditorState.phrases.of({ - "Find": "Find..." - }); - - // Load saved content and editor settings - const savedContent = loadFromStorage(); - loadEditorSettings(); - - // Create compartments for dynamic extensions - lineNumbersCompartment = new Compartment(); - lineWrappingCompartment = new Compartment(); - - const initialLineWrappingExtension = enableLineWrapping ? createLineWrappingExtension() : []; - - // Create CodeMirror editor - editorView = new EditorView({ - state: EditorState.create({ - doc: savedContent, - extensions: [ - customPhrases, - history(), - search(), - closeBrackets(), - keymap.of([ - {key: "Mod-z", run: undo}, - {key: "Mod-y", run: redo}, - {key: "Mod-Shift-z", run: redo}, - {key: "Mod-o", run: () => { window.loadFile(); return true; }}, - {key: "Mod-s", run: () => { window.saveFile(); return true; }}, - {key: "F1", run: () => { toggleLineNumbers(); return true; }}, - {key: "F2", run: () => { toggleLineWrapping(); return true; }}, - indentWithTab, - ...searchKeymap.filter(binding => binding.key !== "Mod-f"), - ...defaultKeymap - ]), - html(), - // Auto-close <style> and <script> tags (not handled by default html() extension) - // Also expand <!> into HTML boilerplate - EditorView.inputHandler.of((view, from, to, text) => { - if (text !== '>') return false; - const before = view.state.doc.sliceString(Math.max(0, from - 20), from); - // Check for <!> boilerplate trigger - if (before.endsWith('<!')) { - const boilerplate = `<!DOCTYPE html> -<html> -\t<head> -\t\t<style> -\t\t\t -\t\t</style> -\t</head> -\t<body> -\t\t -\t</body> -</html>`; - const startPos = from - 2; - const cursorPos = startPos + boilerplate.indexOf('<body>') + 9; - view.dispatch({ - changes: { from: startPos, to: from, insert: boilerplate }, - selection: { anchor: cursorPos } - }); - return true; - } - // Check for <style> or <script> - const match = before.match(/<(style|script)(\s[^>]*)?$/i); - if (!match) return false; - const tagName = match[1].toLowerCase(); - const closingTag = `</${tagName}>`; - view.dispatch({ - changes: { from, to, insert: '>' + closingTag }, - selection: { anchor: from + 1 } - }); - return true; - }), - githubDark, - indentUnit.of("\t"), - placeholder("Build something with HTML..."), - EditorView.updateListener.of((update) => { - if (update.docChanged) { - clearTimeout(updateTimer); - updateTimer = setTimeout(updatePreview, 600); - saveToStorage(); - } - }), - // Disable text correction and autocomplete - EditorView.contentAttributes.of({ - 'autocomplete': 'off', - 'autocorrect': 'off', - 'autocapitalize': 'off', - 'spellcheck': 'false' - }), - lineNumbersCompartment.of(showLineNumbers ? lineNumbers() : []), - lineWrappingCompartment.of(initialLineWrappingExtension) - ] - }), - parent: document.getElementById('editor') - }); - - // Initial render (wait for stock images manifest to load first) - stockImagesReady.then(() => updatePreview()); - - // Track editor focus and handle keyboard dismissal - editorView.contentDOM.addEventListener('focus', () => { isEditorFocused = true; }); - - editorView.contentDOM.addEventListener('blur', () => { - isEditorFocused = false; - - // If we are on mobile and the keyboard mode is active, - // exit immediately. Do not wait for visualViewport resize. - if (isMobileDevice()) { - exitMobileKeyboardMode(); - } - }); - - // Focus the editor - editorView.focus(); - - // Global keydown handler for Cmd+F toggle - document.addEventListener('keydown', function(e) { - if ((e.metaKey || e.ctrlKey) && e.key === 'f') { - e.preventDefault(); - closeSearchPanel(editorView) || openSearchPanel(editorView); - } - }); - - // Disable browser autocomplete on search panel inputs when they appear - const editorElement = document.getElementById('editor'); - const searchInputObserver = new MutationObserver((mutations) => { - for (const mutation of mutations) { - for (const node of mutation.addedNodes) { - if (node.nodeType === Node.ELEMENT_NODE) { - const searchInputs = node.querySelectorAll?.('.cm-search input[name="search"], .cm-search input[name="replace"]'); - searchInputs?.forEach(input => input.setAttribute('autocomplete', 'off')); - } - } - } - }); - searchInputObserver.observe(editorElement, { childList: true, subtree: true }); -} - -// Mobile keyboard detection -function isMobileDevice() { - return window.matchMedia("(pointer: coarse), (pointer: none)").matches; -} - -let initialViewportHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight; -let isEditorFocused = false; - -// Handle transition from fullscreen keyboard mode back to split view -function exitMobileKeyboardMode() { - if (!document.body.classList.contains('mobile-keyboard-open')) return; - - // Hide editor immediately to prevent visual stutter/jump - editorPane.style.opacity = '0'; - document.body.classList.remove('mobile-keyboard-open'); - - // Wait for layout to settle, then restore scroll and opacity - requestAnimationFrame(() => requestAnimationFrame(() => { - if (editorView) { - // Scroll cursor into view in the new 50% layout - const pos = editorView.state.selection.main.head; - const lineBlock = editorView.lineBlockAt(pos); - const targetScroll = lineBlock.top - (editorView.dom.clientHeight / 2); - editorView.scrollDOM.scrollTop = Math.max(0, targetScroll); - } - editorPane.style.opacity = ''; - - // Update preview with any changes made while keyboard was open - updatePreview(); - })); -} - -function updateViewportVariables() { - const vv = window.visualViewport; - if (vv) { - document.documentElement.style.setProperty('--visual-viewport-height', `${vv.height}px`); - document.documentElement.style.setProperty('--visual-viewport-offset-top', `${vv.offsetTop}px`); - } -} - -function handleViewportChange() { - if (!isMobileDevice()) return; - - const currentHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight; - const heightDifference = initialViewportHeight - currentHeight; - const isKeyboardOpen = heightDifference > 150; - - updateViewportVariables(); - - if (isKeyboardOpen && isEditorFocused) { - document.body.classList.add('mobile-keyboard-open'); - } else if (!isKeyboardOpen) { - // Fallback for keyboard dismissal that doesn't trigger blur (e.g. Android) - exitMobileKeyboardMode(); - } -} - -if (window.visualViewport) { - window.visualViewport.addEventListener('resize', handleViewportChange); - window.visualViewport.addEventListener('scroll', updateViewportVariables); -} else { - window.addEventListener('resize', handleViewportChange); -} - -// Initialize when page loads -initializeCodeMirror(); - -// Register service worker -if ('serviceWorker' in navigator) { - window.addEventListener('load', () => { - navigator.serviceWorker.register('./sw.js') - .then(registration => { - console.log('SW registered: ', registration); - }) - .catch(registrationError => { - console.log('SW registration failed: ', registrationError); - }); - }); -}
9 "orientation": "any", 10 "icons": [ 11 { - "src": "resources/construction_padded.png", 12 + "src": "resources/icons/construction_padded.png", 13 "sizes": "200x200", 14 "type": "image/png" 15 }, 16 { - "src": "resources/construction_padded.png", 17 + "src": "resources/icons/construction_padded.png", 18 "sizes": "192x192", 19 "type": "image/png" 20 }
36 </ul> 37 <center> 38 <h1 style="color:goldenrod">Try it Now!! Our yummy avocado coffee beverage</h1> - <img src="https://hunterirving.github.io/web_workshop/images/coffee.png" style="width:200px; height:200px;"> 39 + <img src="https://hunterirving.github.io/web_workshop/resources/images/coffee.png" style="width:200px; height:200px;"> 40 <i><p>"What the? Eww..." - Jen Lu</p></i> 41 <hr> 42 <p>Avocados in the courtroom?<br>This vid made us L-O-L<br>(Laff Out Loud)</p> 44 <br><br> 45 <hr> 46 <br> - <img src="https://hunterirving.github.io/web_workshop/images/sun.png" style="background-color:LemonChiffon; max-width:400px"> 47 + <img src="https://hunterirving.github.io/web_workshop/resources/images/sun.png" style="background-color:LemonChiffon; max-width:400px"> 48 <p>Anyway, thanks for checking out the site! Stay tuned for our family's prized avocado growing tips! 🌞</p> 49 </center> 50 </body>
29 Welcome to my World Wide Web Site 🌞 30 </h1> 31 <center> - <img src="https://hunterirving.github.io/web_workshop/images/welcome.gif" width=400px> 32 + <img src="https://hunterirving.github.io/web_workshop/resources/images/welcome.gif" width=400px> 33 <p> 34 brace yourself. you are in for a wild ride!! 35 </p> - <a href="https://hunterirving.com"><img src="https://hunterirving.github.io/web_workshop/images/windows.gif" style="padding:10px;"></a> 36 + <a href="https://hunterirving.com"><img src="https://hunterirving.github.io/web_workshop/resources/images/windows.gif" style="padding:10px;"></a> 37 <h3>enter by "clicking" on the window above....<br>if you <i><span style="color:red">dare</span></i>.....!</h3> - <img src="https://hunterirving.github.io/web_workshop/images/scared_mouse.gif" style="transform: scaleX(-1); margin-bottom:25px"><img src="https://hunterirving.github.io/web_workshop/images/mac.png" width=200px> 38 + <img src="https://hunterirving.github.io/web_workshop/resources/images/scared_mouse.gif" style="transform: scaleX(-1); margin-bottom:25px"><img src="https://hunterirving.github.io/web_workshop/resources/images/mac.png" width=200px> 39 </center> 40 <h1 class="heading"> 41 Here are a few of my favorite images... 🍀 42 </h1> 43 <center> - <img src="https://hunterirving.github.io/web_workshop/images/cd.gif" style="width:150px"> 44 + <img src="https://hunterirving.github.io/web_workshop/resources/images/cd.gif" style="width:150px"> 45 <br><br> - <img src="https://hunterirving.github.io/web_workshop/images/pika_construction.gif" style="width:50%; image-rendering: pixelated;"> 46 + <img src="https://hunterirving.github.io/web_workshop/resources/images/pika_construction.gif" style="width:50%; image-rendering: pixelated;"> 47 </center> 48 </body> 49 </html>
46 <b><span style="color: red; text-shadow: 3px 3px 5px white;">THE BEST</span> <span style="color:green; text-shadow: 3px 3px 5px white;">PIZZA</span> <span style="color:white">IN THE WORLD</span></b> 47 </p> 48 <hr> - <img src="https://hunterirving.github.io/web_workshop/images/smiling_pizza.gif"> - <img src="https://hunterirving.github.io/web_workshop/images/gaia.gif" width=200px> - <img src="https://hunterirving.github.io/web_workshop/images/smiling_pizza.gif"> 49 + <img src="https://hunterirving.github.io/web_workshop/resources/images/smiling_pizza.gif"> 50 + <img src="https://hunterirving.github.io/web_workshop/resources/images/gaia.gif" width=200px> 51 + <img src="https://hunterirving.github.io/web_workshop/resources/images/smiling_pizza.gif"> 52 <div class="block"> 53 <marquee scrollamount=20>AYY, I'M MAKIN' PIZZA HERE !</marquee> 54 </div> 92 But you better not disparage our pizza pies again, capeeshe??? 93 </p> 94 <br><br> - <center><img src="https://hunterirving.github.io/web_workshop/images/smiling_pizza.gif"></center><br><br> 95 + <center><img src="https://hunterirving.github.io/web_workshop/resources/images/smiling_pizza.gif"></center><br><br> 96 </body> 97 </html>
-{ - "generated_at": "auto-generated by GitHub Actions", - "images": [ - "/web_workshop/images/24.gif", - "/web_workshop/images/bang.gif", - "/web_workshop/images/bg_bricks.jpg", - "/web_workshop/images/bg_checker.gif", - "/web_workshop/images/bg_daisies.jpg", - "/web_workshop/images/bg_desert.gif", - "/web_workshop/images/bg_stars.gif", - "/web_workshop/images/bg_trippy.jpg", - "/web_workshop/images/bg_water.jpg", - "/web_workshop/images/bigN.gif", - "/web_workshop/images/birdbath.gif", - "/web_workshop/images/bizzy_wizzy.gif", - "/web_workshop/images/blahblah.gif", - "/web_workshop/images/bonk.gif", - "/web_workshop/images/bright_idea.gif", - "/web_workshop/images/butterflies.gif", - "/web_workshop/images/butterfly_blue.gif", - "/web_workshop/images/butterfly_monarch.gif", - "/web_workshop/images/cactus.gif", - "/web_workshop/images/cat.gif", - "/web_workshop/images/caterpillar.gif", - "/web_workshop/images/cd.gif", - "/web_workshop/images/christmas_garland.gif", - "/web_workshop/images/christmas_holly.gif", - "/web_workshop/images/christmas_wreath.gif", - "/web_workshop/images/coffee.gif", - "/web_workshop/images/coffee.png", - "/web_workshop/images/computer.gif", - "/web_workshop/images/construct.gif", - "/web_workshop/images/cow.gif", - "/web_workshop/images/daisies.gif", - "/web_workshop/images/dancingman.gif", - "/web_workshop/images/dancingwoman.gif", - "/web_workshop/images/devil.gif", - "/web_workshop/images/disco_ball.gif", - "/web_workshop/images/divider_blissey.gif", - "/web_workshop/images/divider_blue_flowers.gif", - "/web_workshop/images/divider_blue_flowers2.gif", - "/web_workshop/images/divider_rainbow.gif", - "/web_workshop/images/divider_rainbow_dots.gif", - "/web_workshop/images/divider_red_flowers.png", - "/web_workshop/images/divider_wildflowers.gif", - "/web_workshop/images/divider_yellow_flowers.gif", - "/web_workshop/images/dollhouse.gif", - "/web_workshop/images/doraemon.gif", - "/web_workshop/images/doraemon_stamp.gif", - "/web_workshop/images/email.gif", - "/web_workshop/images/email2.gif", - "/web_workshop/images/flowers.gif", - "/web_workshop/images/frog_car.gif", - "/web_workshop/images/furby1.gif", - "/web_workshop/images/furby2.gif", - "/web_workshop/images/furby3.gif", - "/web_workshop/images/furby4.gif", - "/web_workshop/images/furby5.gif", - "/web_workshop/images/furby6.gif", - "/web_workshop/images/furby7.gif", - "/web_workshop/images/furby8.gif", - "/web_workshop/images/furby9.gif", - "/web_workshop/images/furby_banner.gif", - "/web_workshop/images/furby_clouds.gif", - "/web_workshop/images/furby_field.jpg", - "/web_workshop/images/furby_pink.png", - "/web_workshop/images/furby_shocked.gif", - "/web_workshop/images/furby_weird.gif", - "/web_workshop/images/furbyland.gif", - "/web_workshop/images/gaia.gif", - "/web_workshop/images/garf.gif", - "/web_workshop/images/gc_icon.gif", - "/web_workshop/images/hacker_skull.gif", - "/web_workshop/images/hacking.gif", - "/web_workshop/images/hamster.gif", - "/web_workshop/images/hamtaro.gif", - "/web_workshop/images/hellokitty.gif", - "/web_workshop/images/hint.gif", - "/web_workshop/images/home.gif", - "/web_workshop/images/koosh.gif", - "/web_workshop/images/lil_buddha.gif", - "/web_workshop/images/lil_demon.gif", - "/web_workshop/images/linux_power.gif", - "/web_workshop/images/mac.png", - "/web_workshop/images/mailbox.gif", - "/web_workshop/images/mario64.gif", - "/web_workshop/images/modchips.gif", - "/web_workshop/images/neopet.gif", - "/web_workshop/images/ness.gif", - "/web_workshop/images/new_bunny.gif", - "/web_workshop/images/no_java.gif", - "/web_workshop/images/orbit.gif", - "/web_workshop/images/pika_construction.gif", - "/web_workshop/images/pink_clock.gif", - "/web_workshop/images/pochacco.gif", - "/web_workshop/images/providence.gif", - "/web_workshop/images/ratrace.gif", - "/web_workshop/images/ring.gif", - "/web_workshop/images/sailor_moon.gif", - "/web_workshop/images/samus.gif", - "/web_workshop/images/scachis.gif", - "/web_workshop/images/scared_mouse.gif", - "/web_workshop/images/smiley_bar.gif", - "/web_workshop/images/smiley_dancing.gif", - "/web_workshop/images/smiling_pizza.gif", - "/web_workshop/images/spacedog.gif", - "/web_workshop/images/spinninbrain.gif", - "/web_workshop/images/splat.png", - "/web_workshop/images/sun.png", - "/web_workshop/images/surfin.gif", - "/web_workshop/images/tamas.gif", - "/web_workshop/images/telephone.gif", - "/web_workshop/images/underconstruction.gif", - "/web_workshop/images/welcome.gif", - "/web_workshop/images/windows.gif", - "/web_workshop/images/wishingwell.gif", - "/web_workshop/images/wizard.gif", - "/web_workshop/images/ww_9volt.gif", - "/web_workshop/images/ww_ana.gif", - "/web_workshop/images/ww_crygor.gif", - "/web_workshop/images/ww_dribble.gif", - "/web_workshop/images/ww_jimmy.gif", - "/web_workshop/images/ww_kat.gif", - "/web_workshop/images/ww_mona.gif", - "/web_workshop/images/ww_orbulon.gif", - "/web_workshop/images/ww_spitz.gif", - "/web_workshop/images/ww_wario.gif", - "/web_workshop/images/y2k.gif" - ], - "resources": [ - "/web_workshop/resources/construction.png", - "/web_workshop/resources/construction_padded.png", - "/web_workshop/resources/fullscreen.svg" - ] -}
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
Binary files differ.
1 +let updateTimer; 2 +let editorView; 3 +const preview = document.getElementById('preview'); 4 + 5 +// Stock images loaded from manifest - can be referenced by bare filename 6 +let stockImages = new Map(); // lowercase -> original filename 7 + 8 +// Load stock image list from manifest 9 +const stockImagesReady = fetch('resources/resource-manifest.json') 10 + .then(r => r.json()) 11 + .then(manifest => { 12 + manifest.images.forEach(path => { 13 + const filename = path.split('/').pop(); 14 + stockImages.set(filename.toLowerCase(), filename); 15 + }); 16 + }) 17 + .catch(() => console.log('Could not load resource manifest')); 18 + 19 +// Rewrite bare image filenames to use resources/images/ prefix (case-insensitive, uses correct case) 20 +function rewriteBareImageSrcs(html) { 21 + // Rewrite <img src="filename.ext"> 22 + html = html.replace(/(<img\s[^>]*\bsrc\s*=\s*["'])([^"'/:]+\.(gif|png|jpg|jpeg|svg|webp))(["'])/gi, 23 + (match, before, filename, ext, after) => { 24 + const original = stockImages.get(filename.toLowerCase()); 25 + if (original) { 26 + return before + 'resources/images/' + original + after; 27 + } 28 + return match; 29 + }); 30 + // Rewrite url(filename.ext) in CSS 31 + html = html.replace(/(url\(\s*["']?)([^"')/:]+\.(gif|png|jpg|jpeg|svg|webp))(["']?\s*\))/gi, 32 + (match, before, filename, ext, after) => { 33 + const original = stockImages.get(filename.toLowerCase()); 34 + if (original) { 35 + return before + 'resources/images/' + original + after; 36 + } 37 + return match; 38 + }); 39 + return html; 40 +} 41 + 42 +// Expand <img src="?"> tags into gallery table 43 +function expandImagesTag(html) { 44 + return html.replace(/<img\s+src\s*=\s*["']?\?["']?\s*\/?>/gi, () => { 45 + const images = Array.from(stockImages.values()).sort(); 46 + if (images.length === 0) return '<p>No images available</p>'; 47 + const rows = images.map(filename => 48 + `<tr class="stock-image-row" data-filename="${filename}" style="cursor:pointer;user-select:none;"><td>${filename}</td><td style="text-align:center;"><img src="resources/images/${filename}" style="max-width:100%;height:auto;pointer-events:none;"></td></tr>` 49 + ).join(''); 50 + return `<table class="stock-image-table" border="1" cellpadding="8" cellspacing="0" style="max-width:100%;box-sizing:border-box;table-layout:fixed;"><colgroup><col style="width:50%"><col style="width:50%"></colgroup>${rows}</table>`; 51 + }); 52 +} 53 +const editorPane = document.querySelector('.editor-pane'); 54 +const previewPane = document.querySelector('.preview-pane'); 55 +const storageKey = 'html-lab-content'; 56 +let isFullscreen = false; 57 +let showLineNumbers = false; 58 +let enableLineWrapping = false; 59 +let lineNumbersCompartment; 60 +let lineWrappingCompartment; 61 + 62 +function toggleFullscreen() { 63 + isFullscreen = !isFullscreen; 64 + 65 + if (isFullscreen) { 66 + previewPane.classList.add('fullscreen'); 67 + editorPane.classList.add('hidden'); 68 + // Set iframe height to actual visible height (fixes iOS Safari toolbar issue) 69 + preview.style.height = window.innerHeight + 'px'; 70 + } else { 71 + previewPane.classList.remove('fullscreen'); 72 + editorPane.classList.remove('hidden'); 73 + preview.style.height = ''; 74 + } 75 + 76 + // Update button title in iframe 77 + try { 78 + const toggleButton = preview.contentDocument.getElementById('fullscreenToggle'); 79 + if (toggleButton) { 80 + toggleButton.title = isFullscreen ? 'Exit fullscreen' : 'Toggle fullscreen'; 81 + } 82 + } catch (e) { 83 + // Ignore cross-origin errors 84 + } 85 +} 86 + 87 + 88 +// Listen for messages from iframe 89 +window.addEventListener('message', function(event) { 90 + if (event.data === 'toggleFullscreen') { 91 + toggleFullscreen(); 92 + } 93 +}); 94 + 95 +function extractTitleAndFavicon(htmlCode) { 96 + const parser = new DOMParser(); 97 + const doc = parser.parseFromString(htmlCode, 'text/html'); 98 + 99 + // Extract title 100 + const titleElement = doc.querySelector('title'); 101 + const title = titleElement ? titleElement.textContent.trim() : null; 102 + 103 + // Extract favicon 104 + const faviconSelectors = [ 105 + 'link[rel="icon"]', 106 + 'link[rel="shortcut icon"]', 107 + 'link[rel="apple-touch-icon"]', 108 + 'link[rel="mask-icon"]' 109 + ]; 110 + 111 + let favicon = null; 112 + for (const selector of faviconSelectors) { 113 + const faviconElement = doc.querySelector(selector); 114 + if (faviconElement && faviconElement.href) { 115 + favicon = faviconElement.href; 116 + break; 117 + } 118 + } 119 + 120 + return { title, favicon }; 121 +} 122 + 123 +function updateMainPageTitleAndFavicon(title, favicon) { 124 + // Update title 125 + if (title) { 126 + document.title = title; 127 + } else { 128 + document.title = 'Web Workshop'; 129 + } 130 + 131 + // Update favicon 132 + let faviconLink = document.querySelector('link[rel="icon"]'); 133 + if (!faviconLink) { 134 + faviconLink = document.createElement('link'); 135 + faviconLink.rel = 'icon'; 136 + document.head.appendChild(faviconLink); 137 + } 138 + 139 + if (favicon) { 140 + faviconLink.href = favicon; 141 + } else { 142 + faviconLink.href = 'resources/icons/construction.png'; 143 + } 144 +} 145 + 146 +function updatePreview() { 147 + // Skip preview updates while mobile keyboard is open and editor is focused 148 + // This prevents keyboard layer resets on Android 149 + if (isMobileDevice() && isEditorFocused && document.body.classList.contains('mobile-keyboard-open')) { 150 + return; 151 + } 152 + 153 + const code = editorView.state.doc.toString(); 154 + 155 + // Extract and update title and favicon from user's HTML 156 + const { title, favicon } = extractTitleAndFavicon(code); 157 + updateMainPageTitleAndFavicon(title, favicon); 158 + 159 + // Store scroll position before updating 160 + let scrollX = 0, scrollY = 0; 161 + try { 162 + if (preview.contentWindow?.scrollX !== undefined) { 163 + scrollX = preview.contentWindow.scrollX; 164 + scrollY = preview.contentWindow.scrollY; 165 + } 166 + } catch (e) { 167 + // Ignore cross-origin errors 168 + } 169 + 170 + // Use srcdoc to create a completely fresh document context 171 + // Expand <images> tag and rewrite bare stock image filenames 172 + const processedCode = expandImagesTag(rewriteBareImageSrcs(code.trim())) || '<!DOCTYPE html><html><head></head><body></body></html>'; 173 + preview.srcdoc = processedCode; 174 + 175 + // Add our functionality after the iframe loads 176 + const onLoad = () => { 177 + try { 178 + const doc = preview.contentDocument; 179 + if (!doc) return; 180 + 181 + // Add CSS for overscroll and button 182 + const style = doc.createElement('style'); 183 + style.textContent = '* { overscroll-behavior: none !important; } .iframe-fullscreen-toggle { position: fixed; top: 5px; right: 5px; z-index: 10000; background: rgba(0, 0, 0, 0.2); color: white; border: none; border-radius: 4px; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background-color 0.2s; box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); -webkit-tap-highlight-color: transparent; outline: none; user-select: none; } .iframe-fullscreen-toggle svg { filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.3)); opacity: 0.8; transition: opacity 0.2s; } @media (hover: hover) and (pointer: fine) { .iframe-fullscreen-toggle:hover { background: rgba(0, 0, 0, 0.35); } .iframe-fullscreen-toggle:hover svg { opacity: 1; } }'; 184 + doc.head.appendChild(style); 185 + 186 + // Create fullscreen button 187 + const existingButton = doc.getElementById('fullscreenToggle'); 188 + if (existingButton) existingButton.remove(); 189 + 190 + const button = doc.createElement('button'); 191 + button.id = 'fullscreenToggle'; 192 + button.className = 'iframe-fullscreen-toggle'; 193 + button.title = 'Toggle fullscreen'; 194 + 195 + // Use inline SVG to avoid being affected by user's img styles 196 + button.innerHTML = '<svg width="20" height="20" viewBox="0 0 14 14" fill="white"><path d="M 7,14 H 5 v 5 h 5 V 17 H 7 Z M 5,10 H 7 V 7 h 3 V 5 H 5 Z m 12,7 h -3 v 2 h 5 V 14 H 17 Z M 14,5 v 2 h 3 v 3 h 2 V 5 Z" transform="translate(-5,-5)"/></svg>'; 197 + 198 + button.addEventListener('click', function() { 199 + parent.postMessage('toggleFullscreen', '*'); 200 + }); 201 + 202 + if (doc.body) { 203 + doc.body.appendChild(button); 204 + } 205 + 206 + // Add click handlers for stock image table rows 207 + const stockImageRows = doc.querySelectorAll('.stock-image-row'); 208 + stockImageRows.forEach(row => { 209 + row.addEventListener('click', () => { 210 + const filename = row.getAttribute('data-filename'); 211 + if (!filename) return; 212 + 213 + // Find and replace <img src="?"> in the editor 214 + const editorContent = editorView.state.doc.toString(); 215 + const imgPattern = /<img\s+src\s*=\s*["']?\?["']?\s*\/?>/i; 216 + const match = editorContent.match(imgPattern); 217 + 218 + if (match) { 219 + const start = editorContent.indexOf(match[0]); 220 + const end = start + match[0].length; 221 + const replacement = `<img src="${filename}">`; 222 + 223 + editorView.dispatch({ 224 + changes: { from: start, to: end, insert: replacement } 225 + }); 226 + saveToStorage(); 227 + updatePreview(); 228 + } 229 + }); 230 + }); 231 + 232 + // Restore scroll position 233 + setTimeout(() => { 234 + try { 235 + preview.contentWindow?.scrollTo(scrollX, scrollY); 236 + } catch (e) { 237 + // Ignore cross-origin errors 238 + } 239 + }, 10); 240 + } catch (e) { 241 + // Ignore cross-origin errors 242 + } 243 + 244 + preview.removeEventListener('load', onLoad); 245 + }; 246 + preview.addEventListener('load', onLoad); 247 +} 248 + 249 +function saveToStorage() { 250 + try { 251 + localStorage.setItem(storageKey, editorView.state.doc.toString()); 252 + } catch (e) { 253 + console.warn('Could not save to localStorage:', e); 254 + } 255 +} 256 + 257 +function loadFromStorage() { 258 + try { 259 + return localStorage.getItem(storageKey) || ''; 260 + } catch (e) { 261 + console.warn('Could not load from localStorage:', e); 262 + return ''; 263 + } 264 +} 265 + 266 +function loadEditorSettings() { 267 + try { 268 + showLineNumbers = localStorage.getItem('editor-line-numbers') === 'true'; 269 + enableLineWrapping = localStorage.getItem('editor-line-wrapping') !== 'false'; 270 + } catch (e) { 271 + console.warn('Could not load editor settings from localStorage:', e); 272 + showLineNumbers = false; 273 + enableLineWrapping = true; 274 + } 275 +} 276 + 277 +function saveEditorSetting(key, value) { 278 + try { 279 + localStorage.setItem(key, value.toString()); 280 + } catch (e) { 281 + console.warn('Could not save editor setting to localStorage:', e); 282 + } 283 +} 284 + 285 +function toggleLineNumbers() { 286 + const {lineNumbers} = window.CodeMirror; 287 + showLineNumbers = !showLineNumbers; 288 + saveEditorSetting('editor-line-numbers', showLineNumbers); 289 + 290 + editorView.dispatch({ 291 + effects: lineNumbersCompartment.reconfigure(showLineNumbers ? lineNumbers() : []) 292 + }); 293 +} 294 + 295 +function createLineWrappingExtension() { 296 + const {EditorView, Decoration} = window.CodeMirror; 297 + 298 + return [ 299 + EditorView.lineWrapping, 300 + EditorView.decorations.of((view) => { 301 + const decorations = []; 302 + 303 + for (let {from, to} of view.visibleRanges) { 304 + for (let pos = from; pos <= to;) { 305 + const line = view.state.doc.lineAt(pos); 306 + const lineText = line.text; 307 + 308 + // Calculate indentation level (count leading whitespace) 309 + let indentChars = 0; 310 + for (let i = 0; i < lineText.length; i++) { 311 + if (lineText[i] === '\t') { 312 + indentChars += 2; // Convert tab to 2 spaces for calculation 313 + } else if (lineText[i] === ' ') { 314 + indentChars += 1; 315 + } else { 316 + break; 317 + } 318 + } 319 + 320 + // Apply hanging indent if line has indentation 321 + if (indentChars > 0) { 322 + const indentDecoration = Decoration.line({ 323 + attributes: { 324 + style: `text-indent: -${indentChars}ch; padding-left: calc(${indentChars}ch + 6px);` 325 + } 326 + }); 327 + decorations.push(indentDecoration.range(line.from)); 328 + } 329 + 330 + pos = line.to + 1; 331 + } 332 + } 333 + 334 + return decorations.length > 0 ? Decoration.set(decorations) : Decoration.none; 335 + }), 336 + ]; 337 +} 338 + 339 +function toggleLineWrapping() { 340 + enableLineWrapping = !enableLineWrapping; 341 + saveEditorSetting('editor-line-wrapping', enableLineWrapping); 342 + 343 + const lineWrappingExtension = enableLineWrapping ? createLineWrappingExtension() : []; 344 + 345 + editorView.dispatch({ 346 + effects: lineWrappingCompartment.reconfigure(lineWrappingExtension) 347 + }); 348 +} 349 + 350 +// File operations 351 +window.saveFile = function() { 352 + const blob = new Blob([editorView.state.doc.toString()], { type: 'text/html' }); 353 + const url = URL.createObjectURL(blob); 354 + const a = document.createElement('a'); 355 + a.href = url; 356 + a.download = 'index.html'; 357 + a.click(); 358 + URL.revokeObjectURL(url); 359 +}; 360 + 361 +window.loadFile = function() { 362 + const input = document.createElement('input'); 363 + input.type = 'file'; 364 + input.accept = '.html,.htm'; 365 + input.onchange = function(event) { 366 + const file = event.target.files[0]; 367 + if (!file) return; 368 + 369 + const reader = new FileReader(); 370 + reader.onload = function(e) { 371 + editorView.dispatch({ 372 + changes: { from: 0, to: editorView.state.doc.length, insert: e.target.result } 373 + }); 374 + saveToStorage(); 375 + updatePreview(); 376 + }; 377 + reader.readAsText(file); 378 + }; 379 + input.click(); 380 +}; 381 + 382 +// Wait for CodeMirror to be available 383 +function initializeCodeMirror() { 384 + if (!window.CodeMirror) { 385 + setTimeout(initializeCodeMirror, 100); 386 + return; 387 + } 388 + 389 + const {EditorView, EditorState, Compartment, keymap, defaultKeymap, indentWithTab, html, githubDark, indentUnit, placeholder, undo, redo, history, closeBrackets, search, searchKeymap, closeSearchPanel, openSearchPanel, lineNumbers} = window.CodeMirror; 390 + 391 + // Custom phrase overrides for CodeMirror UI (search panel) 392 + const customPhrases = EditorState.phrases.of({ 393 + "Find": "Find..." 394 + }); 395 + 396 + // Load saved content and editor settings 397 + const savedContent = loadFromStorage(); 398 + loadEditorSettings(); 399 + 400 + // Create compartments for dynamic extensions 401 + lineNumbersCompartment = new Compartment(); 402 + lineWrappingCompartment = new Compartment(); 403 + 404 + const initialLineWrappingExtension = enableLineWrapping ? createLineWrappingExtension() : []; 405 + 406 + // Create CodeMirror editor 407 + editorView = new EditorView({ 408 + state: EditorState.create({ 409 + doc: savedContent, 410 + extensions: [ 411 + customPhrases, 412 + history(), 413 + search(), 414 + closeBrackets(), 415 + keymap.of([ 416 + {key: "Mod-z", run: undo}, 417 + {key: "Mod-y", run: redo}, 418 + {key: "Mod-Shift-z", run: redo}, 419 + {key: "Mod-o", run: () => { window.loadFile(); return true; }}, 420 + {key: "Mod-s", run: () => { window.saveFile(); return true; }}, 421 + {key: "F1", run: () => { toggleLineNumbers(); return true; }}, 422 + {key: "F2", run: () => { toggleLineWrapping(); return true; }}, 423 + indentWithTab, 424 + ...searchKeymap.filter(binding => binding.key !== "Mod-f"), 425 + ...defaultKeymap 426 + ]), 427 + html(), 428 + // Auto-close <style> and <script> tags (not handled by default html() extension) 429 + // Also expand <!> into HTML boilerplate 430 + EditorView.inputHandler.of((view, from, to, text) => { 431 + if (text !== '>') return false; 432 + const before = view.state.doc.sliceString(Math.max(0, from - 20), from); 433 + // Check for <!> boilerplate trigger 434 + if (before.endsWith('<!')) { 435 + const boilerplate = `<!DOCTYPE html> 436 +<html> 437 +\t<head> 438 +\t\t<style> 439 +\t\t\t 440 +\t\t</style> 441 +\t</head> 442 +\t<body> 443 +\t\t 444 +\t</body> 445 +</html>`; 446 + const startPos = from - 2; 447 + const cursorPos = startPos + boilerplate.indexOf('<body>') + 9; 448 + view.dispatch({ 449 + changes: { from: startPos, to: from, insert: boilerplate }, 450 + selection: { anchor: cursorPos } 451 + }); 452 + return true; 453 + } 454 + // Check for <style> or <script> 455 + const match = before.match(/<(style|script)(\s[^>]*)?$/i); 456 + if (!match) return false; 457 + const tagName = match[1].toLowerCase(); 458 + const closingTag = `</${tagName}>`; 459 + view.dispatch({ 460 + changes: { from, to, insert: '>' + closingTag }, 461 + selection: { anchor: from + 1 } 462 + }); 463 + return true; 464 + }), 465 + githubDark, 466 + indentUnit.of("\t"), 467 + placeholder("Build something with HTML..."), 468 + EditorView.updateListener.of((update) => { 469 + if (update.docChanged) { 470 + clearTimeout(updateTimer); 471 + updateTimer = setTimeout(updatePreview, 600); 472 + saveToStorage(); 473 + } 474 + }), 475 + // Disable text correction and autocomplete 476 + EditorView.contentAttributes.of({ 477 + 'autocomplete': 'off', 478 + 'autocorrect': 'off', 479 + 'autocapitalize': 'off', 480 + 'spellcheck': 'false' 481 + }), 482 + lineNumbersCompartment.of(showLineNumbers ? lineNumbers() : []), 483 + lineWrappingCompartment.of(initialLineWrappingExtension) 484 + ] 485 + }), 486 + parent: document.getElementById('editor') 487 + }); 488 + 489 + // Initial render (wait for stock images manifest to load first) 490 + stockImagesReady.then(() => updatePreview()); 491 + 492 + // Track editor focus and handle keyboard dismissal 493 + editorView.contentDOM.addEventListener('focus', () => { isEditorFocused = true; }); 494 + 495 + editorView.contentDOM.addEventListener('blur', () => { 496 + isEditorFocused = false; 497 + 498 + // If we are on mobile and the keyboard mode is active, 499 + // exit immediately. Do not wait for visualViewport resize. 500 + if (isMobileDevice()) { 501 + exitMobileKeyboardMode(); 502 + } 503 + }); 504 + 505 + // Focus the editor 506 + editorView.focus(); 507 + 508 + // Global keydown handler for Cmd+F toggle 509 + document.addEventListener('keydown', function(e) { 510 + if ((e.metaKey || e.ctrlKey) && e.key === 'f') { 511 + e.preventDefault(); 512 + closeSearchPanel(editorView) || openSearchPanel(editorView); 513 + } 514 + }); 515 + 516 + // Disable browser autocomplete on search panel inputs when they appear 517 + const editorElement = document.getElementById('editor'); 518 + const searchInputObserver = new MutationObserver((mutations) => { 519 + for (const mutation of mutations) { 520 + for (const node of mutation.addedNodes) { 521 + if (node.nodeType === Node.ELEMENT_NODE) { 522 + const searchInputs = node.querySelectorAll?.('.cm-search input[name="search"], .cm-search input[name="replace"]'); 523 + searchInputs?.forEach(input => input.setAttribute('autocomplete', 'off')); 524 + } 525 + } 526 + } 527 + }); 528 + searchInputObserver.observe(editorElement, { childList: true, subtree: true }); 529 +} 530 + 531 +// Mobile keyboard detection 532 +function isMobileDevice() { 533 + return window.matchMedia("(pointer: coarse), (pointer: none)").matches; 534 +} 535 + 536 +let initialViewportHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight; 537 +let isEditorFocused = false; 538 + 539 +// Handle transition from fullscreen keyboard mode back to split view 540 +function exitMobileKeyboardMode() { 541 + if (!document.body.classList.contains('mobile-keyboard-open')) return; 542 + 543 + // Hide editor immediately to prevent visual stutter/jump 544 + editorPane.style.opacity = '0'; 545 + document.body.classList.remove('mobile-keyboard-open'); 546 + 547 + // Wait for layout to settle, then restore scroll and opacity 548 + requestAnimationFrame(() => requestAnimationFrame(() => { 549 + if (editorView) { 550 + // Scroll cursor into view in the new 50% layout 551 + const pos = editorView.state.selection.main.head; 552 + const lineBlock = editorView.lineBlockAt(pos); 553 + const targetScroll = lineBlock.top - (editorView.dom.clientHeight / 2); 554 + editorView.scrollDOM.scrollTop = Math.max(0, targetScroll); 555 + } 556 + editorPane.style.opacity = ''; 557 + 558 + // Update preview with any changes made while keyboard was open 559 + updatePreview(); 560 + })); 561 +} 562 + 563 +function updateViewportVariables() { 564 + const vv = window.visualViewport; 565 + if (vv) { 566 + document.documentElement.style.setProperty('--visual-viewport-height', `${vv.height}px`); 567 + document.documentElement.style.setProperty('--visual-viewport-offset-top', `${vv.offsetTop}px`); 568 + } 569 +} 570 + 571 +function handleViewportChange() { 572 + if (!isMobileDevice()) return; 573 + 574 + const currentHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight; 575 + const heightDifference = initialViewportHeight - currentHeight; 576 + const isKeyboardOpen = heightDifference > 150; 577 + 578 + updateViewportVariables(); 579 + 580 + if (isKeyboardOpen && isEditorFocused) { 581 + document.body.classList.add('mobile-keyboard-open'); 582 + } else if (!isKeyboardOpen) { 583 + // Fallback for keyboard dismissal that doesn't trigger blur (e.g. Android) 584 + exitMobileKeyboardMode(); 585 + } 586 +} 587 + 588 +if (window.visualViewport) { 589 + window.visualViewport.addEventListener('resize', handleViewportChange); 590 + window.visualViewport.addEventListener('scroll', updateViewportVariables); 591 +} else { 592 + window.addEventListener('resize', handleViewportChange); 593 +} 594 + 595 +// Initialize when page loads 596 +initializeCodeMirror(); 597 + 598 +// Register service worker 599 +if ('serviceWorker' in navigator) { 600 + window.addEventListener('load', () => { 601 + navigator.serviceWorker.register('./sw.js') 602 + .then(registration => { 603 + console.log('SW registered: ', registration); 604 + }) 605 + .catch(registrationError => { 606 + console.log('SW registration failed: ', registrationError); 607 + }); 608 + }); 609 +}
Binary files differ.
Binary files differ.
1 +{ 2 + "generated_at": "auto-generated by GitHub Actions", 3 + "images": [ 4 + "/web_workshop/resources/images/24.gif", 5 + "/web_workshop/resources/images/bang.gif", 6 + "/web_workshop/resources/images/bg_bricks.jpg", 7 + "/web_workshop/resources/images/bg_checker.gif", 8 + "/web_workshop/resources/images/bg_daisies.jpg", 9 + "/web_workshop/resources/images/bg_desert.gif", 10 + "/web_workshop/resources/images/bg_stars.gif", 11 + "/web_workshop/resources/images/bg_trippy.jpg", 12 + "/web_workshop/resources/images/bg_water.jpg", 13 + "/web_workshop/resources/images/bigN.gif", 14 + "/web_workshop/resources/images/birdbath.gif", 15 + "/web_workshop/resources/images/bizzy_wizzy.gif", 16 + "/web_workshop/resources/images/blahblah.gif", 17 + "/web_workshop/resources/images/bonk.gif", 18 + "/web_workshop/resources/images/bright_idea.gif", 19 + "/web_workshop/resources/images/butterflies.gif", 20 + "/web_workshop/resources/images/butterfly_blue.gif", 21 + "/web_workshop/resources/images/butterfly_monarch.gif", 22 + "/web_workshop/resources/images/cactus.gif", 23 + "/web_workshop/resources/images/cat.gif", 24 + "/web_workshop/resources/images/caterpillar.gif", 25 + "/web_workshop/resources/images/cd.gif", 26 + "/web_workshop/resources/images/christmas_garland.gif", 27 + "/web_workshop/resources/images/christmas_holly.gif", 28 + "/web_workshop/resources/images/christmas_wreath.gif", 29 + "/web_workshop/resources/images/coffee.gif", 30 + "/web_workshop/resources/images/coffee.png", 31 + "/web_workshop/resources/images/computer.gif", 32 + "/web_workshop/resources/images/construct.gif", 33 + "/web_workshop/resources/images/cow.gif", 34 + "/web_workshop/resources/images/daisies.gif", 35 + "/web_workshop/resources/images/dancingman.gif", 36 + "/web_workshop/resources/images/dancingwoman.gif", 37 + "/web_workshop/resources/images/devil.gif", 38 + "/web_workshop/resources/images/disco_ball.gif", 39 + "/web_workshop/resources/images/divider_blissey.gif", 40 + "/web_workshop/resources/images/divider_blue_flowers.gif", 41 + "/web_workshop/resources/images/divider_blue_flowers2.gif", 42 + "/web_workshop/resources/images/divider_rainbow.gif", 43 + "/web_workshop/resources/images/divider_rainbow_dots.gif", 44 + "/web_workshop/resources/images/divider_red_flowers.png", 45 + "/web_workshop/resources/images/divider_wildflowers.gif", 46 + "/web_workshop/resources/images/divider_yellow_flowers.gif", 47 + "/web_workshop/resources/images/dollhouse.gif", 48 + "/web_workshop/resources/images/doraemon.gif", 49 + "/web_workshop/resources/images/doraemon_stamp.gif", 50 + "/web_workshop/resources/images/email.gif", 51 + "/web_workshop/resources/images/email2.gif", 52 + "/web_workshop/resources/images/flowers.gif", 53 + "/web_workshop/resources/images/frog_car.gif", 54 + "/web_workshop/resources/images/furby1.gif", 55 + "/web_workshop/resources/images/furby2.gif", 56 + "/web_workshop/resources/images/furby3.gif", 57 + "/web_workshop/resources/images/furby4.gif", 58 + "/web_workshop/resources/images/furby5.gif", 59 + "/web_workshop/resources/images/furby6.gif", 60 + "/web_workshop/resources/images/furby7.gif", 61 + "/web_workshop/resources/images/furby8.gif", 62 + "/web_workshop/resources/images/furby9.gif", 63 + "/web_workshop/resources/images/furby_banner.gif", 64 + "/web_workshop/resources/images/furby_clouds.gif", 65 + "/web_workshop/resources/images/furby_field.jpg", 66 + "/web_workshop/resources/images/furby_pink.png", 67 + "/web_workshop/resources/images/furby_shocked.gif", 68 + "/web_workshop/resources/images/furby_weird.gif", 69 + "/web_workshop/resources/images/furbyland.gif", 70 + "/web_workshop/resources/images/gaia.gif", 71 + "/web_workshop/resources/images/garf.gif", 72 + "/web_workshop/resources/images/gc_icon.gif", 73 + "/web_workshop/resources/images/hacker_skull.gif", 74 + "/web_workshop/resources/images/hacking.gif", 75 + "/web_workshop/resources/images/hamster.gif", 76 + "/web_workshop/resources/images/hamtaro.gif", 77 + "/web_workshop/resources/images/hellokitty.gif", 78 + "/web_workshop/resources/images/hint.gif", 79 + "/web_workshop/resources/images/home.gif", 80 + "/web_workshop/resources/images/koosh.gif", 81 + "/web_workshop/resources/images/lil_buddha.gif", 82 + "/web_workshop/resources/images/lil_demon.gif", 83 + "/web_workshop/resources/images/linux_power.gif", 84 + "/web_workshop/resources/images/mac.png", 85 + "/web_workshop/resources/images/mailbox.gif", 86 + "/web_workshop/resources/images/mario64.gif", 87 + "/web_workshop/resources/images/modchips.gif", 88 + "/web_workshop/resources/images/neopet.gif", 89 + "/web_workshop/resources/images/ness.gif", 90 + "/web_workshop/resources/images/new_bunny.gif", 91 + "/web_workshop/resources/images/no_java.gif", 92 + "/web_workshop/resources/images/orbit.gif", 93 + "/web_workshop/resources/images/pika_construction.gif", 94 + "/web_workshop/resources/images/pink_clock.gif", 95 + "/web_workshop/resources/images/pochacco.gif", 96 + "/web_workshop/resources/images/providence.gif", 97 + "/web_workshop/resources/images/ratrace.gif", 98 + "/web_workshop/resources/images/ring.gif", 99 + "/web_workshop/resources/images/sailor_moon.gif", 100 + "/web_workshop/resources/images/samus.gif", 101 + "/web_workshop/resources/images/scachis.gif", 102 + "/web_workshop/resources/images/scared_mouse.gif", 103 + "/web_workshop/resources/images/smiley_bar.gif", 104 + "/web_workshop/resources/images/smiley_dancing.gif", 105 + "/web_workshop/resources/images/smiling_pizza.gif", 106 + "/web_workshop/resources/images/spacedog.gif", 107 + "/web_workshop/resources/images/spinninbrain.gif", 108 + "/web_workshop/resources/images/splat.png", 109 + "/web_workshop/resources/images/sun.png", 110 + "/web_workshop/resources/images/surfin.gif", 111 + "/web_workshop/resources/images/tamas.gif", 112 + "/web_workshop/resources/images/telephone.gif", 113 + "/web_workshop/resources/images/underconstruction.gif", 114 + "/web_workshop/resources/images/welcome.gif", 115 + "/web_workshop/resources/images/windows.gif", 116 + "/web_workshop/resources/images/wishingwell.gif", 117 + "/web_workshop/resources/images/wizard.gif", 118 + "/web_workshop/resources/images/ww_9volt.gif", 119 + "/web_workshop/resources/images/ww_ana.gif", 120 + "/web_workshop/resources/images/ww_crygor.gif", 121 + "/web_workshop/resources/images/ww_dribble.gif", 122 + "/web_workshop/resources/images/ww_jimmy.gif", 123 + "/web_workshop/resources/images/ww_kat.gif", 124 + "/web_workshop/resources/images/ww_mona.gif", 125 + "/web_workshop/resources/images/ww_orbulon.gif", 126 + "/web_workshop/resources/images/ww_spitz.gif", 127 + "/web_workshop/resources/images/ww_wario.gif", 128 + "/web_workshop/resources/images/y2k.gif" 129 + ], 130 + "resources": [ 131 + "/web_workshop/resources/css/styles.css", 132 + "/web_workshop/resources/icons/construction.png", 133 + "/web_workshop/resources/icons/construction_padded.png", 134 + "/web_workshop/resources/images/.DS_Store", 135 + "/web_workshop/resources/images/24.gif", 136 + "/web_workshop/resources/images/bang.gif", 137 + "/web_workshop/resources/images/bg_bricks.jpg", 138 + "/web_workshop/resources/images/bg_checker.gif", 139 + "/web_workshop/resources/images/bg_daisies.jpg", 140 + "/web_workshop/resources/images/bg_desert.gif", 141 + "/web_workshop/resources/images/bg_stars.gif", 142 + "/web_workshop/resources/images/bg_trippy.jpg", 143 + "/web_workshop/resources/images/bg_water.jpg", 144 + "/web_workshop/resources/images/bigN.gif", 145 + "/web_workshop/resources/images/birdbath.gif", 146 + "/web_workshop/resources/images/bizzy_wizzy.gif", 147 + "/web_workshop/resources/images/blahblah.gif", 148 + "/web_workshop/resources/images/bonk.gif", 149 + "/web_workshop/resources/images/bright_idea.gif", 150 + "/web_workshop/resources/images/butterflies.gif", 151 + "/web_workshop/resources/images/butterfly_blue.gif", 152 + "/web_workshop/resources/images/butterfly_monarch.gif", 153 + "/web_workshop/resources/images/cactus.gif", 154 + "/web_workshop/resources/images/cat.gif", 155 + "/web_workshop/resources/images/caterpillar.gif", 156 + "/web_workshop/resources/images/cd.gif", 157 + "/web_workshop/resources/images/christmas_garland.gif", 158 + "/web_workshop/resources/images/christmas_holly.gif", 159 + "/web_workshop/resources/images/christmas_wreath.gif", 160 + "/web_workshop/resources/images/coffee.gif", 161 + "/web_workshop/resources/images/coffee.png", 162 + "/web_workshop/resources/images/computer.gif", 163 + "/web_workshop/resources/images/construct.gif", 164 + "/web_workshop/resources/images/cow.gif", 165 + "/web_workshop/resources/images/daisies.gif", 166 + "/web_workshop/resources/images/dancingman.gif", 167 + "/web_workshop/resources/images/dancingwoman.gif", 168 + "/web_workshop/resources/images/devil.gif", 169 + "/web_workshop/resources/images/disco_ball.gif", 170 + "/web_workshop/resources/images/divider_blissey.gif", 171 + "/web_workshop/resources/images/divider_blue_flowers.gif", 172 + "/web_workshop/resources/images/divider_blue_flowers2.gif", 173 + "/web_workshop/resources/images/divider_rainbow.gif", 174 + "/web_workshop/resources/images/divider_rainbow_dots.gif", 175 + "/web_workshop/resources/images/divider_red_flowers.png", 176 + "/web_workshop/resources/images/divider_wildflowers.gif", 177 + "/web_workshop/resources/images/divider_yellow_flowers.gif", 178 + "/web_workshop/resources/images/dollhouse.gif", 179 + "/web_workshop/resources/images/doraemon.gif", 180 + "/web_workshop/resources/images/doraemon_stamp.gif", 181 + "/web_workshop/resources/images/email.gif", 182 + "/web_workshop/resources/images/email2.gif", 183 + "/web_workshop/resources/images/flowers.gif", 184 + "/web_workshop/resources/images/frog_car.gif", 185 + "/web_workshop/resources/images/furby1.gif", 186 + "/web_workshop/resources/images/furby2.gif", 187 + "/web_workshop/resources/images/furby3.gif", 188 + "/web_workshop/resources/images/furby4.gif", 189 + "/web_workshop/resources/images/furby5.gif", 190 + "/web_workshop/resources/images/furby6.gif", 191 + "/web_workshop/resources/images/furby7.gif", 192 + "/web_workshop/resources/images/furby8.gif", 193 + "/web_workshop/resources/images/furby9.gif", 194 + "/web_workshop/resources/images/furby_banner.gif", 195 + "/web_workshop/resources/images/furby_clouds.gif", 196 + "/web_workshop/resources/images/furby_field.jpg", 197 + "/web_workshop/resources/images/furby_pink.png", 198 + "/web_workshop/resources/images/furby_shocked.gif", 199 + "/web_workshop/resources/images/furby_weird.gif", 200 + "/web_workshop/resources/images/furbyland.gif", 201 + "/web_workshop/resources/images/gaia.gif", 202 + "/web_workshop/resources/images/garf.gif", 203 + "/web_workshop/resources/images/gc_icon.gif", 204 + "/web_workshop/resources/images/hacker_skull.gif", 205 + "/web_workshop/resources/images/hacking.gif", 206 + "/web_workshop/resources/images/hamster.gif", 207 + "/web_workshop/resources/images/hamtaro.gif", 208 + "/web_workshop/resources/images/hellokitty.gif", 209 + "/web_workshop/resources/images/hint.gif", 210 + "/web_workshop/resources/images/home.gif", 211 + "/web_workshop/resources/images/koosh.gif", 212 + "/web_workshop/resources/images/lil_buddha.gif", 213 + "/web_workshop/resources/images/lil_demon.gif", 214 + "/web_workshop/resources/images/linux_power.gif", 215 + "/web_workshop/resources/images/mac.png", 216 + "/web_workshop/resources/images/mailbox.gif", 217 + "/web_workshop/resources/images/mario64.gif", 218 + "/web_workshop/resources/images/modchips.gif", 219 + "/web_workshop/resources/images/neopet.gif", 220 + "/web_workshop/resources/images/ness.gif", 221 + "/web_workshop/resources/images/new_bunny.gif", 222 + "/web_workshop/resources/images/no_java.gif", 223 + "/web_workshop/resources/images/orbit.gif", 224 + "/web_workshop/resources/images/pika_construction.gif", 225 + "/web_workshop/resources/images/pink_clock.gif", 226 + "/web_workshop/resources/images/pochacco.gif", 227 + "/web_workshop/resources/images/providence.gif", 228 + "/web_workshop/resources/images/ratrace.gif", 229 + "/web_workshop/resources/images/ring.gif", 230 + "/web_workshop/resources/images/sailor_moon.gif", 231 + "/web_workshop/resources/images/samus.gif", 232 + "/web_workshop/resources/images/scachis.gif", 233 + "/web_workshop/resources/images/scared_mouse.gif", 234 + "/web_workshop/resources/images/smiley_bar.gif", 235 + "/web_workshop/resources/images/smiley_dancing.gif", 236 + "/web_workshop/resources/images/smiling_pizza.gif", 237 + "/web_workshop/resources/images/spacedog.gif", 238 + "/web_workshop/resources/images/spinninbrain.gif", 239 + "/web_workshop/resources/images/splat.png", 240 + "/web_workshop/resources/images/sun.png", 241 + "/web_workshop/resources/images/surfin.gif", 242 + "/web_workshop/resources/images/tamas.gif", 243 + "/web_workshop/resources/images/telephone.gif", 244 + "/web_workshop/resources/images/underconstruction.gif", 245 + "/web_workshop/resources/images/welcome.gif", 246 + "/web_workshop/resources/images/windows.gif", 247 + "/web_workshop/resources/images/wishingwell.gif", 248 + "/web_workshop/resources/images/wizard.gif", 249 + "/web_workshop/resources/images/ww_9volt.gif", 250 + "/web_workshop/resources/images/ww_ana.gif", 251 + "/web_workshop/resources/images/ww_crygor.gif", 252 + "/web_workshop/resources/images/ww_dribble.gif", 253 + "/web_workshop/resources/images/ww_jimmy.gif", 254 + "/web_workshop/resources/images/ww_kat.gif", 255 + "/web_workshop/resources/images/ww_mona.gif", 256 + "/web_workshop/resources/images/ww_orbulon.gif", 257 + "/web_workshop/resources/images/ww_spitz.gif", 258 + "/web_workshop/resources/images/ww_wario.gif", 259 + "/web_workshop/resources/images/y2k.gif", 260 + "/web_workshop/resources/js/codemirror-bundle.js", 261 + "/web_workshop/resources/js/main.js", 262 + "/web_workshop/resources/readme_images/picnic.png", 263 + "/web_workshop/resources/readme_images/stock_images.png", 264 + "/web_workshop/resources/resource-manifest.json", 265 + "/web_workshop/resources/scripts/generate_resource_manifest.py", 266 + "/web_workshop/resources/ui/fullscreen.svg", 267 + "/web_workshop/resources/vendor/build.mjs", 268 + "/web_workshop/resources/vendor/node_modules/.bin/esbuild", 269 + "/web_workshop/resources/vendor/node_modules/.package-lock.json", 270 + "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/.github/workflows/dispatch.yml", 271 + "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/CHANGELOG.md", 272 + "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/LICENSE", 273 + "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/README.md", 274 + "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/dist/index.cjs", 275 + "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/dist/index.d.cts", 276 + "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/dist/index.d.ts", 277 + "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/dist/index.js", 278 + "/web_workshop/resources/vendor/node_modules/@codemirror/autocomplete/package.json", 279 + "/web_workshop/resources/vendor/node_modules/@codemirror/commands/.github/workflows/dispatch.yml", 280 + "/web_workshop/resources/vendor/node_modules/@codemirror/commands/CHANGELOG.md", 281 + "/web_workshop/resources/vendor/node_modules/@codemirror/commands/LICENSE", 282 + "/web_workshop/resources/vendor/node_modules/@codemirror/commands/README.md", 283 + "/web_workshop/resources/vendor/node_modules/@codemirror/commands/dist/index.cjs", 284 + "/web_workshop/resources/vendor/node_modules/@codemirror/commands/dist/index.d.cts", 285 + "/web_workshop/resources/vendor/node_modules/@codemirror/commands/dist/index.d.ts", 286 + "/web_workshop/resources/vendor/node_modules/@codemirror/commands/dist/index.js", 287 + "/web_workshop/resources/vendor/node_modules/@codemirror/commands/package.json", 288 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/.github/workflows/dispatch.yml", 289 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/CHANGELOG.md", 290 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/LICENSE", 291 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/README.md", 292 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/dist/index.cjs", 293 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/dist/index.d.cts", 294 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/dist/index.d.ts", 295 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/dist/index.js", 296 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-css/package.json", 297 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/.github/workflows/dispatch.yml", 298 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/CHANGELOG.md", 299 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/LICENSE", 300 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/README.md", 301 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/dist/index.cjs", 302 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/dist/index.d.cts", 303 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/dist/index.d.ts", 304 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/dist/index.js", 305 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-html/package.json", 306 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/.github/workflows/dispatch.yml", 307 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/CHANGELOG.md", 308 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/LICENSE", 309 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/README.md", 310 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/dist/index.cjs", 311 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/dist/index.d.cts", 312 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/dist/index.d.ts", 313 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/dist/index.js", 314 + "/web_workshop/resources/vendor/node_modules/@codemirror/lang-javascript/package.json", 315 + "/web_workshop/resources/vendor/node_modules/@codemirror/language/.github/workflows/dispatch.yml", 316 + "/web_workshop/resources/vendor/node_modules/@codemirror/language/CHANGELOG.md", 317 + "/web_workshop/resources/vendor/node_modules/@codemirror/language/LICENSE", 318 + "/web_workshop/resources/vendor/node_modules/@codemirror/language/README.md", 319 + "/web_workshop/resources/vendor/node_modules/@codemirror/language/dist/index.cjs", 320 + "/web_workshop/resources/vendor/node_modules/@codemirror/language/dist/index.d.cts", 321 + "/web_workshop/resources/vendor/node_modules/@codemirror/language/dist/index.d.ts", 322 + "/web_workshop/resources/vendor/node_modules/@codemirror/language/dist/index.js", 323 + "/web_workshop/resources/vendor/node_modules/@codemirror/language/package.json", 324 + "/web_workshop/resources/vendor/node_modules/@codemirror/lint/.github/workflows/dispatch.yml", 325 + "/web_workshop/resources/vendor/node_modules/@codemirror/lint/CHANGELOG.md", 326 + "/web_workshop/resources/vendor/node_modules/@codemirror/lint/LICENSE", 327 + "/web_workshop/resources/vendor/node_modules/@codemirror/lint/README.md", 328 + "/web_workshop/resources/vendor/node_modules/@codemirror/lint/dist/index.cjs", 329 + "/web_workshop/resources/vendor/node_modules/@codemirror/lint/dist/index.d.cts", 330 + "/web_workshop/resources/vendor/node_modules/@codemirror/lint/dist/index.d.ts", 331 + "/web_workshop/resources/vendor/node_modules/@codemirror/lint/dist/index.js", 332 + "/web_workshop/resources/vendor/node_modules/@codemirror/lint/package.json", 333 + "/web_workshop/resources/vendor/node_modules/@codemirror/search/.github/workflows/dispatch.yml", 334 + "/web_workshop/resources/vendor/node_modules/@codemirror/search/CHANGELOG.md", 335 + "/web_workshop/resources/vendor/node_modules/@codemirror/search/LICENSE", 336 + "/web_workshop/resources/vendor/node_modules/@codemirror/search/README.md", 337 + "/web_workshop/resources/vendor/node_modules/@codemirror/search/dist/index.cjs", 338 + "/web_workshop/resources/vendor/node_modules/@codemirror/search/dist/index.d.cts", 339 + "/web_workshop/resources/vendor/node_modules/@codemirror/search/dist/index.d.ts", 340 + "/web_workshop/resources/vendor/node_modules/@codemirror/search/dist/index.js", 341 + "/web_workshop/resources/vendor/node_modules/@codemirror/search/package.json", 342 + "/web_workshop/resources/vendor/node_modules/@codemirror/state/.github/workflows/dispatch.yml", 343 + "/web_workshop/resources/vendor/node_modules/@codemirror/state/CHANGELOG.md", 344 + "/web_workshop/resources/vendor/node_modules/@codemirror/state/LICENSE", 345 + "/web_workshop/resources/vendor/node_modules/@codemirror/state/README.md", 346 + "/web_workshop/resources/vendor/node_modules/@codemirror/state/dist/index.cjs", 347 + "/web_workshop/resources/vendor/node_modules/@codemirror/state/dist/index.d.cts", 348 + "/web_workshop/resources/vendor/node_modules/@codemirror/state/dist/index.d.ts", 349 + "/web_workshop/resources/vendor/node_modules/@codemirror/state/dist/index.js", 350 + "/web_workshop/resources/vendor/node_modules/@codemirror/state/package.json", 351 + "/web_workshop/resources/vendor/node_modules/@codemirror/view/.github/workflows/dispatch.yml", 352 + "/web_workshop/resources/vendor/node_modules/@codemirror/view/CHANGELOG.md", 353 + "/web_workshop/resources/vendor/node_modules/@codemirror/view/LICENSE", 354 + "/web_workshop/resources/vendor/node_modules/@codemirror/view/README.md", 355 + "/web_workshop/resources/vendor/node_modules/@codemirror/view/dist/index.cjs", 356 + "/web_workshop/resources/vendor/node_modules/@codemirror/view/dist/index.d.cts", 357 + "/web_workshop/resources/vendor/node_modules/@codemirror/view/dist/index.d.ts", 358 + "/web_workshop/resources/vendor/node_modules/@codemirror/view/dist/index.js", 359 + "/web_workshop/resources/vendor/node_modules/@codemirror/view/package.json", 360 + "/web_workshop/resources/vendor/node_modules/@esbuild/darwin-arm64/README.md", 361 + "/web_workshop/resources/vendor/node_modules/@esbuild/darwin-arm64/bin/esbuild", 362 + "/web_workshop/resources/vendor/node_modules/@esbuild/darwin-arm64/package.json", 363 + "/web_workshop/resources/vendor/node_modules/@fsegurai/codemirror-theme-github-dark/README.md", 364 + "/web_workshop/resources/vendor/node_modules/@fsegurai/codemirror-theme-github-dark/dist/index.cjs", 365 + "/web_workshop/resources/vendor/node_modules/@fsegurai/codemirror-theme-github-dark/dist/index.d.cts", 366 + "/web_workshop/resources/vendor/node_modules/@fsegurai/codemirror-theme-github-dark/dist/index.d.ts", 367 + "/web_workshop/resources/vendor/node_modules/@fsegurai/codemirror-theme-github-dark/dist/index.js", 368 + "/web_workshop/resources/vendor/node_modules/@fsegurai/codemirror-theme-github-dark/package.json", 369 + "/web_workshop/resources/vendor/node_modules/@lezer/common/LICENSE", 370 + "/web_workshop/resources/vendor/node_modules/@lezer/common/README.md", 371 + "/web_workshop/resources/vendor/node_modules/@lezer/common/dist/index.cjs", 372 + "/web_workshop/resources/vendor/node_modules/@lezer/common/dist/index.d.cts", 373 + "/web_workshop/resources/vendor/node_modules/@lezer/common/dist/index.d.ts", 374 + "/web_workshop/resources/vendor/node_modules/@lezer/common/dist/index.js", 375 + "/web_workshop/resources/vendor/node_modules/@lezer/common/package.json", 376 + "/web_workshop/resources/vendor/node_modules/@lezer/css/CHANGELOG.md", 377 + "/web_workshop/resources/vendor/node_modules/@lezer/css/LICENSE", 378 + "/web_workshop/resources/vendor/node_modules/@lezer/css/README.md", 379 + "/web_workshop/resources/vendor/node_modules/@lezer/css/dist/index.cjs", 380 + "/web_workshop/resources/vendor/node_modules/@lezer/css/dist/index.d.cts", 381 + "/web_workshop/resources/vendor/node_modules/@lezer/css/dist/index.d.ts", 382 + "/web_workshop/resources/vendor/node_modules/@lezer/css/dist/index.js", 383 + "/web_workshop/resources/vendor/node_modules/@lezer/css/package.json", 384 + "/web_workshop/resources/vendor/node_modules/@lezer/css/rollup.config.js", 385 + "/web_workshop/resources/vendor/node_modules/@lezer/css/src/css.grammar", 386 + "/web_workshop/resources/vendor/node_modules/@lezer/css/src/highlight.js", 387 + "/web_workshop/resources/vendor/node_modules/@lezer/css/src/parser.js", 388 + "/web_workshop/resources/vendor/node_modules/@lezer/css/src/parser.terms.js", 389 + "/web_workshop/resources/vendor/node_modules/@lezer/css/src/tokens.js", 390 + "/web_workshop/resources/vendor/node_modules/@lezer/css/test/declarations.txt", 391 + "/web_workshop/resources/vendor/node_modules/@lezer/css/test/selector.txt", 392 + "/web_workshop/resources/vendor/node_modules/@lezer/css/test/statements.txt", 393 + "/web_workshop/resources/vendor/node_modules/@lezer/css/test/test-css.js", 394 + "/web_workshop/resources/vendor/node_modules/@lezer/highlight/LICENSE", 395 + "/web_workshop/resources/vendor/node_modules/@lezer/highlight/README.md", 396 + "/web_workshop/resources/vendor/node_modules/@lezer/highlight/dist/index.cjs", 397 + "/web_workshop/resources/vendor/node_modules/@lezer/highlight/dist/index.d.cts", 398 + "/web_workshop/resources/vendor/node_modules/@lezer/highlight/dist/index.d.ts", 399 + "/web_workshop/resources/vendor/node_modules/@lezer/highlight/dist/index.js", 400 + "/web_workshop/resources/vendor/node_modules/@lezer/highlight/package.json", 401 + "/web_workshop/resources/vendor/node_modules/@lezer/html/CHANGELOG.md", 402 + "/web_workshop/resources/vendor/node_modules/@lezer/html/LICENSE", 403 + "/web_workshop/resources/vendor/node_modules/@lezer/html/README.md", 404 + "/web_workshop/resources/vendor/node_modules/@lezer/html/dist/index.cjs", 405 + "/web_workshop/resources/vendor/node_modules/@lezer/html/dist/index.d.cts", 406 + "/web_workshop/resources/vendor/node_modules/@lezer/html/dist/index.d.ts", 407 + "/web_workshop/resources/vendor/node_modules/@lezer/html/dist/index.js", 408 + "/web_workshop/resources/vendor/node_modules/@lezer/html/package.json", 409 + "/web_workshop/resources/vendor/node_modules/@lezer/html/rollup.config.js", 410 + "/web_workshop/resources/vendor/node_modules/@lezer/html/src/.tern-port", 411 + "/web_workshop/resources/vendor/node_modules/@lezer/html/src/content.js", 412 + "/web_workshop/resources/vendor/node_modules/@lezer/html/src/highlight.js", 413 + "/web_workshop/resources/vendor/node_modules/@lezer/html/src/html.grammar", 414 + "/web_workshop/resources/vendor/node_modules/@lezer/html/src/index.js", 415 + "/web_workshop/resources/vendor/node_modules/@lezer/html/src/parser.js", 416 + "/web_workshop/resources/vendor/node_modules/@lezer/html/src/parser.terms.js", 417 + "/web_workshop/resources/vendor/node_modules/@lezer/html/src/tokens.js", 418 + "/web_workshop/resources/vendor/node_modules/@lezer/html/test/mixed.txt", 419 + "/web_workshop/resources/vendor/node_modules/@lezer/html/test/tags.txt", 420 + "/web_workshop/resources/vendor/node_modules/@lezer/html/test/test-html.js", 421 + "/web_workshop/resources/vendor/node_modules/@lezer/html/test/test-incremental.js", 422 + "/web_workshop/resources/vendor/node_modules/@lezer/html/test/vue.txt", 423 + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/CHANGELOG.md", 424 + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/LICENSE", 425 + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/README.md", 426 + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/dist/index.cjs", 427 + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/dist/index.d.cts", 428 + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/dist/index.d.ts", 429 + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/dist/index.js", 430 + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/package.json", 431 + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/rollup.config.js", 432 + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/src/highlight.js", 433 + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/src/javascript.grammar", 434 + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/src/parser.js", 435 + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/src/parser.terms.js", 436 + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/src/tokens.js", 437 + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/test/decorator.txt", 438 + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/test/expression.txt", 439 + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/test/jsx.txt", 440 + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/test/semicolon.txt", 441 + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/test/statement.txt", 442 + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/test/test-javascript.js", 443 + "/web_workshop/resources/vendor/node_modules/@lezer/javascript/test/typescript.txt", 444 + "/web_workshop/resources/vendor/node_modules/@lezer/lr/LICENSE", 445 + "/web_workshop/resources/vendor/node_modules/@lezer/lr/README.md", 446 + "/web_workshop/resources/vendor/node_modules/@lezer/lr/dist/constants.d.ts", 447 + "/web_workshop/resources/vendor/node_modules/@lezer/lr/dist/constants.js", 448 + "/web_workshop/resources/vendor/node_modules/@lezer/lr/dist/index.cjs", 449 + "/web_workshop/resources/vendor/node_modules/@lezer/lr/dist/index.d.cts", 450 + "/web_workshop/resources/vendor/node_modules/@lezer/lr/dist/index.d.ts", 451 + "/web_workshop/resources/vendor/node_modules/@lezer/lr/dist/index.js", 452 + "/web_workshop/resources/vendor/node_modules/@lezer/lr/package.json", 453 + "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/LICENSE", 454 + "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/README.md", 455 + "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/dist/index.cjs", 456 + "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/dist/index.d.cts", 457 + "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/package.json", 458 + "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/rollup.config.js", 459 + "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/src/index.d.ts", 460 + "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/src/index.js", 461 + "/web_workshop/resources/vendor/node_modules/@marijn/find-cluster-break/test/test-cluster.js", 462 + "/web_workshop/resources/vendor/node_modules/crelt/LICENSE", 463 + "/web_workshop/resources/vendor/node_modules/crelt/README.md", 464 + "/web_workshop/resources/vendor/node_modules/crelt/dist/index.cjs", 465 + "/web_workshop/resources/vendor/node_modules/crelt/dist/index.d.cts", 466 + "/web_workshop/resources/vendor/node_modules/crelt/index.d.ts", 467 + "/web_workshop/resources/vendor/node_modules/crelt/index.js", 468 + "/web_workshop/resources/vendor/node_modules/crelt/package.json", 469 + "/web_workshop/resources/vendor/node_modules/crelt/rollup.config.js", 470 + "/web_workshop/resources/vendor/node_modules/esbuild/LICENSE.md", 471 + "/web_workshop/resources/vendor/node_modules/esbuild/README.md", 472 + "/web_workshop/resources/vendor/node_modules/esbuild/bin/esbuild", 473 + "/web_workshop/resources/vendor/node_modules/esbuild/install.js", 474 + "/web_workshop/resources/vendor/node_modules/esbuild/lib/main.d.ts", 475 + "/web_workshop/resources/vendor/node_modules/esbuild/lib/main.js", 476 + "/web_workshop/resources/vendor/node_modules/esbuild/package.json", 477 + "/web_workshop/resources/vendor/node_modules/style-mod/LICENSE", 478 + "/web_workshop/resources/vendor/node_modules/style-mod/README.md", 479 + "/web_workshop/resources/vendor/node_modules/style-mod/dist/style-mod.cjs", 480 + "/web_workshop/resources/vendor/node_modules/style-mod/dist/style-mod.d.cts", 481 + "/web_workshop/resources/vendor/node_modules/style-mod/package.json", 482 + "/web_workshop/resources/vendor/node_modules/style-mod/src/README.md", 483 + "/web_workshop/resources/vendor/node_modules/style-mod/src/style-mod.d.ts", 484 + "/web_workshop/resources/vendor/node_modules/style-mod/src/style-mod.js", 485 + "/web_workshop/resources/vendor/node_modules/style-mod/test/test-style-mod.js", 486 + "/web_workshop/resources/vendor/node_modules/w3c-keyname/.tern-port", 487 + "/web_workshop/resources/vendor/node_modules/w3c-keyname/LICENSE", 488 + "/web_workshop/resources/vendor/node_modules/w3c-keyname/README.md", 489 + "/web_workshop/resources/vendor/node_modules/w3c-keyname/index.cjs", 490 + "/web_workshop/resources/vendor/node_modules/w3c-keyname/index.d.cts", 491 + "/web_workshop/resources/vendor/node_modules/w3c-keyname/index.d.ts", 492 + "/web_workshop/resources/vendor/node_modules/w3c-keyname/index.js", 493 + "/web_workshop/resources/vendor/node_modules/w3c-keyname/package.json", 494 + "/web_workshop/resources/vendor/package-lock.json", 495 + "/web_workshop/resources/vendor/package.json" 496 + ] 497 +}
1 +#!/usr/bin/env python3 2 +""" 3 +Generate resource manifest for PWA caching 4 +Scans the images/ and resources/ directories and creates a JSON manifest file 5 +""" 6 + 7 +import os 8 +import json 9 +from pathlib import Path 10 + 11 +def generate_resource_manifest(): 12 + """Generate a JSON manifest of all resources in the images/ and resources/ directories""" 13 + 14 + # Define supported image extensions 15 + image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico', '.bmp'} 16 + 17 + # Get the repo root (two levels up from resources/scripts/) 18 + repo_root = Path(__file__).parent.parent.parent 19 + resources_dir = repo_root / 'resources' 20 + images_dir = resources_dir / 'images' 21 + 22 + # Collect all image files 23 + image_files = [] 24 + if images_dir.exists(): 25 + for file_path in images_dir.iterdir(): 26 + if file_path.is_file() and file_path.suffix.lower() in image_extensions: 27 + # Create relative path from web root (GitHub Pages subdirectory) 28 + relative_path = f"/web_workshop/resources/images/{file_path.name}" 29 + image_files.append(relative_path) 30 + 31 + # Collect ALL resource files (no extension filtering) 32 + resource_files = [] 33 + if resources_dir.exists(): 34 + for file_path in resources_dir.rglob('*'): 35 + if file_path.is_file(): 36 + # Create relative path from web root (GitHub Pages subdirectory) 37 + relative_path = f"/web_workshop/resources/{file_path.relative_to(resources_dir).as_posix()}" 38 + resource_files.append(relative_path) 39 + 40 + # Sort for consistent output 41 + image_files.sort() 42 + resource_files.sort() 43 + 44 + # Create manifest object 45 + manifest = { 46 + "images": image_files, 47 + "resources": resource_files, 48 + "generated_at": "auto-generated by GitHub Actions" 49 + } 50 + 51 + # Write manifest file 52 + manifest_path = resources_dir / 'resource-manifest.json' 53 + with open(manifest_path, 'w', encoding='utf-8') as f: 54 + json.dump(manifest, f, indent=2, sort_keys=True) 55 + 56 + print(f"Generated manifest with {len(image_files)} images and {len(resource_files)} resources:") 57 + for img in image_files: 58 + print(f" - {img}") 59 + for res in resource_files: 60 + print(f" - {res}") 61 + 62 + return manifest 63 + 64 +if __name__ == "__main__": 65 + manifest = generate_resource_manifest() 66 + print(f"\nManifest saved to: resources/resource-manifest.json")
1 +#!/usr/bin/env node 2 +// Build script to bundle CodeMirror for offline use 3 +// 4 +// Usage: 5 +// cd vendor 6 +// npm install 7 +// npm run build 8 +// 9 +// This will regenerate ../js/codemirror-bundle.js 10 + 11 +import * as esbuild from 'esbuild'; 12 +import { fileURLToPath } from 'url'; 13 +import { dirname, join } from 'path'; 14 + 15 +const __dirname = dirname(fileURLToPath(import.meta.url)); 16 + 17 +await esbuild.build({ 18 + stdin: { 19 + contents: ` 20 + export {EditorView, keymap, placeholder, lineNumbers, Decoration} from "@codemirror/view"; 21 + export {EditorState, Compartment} from "@codemirror/state"; 22 + export {defaultKeymap, indentWithTab, undo, redo, undoDepth, redoDepth, history, historyKeymap} from "@codemirror/commands"; 23 + export {closeBrackets, closeBracketsKeymap} from "@codemirror/autocomplete"; 24 + export {html} from "@codemirror/lang-html"; 25 + export {githubDark} from "@fsegurai/codemirror-theme-github-dark"; 26 + export {indentUnit} from "@codemirror/language"; 27 + export {search, searchKeymap, closeSearchPanel, openSearchPanel} from "@codemirror/search"; 28 + `, 29 + resolveDir: __dirname, 30 + loader: 'js', 31 + }, 32 + bundle: true, 33 + format: 'esm', 34 + outfile: join(__dirname, '..', 'js', 'codemirror-bundle.js'), 35 + minify: true, 36 + target: ['es2020'], 37 +}); 38 + 39 +console.log('✓ CodeMirror bundle created: ../js/codemirror-bundle.js');
3 '/web_workshop/', 4 '/web_workshop/index.html', 5 '/web_workshop/manifest.json', - '/web_workshop/resource-manifest.json', - '/web_workshop/styles.css', - '/web_workshop/main.js', - '/web_workshop/codemirror-bundle.js' 6 + '/web_workshop/resources/resource-manifest.json', 7 + '/web_workshop/resources/css/styles.css', 8 + '/web_workshop/resources/js/main.js', 9 + '/web_workshop/resources/js/codemirror-bundle.js' 10 ]; 11 12 // Function to discover and cache all files in directories 44 // Function to cache all resources using the generated manifest 45 async function cacheResources(cache) { 46 try { - const response = await fetch('/web_workshop/resource-manifest.json'); 47 + const response = await fetch('/web_workshop/resources/resource-manifest.json'); 48 if (response.ok) { 49 const manifest = await response.json(); 50 console.log(`Caching ${manifest.images.length} images and ${manifest.resources.length} resources from manifest`);
-#!/usr/bin/env node -// Build script to bundle CodeMirror for offline use -// -// Usage: -// cd vendor -// npm install -// npm run build -// -// This will regenerate ../codemirror-bundle.js - -import * as esbuild from 'esbuild'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -await esbuild.build({ - stdin: { - contents: ` - export {EditorView, keymap, placeholder, lineNumbers, Decoration} from "@codemirror/view"; - export {EditorState, Compartment} from "@codemirror/state"; - export {defaultKeymap, indentWithTab, undo, redo, undoDepth, redoDepth, history, historyKeymap} from "@codemirror/commands"; - export {closeBrackets, closeBracketsKeymap} from "@codemirror/autocomplete"; - export {html} from "@codemirror/lang-html"; - export {githubDark} from "@fsegurai/codemirror-theme-github-dark"; - export {indentUnit} from "@codemirror/language"; - export {search, searchKeymap, closeSearchPanel, openSearchPanel} from "@codemirror/search"; - `, - resolveDir: __dirname, - loader: 'js', - }, - bundle: true, - format: 'esm', - outfile: join(__dirname, '..', 'codemirror-bundle.js'), - minify: true, - target: ['es2020'], -}); - -console.log('✓ CodeMirror bundle created: ../codemirror-bundle.js');