Skip to content

Commit e605251

Browse files
gmalettekornelski
authored andcommitted
Add name_source_location() and value_source_location() to Attribute
Exposes document-absolute byte ranges for attribute names and values via the existing SourceLocation type. Returns None for attributes added programmatically via set_attribute. This enables downstream consumers to perform byte-level splice rewrites on attribute values without re-parsing the start tag.
1 parent ba5de8e commit e605251

4 files changed

Lines changed: 122 additions & 2 deletions

File tree

src/parser/lexer/lexeme/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ impl<'i, T> Lexeme<'i, T> {
3838
&self.input
3939
}
4040

41+
#[inline]
42+
pub const fn input_byte_offset(&self) -> usize {
43+
self.previously_consumed_byte_count
44+
}
45+
4146
#[inline]
4247
pub const fn token_outline(&self) -> &T {
4348
&self.token_outline

src/rewritable_units/tokens/attributes.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::base::{Bytes, BytesCow, eq_case_insensitive};
1+
use crate::base::{Bytes, BytesCow, SourceLocation, eq_case_insensitive};
22
use crate::errors::RewritingError;
33
use crate::html::escape_double_quotes_only;
44
use crate::parser::AttributeBuffer;
@@ -41,6 +41,8 @@ pub struct Attribute<'i> {
4141
value: BytesCow<'i>,
4242
raw: Option<Bytes<'i>>,
4343
encoding: &'static Encoding,
44+
name_range: Option<std::ops::Range<usize>>,
45+
value_range: Option<std::ops::Range<usize>>,
4446
}
4547

4648
impl<'i> Attribute<'i> {
@@ -51,12 +53,16 @@ impl<'i> Attribute<'i> {
5153
value: BytesCow<'i>,
5254
raw: Bytes<'i>,
5355
encoding: &'static Encoding,
56+
name_range: Option<std::ops::Range<usize>>,
57+
value_range: Option<std::ops::Range<usize>>,
5458
) -> Self {
5559
Attribute {
5660
name,
5761
value,
5862
raw: Some(raw),
5963
encoding,
64+
name_range,
65+
value_range,
6066
}
6167
}
6268

@@ -104,10 +110,37 @@ impl<'i> Attribute<'i> {
104110
self.value.as_string(self.encoding)
105111
}
106112

113+
/// Returns the source location of the attribute name in the original document.
114+
///
115+
/// Returns `None` for attributes added programmatically via
116+
/// [`Element::set_attribute`][crate::html_content::Element::set_attribute].
117+
#[inline]
118+
#[must_use]
119+
pub fn name_source_location(&self) -> Option<SourceLocation> {
120+
self.name_range
121+
.as_ref()
122+
.map(|r| SourceLocation::from_start_len(r.start, r.end - r.start))
123+
}
124+
125+
/// Returns the source location of the attribute value in the original document.
126+
///
127+
/// The range covers only the value itself, excluding any quotes or the `=` sign.
128+
///
129+
/// Returns `None` for attributes added programmatically via
130+
/// [`Element::set_attribute`][crate::html_content::Element::set_attribute].
131+
#[inline]
132+
#[must_use]
133+
pub fn value_source_location(&self) -> Option<SourceLocation> {
134+
self.value_range
135+
.as_ref()
136+
.map(|r| SourceLocation::from_start_len(r.start, r.end - r.start))
137+
}
138+
107139
#[inline]
108140
fn set_value(&mut self, value: &str) {
109141
self.value = BytesCow::owned_from_str(value, self.encoding);
110142
self.raw = None;
143+
self.value_range = None;
111144
}
112145
}
113146

@@ -141,6 +174,7 @@ pub(crate) struct Attributes<'i> {
141174
attribute_buffer: &'i AttributeBuffer,
142175
items: OnceCell<Vec<Attribute<'i>>>,
143176
pub(crate) encoding: &'static Encoding,
177+
source_byte_offset: usize,
144178
}
145179

