From f47dba98235860841f7a349607b0113d6ae46b12 Mon Sep 17 00:00:00 2001 From: Guillaume Malette Date: Wed, 22 Apr 2026 16:56:14 -0400 Subject: [PATCH 1/2] 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. --- src/parser/lexer/lexeme/mod.rs | 5 ++ src/rewritable_units/tokens/attributes.rs | 43 ++++++++++- .../tokens/capturer/to_token.rs | 2 +- src/rewriter/mod.rs | 74 +++++++++++++++++++ 4 files changed, 122 insertions(+), 2 deletions(-) diff --git a/src/parser/lexer/lexeme/mod.rs b/src/parser/lexer/lexeme/mod.rs index bf7c62e0..7474e058 100644 --- a/src/parser/lexer/lexeme/mod.rs +++ b/src/parser/lexer/lexeme/mod.rs @@ -38,6 +38,11 @@ impl<'i, T> Lexeme<'i, T> { &self.input } + #[inline] + pub const fn input_byte_offset(&self) -> usize { + self.previously_consumed_byte_count + } + #[inline] pub const fn token_outline(&self) -> &T { &self.token_outline diff --git a/src/rewritable_units/tokens/attributes.rs b/src/rewritable_units/tokens/attributes.rs index e4de03f2..cd9355bc 100644 --- a/src/rewritable_units/tokens/attributes.rs +++ b/src/rewritable_units/tokens/attributes.rs @@ -1,4 +1,4 @@ -use crate::base::{Bytes, BytesCow, eq_case_insensitive}; +use crate::base::{Bytes, BytesCow, SourceLocation, eq_case_insensitive}; use crate::errors::RewritingError; use crate::html::escape_double_quotes_only; use crate::parser::AttributeBuffer; @@ -41,6 +41,8 @@ pub struct Attribute<'i> { value: BytesCow<'i>, raw: Option>, encoding: &'static Encoding, + name_range: Option>, + value_range: Option>, } impl<'i> Attribute<'i> { @@ -51,12 +53,16 @@ impl<'i> Attribute<'i> { value: BytesCow<'i>, raw: Bytes<'i>, encoding: &'static Encoding, + name_range: Option>, + value_range: Option>, ) -> Self { Attribute { name, value, raw: Some(raw), encoding, + name_range, + value_range, } } @@ -104,10 +110,37 @@ impl<'i> Attribute<'i> { self.value.as_string(self.encoding) } + /// Returns the source location of the attribute name in the original document. + /// + /// Returns `None` for attributes added programmatically via + /// [`Element::set_attribute`][crate::html_content::Element::set_attribute]. + #[inline] + #[must_use] + pub fn name_source_location(&self) -> Option { + self.name_range + .as_ref() + .map(|r| SourceLocation::from_start_len(r.start, r.end - r.start)) + } + + /// Returns the source location of the attribute value in the original document. + /// + /// The range covers only the value itself, excluding any quotes or the `=` sign. + /// + /// Returns `None` for attributes added programmatically via + /// [`Element::set_attribute`][crate::html_content::Element::set_attribute]. + #[inline] + #[must_use] + pub fn value_source_location(&self) -> Option { + self.value_range + .as_ref() + .map(|r| SourceLocation::from_start_len(r.start, r.end - r.start)) + } + #[inline] fn set_value(&mut self, value: &str) { self.value = BytesCow::owned_from_str(value, self.encoding); self.raw = None; + self.value_range = None; } } @@ -141,6 +174,7 @@ pub(crate) struct Attributes<'i> { attribute_buffer: &'i AttributeBuffer, items: OnceCell>>, pub(crate) encoding: &'static Encoding, + source_byte_offset: usize, } impl<'i> Attributes<'i> { @@ -150,12 +184,14 @@ impl<'i> Attributes<'i> { input: &'i Bytes<'i>, attribute_buffer: &'i AttributeBuffer, encoding: &'static Encoding, + source_byte_offset: usize, ) -> Self { Attributes { input, attribute_buffer, items: OnceCell::default(), encoding, + source_byte_offset, } } @@ -210,6 +246,8 @@ impl<'i> Attributes<'i> { value: BytesCow::owned_from_str(value, encoding), raw: None, encoding, + name_range: None, + value_range: None, }); } } @@ -240,6 +278,7 @@ impl<'i> Attributes<'i> { debug_assert!(false); Bytes::default() }; + let base = self.source_byte_offset; self.attribute_buffer.iter().map(move |a| { Attribute::new( self.input @@ -254,6 +293,8 @@ impl<'i> Attributes<'i> { .opt_slice(Some(a.raw_range)) .unwrap_or_else(cant_fail), self.encoding, + Some(base + a.name.start..base + a.name.end), + Some(base + a.value.start..base + a.value.end), ) }) } diff --git a/src/rewritable_units/tokens/capturer/to_token.rs b/src/rewritable_units/tokens/capturer/to_token.rs index 5627bc24..d52f0319 100644 --- a/src/rewritable_units/tokens/capturer/to_token.rs +++ b/src/rewritable_units/tokens/capturer/to_token.rs @@ -37,7 +37,7 @@ impl ToToken for TagLexeme<'_> { capture_flags.remove(TokenCaptureFlags::NEXT_START_TAG); ToTokenResult::Token(StartTag::new_token( self.part(name), - Attributes::new(self.input(), attributes, encoding), + Attributes::new(self.input(), attributes, encoding, self.input_byte_offset()), ns, self_closing, self.spanned().into(), diff --git a/src/rewriter/mod.rs b/src/rewriter/mod.rs index 8cc1b461..99241499 100644 --- a/src/rewriter/mod.rs +++ b/src/rewriter/mod.rs @@ -1105,5 +1105,79 @@ mod tests { "Error in element text handler", ); } + + #[test] + fn attribute_source_locations() { + let html = r#"
"#; + let locations = Arc::new(Mutex::new(Vec::new())); + let locations_clone = Arc::clone(&locations); + + rewrite_str::( + html, + RewriteStrSettings { + element_content_handlers: vec![element!("div", move |el| { + for attr in el.attributes() { + let name_loc = attr.name_source_location(); + let value_loc = attr.value_source_location(); + locations_clone.lock().unwrap().push(( + attr.name(), + attr.value(), + name_loc.map(|l| l.bytes()), + value_loc.map(|l| l.bytes()), + )); + } + Ok(()) + })], + ..RewriteStrSettings::new() + }, + ) + .unwrap(); + + let locs = locations.lock().unwrap(); + // class="foo" + assert_eq!(locs[0].0, "class"); + assert_eq!(locs[0].1, "foo"); + assert_eq!(&html[locs[0].2.clone().unwrap()], "class"); + assert_eq!(&html[locs[0].3.clone().unwrap()], "foo"); + + // id='bar' + assert_eq!(locs[1].0, "id"); + assert_eq!(locs[1].1, "bar"); + assert_eq!(&html[locs[1].2.clone().unwrap()], "id"); + assert_eq!(&html[locs[1].3.clone().unwrap()], "bar"); + + // data-x=baz (unquoted) + assert_eq!(locs[2].0, "data-x"); + assert_eq!(locs[2].1, "baz"); + assert_eq!(&html[locs[2].2.clone().unwrap()], "data-x"); + assert_eq!(&html[locs[2].3.clone().unwrap()], "baz"); + } + + #[test] + fn attribute_source_locations_none_for_programmatic_attributes() { + rewrite_str::( + "
", + RewriteStrSettings { + element_content_handlers: vec![element!("div", |el| { + el.set_attribute("added", "val").unwrap(); + for attr in el.attributes() { + if attr.name() == "added" { + assert!( + attr.name_source_location().is_none(), + "programmatic attribute should have no name source location", + ); + assert!( + attr.value_source_location().is_none(), + "programmatic attribute should have no value source location", + ); + } + } + Ok(()) + })], + ..RewriteStrSettings::new() + }, + ) + .unwrap(); + } } } From b33681f566956a2ee47e053dad8c0031fea31170 Mon Sep 17 00:00:00 2001 From: Kornel Date: Tue, 28 Apr 2026 12:09:54 +0100 Subject: [PATCH 2/2] Reduce struct size --- src/rewritable_units/tokens/attributes.rs | 35 +++++++++-------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/rewritable_units/tokens/attributes.rs b/src/rewritable_units/tokens/attributes.rs index cd9355bc..cdb3e1d7 100644 --- a/src/rewritable_units/tokens/attributes.rs +++ b/src/rewritable_units/tokens/attributes.rs @@ -6,6 +6,7 @@ use crate::rewritable_units::Serialize; use encoding_rs::Encoding; use std::cell::OnceCell; use std::fmt::{self, Debug}; +use std::num::NonZero; use thiserror::Error; /// An error that occurs when invalid value is provided for the attribute name. @@ -41,8 +42,8 @@ pub struct Attribute<'i> { value: BytesCow<'i>, raw: Option>, encoding: &'static Encoding, - name_range: Option>, - value_range: Option>, + /// absolute document position of attribute name and attribute value + name_value_start: Option<(usize, NonZero)>, } impl<'i> Attribute<'i> { @@ -53,16 +54,14 @@ impl<'i> Attribute<'i> { value: BytesCow<'i>, raw: Bytes<'i>, encoding: &'static Encoding, - name_range: Option>, - value_range: Option>, + name_value_start: Option<(usize, NonZero)>, ) -> Self { Attribute { name, value, raw: Some(raw), encoding, - name_range, - value_range, + name_value_start, } } @@ -112,35 +111,31 @@ impl<'i> Attribute<'i> { /// Returns the source location of the attribute name in the original document. /// - /// Returns `None` for attributes added programmatically via - /// [`Element::set_attribute`][crate::html_content::Element::set_attribute]. + /// Returns `None` for attributes that were added or modified. #[inline] #[must_use] pub fn name_source_location(&self) -> Option { - self.name_range - .as_ref() - .map(|r| SourceLocation::from_start_len(r.start, r.end - r.start)) + self.name_value_start + .map(|(name, _)| SourceLocation::from_start_len(name, self.name.len())) } /// Returns the source location of the attribute value in the original document. /// /// The range covers only the value itself, excluding any quotes or the `=` sign. /// - /// Returns `None` for attributes added programmatically via - /// [`Element::set_attribute`][crate::html_content::Element::set_attribute]. + /// Returns `None` for attributes that were added or modified. #[inline] #[must_use] pub fn value_source_location(&self) -> Option { - self.value_range - .as_ref() - .map(|r| SourceLocation::from_start_len(r.start, r.end - r.start)) + self.name_value_start + .map(|(_, value)| SourceLocation::from_start_len(value.get(), self.value.len())) } #[inline] fn set_value(&mut self, value: &str) { self.value = BytesCow::owned_from_str(value, self.encoding); self.raw = None; - self.value_range = None; + self.name_value_start = None; } } @@ -246,8 +241,7 @@ impl<'i> Attributes<'i> { value: BytesCow::owned_from_str(value, encoding), raw: None, encoding, - name_range: None, - value_range: None, + name_value_start: None, }); } } @@ -293,8 +287,7 @@ impl<'i> Attributes<'i> { .opt_slice(Some(a.raw_range)) .unwrap_or_else(cant_fail), self.encoding, - Some(base + a.name.start..base + a.name.end), - Some(base + a.value.start..base + a.value.end), + NonZero::new(base + a.value.start).map(|val| (base + a.name.start, val)), ) }) }