Skip to content

Commit

Permalink
Foundations for CSS OM (#165)
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett authored May 2, 2022
1 parent 37a1bce commit 1b947b4
Show file tree
Hide file tree
Showing 33 changed files with 2,860 additions and 747 deletions.
195 changes: 193 additions & 2 deletions src/declaration.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
//! CSS declarations.
use std::borrow::Cow;

use crate::context::PropertyHandlerContext;
use crate::error::{ParserError, PrinterError};
use crate::parser::ParserOptions;
use crate::printer::Printer;
use crate::properties::box_shadow::BoxShadowHandler;
use crate::properties::masking::MaskHandler;
use crate::properties::Property;
use crate::properties::{
align::AlignHandler,
animation::AnimationHandler,
Expand All @@ -27,8 +28,10 @@ use crate::properties::{
transform::TransformHandler,
transition::TransitionHandler,
};
use crate::properties::{Property, PropertyId};
use crate::targets::Browsers;
use crate::traits::{PropertyHandler, ToCss};
use crate::values::string::CowArcStr;
use cssparser::*;

/// A CSS declaration block.
Expand Down Expand Up @@ -71,10 +74,46 @@ impl<'i> DeclarationBlock<'i> {
declarations,
})
}

/// Parses a declaration block from a string.
pub fn parse_string(input: &'i str, options: ParserOptions) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let mut input = ParserInput::new(input);
let mut parser = Parser::new(&mut input);
let result = Self::parse(&mut parser, &options)?;
parser.expect_exhausted()?;
Ok(result)
}
}

impl<'i> ToCss for DeclarationBlock<'i> {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
let len = self.declarations.len() + self.important_declarations.len();
let mut i = 0;

macro_rules! write {
($decls: expr, $important: literal) => {
for decl in &$decls {
decl.to_css(dest, $important)?;
if i != len - 1 {
dest.write_char(';')?;
dest.whitespace()?;
}
i += 1;
}
};
}

write!(self.declarations, false);
write!(self.important_declarations, true);
Ok(())
}
}

impl<'i> DeclarationBlock<'i> {
pub(crate) fn to_css_block<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
Expand Down Expand Up @@ -142,6 +181,156 @@ impl<'i> DeclarationBlock<'i> {
}
}

impl<'i> DeclarationBlock<'i> {
/// Returns an iterator over all properties in the declaration.
pub fn iter(&self) -> impl std::iter::DoubleEndedIterator<Item = (&Property<'i>, bool)> {
self
.declarations
.iter()
.map(|property| (property, false))
.chain(self.important_declarations.iter().map(|property| (property, true)))
}

/// Returns a mutable iterator over all properties in the declaration.
pub fn iter_mut(&mut self) -> impl std::iter::DoubleEndedIterator<Item = &mut Property<'i>> {
self.declarations.iter_mut().chain(self.important_declarations.iter_mut())
}

/// Returns the value for a given property id based on the properties in this declaration block.
///
/// If the property is a shorthand, the result will be a combined value of all of the included
/// longhands, or `None` if some of the longhands are not declared. Otherwise, the value will be
/// either an explicitly declared longhand, or a value extracted from a shorthand property.
pub fn get<'a>(&'a self, property_id: &PropertyId) -> Option<(Cow<'a, Property<'i>>, bool)> {
if property_id.is_shorthand() {
if let Some((shorthand, important)) = property_id.shorthand_value(&self) {
return Some((Cow::Owned(shorthand), important));
}
} else {
for (property, important) in self.iter().rev() {
if property.property_id() == *property_id {
return Some((Cow::Borrowed(property), important));
}

if let Some(val) = property.longhand(&property_id) {
return Some((Cow::Owned(val), important));
}
}
}

None
}

