mirror of
https://github.com/lexmount/moli.git
synced 2026-10-03 16:00:45 +00:00
refactor(html): extract window features parser
This commit is contained in:
Generated
+9
@@ -2935,6 +2935,7 @@ dependencies = [
|
||||
"moli-webidl",
|
||||
"moli-webidl-callback",
|
||||
"moli-websocket",
|
||||
"moli-window-features",
|
||||
"moli-xpath",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
@@ -3251,6 +3252,14 @@ dependencies = [
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moli-window-features"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"stylo",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moli-wpt-compat"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -76,6 +76,7 @@ members = [
|
||||
"moli-geometry",
|
||||
"moli-header-field",
|
||||
"moli-html-input-temporal",
|
||||
"moli-window-features",
|
||||
"moli-time",
|
||||
"moli-trace",
|
||||
"moli-http-cache",
|
||||
|
||||
@@ -83,6 +83,7 @@ moli-webcrypto = { path = "../moli-webcrypto" }
|
||||
moli-webapi-declare = { path = "../moli-webapi-declare" }
|
||||
moli-webidl = { path = "../moli-webidl" }
|
||||
moli-webidl-callback = { path = "../moli-webidl-callback" }
|
||||
moli-window-features = { path = "../moli-window-features" }
|
||||
moli-websocket = { path = "../moli-websocket" }
|
||||
moli-xpath = { path = "../moli-xpath" }
|
||||
curl = { version = "0.4.49", default-features = false, features = ["brotli", "http2", "poll_7_68_0", "ssl", "static-curl"] }
|
||||
|
||||
@@ -8,7 +8,6 @@ mod obsolete;
|
||||
mod performance;
|
||||
mod service_worker;
|
||||
mod structured_clone;
|
||||
mod window_features;
|
||||
|
||||
use moli_webapi_declare::WebApiObject;
|
||||
|
||||
|
||||
@@ -20,10 +20,9 @@ use crate::{
|
||||
util::{context_host_ptr_from_global_bridge, get_private_value},
|
||||
webidl,
|
||||
};
|
||||
use moli_window_features::WindowOpenFeatures;
|
||||
use url::Url;
|
||||
|
||||
use super::window_features::WindowOpenFeatures;
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "Window dialog")]
|
||||
struct WindowDialogMessageArgs {
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) struct WindowOpenFeatures {
|
||||
x: Option<i32>,
|
||||
y: Option<i32>,
|
||||
width: Option<i32>,
|
||||
height: Option<i32>,
|
||||
menu_bar: bool,
|
||||
status_bar: bool,
|
||||
tool_bar: bool,
|
||||
scrollbars: bool,
|
||||
resizable: bool,
|
||||
is_popup: bool,
|
||||
noopener: bool,
|
||||
noreferrer: bool,
|
||||
background: bool,
|
||||
persistent: bool,
|
||||
}
|
||||
|
||||
impl Default for WindowOpenFeatures {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
x: None,
|
||||
y: None,
|
||||
width: None,
|
||||
height: None,
|
||||
menu_bar: true,
|
||||
status_bar: true,
|
||||
tool_bar: true,
|
||||
scrollbars: true,
|
||||
resizable: true,
|
||||
is_popup: false,
|
||||
noopener: false,
|
||||
noreferrer: false,
|
||||
background: false,
|
||||
persistent: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowOpenFeatures {
|
||||
pub(super) fn parse(feature_string: &str) -> Self {
|
||||
let mut features = Self::default();
|
||||
if feature_string.is_empty() {
|
||||
return features;
|
||||
}
|
||||
|
||||
let buffer = feature_string.to_ascii_lowercase();
|
||||
let bytes = buffer.as_bytes();
|
||||
let mut ui_features_were_disabled = false;
|
||||
let mut explicit_popup = None;
|
||||
let mut index = 0;
|
||||
while index < bytes.len() {
|
||||
while index < bytes.len() && is_separator(bytes[index]) {
|
||||
index += 1;
|
||||
}
|
||||
let key_begin = index;
|
||||
while index < bytes.len() && !is_separator(bytes[index]) {
|
||||
index += 1;
|
||||
}
|
||||
let key_end = index;
|
||||
|
||||
while index < bytes.len() && bytes[index] != b'=' {
|
||||
if bytes[index] == b',' || !is_separator(bytes[index]) {
|
||||
break;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
|
||||
let (value_begin, value_end) = if index < bytes.len() && is_separator(bytes[index]) {
|
||||
while index < bytes.len() && is_separator(bytes[index]) {
|
||||
if bytes[index] == b',' {
|
||||
break;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
let value_begin = index;
|
||||
while index < bytes.len() && !is_separator(bytes[index]) {
|
||||
index += 1;
|
||||
}
|
||||
(value_begin, index)
|
||||
} else {
|
||||
(index, index)
|
||||
};
|
||||
|
||||
if key_begin == key_end {
|
||||
continue;
|
||||
}
|
||||
|
||||
let key = &buffer[key_begin..key_end];
|
||||
let value = feature_value(&buffer[value_begin..value_end]);
|
||||
if !ui_features_were_disabled
|
||||
&& !matches!(key, "noopener" | "noreferrer" | "attributionsrc")
|
||||
{
|
||||
ui_features_were_disabled = true;
|
||||
features.menu_bar = false;
|
||||
features.status_bar = false;
|
||||
features.tool_bar = false;
|
||||
features.scrollbars = false;
|
||||
}
|
||||
|
||||
match key {
|
||||
"left" | "screenx" => features.x = Some(value),
|
||||
"top" | "screeny" => features.y = Some(value),
|
||||
"width" | "innerwidth" => features.width = Some(value),
|
||||
"height" | "innerheight" => features.height = Some(value),
|
||||
"popup" => explicit_popup = Some(value != 0),
|
||||
"menubar" => features.menu_bar = value != 0,
|
||||
"toolbar" | "location" => features.tool_bar |= value != 0,
|
||||
"status" => features.status_bar = value != 0,
|
||||
"scrollbars" => features.scrollbars = value != 0,
|
||||
"resizable" => features.resizable = value != 0,
|
||||
"noopener" => features.noopener = value != 0,
|
||||
"noreferrer" => features.noreferrer = value != 0,
|
||||
"background" => features.background = true,
|
||||
"persistent" => features.persistent = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if features.noreferrer {
|
||||
features.noopener = true;
|
||||
}
|
||||
features.is_popup = explicit_popup.unwrap_or(
|
||||
!features.tool_bar
|
||||
|| !features.menu_bar
|
||||
|| !features.scrollbars
|
||||
|| !features.status_bar
|
||||
|| !features.resizable,
|
||||
);
|
||||
features
|
||||
}
|
||||
|
||||
pub(super) fn suppresses_opener(&self) -> bool {
|
||||
self.noopener
|
||||
}
|
||||
|
||||
pub(super) fn enabled_feature_strings(&self) -> Vec<String> {
|
||||
let mut enabled = Vec::new();
|
||||
if let Some(x) = self.x {
|
||||
enabled.push(format!("left={x}"));
|
||||
}
|
||||
if let Some(y) = self.y {
|
||||
enabled.push(format!("top={y}"));
|
||||
}
|
||||
if let Some(width) = self.width {
|
||||
enabled.push(format!("width={width}"));
|
||||
}
|
||||
if let Some(height) = self.height {
|
||||
enabled.push(format!("height={height}"));
|
||||
}
|
||||
if !self.is_popup {
|
||||
enabled.push("menubar".to_owned());
|
||||
enabled.push("toolbar".to_owned());
|
||||
enabled.push("status".to_owned());
|
||||
enabled.push("scrollbars".to_owned());
|
||||
}
|
||||
if self.resizable {
|
||||
enabled.push("resizable".to_owned());
|
||||
}
|
||||
if self.noopener {
|
||||
enabled.push("noopener".to_owned());
|
||||
}
|
||||
if self.background {
|
||||
enabled.push("background".to_owned());
|
||||
}
|
||||
if self.persistent {
|
||||
enabled.push("persistent".to_owned());
|
||||
}
|
||||
enabled
|
||||
}
|
||||
}
|
||||
|
||||
fn is_separator(byte: u8) -> bool {
|
||||
matches!(byte, b' ' | b'\t' | b'\n' | b'\r' | b'=' | b',' | 0x0c)
|
||||
}
|
||||
|
||||
fn feature_value(value: &str) -> i32 {
|
||||
if value.is_empty() || matches!(value, "yes" | "true") {
|
||||
return 1;
|
||||
}
|
||||
|
||||
let bytes = value.as_bytes();
|
||||
let mut end = usize::from(matches!(bytes.first().copied(), Some(b'+' | b'-')));
|
||||
let digit_begin = end;
|
||||
while end < bytes.len() && bytes[end].is_ascii_digit() {
|
||||
end += 1;
|
||||
}
|
||||
if end == digit_begin {
|
||||
return 0;
|
||||
}
|
||||
value[..end].parse::<i32>().unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::WindowOpenFeatures;
|
||||
|
||||
#[test]
|
||||
fn empty_features_match_chromium_enabled_window_features() {
|
||||
let features = WindowOpenFeatures::parse("");
|
||||
assert_eq!(
|
||||
features.enabled_feature_strings(),
|
||||
["menubar", "toolbar", "status", "scrollbars", "resizable"]
|
||||
);
|
||||
assert!(!features.suppresses_opener());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimensions_aliases_and_boolean_features_match_chromium_shape() {
|
||||
let features = WindowOpenFeatures::parse(
|
||||
"screenX=12, screenY=24, innerWidth=640, innerHeight=480, \
|
||||
location=yes, status=0, scrollbars=1, resizable=no, noopener",
|
||||
);
|
||||
assert_eq!(
|
||||
features.enabled_feature_strings(),
|
||||
["left=12", "top=24", "width=640", "height=480", "noopener",]
|
||||
);
|
||||
assert!(features.suppresses_opener());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn popup_override_and_loose_integers_match_chromium_shape() {
|
||||
let features = WindowOpenFeatures::parse("width=640px,popup=0,background=0,persistent=no");
|
||||
assert_eq!(
|
||||
features.enabled_feature_strings(),
|
||||
[
|
||||
"width=640",
|
||||
"menubar",
|
||||
"toolbar",
|
||||
"status",
|
||||
"scrollbars",
|
||||
"resizable",
|
||||
"background",
|
||||
"persistent",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noreferrer_implies_noopener_and_last_value_wins() {
|
||||
assert!(WindowOpenFeatures::parse("noreferrer=0,noreferrer=1").suppresses_opener());
|
||||
assert!(!WindowOpenFeatures::parse("noreferrer=1,noreferrer=0").suppresses_opener());
|
||||
assert_eq!(
|
||||
WindowOpenFeatures::parse("noreferrer")
|
||||
.enabled_feature_strings()
|
||||
.last()
|
||||
.map(String::as_str),
|
||||
Some("noopener")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "moli-window-features"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
indexmap = "2.6"
|
||||
stylo = "0.20"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,377 @@
|
||||
//! Parsing for the HTML `window.open()` features argument.
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use style::attr::parse_integer;
|
||||
|
||||
/// The ordered map produced by tokenizing a `window.open()` features argument.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct WindowFeatures {
|
||||
values: IndexMap<String, String>,
|
||||
entries: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl WindowFeatures {
|
||||
/// Returns the normalized value of `name`, if the feature was present.
|
||||
pub fn get(&self, name: &str) -> Option<&str> {
|
||||
self.values.get(name).map(String::as_str)
|
||||
}
|
||||
|
||||
/// Returns whether the named feature is present and parses as true.
|
||||
pub fn boolean(&self, name: &str) -> bool {
|
||||
self.get(name).is_some_and(parse_boolean_feature_value)
|
||||
}
|
||||
|
||||
/// Returns whether the tokenized feature map is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.values.is_empty()
|
||||
}
|
||||
|
||||
/// Iterates over normalized feature names and values in insertion order.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
|
||||
self.values
|
||||
.iter()
|
||||
.map(|(name, value)| (name.as_str(), value.as_str()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed `window.open()` features consumed by the browser runtime.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WindowOpenFeatures {
|
||||
x: Option<i32>,
|
||||
y: Option<i32>,
|
||||
width: Option<i32>,
|
||||
height: Option<i32>,
|
||||
menu_bar: bool,
|
||||
status_bar: bool,
|
||||
tool_bar: bool,
|
||||
scrollbars: bool,
|
||||
resizable: bool,
|
||||
is_popup: bool,
|
||||
noopener: bool,
|
||||
noreferrer: bool,
|
||||
background: bool,
|
||||
persistent: bool,
|
||||
}
|
||||
|
||||
impl Default for WindowOpenFeatures {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
x: None,
|
||||
y: None,
|
||||
width: None,
|
||||
height: None,
|
||||
menu_bar: true,
|
||||
status_bar: true,
|
||||
tool_bar: true,
|
||||
scrollbars: true,
|
||||
resizable: true,
|
||||
is_popup: false,
|
||||
noopener: false,
|
||||
noreferrer: false,
|
||||
background: false,
|
||||
persistent: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowOpenFeatures {
|
||||
/// Parses the HTML `window.open()` features argument.
|
||||
pub fn parse(feature_string: &str) -> Self {
|
||||
let tokenized = tokenize_window_features(feature_string);
|
||||
let mut features = Self::default();
|
||||
if tokenized.entries.is_empty() {
|
||||
return features;
|
||||
}
|
||||
|
||||
let mut ui_features_were_disabled = false;
|
||||
let mut explicit_popup = None;
|
||||
for (name, value) in &tokenized.entries {
|
||||
let value = feature_integer_value(value);
|
||||
if !ui_features_were_disabled
|
||||
&& !matches!(name.as_str(), "noopener" | "noreferrer" | "attributionsrc")
|
||||
{
|
||||
ui_features_were_disabled = true;
|
||||
features.menu_bar = false;
|
||||
features.status_bar = false;
|
||||
features.tool_bar = false;
|
||||
features.scrollbars = false;
|
||||
}
|
||||
|
||||
match name.as_str() {
|
||||
"left" => features.x = Some(value),
|
||||
"top" => features.y = Some(value),
|
||||
"width" => features.width = Some(value),
|
||||
"height" => features.height = Some(value),
|
||||
"popup" => explicit_popup = Some(value != 0),
|
||||
"menubar" => features.menu_bar = value != 0,
|
||||
"toolbar" | "location" => features.tool_bar |= value != 0,
|
||||
"status" => features.status_bar = value != 0,
|
||||
"scrollbars" => features.scrollbars = value != 0,
|
||||
"resizable" => features.resizable = value != 0,
|
||||
"noopener" => features.noopener = value != 0,
|
||||
"noreferrer" => features.noreferrer = value != 0,
|
||||
"background" => features.background = true,
|
||||
"persistent" => features.persistent = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if features.noreferrer {
|
||||
features.noopener = true;
|
||||
}
|
||||
features.is_popup = explicit_popup.unwrap_or(
|
||||
!features.tool_bar
|
||||
|| !features.menu_bar
|
||||
|| !features.scrollbars
|
||||
|| !features.status_bar
|
||||
|| !features.resizable,
|
||||
);
|
||||
features
|
||||
}
|
||||
|
||||
/// Returns whether the parsed features suppress the opener relationship.
|
||||
pub fn suppresses_opener(&self) -> bool {
|
||||
self.noopener
|
||||
}
|
||||
|
||||
/// Returns the normalized enabled features used by the popup runtime.
|
||||
pub fn enabled_feature_strings(&self) -> Vec<String> {
|
||||
let mut enabled = Vec::new();
|
||||
if let Some(x) = self.x {
|
||||
enabled.push(format!("left={x}"));
|
||||
}
|
||||
if let Some(y) = self.y {
|
||||
enabled.push(format!("top={y}"));
|
||||
}
|
||||
if let Some(width) = self.width {
|
||||
enabled.push(format!("width={width}"));
|
||||
}
|
||||
if let Some(height) = self.height {
|
||||
enabled.push(format!("height={height}"));
|
||||
}
|
||||
if !self.is_popup {
|
||||
enabled.push("menubar".to_owned());
|
||||
enabled.push("toolbar".to_owned());
|
||||
enabled.push("status".to_owned());
|
||||
enabled.push("scrollbars".to_owned());
|
||||
}
|
||||
if self.resizable {
|
||||
enabled.push("resizable".to_owned());
|
||||
}
|
||||
if self.noopener {
|
||||
enabled.push("noopener".to_owned());
|
||||
}
|
||||
if self.background {
|
||||
enabled.push("background".to_owned());
|
||||
}
|
||||
if self.persistent {
|
||||
enabled.push("persistent".to_owned());
|
||||
}
|
||||
enabled
|
||||
}
|
||||
}
|
||||
|
||||
/// Tokenizes a `window.open()` features argument according to the HTML Standard.
|
||||
pub fn tokenize_window_features(features: &str) -> WindowFeatures {
|
||||
let mut values = IndexMap::new();
|
||||
let mut entries = Vec::new();
|
||||
let mut input = features.chars().peekable();
|
||||
|
||||
while input.peek().is_some() {
|
||||
while input.peek().is_some_and(|&c| is_feature_separator(c)) {
|
||||
input.next();
|
||||
}
|
||||
|
||||
let mut name = String::new();
|
||||
while let Some(&c) = input.peek() {
|
||||
if is_feature_separator(c) {
|
||||
break;
|
||||
}
|
||||
name.push(c.to_ascii_lowercase());
|
||||
input.next();
|
||||
}
|
||||
let name = normalize_feature_name(name);
|
||||
|
||||
while let Some(&c) = input.peek() {
|
||||
if c == '=' || c == ',' || !is_feature_separator(c) {
|
||||
break;
|
||||
}
|
||||
input.next();
|
||||
}
|
||||
|
||||
let mut value = String::new();
|
||||
if input.peek().is_some_and(|&c| is_feature_separator(c)) {
|
||||
while let Some(&c) = input.peek() {
|
||||
if c == ',' || !is_feature_separator(c) {
|
||||
break;
|
||||
}
|
||||
input.next();
|
||||
}
|
||||
while let Some(&c) = input.peek() {
|
||||
if is_feature_separator(c) {
|
||||
break;
|
||||
}
|
||||
value.push(c.to_ascii_lowercase());
|
||||
input.next();
|
||||
}
|
||||
}
|
||||
|
||||
if !name.is_empty() {
|
||||
entries.push((name.clone(), value.clone()));
|
||||
values.insert(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
WindowFeatures { values, entries }
|
||||
}
|
||||
|
||||
/// Parses the value of a boolean window feature according to the HTML Standard.
|
||||
pub fn parse_boolean_feature_value(value: &str) -> bool {
|
||||
feature_integer_value(value) != 0
|
||||
}
|
||||
|
||||
fn feature_integer_value(value: &str) -> i32 {
|
||||
if value.is_empty() || matches!(value, "yes" | "true") {
|
||||
return 1;
|
||||
}
|
||||
parse_integer(value.chars()).unwrap_or(0)
|
||||
}
|
||||
|
||||
fn is_feature_separator(c: char) -> bool {
|
||||
matches!(c, ' ' | '\t' | '\n' | '\r' | '\u{000c}' | '=' | ',')
|
||||
}
|
||||
|
||||
fn normalize_feature_name(name: String) -> String {
|
||||
match name.as_str() {
|
||||
"screenx" => "left".to_owned(),
|
||||
"screeny" => "top".to_owned(),
|
||||
"innerwidth" => "width".to_owned(),
|
||||
"innerheight" => "height".to_owned(),
|
||||
_ => name,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{WindowOpenFeatures, parse_boolean_feature_value, tokenize_window_features};
|
||||
|
||||
#[test]
|
||||
fn tokenizes_names_values_and_ascii_case() {
|
||||
let features = tokenize_window_features(
|
||||
" NoOpEnEr = YES, popup=true,custom=value,feature-without-value ",
|
||||
);
|
||||
|
||||
assert_eq!(features.get("noopener"), Some("yes"));
|
||||
assert_eq!(features.get("popup"), Some("true"));
|
||||
assert_eq!(features.get("custom"), Some("value"));
|
||||
assert_eq!(features.get("feature-without-value"), Some(""));
|
||||
assert_eq!(features.get("NoOpEnEr"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalizes_legacy_geometry_aliases() {
|
||||
let features =
|
||||
tokenize_window_features("screenX=1,screenY=2,innerWidth=3,innerHeight=4,left=5");
|
||||
|
||||
assert_eq!(features.get("left"), Some("5"));
|
||||
assert_eq!(features.get("top"), Some("2"));
|
||||
assert_eq!(features.get("width"), Some("3"));
|
||||
assert_eq!(features.get("height"), Some("4"));
|
||||
assert_eq!(features.get("screenx"), None);
|
||||
assert_eq!(
|
||||
features.iter().collect::<Vec<_>>(),
|
||||
vec![("left", "5"), ("top", "2"), ("width", "3"), ("height", "4")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn later_duplicate_values_replace_earlier_values() {
|
||||
let features = tokenize_window_features("noopener=1,noopener=0,noreferrer=0,noreferrer");
|
||||
|
||||
assert!(!features.boolean("noopener"));
|
||||
assert!(features.boolean("noreferrer"));
|
||||
assert_eq!(features.iter().count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uses_only_html_ascii_whitespace_as_separators() {
|
||||
let features =
|
||||
tokenize_window_features("left\t=1\n,top\r=2\u{000c},width =3,noopener\u{000b}=0");
|
||||
|
||||
assert_eq!(features.get("left"), Some("1"));
|
||||
assert_eq!(features.get("top"), Some("2"));
|
||||
assert_eq!(features.get("width"), Some("3"));
|
||||
assert_eq!(features.get("noopener"), None);
|
||||
assert_eq!(features.get("noopener\u{000b}"), Some("0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_boolean_feature_values_with_html_integer_rules() {
|
||||
for value in ["", "yes", "true", "1", "+1", "-1", "1.5", "-1.5"] {
|
||||
assert!(parse_boolean_feature_value(value), "{value:?}");
|
||||
}
|
||||
for value in ["no", "false", "0", "+0", "-0", "0.5", "error", "+"] {
|
||||
assert!(!parse_boolean_feature_value(value), "{value:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_and_separator_only_inputs_produce_empty_maps() {
|
||||
assert!(tokenize_window_features("").is_empty());
|
||||
assert!(tokenize_window_features(" \t\n\r\u{000c}=,,").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_features_match_chromium_enabled_window_features() {
|
||||
let features = WindowOpenFeatures::parse("");
|
||||
assert_eq!(
|
||||
features.enabled_feature_strings(),
|
||||
["menubar", "toolbar", "status", "scrollbars", "resizable"]
|
||||
);
|
||||
assert!(!features.suppresses_opener());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimensions_aliases_and_boolean_features_match_chromium_shape() {
|
||||
let features = WindowOpenFeatures::parse(
|
||||
"screenX=12, screenY=24, innerWidth=640, innerHeight=480, \
|
||||
location=yes, status=0, scrollbars=1, resizable=no, noopener",
|
||||
);
|
||||
assert_eq!(
|
||||
features.enabled_feature_strings(),
|
||||
["left=12", "top=24", "width=640", "height=480", "noopener",]
|
||||
);
|
||||
assert!(features.suppresses_opener());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn popup_override_and_loose_integers_match_chromium_shape() {
|
||||
let features = WindowOpenFeatures::parse("width=640px,popup=0,background=0,persistent=no");
|
||||
assert_eq!(
|
||||
features.enabled_feature_strings(),
|
||||
[
|
||||
"width=640",
|
||||
"menubar",
|
||||
"toolbar",
|
||||
"status",
|
||||
"scrollbars",
|
||||
"resizable",
|
||||
"background",
|
||||
"persistent",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noreferrer_implies_noopener_and_last_value_wins() {
|
||||
assert!(WindowOpenFeatures::parse("noreferrer=0,noreferrer=1").suppresses_opener());
|
||||
assert!(!WindowOpenFeatures::parse("noreferrer=1,noreferrer=0").suppresses_opener());
|
||||
assert_eq!(
|
||||
WindowOpenFeatures::parse("noreferrer")
|
||||
.enabled_feature_strings()
|
||||
.last()
|
||||
.map(String::as_str),
|
||||
Some("noopener")
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user