// pkg/webview/diagnostics.go package webview import ( "encoding/json" "fmt" "strings" ) func jsQuote(v string) string { b, _ := json.Marshal(v) return string(b) } func computedStyleScript(selector string) string { sel := jsQuote(selector) return fmt.Sprintf(`(function(){ const el = document.querySelector(%s); if (!el) return null; const style = window.getComputedStyle(el); const out = {}; for (let i = 0; i < style.length; i++) { const key = style[i]; out[key] = style.getPropertyValue(key); } return out; })()`, sel) } func highlightScript(selector, colour string) string { sel := jsQuote(selector) if colour == "" { colour = "#ff9800" } col := jsQuote(colour) return fmt.Sprintf(`(function(){ const el = document.querySelector(%s); if (!el) return false; if (el.__coreHighlightOrigOutline === undefined) { el.__coreHighlightOrigOutline = el.style.outline || ""; } el.style.outline = "3px solid " + %s; el.style.outlineOffset = "2px"; try { el.scrollIntoView({block: "center", inline: "center", behavior: "smooth"}); } catch (e) {} return true; })()`, sel, col) } func performanceScript() string { return `(function(){ const nav = performance.getEntriesByType("navigation")[0] || {}; const paints = performance.getEntriesByType("paint"); const firstPaint = paints.find((entry) => entry.name === "first-paint"); const firstContentfulPaint = paints.find((entry) => entry.name === "first-contentful-paint"); const memory = performance.memory || {}; return { navigationStart: nav.startTime || 0, domContentLoaded: nav.domContentLoadedEventEnd || 0, loadEventEnd: nav.loadEventEnd || 0, firstPaint: firstPaint ? firstPaint.startTime : 0, firstContentfulPaint: firstContentfulPaint ? firstContentfulPaint.startTime : 0, usedJSHeapSize: memory.usedJSHeapSize || 0, totalJSHeapSize: memory.totalJSHeapSize || 0 }; })()` } func resourcesScript() string { return `(function(){ return performance.getEntriesByType("resource").map((entry) => ({ name: entry.name, entryType: entry.entryType, initiatorType: entry.initiatorType, startTime: entry.startTime, duration: entry.duration, transferSize: entry.transferSize || 0, encodedBodySize: entry.encodedBodySize || 0, decodedBodySize: entry.decodedBodySize || 0 })); })()` } func networkInitScript() string { return `(function(){ if (window.__coreNetworkLog) return true; window.__coreNetworkLog = []; const log = (entry) => { window.__coreNetworkLog.push(entry); }; const originalFetch = window.fetch; if (originalFetch) { window.fetch = async function(input, init) { const request = typeof input === "string" ? input : (input && input.url) ? input.url : ""; const method = (init && init.method) || (input && input.method) || "GET"; const started = Date.now(); try { const response = await originalFetch.call(this, input, init); log({ url: response.url || request, method: method, status: response.status, ok: response.ok, resource: "fetch", timestamp: started }); return response; } catch (error) { log({ url: request, method: method, error: String(error), resource: "fetch", timestamp: started }); throw error; } }; } const originalOpen = XMLHttpRequest.prototype.open; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url) { this.__coreMethod = method; this.__coreUrl = url; return originalOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function(body) { const started = Date.now(); this.addEventListener("loadend", () => { log({ url: this.__coreUrl || "", method: this.__coreMethod || "GET", status: this.status || 0, ok: this.status >= 200 && this.status < 400, resource: "xhr", timestamp: started }); }); return originalSend.apply(this, arguments); }; return true; })()` } func networkClearScript() string { return `(function(){ window.__coreNetworkLog = []; return true; })()` } func networkLogScript(limit int) string { if limit <= 0 { return `(window.__coreNetworkLog || [])` } return fmt.Sprintf(`(window.__coreNetworkLog || []).slice(-%d)`, limit) } func normalizeWhitespace(s string) string { return strings.TrimSpace(s) }