mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
script: Use time@0.3
for input elements and do conversion in a &str trait (#33355)
This changes converts all input element parsing and normalization to use `time` instead of `chrono`. `time` is used by our dependencies, so it makes sense to work toward removing the Servo dependency on chrono. In addition, parsing and normalization also moves to a trait on &str to prepare for the possibility of all script parsers moving to a separate crate that can have unit tests written against it. Code duplication is eliminated when possible and more conversion is done using integer types. These two things together mean we pass more tests now. Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
parent
687f356db9
commit
8842fe9df5
5 changed files with 528 additions and 482 deletions
|
@ -122,7 +122,7 @@ syn = { version = "2", default-features = false, features = ["clone-impls", "der
|
|||
synstructure = "0.13"
|
||||
thin-vec = "0.2.13"
|
||||
time = "0.1.41"
|
||||
time_03 = { package = "time", version = "0.3", features = ["serde"] }
|
||||
time_03 = { package = "time", version = "0.3", features = ["large-dates", "serde"] }
|
||||
to_shmem = { git = "https://github.com/servo/stylo", branch = "2024-07-16" }
|
||||
tokio = "1"
|
||||
tokio-rustls = "0.24"
|
||||
|
|
|
@ -12,13 +12,12 @@ use std::str::FromStr;
|
|||
use std::sync::LazyLock;
|
||||
use std::{fmt, ops, str};
|
||||
|
||||
use chrono::prelude::{Utc, Weekday};
|
||||
use chrono::{Datelike, TimeZone};
|
||||
use cssparser::CowRcStr;
|
||||
use html5ever::{LocalName, Namespace};
|
||||
use num_traits::Zero;
|
||||
use regex::Regex;
|
||||
use servo_atoms::Atom;
|
||||
use time_03::{Date, Month, OffsetDateTime, Time, Weekday};
|
||||
|
||||
/// Encapsulates the IDL `ByteString` type.
|
||||
#[derive(Clone, Debug, Default, Eq, JSTraceable, MallocSizeOf, PartialEq)]
|
||||
|
@ -205,6 +204,11 @@ impl DOMString {
|
|||
DOMString(s, PhantomData)
|
||||
}
|
||||
|
||||
/// Get the internal `&str` value of this [`DOMString`].
|
||||
pub fn str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Appends a given string slice onto the end of this String.
|
||||
pub fn push_str(&mut self, string: &str) {
|
||||
self.0.push_str(string)
|
||||
|
@ -245,189 +249,6 @@ impl DOMString {
|
|||
self.0.replace_range(0..first_non_whitespace, "");
|
||||
}
|
||||
|
||||
/// Validates this `DOMString` is a time string according to
|
||||
/// <https://html.spec.whatwg.org/multipage/#valid-time-string>.
|
||||
pub fn is_valid_time_string(&self) -> bool {
|
||||
enum State {
|
||||
HourHigh,
|
||||
HourLow09,
|
||||
HourLow03,
|
||||
MinuteColon,
|
||||
MinuteHigh,
|
||||
MinuteLow,
|
||||
SecondColon,
|
||||
SecondHigh,
|
||||
SecondLow,
|
||||
MilliStop,
|
||||
MilliHigh,
|
||||
MilliMiddle,
|
||||
MilliLow,
|
||||
Done,
|
||||
Error,
|
||||
}
|
||||
let next_state = |valid: bool, next: State| -> State {
|
||||
if valid {
|
||||
next
|
||||
} else {
|
||||
State::Error
|
||||
}
|
||||
};
|
||||
|
||||
let state = self.chars().fold(State::HourHigh, |state, c| {
|
||||
match state {
|
||||
// Step 1 "HH"
|
||||
State::HourHigh => match c {
|
||||
'0' | '1' => State::HourLow09,
|
||||
'2' => State::HourLow03,
|
||||
_ => State::Error,
|
||||
},
|
||||
State::HourLow09 => next_state(c.is_ascii_digit(), State::MinuteColon),
|
||||
State::HourLow03 => next_state(c.is_digit(4), State::MinuteColon),
|
||||
|
||||
// Step 2 ":"
|
||||
State::MinuteColon => next_state(c == ':', State::MinuteHigh),
|
||||
|
||||
// Step 3 "mm"
|
||||
State::MinuteHigh => next_state(c.is_digit(6), State::MinuteLow),
|
||||
State::MinuteLow => next_state(c.is_ascii_digit(), State::SecondColon),
|
||||
|
||||
// Step 4.1 ":"
|
||||
State::SecondColon => next_state(c == ':', State::SecondHigh),
|
||||
// Step 4.2 "ss"
|
||||
State::SecondHigh => next_state(c.is_digit(6), State::SecondLow),
|
||||
State::SecondLow => next_state(c.is_ascii_digit(), State::MilliStop),
|
||||
|
||||
// Step 4.3.1 "."
|
||||
State::MilliStop => next_state(c == '.', State::MilliHigh),
|
||||
// Step 4.3.2 "SSS"
|
||||
State::MilliHigh => next_state(c.is_ascii_digit(), State::MilliMiddle),
|
||||
State::MilliMiddle => next_state(c.is_ascii_digit(), State::MilliLow),
|
||||
State::MilliLow => next_state(c.is_ascii_digit(), State::Done),
|
||||
|
||||
_ => State::Error,
|
||||
}
|
||||
});
|
||||
|
||||
match state {
|
||||
State::Done |
|
||||
// Step 4 (optional)
|
||||
State::SecondColon |
|
||||
// Step 4.3 (optional)
|
||||
State::MilliStop |
|
||||
// Step 4.3.2 (only 1 digit required)
|
||||
State::MilliMiddle | State::MilliLow => true,
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
|
||||
/// A valid date string should be "YYYY-MM-DD"
|
||||
/// YYYY must be four or more digits, MM and DD both must be two digits
|
||||
/// <https://html.spec.whatwg.org/multipage/#valid-date-string>
|
||||
pub fn is_valid_date_string(&self) -> bool {
|
||||
self.parse_date_string().is_some()
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#parse-a-date-string>
|
||||
pub fn parse_date_string(&self) -> Option<(i32, u32, u32)> {
|
||||
let value = &self.0;
|
||||
// Step 1, 2, 3
|
||||
let (year_int, month_int, day_int) = parse_date_component(value)?;
|
||||
|
||||
// Step 4
|
||||
if value.split('-').nth(3).is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Step 5, 6
|
||||
Some((year_int, month_int, day_int))
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#parse-a-time-string>
|
||||
pub fn parse_time_string(&self) -> Option<(u32, u32, f64)> {
|
||||
let value = &self.0;
|
||||
// Step 1, 2, 3
|
||||
let (hour_int, minute_int, second_float) = parse_time_component(value)?;
|
||||
|
||||
// Step 4
|
||||
if value.split(':').nth(3).is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Step 5, 6
|
||||
Some((hour_int, minute_int, second_float))
|
||||
}
|
||||
|
||||
/// A valid month string should be "YYYY-MM"
|
||||
/// YYYY must be four or more digits, MM both must be two digits
|
||||
/// <https://html.spec.whatwg.org/multipage/#valid-month-string>
|
||||
pub fn is_valid_month_string(&self) -> bool {
|
||||
self.parse_month_string().is_some()
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#parse-a-month-string>
|
||||
pub fn parse_month_string(&self) -> Option<(i32, u32)> {
|
||||
let value = &self;
|
||||
// Step 1, 2, 3
|
||||
let (year_int, month_int) = parse_month_component(value)?;
|
||||
|
||||
// Step 4
|
||||
if value.split('-').nth(2).is_some() {
|
||||
return None;
|
||||
}
|
||||
// Step 5
|
||||
Some((year_int, month_int))
|
||||
}
|
||||
|
||||
/// A valid week string should be like {YYYY}-W{WW}, such as "2017-W52"
|
||||
/// YYYY must be four or more digits, WW both must be two digits
|
||||
/// <https://html.spec.whatwg.org/multipage/#valid-week-string>
|
||||
pub fn is_valid_week_string(&self) -> bool {
|
||||
self.parse_week_string().is_some()
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#parse-a-week-string>
|
||||
pub fn parse_week_string(&self) -> Option<(i32, u32)> {
|
||||
let value = &self.0;
|
||||
// Step 1, 2, 3
|
||||
let mut iterator = value.split('-');
|
||||
let year = iterator.next()?;
|
||||
|
||||
// Step 4
|
||||
let year_int = year.parse::<i32>().ok()?;
|
||||
if year.len() < 4 || year_int == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Step 5, 6
|
||||
let week = iterator.next()?;
|
||||
let (week_first, week_last) = week.split_at(1);
|
||||
if week_first != "W" {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Step 7
|
||||
let week_int = week_last.parse::<u32>().ok()?;
|
||||
if week_last.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Step 8
|
||||
let max_week = max_week_in_year(year_int);
|
||||
|
||||
// Step 9
|
||||
if week_int < 1 || week_int > max_week {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Step 10
|
||||
if iterator.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Step 11
|
||||
Some((year_int, week_int))
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#valid-floating-point-number>
|
||||
pub fn is_valid_floating_point_number_string(&self) -> bool {
|
||||
static RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
|
@ -473,90 +294,6 @@ impl DOMString {
|
|||
self.0 = parsed_value.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// A valid normalized local date and time string should be "{date}T{time}"
|
||||
/// where date and time are both valid, and the time string must be as short as possible
|
||||
/// <https://html.spec.whatwg.org/multipage/#valid-normalised-local-date-and-time-string>
|
||||
pub fn convert_valid_normalized_local_date_and_time_string(&mut self) -> Option<()> {
|
||||
let date = self.parse_local_date_and_time_string()?;
|
||||
if date.seconds == 0.0 {
|
||||
self.0 = format!(
|
||||
"{:04}-{:02}-{:02}T{:02}:{:02}",
|
||||
date.year, date.month, date.day, date.hour, date.minute
|
||||
);
|
||||
} else if date.seconds < 10.0 {
|
||||
// we need exactly one leading zero on the seconds,
|
||||
// whatever their total string length might be
|
||||
self.0 = format!(
|
||||
"{:04}-{:02}-{:02}T{:02}:{:02}:0{}",
|
||||
date.year, date.month, date.day, date.hour, date.minute, date.seconds
|
||||
);
|
||||
} else {
|
||||
// we need no leading zeroes on the seconds
|
||||
self.0 = format!(
|
||||
"{:04}-{:02}-{:02}T{:02}:{:02}:{}",
|
||||
date.year, date.month, date.day, date.hour, date.minute, date.seconds
|
||||
);
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#parse-a-local-date-and-time-string>
|
||||
pub(crate) fn parse_local_date_and_time_string(&self) -> Option<ParsedDate> {
|
||||
let value = &self;
|
||||
// Step 1, 2, 4
|
||||
let mut iterator = if value.contains('T') {
|
||||
value.split('T')
|
||||
} else {
|
||||
value.split(' ')
|
||||
};
|
||||
|
||||
// Step 3
|
||||
let date = iterator.next()?;
|
||||
let (year, month, day) = parse_date_component(date)?;
|
||||
|
||||
// Step 5
|
||||
let time = iterator.next()?;
|
||||
let (hour, minute, seconds) = parse_time_component(time)?;
|
||||
|
||||
// Step 6
|
||||
if iterator.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Step 7, 8, 9
|
||||
Some(ParsedDate {
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
seconds,
|
||||
})
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#valid-e-mail-address>
|
||||
pub fn is_valid_email_address_string(&self) -> bool {
|
||||
static RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(concat!(
|
||||
r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?",
|
||||
r"(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
|
||||
))
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
RE.is_match(&self.0)
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#valid-simple-colour>
|
||||
pub fn is_valid_simple_color_string(&self) -> bool {
|
||||
let mut chars = self.0.chars();
|
||||
if self.0.len() == 7 && chars.next() == Some('#') {
|
||||
chars.all(|c| c.is_ascii_hexdigit())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<str> for DOMString {
|
||||
|
@ -731,52 +468,68 @@ fn parse_date_component(value: &str) -> Option<(i32, u32, u32)> {
|
|||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#parse-a-time-component>
|
||||
fn parse_time_component(value: &str) -> Option<(u32, u32, f64)> {
|
||||
// Step 1
|
||||
fn parse_time_component(value: &str) -> Option<(u8, u8, u8, u16)> {
|
||||
// Step 1: Collect a sequence of code points that are ASCII digits from input given
|
||||
// position. If the collected sequence is not exactly two characters long, then fail.
|
||||
// Otherwise, interpret the resulting sequence as a base-ten integer. Let that number
|
||||
// be the hour.
|
||||
let mut iterator = value.split(':');
|
||||
let hour = iterator.next()?;
|
||||
if hour.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
let hour_int = hour.parse::<u32>().ok()?;
|
||||
|
||||
// Step 2
|
||||
// Step 2: If hour is not a number in the range 0 ≤ hour ≤ 23, then fail.
|
||||
let hour_int = hour.parse::<u8>().ok()?;
|
||||
if hour_int > 23 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Step 3, 4
|
||||
// Step 3: If position is beyond the end of input or if the character at position is
|
||||
// not a U+003A COLON character, then fail. Otherwise, move position forwards one
|
||||
// character.
|
||||
// Step 4: Collect a sequence of code points that are ASCII digits from input given
|
||||
// position. If the collected sequence is not exactly two characters long, then fail.
|
||||
// Otherwise, interpret the resulting sequence as a base-ten integer. Let that number
|
||||
// be the minute.
|
||||
// Step 5: If minute is not a number in the range 0 ≤ minute ≤ 59, then fail.
|
||||
let minute = iterator.next()?;
|
||||
if minute.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
let minute_int = minute.parse::<u32>().ok()?;
|
||||
|
||||
// Step 5
|
||||
let minute_int = minute.parse::<u8>().ok()?;
|
||||
if minute_int > 59 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Step 6, 7
|
||||
let second_float = match iterator.next() {
|
||||
Some(second) => {
|
||||
let mut second_iterator = second.split('.');
|
||||
if second_iterator.next()?.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
if let Some(second_last) = second_iterator.next() {
|
||||
if second_last.len() > 3 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
second.parse::<f64>().ok()?
|
||||
},
|
||||
None => 0.0,
|
||||
// Step 6, 7: Asks us to parse the seconds as a floating point number, but below this
|
||||
// is done as integral parts in order to avoid floating point precision issues.
|
||||
let Some(seconds_and_milliseconds) = iterator.next() else {
|
||||
return Some((hour_int, minute_int, 0, 0));
|
||||
};
|
||||
|
||||
// Step 8
|
||||
Some((hour_int, minute_int, second_float))
|
||||
// Parse the seconds portion.
|
||||
let mut second_iterator = seconds_and_milliseconds.split('.');
|
||||
let second = second_iterator.next()?;
|
||||
if second.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
let second_int = second.parse::<u8>().ok()?;
|
||||
|
||||
// Parse the milliseconds portion as a u16 (milliseconds can be up to 1000) and
|
||||
// make sure that it has the proper value based on how long the string is.
|
||||
let Some(millisecond) = second_iterator.next() else {
|
||||
return Some((hour_int, minute_int, second_int, 0));
|
||||
};
|
||||
let millisecond_length = millisecond.len() as u32;
|
||||
if millisecond_length > 3 {
|
||||
return None;
|
||||
}
|
||||
let millisecond_int = millisecond.parse::<u16>().ok()?;
|
||||
let millisecond_int = millisecond_int * 10_u16.pow(3 - millisecond_length);
|
||||
|
||||
// Step 8: Return hour, minute, and second (and in our case the milliseconds due to the note
|
||||
// above about floating point precision).
|
||||
Some((hour_int, minute_int, second_int, millisecond_int))
|
||||
}
|
||||
|
||||
fn max_day_in_month(year_num: i32, month_num: u32) -> Option<u32> {
|
||||
|
@ -795,15 +548,22 @@ fn max_day_in_month(year_num: i32, month_num: u32) -> Option<u32> {
|
|||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#week-number-of-the-last-day>
|
||||
///
|
||||
/// > A week-year with a number year has 53 weeks if it corresponds to either a year year
|
||||
/// > in the proleptic Gregorian calendar that has a Thursday as its first day (January
|
||||
/// > 1st), or a year year in the proleptic Gregorian calendar that has a Wednesday as its
|
||||
/// > first day (January 1st) and where year is a number divisible by 400, or a number
|
||||
/// > divisible by 4 but not by 100. All other week-years have 52 weeks.
|
||||
fn max_week_in_year(year: i32) -> u32 {
|
||||
Utc.with_ymd_and_hms(year, 1, 1, 0, 0, 0)
|
||||
.earliest()
|
||||
.map(|date_time| match date_time.weekday() {
|
||||
Weekday::Thu => 53,
|
||||
Weekday::Wed if is_leap_year(year) => 53,
|
||||
_ => 52,
|
||||
})
|
||||
.unwrap_or(52)
|
||||
let Ok(date) = Date::from_calendar_date(year, Month::January, 1) else {
|
||||
return 52;
|
||||
};
|
||||
|
||||
match OffsetDateTime::new_utc(date, Time::MIDNIGHT).weekday() {
|
||||
Weekday::Thursday => 53,
|
||||
Weekday::Wednesday if is_leap_year(year) => 53,
|
||||
_ => 52,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
@ -811,12 +571,340 @@ fn is_leap_year(year: i32) -> bool {
|
|||
year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, MallocSizeOf, PartialEq)]
|
||||
pub(crate) struct ParsedDate {
|
||||
pub year: i32,
|
||||
pub month: u32,
|
||||
pub day: u32,
|
||||
pub hour: u32,
|
||||
pub minute: u32,
|
||||
pub seconds: f64,
|
||||
pub(crate) trait ToInputValueString {
|
||||
fn to_date_string(&self) -> String;
|
||||
fn to_month_string(&self) -> String;
|
||||
fn to_week_string(&self) -> String;
|
||||
fn to_time_string(&self) -> String;
|
||||
|
||||
/// A valid normalized local date and time string should be "{date}T{time}"
|
||||
/// where date and time are both valid, and the time string must be as short as possible
|
||||
/// <https://html.spec.whatwg.org/multipage/#valid-normalised-local-date-and-time-string>
|
||||
fn to_local_date_time_string(&self) -> String;
|
||||
}
|
||||
|
||||
impl ToInputValueString for OffsetDateTime {
|
||||
fn to_date_string(&self) -> String {
|
||||
format!(
|
||||
"{:04}-{:02}-{:02}",
|
||||
self.year(),
|
||||
self.month() as u8,
|
||||
self.day()
|
||||
)
|
||||
}
|
||||
|
||||
fn to_month_string(&self) -> String {
|
||||
format!("{:04}-{:02}", self.year(), self.month() as u8)
|
||||
}
|
||||
|
||||
fn to_week_string(&self) -> String {
|
||||
// NB: The ISO week year might be different than the year of the day.
|
||||
let (year, week, _) = self.to_iso_week_date();
|
||||
format!("{:04}-W{:02}", year, week)
|
||||
}
|
||||
|
||||
fn to_time_string(&self) -> String {
|
||||
if self.second().is_zero() && self.millisecond().is_zero() {
|
||||
format!("{:02}:{:02}", self.hour(), self.minute())
|
||||
} else {
|
||||
// This needs to trim off the zero parts of the milliseconds.
|
||||
format!(
|
||||
"{:02}:{:02}:{:02}.{:03}",
|
||||
self.hour(),
|
||||
self.minute(),
|
||||
self.second(),
|
||||
self.millisecond()
|
||||
)
|
||||
.trim_end_matches(['.', '0'])
|
||||
.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
fn to_local_date_time_string(&self) -> String {
|
||||
format!("{}T{}", self.to_date_string(), self.to_time_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait FromInputValueString {
|
||||
/// <https://html.spec.whatwg.org/multipage/#parse-a-date-string>
|
||||
///
|
||||
/// Parse the date string and return an [`OffsetDateTime`] on midnight of the
|
||||
/// given date in UTC.
|
||||
///
|
||||
/// A valid date string should be "YYYY-MM-DD"
|
||||
/// YYYY must be four or more digits, MM and DD both must be two digits
|
||||
/// <https://html.spec.whatwg.org/multipage/#valid-date-string>
|
||||
fn parse_date_string(&self) -> Option<OffsetDateTime>;
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#parse-a-month-string>
|
||||
///
|
||||
/// Parse the month and return an [`OffsetDate`] on midnight of UTC of the morning of
|
||||
/// the first day of the parsed month.
|
||||
///
|
||||
/// A valid month string should be "YYYY-MM" YYYY must be four or more digits, MM both
|
||||
/// must be two digits <https://html.spec.whatwg.org/multipage/#valid-month-string>
|
||||
fn parse_month_string(&self) -> Option<OffsetDateTime>;
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#parse-a-week-string>
|
||||
///
|
||||
/// Parse the week string, returning an [`OffsetDateTime`] on the Monday of the parsed
|
||||
/// week.
|
||||
///
|
||||
/// A valid week string should be like {YYYY}-W{WW}, such as "2017-W52" YYYY must be
|
||||
/// four or more digits, WW both must be two digits
|
||||
/// <https://html.spec.whatwg.org/multipage/#valid-week-string>
|
||||
fn parse_week_string(&self) -> Option<OffsetDateTime>;
|
||||
|
||||
/// Parse this value as a time string according to
|
||||
/// <https://html.spec.whatwg.org/multipage/#valid-time-string>.
|
||||
fn parse_time_string(&self) -> Option<OffsetDateTime>;
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#parse-a-local-date-and-time-string>
|
||||
///
|
||||
/// Parse the local date and time, returning an [`OffsetDateTime`] in UTC or None.
|
||||
fn parse_local_date_time_string(&self) -> Option<OffsetDateTime>;
|
||||
|
||||
/// Validates whether or not this value is a valid date string according to
|
||||
/// <https://html.spec.whatwg.org/multipage/#valid-date-string>.
|
||||
fn is_valid_date_string(&self) -> bool {
|
||||
self.parse_date_string().is_some()
|
||||
}
|
||||
|
||||
/// Validates whether or not this value is a valid month string according to
|
||||
/// <https://html.spec.whatwg.org/multipage/#valid-month-string>.
|
||||
fn is_valid_month_string(&self) -> bool {
|
||||
self.parse_month_string().is_some()
|
||||
}
|
||||
/// Validates whether or not this value is a valid week string according to
|
||||
/// <https://html.spec.whatwg.org/multipage/#valid-week-string>.
|
||||
fn is_valid_week_string(&self) -> bool {
|
||||
self.parse_week_string().is_some()
|
||||
}
|
||||
/// Validates whether or not this value is a valid time string according to
|
||||
/// <https://html.spec.whatwg.org/multipage/#valid-time-string>.
|
||||
fn is_valid_time_string(&self) -> bool;
|
||||
|
||||
/// Validates whether or not this value is a valid local date time string according to
|
||||
/// <https://html.spec.whatwg.org/multipage/#valid-week-string>.
|
||||
fn is_valid_local_date_time_string(&self) -> bool {
|
||||
self.parse_local_date_time_string().is_some()
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#valid-simple-colour>
|
||||
fn is_valid_simple_color_string(&self) -> bool;
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#valid-e-mail-address>
|
||||
fn is_valid_email_address_string(&self) -> bool;
|
||||
}
|
||||
|
||||
impl FromInputValueString for &str {
|
||||
fn parse_date_string(&self) -> Option<OffsetDateTime> {
|
||||
// Step 1, 2, 3
|
||||
let (year_int, month_int, day_int) = parse_date_component(self)?;
|
||||
|
||||
// Step 4
|
||||
if self.split('-').nth(3).is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Step 5, 6
|
||||
let month = (month_int as u8).try_into().ok()?;
|
||||
let date = Date::from_calendar_date(year_int, month, day_int as u8).ok()?;
|
||||
Some(OffsetDateTime::new_utc(date, Time::MIDNIGHT))
|
||||
}
|
||||
|
||||
fn parse_month_string(&self) -> Option<OffsetDateTime> {
|
||||
// Step 1, 2, 3
|
||||
let (year_int, month_int) = parse_month_component(self)?;
|
||||
|
||||
// Step 4
|
||||
if self.split('-').nth(2).is_some() {
|
||||
return None;
|
||||
}
|
||||
// Step 5
|
||||
let month = (month_int as u8).try_into().ok()?;
|
||||
let date = Date::from_calendar_date(year_int, month, 1).ok()?;
|
||||
Some(OffsetDateTime::new_utc(date, Time::MIDNIGHT))
|
||||
}
|
||||
|
||||
fn parse_week_string(&self) -> Option<OffsetDateTime> {
|
||||
// Step 1, 2, 3
|
||||
let mut iterator = self.split('-');
|
||||
let year = iterator.next()?;
|
||||
|
||||
// Step 4
|
||||
let year_int = year.parse::<i32>().ok()?;
|
||||
if year.len() < 4 || year_int == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Step 5, 6
|
||||
let week = iterator.next()?;
|
||||
let (week_first, week_last) = week.split_at(1);
|
||||
if week_first != "W" {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Step 7
|
||||
let week_int = week_last.parse::<u32>().ok()?;
|
||||
if week_last.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Step 8
|
||||
let max_week = max_week_in_year(year_int);
|
||||
|
||||
// Step 9
|
||||
if week_int < 1 || week_int > max_week {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Step 10
|
||||
if iterator.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Step 11
|
||||
let date = Date::from_iso_week_date(year_int, week_int as u8, Weekday::Monday).ok()?;
|
||||
Some(OffsetDateTime::new_utc(date, Time::MIDNIGHT))
|
||||
}
|
||||
|
||||
fn parse_time_string(&self) -> Option<OffsetDateTime> {
|
||||
// Step 1, 2, 3
|
||||
let (hour, minute, second, millisecond) = parse_time_component(self)?;
|
||||
|
||||
// Step 4
|
||||
if self.split(':').nth(3).is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Step 5, 6
|
||||
let time = Time::from_hms_milli(hour, minute, second, millisecond).ok()?;
|
||||
Some(OffsetDateTime::new_utc(
|
||||
OffsetDateTime::UNIX_EPOCH.date(),
|
||||
time,
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_local_date_time_string(&self) -> Option<OffsetDateTime> {
|
||||
// Step 1, 2, 4
|
||||
let mut iterator = if self.contains('T') {
|
||||
self.split('T')
|
||||
} else {
|
||||
self.split(' ')
|
||||
};
|
||||
|
||||
// Step 3
|
||||
let date = iterator.next()?;
|
||||
let (year, month, day) = parse_date_component(date)?;
|
||||
|
||||
// Step 5
|
||||
let time = iterator.next()?;
|
||||
let (hour, minute, second, millisecond) = parse_time_component(time)?;
|
||||
|
||||
// Step 6
|
||||
if iterator.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Step 7, 8, 9
|
||||
// TODO: Is this supposed to know the locale's daylight-savings-time rules?
|
||||
let month = (month as u8).try_into().ok()?;
|
||||
let date = Date::from_calendar_date(year, month, day as u8).ok()?;
|
||||
let time = Time::from_hms_milli(hour, minute, second, millisecond).ok()?;
|
||||
Some(OffsetDateTime::new_utc(date, time))
|
||||
}
|
||||
|
||||
fn is_valid_time_string(&self) -> bool {
|
||||
enum State {
|
||||
HourHigh,
|
||||
HourLow09,
|
||||
HourLow03,
|
||||
MinuteColon,
|
||||
MinuteHigh,
|
||||
MinuteLow,
|
||||
SecondColon,
|
||||
SecondHigh,
|
||||
SecondLow,
|
||||
MilliStop,
|
||||
MilliHigh,
|
||||
MilliMiddle,
|
||||
MilliLow,
|
||||
Done,
|
||||
Error,
|
||||
}
|
||||
let next_state = |valid: bool, next: State| -> State {
|
||||
if valid {
|
||||
next
|
||||
} else {
|
||||
State::Error
|
||||
}
|
||||
};
|
||||
|
||||
let state = self.chars().fold(State::HourHigh, |state, c| {
|
||||
match state {
|
||||
// Step 1 "HH"
|
||||
State::HourHigh => match c {
|
||||
'0' | '1' => State::HourLow09,
|
||||
'2' => State::HourLow03,
|
||||
_ => State::Error,
|
||||
},
|
||||
State::HourLow09 => next_state(c.is_ascii_digit(), State::MinuteColon),
|
||||
State::HourLow03 => next_state(c.is_digit(4), State::MinuteColon),
|
||||
|
||||
// Step 2 ":"
|
||||
State::MinuteColon => next_state(c == ':', State::MinuteHigh),
|
||||
|
||||
// Step 3 "mm"
|
||||
State::MinuteHigh => next_state(c.is_digit(6), State::MinuteLow),
|
||||
State::MinuteLow => next_state(c.is_ascii_digit(), State::SecondColon),
|
||||
|
||||
// Step 4.1 ":"
|
||||
State::SecondColon => next_state(c == ':', State::SecondHigh),
|
||||
// Step 4.2 "ss"
|
||||
State::SecondHigh => next_state(c.is_digit(6), State::SecondLow),
|
||||
State::SecondLow => next_state(c.is_ascii_digit(), State::MilliStop),
|
||||
|
||||
// Step 4.3.1 "."
|
||||
State::MilliStop => next_state(c == '.', State::MilliHigh),
|
||||
// Step 4.3.2 "SSS"
|
||||
State::MilliHigh => next_state(c.is_ascii_digit(), State::MilliMiddle),
|
||||
State::MilliMiddle => next_state(c.is_ascii_digit(), State::MilliLow),
|
||||
State::MilliLow => next_state(c.is_ascii_digit(), State::Done),
|
||||
|
||||
_ => State::Error,
|
||||
}
|
||||
});
|
||||
|
||||
match state {
|
||||
State::Done |
|
||||
// Step 4 (optional)
|
||||
State::SecondColon |
|
||||
// Step 4.3 (optional)
|
||||
State::MilliStop |
|
||||
// Step 4.3.2 (only 1 digit required)
|
||||
State::MilliMiddle | State::MilliLow => true,
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid_simple_color_string(&self) -> bool {
|
||||
let mut chars = self.chars();
|
||||
if self.len() == 7 && chars.next() == Some('#') {
|
||||
chars.all(|c| c.is_ascii_hexdigit())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid_email_address_string(&self) -> bool {
|
||||
static RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(concat!(
|
||||
r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?",
|
||||
r"(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
|
||||
))
|
||||
.unwrap()
|
||||
});
|
||||
RE.is_match(&self)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,12 +4,11 @@
|
|||
|
||||
use std::borrow::Cow;
|
||||
use std::cell::Cell;
|
||||
use std::cmp::Ordering;
|
||||
use std::ops::Range;
|
||||
use std::ptr::NonNull;
|
||||
use std::{f64, ptr};
|
||||
|
||||
use chrono::naive::{NaiveDate, NaiveDateTime};
|
||||
use chrono::{DateTime, Datelike, Weekday};
|
||||
use dom_struct::dom_struct;
|
||||
use embedder_traits::{FilterPattern, InputMethodType};
|
||||
use encoding_rs::Encoding;
|
||||
|
@ -30,9 +29,11 @@ use servo_atoms::Atom;
|
|||
use style::attr::AttrValue;
|
||||
use style::str::{split_commas, str_join};
|
||||
use style_dom::ElementState;
|
||||
use time_03::{Month, OffsetDateTime, Time};
|
||||
use unicode_bidi::{bidi_class, BidiClass};
|
||||
use url::Url;
|
||||
|
||||
use super::bindings::str::{FromInputValueString, ToInputValueString};
|
||||
use crate::dom::activation::Activatable;
|
||||
use crate::dom::attr::Attr;
|
||||
use crate::dom::bindings::cell::DomRefCell;
|
||||
|
@ -808,11 +809,9 @@ impl HTMLInputElement {
|
|||
// https://html.spec.whatwg.org/multipage/#e-mail-state-(type%3Demail)%3Asuffering-from-a-type-mismatch-2
|
||||
InputType::Email => {
|
||||
if self.Multiple() {
|
||||
!split_commas(value).all(|s| {
|
||||
DOMString::from_string(s.to_string()).is_valid_email_address_string()
|
||||
})
|
||||
!split_commas(value).all(|string| string.is_valid_email_address_string())
|
||||
} else {
|
||||
!value.is_valid_email_address_string()
|
||||
!value.str().is_valid_email_address_string()
|
||||
}
|
||||
},
|
||||
// Other input types don't suffer from type mismatch
|
||||
|
@ -863,20 +862,20 @@ impl HTMLInputElement {
|
|||
false
|
||||
},
|
||||
// https://html.spec.whatwg.org/multipage/#date-state-(type%3Ddate)%3Asuffering-from-bad-input
|
||||
InputType::Date => !value.is_valid_date_string(),
|
||||
InputType::Date => !value.str().is_valid_date_string(),
|
||||
// https://html.spec.whatwg.org/multipage/#month-state-(type%3Dmonth)%3Asuffering-from-bad-input
|
||||
InputType::Month => !value.is_valid_month_string(),
|
||||
InputType::Month => !value.str().is_valid_month_string(),
|
||||
// https://html.spec.whatwg.org/multipage/#week-state-(type%3Dweek)%3Asuffering-from-bad-input
|
||||
InputType::Week => !value.is_valid_week_string(),
|
||||
InputType::Week => !value.str().is_valid_week_string(),
|
||||
// https://html.spec.whatwg.org/multipage/#time-state-(type%3Dtime)%3Asuffering-from-bad-input
|
||||
InputType::Time => !value.is_valid_time_string(),
|
||||
InputType::Time => !value.str().is_valid_time_string(),
|
||||
// https://html.spec.whatwg.org/multipage/#local-date-and-time-state-(type%3Ddatetime-local)%3Asuffering-from-bad-input
|
||||
InputType::DatetimeLocal => value.parse_local_date_and_time_string().is_none(),
|
||||
InputType::DatetimeLocal => !value.str().is_valid_local_date_time_string(),
|
||||
// https://html.spec.whatwg.org/multipage/#number-state-(type%3Dnumber)%3Asuffering-from-bad-input
|
||||
// https://html.spec.whatwg.org/multipage/#range-state-(type%3Drange)%3Asuffering-from-bad-input
|
||||
InputType::Number | InputType::Range => !value.is_valid_floating_point_number_string(),
|
||||
// https://html.spec.whatwg.org/multipage/#color-state-(type%3Dcolor)%3Asuffering-from-bad-input
|
||||
InputType::Color => !value.is_valid_simple_color_string(),
|
||||
InputType::Color => !value.str().is_valid_simple_color_string(),
|
||||
// Other input types don't suffer from bad input
|
||||
_ => false,
|
||||
}
|
||||
|
@ -1305,9 +1304,9 @@ impl HTMLInputElementMethods for HTMLInputElement {
|
|||
#[allow(unsafe_code)]
|
||||
fn GetValueAsDate(&self, cx: SafeJSContext) -> Option<NonNull<JSObject>> {
|
||||
self.convert_string_to_naive_datetime(self.Value())
|
||||
.map(|dt| unsafe {
|
||||
.map(|date_time| unsafe {
|
||||
let time = ClippedTime {
|
||||
t: dt.and_utc().timestamp_millis() as f64,
|
||||
t: (date_time - OffsetDateTime::UNIX_EPOCH).whole_milliseconds() as f64,
|
||||
};
|
||||
NonNull::new_unchecked(NewDateObject(*cx, time))
|
||||
})
|
||||
|
@ -1342,15 +1341,11 @@ impl HTMLInputElementMethods for HTMLInputElement {
|
|||
return self.SetValue(DOMString::from(""));
|
||||
}
|
||||
}
|
||||
// now we make a Rust date out of it so we can use safe code for the
|
||||
// actual conversion logic
|
||||
match milliseconds_to_datetime(msecs) {
|
||||
Ok(dt) => match self.convert_naive_datetime_to_string(dt) {
|
||||
Ok(converted) => self.SetValue(converted),
|
||||
_ => self.SetValue(DOMString::from("")),
|
||||
},
|
||||
_ => self.SetValue(DOMString::from("")),
|
||||
}
|
||||
|
||||
let Ok(date_time) = OffsetDateTime::from_unix_timestamp_nanos((msecs * 1e6) as i128) else {
|
||||
return self.SetValue(DOMString::from(""));
|
||||
};
|
||||
self.SetValue(self.convert_datetime_to_dom_string(date_time))
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-input-valueasnumber
|
||||
|
@ -1367,14 +1362,13 @@ impl HTMLInputElementMethods for HTMLInputElement {
|
|||
Err(Error::InvalidState)
|
||||
} else if value.is_nan() {
|
||||
self.SetValue(DOMString::from(""))
|
||||
} else if let Ok(converted) = self.convert_number_to_string(value) {
|
||||
} else if let Some(converted) = self.convert_number_to_string(value) {
|
||||
self.SetValue(converted)
|
||||
} else {
|
||||
// The most literal spec-compliant implementation would
|
||||
// use bignum chrono types so overflow is impossible,
|
||||
// but just setting an overflow to the empty string matches
|
||||
// Firefox's behavior.
|
||||
// (for example, try input.valueAsNumber=1e30 on a type="date" input)
|
||||
// The most literal spec-compliant implementation would use bignum types so
|
||||
// overflow is impossible, but just setting an overflow to the empty string
|
||||
// matches Firefox's behavior. For example, try input.valueAsNumber=1e30 on
|
||||
// a type="date" input.
|
||||
self.SetValue(DOMString::from(""))
|
||||
}
|
||||
}
|
||||
|
@ -1918,38 +1912,40 @@ impl HTMLInputElement {
|
|||
value.strip_leading_and_trailing_ascii_whitespace();
|
||||
},
|
||||
InputType::Date => {
|
||||
if !value.is_valid_date_string() {
|
||||
if !value.str().is_valid_date_string() {
|
||||
value.clear();
|
||||
}
|
||||
},
|
||||
InputType::Month => {
|
||||
if !value.is_valid_month_string() {
|
||||
if !value.str().is_valid_month_string() {
|
||||
value.clear();
|
||||
}
|
||||
},
|
||||
InputType::Week => {
|
||||
if !value.is_valid_week_string() {
|
||||
if !value.str().is_valid_week_string() {
|
||||
value.clear();
|
||||
}
|
||||
},
|
||||
InputType::Color => {
|
||||
if value.is_valid_simple_color_string() {
|
||||
if value.str().is_valid_simple_color_string() {
|
||||
value.make_ascii_lowercase();
|
||||
} else {
|
||||
*value = "#000000".into();
|
||||
}
|
||||
},
|
||||
InputType::Time => {
|
||||
if !value.is_valid_time_string() {
|
||||
if !value.str().is_valid_time_string() {
|
||||
value.clear();
|
||||
}
|
||||
},
|
||||
InputType::DatetimeLocal => {
|
||||
if value
|
||||
.convert_valid_normalized_local_date_and_time_string()
|
||||
.is_none()
|
||||
match value
|
||||
.str()
|
||||
.parse_local_date_time_string()
|
||||
.map(|date_time| date_time.to_local_date_time_string())
|
||||
{
|
||||
value.clear();
|
||||
Some(normalized_string) => *value = DOMString::from_string(normalized_string),
|
||||
None => value.clear(),
|
||||
}
|
||||
},
|
||||
InputType::Number => {
|
||||
|
@ -2112,44 +2108,55 @@ impl HTMLInputElement {
|
|||
}
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#concept-input-value-string-number
|
||||
/// <https://html.spec.whatwg.org/multipage/#concept-input-value-string-number>
|
||||
fn convert_string_to_number(&self, value: &DOMString) -> Option<f64> {
|
||||
match self.input_type() {
|
||||
InputType::Date => value
|
||||
.parse_date_string()
|
||||
.and_then(|(year, month, day)| NaiveDate::from_ymd_opt(year, month, day))
|
||||
.and_then(|date| date.and_hms_opt(0, 0, 0))
|
||||
.map(|time| time.and_utc().timestamp_millis() as f64),
|
||||
InputType::Month => match value.parse_month_string() {
|
||||
// This one returns number of months, not milliseconds
|
||||
// (specification requires this, presumably because number of
|
||||
// milliseconds is not consistent across months)
|
||||
// the - 1.0 is because january is 1, not 0
|
||||
Some((year, month)) => Some(((year - 1970) * 12) as f64 + (month as f64 - 1.0)),
|
||||
_ => None,
|
||||
},
|
||||
InputType::Week => value
|
||||
.parse_week_string()
|
||||
.and_then(|(year, weeknum)| NaiveDate::from_isoywd_opt(year, weeknum, Weekday::Mon))
|
||||
.and_then(|date| date.and_hms_opt(0, 0, 0))
|
||||
.map(|time| time.and_utc().timestamp_millis() as f64),
|
||||
InputType::Time => match value.parse_time_string() {
|
||||
Some((hours, minutes, seconds)) => {
|
||||
Some((seconds + 60.0 * minutes as f64 + 3600.0 * hours as f64) * 1000.0)
|
||||
},
|
||||
_ => None,
|
||||
},
|
||||
// > The algorithm to convert a string to a number, given a string input, is as
|
||||
// > follows: If parsing a date from input results in an error, then return an
|
||||
// > error; otherwise, return the number of milliseconds elapsed from midnight
|
||||
// > UTC on the morning of 1970-01-01 (the time represented by the value
|
||||
// > "1970-01-01T00:00:00.0Z") to midnight UTC on the morning of the parsed
|
||||
// > date, ignoring leap seconds.
|
||||
InputType::Date => value.str().parse_date_string().map(|date_time| {
|
||||
(date_time - OffsetDateTime::UNIX_EPOCH).whole_milliseconds() as f64
|
||||
}),
|
||||
// > The algorithm to convert a string to a number, given a string input, is as
|
||||
// > follows: If parsing a month from input results in an error, then return an
|
||||
// > error; otherwise, return the number of months between January 1970 and the
|
||||
// > parsed month.
|
||||
//
|
||||
// This one returns number of months, not milliseconds (specification requires
|
||||
// this, presumably because number of milliseconds is not consistent across
|
||||
// months) the - 1.0 is because january is 1, not 0
|
||||
InputType::Month => value.str().parse_month_string().map(|date_time| {
|
||||
((date_time.year() - 1970) * 12) as f64 + (date_time.month() as u8 - 1) as f64
|
||||
}),
|
||||
// > The algorithm to convert a string to a number, given a string input, is as
|
||||
// > follows: If parsing a week string from input results in an error, then
|
||||
// > return an error; otherwise, return the number of milliseconds elapsed from
|
||||
// > midnight UTC on the morning of 1970-01-01 (the time represented by the
|
||||
// > value "1970-01-01T00:00:00.0Z") to midnight UTC on the morning of the
|
||||
// > Monday of the parsed week, ignoring leap seconds.
|
||||
InputType::Week => value.str().parse_week_string().map(|date_time| {
|
||||
(date_time - OffsetDateTime::UNIX_EPOCH).whole_milliseconds() as f64
|
||||
}),
|
||||
// > The algorithm to convert a string to a number, given a string input, is as
|
||||
// > follows: If parsing a time from input results in an error, then return an
|
||||
// > error; otherwise, return the number of milliseconds elapsed from midnight to
|
||||
// > the parsed time on a day with no time changes.
|
||||
InputType::Time => value
|
||||
.str()
|
||||
.parse_time_string()
|
||||
.map(|date_time| (date_time.time() - Time::MIDNIGHT).whole_milliseconds() as f64),
|
||||
// > The algorithm to convert a string to a number, given a string input, is as
|
||||
// > follows: If parsing a date and time from input results in an error, then
|
||||
// > return an error; otherwise, return the number of milliseconds elapsed from
|
||||
// > midnight on the morning of 1970-01-01 (the time represented by the value
|
||||
// > "1970-01-01T00:00:00.0") to the parsed local date and time, ignoring leap
|
||||
// > seconds.
|
||||
InputType::DatetimeLocal => {
|
||||
// Is this supposed to know the locale's daylight-savings-time rules?
|
||||
value.parse_local_date_and_time_string().and_then(|date| {
|
||||
let seconds = date.seconds as u32;
|
||||
let milliseconds = ((date.seconds - seconds as f64) * 1000.) as u32;
|
||||
Some(
|
||||
NaiveDate::from_ymd_opt(date.year, date.month, date.day)?
|
||||
.and_hms_milli_opt(date.hour, date.minute, seconds, milliseconds)?
|
||||
.and_utc()
|
||||
.timestamp_millis() as f64,
|
||||
)
|
||||
value.str().parse_local_date_time_string().map(|date_time| {
|
||||
(date_time - OffsetDateTime::UNIX_EPOCH).whole_milliseconds() as f64
|
||||
})
|
||||
},
|
||||
InputType::Number | InputType::Range => value.parse_floating_point_number(),
|
||||
|
@ -2159,94 +2166,73 @@ impl HTMLInputElement {
|
|||
}
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#concept-input-value-string-number
|
||||
fn convert_number_to_string(&self, value: f64) -> Result<DOMString, ()> {
|
||||
/// <https://html.spec.whatwg.org/multipage/#concept-input-value-string-number>
|
||||
fn convert_number_to_string(&self, value: f64) -> Option<DOMString> {
|
||||
match self.input_type() {
|
||||
InputType::Date => {
|
||||
let datetime = milliseconds_to_datetime(value)?;
|
||||
Ok(DOMString::from(datetime.format("%Y-%m-%d").to_string()))
|
||||
InputType::Date | InputType::Week | InputType::Time | InputType::DatetimeLocal => {
|
||||
OffsetDateTime::from_unix_timestamp_nanos((value * 1e6) as i128)
|
||||
.ok()
|
||||
.map(|value| self.convert_datetime_to_dom_string(value))
|
||||
},
|
||||
InputType::Month => {
|
||||
// interpret value as months(not millis) in epoch, return monthstring
|
||||
let year_from_1970 = (value / 12.0).floor();
|
||||
let month = (value - year_from_1970 * 12.0).floor() as u32 + 1; // january is 1, not 0
|
||||
let year = (year_from_1970 + 1970.0) as u64;
|
||||
Ok(DOMString::from(format!("{:04}-{:02}", year, month)))
|
||||
},
|
||||
InputType::Week => {
|
||||
let datetime = milliseconds_to_datetime(value)?;
|
||||
let year = datetime.iso_week().year(); // not necessarily the same as datetime.year()
|
||||
let week = datetime.iso_week().week();
|
||||
Ok(DOMString::from(format!("{:04}-W{:02}", year, week)))
|
||||
},
|
||||
InputType::Time => {
|
||||
let datetime = milliseconds_to_datetime(value)?;
|
||||
Ok(DOMString::from(datetime.format("%H:%M:%S%.3f").to_string()))
|
||||
},
|
||||
InputType::DatetimeLocal => {
|
||||
let datetime = milliseconds_to_datetime(value)?;
|
||||
Ok(DOMString::from(
|
||||
datetime.format("%Y-%m-%dT%H:%M:%S%.3f").to_string(),
|
||||
))
|
||||
// > The algorithm to convert a number to a string, given a number input,
|
||||
// > is as follows: Return a valid month string that represents the month
|
||||
// > that has input months between it and January 1970.
|
||||
let date = OffsetDateTime::UNIX_EPOCH;
|
||||
let years = (value / 12.) as i32;
|
||||
let year = date.year() + years;
|
||||
|
||||
let months = value as i32 - (years * 12);
|
||||
let months = match months.cmp(&0) {
|
||||
Ordering::Less => (12 - months) as u8,
|
||||
Ordering::Equal | Ordering::Greater => months as u8,
|
||||
} + 1;
|
||||
|
||||
let date = date
|
||||
.replace_year(year)
|
||||
.ok()?
|
||||
.replace_month(Month::try_from(months).ok()?)
|
||||
.ok()?;
|
||||
Some(self.convert_datetime_to_dom_string(date))
|
||||
},
|
||||
InputType::Number | InputType::Range => {
|
||||
let mut value = DOMString::from(value.to_string());
|
||||
|
||||
value.set_best_representation_of_the_floating_point_number();
|
||||
|
||||
Ok(value)
|
||||
Some(value)
|
||||
},
|
||||
// this won't be called from other input types
|
||||
_ => unreachable!(),
|
||||
_ => unreachable!("Should not have called convert_number_to_string for non-Date types"),
|
||||
}
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#concept-input-value-string-date
|
||||
// <https://html.spec.whatwg.org/multipage/#concept-input-value-string-date>
|
||||
// This does the safe Rust part of conversion; the unsafe JS Date part
|
||||
// is in GetValueAsDate
|
||||
fn convert_string_to_naive_datetime(&self, value: DOMString) -> Option<NaiveDateTime> {
|
||||
fn convert_string_to_naive_datetime(&self, value: DOMString) -> Option<OffsetDateTime> {
|
||||
match self.input_type() {
|
||||
InputType::Date => value
|
||||
.parse_date_string()
|
||||
.and_then(|(y, m, d)| NaiveDate::from_ymd_opt(y, m, d))
|
||||
.and_then(|date| date.and_hms_opt(0, 0, 0)),
|
||||
InputType::Time => value.parse_time_string().and_then(|(h, m, s)| {
|
||||
let whole_seconds = s.floor();
|
||||
let nanos = ((s - whole_seconds) * 1e9).floor() as u32;
|
||||
NaiveDate::from_ymd_opt(1970, 1, 1)
|
||||
.and_then(|date| date.and_hms_nano_opt(h, m, whole_seconds as u32, nanos))
|
||||
}),
|
||||
InputType::Week => value
|
||||
.parse_week_string()
|
||||
.and_then(|(iso_year, week)| {
|
||||
NaiveDate::from_isoywd_opt(iso_year, week, Weekday::Mon)
|
||||
})
|
||||
.and_then(|date| date.and_hms_opt(0, 0, 0)),
|
||||
InputType::Month => value
|
||||
.parse_month_string()
|
||||
.and_then(|(y, m)| NaiveDate::from_ymd_opt(y, m, 1))
|
||||
.and_then(|date| date.and_hms_opt(0, 0, 0)),
|
||||
InputType::Date => value.str().parse_date_string(),
|
||||
InputType::Time => value.str().parse_time_string(),
|
||||
InputType::Week => value.str().parse_week_string(),
|
||||
InputType::Month => value.str().parse_month_string(),
|
||||
InputType::DatetimeLocal => value.str().parse_local_date_time_string(),
|
||||
// does not apply to other types
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#concept-input-value-date-string
|
||||
// This does the safe Rust part of conversion; the unsafe JS Date part
|
||||
// is in SetValueAsDate
|
||||
fn convert_naive_datetime_to_string(&self, value: NaiveDateTime) -> Result<DOMString, ()> {
|
||||
match self.input_type() {
|
||||
InputType::Date => Ok(DOMString::from(value.format("%Y-%m-%d").to_string())),
|
||||
InputType::Month => Ok(DOMString::from(value.format("%Y-%m").to_string())),
|
||||
InputType::Week => {
|
||||
let year = value.iso_week().year(); // not necessarily the same as value.year()
|
||||
let week = value.iso_week().week();
|
||||
Ok(DOMString::from(format!("{:04}-W{:02}", year, week)))
|
||||
/// <https://html.spec.whatwg.org/multipage/#concept-input-value-date-string>
|
||||
/// This does the safe Rust part of conversion; the unsafe JS Date part
|
||||
/// is in SetValueAsDate
|
||||
fn convert_datetime_to_dom_string(&self, value: OffsetDateTime) -> DOMString {
|
||||
DOMString::from_string(match self.input_type() {
|
||||
InputType::Date => value.to_date_string(),
|
||||
InputType::Month => value.to_month_string(),
|
||||
InputType::Week => value.to_week_string(),
|
||||
InputType::Time => value.to_time_string(),
|
||||
InputType::DatetimeLocal => value.to_local_date_time_string(),
|
||||
_ => {
|
||||
unreachable!("Should not have called convert_datetime_to_string for non-Date types")
|
||||
},
|
||||
InputType::Time => Ok(DOMString::from(value.format("%H:%M:%S%.3f").to_string())),
|
||||
// this won't be called from other input types
|
||||
_ => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2877,16 +2863,6 @@ fn round_halves_positive(n: f64) -> f64 {
|
|||
}
|
||||
}
|
||||
|
||||
fn milliseconds_to_datetime(value: f64) -> Result<NaiveDateTime, ()> {
|
||||
let seconds = (value / 1000.0).floor();
|
||||
let milliseconds = value - (seconds * 1000.0);
|
||||
let nanoseconds = milliseconds * 1e6;
|
||||
match DateTime::from_timestamp(seconds as i64, nanoseconds as u32) {
|
||||
Some(datetime) => Ok(datetime.naive_utc()),
|
||||
None => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
// This is used to compile JS-compatible regex provided in pattern attribute
|
||||
// that matches only the entirety of string.
|
||||
// https://html.spec.whatwg.org/multipage/#compiled-pattern-regular-expression
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
[input-valueasdate.html]
|
||||
[valueAsDate setter on type time (new Date("1970-01-01T00:00:00.000Z"))]
|
||||
expected: FAIL
|
||||
|
||||
[valueAsDate setter on type time (new Date("1970-01-01T12:00:00.000Z"))]
|
||||
expected: FAIL
|
||||
|
||||
[valueAsDate setter on type time (new Date("1970-01-01T23:59:00.000Z"))]
|
||||
expected: FAIL
|
|
@ -1,9 +0,0 @@
|
|||
[input-valueasnumber.html]
|
||||
[valueAsNumber setter on type time (actual valueAsNumber: 86340000, expected value: 23:59)]
|
||||
expected: FAIL
|
||||
|
||||
[valueAsNumber setter on type time (actual valueAsNumber: 43200000, expected value: 12:00)]
|
||||
expected: FAIL
|
||||
|
||||
[valueAsNumber setter on type time (actual valueAsNumber: 0, expected value: 00:00)]
|
||||
expected: FAIL
|
Loading…
Add table
Add a link
Reference in a new issue