use std::collections::HashMap;
use super::assign::{Entry, Item, Owner, assign};
use super::entity::decode;
use super::fieldset::Fieldsets;
use super::scan::{Scanner, Tag};
use super::{Button, Form, FormEnctype, FormMethod, lookup};
#[derive(Clone, Copy, PartialEq, Eq)]
enum Control {
Value,
Checkbox,
Radio,
Submit,
Image,
Ignored,
}
const INPUT_TYPES: [(&str, Control); 7] = [
("checkbox", Control::Checkbox),
("radio", Control::Radio),
("submit", Control::Submit),
("image", Control::Image),
("button", Control::Ignored),
("reset", Control::Ignored),
("file", Control::Ignored),
];
const BUTTON_TYPES: [(&str, Control); 2] =
[("button", Control::Ignored), ("reset", Control::Ignored)];
const TOGGLE_DEFAULT: &str = "on";
struct Select {
field: Option<(Owner, String)>,
single: bool,
listbox: bool,
group_disabled: bool,
first: Option<String>,
chosen: Vec<String>,
}
#[derive(Default)]
struct Builder {
forms: Vec<Form>,
open: Option<usize>,
entries: Vec<Entry>,
select: Option<Select>,
fieldsets: Fieldsets,
base: Option<String>,
ids: HashMap<String, Option<usize>>,
}
pub(super) fn forms(document: &str) -> Vec<Form> {
let mut builder = Builder {
base: super::base(document),
..Builder::default()
};
for tag in Scanner::new(document) {
builder.tag(&tag);
}
builder.finish()
}
impl Builder {
fn tag(&mut self, tag: &Tag<'_>) {
match (tag.closing, tag.name.as_str()) {
(false, "form") if self.open.is_none() => {
self.close_select();
self.forms.push(open(tag));
self.open = Some(self.forms.len() - 1);
self.record(tag, self.open);
}
(false, "form") => {}
(true, "form") => self.close_form(),
_ => {
if !tag.closing {
self.record(tag, None);
}
self.fieldsets.tag(tag);
self.control(tag);
}
}
}
fn record(&mut self, tag: &Tag<'_>, form: Option<usize>) {
if let Some(id) = tag.attr("id").filter(|id| !id.is_empty()) {
self.ids.entry(id.to_owned()).or_insert(form);
}
}
fn control(&mut self, tag: &Tag<'_>) {
match (tag.closing, tag.name.as_str()) {
(false, "input") => self.input(tag),
(false, "button") => self.button(tag),
(false, "textarea") => self.textarea(tag),
(false, "select") => {
self.close_select();
self.select = Some(open_select(self.field(tag), tag));
}
(false, "optgroup") => self.optgroup(tag.has("disabled")),
(true, "optgroup") => self.optgroup(false),
(false, "option") => self.option(tag),
(true, "select") => self.close_select(),
_ => {}
}
}
fn field(&self, tag: &Tag<'_>) -> Option<(Owner, String)> {
if tag.has("disabled") || self.fieldsets.disabled() {
return None;
}
let name = tag.attr("name").filter(|name| !name.is_empty())?;
let owner = match tag.attr("form") {
Some("") => return None,
Some(id) => Owner::Id(id.to_owned()),
None => Owner::Open(self.open?),
};
Some((owner, name.to_owned()))
}
fn push(&mut self, owner: Owner, name: String, value: String, radio: bool) {
self.entries.push(Entry {
owner,
item: Item::Field { name, value, radio },
});
}
fn input(&mut self, tag: &Tag<'_>) {
let Some((owner, name)) = self.field(tag) else {
return;
};
let value = tag.attr("value");
let toggled = value.unwrap_or(TOGGLE_DEFAULT).to_owned();
match tag.kind(&INPUT_TYPES, Control::Value) {
Control::Value => self.push(owner, name, value.unwrap_or_default().to_owned(), false),
Control::Checkbox if tag.has("checked") => self.push(owner, name, toggled, false),
Control::Radio if tag.has("checked") => self.push(owner, name, toggled, true),
Control::Submit => self.add_button(owner, name, value, false),
Control::Image => self.add_button(owner, name, value, true),
Control::Checkbox | Control::Radio | Control::Ignored => {}
}
}
fn button(&mut self, tag: &Tag<'_>) {
let Some((owner, name)) = self.field(tag) else {
return;
};
if tag.kind(&BUTTON_TYPES, Control::Submit) == Control::Submit {
self.add_button(owner, name, tag.attr("value"), false);
}
}
fn add_button(&mut self, owner: Owner, name: String, value: Option<&str>, image: bool) {
self.entries.push(Entry {
owner,
item: Item::Button(Button {
name,
value: value.unwrap_or_default().to_owned(),
image,
}),
});
}
fn textarea(&mut self, tag: &Tag<'_>) {
let Some((owner, name)) = self.field(tag) else {
return;
};
let raw = tag.raw.unwrap_or_default();
let raw = raw
.strip_prefix("\r\n")
.or_else(|| raw.strip_prefix('\n'))
.unwrap_or(raw);
self.push(owner, name, decode(raw), false);
}
fn optgroup(&mut self, disabled: bool) {
if let Some(select) = self.select.as_mut() {
select.group_disabled = disabled;
}
}
fn option(&mut self, tag: &Tag<'_>) {
let Some(select) = self.select.as_mut() else {
return;
};
if select.group_disabled || tag.has("disabled") {
return;
}
let value = match tag.attr("value") {
Some(value) => value.to_owned(),
None => collapse(&decode(&tag.text)),
};
if select.first.is_none() {
select.first = Some(value.clone());
}
if tag.has("selected") {
select.chosen.push(value);
}
}
fn close_select(&mut self) {
let Some(select) = self.select.take() else {
return;
};
let Some((owner, name)) = select.field else {
return;
};
let values = match (select.single, select.chosen.is_empty()) {
(false, _) => select.chosen,
(true, false) => select.chosen.last().cloned().into_iter().collect(),
(true, true) if select.listbox => Vec::new(),
(true, true) => select.first.into_iter().collect(),
};
for value in values {
self.push(owner.clone(), name.clone(), value, false);
}
}
fn close_form(&mut self) {
self.close_select();
self.open = None;
}
fn finish(mut self) -> Vec<Form> {
self.close_form();
assign(&mut self.forms, self.entries, &self.ids);
for form in &mut self.forms {
form.base.clone_from(&self.base);
}
self.forms
}
}
fn open_select(field: Option<(Owner, String)>, tag: &Tag<'_>) -> Select {
let size = tag
.attr("size")
.and_then(|size| size.trim().parse::<u64>().ok())
.unwrap_or(0);
Select {
field,
single: !tag.has("multiple"),
listbox: size > 1,
group_disabled: false,
first: None,
chosen: Vec::new(),
}
}
fn open(tag: &Tag<'_>) -> Form {
let method = match tag.attr("method") {
Some(method) if method.trim().eq_ignore_ascii_case("post") => FormMethod::Post,
_ => FormMethod::Get,
};
Form {
action: tag.attr("action").unwrap_or_default().trim().to_owned(),
method,
enctype: tag
.attr("enctype")
.map(FormEnctype::parse)
.unwrap_or_default(),
base: None,
id: tag.attr("id").map(str::to_owned),
name: tag.attr("name").map(str::to_owned),
fields: Vec::new(),
buttons: Vec::new(),
}
}
impl Tag<'_> {
fn kind(&self, table: &[(&str, Control)], default: Control) -> Control {
let Some(kind) = self.attr("type").map(str::trim) else {
return default;
};
lookup(table, kind).unwrap_or(default)
}
}
pub(super) fn collapse(text: &str) -> String {
text.split_ascii_whitespace().collect::<Vec<_>>().join(" ")
}