Skip to content

Commit b1afb80

Browse files
authored
Add method to lazily compute property source ranges (parcel-bundler#169)
1 parent 839bcd1 commit b1afb80

File tree

5 files changed

+160
-2
lines changed

5 files changed

+160
-2
lines changed

src/declaration.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! CSS declarations.
22
33
use std::borrow::Cow;
4+
use std::ops::Range;
45

56
use crate::context::PropertyHandlerContext;
67
use crate::error::{ParserError, PrinterError};
@@ -179,6 +180,43 @@ impl<'i> DeclarationBlock<'i> {
179180
pub fn is_empty(&self) -> bool {
180181
return self.declarations.is_empty() && self.important_declarations.is_empty();
181182
}
183+
184+
pub(crate) fn property_location<'t>(
185+
&self,
186+
input: &mut Parser<'i, 't>,
187+
index: usize,
188+
) -> Result<(Range<SourceLocation>, Range<SourceLocation>), ParseError<'i, ParserError<'i>>> {
189+
// Skip to the requested property index.
190+
for _ in 0..index {
191+
input.expect_ident()?;
192+
input.expect_colon()?;
193+
input.parse_until_after(Delimiter::Semicolon, |parser| {
194+
while parser.next().is_ok() {}
195+
Ok(())
196+
})?;
197+
}
198+
199+
// Get property name range.
200+
input.skip_whitespace();
201+
let key_start = input.current_source_location();
202+
input.expect_ident()?;
203+
let key_end = input.current_source_location();
204+
let key_range = key_start..key_end;
205+
206+
input.expect_colon()?;
207+
input.skip_whitespace();
208+
209+
// Get value range.
210+
let val_start = input.current_source_location();
211+
input.parse_until_before(Delimiter::Semicolon, |parser| {
212+
while parser.next().is_ok() {}
213+
Ok(())
214+
})?;
215+
let val_end = input.current_source_location();
216+
let val_range = val_start..val_end;
217+
218+
Ok((key_range, val_range))
219+
}
182220
}
183221

