Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/declaration.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! CSS declarations.

use std::borrow::Cow;
use std::ops::Range;

use crate::context::PropertyHandlerContext;
use crate::error::{ParserError, PrinterError};
Expand Down Expand Up @@ -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<SourceLocation>, Range<SourceLocation>), 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> {
Expand Down
2 changes: 1 addition & 1 deletion src/dependencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down
2 changes: 1 addition & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down
55 changes: 55 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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]
Expand Down
65 changes: 65 additions & 0 deletions src/rules/style.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
//! 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};
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).
Expand Down Expand Up @@ -74,6 +78,67 @@ impl<'i> StyleRule<'i> {
pub fn is_compatible(&self, targets: Option<Browsers>) -> 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<SourceLocation>, Range<SourceLocation>), 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<T, ParseError<'i, ParserError<'i>>>
where
F: Copy + for<'tt> FnOnce(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, ParserError<'i>>>,
{
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> {
Expand Down