Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve parse_declarations! performance #166

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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|
leonid-shevtsov marked this conversation as resolved.
Show resolved Hide resolved
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