diff --git a/Libraries/LibWeb/HTML/Dates.cpp b/Libraries/LibWeb/HTML/Dates.cpp index 004a1cf1824..0e3870515f0 100644 --- a/Libraries/LibWeb/HTML/Dates.cpp +++ b/Libraries/LibWeb/HTML/Dates.cpp @@ -401,4 +401,103 @@ Optional parse_a_date_string(StringView input_view) return year_month_day.release_value(); } +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-time-component +static Optional parse_a_time_component(GenericLexer& input) +{ + // 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. + auto hour_string = input.consume_while(is_ascii_digit); + if (hour_string.length() != 2) + return {}; + auto maybe_hour = hour_string.to_number(); + if (!maybe_hour.has_value()) + return {}; + auto hour = maybe_hour.value(); + // 2. If hour is not a number in the range 0 ≤ hour ≤ 23, then fail. + if (hour < 0 || hour > 23) + return {}; + // 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. + if (!input.consume_specific(':')) + return {}; + // 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. + auto minute_string = input.consume_while(is_ascii_digit); + if (minute_string.length() != 2) + return {}; + auto maybe_minute = minute_string.to_number(); + if (!maybe_minute.has_value()) + return {}; + auto minute = maybe_minute.value(); + // 5. If minute is not a number in the range 0 ≤ minute ≤ 59, then fail. + if (minute < 0 || hour > 59) + return {}; + // 6. Let second be 0. + i32 second = 0; + // 7. If position is not beyond the end of input and the character at position is U+003A (:), then: + if (!input.consume_specific(':')) + return {}; + // 7.1. Advance position to the next character in input. + // 7.2. If position is beyond the end of input, or at the last character in input, or if the next two characters in + // input starting at position are not both ASCII digits, then fail. + if (input.is_eof() || input.tell_remaining() == 1 || (!is_ascii_digit(input.peek()) && !is_ascii_digit(input.peek(1)))) + return {}; + // 7.3. Collect a sequence of code points that are either ASCII digits or U+002E FULL STOP characters from input + // given position. + auto second_string = input.consume_while([](auto ch) { return is_ascii_digit(ch) || ch == '.'; }); + // If the collected sequence is three characters long, or if it is longer than three characters long and the third + // character is not a U+002E FULL STOP character, or if it has more than one U+002E FULL STOP character, then fail. + if (second_string.length() == 3) + return {}; + if (second_string.length() > 3 && second_string[2] != '.') + return {}; + if (second_string.find_all("."sv).size() > 1) + return {}; + // Otherwise, interpret the resulting sequence as a base-ten number (possibly with a fractional part). Set second + // to that number. + // NB: The spec doesn't state requirements for what we must do with the fractional part of the second(s) string. + // The spec neither requires that we separately preserve it nor requires that we completely discard it. If we + // did have any reason at all to preserve it, we could parse the string into a float here. But there doesn't + // seem to be any point in doing that, because there’s nothing in the corresponding calling algorithm(s) in the + // spec that takes a milliseconds field and does anything with it anyway. + auto maybe_second = second_string.to_number(); + if (!maybe_second.has_value()) + return {}; + second = maybe_second.value(); + // 7.4. If second is not a number in the range 0 ≤ second < 60, then fail. + if (second < 0 || second > 60) + return {}; + // 8. Return hour, minute, and second. + return HourMinuteSecond { hour, minute, second }; +} + +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-local-date-and-time-string +Optional parse_a_local_date_and_time_string(StringView input_view) +{ + // 1. Let input be the string being parsed. + // 2. Let position be a pointer into input, initially pointing at the start of the string. + GenericLexer input { input_view }; + // 3. Parse a date component to obtain year, month, and day. If this returns nothing, then fail. + auto year_month_day = parse_a_date_component(input); + if (!year_month_day.has_value()) + return {}; + // 4. If position is beyond the end of input or if the character at position is neither a U+0054 LATIN CAPITAL + // LETTER T character (T) nor a U+0020 SPACE character, then fail. Otherwise, move position forwards one character. + if (!input.consume_specific("T") && !input.consume_specific(" ")) + return {}; + // 5. Parse a time component to obtain hour, minute, and second. If this returns nothing, then fail. + auto hour_minute_second = parse_a_time_component(input); + if (!hour_minute_second.has_value()) + return {}; + // 6. If position is not beyond the end of input, then fail. + if (!input.is_eof()) + return {}; + // 7. Let date be the date with year year, month month, and day day. + // 8. Let time be the time with hour hour, minute minute, and second second. + // 9. Return date and time. + return DateAndTime { year_month_day.release_value(), hour_minute_second.release_value() }; +} + } diff --git a/Libraries/LibWeb/HTML/Dates.h b/Libraries/LibWeb/HTML/Dates.h index 80d5500be2b..a3186299452 100644 --- a/Libraries/LibWeb/HTML/Dates.h +++ b/Libraries/LibWeb/HTML/Dates.h @@ -40,8 +40,21 @@ struct YearMonthDay { u32 day; }; +struct HourMinuteSecond { + i32 hour; + i32 minute; + i32 second; +}; + +struct DateAndTime { + YearMonthDay date; + HourMinuteSecond time; +}; + Optional parse_a_date_string(StringView); +Optional parse_a_local_date_and_time_string(StringView); + i32 number_of_months_since_unix_epoch(YearAndMonth); } diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Libraries/LibWeb/HTML/HTMLInputElement.cpp index 3214cd28050..ae124229180 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -2170,6 +2170,24 @@ static Optional convert_date_string_to_number(StringView input) return date_time.milliseconds_since_epoch(); } +// https://html.spec.whatwg.org/multipage/input.html#local-date-and-time-state-(type=datetime-local):parse-a-local-date-and-time-string-2 +static Optional convert_local_date_and_time_string_to_number(StringView input) +{ + // 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. + auto maybe_date_and_time = parse_a_local_date_and_time_string(input); + if (!maybe_date_and_time.has_value()) + return {}; + auto date_and_time = maybe_date_and_time.value(); + auto date = date_and_time.date; + auto time = date_and_time.time; + + auto date_time = UnixDateTime::from_unix_time_parts(date.year, date.month, date.day, time.hour, time.minute, time.second, 0); + return date_time.milliseconds_since_epoch(); +} + // https://html.spec.whatwg.org/multipage/input.html#time-state-(type=time):concept-input-value-string-number Optional HTMLInputElement::convert_time_string_to_number(StringView input) const { @@ -2205,7 +2223,9 @@ Optional HTMLInputElement::convert_string_to_number(StringView input) co if (type_state() == TypeAttributeState::Time) return convert_time_string_to_number(input); - dbgln("HTMLInputElement::convert_string_to_number() not implemented for input type {}", type()); + if (type_state() == TypeAttributeState::LocalDateAndTime) + return convert_local_date_and_time_string_to_number(input); + return {}; } diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/semantics/forms/constraints/form-validation-checkValidity.txt b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/forms/constraints/form-validation-checkValidity.txt index 1df5073404b..d256ffa58dc 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/semantics/forms/constraints/form-validation-checkValidity.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/forms/constraints/form-validation-checkValidity.txt @@ -2,8 +2,7 @@ Harness status: OK Found 132 tests -126 Pass -6 Fail +132 Pass Pass [INPUT in TEXT status] no constraint Pass [INPUT in TEXT status] no constraint (in a form) Pass [INPUT in TEXT status] not suffering from being too long @@ -58,12 +57,12 @@ Pass [INPUT in EMAIL status] suffering from being missing Pass [INPUT in EMAIL status] suffering from being missing (in a form) Pass [INPUT in DATETIME-LOCAL status] no constraint Pass [INPUT in DATETIME-LOCAL status] no constraint (in a form) -Fail [INPUT in DATETIME-LOCAL status] suffering from an overflow -Fail [INPUT in DATETIME-LOCAL status] suffering from an overflow (in a form) -Fail [INPUT in DATETIME-LOCAL status] suffering from an underflow -Fail [INPUT in DATETIME-LOCAL status] suffering from an underflow (in a form) -Fail [INPUT in DATETIME-LOCAL status] suffering from a step mismatch -Fail [INPUT in DATETIME-LOCAL status] suffering from a step mismatch (in a form) +Pass [INPUT in DATETIME-LOCAL status] suffering from an overflow +Pass [INPUT in DATETIME-LOCAL status] suffering from an overflow (in a form) +Pass [INPUT in DATETIME-LOCAL status] suffering from an underflow +Pass [INPUT in DATETIME-LOCAL status] suffering from an underflow (in a form) +Pass [INPUT in DATETIME-LOCAL status] suffering from a step mismatch +Pass [INPUT in DATETIME-LOCAL status] suffering from a step mismatch (in a form) Pass [INPUT in DATETIME-LOCAL status] suffering from being missing Pass [INPUT in DATETIME-LOCAL status] suffering from being missing (in a form) Pass [INPUT in DATE status] no constraint diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/semantics/forms/constraints/form-validation-validity-rangeOverflow.txt b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/forms/constraints/form-validation-validity-rangeOverflow.txt index 942a7da6894..897e3e2ad25 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/semantics/forms/constraints/form-validation-validity-rangeOverflow.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/forms/constraints/form-validation-validity-rangeOverflow.txt @@ -2,19 +2,19 @@ Harness status: OK Found 49 tests -41 Pass -8 Fail +43 Pass +6 Fail Pass [INPUT in DATETIME-LOCAL status] The max attribute is not set Pass [INPUT in DATETIME-LOCAL status] Value is empty string Pass [INPUT in DATETIME-LOCAL status] The max attribute is an invalid local date time string Pass [INPUT in DATETIME-LOCAL status] The max attribute is greater than the value attribute Pass [INPUT in DATETIME-LOCAL status] The value is an invalid local date time string(hour is greater than 23) Pass [INPUT in DATETIME-LOCAL status] The value if an invalid local date time string(year is two digits) -Fail [INPUT in DATETIME-LOCAL status] The value is greater than max +Pass [INPUT in DATETIME-LOCAL status] The value is greater than max Fail [INPUT in DATETIME-LOCAL status] The value is greater than max(with millisecond in 1 digit) Fail [INPUT in DATETIME-LOCAL status] The value is greater than max(with millisecond in 2 digits) Fail [INPUT in DATETIME-LOCAL status] The value is greater than max(with millisecond in 3 digits) -Fail [INPUT in DATETIME-LOCAL status] The value is greater than max(Year is 10000 should be valid) +Pass [INPUT in DATETIME-LOCAL status] The value is greater than max(Year is 10000 should be valid) Pass [INPUT in DATE status] The max attribute is not set Pass [INPUT in DATE status] Value is empty string Pass [INPUT in DATE status] The max attribute is an invalid date diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/semantics/forms/constraints/form-validation-validity-rangeUnderflow.txt b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/forms/constraints/form-validation-validity-rangeUnderflow.txt index ffef3c6b306..b75209dfccf 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/semantics/forms/constraints/form-validation-validity-rangeUnderflow.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/forms/constraints/form-validation-validity-rangeUnderflow.txt @@ -2,19 +2,19 @@ Harness status: OK Found 47 tests -39 Pass -8 Fail +41 Pass +6 Fail Pass [INPUT in DATETIME-LOCAL status] The min attribute is not set Pass [INPUT in DATETIME-LOCAL status] Value is empty string Pass [INPUT in DATETIME-LOCAL status] The min attribute is an invalid local date time string Pass [INPUT in DATETIME-LOCAL status] The min attribute is less than the value attribute Pass [INPUT in DATETIME-LOCAL status] The value is an invalid local date time string(hour is greater than 23) Pass [INPUT in DATETIME-LOCAL status] The value is an invalid local date time string(year is two digits) -Fail [INPUT in DATETIME-LOCAL status] The value is less than min +Pass [INPUT in DATETIME-LOCAL status] The value is less than min Fail [INPUT in DATETIME-LOCAL status] The value is less than min(with millisecond in 1 digit) Fail [INPUT in DATETIME-LOCAL status] The value is less than min(with millisecond in 2 digits) Fail [INPUT in DATETIME-LOCAL status] The value is less than min(with millisecond in 3 digits) -Fail [INPUT in DATETIME-LOCAL status] The value is less than min(Year is 10000 should be valid) +Pass [INPUT in DATETIME-LOCAL status] The value is less than min(Year is 10000 should be valid) Pass [INPUT in DATETIME-LOCAL status] The value is greater than max Pass [INPUT in DATE status] The min attribute is not set Pass [INPUT in DATE status] Value is empty string