mirror of
https://github.com/lexmount/moli.git
synced 2026-10-07 00:01:00 +00:00
fix(forms): wrap textarea submission values
This commit is contained in:
@@ -39,10 +39,10 @@ pub use option::{
|
||||
option_disabled_ancestor_step,
|
||||
};
|
||||
pub use text::{
|
||||
normalize_custom_validation_message, normalize_form_submission_newlines,
|
||||
parse_non_negative_integer_prefix, parse_non_negative_length_attribute,
|
||||
parse_positive_integer_prefix, text_control_suffers_too_long, text_control_suffers_too_short,
|
||||
text_control_value_length,
|
||||
apply_textarea_wrapping_transformation, normalize_custom_validation_message,
|
||||
normalize_form_submission_newlines, parse_non_negative_integer_prefix,
|
||||
parse_non_negative_length_attribute, parse_positive_integer_prefix,
|
||||
text_control_suffers_too_long, text_control_suffers_too_short, text_control_value_length,
|
||||
};
|
||||
pub use validity::FormControlValidity;
|
||||
|
||||
|
||||
@@ -59,6 +59,8 @@ fn integer_prefix_parsers_follow_html_attribute_rules() {
|
||||
assert_eq!(parse_non_negative_integer_prefix("999999999999"), 0);
|
||||
|
||||
assert_eq!(parse_positive_integer_prefix(" 12px"), Some(12));
|
||||
assert_eq!(parse_positive_integer_prefix(" +12px"), Some(12));
|
||||
assert_eq!(parse_positive_integer_prefix("\u{a0}12px"), None);
|
||||
assert_eq!(parse_positive_integer_prefix("0"), None);
|
||||
assert_eq!(parse_positive_integer_prefix("-1"), None);
|
||||
assert_eq!(parse_positive_integer_prefix("abc"), None);
|
||||
@@ -71,6 +73,27 @@ fn integer_prefix_parsers_follow_html_attribute_rules() {
|
||||
assert_eq!(parse_non_negative_length_attribute("2147483648"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn textarea_wrapping_transformation_respects_hard_state_and_character_width() {
|
||||
let wrap = |value: &str, wrap, cols| {
|
||||
apply_textarea_wrapping_transformation(value.to_owned(), wrap, cols)
|
||||
};
|
||||
|
||||
assert_eq!(wrap("hello world", Some("soft"), Some("7")), "hello world");
|
||||
assert_eq!(wrap("hello world", Some("ſoft"), Some("7")), "hello world");
|
||||
assert_eq!(wrap("1234567", Some("hard"), Some("7")), "1234567");
|
||||
assert_eq!(
|
||||
wrap("hello world", Some("HaRd"), Some("7")),
|
||||
"hello w\norld"
|
||||
);
|
||||
assert_eq!(wrap("ab\ncdef", Some("hard"), Some("3")), "ab\ncde\nf");
|
||||
assert_eq!(wrap("é🙂x", Some("hard"), Some("2")), "é🙂\nx");
|
||||
assert_eq!(
|
||||
wrap("123456789012345678901", Some("hard"), None),
|
||||
"12345678901234567890\n1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_type_tokens_and_support_matrices_are_shared() {
|
||||
assert!(InputType::Week.supports_value_as_number());
|
||||
|
||||
@@ -34,7 +34,12 @@ pub fn parse_non_negative_integer_prefix(value: &str) -> i32 {
|
||||
}
|
||||
|
||||
pub fn parse_positive_integer_prefix(value: &str) -> Option<u32> {
|
||||
let digits = integer_prefix_digits(value);
|
||||
let value = value.trim_start_matches(|ch: char| ch.is_ascii_whitespace());
|
||||
let value = value.strip_prefix('+').unwrap_or(value);
|
||||
let digits = value
|
||||
.chars()
|
||||
.take_while(|ch| ch.is_ascii_digit())
|
||||
.collect::<String>();
|
||||
digits
|
||||
.parse::<i32>()
|
||||
.ok()
|
||||
@@ -42,6 +47,40 @@ pub fn parse_positive_integer_prefix(value: &str) -> Option<u32> {
|
||||
.map(|value| value as u32)
|
||||
}
|
||||
|
||||
pub fn apply_textarea_wrapping_transformation(
|
||||
value: String,
|
||||
wrap_attribute: Option<&str>,
|
||||
cols_attribute: Option<&str>,
|
||||
) -> String {
|
||||
if !wrap_attribute.is_some_and(|wrap| wrap.eq_ignore_ascii_case("hard")) {
|
||||
return value;
|
||||
}
|
||||
let character_width = cols_attribute
|
||||
.and_then(parse_positive_integer_prefix)
|
||||
.filter(|width| *width <= i32::MAX as u32)
|
||||
.unwrap_or(20);
|
||||
hard_wrap_textarea_value(&value, character_width)
|
||||
}
|
||||
|
||||
fn hard_wrap_textarea_value(value: &str, character_width: u32) -> String {
|
||||
let mut output = String::with_capacity(value.len());
|
||||
let mut current_width = 0;
|
||||
for character in value.chars() {
|
||||
if character == '\n' {
|
||||
output.push(character);
|
||||
current_width = 0;
|
||||
continue;
|
||||
}
|
||||
if current_width == character_width {
|
||||
output.push('\n');
|
||||
current_width = 0;
|
||||
}
|
||||
output.push(character);
|
||||
current_width += 1;
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
pub fn parse_non_negative_length_attribute(value: &str) -> Option<usize> {
|
||||
parse_html_integer_prefix(value)
|
||||
.filter(|value| *value >= 0)
|
||||
|
||||
@@ -2,7 +2,10 @@ use super::storage::{form_data_entries, form_data_is_object, push_form_data_entr
|
||||
use super::*;
|
||||
use crate::custom_elements::is_form_associated_custom_element_handle;
|
||||
use crate::dom::{
|
||||
forms::{InputType, OptionDisabledAncestorStep, option_disabled_ancestor_step},
|
||||
forms::{
|
||||
InputType, OptionDisabledAncestorStep, apply_textarea_wrapping_transformation,
|
||||
option_disabled_ancestor_step,
|
||||
},
|
||||
native::Node,
|
||||
};
|
||||
use crate::native_bridge::{
|
||||
@@ -399,9 +402,16 @@ fn native_form_control_value(
|
||||
let (runtime_ptr, handle) = node_runtime_and_handle_from_object(scope, control).ok()?;
|
||||
let runtime = unsafe { &*runtime_ptr };
|
||||
let element = runtime.dom_host().node(handle).and_then(Node::as_element)?;
|
||||
if element.is_html_input() || element.is_html_textarea() {
|
||||
if element.is_html_input() {
|
||||
return Some(text_control_value(runtime, handle));
|
||||
}
|
||||
if element.is_html_textarea() {
|
||||
return Some(apply_textarea_wrapping_transformation(
|
||||
text_control_value(runtime, handle),
|
||||
element.attribute_ns("", "wrap"),
|
||||
element.attribute_ns("", "cols"),
|
||||
));
|
||||
}
|
||||
if element.is_html_option() {
|
||||
return Some(element.option_value(runtime.dom_host().dom(), handle));
|
||||
}
|
||||
|
||||
+7
-3
@@ -348,14 +348,18 @@ fn detached_control_submission_value<'s>(
|
||||
return crate::native_bridge::element::text_control_value(runtime, handle);
|
||||
}
|
||||
if element.is_html_textarea() {
|
||||
let value = crate::native_bridge::element::text_control_value(runtime, handle);
|
||||
let mut value = crate::native_bridge::element::text_control_value(runtime, handle);
|
||||
if !element.input_value_dirty()
|
||||
&& value.is_empty()
|
||||
&& let Some(attribute_value) = element.attribute_ns("", "value")
|
||||
{
|
||||
return attribute_value.to_owned();
|
||||
value = attribute_value.to_owned();
|
||||
}
|
||||
return value;
|
||||
return moli_dom::forms::apply_textarea_wrapping_transformation(
|
||||
value,
|
||||
element.attribute_ns("", "wrap"),
|
||||
element.attribute_ns("", "cols"),
|
||||
);
|
||||
}
|
||||
if element.is_html_button() {
|
||||
return element
|
||||
|
||||
@@ -5593,6 +5593,79 @@ fn empty_get_form_submit_replaces_existing_action_query() {
|
||||
.expect("empty GET form submit should queue a pending location navigation");
|
||||
assert_eq!(pending.url.as_str(), "https://form-empty-get.test/submit?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn textarea_hard_wrap_is_ascii_case_insensitive_in_form_entries() {
|
||||
let mut vm = new_storage_test_vm("https://textarea-hard-wrap.test/path/index.html");
|
||||
|
||||
let form_data_values = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const root = document.documentElement || document.appendChild(document.createElement('html'));
|
||||
const body = document.body || root.appendChild(document.createElement('body'));
|
||||
const form = document.createElement('form');
|
||||
form.action = '/submit';
|
||||
const add = (name, wrap, cols, value) => {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.name = name;
|
||||
textarea.setAttribute('wrap', wrap);
|
||||
textarea.setAttribute('cols', cols);
|
||||
textarea.value = value;
|
||||
form.append(textarea);
|
||||
return textarea;
|
||||
};
|
||||
const lower = add('lower', 'hard', '7', 'hello world');
|
||||
add('mixed', 'HaRd', '7', 'hello world');
|
||||
add('soft', 'SoFt', '7', 'hello world');
|
||||
add('invalid', 'ſoft', '7', 'hello world');
|
||||
add('existing', 'HARD', '3', 'ab\ncdef');
|
||||
body.append(form);
|
||||
const data = new FormData(form);
|
||||
const visible = value => value.replaceAll('\n', '<LF>');
|
||||
const result = [
|
||||
lower.value.includes('\n'),
|
||||
...['lower', 'mixed', 'soft', 'invalid', 'existing'].map(name => visible(data.get(name)))
|
||||
].join('|');
|
||||
form.submit();
|
||||
return result;
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("textarea hard-wrap form submission should evaluate");
|
||||
|
||||
assert_eq!(
|
||||
form_data_values,
|
||||
"false|hello w<LF>orld|hello w<LF>orld|hello world|hello world|ab<LF>cde<LF>f"
|
||||
);
|
||||
|
||||
let pending = vm
|
||||
.take_pending_location_navigation_with_seed()
|
||||
.expect("textarea GET submission should queue a pending navigation");
|
||||
let values = pending
|
||||
.url
|
||||
.query_pairs()
|
||||
.into_owned()
|
||||
.collect::<std::collections::HashMap<_, _>>();
|
||||
assert_eq!(
|
||||
values.get("lower").map(String::as_str),
|
||||
Some("hello w\r\norld")
|
||||
);
|
||||
assert_eq!(
|
||||
values.get("mixed").map(String::as_str),
|
||||
Some("hello w\r\norld")
|
||||
);
|
||||
assert_eq!(values.get("soft").map(String::as_str), Some("hello world"));
|
||||
assert_eq!(
|
||||
values.get("invalid").map(String::as_str),
|
||||
Some("hello world")
|
||||
);
|
||||
assert_eq!(
|
||||
values.get("existing").map(String::as_str),
|
||||
Some("ab\r\ncde\r\nf")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_form_submit_dispatches_cancelable_navigate_event_with_source_element() {
|
||||
let mut vm = new_storage_test_vm("https://form-get-navigate.test/path/index.html");
|
||||
|
||||
Reference in New Issue
Block a user