use std::collections::HashMap;
use crate::models::token::TagToken;
use crate::utils::builder::dom::{Dom, NodeId};
const NOAH_ARK_CAPACITY: usize = 3;
#[derive(Clone, Debug)]
pub(crate) enum Entry {
Marker,
Element(ElementEntry),
}
#[derive(Clone, Debug)]
pub(crate) struct ElementEntry {
pub(crate) id: usize,
pub(crate) element: NodeId,
pub(crate) token: TagToken,
segment: usize,
identity: String,
}
#[derive(Debug, Default)]
struct Counts {
names: HashMap<String, usize>,
identities: HashMap<String, usize>,
}
#[derive(Debug, Default)]
pub(crate) struct FormattingList {
pub(crate) entries: Vec<Entry>,
pub(crate) bookmark: Option<usize>,
next_id: usize,
counts: Vec<Counts>,
}
impl FormattingList {
pub(crate) fn insert_marker(&mut self) {
if self.counts.is_empty() {
self.counts.push(Counts::default());
}
self.entries.push(Entry::Marker);
self.counts.push(Counts::default());
}
pub(crate) fn push_element(&mut self, dom: &Dom, element: NodeId, token: TagToken) {
let identity = create_identity(dom, element);
if self.count(&identity) >= NOAH_ARK_CAPACITY {
self.ensure_noah_ark(dom, element);
}
let entry = self.create_entry(element, token, identity, self.segment());
self.entries.push(Entry::Element(entry));
}
fn segment(&self) -> usize {
self.counts.len().saturating_sub(1)
}
fn count(&self, identity: &str) -> usize {
self.counts
.last()
.and_then(|counts| counts.identities.get(identity))
.copied()
.unwrap_or(0)
}
fn create_entry(
&mut self,
element: NodeId,
token: TagToken,
identity: String,
segment: usize,
) -> ElementEntry {
if self.counts.is_empty() {
self.counts.push(Counts::default());
}
let last = self.counts.len() - 1;
let counts = &mut self.counts[segment.min(last)];
*counts.names.entry(token.tag_name.clone()).or_default() += 1;
*counts.identities.entry(identity.clone()).or_default() += 1;
let id = self.create_id();
ElementEntry {
id,
element,
token,
segment,
identity,
}
}
fn forget(&mut self, entry: &Entry) {
let Entry::Element(entry) = entry else {
return;
};
if let Some(counts) = self.counts.get_mut(entry.segment) {
if let Some(count) = counts.names.get_mut(&entry.token.tag_name) {
*count = count.saturating_sub(1);
}
if let Some(count) = counts.identities.get_mut(&entry.identity) {
*count = count.saturating_sub(1);
}
}
}
fn ensure_noah_ark(&mut self, dom: &Dom, element: NodeId) {
if self.entries.len() < NOAH_ARK_CAPACITY {
return;
}
let Some(new) = dom.element(element) else {
return;
};
let mut candidates = Vec::new();
for (index, entry) in self.entries.iter().enumerate().rev() {
let Entry::Element(entry) = entry else {
break;
};
if let Some(old) = dom.element(entry.element)
&& old.tag_name == new.tag_name
&& old.namespace == new.namespace
&& old.attrs.len() == new.attrs.len()
{
candidates.push((index, entry.element));
}
}
if candidates.len() < NOAH_ARK_CAPACITY {
return;
}
let mut valid = 0;
for (index, candidate) in candidates {
let Some(old) = dom.element(candidate) else {
continue;
};
let matches = old.attrs.iter().all(|old| {
new.attrs
.iter()
.rev()
.find(|each| each.name == old.name)
.map(|each| &each.value)
== Some(&old.value)
});
if matches {
valid += 1;
if valid >= NOAH_ARK_CAPACITY {
let entry = self.entries.remove(index);
self.forget(&entry);
return;
}
}
}
}
pub(crate) fn insert_element_after_bookmark(
&mut self,
dom: &Dom,
element: NodeId,
token: TagToken,
) {
let identity = create_identity(dom, element);
let position = self.bookmark.and_then(|bookmark| self.position(bookmark));
let segment = match position.map(|index| &self.entries[index]) {
Some(Entry::Element(entry)) => entry.segment,
_ => self.segment(),
};
let entry = Entry::Element(self.create_entry(element, token, identity, segment));
match position {
Some(index) => self.entries.insert(index + 1, entry),
None => self.entries.push(entry),
}
}
pub(crate) fn remove_entry(&mut self, id: usize) {
if let Some(index) = self.position(id) {
let entry = self.entries.remove(index);
self.forget(&entry);
}
}
pub(crate) fn clear_to_last_marker(&mut self) {
while let Some(entry) = self.entries.pop() {
if matches!(entry, Entry::Marker) {
self.counts.pop();
break;
}
self.forget(&entry);
}
}
pub(crate) fn find_in_scope(
&self,
dom: &Dom,
tag_name: &str,
) -> Option<ElementEntry> {
let count = self
.counts
.last()
.and_then(|counts| counts.names.get(tag_name));
if count.is_none_or(|count| *count == 0) {
return None;
}
for entry in self.entries.iter().rev() {
match entry {
Entry::Marker => return None,
Entry::Element(entry) if dom.tag_name(entry.element) == tag_name => {
return Some(entry.clone());
}
Entry::Element(_) => {}
}
}
None
}
pub(crate) fn find_by_element(&self, element: NodeId) -> Option<ElementEntry> {
self.entries.iter().rev().find_map(|entry| match entry {
Entry::Element(entry) if entry.element == element => Some(entry.clone()),
_ => None,
})
}
pub(crate) fn find_by_id(&self, id: usize) -> Option<ElementEntry> {
self.position(id)
.and_then(|index| match &self.entries[index] {
Entry::Element(entry) => Some(entry.clone()),
Entry::Marker => None,
})
}
pub(crate) fn set_element(&mut self, id: usize, element: NodeId) {
if let Some(index) = self.position(id)
&& let Entry::Element(entry) = &mut self.entries[index]
{
entry.element = element;
}
}
fn position(&self, id: usize) -> Option<usize> {
self.entries
.iter()
.rposition(|entry| matches!(entry, Entry::Element(entry) if entry.id == id))
}
fn create_id(&mut self) -> usize {
self.next_id += 1;
self.next_id
}
}
fn create_identity(dom: &Dom, element: NodeId) -> String {
let Some(element) = dom.element(element) else {
return String::new();
};
let mut attributes: Vec<(&str, &str)> = element
.attrs
.iter()
.map(|attribute| (attribute.name.as_str(), attribute.value.as_str()))
.collect();
attributes.sort_unstable();
let mut identity = format!("{}\0{:?}", element.tag_name, element.namespace);
for (name, value) in attributes {
identity.push('\0');
identity.push_str(name);
identity.push('=');
identity.push_str(value);
}
identity
}