/// Sets the value and importance for a given property, replacing any existing declarations.
///
/// If the property already exists within the declaration block, it is updated in place. Otherwise,
/// a new declaration is appended. When updating a longhand property and a shorthand is defined which
/// includes the longhand, the shorthand will be updated rather than appending a new declaration.
pub fn set(&mut self, property: Property<'i>, important: bool) {
let property_id = property.property_id();
let declarations = if important {
// Remove any non-important properties with this id.
self.declarations.retain(|decl| decl.property_id() != property_id);
&mut self.important_declarations
} else {
// Remove any important properties with this id.
self.important_declarations.retain(|decl| decl.property_id() != property_id);
&mut self.declarations
};

let longhands = property_id.longhands().unwrap_or_else(|| vec![property.property_id()]);

for decl in declarations.iter_mut().rev() {
{
// If any of the longhands being set are in the same logical property group as any of the
// longhands in this property, but in a different category (i.e. logical or physical),
// then we cannot modify in place, and need to append a new property.
let id = decl.property_id();
let id_longhands = id.longhands().unwrap_or_else(|| vec![id]);
if longhands.iter().any(|longhand| {
let logical_group = longhand.logical_group();
let category = longhand.category();

logical_group.is_some()
&& id_longhands.iter().any(|id_longhand| {
logical_group == id_longhand.logical_group() && category != id_longhand.category()
})
}) {
break;
}
}

if decl.property_id() == property_id {
*decl = property;
return;
}

// Update shorthand.
if decl.set_longhand(&property).is_ok() {
return;
}
}

declarations.push(property)
}

/// Removes all declarations of the given property id from the declaration block.
///
/// When removing a longhand property and a shorthand is defined which includes the longhand,
/// the shorthand will be split apart into its component longhand properties, minus the property
/// to remove. When removing a shorthand, all included longhand properties are also removed.
pub fn remove(&mut self, property_id: &PropertyId) {
fn remove<'i, 'a>(declarations: &mut Vec<Property<'i>>, property_id: &PropertyId<'a>) {
let longhands = property_id.longhands().unwrap_or(vec![]);
let mut i = 0;
while i < declarations.len() {
let replacement = {
let property = &declarations[i];
let id = property.property_id();
if id == *property_id || longhands.contains(&id) {
// If the property matches the requested property id, or is a longhand
// property that is included in the requested shorthand, remove it.
None
} else if longhands.is_empty() && id.longhands().unwrap_or(vec![]).contains(&property_id) {
// If this is a shorthand property that includes the requested longhand,
// split it apart into its component longhands, excluding the requested one.
Some(
id.longhands()
.unwrap()
.iter()
.filter_map(|longhand| {
if *longhand == *property_id {
None
} else {
property.longhand(longhand)
}
})
.collect::<Vec<Property>>(),
)
} else {
i += 1;
continue;
}
};

match replacement {
Some(properties) => {
let count = properties.len();
declarations.splice(i..i + 1, properties);
i += count;
}
None => {
declarations.remove(i);
}
}
}
}

remove(&mut self.declarations, property_id);
remove(&mut self.important_declarations, property_id);
}
}

struct PropertyDeclarationParser<'a, 'i> {
important_declarations: &'a mut Vec<Property<'i>>,
declarations: &'a mut Vec<Property<'i>>,
Expand Down Expand Up @@ -182,7 +371,9 @@ pub(crate) fn parse_declaration<'i, 't>(
important_declarations: &mut DeclarationList<'i>,
options: &ParserOptions,
) -> Result<(), cssparser::ParseError<'i, ParserError<'i>>> {
let property = input.parse_until_before(Delimiter::Bang, |input| Property::parse(name, input, options))?;
let property = input.parse_until_before(Delimiter::Bang, |input| {
Property::parse(PropertyId::from(CowArcStr::from(name)), input, options)
})?;
let important = input
.try_parse(|input| {
input.expect_delim('!')?;
Expand Down
5 changes: 2 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -566,8 +566,7 @@ mod tests {
indoc! {r#"
.foo {
border-block-width: 1px;
border-inline-start-width: 2px;
border-inline-end-width: 3px;
border-inline-width: 2px 3px;
}
"#
},
Expand Down Expand Up @@ -17798,7 +17797,7 @@ mod tests {
}"#}
);

let property = Property::parse_string("color", "#f0f", ParserOptions::default()).unwrap();
let property = Property::parse_string("color".into(), "#f0f", ParserOptions::default()).unwrap();
assert_eq!(
property.to_css_string(false, PrinterOptions::default()).unwrap(),
"color: #f0f"
Expand Down
18 changes: 17 additions & 1 deletion src/logical.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#[derive(Debug, PartialEq)]
pub(crate) enum PropertyCategory {
pub enum PropertyCategory {
Logical,
Physical,
}
Expand All @@ -9,3 +9,19 @@ impl Default for PropertyCategory {
PropertyCategory::Physical
}
}

#[derive(PartialEq)]
pub enum LogicalGroup {
BorderColor,
BorderStyle,
BorderWidth,
BorderRadius,
Margin,
ScrollMargin,
Padding,
ScrollPadding,
Inset,
Size,
MinSize,
MaxSize,
}
Loading

0 comments on commit 1b947b4

Please sign in to comment.