refactor: inline redundant wrapper functions for webutil calls
- Replace escapeForJSString() wrapper with direct webutil.EscapeForJSString() calls in content_injector.go and input_bridge.go - Replace getMimeType() wrapper with direct webutil.GetMimeType() call in scheme_handler.go
- commit
- 107ed41ed26ff0cb978cb9451d0f7d63ceeac7fa
- author
- brice · · 2026-04-05 06:15 UTC
- parent
- 630eef1
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +12 -246
M internal/infrastructure/cef/content_injector.go +6 -68
| @@ -4,10 +4,11 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "fmt" | |
| 6 | 6 | "net/url" | |
| 7 | - | "os" | |
| 8 | 7 | "strings" | |
| 9 | 8 | "sync" | |
| 10 | 9 | ||
| 10 | + | "github.com/bnema/dumber/internal/infrastructure/webutil" | |
| 11 | + | ||
| 11 | 12 | "github.com/bnema/dumber/internal/application/port" | |
| 12 | 13 | "github.com/bnema/dumber/internal/logging" | |
| 13 | 14 | ) | |
| @@ -430,11 +431,7 @@ func (ci *contentInjector) setColorResolver(resolver port.ColorSchemeResolver) { | |||
| 430 | 431 | // newContentInjector creates a content injector wired to the given engine. | |
| 431 | 432 | // Video diagnostics are enabled when DUMBER_VIDEO_DIAG=1 is set. | |
| 432 | 433 | func newContentInjector(engine *Engine, resolver port.ColorSchemeResolver) *contentInjector { | |
| 433 | - | diagEnabled := false | |
| 434 | - | switch strings.ToLower(strings.TrimSpace(os.Getenv("DUMBER_VIDEO_DIAG"))) { | |
| 435 | - | case "1", "true", "yes", "on": | |
| 436 | - | diagEnabled = true | |
| 437 | - | } | |
| 434 | + | diagEnabled := envBoolEnabled("DUMBER_VIDEO_DIAG") | |
| 438 | 435 | return &contentInjector{ | |
| 439 | 436 | engine: engine, | |
| 440 | 437 | colorResolver: resolver, | |
| @@ -550,8 +547,8 @@ func (ci *contentInjector) onLoadEnd(wv *WebView) { | |||
| 550 | 547 | ||
| 551 | 548 | // injectCSS injects a CSS string as a <style> element via JavaScript. | |
| 552 | 549 | func (ci *contentInjector) injectCSS(wv *WebView, id, css string) { | |
| 553 | - | escapedID := escapeForJSString(id) | |
| 554 | - | escaped := escapeForJSString(css) | |
| 550 | + | escapedID := webutil.EscapeForJSString(id) | |
| 551 | + | escaped := webutil.EscapeForJSString(css) | |
| 555 | 552 | script := fmt.Sprintf(`(function(){ | |
| 556 | 553 | var el = document.getElementById('%s'); | |
| 557 | 554 | if (!el) { el = document.createElement('style'); el.id = '%s'; document.head.appendChild(el); } | |
| @@ -564,55 +561,7 @@ func (ci *contentInjector) injectCSS(wv *WebView, id, css string) { | |||
| 564 | 561 | // injectDarkModeScript sets dark/light class on <html> and patches matchMedia | |
| 565 | 562 | // for prefers-color-scheme queries on internal pages. | |
| 566 | 563 | func (ci *contentInjector) injectDarkModeScript(wv *WebView, prefersDark bool) { | |
| 567 | - | script := fmt.Sprintf(`(function() { | |
| 568 | - | var prefersDark = %t; | |
| 569 | - | window.__dumber_cef_prefers_dark = prefersDark; | |
| 570 | - | var originalMatchMedia = window.matchMedia.bind(window); | |
| 571 | - | ||
| 572 | - | if (prefersDark) { | |
| 573 | - | document.documentElement.classList.add('dark'); | |
| 574 | - | document.documentElement.classList.remove('light'); | |
| 575 | - | } else { | |
| 576 | - | document.documentElement.classList.add('light'); | |
| 577 | - | document.documentElement.classList.remove('dark'); | |
| 578 | - | } | |
| 579 | - | ||
| 580 | - | function isColorSchemeQuery(query, scheme) { | |
| 581 | - | if (typeof query !== 'string') return false; | |
| 582 | - | var normalized = query.replace(/\s+/g, '').toLowerCase(); | |
| 583 | - | return normalized.indexOf('prefers-color-scheme:' + scheme) !== -1; | |
| 584 | - | } | |
| 585 | - | ||
| 586 | - | function createFakeMediaQueryList(query, matches) { | |
| 587 | - | var listeners = []; | |
| 588 | - | var onchangeHandler = null; | |
| 589 | - | return { | |
| 590 | - | matches: matches, | |
| 591 | - | media: query, | |
| 592 | - | get onchange() { return onchangeHandler; }, | |
| 593 | - | set onchange(fn) { onchangeHandler = fn; }, | |
| 594 | - | addListener: function(cb) { if (typeof cb === 'function') listeners.push(cb); }, | |
| 595 | - | removeListener: function(cb) { var idx = listeners.indexOf(cb); if (idx !== -1) listeners.splice(idx, 1); }, | |
| 596 | - | addEventListener: function(type, cb) { if (type === 'change' && typeof cb === 'function') listeners.push(cb); }, | |
| 597 | - | removeEventListener: function(type, cb) { | |
| 598 | - | if (type === 'change') { var idx = listeners.indexOf(cb); if (idx !== -1) listeners.splice(idx, 1); } | |
| 599 | - | }, | |
| 600 | - | dispatchEvent: function(event) { | |
| 601 | - | for (var i = 0; i < listeners.length; i++) { try { listeners[i](event); } catch (e) {} } | |
| 602 | - | if (onchangeHandler) { try { onchangeHandler(event); } catch (e) {} } | |
| 603 | - | return true; | |
| 604 | - | } | |
| 605 | - | }; | |
| 606 | - | } | |
| 607 | - | ||
| 608 | - | window.matchMedia = function(query) { | |
| 609 | - | if (isColorSchemeQuery(query, 'dark')) return createFakeMediaQueryList(query, prefersDark); | |
| 610 | - | if (isColorSchemeQuery(query, 'light')) return createFakeMediaQueryList(query, !prefersDark); | |
| 611 | - | return originalMatchMedia(query); | |
| 612 | - | }; | |
| 613 | - | })();`, prefersDark) | |
| 614 | - | ||
| 615 | - | wv.RunJavaScript(context.Background(), script) | |
| 564 | + | wv.RunJavaScript(context.Background(), webutil.DarkModeScript(prefersDark, "__dumber_cef_prefers_dark")) | |
| 616 | 565 | } | |
| 617 | 566 | ||
| 618 | 567 | // injectMessageBridgeShim injects the window.dumber.postMessage JS client shim | |
| @@ -622,14 +571,3 @@ func (ci *contentInjector) injectDarkModeScript(wv *WebView, prefersDark bool) { | |||
| 622 | 571 | func (ci *contentInjector) injectMessageBridgeShim(wv *WebView) { | |
| 623 | 572 | wv.RunJavaScript(context.Background(), MessageBridgeJS) | |
| 624 | 573 | } | |
| 625 | - | ||
| 626 | - | // escapeForJSString escapes a string for use inside a JS single-quoted string literal. | |
| 627 | - | func escapeForJSString(s string) string { | |
| 628 | - | s = strings.ReplaceAll(s, "\\", "\\\\") | |
| 629 | - | s = strings.ReplaceAll(s, "'", "\\'") | |
| 630 | - | s = strings.ReplaceAll(s, "\n", "\\n") | |
| 631 | - | s = strings.ReplaceAll(s, "\r", "\\r") | |
| 632 | - | s = strings.ReplaceAll(s, "\u2028", "\\u2028") | |
| 633 | - | s = strings.ReplaceAll(s, "\u2029", "\\u2029") | |
| 634 | - | return s | |
| 635 | - | } | |
| @@ -4,10 +4,11 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | "net/url" | 6 | "net/url" | ||
| 7 | - | "os" | |||
| 8 | "strings" | 7 | "strings" | ||
| 9 | "sync" | 8 | "sync" | ||
| 10 | 9 | ||||
| 10 | + | "github.com/bnema/dumber/internal/infrastructure/webutil" | |||
| 11 | + | ||||
| 11 | "github.com/bnema/dumber/internal/application/port" | 12 | "github.com/bnema/dumber/internal/application/port" | ||
| 12 | "github.com/bnema/dumber/internal/logging" | 13 | "github.com/bnema/dumber/internal/logging" | ||
| 13 | ) | 14 | ) | ||
| @@ -430,11 +431,7 @@ func (ci *contentInjector) setColorResolver(resolver port.ColorSchemeResolver) { | |||||
| 430 | // newContentInjector creates a content injector wired to the given engine. | 431 | // newContentInjector creates a content injector wired to the given engine. | ||
| 431 | // Video diagnostics are enabled when DUMBER_VIDEO_DIAG=1 is set. | 432 | // Video diagnostics are enabled when DUMBER_VIDEO_DIAG=1 is set. | ||
| 432 | func newContentInjector(engine *Engine, resolver port.ColorSchemeResolver) *contentInjector { | 433 | func newContentInjector(engine *Engine, resolver port.ColorSchemeResolver) *contentInjector { | ||
| 433 | - | diagEnabled := false | 434 | + | diagEnabled := envBoolEnabled("DUMBER_VIDEO_DIAG") |
| 434 | - | switch strings.ToLower(strings.TrimSpace(os.Getenv("DUMBER_VIDEO_DIAG"))) { | |||
| 435 | - | case "1", "true", "yes", "on": | |||
| 436 | - | diagEnabled = true | |||
| 437 | - | } | |||
| 438 | return &contentInjector{ | 435 | return &contentInjector{ | ||
| 439 | engine: engine, | 436 | engine: engine, | ||
| 440 | colorResolver: resolver, | 437 | colorResolver: resolver, | ||
| @@ -550,8 +547,8 @@ func (ci *contentInjector) onLoadEnd(wv *WebView) { | |||||
| 550 | 547 | ||||
| 551 | // injectCSS injects a CSS string as a <style> element via JavaScript. | 548 | // injectCSS injects a CSS string as a <style> element via JavaScript. | ||
| 552 | func (ci *contentInjector) injectCSS(wv *WebView, id, css string) { | 549 | func (ci *contentInjector) injectCSS(wv *WebView, id, css string) { | ||
| 553 | - | escapedID := escapeForJSString(id) | 550 | + | escapedID := webutil.EscapeForJSString(id) |
| 554 | - | escaped := escapeForJSString(css) | 551 | + | escaped := webutil.EscapeForJSString(css) |
| 555 | script := fmt.Sprintf(`(function(){ | 552 | script := fmt.Sprintf(`(function(){ | ||
| 556 | var el = document.getElementById('%s'); | 553 | var el = document.getElementById('%s'); | ||
| 557 | if (!el) { el = document.createElement('style'); el.id = '%s'; document.head.appendChild(el); } | 554 | if (!el) { el = document.createElement('style'); el.id = '%s'; document.head.appendChild(el); } | ||
| @@ -564,55 +561,7 @@ func (ci *contentInjector) injectCSS(wv *WebView, id, css string) { | |||||
| 564 | // injectDarkModeScript sets dark/light class on <html> and patches matchMedia | 561 | // injectDarkModeScript sets dark/light class on <html> and patches matchMedia | ||
| 565 | // for prefers-color-scheme queries on internal pages. | 562 | // for prefers-color-scheme queries on internal pages. | ||
| 566 | func (ci *contentInjector) injectDarkModeScript(wv *WebView, prefersDark bool) { | 563 | func (ci *contentInjector) injectDarkModeScript(wv *WebView, prefersDark bool) { | ||
| 567 | - | script := fmt.Sprintf(`(function() { | 564 | + | wv.RunJavaScript(context.Background(), webutil.DarkModeScript(prefersDark, "__dumber_cef_prefers_dark")) |
| 568 | - | var prefersDark = %t; | |||
| 569 | - | window.__dumber_cef_prefers_dark = prefersDark; | |||
| 570 | - | var originalMatchMedia = window.matchMedia.bind(window); | |||
| 571 | - | ||||
| 572 | - | if (prefersDark) { | |||
| 573 | - | document.documentElement.classList.add('dark'); | |||
| 574 | - | document.documentElement.classList.remove('light'); | |||
| 575 | - | } else { | |||
| 576 | - | document.documentElement.classList.add('light'); | |||
| 577 | - | document.documentElement.classList.remove('dark'); | |||
| 578 | - | } | |||
| 579 | - | ||||
| 580 | - | function isColorSchemeQuery(query, scheme) { | |||
| 581 | - | if (typeof query !== 'string') return false; | |||
| 582 | - | var normalized = query.replace(/\s+/g, '').toLowerCase(); | |||
| 583 | - | return normalized.indexOf('prefers-color-scheme:' + scheme) !== -1; | |||
| 584 | - | } | |||
| 585 | - | ||||
| 586 | - | function createFakeMediaQueryList(query, matches) { | |||
| 587 | - | var listeners = []; | |||
| 588 | - | var onchangeHandler = null; | |||
| 589 | - | return { | |||
| 590 | - | matches: matches, | |||
| 591 | - | media: query, | |||
| 592 | - | get onchange() { return onchangeHandler; }, | |||
| 593 | - | set onchange(fn) { onchangeHandler = fn; }, | |||
| 594 | - | addListener: function(cb) { if (typeof cb === 'function') listeners.push(cb); }, | |||
| 595 | - | removeListener: function(cb) { var idx = listeners.indexOf(cb); if (idx !== -1) listeners.splice(idx, 1); }, | |||
| 596 | - | addEventListener: function(type, cb) { if (type === 'change' && typeof cb === 'function') listeners.push(cb); }, | |||
| 597 | - | removeEventListener: function(type, cb) { | |||
| 598 | - | if (type === 'change') { var idx = listeners.indexOf(cb); if (idx !== -1) listeners.splice(idx, 1); } | |||
| 599 | - | }, | |||
| 600 | - | dispatchEvent: function(event) { | |||
| 601 | - | for (var i = 0; i < listeners.length; i++) { try { listeners[i](event); } catch (e) {} } | |||
| 602 | - | if (onchangeHandler) { try { onchangeHandler(event); } catch (e) {} } | |||
| 603 | - | return true; | |||
| 604 | - | } | |||
| 605 | - | }; | |||
| 606 | - | } | |||
| 607 | - | ||||
| 608 | - | window.matchMedia = function(query) { | |||
| 609 | - | if (isColorSchemeQuery(query, 'dark')) return createFakeMediaQueryList(query, prefersDark); | |||
| 610 | - | if (isColorSchemeQuery(query, 'light')) return createFakeMediaQueryList(query, !prefersDark); | |||
| 611 | - | return originalMatchMedia(query); | |||
| 612 | - | }; | |||
| 613 | - | })();`, prefersDark) | |||
| 614 | - | ||||
| 615 | - | wv.RunJavaScript(context.Background(), script) | |||
| 616 | } | 565 | } | ||
| 617 | 566 | ||||
| 618 | // injectMessageBridgeShim injects the window.dumber.postMessage JS client shim | 567 | // injectMessageBridgeShim injects the window.dumber.postMessage JS client shim | ||
| @@ -622,14 +571,3 @@ func (ci *contentInjector) injectDarkModeScript(wv *WebView, prefersDark bool) { | |||||
| 622 | func (ci *contentInjector) injectMessageBridgeShim(wv *WebView) { | 571 | func (ci *contentInjector) injectMessageBridgeShim(wv *WebView) { | ||
| 623 | wv.RunJavaScript(context.Background(), MessageBridgeJS) | 572 | wv.RunJavaScript(context.Background(), MessageBridgeJS) | ||
| 624 | } | 573 | } | ||
| 625 | - | ||||
| 626 | - | // escapeForJSString escapes a string for use inside a JS single-quoted string literal. | |||
| 627 | - | func escapeForJSString(s string) string { | |||
| 628 | - | s = strings.ReplaceAll(s, "\\", "\\\\") | |||
| 629 | - | s = strings.ReplaceAll(s, "'", "\\'") | |||
| 630 | - | s = strings.ReplaceAll(s, "\n", "\\n") | |||
| 631 | - | s = strings.ReplaceAll(s, "\r", "\\r") | |||
| 632 | - | s = strings.ReplaceAll(s, "\u2028", "\\u2028") | |||
| 633 | - | s = strings.ReplaceAll(s, "\u2029", "\\u2029") | |||
| 634 | - | return s | |||
| 635 | - | } | |||
M internal/infrastructure/cef/input_bridge.go +2 -3
| @@ -9,6 +9,7 @@ import ( | |||
| 9 | 9 | "github.com/bnema/puregotk/v4/gio" | |
| 10 | 10 | "github.com/bnema/puregotk/v4/gtk" | |
| 11 | 11 | ||
| 12 | + | "github.com/bnema/dumber/internal/infrastructure/webutil" | |
| 12 | 13 | "github.com/bnema/dumber/internal/logging" | |
| 13 | 14 | ) | |
| 14 | 15 | ||
| @@ -464,7 +465,7 @@ func (ib *inputBridge) pasteFromClipboard() { | |||
| 464 | 465 | } | |
| 465 | 466 | ||
| 466 | 467 | // Escape the text for embedding in a JS string literal. | |
| 467 | - | escaped := escapeForJSString(text) | |
| 468 | + | escaped := webutil.EscapeForJSString(text) | |
| 468 | 469 | js := "document.execCommand('insertText',false,'" + escaped + "')" | |
| 469 | 470 | frame.ExecuteJavaScript(js, "", 0) | |
| 470 | 471 | log.Debug().Msg("cef: paste JS executed") | |
| @@ -474,8 +475,6 @@ func (ib *inputBridge) pasteFromClipboard() { | |||
| 474 | 475 | log.Debug().Msg("cef: ReadTextAsync dispatched") | |
| 475 | 476 | } | |
| 476 | 477 | ||
| 477 | - | // escapeForJSString is defined in content_injector.go — reused here. | |
| 478 | - | ||
| 479 | 478 | // --------------------------------------------------------------------------- | |
| 480 | 479 | // Translation helpers | |
| 481 | 480 | // --------------------------------------------------------------------------- | |
| @@ -9,6 +9,7 @@ import ( | |||||
| 9 | "github.com/bnema/puregotk/v4/gio" | 9 | "github.com/bnema/puregotk/v4/gio" | ||
| 10 | "github.com/bnema/puregotk/v4/gtk" | 10 | "github.com/bnema/puregotk/v4/gtk" | ||
| 11 | 11 | ||||
| 12 | + | "github.com/bnema/dumber/internal/infrastructure/webutil" | |||
| 12 | "github.com/bnema/dumber/internal/logging" | 13 | "github.com/bnema/dumber/internal/logging" | ||
| 13 | ) | 14 | ) | ||
| 14 | 15 | ||||
| @@ -464,7 +465,7 @@ func (ib *inputBridge) pasteFromClipboard() { | |||||
| 464 | } | 465 | } | ||
| 465 | 466 | ||||
| 466 | // Escape the text for embedding in a JS string literal. | 467 | // Escape the text for embedding in a JS string literal. | ||
| 467 | - | escaped := escapeForJSString(text) | 468 | + | escaped := webutil.EscapeForJSString(text) |
| 468 | js := "document.execCommand('insertText',false,'" + escaped + "')" | 469 | js := "document.execCommand('insertText',false,'" + escaped + "')" | ||
| 469 | frame.ExecuteJavaScript(js, "", 0) | 470 | frame.ExecuteJavaScript(js, "", 0) | ||
| 470 | log.Debug().Msg("cef: paste JS executed") | 471 | log.Debug().Msg("cef: paste JS executed") | ||
| @@ -474,8 +475,6 @@ func (ib *inputBridge) pasteFromClipboard() { | |||||
| 474 | log.Debug().Msg("cef: ReadTextAsync dispatched") | 475 | log.Debug().Msg("cef: ReadTextAsync dispatched") | ||
| 475 | } | 476 | } | ||
| 476 | 477 | ||||
| 477 | - | // escapeForJSString is defined in content_injector.go — reused here. | |||
| 478 | - | ||||
| 479 | // --------------------------------------------------------------------------- | 478 | // --------------------------------------------------------------------------- | ||
| 480 | // Translation helpers | 479 | // Translation helpers | ||
| 481 | // --------------------------------------------------------------------------- | 480 | // --------------------------------------------------------------------------- | ||
M internal/infrastructure/webkit/scheme_handler.go +4 -175
| @@ -5,9 +5,7 @@ import ( | |||
| 5 | 5 | "embed" | |
| 6 | 6 | "encoding/json" | |
| 7 | 7 | "fmt" | |
| 8 | - | "html" | |
| 9 | 8 | "io/fs" | |
| 10 | - | "mime" | |
| 11 | 9 | "net/http" | |
| 12 | 10 | "net/url" | |
| 13 | 11 | "path/filepath" | |
| @@ -17,6 +15,7 @@ import ( | |||
| 17 | 15 | "github.com/bnema/dumber/internal/application/port" | |
| 18 | 16 | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 19 | 17 | "github.com/bnema/dumber/internal/infrastructure/env" | |
| 18 | + | "github.com/bnema/dumber/internal/infrastructure/webutil" | |
| 20 | 19 | "github.com/bnema/dumber/internal/logging" | |
| 21 | 20 | "github.com/bnema/puregotk-webkit/soup" | |
| 22 | 21 | "github.com/bnema/puregotk-webkit/webkit" | |
| @@ -155,131 +154,11 @@ func crashOriginalURI(requestURI string) string { | |||
| 155 | 154 | } | |
| 156 | 155 | ||
| 157 | 156 | func sanitizeCrashPageOriginalURI(originalURI string) string { | |
| 158 | - | originalURI = strings.TrimSpace(originalURI) | |
| 159 | - | if originalURI == "" { | |
| 160 | - | return "" | |
| 161 | - | } | |
| 162 | - | parsed, err := url.Parse(originalURI) | |
| 163 | - | if err != nil { | |
| 164 | - | return "" | |
| 165 | - | } | |
| 166 | - | switch strings.ToLower(parsed.Scheme) { | |
| 167 | - | case "http", "https": | |
| 168 | - | if parsed.Host == "" { | |
| 169 | - | return "" | |
| 170 | - | } | |
| 171 | - | return parsed.String() | |
| 172 | - | case "dumb": | |
| 173 | - | if parsed.Host == "" && parsed.Opaque == "" { | |
| 174 | - | return "" | |
| 175 | - | } | |
| 176 | - | return parsed.String() | |
| 177 | - | default: | |
| 178 | - | return "" | |
| 179 | - | } | |
| 157 | + | return webutil.SanitizeCrashPageOriginalURI(originalURI) | |
| 180 | 158 | } | |
| 181 | 159 | ||
| 182 | 160 | func buildCrashPageHTML(originalURI string) string { | |
| 183 | - | escapedURI := html.EscapeString(originalURI) | |
| 184 | - | return fmt.Sprintf(`<!DOCTYPE html> | |
| 185 | - | <html lang="en"> | |
| 186 | - | <head> | |
| 187 | - | <meta charset="utf-8"> | |
| 188 | - | <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| 189 | - | <title>Renderer crashed</title> | |
| 190 | - | <style> | |
| 191 | - | :root { | |
| 192 | - | color-scheme: dark; | |
| 193 | - | font-family: "IBM Plex Sans", "Segoe UI", sans-serif; | |
| 194 | - | } | |
| 195 | - | body { | |
| 196 | - | margin: 0; | |
| 197 | - | min-height: 100vh; | |
| 198 | - | display: flex; | |
| 199 | - | align-items: center; | |
| 200 | - | justify-content: center; | |
| 201 | - | background: radial-gradient(circle at top, #253447, #101622 55%%); | |
| 202 | - | color: #f2f6fa; | |
| 203 | - | padding: 24px; | |
| 204 | - | } | |
| 205 | - | .card { | |
| 206 | - | width: min(640px, 100%%); | |
| 207 | - | background: rgba(10, 16, 26, 0.86); | |
| 208 | - | border: 1px solid rgba(144, 173, 205, 0.35); | |
| 209 | - | border-radius: 16px; | |
| 210 | - | box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45); | |
| 211 | - | padding: 28px; | |
| 212 | - | } | |
| 213 | - | h1 { | |
| 214 | - | margin: 0 0 12px; | |
| 215 | - | font-size: 1.8rem; | |
| 216 | - | } | |
| 217 | - | p { | |
| 218 | - | margin: 0 0 16px; | |
| 219 | - | line-height: 1.5; | |
| 220 | - | color: #c8d6e6; | |
| 221 | - | } | |
| 222 | - | .url { | |
| 223 | - | margin: 0 0 20px; | |
| 224 | - | padding: 12px; | |
| 225 | - | border-radius: 10px; | |
| 226 | - | background: rgba(26, 38, 56, 0.85); | |
| 227 | - | border: 1px solid rgba(139, 167, 194, 0.28); | |
| 228 | - | font-family: "IBM Plex Mono", "Fira Code", monospace; | |
| 229 | - | font-size: 0.92rem; | |
| 230 | - | overflow-wrap: anywhere; | |
| 231 | - | } | |
| 232 | - | .actions { | |
| 233 | - | display: flex; | |
| 234 | - | gap: 12px; | |
| 235 | - | flex-wrap: wrap; | |
| 236 | - | } | |
| 237 | - | button { | |
| 238 | - | border: 0; | |
| 239 | - | border-radius: 10px; | |
| 240 | - | padding: 10px 16px; | |
| 241 | - | cursor: pointer; | |
| 242 | - | font-size: 0.95rem; | |
| 243 | - | font-weight: 600; | |
| 244 | - | } | |
| 245 | - | .primary { | |
| 246 | - | background: #4dd0e1; | |
| 247 | - | color: #061018; | |
| 248 | - | } | |
| 249 | - | .secondary { | |
| 250 | - | background: #233346; | |
| 251 | - | color: #d6e5f5; | |
| 252 | - | } | |
| 253 | - | </style> | |
| 254 | - | </head> | |
| 255 | - | <body> | |
| 256 | - | <div class="card"> | |
| 257 | - | <h1>Renderer process ended</h1> | |
| 258 | - | <p>The current page was interrupted. You can reload it to continue browsing.</p> | |
| 259 | - | <div class="url">%s</div> | |
| 260 | - | <div class="actions"> | |
| 261 | - | <button class="primary" id="reload-btn" data-target="%s">Reload page</button> | |
| 262 | - | <button class="secondary" id="stay-btn">Stay on this page</button> | |
| 263 | - | </div> | |
| 264 | - | </div> | |
| 265 | - | <script> | |
| 266 | - | const reloadButton = document.getElementById('reload-btn'); | |
| 267 | - | const targetUrl = (reloadButton.getAttribute('data-target') || '').trim(); | |
| 268 | - | reloadButton.addEventListener('click', function() { | |
| 269 | - | if (targetUrl) { | |
| 270 | - | window.location.href = targetUrl; | |
| 271 | - | return; | |
| 272 | - | } | |
| 273 | - | window.location.reload(); | |
| 274 | - | }); | |
| 275 | - | // "Stay on this page" keeps the crash page visible without navigating away. | |
| 276 | - | document.getElementById('stay-btn').addEventListener('click', function() { | |
| 277 | - | this.disabled = true; | |
| 278 | - | this.textContent = 'Staying on page'; | |
| 279 | - | }); | |
| 280 | - | </script> | |
| 281 | - | </body> | |
| 282 | - | </html>`, escapedURI, escapedURI) | |
| 161 | + | return webutil.BuildCrashPageHTML(originalURI) | |
| 283 | 162 | } | |
| 284 | 163 | ||
| 285 | 164 | func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeResponse { | |
| @@ -420,7 +299,7 @@ func (h *DumbSchemeHandler) handleAsset(u *url.URL) *SchemeResponse { | |||
| 420 | 299 | return nil | |
| 421 | 300 | } | |
| 422 | 301 | ||
| 423 | - | contentType := h.getMimeType(relPath) | |
| 302 | + | contentType := webutil.GetMimeType(relPath) | |
| 424 | 303 | h.logger.Debug(). | |
| 425 | 304 | Str("path", fullPath). | |
| 426 | 305 | Str("content_type", contentType). | |
| @@ -468,56 +347,6 @@ func resolveAssetPath(u *url.URL) (string, bool) { | |||
| 468 | 347 | } | |
| 469 | 348 | } | |
| 470 | 349 | ||
| 471 | - | // getMimeType determines the MIME type for a given file path. | |
| 472 | - | func (h *DumbSchemeHandler) getMimeType(filename string) string { | |
| 473 | - | if h == nil { | |
| 474 | - | return "application/octet-stream" | |
| 475 | - | } | |
| 476 | - | ext := strings.ToLower(filepath.Ext(filename)) | |
| 477 | - | ||
| 478 | - | // Try standard mime type first | |
| 479 | - | mt := mime.TypeByExtension(ext) | |
| 480 | - | if mt != "" { | |
| 481 | - | return mt | |
| 482 | - | } | |
| 483 | - | ||
| 484 | - | // Fallbacks for common web assets | |
| 485 | - | switch ext { | |
| 486 | - | case ".js", ".mjs": | |
| 487 | - | return "application/javascript" | |
| 488 | - | case ".css": | |
| 489 | - | return "text/css" | |
| 490 | - | case ".svg": | |
| 491 | - | return "image/svg+xml" | |
| 492 | - | case ".ico": | |
| 493 | - | return "image/x-icon" | |
| 494 | - | case ".woff": | |
| 495 | - | return "font/woff" | |
| 496 | - | case ".woff2": | |
| 497 | - | return "font/woff2" | |
| 498 | - | case ".ttf": | |
| 499 | - | return "font/ttf" | |
| 500 | - | case ".otf": | |
| 501 | - | return "font/otf" | |
| 502 | - | case ".png": | |
| 503 | - | return "image/png" | |
| 504 | - | case ".jpg", ".jpeg": | |
| 505 | - | return "image/jpeg" | |
| 506 | - | case ".gif": | |
| 507 | - | return "image/gif" | |
| 508 | - | case ".webp": | |
| 509 | - | return "image/webp" | |
| 510 | - | case ".json": | |
| 511 | - | return "application/json" | |
| 512 | - | case ".xml": | |
| 513 | - | return "application/xml" | |
| 514 | - | case ".html", ".htm": | |
| 515 | - | return "text/html; charset=utf-8" | |
| 516 | - | default: | |
| 517 | - | return "text/plain" | |
| 518 | - | } | |
| 519 | - | } | |
| 520 | - | ||
| 521 | 350 | // sendResponse sends the response back to WebKit. | |
| 522 | 351 | func (h *DumbSchemeHandler) sendResponse(req *webkit.URISchemeRequest, response *SchemeResponse) { | |
| 523 | 352 | if response == nil { | |
| @@ -5,9 +5,7 @@ import ( | |||||
| 5 | "embed" | 5 | "embed" | ||
| 6 | "encoding/json" | 6 | "encoding/json" | ||
| 7 | "fmt" | 7 | "fmt" | ||
| 8 | - | "html" | |||
| 9 | "io/fs" | 8 | "io/fs" | ||
| 10 | - | "mime" | |||
| 11 | "net/http" | 9 | "net/http" | ||
| 12 | "net/url" | 10 | "net/url" | ||
| 13 | "path/filepath" | 11 | "path/filepath" | ||
| @@ -17,6 +15,7 @@ import ( | |||||
| 17 | "github.com/bnema/dumber/internal/application/port" | 15 | "github.com/bnema/dumber/internal/application/port" | ||
| 18 | "github.com/bnema/dumber/internal/infrastructure/config" | 16 | "github.com/bnema/dumber/internal/infrastructure/config" | ||
| 19 | "github.com/bnema/dumber/internal/infrastructure/env" | 17 | "github.com/bnema/dumber/internal/infrastructure/env" | ||
| 18 | + | "github.com/bnema/dumber/internal/infrastructure/webutil" | |||
| 20 | "github.com/bnema/dumber/internal/logging" | 19 | "github.com/bnema/dumber/internal/logging" | ||
| 21 | "github.com/bnema/puregotk-webkit/soup" | 20 | "github.com/bnema/puregotk-webkit/soup" | ||
| 22 | "github.com/bnema/puregotk-webkit/webkit" | 21 | "github.com/bnema/puregotk-webkit/webkit" | ||
| @@ -155,131 +154,11 @@ func crashOriginalURI(requestURI string) string { | |||||
| 155 | } | 154 | } | ||
| 156 | 155 | ||||
| 157 | func sanitizeCrashPageOriginalURI(originalURI string) string { | 156 | func sanitizeCrashPageOriginalURI(originalURI string) string { | ||
| 158 | - | originalURI = strings.TrimSpace(originalURI) | 157 | + | return webutil.SanitizeCrashPageOriginalURI(originalURI) |
| 159 | - | if originalURI == "" { | |||
| 160 | - | return "" | |||
| 161 | - | } | |||
| 162 | - | parsed, err := url.Parse(originalURI) | |||
| 163 | - | if err != nil { | |||
| 164 | - | return "" | |||
| 165 | - | } | |||
| 166 | - | switch strings.ToLower(parsed.Scheme) { | |||
| 167 | - | case "http", "https": | |||
| 168 | - | if parsed.Host == "" { | |||
| 169 | - | return "" | |||
| 170 | - | } | |||
| 171 | - | return parsed.String() | |||
| 172 | - | case "dumb": | |||
| 173 | - | if parsed.Host == "" && parsed.Opaque == "" { | |||
| 174 | - | return "" | |||
| 175 | - | } | |||
| 176 | - | return parsed.String() | |||
| 177 | - | default: | |||
| 178 | - | return "" | |||
| 179 | - | } | |||
| 180 | } | 158 | } | ||
| 181 | 159 | ||||
| 182 | func buildCrashPageHTML(originalURI string) string { | 160 | func buildCrashPageHTML(originalURI string) string { | ||
| 183 | - | escapedURI := html.EscapeString(originalURI) | 161 | + | return webutil.BuildCrashPageHTML(originalURI) |
| 184 | - | return fmt.Sprintf(`<!DOCTYPE html> | |||
| 185 | - | <html lang="en"> | |||
| 186 | - | <head> | |||
| 187 | - | <meta charset="utf-8"> | |||
| 188 | - | <meta name="viewport" content="width=device-width, initial-scale=1"> | |||
| 189 | - | <title>Renderer crashed</title> | |||
| 190 | - | <style> | |||
| 191 | - | :root { | |||
| 192 | - | color-scheme: dark; | |||
| 193 | - | font-family: "IBM Plex Sans", "Segoe UI", sans-serif; | |||
| 194 | - | } | |||
| 195 | - | body { | |||
| 196 | - | margin: 0; | |||
| 197 | - | min-height: 100vh; | |||
| 198 | - | display: flex; | |||
| 199 | - | align-items: center; | |||
| 200 | - | justify-content: center; | |||
| 201 | - | background: radial-gradient(circle at top, #253447, #101622 55%%); | |||
| 202 | - | color: #f2f6fa; | |||
| 203 | - | padding: 24px; | |||
| 204 | - | } | |||
| 205 | - | .card { | |||
| 206 | - | width: min(640px, 100%%); | |||
| 207 | - | background: rgba(10, 16, 26, 0.86); | |||
| 208 | - | border: 1px solid rgba(144, 173, 205, 0.35); | |||
| 209 | - | border-radius: 16px; | |||
| 210 | - | box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45); | |||
| 211 | - | padding: 28px; | |||
| 212 | - | } | |||
| 213 | - | h1 { | |||
| 214 | - | margin: 0 0 12px; | |||
| 215 | - | font-size: 1.8rem; | |||
| 216 | - | } | |||
| 217 | - | p { | |||
| 218 | - | margin: 0 0 16px; | |||
| 219 | - | line-height: 1.5; | |||
| 220 | - | color: #c8d6e6; | |||
| 221 | - | } | |||
| 222 | - | .url { | |||
| 223 | - | margin: 0 0 20px; | |||
| 224 | - | padding: 12px; | |||
| 225 | - | border-radius: 10px; | |||
| 226 | - | background: rgba(26, 38, 56, 0.85); | |||
| 227 | - | border: 1px solid rgba(139, 167, 194, 0.28); | |||
| 228 | - | font-family: "IBM Plex Mono", "Fira Code", monospace; | |||
| 229 | - | font-size: 0.92rem; | |||
| 230 | - | overflow-wrap: anywhere; | |||
| 231 | - | } | |||
| 232 | - | .actions { | |||
| 233 | - | display: flex; | |||
| 234 | - | gap: 12px; | |||
| 235 | - | flex-wrap: wrap; | |||
| 236 | - | } | |||
| 237 | - | button { | |||
| 238 | - | border: 0; | |||
| 239 | - | border-radius: 10px; | |||
| 240 | - | padding: 10px 16px; | |||
| 241 | - | cursor: pointer; | |||
| 242 | - | font-size: 0.95rem; | |||
| 243 | - | font-weight: 600; | |||
| 244 | - | } | |||
| 245 | - | .primary { | |||
| 246 | - | background: #4dd0e1; | |||
| 247 | - | color: #061018; | |||
| 248 | - | } | |||
| 249 | - | .secondary { | |||
| 250 | - | background: #233346; | |||
| 251 | - | color: #d6e5f5; | |||
| 252 | - | } | |||
| 253 | - | </style> | |||
| 254 | - | </head> | |||
| 255 | - | <body> | |||
| 256 | - | <div class="card"> | |||
| 257 | - | <h1>Renderer process ended</h1> | |||
| 258 | - | <p>The current page was interrupted. You can reload it to continue browsing.</p> | |||
| 259 | - | <div class="url">%s</div> | |||
| 260 | - | <div class="actions"> | |||
| 261 | - | <button class="primary" id="reload-btn" data-target="%s">Reload page</button> | |||
| 262 | - | <button class="secondary" id="stay-btn">Stay on this page</button> | |||
| 263 | - | </div> | |||
| 264 | - | </div> | |||
| 265 | - | <script> | |||
| 266 | - | const reloadButton = document.getElementById('reload-btn'); | |||
| 267 | - | const targetUrl = (reloadButton.getAttribute('data-target') || '').trim(); | |||
| 268 | - | reloadButton.addEventListener('click', function() { | |||
| 269 | - | if (targetUrl) { | |||
| 270 | - | window.location.href = targetUrl; | |||
| 271 | - | return; | |||
| 272 | - | } | |||
| 273 | - | window.location.reload(); | |||
| 274 | - | }); | |||
| 275 | - | // "Stay on this page" keeps the crash page visible without navigating away. | |||
| 276 | - | document.getElementById('stay-btn').addEventListener('click', function() { | |||
| 277 | - | this.disabled = true; | |||
| 278 | - | this.textContent = 'Staying on page'; | |||
| 279 | - | }); | |||
| 280 | - | </script> | |||
| 281 | - | </body> | |||
| 282 | - | </html>`, escapedURI, escapedURI) | |||
| 283 | } | 162 | } | ||
| 284 | 163 | ||||
| 285 | func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeResponse { | 164 | func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeResponse { | ||
| @@ -420,7 +299,7 @@ func (h *DumbSchemeHandler) handleAsset(u *url.URL) *SchemeResponse { | |||||
| 420 | return nil | 299 | return nil | ||
| 421 | } | 300 | } | ||
| 422 | 301 | ||||
| 423 | - | contentType := h.getMimeType(relPath) | 302 | + | contentType := webutil.GetMimeType(relPath) |
| 424 | h.logger.Debug(). | 303 | h.logger.Debug(). | ||
| 425 | Str("path", fullPath). | 304 | Str("path", fullPath). | ||
| 426 | Str("content_type", contentType). | 305 | Str("content_type", contentType). | ||
| @@ -468,56 +347,6 @@ func resolveAssetPath(u *url.URL) (string, bool) { | |||||
| 468 | } | 347 | } | ||
| 469 | } | 348 | } | ||
| 470 | 349 | ||||
| 471 | - | // getMimeType determines the MIME type for a given file path. | |||
| 472 | - | func (h *DumbSchemeHandler) getMimeType(filename string) string { | |||
| 473 | - | if h == nil { | |||
| 474 | - | return "application/octet-stream" | |||
| 475 | - | } | |||
| 476 | - | ext := strings.ToLower(filepath.Ext(filename)) | |||
| 477 | - | ||||
| 478 | - | // Try standard mime type first | |||
| 479 | - | mt := mime.TypeByExtension(ext) | |||
| 480 | - | if mt != "" { | |||
| 481 | - | return mt | |||
| 482 | - | } | |||
| 483 | - | ||||
| 484 | - | // Fallbacks for common web assets | |||
| 485 | - | switch ext { | |||
| 486 | - | case ".js", ".mjs": | |||
| 487 | - | return "application/javascript" | |||
| 488 | - | case ".css": | |||
| 489 | - | return "text/css" | |||
| 490 | - | case ".svg": | |||
| 491 | - | return "image/svg+xml" | |||
| 492 | - | case ".ico": | |||
| 493 | - | return "image/x-icon" | |||
| 494 | - | case ".woff": | |||
| 495 | - | return "font/woff" | |||
| 496 | - | case ".woff2": | |||
| 497 | - | return "font/woff2" | |||
| 498 | - | case ".ttf": | |||
| 499 | - | return "font/ttf" | |||
| 500 | - | case ".otf": | |||
| 501 | - | return "font/otf" | |||
| 502 | - | case ".png": | |||
| 503 | - | return "image/png" | |||
| 504 | - | case ".jpg", ".jpeg": | |||
| 505 | - | return "image/jpeg" | |||
| 506 | - | case ".gif": | |||
| 507 | - | return "image/gif" | |||
| 508 | - | case ".webp": | |||
| 509 | - | return "image/webp" | |||
| 510 | - | case ".json": | |||
| 511 | - | return "application/json" | |||
| 512 | - | case ".xml": | |||
| 513 | - | return "application/xml" | |||
| 514 | - | case ".html", ".htm": | |||
| 515 | - | return "text/html; charset=utf-8" | |||
| 516 | - | default: | |||
| 517 | - | return "text/plain" | |||
| 518 | - | } | |||
| 519 | - | } | |||
| 520 | - | ||||
| 521 | // sendResponse sends the response back to WebKit. | 350 | // sendResponse sends the response back to WebKit. | ||
| 522 | func (h *DumbSchemeHandler) sendResponse(req *webkit.URISchemeRequest, response *SchemeResponse) { | 351 | func (h *DumbSchemeHandler) sendResponse(req *webkit.URISchemeRequest, response *SchemeResponse) { | ||
| 523 | if response == nil { | 352 | if response == nil { | ||