mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-04-19 19:15:19 +00:00
LibWeb/HTML: Correctly parse milliseconds in time inputs
This commit brings the way we parse time inputs closer to the spec by no longer ignoring milliseconds in time strings.
This commit is contained in:
parent
d37d0e4b59
commit
9c758e5f65
Notes:
github-actions[bot]
2025-04-19 11:10:32 +00:00
Author: https://github.com/skyz1 Commit: https://github.com/LadybirdBrowser/ladybird/commit/9c758e5f653 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/4397 Reviewed-by: https://github.com/trflynn89
9 changed files with 118 additions and 49 deletions
|
@ -209,25 +209,6 @@ bool is_valid_time_string(StringView value)
|
|||
return true;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-time-string
|
||||
WebIDL::ExceptionOr<GC::Ref<JS::Date>> parse_time_string(JS::Realm& realm, StringView value)
|
||||
{
|
||||
// FIXME: Implement spec compliant time string parsing
|
||||
auto parts = value.split_view(':', SplitBehavior::KeepEmpty);
|
||||
if (parts.size() >= 2) {
|
||||
if (auto hours = parts.at(0).to_number<u32>(); hours.has_value()) {
|
||||
if (auto minutes = parts.at(1).to_number<u32>(); minutes.has_value()) {
|
||||
if (parts.size() >= 3) {
|
||||
if (auto seconds = parts.at(2).to_number<u32>(); seconds.has_value())
|
||||
return JS::Date::create(realm, JS::make_time(*hours, *minutes, *seconds, 0));
|
||||
}
|
||||
return JS::Date::create(realm, JS::make_date(0, JS::make_time(*hours, *minutes, 0, 0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
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<YearAndMonth> parse_a_month_component(GenericLexer& input)
|
||||
{
|
||||
|
@ -440,7 +421,7 @@ static Optional<HourMinuteSecond> parse_a_time_component(GenericLexer& input)
|
|||
return {};
|
||||
|
||||
// 6. Let second be 0.
|
||||
i32 second = 0;
|
||||
f32 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(':')) {
|
||||
|
@ -463,18 +444,13 @@ static Optional<HourMinuteSecond> parse_a_time_component(GenericLexer& input)
|
|||
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<i32>();
|
||||
auto maybe_second = second_string.to_number<f32>();
|
||||
if (!maybe_second.has_value())
|
||||
return {};
|
||||
second = maybe_second.value();
|
||||
|
||||
// 4. If second is not a number in the range 0 ≤ second < 60, then fail.
|
||||
if (second < 0 || second > 60)
|
||||
if (second < 0 || second >= 60)
|
||||
return {};
|
||||
}
|
||||
|
||||
|
@ -482,6 +458,27 @@ static Optional<HourMinuteSecond> parse_a_time_component(GenericLexer& input)
|
|||
return HourMinuteSecond { hour, minute, second };
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-time-string
|
||||
WebIDL::ExceptionOr<GC::Ref<JS::Date>> parse_time_string(JS::Realm& realm, StringView value)
|
||||
{
|
||||
// 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 { value };
|
||||
|
||||
// 3. 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 WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Can't parse time string"sv };
|
||||
|
||||
// 4. If position is not beyond the end of input, then fail.
|
||||
if (!input.is_eof())
|
||||
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Can't parse time string"sv };
|
||||
|
||||
// 5. Let time be the time with hour hour, minute minute, and second second.
|
||||
// 6. Return time.
|
||||
return JS::Date::create(realm, JS::make_time(hour_minute_second->hour, hour_minute_second->minute, hour_minute_second->second, static_cast<i32>(hour_minute_second->second * 1000) % 1000));
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-local-date-and-time-string
|
||||
Optional<DateAndTime> parse_a_local_date_and_time_string(StringView input_view)
|
||||
{
|
||||
|
|
|
@ -43,7 +43,7 @@ struct YearMonthDay {
|
|||
struct HourMinuteSecond {
|
||||
i32 hour;
|
||||
i32 minute;
|
||||
i32 second;
|
||||
f32 second;
|
||||
};
|
||||
|
||||
struct DateAndTime {
|
||||
|
|
|
@ -2209,7 +2209,7 @@ static Optional<double> convert_local_date_and_time_string_to_number(StringView
|
|||
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);
|
||||
auto date_time = UnixDateTime::from_unix_time_parts(date.year, date.month, date.day, time.hour, time.minute, time.second, static_cast<i32>(time.second * 1000) % 1000);
|
||||
return date_time.milliseconds_since_epoch();
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +1,6 @@
|
|||
Parsed invalid as NaN
|
||||
Parsed xx:34:56 as NaN
|
||||
Parsed 12:xx:56 as NaN
|
||||
Parsed 12:34:xx as NaN
|
||||
Parsed 12:34:60 as NaN
|
||||
PASS (didn't crash)
|
||||
|
|
|
@ -2,8 +2,7 @@ Harness status: OK
|
|||
|
||||
Found 49 tests
|
||||
|
||||
43 Pass
|
||||
6 Fail
|
||||
49 Pass
|
||||
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
|
||||
|
@ -11,9 +10,9 @@ Pass [INPUT in DATETIME-LOCAL status] The max attribute is greater than the valu
|
|||
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)
|
||||
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)
|
||||
Pass [INPUT in DATETIME-LOCAL status] The value is greater than max(with millisecond in 1 digit)
|
||||
Pass [INPUT in DATETIME-LOCAL status] The value is greater than max(with millisecond in 2 digits)
|
||||
Pass [INPUT in DATETIME-LOCAL status] The value is greater than max(with millisecond in 3 digits)
|
||||
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
|
||||
|
@ -35,9 +34,9 @@ Pass [INPUT in TIME status] The value attribute is an invalid time string(second
|
|||
Pass [INPUT in TIME status] The max attribute is greater than value attribute
|
||||
Pass [INPUT in TIME status] The time missing second and minute parts is invalid
|
||||
Pass [INPUT in TIME status] The value attribute is greater than max attribute
|
||||
Fail [INPUT in TIME status] The value is greater than max(with millisecond in 1 digit)
|
||||
Fail [INPUT in TIME status] The value is greater than max(with millisecond in 2 digit)
|
||||
Fail [INPUT in TIME status] The value is greater than max(with millisecond in 3 digit)
|
||||
Pass [INPUT in TIME status] The value is greater than max(with millisecond in 1 digit)
|
||||
Pass [INPUT in TIME status] The value is greater than max(with millisecond in 2 digit)
|
||||
Pass [INPUT in TIME status] The value is greater than max(with millisecond in 3 digit)
|
||||
Pass [INPUT in TIME status] The time missing second part is valid
|
||||
Pass [INPUT in TIME status] The time is max for reversed range
|
||||
Pass [INPUT in TIME status] The time is outside the accepted range for reversed range
|
||||
|
|
|
@ -2,8 +2,7 @@ Harness status: OK
|
|||
|
||||
Found 47 tests
|
||||
|
||||
41 Pass
|
||||
6 Fail
|
||||
47 Pass
|
||||
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
|
||||
|
@ -11,9 +10,9 @@ Pass [INPUT in DATETIME-LOCAL status] The min attribute is less than the value a
|
|||
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)
|
||||
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)
|
||||
Pass [INPUT in DATETIME-LOCAL status] The value is less than min(with millisecond in 1 digit)
|
||||
Pass [INPUT in DATETIME-LOCAL status] The value is less than min(with millisecond in 2 digits)
|
||||
Pass [INPUT in DATETIME-LOCAL status] The value is less than min(with millisecond in 3 digits)
|
||||
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
|
||||
|
@ -33,9 +32,9 @@ Pass [INPUT in TIME status] The value attribute is an invalid time string
|
|||
Pass [INPUT in TIME status] The min attribute is less than value attribute
|
||||
Pass [INPUT in TIME status] The time missing second and minute parts is invalid
|
||||
Pass [INPUT in TIME status] The value attribute is less than min attribute
|
||||
Fail [INPUT in TIME status] The value is less than min(with millisecond in 1 digit)
|
||||
Fail [INPUT in TIME status] The value is less than min(with millisecond in 2 digit)
|
||||
Fail [INPUT in TIME status] The value is less than min(with millisecond in 3 digit)
|
||||
Pass [INPUT in TIME status] The value is less than min(with millisecond in 1 digit)
|
||||
Pass [INPUT in TIME status] The value is less than min(with millisecond in 2 digit)
|
||||
Pass [INPUT in TIME status] The value is less than min(with millisecond in 3 digit)
|
||||
Pass [INPUT in TIME status] The time missing second part is valid
|
||||
Pass [INPUT in TIME status] The time is max for reversed range
|
||||
Pass [INPUT in TIME status] The time is outside the accepted range for reversed range
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
Harness status: OK
|
||||
|
||||
Found 12 tests
|
||||
|
||||
12 Pass
|
||||
Pass Expected valueAsNumber=946729801000 from 2000-01-01T12:30:01
|
||||
Pass Expected digits unchanged in round-trip of 2000-01-01T12:30:01
|
||||
Pass Expected valueAsNumber=946729800500 from 2000-01-01T12:30:00.5
|
||||
Pass Expected digits unchanged in round-trip of 2000-01-01T12:30:00.5
|
||||
Pass Expected valueAsNumber=946729800040 from 2000-01-01T12:30:00.04
|
||||
Pass Expected digits unchanged in round-trip of 2000-01-01T12:30:00.04
|
||||
Pass Expected valueAsNumber=946729800003 from 2000-01-01T12:30:00.003
|
||||
Pass Expected digits unchanged in round-trip of 2000-01-01T12:30:00.003
|
||||
Pass Expected valueAsNumber=45001000 from 12:30:01
|
||||
Pass Expected valueAsNumber=45000500 from 12:30:00.5
|
||||
Pass Expected valueAsNumber=45000040 from 12:30:00.04
|
||||
Pass Expected valueAsNumber=45000003 from 12:30:00.003
|
|
@ -4,10 +4,10 @@
|
|||
test(() => {
|
||||
const inputElement = document.createElement("input");
|
||||
inputElement.type = "time";
|
||||
inputElement.value = "invalid";
|
||||
inputElement.value = "xx:34:56";
|
||||
inputElement.value = "12:xx:56";
|
||||
inputElement.value = "12:34:xx";
|
||||
["invalid", "xx:34:56", "12:xx:56", "12:34:xx", "12:34:60"].forEach(value => {
|
||||
inputElement.value = value;
|
||||
println(`Parsed ${value} as ${inputElement.valueAsNumber}`);
|
||||
});
|
||||
println("PASS (didn't crash)");
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
<!DOCTYPE HTML>
|
||||
<meta charset="utf-8">
|
||||
<html>
|
||||
<head>
|
||||
<title>HTMLInputElement leading zeroes in seconds/millis</title>
|
||||
<script src="../../../../resources/testharness.js"></script>
|
||||
<script src="../../../../resources/testharnessreport.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h3>input times and datetimes with leading zeroes in seconds/millis</h3>
|
||||
<!-- This test ensures that seconds and milliseconds are being
|
||||
output with the appropriate field widths when sanitizing
|
||||
datetime-locals and times, e.g. that we don't see "12:30:1".
|
||||
The spec is not specific about how much precision to use
|
||||
in a sanitized time string, but an invalid string would
|
||||
fail at .valueAsNumber -->
|
||||
<hr>
|
||||
<div id="log"></div>
|
||||
|
||||
<input id="inp">
|
||||
<script>
|
||||
var inp=document.getElementById("inp");
|
||||
var cases = [
|
||||
["datetime-local", "2000-01-01T12:30:01", 946729801000],
|
||||
["datetime-local", "2000-01-01T12:30:00.5", 946729800500],
|
||||
["datetime-local", "2000-01-01T12:30:00.04", 946729800040],
|
||||
["datetime-local", "2000-01-01T12:30:00.003", 946729800003],
|
||||
|
||||
["time", "12:30:01", 45001000],
|
||||
["time", "12:30:00.5", 45000500],
|
||||
["time", "12:30:00.04", 45000040],
|
||||
["time", "12:30:00.003", 45000003],
|
||||
];
|
||||
|
||||
for (var i in cases) {
|
||||
var c = cases[i];
|
||||
test(function() {
|
||||
inp.setAttribute("type", c[0]);
|
||||
inp.value = c[1];
|
||||
assert_equals(inp.valueAsNumber, c[2]);
|
||||
},"Expected valueAsNumber=" +c[2] + " from " + c[1]);
|
||||
if (c[0] == "datetime-local") {
|
||||
test(function() {
|
||||
inp.setAttribute("type", c[0]);
|
||||
inp.value = c[1];
|
||||
assert_in_array(inp.value, [c[1], c[1].replace("T", " ")]);
|
||||
},"Expected digits unchanged in round-trip of " + c[1])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Add table
Reference in a new issue