use std::convert::TryFrom;
use std::io::Write;
use std::ops::Deref;
use lightningcss::{
declaration::DeclarationBlock,
properties::{
display::{self, DisplayKeyword},
overflow::{Overflow, OverflowKeyword},
Property,
},
rules::CssRule,
stylesheet::{ParserOptions, StyleAttribute, StyleSheet},
traits::Parse,
values::color::CssColor,
};
use crate::{
markup5ever_rcdom::{
Handle,
NodeData::{self, Comment, Document, Element},
},
tree_map_reduce, Result, TreeMapResult,
};
#[derive(Debug, Clone)]
enum SelectorComponent {
Class(String),
Element(String),
Star,
CombChild,
CombDescendant,
}
#[derive(Debug, Clone)]
struct Selector {
components: Vec<SelectorComponent>,
}
impl Selector {
fn do_matches(comps: &[SelectorComponent], node: &Handle) -> bool {
match comps.first() {
None => return true,
Some(comp) => match comp {
SelectorComponent::Class(class) => match &node.data {
Document
| NodeData::Doctype { .. }
| NodeData::Text { .. }
| Comment { .. }
| NodeData::ProcessingInstruction { .. } => {
return false;
}
Element { attrs, .. } => {
let attrs = attrs.borrow();
for attr in attrs.iter() {
if &attr.name.local == "class" {
for cls in attr.value.split_whitespace() {
if cls == class {
return Self::do_matches(&comps[1..], node);
}
}
}
}
return false;
}
},
SelectorComponent::Element(name) => match &node.data {
Element { name: eltname, .. } => {
if name == eltname.expanded().local.deref() {
return Self::do_matches(&comps[1..], node);
} else {
return false;
}
}
_ => {
return false;
}
},
SelectorComponent::Star => {
return Self::do_matches(&comps[1..], node);
}
SelectorComponent::CombChild => {
if let Some(parent) = node.parent.take() {
let parent_handle = parent.upgrade();
node.parent.set(Some(parent));
if let Some(ph) = parent_handle {
return Self::do_matches(&comps[1..], &ph);
} else {
return false;
}
} else {
return false;
}
}
SelectorComponent::CombDescendant => {
if let Some(parent) = node.parent.take() {
let parent_handle = parent.upgrade();
node.parent.set(Some(parent));
if let Some(ph) = parent_handle {
return Self::do_matches(&comps[1..], &ph)
|| Self::do_matches(comps, &ph);
} else {
return false;
}
} else {
return false;
}
}
},
}
}
fn matches(&self, node: &Handle) -> bool {
Self::do_matches(&self.components, node)
}
}
impl<'r, 'i> TryFrom<&'r lightningcss::selector::Selector<'i>> for Selector {
type Error = ();
fn try_from(
selector: &'r lightningcss::selector::Selector<'i>,
) -> std::result::Result<Self, Self::Error> {
let mut components = Vec::new();
use lightningcss::selector::Combinator;
use lightningcss::selector::Component;
let mut si = selector.iter();
loop {
while let Some(item) = si.next() {
match item {
Component::Class(id) => {
components.push(SelectorComponent::Class(String::from(id.deref())));
}
Component::LocalName(name) => {
components.push(SelectorComponent::Element(String::from(
name.lower_name.deref(),
)));
}
Component::ExplicitUniversalType => {
components.push(SelectorComponent::Star);
}
_ => {
html_trace!("Unknown component {:?}", item);
return Err(());
}
}
}
if let Some(comb) = si.next_sequence() {
match comb {
Combinator::Child => {
components.push(SelectorComponent::CombChild);
}
Combinator::Descendant => {
components.push(SelectorComponent::CombDescendant);
}
_ => {
html_trace!("Unknown combinator {:?}", comb);
return Err(());
}
}
} else {
break;
}
}
Ok(Selector { components })
}
}
#[derive(Debug, Clone)]
pub(crate) enum Style {
Colour(CssColor),
BgColour(CssColor),
DisplayNone,
}
#[derive(Debug, Clone)]
struct Ruleset {
selector: Selector,
styles: Vec<Style>,
}
#[derive(Clone, Default, Debug)]
pub struct StyleData {
rules: Vec<Ruleset>,
}
pub(crate) fn parse_style_attribute(text: &str) -> Result<Vec<Style>> {
html_trace_quiet!("Parsing inline style: {text}");
let sattr = StyleAttribute::parse(text, ParserOptions::default())
.map_err(|_| crate::Error::CssParseError)?;
let styles = styles_from_properties(&sattr.declarations);
html_trace_quiet!("Parsed inline style: {:?}", styles);
Ok(styles)
}
fn is_transparent(color: &CssColor) -> bool {
match color {
CssColor::CurrentColor => false,
CssColor::RGBA(rgba) => rgba.alpha == 0,
CssColor::LAB(_) => false,
CssColor::Predefined(_) => false,
CssColor::Float(_) => false,
CssColor::LightDark(_, _) => false,
CssColor::System(_) => false,
}
}
fn styles_from_properties(decls: &DeclarationBlock<'_>) -> Vec<Style> {
let mut styles = Vec::new();
html_trace_quiet!("styles:from_properties: {decls:?}");
let mut overflow_hidden = false;
let mut height_zero = false;
for decl in decls
.declarations
.iter()
.chain(decls.important_declarations.iter())
{
html_trace_quiet!("styles:from_properties: {decl:?}");
match decl {
Property::Color(color) => {
if is_transparent(&color) {
continue;
}
styles.push(Style::Colour(color.clone()));
}
Property::Background(bginfo) => {
let color = bginfo.last().unwrap().color.clone();
if is_transparent(&color) {
continue;
}
styles.push(Style::BgColour(color));
}
Property::BackgroundColor(color) => {
if is_transparent(&color) {
continue;
}
styles.push(Style::BgColour(color.clone()));
}
Property::Height(height) => {
use lightningcss::properties::size::Size::*;
use lightningcss::values::percentage::DimensionPercentage::*;
match height {
LengthPercentage(Dimension(dim)) if dim.to_px() == Some(0.0) => {
height_zero = true;
}
_ => (),
}
}
Property::MaxHeight(height) => {
use lightningcss::properties::size::MaxSize::*;
use lightningcss::values::percentage::DimensionPercentage::*;
match height {
LengthPercentage(Dimension(dim)) => {
if Some(0.0) == dim.to_px() {
height_zero = true;
}
}
_ => (),
}
}
Property::OverflowY(OverflowKeyword::Hidden)
| Property::Overflow(Overflow {
y: OverflowKeyword::Hidden,
..
}) => {
overflow_hidden = true;
}
Property::Display(disp) => {
if let display::Display::Keyword(DisplayKeyword::None) = disp {
styles.push(Style::DisplayNone);
}
}
_ => {
html_trace_quiet!("CSS: Unhandled property {:?}", decl);
}
}
}
if height_zero && overflow_hidden {
styles.push(Style::DisplayNone);
}
styles
}
impl StyleData {
pub fn add_css(&mut self, css: &str) -> Result<()> {
let ss = StyleSheet::parse(css, ParserOptions::default())
.map_err(|_| crate::Error::CssParseError)?;
for rule in &ss.rules.0 {
match rule {
CssRule::Style(style) => {
let styles = styles_from_properties(&style.declarations);
if !styles.is_empty() {
for selector in &style.selectors.0 {
match Selector::try_from(selector) {
Ok(selector) => {
let ruleset = Ruleset {
selector,
styles: styles.clone(),
};
html_trace_quiet!("Adding ruleset {ruleset:?}");
self.rules.push(ruleset);
}
Err(_) => {
html_trace!("Ignoring selector {:?}", selector);
continue;
}
}
}
}
}
_ => (),
}
}
Ok(())
}
pub fn merge(&mut self, other: Self) {
self.rules.extend(other.rules);
}
pub(crate) fn matching_rules(&self, handle: &Handle, use_doc_css: bool) -> Vec<Style> {
let mut result = Vec::new();
for rule in &self.rules {
if rule.selector.matches(handle) {
result.extend(rule.styles.iter().cloned());
}
}
if use_doc_css {
if let Element { attrs, .. } = &handle.data {
let borrowed = attrs.borrow();
for attr in borrowed.iter() {
if &attr.name.local == "style" {
let rules = parse_style_attribute(&attr.value).unwrap_or_default();
result.extend(rules);
} else if &*attr.name.local == "color" {
if let Ok(colour) = CssColor::parse_string(&*attr.value) {
result.push(Style::Colour(colour));
}
} else if &*attr.name.local == "bgcolor" {
if let Ok(colour) = CssColor::parse_string(&*attr.value) {
result.push(Style::BgColour(colour));
}
}
}
}
}
result
}
}
fn pending<'a, F>(handle: Handle, f: F) -> TreeMapResult<'a, (), Handle, Vec<String>>
where
for<'r> F: Fn(&'r mut (), Vec<Vec<String>>) -> Result<Option<Vec<String>>> + 'static,
{
TreeMapResult::PendingChildren {
children: handle.children.borrow().clone(),
cons: Box::new(f),
prefn: None,
postfn: None,
}
}
fn combine_vecs(vecs: Vec<Vec<String>>) -> Vec<String> {
let mut it = vecs.into_iter();
let first = it.next();
match first {
None => Vec::new(),
Some(mut first) => {
for v in it {
first.extend(v.into_iter());
}
first
}
}
}
fn extract_style_nodes<'a, 'b, T: Write>(
handle: Handle,
_err_out: &'b mut T,
) -> TreeMapResult<'a, (), Handle, Vec<String>> {
use TreeMapResult::*;
match handle.clone().data {
Document => pending(handle, |&mut (), cs| Ok(Some(combine_vecs(cs)))),
Comment { .. } => Nothing,
Element { ref name, .. } => {
match name.expanded() {
expanded_name!(html "style") => {
let mut result = String::new();
for child in handle.children.borrow().iter() {
if let NodeData::Text { ref contents } = child.data {
result += &String::from(contents.borrow().deref());
}
}
Finished(vec![result])
}
_ => pending(handle, |_, cs| Ok(Some(combine_vecs(cs)))),
}
}
NodeData::Text {
contents: ref _tstr,
} => Nothing,
_ => {
Nothing
}
}
}
pub fn dom_to_stylesheet<T: Write>(handle: Handle, err_out: &mut T) -> Result<StyleData> {
let styles = tree_map_reduce(&mut (), handle, |_, handle| {
Ok(extract_style_nodes(handle, err_out))
})?;
let mut result = StyleData::default();
if let Some(styles) = styles {
for css in styles {
let _ = result.add_css(&css);
}
}
Ok(result)
}