mirror of
https://github.com/servo/servo.git
synced 2025-08-03 04:30:10 +01: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
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue