Skip to content

Commit

Permalink
Improve parse_declarations! performance
Browse files Browse the repository at this point in the history
  • Loading branch information
leonid-shevtsov committed Aug 23, 2024
1 parent 4fc5b07 commit b4d986a
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* RuleSet initialize now takes keyword argument, positional arguments are still supported but deprecated
* Removed OffsetAwareRuleSet, it's a RuleSet with optional attributes filename and offset
* Improved performance of block parsing by using StringScanner
* Improve `RuleSet#parse_declarations!` performance by using substring search istead of regexps

### Version v1.18.0

Expand Down
37 changes: 27 additions & 10 deletions lib/css_parser/rule_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ class RuleSet

WHITESPACE_REPLACEMENT = '___SPACE___'

# Tokens for parse_declarations!
COLON = ':'.freeze
SEMICOLON = ';'.freeze
LPAREN = '('.freeze
RPAREN = ')'.freeze
class Declarations
class Value
attr_reader :value
Expand Down Expand Up @@ -647,20 +652,32 @@ def parse_declarations!(block) # :nodoc:
return unless block

continuation = nil
block.split(/[;$]+/m).each do |decs|
decs = (continuation ? continuation + decs : decs)
if decs =~ /\([^)]*\Z/ # if it has an unmatched parenthesis
continuation = "#{decs};"
elsif (matches = decs.match(/\s*(.[^:]*)\s*:\s*(?m:(.+))(?:;?\s*\Z)/i))
# skip end_of_declaration
property = matches[1]
value = matches[2]
add_declaration!(property, value)
continuation = nil
block.split(SEMICOLON) do |decs|
decs = (continuation ? "#{continuation};#{decs}" : decs)
if unmatched_open_parenthesis?(decs)
# Semicolon happened within parenthesis, so it is a part of the value
# the rest of the value is in the next segment
continuation = decs
next
end

next unless (colon = decs.index(COLON))

property = decs[0, colon]
value = decs[(colon + 1)..]
property.strip!
value.strip!
next if property.empty? || value.empty?

add_declaration!(property, value)
continuation = nil
end
end

def unmatched_open_parenthesis?(declarations)
(lparen_index = declarations.index(LPAREN)) && !declarations.index(RPAREN, lparen_index)
end

#--
# TODO: way too simplistic
#++
Expand Down
13 changes: 13 additions & 0 deletions test/test_rule_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ def test_each_declaration_containing_semicolons
assert_equal('no-repeat;', rs['background-repeat'])
end

def test_each_declaration_with_newlines
expected = Set[
{property: 'background-image', value: 'url(foo;bar)', is_important: false},
{property: 'font-weight', value: 'bold', is_important: true},
]
rs = RuleSet.new(block: "background-image\n:\nurl(foo;bar);\n\n\n\n\n;;font-weight\n\n\n:bold\n\n\n!important")
actual = Set.new
rs.each_declaration do |prop, val, imp|
actual << {property: prop, value: val, is_important: imp}
end
assert_equal(expected, actual)
end

def test_selector_sanitization
selectors = "h1, h2,\nh3 "
rs = RuleSet.new(selectors: selectors, block: "color: #fff;")
Expand Down

0 comments on commit b4d986a

Please sign in to comment.