184222
impl<'i> DeclarationBlock<'i> {

src/dependencies.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ pub struct SourceRange {
104104
pub file_path: String,
105105
/// The starting line and column position of the dependency.
106106
pub start: Location,
107-
/// THe ending line and column position of the dependency.
107+
/// The ending line and column position of the dependency.
108108
pub end: Location,
109109
}
110110

src/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pub struct ErrorLocation {
3636
pub filename: String,
3737
/// The line number, starting from 0.
3838
pub line: u32,
39-
/// THe column number, starting from 1.
39+
/// The column number, starting from 1.
4040
pub column: u32,
4141
}
4242

src/lib.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ mod tests {
5151
use crate::targets::Browsers;
5252
use crate::traits::{Parse, ToCss};
5353
use crate::values::color::CssColor;
54+
use cssparser::SourceLocation;
5455
use indoc::indoc;
5556
use std::collections::HashMap;
5657

@@ -17960,6 +17961,60 @@ mod tests {
1796017961
property.to_css_string(true, PrinterOptions::default()).unwrap(),
1796117962
"color: #f0f !important"
1796217963
);
17964+
17965+
let code = indoc! { r#"
17966+
.foo {
17967+
color: green;
17968+
}
17969+
17970+
.bar {
17971+
color: red;
17972+
background: pink;
17973+
}
17974+
17975+
@media print {
17976+
.baz {
17977+
color: green;
17978+
}
17979+
}
17980+
"#};
17981+
let stylesheet = StyleSheet::parse("test.css", code, ParserOptions::default()).unwrap();
17982+
if let CssRule::Style(style) = &stylesheet.rules.0[1] {
17983+
let (key, val) = style.property_location(code, 0).unwrap();
17984+
assert_eq!(
17985+
key,
17986+
SourceLocation { line: 5, column: 3 }..SourceLocation { line: 5, column: 8 }
17987+
);
17988+
assert_eq!(
17989+
val,
17990+
SourceLocation { line: 5, column: 10 }..SourceLocation { line: 5, column: 13 }
17991+
);
17992+
}
17993+
17994+
if let CssRule::Style(style) = &stylesheet.rules.0[1] {
17995+
let (key, val) = style.property_location(code, 1).unwrap();
17996+
assert_eq!(
17997+
key,
17998+
SourceLocation { line: 6, column: 3 }..SourceLocation { line: 6, column: 13 }
17999+
);
18000+
assert_eq!(
18001+
val,
18002+
SourceLocation { line: 6, column: 15 }..SourceLocation { line: 6, column: 19 }
18003+
);
18004+
}
18005+
if let CssRule::Media(media) = &stylesheet.rules.0[2] {
18006+
if let CssRule::Style(style) = &media.rules.0[0] {
18007+
let (key, val) = style.property_location(code, 0).unwrap();
18008+
assert_eq!(
18009+
key,
18010+
SourceLocation { line: 11, column: 5 }..SourceLocation { line: 11, column: 10 }
18011+
);
18012+
assert_eq!(
18013+
val,
18014+
SourceLocation { line: 11, column: 12 }..SourceLocation { line: 11, column: 17 }
18015+
);
18016+
}
18017+
}
1796318018
}
1796418019

1796518020
#[test]

src/rules/style.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
//! Style rules.
22
3+
use std::ops::Range;
4+
35
use super::Location;
46
use super::MinifyContext;
57
use crate::compat::Feature;
68
use crate::context::DeclarationContext;
79
use crate::declaration::DeclarationBlock;
10+
use crate::error::ParserError;
811
use crate::error::{MinifyError, PrinterError, PrinterErrorKind};
912
use crate::printer::Printer;
1013
use crate::rules::{CssRuleList, StyleContext, ToCssWithContext};
1114
use crate::selector::{is_compatible, is_unused, Selectors};
1215
use crate::targets::Browsers;
1316
use crate::traits::ToCss;
1417
use crate::vendor_prefix::VendorPrefix;
18+
use cssparser::*;
1519
use parcel_selectors::SelectorList;
1620

1721
/// A CSS [style rule](https://drafts.csswg.org/css-syntax/#style-rules).
@@ -74,6 +78,67 @@ impl<'i> StyleRule<'i> {
7478
pub fn is_compatible(&self, targets: Option<Browsers>) -> bool {
7579
is_compatible(&self.selectors, targets)
7680
}
81+
82+
/// Returns the line and column range of the property key and value at the given index in this style rule.
83+
///
84+
/// For performance and memory efficiency in non-error cases, source locations are not stored during parsing.
85+
/// Instead, they are computed lazily using the original source string that was used to parse the stylesheet/rule.
86+
pub fn property_location<'t>(
87+
&self,
88+
code: &'i str,
89+
index: usize,
90+
) -> Result<(Range<SourceLocation>, Range<SourceLocation>), ParseError<'i, ParserError<'i>>> {
91+
let mut input = ParserInput::new(code);
92+
let mut parser = Parser::new(&mut input);
93+
94+
// advance until start location of this rule.
95+
parse_at(&mut parser, self.loc, |parser| {
96+
// skip selector
97+
parser.parse_until_before(Delimiter::CurlyBracketBlock, |parser| {
98+
while parser.next().is_ok() {}
99+
Ok(())
100+
})?;
101+
102+
parser.expect_curly_bracket_block()?;
103+
parser.parse_nested_block(|parser| {
104+
let loc = self.declarations.property_location(parser, index);
105+
while parser.next().is_ok() {}
106+
loc
107+
})
108+
})
109+
}
110+
}
111+
112+
fn parse_at<'i, 't, T, F>(
113+
parser: &mut Parser<'i, 't>,
114+
dest: Location,
115+
parse: F,
116+
) -> Result<T, ParseError<'i, ParserError<'i>>>
117+
where
118+
F: Copy + for<'tt> FnOnce(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, ParserError<'i>>>,
119+
{
120+
loop {
121+
let loc = parser.current_source_location();
122+
if loc.line >= dest.line || (loc.line == dest.line && loc.column >= dest.column) {
123+
return parse(parser);
124+
}
125+
126+
match parser.next()? {
127+
Token::CurlyBracketBlock => {
128+
// Recursively parse nested blocks.
129+
let res = parser.parse_nested_block(|parser| {
130+
let res = parse_at(parser, dest, parse);
131+
while parser.next().is_ok() {}
132+
res
133+
});
134+
135+
if let Ok(v) = res {
136+
return Ok(v);
137+
}
138+
}
139+
_ => {}
140+
}
141+
}
77142
}
78143

79144
impl<'a, 'i> ToCssWithContext<'a, 'i> for StyleRule<'i> {

0 commit comments

Comments
 (0)