diff --git a/Libraries/LibWeb/HTML/Dates.cpp b/Libraries/LibWeb/HTML/Dates.cpp index 70ca3b58730..7b6532053d0 100644 --- a/Libraries/LibWeb/HTML/Dates.cpp +++ b/Libraries/LibWeb/HTML/Dates.cpp @@ -2,12 +2,13 @@ * Copyright (c) 2018-2023, Andreas Kling * Copyright (c) 2022, Adam Hodgen * Copyright (c) 2022, Andrew Kaster - * Copyright (c) 2023, Shannon Booth + * Copyright (c) 2023-2025, Shannon Booth * Copyright (c) 2023, stelar7 * * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include @@ -243,4 +244,68 @@ WebIDL::ExceptionOr> parse_time_string(JS::Realm& realm, Strin return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Can't parse time string"sv }; } +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-month-component +static Optional parse_a_month_component(GenericLexer& input) +{ + // 1. Collect a sequence of code points that are ASCII digits from input given position. If the collected sequence is + // not at least four characters long, then fail. Otherwise, interpret the resulting sequence as a base-ten integer. + // Let that number be the year. + auto year_string = input.consume_while(is_ascii_digit); + if (year_string.length() < 4) + return {}; + auto maybe_year = year_string.to_number(); + if (!maybe_year.has_value()) + return {}; + auto year = maybe_year.value(); + + // 2. If year is not a number greater than zero, then fail. + if (year < 1) + return {}; + + // 3. If position is beyond the end of input or if the character at position is not a U+002D HYPHEN-MINUS 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 month. + auto month_string = input.consume_while(is_ascii_digit); + if (month_string.length() != 2) + return {}; + auto month = month_string.to_number().value(); + + // 5. If month is not a number in the range 1 ≤ month ≤ 12, then fail. + if (month < 1 || month > 12) + return {}; + + // 6. Return year and month. + return YearAndMonth { year, month }; +} + +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-month-string +Optional parse_a_month_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 month component to obtain year and month. If this returns nothing, then fail. + auto year_and_month = parse_a_month_component(input); + if (!year_and_month.has_value()) + return {}; + + // 4. If position is not beyond the end of input, then fail. + if (!input.is_eof()) + return {}; + + // 5. Return year and month. + return year_and_month; +} + +i32 number_of_months_since_unix_epoch(YearAndMonth year_and_month) +{ + return (year_and_month.year - 1970) * 12 + year_and_month.month - 1; +} + } diff --git a/Libraries/LibWeb/HTML/Dates.h b/Libraries/LibWeb/HTML/Dates.h index 170cdb7441f..f6e1607ff20 100644 --- a/Libraries/LibWeb/HTML/Dates.h +++ b/Libraries/LibWeb/HTML/Dates.h @@ -23,4 +23,11 @@ String normalize_local_date_and_time_string(String const& value); bool is_valid_time_string(StringView value); WebIDL::ExceptionOr> parse_time_string(JS::Realm& realm, StringView value); +struct YearAndMonth { + u32 year; + u32 month; +}; +Optional parse_a_month_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 a633630e1df..80475ea66f9 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -2067,6 +2067,18 @@ WebIDL::ExceptionOr HTMLInputElement::set_width(WebIDL::UnsignedLong value return set_attribute(HTML::AttributeNames::width, String::number(value)); } +// https://html.spec.whatwg.org/multipage/input.html#month-state-(type=month):concept-input-value-string-number +static Optional convert_month_string_to_number(StringView input) +{ + // 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. + auto maybe_year_and_month = parse_a_month_string(input); + if (!maybe_year_and_month.has_value()) + return {}; + return number_of_months_since_unix_epoch(maybe_year_and_month.value()); +} + // https://html.spec.whatwg.org/multipage/input.html#concept-input-value-string-number Optional HTMLInputElement::convert_string_to_number(StringView input) const { @@ -2078,10 +2090,24 @@ Optional HTMLInputElement::convert_string_to_number(StringView input) co if (type_state() == TypeAttributeState::Range) return parse_floating_point_number(input); + if (type_state() == TypeAttributeState::Month) + return convert_month_string_to_number(input); + dbgln("HTMLInputElement::convert_string_to_number() not implemented for input type {}", type()); return {}; } +// https://html.spec.whatwg.org/multipage/input.html#month-state-(type=month):concept-input-value-number-string +static String convert_number_to_month_string(double input) +{ + // 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. + auto months = JS::modulo(input, 12); + auto year = 1970 + (input - months) / 12; + + return MUST(String::formatted("{:04d}-{:02d}", static_cast(year), static_cast(months) + 1)); +} + // https://html.spec.whatwg.org/multipage/input.html#concept-input-value-string-number String HTMLInputElement::convert_number_to_string(double input) const { @@ -2093,6 +2119,9 @@ String HTMLInputElement::convert_number_to_string(double input) const if (type_state() == TypeAttributeState::Range) return String::number(input); + if (type_state() == TypeAttributeState::Month) + return convert_number_to_month_string(input); + dbgln("HTMLInputElement::convert_number_to_string() not implemented for input type {}", type()); return {}; } diff --git a/Tests/LibWeb/Text/expected/HTML/HTMLInputElement-valueAsNumber.txt b/Tests/LibWeb/Text/expected/HTML/HTMLInputElement-valueAsNumber.txt index 847848b7100..4ed2fd35b5e 100644 --- a/Tests/LibWeb/Text/expected/HTML/HTMLInputElement-valueAsNumber.txt +++ b/Tests/LibWeb/Text/expected/HTML/HTMLInputElement-valueAsNumber.txt @@ -32,7 +32,7 @@ url threw exception: InvalidStateError: valueAsNumber: Invalid input type used email threw exception: InvalidStateError: valueAsNumber: Invalid input type used password threw exception: InvalidStateError: valueAsNumber: Invalid input type used date did not throw: NaN -month did not throw: NaN +month did not throw: 100 week did not throw: NaN time did not throw: NaN datetime-local did not throw: NaN diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/semantics/forms/the-input-element/input-valueasnumber.txt b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/forms/the-input-element/input-valueasnumber.txt index 0ebce007a13..403b16c7531 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/semantics/forms/the-input-element/input-valueasnumber.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/forms/the-input-element/input-valueasnumber.txt @@ -2,8 +2,8 @@ Harness status: OK Found 60 tests -34 Pass -26 Fail +38 Pass +22 Fail Pass valueAsNumber getter on type date (actual value: , expected valueAsNumber: NaN) Pass valueAsNumber getter on type date (actual value: 0000-12-10, expected valueAsNumber: NaN) Pass valueAsNumber getter on type date (actual value: 2019-00-12, expected valueAsNumber: NaN) @@ -18,10 +18,10 @@ Fail valueAsNumber setter on type date (actual valueAsNumber: 1456704000000, exp Pass valueAsNumber getter on type month (actual value: , expected valueAsNumber: NaN) Pass valueAsNumber getter on type month (actual value: 0000-12, expected valueAsNumber: NaN) Pass valueAsNumber getter on type month (actual value: 2019-00, expected valueAsNumber: NaN) -Fail valueAsNumber getter on type month (actual value: 2019-12, expected valueAsNumber: 599) -Fail valueAsNumber getter on type month (actual value: 1969-12, expected valueAsNumber: -1) -Fail valueAsNumber setter on type month (actual valueAsNumber: 599, expected value: 2019-12) -Fail valueAsNumber setter on type month (actual valueAsNumber: -1, expected value: 1969-12) +Pass valueAsNumber getter on type month (actual value: 2019-12, expected valueAsNumber: 599) +Pass valueAsNumber getter on type month (actual value: 1969-12, expected valueAsNumber: -1) +Pass valueAsNumber setter on type month (actual valueAsNumber: 599, expected value: 2019-12) +Pass valueAsNumber setter on type month (actual valueAsNumber: -1, expected value: 1969-12) Pass valueAsNumber getter on type week (actual value: , expected valueAsNumber: NaN) Pass valueAsNumber getter on type week (actual value: 0000-W50, expected valueAsNumber: NaN) Pass valueAsNumber getter on type week (actual value: 2019-W00, expected valueAsNumber: NaN)