diff --git a/package.json b/package.json index 2afed21..902ef81 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "waifu-board", - "version": "v1.6.2", + "version": "v1.6.3", "description": "An image board app to store and browse your favorite waifus!", "main": "main.js", "scripts": { diff --git a/src/emulator/emulator.js b/src/emulator/emulator.js new file mode 100644 index 0000000..b42e986 --- /dev/null +++ b/src/emulator/emulator.js @@ -0,0 +1,306 @@ +const CheerioShim = { + load: (html) => { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + + const $ = (selector, context) => { + let elements; + if (selector && selector.nodeType) { + elements = [selector]; + } else if (Array.isArray(selector)) { + elements = selector; + } else { + const root = (context && context[0]) ? context[0] : (context || doc); + elements = Array.from(root.querySelectorAll ? root.querySelectorAll(selector) : []); + } + return createWrapper(elements); + }; + + function createWrapper(elements) { + elements.forEach((el, i) => elements[i] = el); + + elements.attr = (name) => { + if (elements.length === 0) return undefined; + return elements[0].getAttribute ? elements[0].getAttribute(name) : null; + }; + + elements.text = () => { + return elements.map(el => el.textContent).join(''); + }; + + elements.find = (selector) => { + let found = []; + elements.forEach(el => { + if (el.querySelectorAll) { + found = found.concat(Array.from(el.querySelectorAll(selector))); + } + }); + return createWrapper(found); + }; + + elements.each = (callback) => { + elements.forEach((el, i) => { + callback(i, el); + }); + return elements; + }; + + elements.map = (callback) => { + const results = elements.map((el, i) => callback(i, el)); + return createWrapper(results.filter(r => r !== null && r !== undefined)); + }; + + elements.get = () => Array.from(elements); + + elements.filter = (selectorOrFn) => { + if (typeof selectorOrFn === 'function') { + return createWrapper(elements.filter(selectorOrFn)); + } + return createWrapper(elements.filter(el => el.matches && el.matches(selectorOrFn))); + }; + + elements.first = () => createWrapper(elements.length ? [elements[0]] : []); + elements.last = () => createWrapper(elements.length ? [elements[elements.length - 1]] : []); + elements.eq = (i) => createWrapper(elements.length > i ? [elements[i]] : []); + + return elements; + } + + return $; + } +}; + +window.require = (moduleName) => { + if (moduleName === 'cheerio') return CheerioShim; + + if (moduleName === 'node-fetch' || moduleName === 'fetch') return window.fetch.bind(window); + + return {}; +}; + +window.module = { exports: {} }; + +class MockBrowser { + constructor() { + } + + async scrape(url, pageFn, options = {}) { + emulatorLog(`[Browser] Scraping: ${url}`, 'info'); + + try { + const response = await fetch(url); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const html = await response.text(); + + const parser = new DOMParser(); + const doc = parser.parseFromString(html, "text/html"); + + return await this.executePageFn(pageFn, doc); + + } catch (error) { + emulatorLog(`[Browser] Error: ${error.message}`, 'error'); + if (error.name === 'TypeError' && error.message.includes('Failed to fetch')) { + emulatorLog('Tip: This looks like a CORS error. Ensure you have a "Allow CORS" extension enabled in your browser for testing.', 'warn'); + } + throw error; + } + } + + async executePageFn(pageFn, virtualDoc) { + const fnString = pageFn.toString(); + const wrapper = new Function("document", `return (${fnString})();`); + return wrapper(virtualDoc); + } +} + +const codeInput = document.getElementById('code-input'); +const runBtn = document.getElementById('run-btn'); +const outputVisual = document.getElementById('visual-container'); +const outputJson = document.getElementById('json-content'); +const outputConsole = document.getElementById('console-content'); + +const originalConsoleLog = console.log; +const originalConsoleError = console.error; +const originalConsoleWarn = console.warn; + +function captureLogs() { + console.log = (...args) => { + originalConsoleLog(...args); + emulatorLog(args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '), 'info'); + }; + console.error = (...args) => { + originalConsoleError(...args); + emulatorLog(args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '), 'error'); + }; + console.warn = (...args) => { + originalConsoleWarn(...args); + emulatorLog(args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '), 'warn'); + }; +} + +function emulatorLog(msg, type = 'info') { + const div = document.createElement('div'); + div.className = `log-entry log-${type}`; + div.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`; + outputConsole.appendChild(div); + outputConsole.scrollTop = outputConsole.scrollHeight; +} + +window.switchTab = (tabName) => { + ['visual', 'json', 'console'].forEach(t => { + document.getElementById(`output-${t}`).classList.add('hidden'); + }); + document.getElementById(`output-${tabName}`).classList.remove('hidden'); + + document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active')); + if (event && event.target) event.target.classList.add('active'); +}; + +runBtn.addEventListener('click', async () => { + outputVisual.innerHTML = ''; + outputJson.textContent = ''; + outputConsole.innerHTML = ''; + window.module.exports = {}; + captureLogs(); + + const code = codeInput.value; + const functionName = document.getElementById('func-select').value; + const argInput = document.getElementById('arg-input').value; + const pageInput = parseInt(document.getElementById('page-input').value) || 1; + + if (!code.trim()) { + alert("Please paste extension code first."); + return; + } + + emulatorLog("--- Starting Execution ---"); + + try { + try { + new Function(code)(); + } catch (e) { + throw new Error(`Syntax/Runtime Error in Extension Code: ${e.message}`); + } + + const exports = window.module.exports; + const keys = Object.keys(exports); + if (keys.length === 0) throw new Error("No class found in module.exports. Did you add 'module.exports = { ClassName };'?"); + + const ExtensionClass = exports[keys[0]]; + emulatorLog(`Loaded Class: ${keys[0]}`, 'info'); + + const browserInstance = new MockBrowser(); + const extension = new ExtensionClass('node-fetch', 'cheerio', browserInstance); + + if (typeof extension[functionName] !== 'function') { + throw new Error(`Function '${functionName}' not found in class '${keys[0]}'.`); + } + + emulatorLog(`Calling ${functionName}('${argInput}', ${pageInput})...`); + + document.querySelector('.loading-state').classList.remove('hidden'); + + let result; + if (functionName === 'fetchSearchResult') { + result = await extension.fetchSearchResult(argInput, pageInput); + } else if (functionName === 'fetchInfo') { + result = await extension.fetchInfo(argInput); + } else if (functionName === 'findChapters') { + result = await extension.findChapters(argInput); + } else if (functionName === 'findChapterPages') { + result = await extension.findChapterPages(argInput); + } + + document.querySelector('.loading-state').classList.add('hidden'); + + emulatorLog("Data received. Rendering...", 'info'); + renderJson(result); + renderVisuals(result, functionName); + + if (result) switchTab('visual'); + + } catch (err) { + document.querySelector('.loading-state').classList.add('hidden'); + emulatorLog(err.message, 'error'); + console.error(err); + } +}); + +function renderJson(data) { + outputJson.textContent = JSON.stringify(data, null, 2); +} + +function renderVisuals(data, type) { + outputVisual.innerHTML = ''; + + if (!data) { + outputVisual.innerHTML = '
${JSON.stringify(data, null, 2)}`;
+ outputVisual.appendChild(container);
+ }
+}
\ No newline at end of file
diff --git a/src/updateNotification.js b/src/updateNotification.js
index 95af66f..a40af0b 100644
--- a/src/updateNotification.js
+++ b/src/updateNotification.js
@@ -1,6 +1,6 @@
const Gitea_OWNER = 'ItsSkaiya';
const Gitea_REPO = 'WaifuBoard';
-const CURRENT_VERSION = 'v1.6.2';
+const CURRENT_VERSION = 'v1.6.3';
const UPDATE_CHECK_INTERVAL = 5 * 60 * 1000;
let currentVersionDisplay;
diff --git a/views/emulator.html b/views/emulator.html
new file mode 100644
index 0000000..8563ce1
--- /dev/null
+++ b/views/emulator.html
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+