rama_http/protocols/html/rewrite/
mod.rs1use super::selector::Selector;
17use super::selector::ast::{Combinator, ComplexSelector, Compound, NthType};
18use super::tokenizer::{LocalNameHash, StartTag};
19
20mod element;
21mod rewriter;
22
23#[cfg(test)]
24mod tests;
25
26pub use self::element::{
27 AttributeName, Element, ElementContentHandler, HandlerResult, InvalidAttributeName,
28};
29pub use self::rewriter::{ElementContentHandlers, HtmlRewriter, rewrite_str};
30
31const MAX_COMPOUNDS: usize = 63;
34
35const VOID_AREA: LocalNameHash = LocalNameHash::from_static(b"area");
43const VOID_BASE: LocalNameHash = LocalNameHash::from_static(b"base");
44const VOID_BR: LocalNameHash = LocalNameHash::from_static(b"br");
45const VOID_COL: LocalNameHash = LocalNameHash::from_static(b"col");
46const VOID_EMBED: LocalNameHash = LocalNameHash::from_static(b"embed");
47const VOID_HR: LocalNameHash = LocalNameHash::from_static(b"hr");
48const VOID_IMG: LocalNameHash = LocalNameHash::from_static(b"img");
49const VOID_INPUT: LocalNameHash = LocalNameHash::from_static(b"input");
50const VOID_LINK: LocalNameHash = LocalNameHash::from_static(b"link");
51const VOID_META: LocalNameHash = LocalNameHash::from_static(b"meta");
52const VOID_SOURCE: LocalNameHash = LocalNameHash::from_static(b"source");
53const VOID_TRACK: LocalNameHash = LocalNameHash::from_static(b"track");
54const VOID_WBR: LocalNameHash = LocalNameHash::from_static(b"wbr");
55
56const ADDRESS: LocalNameHash = LocalNameHash::from_static(b"address");
59const ARTICLE: LocalNameHash = LocalNameHash::from_static(b"article");
60const ASIDE: LocalNameHash = LocalNameHash::from_static(b"aside");
61const BLOCKQUOTE: LocalNameHash = LocalNameHash::from_static(b"blockquote");
62const DD: LocalNameHash = LocalNameHash::from_static(b"dd");
63const DETAILS: LocalNameHash = LocalNameHash::from_static(b"details");
64const DIV: LocalNameHash = LocalNameHash::from_static(b"div");
65const DL: LocalNameHash = LocalNameHash::from_static(b"dl");
66const DT: LocalNameHash = LocalNameHash::from_static(b"dt");
67const FIELDSET: LocalNameHash = LocalNameHash::from_static(b"fieldset");
68const FIGCAPTION: LocalNameHash = LocalNameHash::from_static(b"figcaption");
69const FIGURE: LocalNameHash = LocalNameHash::from_static(b"figure");
70const FOOTER: LocalNameHash = LocalNameHash::from_static(b"footer");
71const FORM: LocalNameHash = LocalNameHash::from_static(b"form");
72const H1: LocalNameHash = LocalNameHash::from_static(b"h1");
73const H2: LocalNameHash = LocalNameHash::from_static(b"h2");
74const H3: LocalNameHash = LocalNameHash::from_static(b"h3");
75const H4: LocalNameHash = LocalNameHash::from_static(b"h4");
76const H5: LocalNameHash = LocalNameHash::from_static(b"h5");
77const H6: LocalNameHash = LocalNameHash::from_static(b"h6");
78const HEADER: LocalNameHash = LocalNameHash::from_static(b"header");
79const HGROUP: LocalNameHash = LocalNameHash::from_static(b"hgroup");
80const LI: LocalNameHash = LocalNameHash::from_static(b"li");
81const MAIN: LocalNameHash = LocalNameHash::from_static(b"main");
82const MENU: LocalNameHash = LocalNameHash::from_static(b"menu");
83const NAV: LocalNameHash = LocalNameHash::from_static(b"nav");
84const OL: LocalNameHash = LocalNameHash::from_static(b"ol");
85const OPTGROUP: LocalNameHash = LocalNameHash::from_static(b"optgroup");
86const OPTION: LocalNameHash = LocalNameHash::from_static(b"option");
87const P: LocalNameHash = LocalNameHash::from_static(b"p");
88const PRE: LocalNameHash = LocalNameHash::from_static(b"pre");
89const RB: LocalNameHash = LocalNameHash::from_static(b"rb");
90const RP: LocalNameHash = LocalNameHash::from_static(b"rp");
91const RT: LocalNameHash = LocalNameHash::from_static(b"rt");
92const RTC: LocalNameHash = LocalNameHash::from_static(b"rtc");
93const SEARCH: LocalNameHash = LocalNameHash::from_static(b"search");
94const SECTION: LocalNameHash = LocalNameHash::from_static(b"section");
95const TABLE: LocalNameHash = LocalNameHash::from_static(b"table");
96const TBODY: LocalNameHash = LocalNameHash::from_static(b"tbody");
97const TD: LocalNameHash = LocalNameHash::from_static(b"td");
98const TFOOT: LocalNameHash = LocalNameHash::from_static(b"tfoot");
99const TH: LocalNameHash = LocalNameHash::from_static(b"th");
100const THEAD: LocalNameHash = LocalNameHash::from_static(b"thead");
101const TR: LocalNameHash = LocalNameHash::from_static(b"tr");
102const UL: LocalNameHash = LocalNameHash::from_static(b"ul");
103
104fn is_void(name: LocalNameHash) -> bool {
108 matches!(
109 name,
110 VOID_AREA
111 | VOID_BASE
112 | VOID_BR
113 | VOID_COL
114 | VOID_EMBED
115 | VOID_HR
116 | VOID_IMG
117 | VOID_INPUT
118 | VOID_LINK
119 | VOID_META
120 | VOID_SOURCE
121 | VOID_TRACK
122 | VOID_WBR
123 )
124}
125
126#[derive(Debug, Clone, Copy)]
128struct SelectorState {
129 completed: u64,
131 desc_ready: u64,
134}
135
136impl SelectorState {
137 const ROOT: Self = Self {
138 completed: 1,
139 desc_ready: 1,
140 };
141}
142
143#[derive(Debug)]
145struct Frame {
146 name: LocalNameHash,
147 child_count: usize,
148 type_counts: Vec<(LocalNameHash, usize)>,
149}
150
151impl Frame {
152 fn new(name: LocalNameHash) -> Self {
153 Self {
154 name,
155 child_count: 0,
156 type_counts: Vec::new(),
157 }
158 }
159
160 fn type_count(&self, name: LocalNameHash) -> usize {
161 self.type_counts
162 .iter()
163 .find(|(n, _)| *n == name)
164 .map_or(0, |(_, c)| *c)
165 }
166
167 fn record_child(&mut self, name: LocalNameHash, track_type: bool) {
168 self.child_count += 1;
169 if track_type {
172 if let Some(entry) = self.type_counts.iter_mut().find(|(n, _)| *n == name) {
173 entry.1 += 1;
174 } else {
175 self.type_counts.push((name, 1));
176 }
177 }
178 }
179}
180
181#[derive(Debug)]
185pub(crate) struct SelectorMatcher {
186 selectors: Vec<ComplexSelector>,
188 owner: Vec<usize>,
191 stack: Vec<Frame>,
192 states: Vec<SelectorState>,
195 scratch: Vec<SelectorState>,
197 matched: Vec<usize>,
199 tracks_type: bool,
202}
203
204impl SelectorMatcher {
205 #[must_use]
208 pub(crate) fn new(selectors: &[Selector]) -> Self {
209 let mut complexes = Vec::new();
210 let mut owner = Vec::new();
211 for (index, selector) in selectors.iter().enumerate() {
212 for complex in &selector.selectors {
213 complexes.push(complex.clone());
214 owner.push(index);
215 }
216 }
217 let n = complexes.len();
218 let tracks_type = complexes.iter().any(complex_uses_nth_of_type);
219 Self {
220 selectors: complexes,
221 owner,
222 stack: vec![Frame::new(LocalNameHash::NONE)],
223 states: vec![SelectorState::ROOT; n],
224 scratch: Vec::with_capacity(n),
225 matched: Vec::new(),
226 tracks_type,
227 }
228 }
229
230 pub(crate) fn push_element(
238 &mut self,
239 tag: &StartTag<'_>,
240 mut on_match: impl FnMut(usize),
241 ) -> bool {
242 let n = self.selectors.len();
243 let name = tag.name_hash();
244 let parent_index = self.stack.len() - 1;
245 let parent = &self.stack[parent_index];
246 let nth_child = parent.child_count + 1;
247 let nth_of_type = if self.tracks_type {
249 parent.type_count(name) + 1
250 } else {
251 1
252 };
253
254 self.scratch.clear();
255 self.matched.clear();
256 let parent_row = parent_index * n;
257 for s in 0..n {
258 let parent_state = self.states[parent_row + s];
259 let complex = &self.selectors[s];
260 let (state, matched) =
261 eval_selector(complex, parent_state, tag, nth_child, nth_of_type);
262 if matched && !self.matched.contains(&self.owner[s]) {
263 self.matched.push(self.owner[s]);
264 }
265 self.scratch.push(state);
266 }
267 for &index in &self.matched {
268 on_match(index);
269 }
270
271 self.stack[parent_index].record_child(name, self.tracks_type);
272
273 let opened = !is_void(name) && !tag.is_self_closing();
274 if opened {
275 self.stack.push(Frame::new(name));
276 self.states.extend_from_slice(&self.scratch);
277 }
278 opened
279 }
280
281 pub(crate) fn pop_implied_for_start(&mut self, name: LocalNameHash) -> usize {
288 let mut popped = 0;
289 match name {
290 LI => popped += self.pop_nearest(&[LI]),
291 DD | DT => popped += self.pop_nearest(&[DD, DT]),
292 OPTION => popped += self.pop_nearest(&[OPTION]),
293 OPTGROUP => {
294 popped += self.pop_nearest(&[OPTION]);
295 popped += self.pop_nearest(&[OPTGROUP]);
296 }
297 RB | RT | RTC | RP => popped += self.pop_nearest(&[RB, RT, RTC, RP]),
298 TR => {
299 popped += self.pop_nearest(&[TD, TH]);
300 popped += self.pop_nearest(&[TR]);
301 }
302 TD | TH => popped += self.pop_nearest(&[TD, TH]),
303 THEAD | TBODY | TFOOT => {
304 popped += self.pop_nearest(&[TD, TH]);
305 popped += self.pop_nearest(&[TR]);
306 popped += self.pop_nearest(&[THEAD, TBODY, TFOOT]);
307 }
308 _ if closes_p(name) => popped += self.pop_nearest(&[P]),
309 _ => {}
310 }
311 popped
312 }
313
314 pub(crate) fn pop_element(&mut self, name: LocalNameHash) -> usize {
325 let Some(pos) = self.stack.iter().rposition(|f| f.name == name) else {
332 return 0;
333 };
334 if pos == 0 {
335 return 0;
336 }
337 let popped = self.stack.len() - pos;
338 self.stack.truncate(pos);
339 self.states.truncate(pos * self.selectors.len());
340 popped
341 }
342
343 pub(crate) fn finish(&mut self) -> usize {
345 let popped = self.stack.len().saturating_sub(1);
346 self.stack.truncate(1);
347 self.states.truncate(self.selectors.len());
348 popped
349 }
350
351 fn pop_nearest(&mut self, names: &[LocalNameHash]) -> usize {
352 let Some(pos) = self
353 .stack
354 .iter()
355 .rposition(|frame| names.contains(&frame.name))
356 else {
357 return 0;
358 };
359 if pos == 0 {
360 return 0;
361 }
362 let popped = self.stack.len() - pos;
363 self.stack.truncate(pos);
364 self.states.truncate(pos * self.selectors.len());
365 popped
366 }
367}
368
369fn closes_p(name: LocalNameHash) -> bool {
370 matches!(
371 name,
372 ADDRESS
373 | ARTICLE
374 | ASIDE
375 | BLOCKQUOTE
376 | DD
377 | DETAILS
378 | DIV
379 | DL
380 | DT
381 | FIELDSET
382 | FIGCAPTION
383 | FIGURE
384 | FOOTER
385 | FORM
386 | H1
387 | H2
388 | H3
389 | H4
390 | H5
391 | H6
392 | HEADER
393 | HGROUP
394 | VOID_HR
395 | MAIN
396 | MENU
397 | NAV
398 | OL
399 | P
400 | PRE
401 | SEARCH
402 | SECTION
403 | TABLE
404 | UL
405 )
406}
407
408fn complex_uses_nth_of_type(complex: &ComplexSelector) -> bool {
409 complex
410 .parts
411 .iter()
412 .any(|part| compound_uses_nth_of_type(&part.compound))
413}
414
415fn compound_uses_nth_of_type(compound: &Compound) -> bool {
416 compound.nth.iter().any(|nth| nth.ty == NthType::OfType)
417 || compound.negations.iter().any(compound_uses_nth_of_type)
418}
419
420fn eval_selector(
423 complex: &ComplexSelector,
424 parent: SelectorState,
425 tag: &StartTag<'_>,
426 nth_child: usize,
427 nth_of_type: usize,
428) -> (SelectorState, bool) {
429 let k = complex.parts.len();
430 if k == 0 || k > MAX_COMPOUNDS {
431 return (SelectorState::ROOT, false);
432 }
433
434 let mut ready = 1u64;
436 for i in 1..k {
437 let bit = 1u64 << i;
438 match complex.parts[i].combinator {
439 Some(Combinator::Child) if parent.completed & bit != 0 => ready |= bit,
440 Some(Combinator::Descendant) if parent.desc_ready & bit != 0 => ready |= bit,
441 _ => {}
442 }
443 }
444
445 let mut completed = 1u64;
447 for i in 0..k {
448 if ready & (1u64 << i) != 0
449 && compound_matches(&complex.parts[i].compound, tag, nth_child, nth_of_type)
450 {
451 completed |= 1u64 << (i + 1);
452 }
453 }
454 let matched = completed & (1u64 << k) != 0;
455
456 let mut desc_ready = 1u64;
458 for i in 1..k {
459 if complex.parts[i].combinator == Some(Combinator::Descendant) {
460 let bit = 1u64 << i;
461 if (completed | parent.desc_ready) & bit != 0 {
462 desc_ready |= bit;
463 }
464 }
465 }
466
467 (
468 SelectorState {
469 completed,
470 desc_ready,
471 },
472 matched,
473 )
474}
475
476fn compound_matches(
479 compound: &Compound,
480 tag: &StartTag<'_>,
481 nth_child: usize,
482 nth_of_type: usize,
483) -> bool {
484 if let Some(name) = &compound.name
485 && !name.matches_bytes(tag.name())
486 {
487 return false;
488 }
489 if let Some(id) = &compound.id
490 && attribute(tag, b"id") != Some(id.as_bytes())
491 {
492 return false;
493 }
494 for class in &compound.classes {
495 let class = class.as_bytes();
496 let present = attribute(tag, b"class")
497 .is_some_and(|value| value.split(u8::is_ascii_whitespace).any(|c| c == class));
498 if !present {
499 return false;
500 }
501 }
502 for attr in &compound.attributes {
503 if !attribute(tag, attr.name.as_bytes()).is_some_and(|value| attr.matches_value(value)) {
504 return false;
505 }
506 }
507 for nth in &compound.nth {
508 let index = match nth.ty {
509 NthType::Child => nth_child,
510 NthType::OfType => nth_of_type,
511 };
512 if !nth.matches_index(index) {
513 return false;
514 }
515 }
516 !compound
517 .negations
518 .iter()
519 .any(|neg| compound_matches(neg, tag, nth_child, nth_of_type))
520}
521
522fn attribute<'i>(tag: &StartTag<'i>, name: &[u8]) -> Option<&'i [u8]> {
524 tag.attributes()
525 .find(|attr| attr.name().eq_ignore_ascii_case(name))
526 .map(|attr| attr.value())
527}