Skip to content

Commit

Permalink
Rename CssParser::Parser to CssParser::Document
Browse files Browse the repository at this point in the history
Instances of CssParser::Parser had little to nothing to do with parsing
the actual css. It's a wrapper to hold the ruleset and give some
convince method on top of that.
  • Loading branch information
stoivo committed Jun 18, 2024
1 parent 91fd438 commit a3a342a
Show file tree
Hide file tree
Showing 13 changed files with 128 additions and 121 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ gem install css_parser

# Usage

You initiate a document `CssParser::Document.new` and you can start to load it with css. Main methods to add css are: load_uri! (load url and follows @imports based on the full url), load_file! (loads file and follows @imports based on path from file imported) and load_string! (load a block of css). All of these apis tries to absolute all urls
You initiate a document `CssParser::Document.new` and you can start to load it with css. Main methods to add css are: load_uri! (load url and follows @imports based on the full url), load_file! (loads file and follows @imports based on path from file imported) and load_string! (load a block of css). All of these apis tries to absolute all urls.

CssParser::Document -> Wrapper to holds all the rules on one block
CssParser::RuleSet -> Wrapper to hold each use like `.a, .b { color: hotpink; }`. notice this example has two selectors `.a` and `.b`


```Ruby
Expand Down
8 changes: 4 additions & 4 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ task :benchmark do
complex_css_path = fixtures_dir.join('complex.css').to_s.freeze

Benchmark.ips do |x|
x.report('import1.css loading') { CssParser::Parser.new.load_file!(import_css_path) }
x.report('complex.css loading') { CssParser::Parser.new.load_file!(complex_css_path) }
x.report('import1.css loading') { CssParser::Document.new.load_file!(import_css_path) }
x.report('complex.css loading') { CssParser::Document.new.load_file!(complex_css_path) }
end

puts

report = MemoryProfiler.report { CssParser::Parser.new.load_file!(import_css_path) }
report = MemoryProfiler.report { CssParser::Document.new.load_file!(import_css_path) }
puts "Loading `import1.css` allocated #{report.total_allocated} objects, #{report.total_allocated_memsize / 1024} KiB"

report = MemoryProfiler.report { CssParser::Parser.new.load_file!(complex_css_path) }
report = MemoryProfiler.report { CssParser::Document.new.load_file!(complex_css_path) }
puts "Loading `complex.css` allocated #{report.total_allocated} objects, #{report.total_allocated_memsize / 1024} KiB"
end
2 changes: 1 addition & 1 deletion lib/css_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
require 'css_parser/rule_set/declarations'
require 'css_parser/regexps'
require 'css_parser/parser_fx'
require 'css_parser/parser'
require 'css_parser/document'

module CssParser
class Error < StandardError; end
Expand Down
200 changes: 102 additions & 98 deletions lib/css_parser/parser.rb → lib/css_parser/document.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# frozen_string_literal: true

module CssParser
# == Parser class
# == Document class
#
# All CSS is converted to UTF-8.
#
# When calling Parser#new there are some configuaration options:
# When calling Document#new there are some configuaration options:
# [<tt>absolute_paths</tt>] Convert relative paths to absolute paths (<tt>href</tt>, <tt>src</tt> and <tt>url('')</tt>. Boolean, default is <tt>false</tt>.
# [<tt>import</tt>] Follow <tt>@import</tt> rules. Boolean, default is <tt>true</tt>.
# [<tt>io_exceptions</tt>] Throw an exception if a link can not be found. Boolean, default is <tt>true</tt>.
class Parser
class Document
module Util
def self.ensure_media_types(media_types)
Array(media_types)
Expand Down Expand Up @@ -39,6 +39,37 @@ def initialize(options = {})
@rules = []
end

# Iterate through RuleSet objects.
#
# +media_types+ can be a symbol or an array of media queries (:all or string).
def each_rule_set(media_types = :all) # :yields: rule_set, media_types
return to_enum(__method__, media_types) unless block_given?

media_types = Util.ensure_media_types(media_types)
@rules.each do |block|
if media_types.include?(:all) or block[:media_types].any? { |mt| media_types.include?(mt) }
yield(block[:rules], block[:media_types])
end
end
end

# Iterate through CSS selectors.
#
# The difference between each_rule_set and this method is that this method
# exposes each selector to to the rule.
#
# +media_types+ can be a symbol or an array of media queries (:all or string).
# See RuleSet#each_selector for +options+.
def each_selector(all_media_types = :all, options = {}) # :yields: selectors, declarations, specificity, media_types
return to_enum(__method__, all_media_types, options) unless block_given?

each_rule_set(all_media_types) do |rule_set, media_types|
rule_set.each_selector(options) do |selectors, declarations, specificity|
yield selectors, declarations, specificity, media_types
end
end
end

# Get declarations by selector.
#
# +media_types+ are optional, and can be a symbol or an array of media queries (:all or string).
Expand Down Expand Up @@ -80,6 +111,73 @@ def find_rule_sets(selectors, media_types = :all)
rule_sets
end

# A hash of { :media_query => rule_sets }
def rules_by_media_query
rules_by_media = {}
@rules.each do |block|
block[:media_types].each do |mt|
unless rules_by_media.key?(mt)
rules_by_media[mt] = []
end
rules_by_media[mt] << block[:rules]
end
end

rules_by_media
end

# Load a remote CSS file.
#
# You can also pass in file://test.css
#
# See add_block! for options.
def load_uri!(uri, options = {})
uri = Addressable::URI.parse(uri) unless uri.respond_to? :scheme

opts = {base_uri: nil, media_types: :all}
opts.merge!(options)

if uri.scheme == 'file' or uri.scheme.nil?
uri.path = File.expand_path(uri.path)
uri.scheme = 'file'
end

opts[:base_uri] = uri if opts[:base_uri].nil?

# pass on the uri if we are capturing file offsets
opts[:filename] = uri.to_s if opts[:capture_offsets]

src, = @options[:http_resource].read_remote_file(uri) # skip charset

add_block!(src, opts) if src
end

# Load a local CSS file.
def load_file!(file_name, options = {})
opts = {base_dir: nil, media_types: :all}
opts.merge!(options)

file_path = @options[:file_resource]
.find_file(file_name, base_dir: opts[:base_dir])
# we we cant read the file it's nil
return if file_path.nil?

src = File.read(file_path)

opts[:filename] = file_path if opts[:capture_offsets]
opts[:base_dir] = File.dirname(file_path)

add_block!(src, opts)
end

# Load a local CSS string.
def load_string!(src, options = {})
opts = {base_dir: nil, media_types: :all}
opts.merge!(options)

add_block!(src, opts)
end

# Add a raw block of CSS.
#
# In order to follow +@import+ rules you must supply either a
Expand All @@ -98,7 +196,7 @@ def find_rule_sets(selectors, media_types = :all)
# }
# EOT
#
# parser = CssParser::Parser.new
# parser = CssParser::Document.new
# parser.add_block!(css)
def add_block!(block, options = {})
options = {base_uri: nil, base_dir: nil, charset: nil, media_types: [:all], only_media_types: [:all]}.merge(options)
Expand Down Expand Up @@ -255,19 +353,6 @@ def remove_rule_set!(ruleset, media_types = :all)
end
end

# Iterate through RuleSet objects.
#
# +media_types+ can be a symbol or an array of media queries (:all or string).
def each_rule_set(media_types = :all) # :yields: rule_set, media_types
media_types = Util.ensure_media_types(media_types)

@rules.each do |block|
if media_types.include?(:all) or block[:media_types].any? { |mt| media_types.include?(mt) }
yield(block[:rules], block[:media_types])
end
end
end

# Output all CSS rules as a Hash
def to_h(which_media = :all)
out = {}
Expand All @@ -289,20 +374,6 @@ def to_h(which_media = :all)
out
end

# Iterate through CSS selectors.
#
# +media_types+ can be a symbol or an array of media queries (:all or string).
# See RuleSet#each_selector for +options+.
def each_selector(all_media_types = :all, options = {}) # :yields: selectors, declarations, specificity, media_types
return to_enum(__method__, all_media_types, options) unless block_given?

each_rule_set(all_media_types) do |rule_set, media_types|
rule_set.each_selector(options) do |selectors, declarations, specificity|
yield selectors, declarations, specificity, media_types
end
end
end

# Output all CSS rules as a single stylesheet.
def to_s(which_media = :all)
out = []
Expand Down Expand Up @@ -334,73 +405,6 @@ def to_s(which_media = :all)
out.join("\n")
end

# A hash of { :media_query => rule_sets }
def rules_by_media_query
rules_by_media = {}
@rules.each do |block|
block[:media_types].each do |mt|
unless rules_by_media.key?(mt)
rules_by_media[mt] = []
end
rules_by_media[mt] << block[:rules]
end
end

rules_by_media
end

# Load a remote CSS file.
#
# You can also pass in file://test.css
#
# See add_block! for options.
def load_uri!(uri, options = {})
uri = Addressable::URI.parse(uri) unless uri.respond_to? :scheme

opts = {base_uri: nil, media_types: :all}
opts.merge!(options)

if uri.scheme == 'file' or uri.scheme.nil?
uri.path = File.expand_path(uri.path)
uri.scheme = 'file'
end

opts[:base_uri] = uri if opts[:base_uri].nil?

# pass on the uri if we are capturing file offsets
opts[:filename] = uri.to_s if opts[:capture_offsets]

src, = @options[:http_resource].read_remote_file(uri) # skip charset

add_block!(src, opts) if src
end

# Load a local CSS file.
def load_file!(file_name, options = {})
opts = {base_dir: nil, media_types: :all}
opts.merge!(options)

file_path = @options[:file_resource]
.find_file(file_name, base_dir: opts[:base_dir])
# we we cant read the file it's nil
return if file_path.nil?

src = File.read(file_path)

opts[:filename] = file_path if opts[:capture_offsets]
opts[:base_dir] = File.dirname(file_path)

add_block!(src, opts)
end

# Load a local CSS string.
def load_string!(src, options = {})
opts = {base_dir: nil, media_types: :all}
opts.merge!(options)

add_block!(src, opts)
end

private

# recurse through nested nodes and return them as Hashes nested in
Expand Down
8 changes: 4 additions & 4 deletions test/test_css_parser_basic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class CssParserBasicTests < Minitest::Test
include CssParser

def setup
@cp = CssParser::Parser.new
@cp = Document.new
@css = <<-CSS
html, body, p { margin: 0px; }
p { padding: 0px; }
Expand Down Expand Up @@ -55,15 +55,15 @@ def test_removing_a_rule_set

def test_toggling_uri_conversion
# with conversion
cp_with_conversion = Parser.new(absolute_paths: true)
cp_with_conversion = Document.new(absolute_paths: true)
cp_with_conversion.add_block!("body { background: url('../style/yellow.png?abc=123') };",
base_uri: 'http://example.org/style/basic.css')

assert_equal "background: url('http://example.org/style/yellow.png?abc=123');",
cp_with_conversion['body'].join(' ')

# without conversion
cp_without_conversion = Parser.new(absolute_paths: false)
cp_without_conversion = Document.new(absolute_paths: false)
cp_without_conversion.add_block!("body { background: url('../style/yellow.png?abc=123') };",
base_uri: 'http://example.org/style/basic.css')

Expand All @@ -72,7 +72,7 @@ def test_toggling_uri_conversion
end

def test_converting_to_hash
rs = CssParser::RuleSet.new(selectors: 'div', block: 'color: blue;')
rs = RuleSet.new(selectors: 'div', block: 'color: blue;')
@cp.add_rule_set!(rs)
hash = @cp.to_h
assert_equal 'blue', hash['all']['div']['color']
Expand Down
10 changes: 5 additions & 5 deletions test/test_css_parser_loading.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class CssParserLoadingTests < Minitest::Test
include CssParser

def setup
@cp = Parser.new
@cp = Document.new
@uri_base = 'http://localhost:12000'
end

Expand Down Expand Up @@ -109,7 +109,7 @@ def test_following_at_import_rules_remote
def test_imports_disabled
stub_request_file("import1.css")

cp = Parser.new(import: false)
cp = Document.new(import: false)
cp.load_uri!("#{@uri_base}/import1.css")

# from '/import1.css'
Expand Down Expand Up @@ -202,7 +202,7 @@ def test_remote_circular_reference_exception
def test_suppressing_circular_reference_exceptions
stub_request_file("import-circular-reference.css")

cp_without_exceptions = Parser.new(io_exceptions: false)
cp_without_exceptions = Document.new(io_exceptions: false)

cp_without_exceptions.load_uri!("#{@uri_base}/import-circular-reference.css")
end
Expand All @@ -211,15 +211,15 @@ def test_toggling_not_found_exceptions
stub_request(:get, "http://localhost:12000/no-exist.xyz")
.to_return(status: 404, body: "", headers: {})

cp_with_exceptions = Parser.new(io_exceptions: true)
cp_with_exceptions = Document.new(io_exceptions: true)

err = assert_raises HTTPReadURL::RemoteFileError do
cp_with_exceptions.load_uri!("#{@uri_base}/no-exist.xyz")
end

assert_includes err.message, "#{@uri_base}/no-exist.xyz"

cp_without_exceptions = Parser.new(io_exceptions: false)
cp_without_exceptions = Document.new(io_exceptions: false)

cp_without_exceptions.load_uri!("#{@uri_base}/no-exist.xyz")
end
Expand Down
2 changes: 1 addition & 1 deletion test/test_css_parser_media_types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class CssParserMediaTypesTests < Minitest::Test
include CssParser

def setup
@cp = Parser.new
@cp = Document.new
end

def test_that_media_types_dont_include_all
Expand Down
2 changes: 1 addition & 1 deletion test/test_css_parser_misc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class CssParserTests < Minitest::Test
include CssParser

def setup
@cp = Parser.new
@cp = Document.new
end

def test_utf8
Expand Down
Loading

0 comments on commit a3a342a

Please sign in to comment.