use alloc::string::String;
use alloc::vec::Vec;
use super::{collapse, document_order, non_empty};
use crate::node::{NodeId, NodeKind};
use crate::serialize::{is_space, walk};
use crate::tags::{self, Display};
use crate::tree::{Dom, NodeRef};
use crate::url::{is_javascript, normalize, resolve_url, srcset_candidates};
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct Link<'a> {
pub node: NodeRef<'a>,
pub href: &'a str,
pub url: String,
pub text: String,
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct Resource<'a> {
pub node: NodeRef<'a>,
pub attr: &'static str,
pub value: &'a str,
pub url: String,
}
impl Dom {
pub fn base_url(&self, base: Option<&str>) -> Option<String> {
let mut tag = None;
for root in self.roots() {
walk(root, |node, enter| match node.tag_name() {
_ if !enter || tag.is_some() => false,
Some("base") => {
tag = node.attr("href");
false
}
Some("template" | "svg" | "math") => false,
Some(_) => true,
None => false,
});
if tag.is_some() {
break;
}
}
match (tag, base) {
(Some(href), Some(base)) => Some(resolve_url(base, href)),
(Some(href), None) => Some(normalize(href)).filter(|h| !h.is_empty()),
(None, Some(base)) => Some(normalize(base)),
(None, None) => None,
}
}
pub fn links(&self, base: Option<&str>) -> Vec<Link<'_>> {
let base = self.base_url(base);
let base = base.as_deref().unwrap_or("");
let mut links: Vec<Link<'_>> = Vec::new();
let mut collector = TextCollector::default();
for root in self.roots() {
walk(root, |node, enter| {
collector.visit(node, enter, |node, href| {
links.push(Link {
node,
href,
url: resolve_url(base, href),
text: String::new(),
});
links.len() - 1
})
});
}
for (i, state) in collector.done.into_iter().enumerate() {
if let Some(link) = links.get_mut(i) {
link.text = state.finish(link.node);
}
}
links
}
pub fn resources(&self, base: Option<&str>) -> Vec<Resource<'_>> {
let base = self.base_url(base);
let base = base.as_deref().unwrap_or("");
let mut out = Vec::new();
for node in document_order(self) {
let Some(tag) = node.tag_name() else {
continue;
};
let mut attrs: [Option<&'static str>; 5] =
[None, Some("src"), Some("srcset"), Some("poster"), None];
if tag == "link" {
attrs[0] = Some("href");
}
if tag == "object" {
attrs[4] = Some("data");
}
for attr in attrs.into_iter().flatten() {
let Some(value) = node.attr(attr) else {
continue;
};
if attr == "srcset" {
for (url, _) in srcset_candidates(value) {
add_resource(&mut out, base, node, attr, url);
}
} else {
add_resource(&mut out, base, node, attr, value);
}
}
}
out
}
}
fn add_resource<'a>(
out: &mut Vec<Resource<'a>>,
base: &str,
node: NodeRef<'a>,
attr: &'static str,
value: &'a str,
) {
if non_empty(value).is_some() && !is_javascript(value) {
out.push(Resource {
node,
attr,
value,
url: resolve_url(base, value),
});
}
}
#[derive(Default)]
struct TextCollector<'a> {
open: Vec<(NodeRef<'a>, usize)>,
done: Vec<LinkText<'a>>,
hidden: Option<NodeId>,
}
#[derive(Default)]
struct LinkText<'a> {
text: String,
space: bool,
alt: Option<&'a str>,
}
impl<'a> TextCollector<'a> {
fn visit(
&mut self,
node: NodeRef<'a>,
enter: bool,
mut found: impl FnMut(NodeRef<'a>, &'a str) -> usize,
) -> bool {
let tag = match node.kind() {
NodeKind::Element { tag } => tag,
NodeKind::Text(t) => {
if self.hidden.is_none()
&& let Some(link) = self.current()
{
link.push(t);
}
return false;
}
NodeKind::Comment(_) | NodeKind::Doctype(_) => return false,
};
if !enter {
if self.open.last().is_some_and(|(n, _)| n.id() == node.id()) {
self.open.pop();
}
if self.hidden.is_some() {
if self.hidden == Some(node.id()) {
self.hidden = None;
}
return false;
}
if tags::display(tag) != Display::Inline
&& let Some(link) = self.current()
{
link.space = true;
}
return false;
}
let display = tags::display(tag);
if display == Display::Hidden {
return false;
}
if self.hidden.is_none() && !self.open.is_empty() && node.hides_itself() {
self.hidden = Some(node.id());
}
if self.hidden.is_none()
&& let Some(link) = self.current()
{
if display != Display::Inline || tag == "br" {
link.space = true;
}
if tag == "img" && link.alt.is_none() {
link.alt = node.attr("alt").and_then(non_empty);
}
}
if matches!(tag, "a" | "area")
&& let Some(href) = node.attr("href").filter(|h| !is_javascript(h))
{
let index = found(node, href);
self.done.push(LinkText::default());
self.open.push((node, index));
}
true
}
fn current(&mut self) -> Option<&mut LinkText<'a>> {
let &(_, index) = self.open.last()?;
self.done.get_mut(index)
}
}
impl<'a> LinkText<'a> {
fn push(&mut self, text: &str) {
for (i, word) in text.split(is_space).enumerate() {
if i > 0 {
self.space = true;
}
if !word.is_empty() {
if self.space && !self.text.is_empty() {
self.text.push(' ');
}
self.space = false;
self.text.push_str(word);
}
}
}
fn finish(self, node: NodeRef<'a>) -> String {
if !self.text.is_empty() {
return self.text;
}
let attr = |name| node.attr(name).and_then(non_empty);
attr("aria-label")
.or_else(|| attr("alt"))
.or(self.alt)
.or_else(|| attr("title"))
.map(collapse)
.unwrap_or_default()
}
}