Skip to content

Commit eff9dd6

Browse files
committed
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 9d22689 commit eff9dd6

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

@@ -105,10 +111,37 @@ impl<'i> Attribute<'i> {
105111
self.value.as_string(self.encoding)
106112
}
107113

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

@@ -142,6 +175,7 @@ pub(crate) struct Attributes<'i> {
142175
attribute_buffer: &'i AttributeBuffer,
143176
items: OnceCell<Vec<Attribute<'i>>>,
144177
pub(crate) encoding: &'static Encoding,
178+
source_byte_offset: usize,
145179
}
146180

147181
impl<'i> Attributes<'i> {
@@ -151,12 +185,14 @@ impl<'i> Attributes<'i> {
151185
input: &'i Bytes<'i>,
152186
attribute_buffer: &'i AttributeBuffer,
153187
encoding: &'static Encoding,
188+
source_byte_offset: usize,
154189
) -> Self {
155190
Attributes {
156191
input,
157192
attribute_buffer,
158193
items: OnceCell::default(),
159194
encoding,
195+
source_byte_offset,
160196
}
161197
}
162198

@@ -211,6 +247,8 @@ impl<'i> Attributes<'i> {
211247
value: BytesCow::from_str(value, encoding).into_owned(),
212248
raw: None,
213249
encoding,
250+
name_range: None,
251+
value_range: None,
214252
});
215253
}
216254
}
@@ -241,6 +279,7 @@ impl<'i> Attributes<'i> {
241279
debug_assert!(false);
242280
Bytes::default()
243281
};
282+
let base = self.source_byte_offset;
244283
self.attribute_buffer.iter().map(move |a| {
245284
Attribute::new(
246285
self.input
@@ -255,6 +294,8 @@ impl<'i> Attributes<'i> {
255294
.opt_slice(Some(a.raw_range))
256295
.unwrap_or_else(cant_fail),
257296
self.encoding,
297+
Some(base + a.name.start..base + a.name.end),
298+
Some(base + a.value.start..base + a.value.end),
258299
)
259300
})
260301
}

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)