//! The tab bridge: the script every tab webview gets as a main-frame initialization script //! ([`script`]). It reports the tab's own URL, title, favicon and focus to the shell (`tab_report`) //! and, when the app enables them, forwards browser shortcuts pressed in the page (`tab_shortcut`, //! [`crate::tabs::valid_shortcut`]) and link drags (`tab_link_drag`, [`crate::tabs::link_drag`]). //! The app's `tab-chrome` capability grants exactly the commands it calls, about the tab itself. /// The bridge source (`tab-bridge.js` next to this crate's `Cargo.toml`), all features on. pub const SOURCE: &str = include_str!("../tab-bridge.js"); const DEFAULT_OPTIONS: &str = "const options = { shortcuts: true, linkDrags: true };"; /// What the bridge forwards besides the tab report. Turn a feature off when the app's shell does /// not handle it: a forwarded shortcut is taken from the page (`preventDefault`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct BridgeOptions { /// Browser shortcuts (new/close/reopen tab, reload, back/forward, split, palette, the app menu, zoom, deck keys). pub shortcuts: bool, /// Link drags out of (or into) the page, for the shell's split/tab drop zones. pub link_drags: bool, } impl BridgeOptions { pub const ALL: Self = Self { shortcuts: true, link_drags: true }; /// Only `tab_report` (URL, title, favicon, focus). pub const REPORT_ONLY: Self = Self { shortcuts: false, link_drags: false }; } /// The bridge script with `options`. pub fn script(options: BridgeOptions) -> String { let set = format!("const options = {{ shortcuts: {}, linkDrags: {} }};", options.shortcuts, options.link_drags); SOURCE.replacen(DEFAULT_OPTIONS, &set, 1) } #[cfg(test)] mod tests { use super::*; #[test] fn options_are_set_in_the_one_options_line() { assert_eq!(SOURCE.matches(DEFAULT_OPTIONS).count(), 1); assert_eq!(script(BridgeOptions::ALL), SOURCE); let report = script(BridgeOptions::REPORT_ONLY); assert!(report.contains("const options = { shortcuts: false, linkDrags: false };")); assert!(!report.contains(DEFAULT_OPTIONS)); assert!(report.contains("if (options.shortcuts) addEventListener('keydown'")); assert!(report.contains("if (options.linkDrags) {")); } #[test] fn bridge_calls_only_the_tab_chrome_commands() { let mut commands: Vec<&str> = SOURCE.match_indices("invoke('").map(|(at, _)| SOURCE[at + 8..].split('\'').next().unwrap()).collect(); commands.sort(); commands.dedup(); assert_eq!(commands, ["tab_link_drag", "tab_report", "tab_shortcut"]); assert!(SOURCE.contains("if (window.top !== window || window.__wbTabBridge) return;"), "top frame only, once"); } #[test] fn forwarded_shortcuts_are_ones_the_host_accepts() { for name in ["deck-toggle", "deck-compose", "column-page-prev", "column-page-next", "column-prev", "column-next", "reopen-tab", "new-tab", "open-panel", "close-tab", "focus-address", "command-palette", "prev-tab", "next-tab", "hard-reload", "reload", "split-down", "split-right", "back", "forward", "app-menu", "zoom-in", "zoom-out", "zoom-reset", "find"] { assert!(SOURCE.contains(&format!("'{name}'")), "{name} is not in the bridge"); assert!(crate::tabs::valid_shortcut(name), "{name} would be refused"); } assert!(crate::tabs::valid_shortcut("column-3")); } }