diff --git a/src/declaration.rs b/src/declaration.rs index 9ed84e33..b8e62efc 100644 --- a/src/declaration.rs +++ b/src/declaration.rs @@ -1,6 +1,7 @@ //! CSS declarations. use std::borrow::Cow; +use std::ops::Range; use crate::context::PropertyHandlerContext; use crate::error::{ParserError, PrinterError}; @@ -179,6 +180,43 @@ impl<'i> DeclarationBlock<'i> { pub fn is_empty(&self) -> bool { return self.declarations.is_empty() && self.important_declarations.is_empty(); } + + pub(crate) fn property_location<'t>( + &self, + input: &mut Parser<'i, 't>, + index: usize, + ) -> Result<(Range, Range), ParseError<'i, ParserError<'i>>> { + // Skip to the requested property index. + for _ in 0..index { + input.expect_ident()?; + input.expect_colon()?; + input.parse_until_after(Delimiter::Semicolon, |parser| { + while parser.next().is_ok() {} + Ok(()) + })?; + } + + // Get property name range. + input.skip_whitespace(); + let key_start = input.current_source_location(); + input.expect_ident()?; + let key_end = input.current_source_location(); + let key_range = key_start..key_end; + + input.expect_colon()?; + input.skip_whitespace(); + + // Get value range. + let val_start = input.current_source_location(); + input.parse_until_before(Delimiter::Semicolon, |parser| { + while parser.next().is_ok() {} + Ok(()) + })?; + let val_end = input.current_source_location(); + let val_range = val_start..val_end; + + Ok((key_range, val_range)) + } } impl<'i> DeclarationBlock<'i> { diff --git a/src/dependencies.rs b/src/dependencies.rs index 6bfa5049..1f31af92 100644 --- a/src/dependencies.rs +++ b/src/dependencies.rs @@ -104,7 +104,7 @@ pub struct SourceRange { pub file_path: String, /// The starting line and column position of the dependency. pub start: Location, - /// THe ending line and column position of the dependency. + /// The ending line and column position of the dependency. pub end: Location, } diff --git a/src/error.rs b/src/error.rs index 5afe12a9..d1945c98 100644 --- a/src/error.rs +++ b/src/error.rs @@ -36,7 +36,7 @@ pub struct ErrorLocation { pub filename: String, /// The line number, starting from 0. pub line: u32, - /// THe column number, starting from 1. + /// The column number, starting from 1. pub column: u32, } diff --git a/src/lib.rs b/src/lib.rs index c6cf9687..6c8e5ee3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,6 +51,7 @@ mod tests { use crate::targets::Browsers; use crate::traits::{Parse, ToCss}; use crate::values::color::CssColor; + use cssparser::SourceLocation; use indoc::indoc; use std::collections::HashMap; @@ -17960,6 +17961,60 @@ mod tests { property.to_css_string(true, PrinterOptions::default()).unwrap(), "color: #f0f !important" ); + + let code = indoc! { r#" + .foo { + color: green; + } + + .bar { + color: red; + background: pink; + } + + @media print { + .baz { + color: green; + } + } + "#}; + let stylesheet = StyleSheet::parse("test.css", code, ParserOptions::default()).unwrap(); + if let CssRule::Style(style) = &stylesheet.rules.0[1] { + let (key, val) = style.property_location(code, 0).unwrap(); + assert_eq!( + key, + SourceLocation { line: 5, column: 3 }..SourceLocation { line: 5, column: 8 } + ); + assert_eq!( + val, + SourceLocation { line: 5, column: 10 }..SourceLocation { line: 5, column: 13 } + ); + } + + if let CssRule::Style(style) = &stylesheet.rules.0[1] { + let (key, val) = style.property_location(code, 1).unwrap(); + assert_eq!( + key, + SourceLocation { line: 6, column: 3 }..SourceLocation { line: 6, column: 13 } + ); + assert_eq!( + val, + SourceLocation { line: 6, column: 15 }..SourceLocation { line: 6, column: 19 } + ); + } + if let CssRule::Media(media) = &stylesheet.rules.0[2] { + if let CssRule::Style(style) = &media.rules.0[0] { + let (key, val) = style.property_location(code, 0).unwrap(); + assert_eq!( + key, + SourceLocation { line: 11, column: 5 }..SourceLocation { line: 11, column: 10 } + ); + assert_eq!( + val, + SourceLocation { line: 11, column: 12 }..SourceLocation { line: 11, column: 17 } + ); + } + } } #[test] diff --git a/src/rules/style.rs b/src/rules/style.rs index 9b30d724..cbaf4c4b 100644 --- a/src/rules/style.rs +++ b/src/rules/style.rs @@ -1,10 +1,13 @@ //! Style rules. +use std::ops::Range; + use super::Location; use super::MinifyContext; use crate::compat::Feature; use crate::context::DeclarationContext; use crate::declaration::DeclarationBlock; +use crate::error::ParserError; use crate::error::{MinifyError, PrinterError, PrinterErrorKind}; use crate::printer::Printer; use crate::rules::{CssRuleList, StyleContext, ToCssWithContext}; @@ -12,6 +15,7 @@ use crate::selector::{is_compatible, is_unused, Selectors}; use crate::targets::Browsers; use crate::traits::ToCss; use crate::vendor_prefix::VendorPrefix; +use cssparser::*; use parcel_selectors::SelectorList; /// A CSS [style rule](https://drafts.csswg.org/css-syntax/#style-rules). @@ -74,6 +78,67 @@ impl<'i> StyleRule<'i> { pub fn is_compatible(&self, targets: Option) -> bool { is_compatible(&self.selectors, targets) } + + /// Returns the line and column range of the property key and value at the given index in this style rule. + /// + /// For performance and memory efficiency in non-error cases, source locations are not stored during parsing. + /// Instead, they are computed lazily using the original source string that was used to parse the stylesheet/rule. + pub fn property_location<'t>( + &self, + code: &'i str, + index: usize, + ) -> Result<(Range, Range), ParseError<'i, ParserError<'i>>> { + let mut input = ParserInput::new(code); + let mut parser = Parser::new(&mut input); + + // advance until start location of this rule. + parse_at(&mut parser, self.loc, |parser| { + // skip selector + parser.parse_until_before(Delimiter::CurlyBracketBlock, |parser| { + while parser.next().is_ok() {} + Ok(()) + })?; + + parser.expect_curly_bracket_block()?; + parser.parse_nested_block(|parser| { + let loc = self.declarations.property_location(parser, index); + while parser.next().is_ok() {} + loc + }) + }) + } +} + +fn parse_at<'i, 't, T, F>( + parser: &mut Parser<'i, 't>, + dest: Location, + parse: F, +) -> Result>> +where + F: Copy + for<'tt> FnOnce(&mut Parser<'i, 'tt>) -> Result>>, +{ + loop { + let loc = parser.current_source_location(); + if loc.line >= dest.line || (loc.line == dest.line && loc.column >= dest.column) { + return parse(parser); + } + + match parser.next()? { + Token::CurlyBracketBlock => { + // Recursively parse nested blocks. + let res = parser.parse_nested_block(|parser| { + let res = parse_at(parser, dest, parse); + while parser.next().is_ok() {} + res + }); + + if let Ok(v) = res { + return Ok(v); + } + } + _ => {} + } + } } impl<'a, 'i> ToCssWithContext<'a, 'i> for StyleRule<'i> {