refactor(html): extract window features parser

This commit is contained in:
ldm0
2026-09-09 06:31:04 +08:00
parent e23e5deffa
commit 0baaeda002
8 changed files with 400 additions and 254 deletions
Generated
+9
View File
@@ -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"
+1
View File
@@ -76,6 +76,7 @@ members = [
"moli-geometry",
"moli-header-field",
"moli-html-input-temporal",
"moli-window-features",
"moli-time",
"moli-trace",
"moli-http-cache",
+1
View File
@@ -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")
);
}
}
+11
View File
@@ -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
+377
View File
@@ -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")
);
}
}