selma 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5d5be18f9fcdf811681e74b5c0f502e7576929225dd871634ff7ef61b75420fb
4
- data.tar.gz: 0eaec610d88093dd716e4958f6f0c41d589459204abc51b01091788846f402c9
3
+ metadata.gz: ed5dccbff7a289d57157e0ce627e31ed233ba316d6989ab14bfc6a9f28d241ec
4
+ data.tar.gz: de7de41bb7cd201f866515e3ef730491d435c90610e771dac5f928e1ca2e8688
5
5
  SHA512:
6
- metadata.gz: 4a550a34761d051c04203a56fb4beb74686bf810c5c68244b308396a98f802c8a6a3d8d1d14e8097006861c1a6da8bd8fad36561f3605d87900c669fd2e8fb52
7
- data.tar.gz: b260d1421ac8ce241ffe2999cd8da4037f61c1059ea3534327c0e3cf25dd5ba9b3778037505a1eea932ecf6a5b01dd0b8e27ff4a385dc165f498f056bbda8b37
6
+ metadata.gz: 488c6240447f302b3479a20b76e17552648399dc82a465ddd0bf3d59cb058ec095a5ceae57a4f5cfef1a3865f6e765d2fdc4a94d79796f5d654d115982c53755
7
+ data.tar.gz: 371014f6ed8074097f1d7fd0c0c9600bbe7a5284248c7e7fdca6db9e4a5e26c4e565e3f20fa2c61612ad3a2ef199a8e8d87dc2103dbb987aa393e53f1f95e00d
data/Cargo.lock CHANGED
@@ -202,9 +202,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
202
202
 
203
203
  [[package]]
204
204
  name = "hashbrown"
205
- version = "0.16.1"
205
+ version = "0.17.1"
206
206
  source = "registry+https://github.com/rust-lang/crates.io-index"
