Skip to main content

leyline/
html.rs

1mod assign;
2mod build;
3mod directives;
4mod entity;
5mod fieldset;
6mod page;
7mod scan;
8mod sitemap;
9
10pub(crate) use sitemap::Sitemap;
11
12use crate::core::lookup;
13
14pub(crate) const WEB_SCHEMES: [&str; 2] = ["http", "https"];
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
17#[non_exhaustive]
18pub enum FormMethod {
19    #[default]
20    Get,
21    Post,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
25#[non_exhaustive]
26pub enum FormEnctype {
27    #[default]
28    UrlEncoded,
29    Multipart,
30    TextPlain,
31}
32
33impl FormEnctype {
34    const ALL: [FormEnctype; 3] = [Self::UrlEncoded, Self::Multipart, Self::TextPlain];
35
36    #[must_use]
37    pub fn as_str(self) -> &'static str {
38        match self {
39            Self::UrlEncoded => "application/x-www-form-urlencoded",
40            Self::Multipart => "multipart/form-data",
41            Self::TextPlain => "text/plain",
42        }
43    }
44
45    fn parse(value: &str) -> Self {
46        let value = value.trim();
47        Self::ALL
48            .into_iter()
49            .find(|enctype| enctype.as_str().eq_ignore_ascii_case(value))
50            .unwrap_or_default()
51    }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Default)]
55#[non_exhaustive]
56pub struct Form {
57    action: String,
58    method: FormMethod,
59    enctype: FormEnctype,
60    base: Option<String>,
61    id: Option<String>,
62    name: Option<String>,
63    fields: Vec<(String, String)>,
64    buttons: Vec<Button>,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68struct Button {
69    name: String,
70    value: String,
71    image: bool,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
75#[non_exhaustive]
76pub struct Anchor {
77    href: String,
78    text: String,
79    rel: Option<String>,
80}
81
82#[must_use]
83pub fn meta(document: &str, name: &str) -> Option<String> {
84    page::meta(document, name)
85}
86
87#[must_use]
88pub fn links(document: &str) -> Vec<Anchor> {
89    page::links(document)
90}
91
92#[must_use]
93pub fn text(document: &str) -> String {
94    page::text(document)
95}
96
97pub(crate) fn base(document: &str) -> Option<String> {
98    page::base(document)
99}
100
101#[must_use]
102pub fn canonical(document: &str) -> Option<String> {
103    page::canonical(document)
104}
105
106pub(crate) fn sitemap(document: &str) -> Sitemap {
107    sitemap::read(document)
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111#[non_exhaustive]
112pub struct Directives {
113    index: bool,
114    follow: bool,
115    ai: bool,
116}
117
118impl Directives {
119    #[must_use]
120    pub fn read(document: &str, headers: &http::HeaderMap, agent: &str) -> Directives {
121        directives::read(document, headers, agent)
122    }
123
124    #[must_use]
125    pub fn index(&self) -> bool {
126        self.index
127    }
128
129    #[must_use]
130    pub fn follow(&self) -> bool {
131        self.follow
132    }
133
134    #[must_use]
135    pub fn ai(&self) -> bool {
136        self.ai
137    }
138}
139
140impl Anchor {
141    #[must_use]
142    pub fn href(&self) -> &str {
143        &self.href
144    }
145
146    #[must_use]
147    pub fn text(&self) -> &str {
148        &self.text
149    }
150
151    #[must_use]
152    pub fn rel(&self) -> Option<&str> {
153        self.rel.as_deref()
154    }
155}
156
157#[must_use]
158pub fn forms(document: &str) -> Vec<Form> {
159    build::forms(document)
160}
161
162impl Form {
163    #[must_use]
164    pub fn find(document: &str, selector: &str) -> Option<Form> {
165        forms(document).into_iter().find(|form| {
166            form.id.as_deref() == Some(selector) || form.name.as_deref() == Some(selector)
167        })
168    }
169
170    #[must_use]
171    pub fn action(&self) -> &str {
172        &self.action
173    }
174
175    #[must_use]
176    pub fn method(&self) -> FormMethod {
177        self.method
178    }
179
180    #[must_use]
181    pub fn enctype(&self) -> FormEnctype {
182        self.enctype
183    }
184
185    #[must_use]
186    pub fn base(&self) -> Option<&str> {
187        self.base.as_deref()
188    }
189
190    #[must_use]
191    pub fn id(&self) -> Option<&str> {
192        self.id.as_deref()
193    }
194
195    #[must_use]
196    pub fn name(&self) -> Option<&str> {
197        self.name.as_deref()
198    }
199
200    #[must_use]
201    pub fn fields(&self) -> &[(String, String)] {
202        &self.fields
203    }
204
205    #[must_use]
206    pub fn field(&self, name: &str) -> Option<&str> {
207        self.fields
208            .iter()
209            .find(|(key, _)| key == name)
210            .map(|(_, value)| value.as_str())
211    }
212
213    pub fn set(&mut self, name: impl Into<String>, value: impl Into<String>) -> &mut Self {
214        let name = name.into();
215        let value = value.into();
216        match self.fields.iter().position(|(key, _)| *key == name) {
217            Some(first) => {
218                self.fields[first].1 = value;
219                let mut index = 0;
220                self.fields.retain(|(key, _)| {
221                    let keep = index <= first || *key != name;
222                    index += 1;
223                    keep
224                });
225            }
226            None => self.fields.push((name, value)),
227        }
228        self
229    }
230
231    #[must_use]
232    pub fn buttons(&self) -> Vec<&str> {
233        self.buttons.iter().map(|b| b.name.as_str()).collect()
234    }
235
236    pub fn press(&mut self, button: &str) -> bool {
237        let Some(found) = self.buttons.iter().find(|b| b.name == button).cloned() else {
238            return false;
239        };
240        if found.image {
241            self.fields
242                .push((format!("{}.x", found.name), "0".to_owned()));
243            self.fields
244                .push((format!("{}.y", found.name), "0".to_owned()));
245        } else {
246            self.fields.push((found.name, found.value));
247        }
248        true
249    }
250}