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 core CSS parser. #129

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .rubocop.yml
@@ -1,5 +1,5 @@
AllCops:
TargetRubyVersion: 2.4 # lowest supported version
TargetRubyVersion: 2.5 # lowest supported version
NewCops: enable

Layout/ArgumentAlignment:
Expand Down
2 changes: 1 addition & 1 deletion css_parser.gemspec
Expand Up @@ -11,7 +11,7 @@ Gem::Specification.new name, CssParser::VERSION do |s|
s.author = 'Alex Dunae'
s.files = Dir.glob('lib/**/*') + ['MIT-LICENSE']
s.license = 'MIT'
s.required_ruby_version = '>= 2.4'
s.required_ruby_version = '>= 2.5'

s.metadata['changelog_uri'] = 'https://github.com/premailer/css_parser/blob/master/CHANGELOG.md'
s.metadata['source_code_uri'] = 'https://github.com/premailer/css_parser'
Expand Down
2 changes: 2 additions & 0 deletions lib/css_parser/parser.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require_relative 'parser/parser'

module CssParser
# Exception class used for any errors encountered while downloading remote files.
class RemoteFileError < IOError; end
Expand Down
124 changes: 124 additions & 0 deletions lib/css_parser/parser/parser.rb
@@ -0,0 +1,124 @@
# frozen_string_literal: true

module CssParser
class Parser
module Parser
def self.parse(css)
puts css

rules = []
scanner = StringScanner.new(css)

until scanner.eos?
rule = {
selector: '',
properties: ''
}
scanner.skip(/\s*/)
rule[:selector] = parse_selector(scanner).strip
rule[:properties] = parse_proppertied(scanner).strip

rules << rule
puts "pushing rule #{rule.inspect}"
scanner.skip(/\s*/)
end

rules
end

def self.parse_string(scanner, quote)
quoted_string = ''
string_ended = false

until string_ended
selector = scanner.scan_until(/#{quote}/)
quoted_string += selector
backslashes = /\\*\z/.match(selector[0..-2])[0].length
string_ended = backslashes.even?
end

quoted_string.gsub(/\\{2}/, '\\')
end

def self.parse_selector(scanner)
new_selector = ''
got_to_properties = false

until got_to_properties
selector = scanner.scan_until(/'|"|{|\\{2}/)
case scanner[0]
when nil
raise 'CSS invalid stylesheet, could not find end of selector'
when '\\\\' # maybe this can be replaced with a gsub at the end .tr("\\\\", "\\")
new_selector += selector[0..(- 1 - scanner[0].length)]
# if you have two backslashes the selector should have one
new_selector += '\\'

when '"', "'"
backslashes = /\\*\z/.match(selector[0..-2])[0].length
case backslashes
when 0
new_selector += selector
new_selector += parse_string(scanner, scanner[0])
when 1
new_selector += selector[0..-3]
new_selector += '"'
else
raise 'should only be 0 or 1 backslash at this location'
end

when '{'
# check if it is escaped
# should only be 0 or 1 backslash before { since we scan until will we find an escaped backslash or curly
backslashes = /\\*\z/
.match(selector[0..-2])
.yield_self { |match| match[0].length }

case backslashes
when 0
new_selector += selector[0..-2]
when 1
new_selector += selector[0..-3] # "remove last \ and { and append { to selector"
new_selector += '{'
else
raise 'should only be 0 or 1 backslash at this location'
end

# If these is an odd number of backslashes it is expected
got_to_properties = backslashes.even?
else
raise "got something unexpected #{scanner.values_at(0).inspect}"
end
end

new_selector
end

def self.parse_proppertied(scanner)
properties = ''
end_of_propertied = false

until end_of_propertied
selector = scanner.scan_until(/'|"|}/)
case scanner[0]
when nil
raise 'CSS invalid stylesheet, could not find end of properties'
when '"', "'"
backslashes = /\\*\z/.match(selector[0..-2])[0].length
raise 'Dont think you can have any escaped quates here' unless backslashes == 0

properties += selector
properties += parse_string(scanner, scanner[0])
when '}'
properties += selector[0..-2]
end_of_propertied = true
else
raise "got something unexpected #{scanner.values_at(0).inspect}"
end
end

properties.strip
end
end
end
end
2 changes: 1 addition & 1 deletion test/rule_set/test_declarations.rb
Expand Up @@ -159,7 +159,7 @@ class RuleSetDeclarationsTest < Minitest::Test
declarations = CssParser::RuleSet::Declarations.new({foo: 'foo value'})
assert_equal 1, declarations.size

declarations.remove_declaration!('fOo'.to_sym)
declarations.remove_declaration!(:fOo)

assert_equal 0, declarations.size
end
Expand Down
53 changes: 50 additions & 3 deletions test/test_css_parser_basic.rb
Expand Up @@ -34,9 +34,51 @@ def test_adding_block_without_closing_brace
assert_equal 'color: red;', @cp.find_by_selector('p').join
end

def test_adding_a_rule
@cp.add_rule!('div', 'color: blue;')
assert_equal 'color: blue;', @cp.find_by_selector('div').join(' ')
def test_add_multiple_selectors_in_one_go
@cp.add_block!('p, span { color: red; }')
assert_equal({"p" => {"color" => "red"}, "span" => {"color" => "red"}}, @cp.to_h["all"])
end

def test_selector_with_backslash
@cp.add_block!('.back\\slash { color: red; }')
assert_equal({".back\\slash" => {"color" => "red"}}, @cp.to_h["all"])
end

def test_selector_with_doublequoute
@cp.add_block!(<<-CSS)
.double\\"quote { color: red; }
CSS
assert_equal({'.double"quote' => {"color" => "red"}}, @cp.to_h["all"])
end

def test_selector_with_singlequoute
@cp.add_block!(<<-CSS)
.double\\"quote { color: red; }
CSS
pp @cp.to_h["all"]
assert_equal({".double'quote" => {"color" => "red"}}, @cp.to_h["all"])
end

def test_add_nested_selection
@cp.add_block!(<<-CSS)
p > span { color: red; }
CSS
assert_equal({"p > span" => {"color" => "red"}}, @cp.to_h["all"])
end

def test_selector_with_attrubute_selector
@cp.add_block!('input[type="checkbox"] { color: red; }')
assert_equal({"input[type=\"checkbox\"]" => {"color" => "red"}}, @cp.to_h["all"])
end

def test_not_modifier
@cp.add_block!('td:not(.active) { color: red;')
assert_equal({"td:not(.active)" => {"color" => "red"}}, @cp.to_h["all"])
end

def test_not_modifier_with_two_classes
@cp.add_block!('td:not(.active, .disabled) { color: red;')
assert_equal({"td:not(.active, .disabled)" => {"color" => "red"}}, @cp.to_h["all"])
end

def test_adding_a_rule_set
Expand All @@ -45,6 +87,11 @@ def test_adding_a_rule_set
assert_equal 'color: blue;', @cp.find_by_selector('div').join(' ')
end

def test_adding_a_rule
@cp.add_rule!('div', 'color: blue;')
assert_equal 'color: blue;', @cp.find_by_selector('div').join(' ')
end

def test_removing_a_rule_set
rs = CssParser::RuleSet.new('div', 'color: blue;')
@cp.add_rule_set!(rs)
Expand Down