From 1c16567e2e3ccdf09b4502c4bbaff9b0e18fcf66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Joubert?= Date: Fri, 23 Oct 2026 18:16:22 +0300 Subject: [PATCH 1/3] Speed up Declarations#to_s: stop its clean-up regexp from matching at every character `[\n\r\f\t]*` matches the empty string, so gsub! performed a replacement at every position of the declarations string. Leftmost alternation also made the trailing `\s+$` branch unreachable. `+` with the dead branch dropped gives the same output (fuzzed on 200k random strings) ~24x faster. --- CHANGELOG.md | 1 + lib/css_parser/rule_set.rb | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9b2cdf..96ff017 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Ruby CSS Parser CHANGELOG ### Unreleased +* Speed up `RuleSet::Declarations#to_s` ~20x: its clean-up regexp matched the empty string at every character (`[\n\r\f\t]*`), making `gsub!` replace at every position; the unreachable `\s+$` branch is dropped, output unchanged ### Version 3.3.0 * Fix Arbitrary local-file read via attacker-controlled `@import` diff --git a/lib/css_parser/rule_set.rb b/lib/css_parser/rule_set.rb index 453c34f..e7d37ac 100644 --- a/lib/css_parser/rule_set.rb +++ b/lib/css_parser/rule_set.rb @@ -205,8 +205,7 @@ def to_s(options = {}) importance = options[:force_important] || value.important ? ' !important' : '' memo << "#{prop}: #{value.value}#{importance}; " end - # TODO: Clean-up regexp doesn't seem to work - str.gsub!(/^[\s^({)]+|[\n\r\f\t]*|\s+$/mx, '') + str.gsub!(/^[\s^({)]+|[\n\r\f\t]+/, '') str.strip! str end From 68bfef269c3cbf9340d2b70afc82a73efc53b445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Joubert?= Date: Fri, 23 Oct 2026 18:19:50 +0300 Subject: [PATCH 2/3] Allocate less when looking up and shortening declarations normalize_property returned a fresh downcased copy on every lookup, even for the already normalized literals used internally (margin-top, ...). Skip the copy when the name is already lowercase ASCII. create_dimensions_shorthand! built two enumerators and a pair array per direction; fill a plain hash. --- CHANGELOG.md | 1 + lib/css_parser/rule_set.rb | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96ff017..52e1cbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Unreleased * Speed up `RuleSet::Declarations#to_s` ~20x: its clean-up regexp matched the empty string at every character (`[\n\r\f\t]*`), making `gsub!` replace at every position; the unreachable `\s+$` branch is dropped, output unchanged +* Allocate less in `RuleSet`: skip the downcased copy of already normalized property names, and build dimension shorthands without enumerators ### Version 3.3.0 * Fix Arbitrary local-file read via attacker-controlled `@import` diff --git a/lib/css_parser/rule_set.rb b/lib/css_parser/rule_set.rb index e7d37ac..69cb057 100644 --- a/lib/css_parser/rule_set.rb +++ b/lib/css_parser/rule_set.rb @@ -18,6 +18,8 @@ class RuleSet NUMBER_OF_DIMENSIONS = 4 + NORMALIZED_PROPERTY_RX = /\A[-a-z0-9_]+\z/.freeze + DIMENSIONS = [ ['margin', %w[margin-top margin-right margin-bottom margin-left]], ['padding', %w[padding-top padding-right padding-bottom padding-left]], @@ -225,6 +227,8 @@ def ==(other) attr_writer :declarations def normalize_property(property) + return property if property.is_a?(String) && property.match?(NORMALIZED_PROPERTY_RX) + property = property.to_s.downcase property.strip! property @@ -583,10 +587,11 @@ def create_dimensions_shorthand! # :nodoc: return if declarations.size < NUMBER_OF_DIMENSIONS DIMENSIONS.each do |property, dimensions| - values = DIMENSION_DIRECTIONS.each_with_index.with_object({}) do |(side, index), result| + values = {} + DIMENSION_DIRECTIONS.each_with_index do |side, index| next unless (declaration = declarations[dimensions[index]]) - result[side] = declaration.value + values[side] = declaration.value end # All four dimensions must be present From 7ec0f079d6e0a0c012630f6386614117315876f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Joubert?= Date: Sat, 24 Oct 2026 22:38:53 +0300 Subject: [PATCH 3/3] Convert the property to a string before matching it, so symbols skip the downcase too --- lib/css_parser/rule_set.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/css_parser/rule_set.rb b/lib/css_parser/rule_set.rb index 69cb057..cff4bd7 100644 --- a/lib/css_parser/rule_set.rb +++ b/lib/css_parser/rule_set.rb @@ -227,9 +227,10 @@ def ==(other) attr_writer :declarations def normalize_property(property) - return property if property.is_a?(String) && property.match?(NORMALIZED_PROPERTY_RX) + property = property.to_s + return property if property.match?(NORMALIZED_PROPERTY_RX) - property = property.to_s.downcase + property = property.downcase property.strip! property end