146180
impl<'i> Attributes<'i> {
@@ -150,12 +184,14 @@ impl<'i> Attributes<'i> {
150184
input: &'i Bytes<'i>,
151185
attribute_buffer: &'i AttributeBuffer,
152186
encoding: &'static Encoding,
187+
source_byte_offset: usize,
153188
) -> Self {
154189
Attributes {
155190
input,
156191
attribute_buffer,
157192
items: OnceCell::default(),
158193
encoding,
194+
source_byte_offset,
159195
}
160196
}
161197

@@ -210,6 +246,8 @@ impl<'i> Attributes<'i> {
210246
value: BytesCow::owned_from_str(value, encoding),
211247
raw: None,
212248
encoding,
249+
name_range: None,
250+
value_range: None,
213251
});
214252
}
215253
}
@@ -240,6 +278,7 @@ impl<'i> Attributes<'i> {
240278
debug_assert!(false);
241279
Bytes::default()
242280
};
281+
let base = self.source_byte_offset;
243282
self.attribute_buffer.iter().map(move |a| {
244283
Attribute::new(
245284
self.input
@@ -254,6 +293,8 @@ impl<'i> Attributes<'i> {
254293
.opt_slice(Some(a.raw_range))
255294
.unwrap_or_else(cant_fail),
256295
self.encoding,
296+
Some(base + a.name.start..base + a.name.end),
297+
Some(base + a.value.start..base + a.value.end),
257298
)
258299
})
259300
}

src/rewritable_units/tokens/capturer/to_token.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ impl ToToken for TagLexeme<'_> {
3737
capture_flags.remove(TokenCaptureFlags::NEXT_START_TAG);
3838
ToTokenResult::Token(StartTag::new_token(
3939
self.part(name),
40-
Attributes::new(self.input(), attributes, encoding),
40+
Attributes::new(self.input(), attributes, encoding, self.input_byte_offset()),
4141
ns,
4242
self_closing,
4343
self.spanned().into(),

src/rewriter/mod.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,5 +991,79 @@ mod tests {
991991
"Error in element text handler",
992992
);
993993
}
994+
995+
#[test]
996+
fn attribute_source_locations() {
997+
let html = r#"<div class="foo" id='bar' data-x=baz>"#;
998+
let locations = Arc::new(Mutex::new(Vec::new()));
999+
let locations_clone = Arc::clone(&locations);
1000+
1001+
rewrite_str::<LocalHandlerTypes>(
1002+
html,
1003+
RewriteStrSettings {
1004+
element_content_handlers: vec![element!("div", move |el| {
1005+
for attr in el.attributes() {
1006+
let name_loc = attr.name_source_location();
1007+
let value_loc = attr.value_source_location();
1008+
locations_clone.lock().unwrap().push((
1009+
attr.name(),
1010+
attr.value(),
1011+
name_loc.map(|l| l.bytes()),
1012+
value_loc.map(|l| l.bytes()),
1013+
));
1014+
}
1015+
Ok(())
1016+
})],
1017+
..RewriteStrSettings::new()
1018+
},
1019+
)
1020+
.unwrap();
1021+
1022+
let locs = locations.lock().unwrap();
1023+
// class="foo"
1024+
assert_eq!(locs[0].0, "class");
1025+
assert_eq!(locs[0].1, "foo");
1026+
assert_eq!(&html[locs[0].2.clone().unwrap()], "class");
1027+
assert_eq!(&html[locs[0].3.clone().unwrap()], "foo");
1028+
1029+
// id='bar'
1030+
assert_eq!(locs[1].0, "id");
1031+
assert_eq!(locs[1].1, "bar");
1032+
assert_eq!(&html[locs[1].2.clone().unwrap()], "id");
1033+
assert_eq!(&html[locs[1].3.clone().unwrap()], "bar");
1034+
1035+
// data-x=baz (unquoted)
1036+
assert_eq!(locs[2].0, "data-x");
1037+
assert_eq!(locs[2].1, "baz");
1038+
assert_eq!(&html[locs[2].2.clone().unwrap()], "data-x");
1039+
assert_eq!(&html[locs[2].3.clone().unwrap()], "baz");
1040+
}
1041+
1042+
#[test]
1043+
fn attribute_source_locations_none_for_programmatic_attributes() {
1044+
rewrite_str::<LocalHandlerTypes>(
1045+
"<div></div>",
1046+
RewriteStrSettings {
1047+
element_content_handlers: vec![element!("div", |el| {
1048+
el.set_attribute("added", "val").unwrap();
1049+
for attr in el.attributes() {
1050+
if attr.name() == "added" {
1051+
assert!(
1052+
attr.name_source_location().is_none(),
1053+
"programmatic attribute should have no name source location",
1054+
);
1055+
assert!(
1056+
attr.value_source_location().is_none(),
1057+
"programmatic attribute should have no value source location",
1058+
);
1059+
}
1060+
}
1061+
Ok(())
1062+
})],
1063+
..RewriteStrSettings::new()
1064+
},
1065+
)
1066+
.unwrap();
1067+
}
9941068
}
9951069
}

0 commit comments

Comments
 (0)