207
- checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
207
+ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
208
208
  dependencies = [
209
209
  "allocator-api2",
210
210
  "equivalent",
@@ -256,9 +256,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
256
256
 
257
257
  [[package]]
258
258
  name = "lol_html"
259
- version = "2.8.1"
259
+ version = "3.0.0"
260
260
  source = "registry+https://github.com/rust-lang/crates.io-index"
261
- checksum = "6888e8653f6e49cb2924c660fc367a8beeb6239b71e117fa082153c6ea44d427"
261
+ checksum = "2ae574a677ef0443a0bd3c291f0cab9d1e61a3ad85a1515e3cfc0bc720d5a48e"
262
262
  dependencies = [
263
263
  "bitflags",
264
264
  "cfg-if",
@@ -409,18 +409,18 @@ dependencies = [
409
409
 
410
410
  [[package]]
411
411
  name = "rb-sys"
412
- version = "0.9.127"
412
+ version = "0.9.128"
413
413
  source = "registry+https://github.com/rust-lang/crates.io-index"
414
- checksum = "d7d7c9560fe42dcffa576941394075f18a17dce89fcf718a2fa90b7dc2134d12"
414
+ checksum = "45ca28513560e56cfb79a62b1fce363c73af170a182024ce880c77ee9429920a"
415
415
  dependencies = [
416
416
  "rb-sys-build",
417
417
  ]
418
418
 
419
419
  [[package]]
420
420
  name = "rb-sys-build"
421
- version = "0.9.127"
421
+ version = "0.9.128"
422
422
  source = "registry+https://github.com/rust-lang/crates.io-index"
423
- checksum = "f1688e8f32967ba48c89e4dfa283b57f901075f542fc7ee9c3d7c5f9091ca1d9"
423
+ checksum = "ce04b2c55eff3a21aaa623fcc655d94373238e72cac6b3e1a3641ff31649f99a"
424
424
  dependencies = [
425
425
  "bindgen",
426
426
  "lazy_static",
@@ -483,9 +483,9 @@ dependencies = [
483
483
 
484
484
  [[package]]
485
485
  name = "selectors"
486
- version = "0.35.0"
486
+ version = "0.37.0"
487
487
  source = "registry+https://github.com/rust-lang/crates.io-index"
488
- checksum = "93fdfed56cd634f04fe8b9ddf947ae3dc493483e819593d2ba17df9ad05db8b2"
488
+ checksum = "2cfaaa6035167f0e604e42723c7650d59ee269ef220d7bbe0565602c8a0173b9"
489
489
  dependencies = [
490
490
  "bitflags",
491
491
  "cssparser",
data/README.md CHANGED
@@ -161,6 +161,7 @@ The `element` argument in `handle_element` has the following methods:
161
161
  - `remove_attribute`: Remove an attribute
162
162
  - `has_attribute?`: A bool which identifies whether or not the element has an attribute
163
163
  - `attributes`: List all the attributes
164
+ - `attribute_source_location(name)`: Returns the byte ranges of an attribute's name and value within the original input as `{ name: Range, value: Range | nil }`, or `nil` if the attribute is missing or was added/modified during the rewrite. Pure boolean attributes written without `=` (e.g. `<input disabled>`) return `nil` because lol_html does not record their position.
164
165
  - `ancestors`: List all of an element's ancestors as an array of strings
165
166
  - `before(content, as: content_type)`: Inserts `content` before the element. `content_type` is either `:text` or `:html` and determines how the content will be applied.
166
167
  - `after(content, as: content_type)`: Inserts `content` after the element. `content_type` is either `:text` or `:html` and determines how the content will be applied.
data/ext/selma/Cargo.toml CHANGED
@@ -12,7 +12,7 @@ magnus = { version = "0.8", features = ["rb-sys"] }
12
12
  rb-sys = { version = "*", default-features = false, features = [
13
13
  "stable-api-compiled-fallback",
14
14
  ] }
15
- lol_html = "2.8"
15
+ lol_html = "3.0"
16
16
 
17
17
  [lib]
18
18
  name = "selma"
@@ -114,6 +114,47 @@ impl SelmaHTMLElement {
114
114
  }
115
115
  }
116
116
 
117
+ fn get_attribute_source_location(&self, attr: String) -> Result<Option<RHash>, Error> {
118
+ let binding = self.0.borrow();
119
+ let ruby = Ruby::get().unwrap();
120
+
121
+ let Ok(e) = binding.element.get() else {
122
+ return Ok(None);
123
+ };
124
+
125
+ let lowered = attr.to_lowercase();
126
+ let attrs = e.attributes();
127
+ let Some(attribute) = attrs.iter().find(|a| a.name() == lowered) else {
128
+ return Ok(None);
129
+ };
130
+
131
+ let Some(name_loc) = attribute.name_source_location() else {
132
+ return Ok(None);
133
+ };
134
+
135
+ let hash = ruby.hash_new();
136
+ let name_range = name_loc.bytes();
137
+ hash.aset(
138
+ ruby.to_symbol("name"),
139
+ ruby.range_new(name_range.start, name_range.end, true)?,
140
+ )?;
141
+
142
+ match attribute.value_source_location() {
143
+ Some(loc) => {
144
+ let r = loc.bytes();
145
+ hash.aset(
146
+ ruby.to_symbol("value"),
147
+ ruby.range_new(r.start, r.end, true)?,
148
+ )?;
149
+ }
150
+ None => {
151
+ hash.aset(ruby.to_symbol("value"), ruby.qnil())?;
152
+ }
153
+ }
154
+
155
+ Ok(Some(hash))
156
+ }
157
+
117
158
  fn get_attributes(&self) -> Result<RHash, Error> {
118
159
  let binding = self.0.borrow();
119
160
  let ruby = Ruby::get().unwrap();
@@ -281,6 +322,10 @@ pub fn init(c_html: RClass) -> Result<(), Error> {
281
322
  method!(SelmaHTMLElement::has_attribute, 1),
282
323
  )?;
283
324
  c_element.define_method("attributes", method!(SelmaHTMLElement::get_attributes, 0))?;
325
+ c_element.define_method(
326
+ "attribute_source_location",
327
+ method!(SelmaHTMLElement::get_attribute_source_location, 1),
328
+ )?;
284
329
  c_element.define_method("ancestors", method!(SelmaHTMLElement::get_ancestors, 0))?;
285
330
 
286
331
  c_element.define_method("before", method!(SelmaHTMLElement::before, -1))?;
@@ -34,7 +34,11 @@ use crate::{
34
34
  #[derive(Clone)]
35
35
  pub struct Handler {
36
36
  rb_handler: Opaque<Value>,
37
- rb_selector: Opaque<Obj<SelmaSelector>>,
37
+ // Store the selector's data by value (Rust-owned). The `#selector` method may return a
38
+ // freshly-built `Selma::Selector` that nothing else on the Ruby side references; keeping a
39
+ // `Obj`/`Opaque` handle to it would make its lifetime depend on GC (use-after-free if it is
40
+ // collected while the Rewriter is alive). We only ever read Rust data off it, so clone it.
41
+ selector: SelmaSelector,
38
42
  // total_element_handler_calls: usize,
39
43
  // total_elapsed_element_handlers: f64,
40
44
 
@@ -43,7 +47,8 @@ pub struct Handler {
43
47
  }
44
48
 
45
49
  struct RewriterOptions {
46
- memory_options: MemorySettings,
50
+ max_allowed_memory_usage: usize,
51
+ preallocated_parsing_buffer_size: usize,
47
52
  }
48
53
 
49
54
  pub struct Rewriter {
@@ -126,7 +131,9 @@ impl SelmaRewriter {
126
131
  };
127
132
  let handler = Handler {
128
133
  rb_handler: Opaque::from(rb_handler),
129
- rb_selector: Opaque::from(rb_selector),
134
+ // clone the selector's data out of the Ruby object right away so the
135
+ // Handler no longer depends on that object surviving GC (see struct docs)
136
+ selector: (*rb_selector).clone(),
130
137
  // total_element_handler_calls: 0,
131
138
  // total_elapsed_element_handlers: 0.0,
132
139
 
@@ -162,7 +169,7 @@ impl SelmaRewriter {
162
169
  if let Some(max_allowed_memory_usage) = max_allowed_memory_usage {
163
170
  match max_allowed_memory_usage.to_u64() {
164
171
  Ok(max_allowed_memory_usage) => {
165
- rewriter_options.memory_options.max_allowed_memory_usage =
172
+ rewriter_options.max_allowed_memory_usage =
166
173
  max_allowed_memory_usage as usize;
167
174
  }
168
175
  Err(_e) => {
@@ -173,7 +180,7 @@ impl SelmaRewriter {
173
180
  }
174
181
  }
175
182
  } else {
176
- rewriter_options.memory_options.max_allowed_memory_usage = MemorySettings::default().max_allowed_memory_usage;
183
+ rewriter_options.max_allowed_memory_usage = RewriterOptions::DEFAULT_MAX_ALLOWED_MEMORY_USAGE;
177
184
  }
178
185
  }
179
186
 
@@ -183,7 +190,7 @@ impl SelmaRewriter {
183
190
  if let Some(preallocated_parsing_buffer_size) = preallocated_parsing_buffer_size {
184
191
  match preallocated_parsing_buffer_size.to_u64() {
185
192
  Ok(preallocated_parsing_buffer_size) => {
186
- rewriter_options.memory_options.preallocated_parsing_buffer_size =
193
+ rewriter_options.preallocated_parsing_buffer_size =
187
194
  preallocated_parsing_buffer_size as usize;
188
195
  }
189
196
  Err(_e) => {
@@ -194,7 +201,7 @@ impl SelmaRewriter {
194
201
  }
195
202
  }
196
203
  } else {
197
- rewriter_options.memory_options.preallocated_parsing_buffer_size = MemorySettings::default().preallocated_parsing_buffer_size;
204
+ rewriter_options.preallocated_parsing_buffer_size = RewriterOptions::DEFAULT_PREALLOCATED_PARSING_BUFFER_SIZE;
198
205
  }
199
206
  }
200
207
  }
@@ -210,10 +217,8 @@ impl SelmaRewriter {
210
217
  }
211
218
  }
212
219
 
213
- if rewriter_options
214
- .memory_options
215
- .preallocated_parsing_buffer_size
216
- > rewriter_options.memory_options.max_allowed_memory_usage
220
+ if rewriter_options.preallocated_parsing_buffer_size
221
+ > rewriter_options.max_allowed_memory_usage
217
222
  {
218
223
  return Err(magnus::Error::new(
219
224
  ruby.exception_arg_error(),
@@ -364,55 +369,42 @@ impl SelmaRewriter {
364
369
  handlers.iter().for_each(|handler| {
365
370
  let element_stack: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(vec![]));
366
371
 
367
- let ruby = Ruby::get().unwrap();
368
-
369
- let selector = ruby.get_inner(handler.rb_selector);
372
+ let selector = &handler.selector;
370
373
 
371
374
  // TODO: test final raise by simulating errors
372
- if selector.match_element().is_some() {
375
+ if let Some(match_element) = selector.match_element() {
373
376
  let closure_element_stack = element_stack.clone();
374
377
 
375
- element_content_handlers.push(element!(
376
- selector.match_element().unwrap(),
377
- move |el| {
378
- match Self::process_element_handlers(
379
- handler,
380
- el,
381
- &closure_element_stack.borrow(),
382
- ) {
383
- Ok(_) => Ok(()),
384
- Err(err) => Err(err.to_string().into()),
385
- }
378
+ element_content_handlers.push(element!(match_element, move |el| {
379
+ match Self::process_element_handlers(
380
+ handler,
381
+ el,
382
+ &closure_element_stack.borrow(),
383
+ ) {
384
+ Ok(_) => Ok(()),
385
+ Err(err) => Err(err.to_string().into()),
386
386
  }
387
- ));
387
+ }));
388
388
  }
389
389
 
390
- if selector.match_text_within().is_some() {
390
+ if let Some(match_text_within) = selector.match_text_within() {
391
391
  let closure_element_stack = element_stack.clone();
392
392
 
393
- element_content_handlers.push(text!(
394
- selector.match_text_within().unwrap(),
395
- move |text| {
396
- let element_stack = closure_element_stack.as_ref().borrow();
397
- if selector.ignore_text_within().is_some() {
398
- // check if current tag is a tag we should be ignoring text within;
399
- // also checks if tag is within an ancestery of ignored tags
400
- if selector
401
- .ignore_text_within()
402
- .unwrap()
403
- .iter()
404
- .any(|t| element_stack.contains(t))
405
- {
406
- return Ok(());
407
- }
393
+ element_content_handlers.push(text!(match_text_within, move |text| {
394
+ let element_stack = closure_element_stack.as_ref().borrow();
395
+ // check if current tag is a tag we should be ignoring text within;
396
+ // also checks if tag is within an ancestery of ignored tags
397
+ if let Some(ignore_text_within) = selector.ignore_text_within() {
398
+ if ignore_text_within.iter().any(|t| element_stack.contains(t)) {
399
+ return Ok(());
408
400
  }
401
+ }
409
402
 
410
- match Self::process_text_handlers(handler, text) {
411
- Ok(_) => Ok(()),
412
- Err(err) => Err(err.to_string().into()),
413
- }
403
+ match Self::process_text_handlers(handler, text) {
404
+ Ok(_) => Ok(()),
405
+ Err(err) => Err(err.to_string().into()),
414
406
  }
415
- ));
407
+ }));
416
408
  }
417
409
 
418
410
  // we need to check *every* element we iterate over, to create a stack of elements
@@ -456,15 +448,16 @@ impl SelmaRewriter {
456
448
  let binding = &self.0.borrow();
457
449
  let mut output = vec![];
458
450
  {
459
- let mut rewriter = HtmlRewriter::new(
460
- Settings {
461
- document_content_handlers,
462
- element_content_handlers,
463
- memory_settings: Self::get_memory_options(binding),
464
- ..Settings::default()
465
- },
466
- |c: &[u8]| output.extend_from_slice(c),
467
- );
451
+ let mut settings =
452
+ Settings::new().with_memory_settings(Self::get_memory_options(binding));
453
+ for handler in document_content_handlers {
454
+ settings = settings.append_document_content_handler(handler);
455
+ }
456
+ for handler in element_content_handlers {
457
+ settings = settings.append_element_content_handler(handler);
458
+ }
459
+
460
+ let mut rewriter = HtmlRewriter::new(settings, |c: &[u8]| output.extend_from_slice(c));
468
461
  match rewriter.write(html) {
469
462
  Ok(_) => {}
470
463
  Err(err) => {
@@ -561,18 +554,23 @@ impl SelmaRewriter {
561
554
  }
562
555
 
563
556
  fn get_memory_options(binding: &Ref<Rewriter>) -> MemorySettings {
564
- let options = &binding.options.memory_options;
565
- MemorySettings {
566
- max_allowed_memory_usage: options.max_allowed_memory_usage,
567
- preallocated_parsing_buffer_size: options.preallocated_parsing_buffer_size,
568
- }
557
+ let options = &binding.options;
558
+ MemorySettings::new()
559
+ .with_max_allowed_memory_usage(options.max_allowed_memory_usage)
560
+ .with_preallocated_parsing_buffer_size(options.preallocated_parsing_buffer_size)
569
561
  }
570
562
  }
571
563
 
572
564
  impl RewriterOptions {
565
+ // Mirrors lol_html's `MemorySettings` defaults, which are no longer exposed as
566
+ // public fields as of lol_html 3.0 (settings are now consuming builders).
567
+ const DEFAULT_MAX_ALLOWED_MEMORY_USAGE: usize = usize::MAX;
568
+ const DEFAULT_PREALLOCATED_PARSING_BUFFER_SIZE: usize = 1024;
569
+
573
570
  pub fn new() -> Self {
574
571
  Self {
575
- memory_options: MemorySettings::default(),
572
+ max_allowed_memory_usage: Self::DEFAULT_MAX_ALLOWED_MEMORY_USAGE,
573
+ preallocated_parsing_buffer_size: Self::DEFAULT_PREALLOCATED_PARSING_BUFFER_SIZE,
576
574
  }
577
575
  }
578
576
  }
@@ -87,16 +87,16 @@ impl SelmaSelector {
87
87
  Ok((match_element, match_text_within, rb_ignore_text_within))
88
88
  }
89
89
 
90
- pub fn match_element(&self) -> Option<String> {
91
- self.match_element.clone()
90
+ pub fn match_element(&self) -> Option<&str> {
91
+ self.match_element.as_deref()
92
92
  }
93
93
 
94
- pub fn match_text_within(&self) -> Option<String> {
95
- self.match_text_within.clone()
94
+ pub fn match_text_within(&self) -> Option<&str> {
95
+ self.match_text_within.as_deref()
96
96
  }
97
97
 
98
- pub fn ignore_text_within(&self) -> Option<Vec<String>> {
99
- self.ignore_text_within.clone()
98
+ pub fn ignore_text_within(&self) -> Option<&[String]> {
99
+ self.ignore_text_within.as_deref()
100
100
  }
101
101
  }
102
102
 
data/lib/selma/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Selma
4
- VERSION = "0.5.0"
4
+ VERSION = "0.5.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: selma
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Garen J. Torikian
@@ -112,7 +112,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
112
112
  - !ruby/object:Gem::Version
113
113
  version: '3.4'
114
114
  requirements: []
115
- rubygems_version: 4.0.6
115
+ rubygems_version: 4.0.10
116
116
  specification_version: 4
117
117
  summary: Selma selects and matches HTML nodes using CSS rules. Backed by Rust's lol_html
118
118
  parser.