css_parser 1.11.0 → 1.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 351dd37c4f67732bcc07deb8d86c00ff6d15931787a3c08880ee8b4321fdf27e
4
- data.tar.gz: bc830e26b6b15e06af401f0a1e9abed835e2deec34cd2986c428baca1d51d18b
3
+ metadata.gz: ebc43b9f09241b83ffe9a2878e0a2e5295c79f6d2e02bb20dcaf54d376f91e58
4
+ data.tar.gz: 5b29858923af894f1b84251c59de6a4638ae0929b63ac0b432a7607d9dd32111
5
5
  SHA512:
6
- metadata.gz: 0013771da7b78cf5f9ef4edb4af039a535d4b9e8709d68808159a27df9f6d76649f6976f4bbecace5773e5b7b1d1fc3e3a7afbd85be267765d5beee2ac565877
7
- data.tar.gz: 7ef404fe368409bb2951764e232b2364522b76a5c72f171c4560f5ba70a3c51b0d149709a9e4777ae2bc8049241358b6b382005fdc18298ea0d8b24085476a15
6
+ metadata.gz: fd7a00f2be5a2ddbf730eb115dbc0e15f9ac7a71ac92e1a056161a2b418e01d7abeb757c6f8d5fabafd9e4f7dba034b209ca571012ee44ee6b377b2e4bd8100f
7
+ data.tar.gz: ac8aeba706f1ae35126c6d5c4a150a52862af8654f1342e6c5b36ebce8c0b71c26e0d0931d09081b3b01a70685c3d2b173031a3e1ff32a8477799c3e39e013e4
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'strscan'
4
+
3
5
  module CssParser
4
6
  # Exception class used for any errors encountered while downloading remote files.
5
7
  class RemoteFileError < IOError; end
@@ -16,8 +18,8 @@ module CssParser
16
18
  # [<tt>import</tt>] Follow <tt>@import</tt> rules. Boolean, default is <tt>true</tt>.
17
19
  # [<tt>io_exceptions</tt>] Throw an exception if a link can not be found. Boolean, default is <tt>true</tt>.
18
20
  class Parser
19
- USER_AGENT = "Ruby CSS Parser/#{CssParser::VERSION} (https://github.com/premailer/css_parser)"
20
-
21
+ USER_AGENT = "Ruby CSS Parser/#{CssParser::VERSION} (https://github.com/premailer/css_parser)".freeze
22
+ RULESET_TOKENIZER_RX = /\s+|\\{2,}|\\?[{}\s"]|[()]|.[^\s"{}()\\]*/.freeze
21
23
  STRIP_CSS_COMMENTS_RX = %r{/\*.*?\*/}m.freeze
22
24
  STRIP_HTML_COMMENTS_RX = /<!--|-->/m.freeze
23
25
 
@@ -36,10 +38,14 @@ module CssParser
36
38
  class << self; attr_reader :folded_declaration_cache; end
37
39
 
38
40
  def initialize(options = {})
39
- @options = {absolute_paths: false,
40
- import: true,
41
- io_exceptions: true,
42
- capture_offsets: false}.merge(options)
41
+ @options = {
42
+ absolute_paths: false,
43
+ import: true,
44
+ io_exceptions: true,
45
+ rule_set_exceptions: true,
46
+ capture_offsets: false,
47
+ user_agent: USER_AGENT
48
+ }.merge(options)
43
49
 
44
50
  # array of RuleSets
45
51
  @rules = []
@@ -130,7 +136,7 @@ module CssParser
130
136
  block.scan(RE_AT_IMPORT_RULE).each do |import_rule|
131
137
  media_types = []
132
138
  if (media_string = import_rule[-1])
133
- media_string.split(/,/).each do |t|
139
+ media_string.split(',').each do |t|
134
140
  media_types << CssParser.sanitize_media_query(t) unless t.empty?
135
141
  end
136
142
  else
@@ -161,12 +167,44 @@ module CssParser
161
167
  parse_block_into_rule_sets!(block, options)
162
168
  end
163
169
 
164
- # Add a CSS rule by setting the +selectors+, +declarations+ and +media_types+.
170
+ # Add a CSS rule by setting the +selectors+, +declarations+
171
+ # and +media_types+. Optional pass +filename+ , +offset+ for source
172
+ # reference too.
165
173
  #
166
- # +media_types+ can be a symbol or an array of symbols.
167
- def add_rule!(selectors, declarations, media_types = :all)
168
- rule_set = RuleSet.new(selectors, declarations)
169
- add_rule_set!(rule_set, media_types)
174
+ # +media_types+ can be a symbol or an array of symbols. default to :all
175
+ # optional fields for source location for source location
176
+ # +filename+ can be a string or uri pointing to the file or url location.
177
+ # +offset+ should be Range object representing the start and end byte locations where the rule was found in the file.
178
+ def add_rule!(*args, selectors: nil, block: nil, filename: nil, offset: nil, media_types: :all) # rubocop:disable Metrics/ParameterLists
179
+ if args.any?
180
+ media_types = nil
181
+ if selectors || block || filename || offset || media_types
182
+ raise ArgumentError, "don't mix positional and keyword arguments arguments"
183
+ end
184
+
185
+ warn '[DEPRECATION] `add_rule!` with positional arguments is deprecated. ' \
186
+ 'Please use keyword arguments instead.', uplevel: 1
187
+
188
+ case args.length
189
+ when 2
190
+ selectors, block = args
191
+ when 3
192
+ selectors, block, media_types = args
193
+ else
194
+ raise ArgumentError
195
+ end
196
+ end
197
+
198
+ begin
199
+ rule_set = RuleSet.new(
200
+ selectors: selectors, block: block,
201
+ offset: offset, filename: filename
202
+ )
203
+
204
+ add_rule_set!(rule_set, media_types)
205
+ rescue ArgumentError => e
206
+ raise e if @options[:rule_set_exceptions]
207
+ end
170
208
  end
171
209
 
172
210
  # Add a CSS rule by setting the +selectors+, +declarations+, +filename+, +offset+ and +media_types+.
@@ -175,8 +213,11 @@ module CssParser
175
213
  # +offset+ should be Range object representing the start and end byte locations where the rule was found in the file.
176
214
  # +media_types+ can be a symbol or an array of symbols.
177
215
  def add_rule_with_offsets!(selectors, declarations, filename, offset, media_types = :all)
178
- rule_set = OffsetAwareRuleSet.new(filename, offset, selectors, declarations)
179
- add_rule_set!(rule_set, media_types)
216
+ warn '[DEPRECATION] `add_rule_with_offsets!` is deprecated. Please use `add_rule!` instead.', uplevel: 1
217
+ add_rule!(
218
+ selectors: selectors, block: declarations, media_types: media_types,
219
+ filename: filename, offset: offset
220
+ )
180
221
  end
181
222
 
182
223
  # Add a CssParser RuleSet object.
@@ -318,17 +359,21 @@ module CssParser
318
359
  in_at_media_rule = false
319
360
  in_media_block = false
320
361
 
321
- current_selectors = String.new
322
- current_media_query = String.new
323
- current_declarations = String.new
362
+ current_selectors = +''
363
+ current_media_query = +''
364
+ current_declarations = +''
324
365
 
325
366
  # once we are in a rule, we will use this to store where we started if we are capturing offsets
326
367
  rule_start = nil
327
- offset = nil
368
+ start_offset = nil
369
+ end_offset = nil
328
370
 
329
- block.scan(/\s+|\\{2,}|\\?[{}\s"]|.[^\s"{}\\]*/) do |token|
371
+ scanner = StringScanner.new(block)
372
+ until scanner.eos?
330
373
  # save the regex offset so that we know where in the file we are
331
- offset = Regexp.last_match.offset(0) if options[:capture_offsets]
374
+ start_offset = scanner.pos
375
+ token = scanner.scan(RULESET_TOKENIZER_RX)
376
+ end_offset = scanner.pos
332
377
 
333
378
  if token.start_with?('"') # found un-escaped double quote
334
379
  in_string = !in_string
@@ -355,20 +400,24 @@ module CssParser
355
400
  current_declarations.strip!
356
401
 
357
402
  unless current_declarations.empty?
403
+ add_rule_options = {
404
+ selectors: current_selectors, block: current_declarations,
405
+ media_types: current_media_queries
406
+ }
358
407
  if options[:capture_offsets]
359
- add_rule_with_offsets!(current_selectors, current_declarations, options[:filename], (rule_start..offset.last), current_media_queries)
360
- else
361
- add_rule!(current_selectors, current_declarations, current_media_queries)
408
+ add_rule_options[:filename] = options[:filename]
409
+ add_rule_options[:offset] = rule_start..end_offset
362
410
  end
411
+ add_rule!(**add_rule_options)
363
412
  end
364
413
 
365
- current_selectors = String.new
366
- current_declarations = String.new
414
+ current_selectors = +''
415
+ current_declarations = +''
367
416
 
368
417
  # restart our search for selectors and declarations
369
418
  rule_start = nil if options[:capture_offsets]
370
419
  end
371
- elsif token =~ /@media/i
420
+ elsif /@media/i.match?(token)
372
421
  # found '@media', reset current media_types
373
422
  in_at_media_rule = true
374
423
  current_media_queries = []
@@ -378,19 +427,28 @@ module CssParser
378
427
  in_at_media_rule = false
379
428
  in_media_block = true
380
429
  current_media_queries << CssParser.sanitize_media_query(current_media_query)
381
- current_media_query = String.new
430
+ current_media_query = +''
382
431
  elsif token.include?(',')
383
432
  # new media query begins
384
433
  token.tr!(',', ' ')
385
434
  token.strip!
386
435
  current_media_query << token << ' '
387
436
  current_media_queries << CssParser.sanitize_media_query(current_media_query)
388
- current_media_query = String.new
437
+ current_media_query = +''
389
438
  else
390
439
  token.strip!
391
- current_media_query << token << ' '
440
+ # special-case the ( and ) tokens to remove inner-whitespace
441
+ # (eg we'd prefer '(width: 500px)' to '( width: 500px )' )
442
+ case token
443
+ when '('
444
+ current_media_query << token
445
+ when ')'
446
+ current_media_query.sub!(/ ?$/, token)
447
+ else
448
+ current_media_query << token << ' '
449
+ end
392
450
  end
393
- elsif in_charset or token =~ /@charset/i
451
+ elsif in_charset or /@charset/i.match?(token)
394
452
  # iterate until we are out of the charset declaration
395
453
  in_charset = !token.include?(';')
396
454
  elsif !in_string && token.include?('}')
@@ -409,18 +467,22 @@ module CssParser
409
467
  current_selectors << token
410
468
 
411
469
  # mark this as the beginning of the selector unless we have already marked it
412
- rule_start = offset.first if options[:capture_offsets] && rule_start.nil? && token =~ /^[^\s]+$/
470
+ rule_start = start_offset if options[:capture_offsets] && rule_start.nil? && /^[^\s]+$/.match?(token)
413
471
  end
414
472
  end
415
473
 
416
474
  # check for unclosed braces
417
475
  return unless in_declarations > 0
418
476
 
419
- unless options[:capture_offsets]
420
- return add_rule!(current_selectors, current_declarations, current_media_queries)
477
+ add_rule_options = {
478
+ selectors: current_selectors, block: current_declarations,
479
+ media_types: current_media_queries
480
+ }
481
+ if options[:capture_offsets]
482
+ add_rule_options[:filename] = options[:filename]
483
+ add_rule_options[:offset] = rule_start..end_offset
421
484
  end
422
-
423
- add_rule_with_offsets!(current_selectors, current_declarations, options[:filename], (rule_start..offset.last), current_media_queries)
485
+ add_rule!(**add_rule_options)
424
486
  end
425
487
 
426
488
  # Load a remote CSS file.
@@ -438,6 +500,8 @@ module CssParser
438
500
  if options.is_a? Hash
439
501
  opts.merge!(options)
440
502
  else
503
+ warn '[DEPRECATION] `load_uri!` with positional arguments is deprecated. ' \
504
+ 'Please use keyword arguments instead.', uplevel: 1
441
505
  opts[:base_uri] = options if options.is_a? String
442
506
  opts[:media_types] = deprecated if deprecated
443
507
  end
@@ -464,6 +528,8 @@ module CssParser
464
528
  if options.is_a? Hash
465
529
  opts.merge!(options)
466
530
  else
531
+ warn '[DEPRECATION] `load_file!` with positional arguments is deprecated. ' \
532
+ 'Please use keyword arguments instead.', uplevel: 1
467
533
  opts[:base_dir] = options if options.is_a? String
468
534
  opts[:media_types] = deprecated if deprecated
469
535
  end
@@ -472,7 +538,7 @@ module CssParser
472
538
  return unless File.readable?(file_name)
473
539
  return unless circular_reference_check(file_name)
474
540
 
475
- src = IO.read(file_name)
541
+ src = File.read(file_name)
476
542
 
477
543
  opts[:filename] = file_name if opts[:capture_offsets]
478
544
  opts[:base_dir] = File.dirname(file_name)
@@ -487,6 +553,8 @@ module CssParser
487
553
  if options.is_a? Hash
488
554
  opts.merge!(options)
489
555
  else
556
+ warn '[DEPRECATION] `load_file!` with positional arguments is deprecated. ' \
557
+ 'Please use keyword arguments instead.', uplevel: 1
490
558
  opts[:base_dir] = options if options.is_a? String
491
559
  opts[:media_types] = deprecated if deprecated
492
560
  end
@@ -585,7 +653,7 @@ module CssParser
585
653
  http = Net::HTTP.new(uri.host, uri.port)
586
654
  end
587
655
 
588
- res = http.get(uri.request_uri, {'User-Agent' => USER_AGENT, 'Accept-Encoding' => 'gzip'})
656
+ res = http.get(uri.request_uri, {'User-Agent' => @options[:user_agent], 'Accept-Encoding' => 'gzip'})
589
657
  src = res.body
590
658
  charset = res.respond_to?(:charset) ? res.encoding : 'utf-8'
591
659
 
@@ -611,12 +679,7 @@ module CssParser
611
679
  end
612
680
 
613
681
  if charset
614
- if String.method_defined?(:encode)
615
- src.encode!('UTF-8', charset)
616
- else
617
- ic = Iconv.new('UTF-8//IGNORE', charset)
618
- src = ic.iconv(src)
619
- end
682
+ src.encode!('UTF-8', charset)
620
683
  end
621
684
  rescue
622
685
  @redirect_count = nil
@@ -657,7 +720,7 @@ module CssParser
657
720
  nodes = {}
658
721
  lines.each do |line|
659
722
  parts = line.split(':', 2)
660
- if parts[1] =~ /:/
723
+ if parts[1].include?(':')
661
724
  nodes[parts[0]] = css_node_to_h(hash, parts[0], parts[1])
662
725
  else
663
726
  nodes[parts[0].to_s.strip] = parts[1].to_s.strip
@@ -8,10 +8,10 @@ module CssParser
8
8
  # :stopdoc:
9
9
  # Base types
10
10
  RE_NL = Regexp.new('(\n|\r\n|\r|\f)')
11
- RE_NON_ASCII = Regexp.new('([\x00-\xFF])', Regexp::IGNORECASE, 'n') # [^\0-\177]
12
- RE_UNICODE = Regexp.new('(\\\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])*)', Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE, 'n')
11
+ RE_NON_ASCII = Regexp.new('([\x00-\xFF])', Regexp::IGNORECASE | Regexp::NOENCODING) # [^\0-\177]
12
+ RE_UNICODE = Regexp.new('(\\\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])*)', Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE | Regexp::NOENCODING)
13
13
  RE_ESCAPE = Regexp.union(RE_UNICODE, '|(\\\\[^\n\r\f0-9a-f])')
14
- RE_IDENT = Regexp.new("[\-]?([_a-z]|#{RE_NON_ASCII}|#{RE_ESCAPE})([_a-z0-9\-]|#{RE_NON_ASCII}|#{RE_ESCAPE})*", Regexp::IGNORECASE, 'n')
14
+ RE_IDENT = Regexp.new("[-]?([_a-z]|#{RE_NON_ASCII}|#{RE_ESCAPE})([_a-z0-9-]|#{RE_NON_ASCII}|#{RE_ESCAPE})*", Regexp::IGNORECASE | Regexp::NOENCODING)
15
15
 
16
16
  # General strings
17
17
  RE_STRING1 = /("(.[^\n\r\f"]*|\\#{RE_NL}|#{RE_ESCAPE})*")/.freeze
@@ -52,8 +52,10 @@ module CssParser
52
52
  # Special units
53
53
  BOX_MODEL_UNITS_RX = /(auto|inherit|0|(-*([0-9]+|[0-9]*\.[0-9]+)(rem|vw|vh|vm|vmin|vmax|e[mx]+|px|[cm]+m|p[tc+]|in|%)))([\s;]|\Z)/imx.freeze
54
54
  RE_LENGTH_OR_PERCENTAGE = Regexp.new('([\-]*(([0-9]*\.[0-9]+)|[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%))', Regexp::IGNORECASE)
55
- RE_BACKGROUND_POSITION = Regexp.new("((((#{RE_LENGTH_OR_PERCENTAGE})|left|center|right|top|bottom)[\s]*){1,2})", Regexp::IGNORECASE | Regexp::EXTENDED)
56
- RE_BACKGROUND_SIZE = Regexp.new("\\s*/\\s*((((#{RE_LENGTH_OR_PERCENTAGE})|auto|cover|contain|initial|inherit)[\\s]*){1,2})", Regexp::IGNORECASE | Regexp::EXTENDED)
55
+ RE_SINGLE_BACKGROUND_POSITION = /#{RE_LENGTH_OR_PERCENTAGE}|left|center|right|top|bottom/i.freeze
56
+ RE_SINGLE_BACKGROUND_SIZE = /#{RE_LENGTH_OR_PERCENTAGE}|auto|cover|contain|initial|inherit/i.freeze
57
+ RE_BACKGROUND_POSITION = /#{RE_SINGLE_BACKGROUND_POSITION}\s+#{RE_SINGLE_BACKGROUND_POSITION}|#{RE_SINGLE_BACKGROUND_POSITION}/.freeze
58
+ RE_BACKGROUND_SIZE = %r{\s*/\s*(#{RE_SINGLE_BACKGROUND_SIZE}\s+#{RE_SINGLE_BACKGROUND_SIZE}|#{RE_SINGLE_BACKGROUND_SIZE})}.freeze
57
59
  FONT_UNITS_RX = /((x+-)*small|medium|larger*|auto|inherit|([0-9]+|[0-9]*\.[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|%)*)/i.freeze
58
60
  RE_BORDER_STYLE = /(\s*^)?(none|hidden|dotted|dashed|solid|double|dot-dash|dot-dot-dash|wave|groove|ridge|inset|outset)(\s*$)?/imx.freeze
59
61
  RE_BORDER_UNITS = Regexp.union(BOX_MODEL_UNITS_RX, /(thin|medium|thick)/i)
@@ -11,8 +11,10 @@ module CssParser
11
11
  BACKGROUND_PROPERTIES = ['background-color', 'background-image', 'background-repeat', 'background-position', 'background-size', 'background-attachment'].freeze
12
12
  LIST_STYLE_PROPERTIES = ['list-style-type', 'list-style-position', 'list-style-image'].freeze
13
13
  FONT_STYLE_PROPERTIES = ['font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family'].freeze
14
+ FONT_WEIGHT_PROPERTIES = ['font-style', 'font-weight', 'font-variant'].freeze
14
15
  BORDER_STYLE_PROPERTIES = ['border-width', 'border-style', 'border-color'].freeze
15
16
  BORDER_PROPERTIES = ['border', 'border-left', 'border-right', 'border-top', 'border-bottom'].freeze
17
+ DIMENSION_DIRECTIONS = [:top, :right, :bottom, :left].freeze
16
18
 
17
19
  NUMBER_OF_DIMENSIONS = 4
18
20
 
@@ -26,6 +28,12 @@ module CssParser
26
28
 
27
29
  WHITESPACE_REPLACEMENT = '___SPACE___'
28
30
 
31
+ # Tokens for parse_declarations!
32
+ COLON = ':'.freeze
33
+ SEMICOLON = ';'.freeze
34
+ LPAREN = '('.freeze
35
+ RPAREN = ')'.freeze
36
+ IMPORTANT = '!important'.freeze
29
37
  class Declarations
30
38
  class Value
31
39
  attr_reader :value
@@ -58,7 +66,7 @@ module CssParser
58
66
 
59
67
  extend Forwardable
60
68
 
61
- def_delegators :declarations, :each
69
+ def_delegators :declarations, :each, :each_value
62
70
 
63
71
  def initialize(declarations = {})
64
72
  self.declarations = {}
@@ -81,18 +89,23 @@ module CssParser
81
89
  # puts declarations['margin']
82
90
  # => #<CssParser::RuleSet::Declarations::Value:0x00000000030c1838 @important=true, @order=2, @value="0px auto">
83
91
  #
84
- # If the property already exists its value will be over-written.
92
+ # If the property already exists its value will be over-written unless it was !important and the new value
93
+ # is not !important.
85
94
  # If the value is empty - property will be deleted
86
95
  def []=(property, value)
87
96
  property = normalize_property(property)
97
+ currently_important = declarations[property]&.important
88
98
 
89
- if value.is_a?(Value)
99
+ if value.is_a?(Value) && (!currently_important || value.important)
90
100
  declarations[property] = value
91
101
  elsif value.to_s.strip.empty?
92
102
  delete property
93
103
  else
94
- declarations[property] = Value.new(value)
104
+ value = Value.new(value)
105
+ declarations[property] = value if !currently_important || value.important
95
106
  end
107
+ rescue ArgumentError => e
108
+ raise e.exception, "#{property} #{e.message}"
96
109
  end
97
110
  alias add_declaration! []=
98
111
 
@@ -140,7 +153,7 @@ module CssParser
140
153
 
141
154
  if preserve_importance
142
155
  importance = get_value(property).important
143
- replacement_declarations.each { |_key, value| value.important = importance }
156
+ replacement_declarations.each_value { |value| value.important = importance }
144
157
  end
145
158
 
146
159
  replacement_keys = declarations.keys
@@ -188,7 +201,7 @@ module CssParser
188
201
  end
189
202
 
190
203
  def to_s(options = {})
191
- str = declarations.reduce(String.new) do |memo, (prop, value)|
204
+ str = declarations.reduce(+'') do |memo, (prop, value)|
192
205
  importance = options[:force_important] || value.important ? ' !important' : ''
193
206
  memo << "#{prop}: #{value.value}#{importance}; "
194
207
  end
@@ -221,6 +234,12 @@ module CssParser
221
234
 
222
235
  extend Forwardable
223
236
 
237
+ # optional field for storing source reference
238
+ # File offset range
239
+ attr_reader :offset
240
+ # the local or remote location
241
+ attr_accessor :filename
242
+
224
243
  # Array of selector strings.
225
244
  attr_reader :selectors
226
245
 
@@ -235,9 +254,38 @@ module CssParser
235
254
  alias []= add_declaration!
236
255
  alias remove_declaration! delete
237
256
 
238
- def initialize(selectors, block, specificity = nil)
257
+ def initialize(*args, selectors: nil, block: nil, offset: nil, filename: nil, specificity: nil) # rubocop:disable Metrics/ParameterLists
258
+ if args.any?
259
+ if selectors || block || offset || filename || specificity
260
+ raise ArgumentError, "don't mix positional and keyword arguments"
261
+ end
262
+
263
+ warn '[DEPRECATION] positional arguments are deprecated use keyword instead.', uplevel: 1
264
+
265
+ case args.length
266
+ when 2
267
+ selectors, block = args
268
+ when 3
269
+ selectors, block, specificity = args
270
+ when 4
271
+ filename, offset, selectors, block = args
272
+ when 5
273
+ filename, offset, selectors, block, specificity = args
274
+ else
275
+ raise ArgumentError
276
+ end
277
+ end
278
+
239
279
  @selectors = []
240
280
  @specificity = specificity
281
+
282
+ unless offset.nil? == filename.nil?
283
+ raise ArgumentError, 'require both offset and filename or no offset and no filename'
284
+ end
285
+
286
+ @offset = offset
287
+ @filename = filename
288
+
241
289
  parse_selectors!(selectors) if selectors
242
290
  parse_declarations!(block)
243
291
  end
@@ -306,7 +354,7 @@ module CssParser
306
354
 
307
355
  replacement =
308
356
  if value.match(CssParser::RE_INHERIT)
309
- BACKGROUND_PROPERTIES.map { |key| [key, 'inherit'] }.to_h
357
+ BACKGROUND_PROPERTIES.to_h { |key| [key, 'inherit'] }
310
358
  else
311
359
  {
312
360
  'background-image' => value.slice!(CssParser::RE_IMAGE),
@@ -412,17 +460,17 @@ module CssParser
412
460
  else
413
461
  font_props['font-family'] = m
414
462
  end
415
- elsif m =~ /normal|inherit/i
416
- ['font-style', 'font-weight', 'font-variant'].each do |font_prop|
463
+ elsif /normal|inherit/i.match?(m)
464
+ FONT_WEIGHT_PROPERTIES.each do |font_prop|
417
465
  font_props[font_prop] ||= m
418
466
  end
419
- elsif m =~ /italic|oblique/i
467
+ elsif /italic|oblique/i.match?(m)
420
468
  font_props['font-style'] = m
421
- elsif m =~ /small-caps/i
469
+ elsif /small-caps/i.match?(m)
422
470
  font_props['font-variant'] = m
423
- elsif m =~ /[1-9]00$|bold|bolder|lighter/i
471
+ elsif /[1-9]00$|bold|bolder|lighter/i.match?(m)
424
472
  font_props['font-weight'] = m
425
- elsif m =~ CssParser::FONT_UNITS_RX
473
+ elsif CssParser::FONT_UNITS_RX.match?(m)
426
474
  if m.include?('/')
427
475
  font_props['font-size'], font_props['line-height'] = m.split('/', 2)
428
476
  else
@@ -445,8 +493,8 @@ module CssParser
445
493
  value = declaration.value.dup
446
494
 
447
495
  replacement =
448
- if value =~ CssParser::RE_INHERIT
449
- LIST_STYLE_PROPERTIES.map { |key| [key, 'inherit'] }.to_h
496
+ if CssParser::RE_INHERIT.match?(value)
497
+ LIST_STYLE_PROPERTIES.to_h { |key| [key, 'inherit'] }
450
498
  else
451
499
  {
452
500
  'list-style-type' => value.slice!(CssParser::RE_LIST_STYLE_TYPE),
@@ -511,15 +559,15 @@ module CssParser
511
559
  #
512
560
  # TODO: this is extremely similar to create_background_shorthand! and should be combined
513
561
  def create_border_shorthand! # :nodoc:
514
- values = BORDER_STYLE_PROPERTIES.map do |property|
562
+ values = BORDER_STYLE_PROPERTIES.filter_map do |property|
515
563
  next unless (declaration = declarations[property])
516
564
  next if declaration.important
517
565
  # can't merge if any value contains a space (i.e. has multiple values)
518
566
  # we temporarily remove any spaces after commas for the check (inside rgba, etc...)
519
- next if declaration.value.gsub(/,\s/, ',').strip =~ /\s/
567
+ next if /\s/.match?(declaration.value.gsub(/,\s/, ',').strip)
520
568
 
521
569
  declaration.value
522
- end.compact
570
+ end
523
571
 
524
572
  return if values.size != BORDER_STYLE_PROPERTIES.size
525
573
 
@@ -536,7 +584,7 @@ module CssParser
536
584
  return if declarations.size < NUMBER_OF_DIMENSIONS
537
585
 
538
586
  DIMENSIONS.each do |property, dimensions|
539
- values = [:top, :right, :bottom, :left].each_with_index.with_object({}) do |(side, index), result|
587
+ values = DIMENSION_DIRECTIONS.each_with_index.with_object({}) do |(side, index), result|
540
588
  next unless (declaration = declarations[dimensions[index]])
541
589
 
542
590
  result[side] = declaration.value
@@ -559,7 +607,7 @@ module CssParser
559
607
  def create_font_shorthand! # :nodoc:
560
608
  return unless FONT_STYLE_PROPERTIES.all? { |prop| declarations.key?(prop) }
561
609
 
562
- new_value = String.new
610
+ new_value = +''
563
611
  ['font-style', 'font-variant', 'font-weight'].each do |property|
564
612
  unless declarations[property].value == 'normal'
565
613
  new_value << declarations[property].value << ' '
@@ -596,7 +644,7 @@ module CssParser
596
644
  return [:top] if values.values.uniq.count == 1
597
645
 
598
646
  # `/* top | right | bottom | left */`
599
- return [:top, :right, :bottom, :left] if values[:left] != values[:right]
647
+ return DIMENSION_DIRECTIONS if values[:left] != values[:right]
600
648
 
601
649
  # Vertical are the same & horizontal are the same, `/* vertical | horizontal */`
602
650
  return [:top, :left] if values[:top] == values[:bottom]
@@ -610,20 +658,32 @@ module CssParser
610
658
  return unless block
611
659
 
612
660
  continuation = nil
613
- block.split(/[;$]+/m).each do |decs|
614
- decs = (continuation ? continuation + decs : decs)
615
- if decs =~ /\([^)]*\Z/ # if it has an unmatched parenthesis
616
- continuation = "#{decs};"
617
- elsif (matches = decs.match(/\s*(.[^:]*)\s*:\s*(.+?)(?:;?\s*\Z)/i))
618
- # skip end_of_declaration
619
- property = matches[1]
620
- value = matches[2]
621
- add_declaration!(property, value)
622
- continuation = nil
661
+ block.split(SEMICOLON) do |decs|
662
+ decs = (continuation ? "#{continuation};#{decs}" : decs)
663
+ if unmatched_open_parenthesis?(decs)
664
+ # Semicolon happened within parenthesis, so it is a part of the value
665
+ # the rest of the value is in the next segment
666
+ continuation = decs
667
+ next
623
668
  end
669
+
670
+ next unless (colon = decs.index(COLON))
671
+
672
+ property = decs[0, colon]
673
+ value = decs[(colon + 1)..]
674
+ property.strip!
675
+ value.strip!
676
+ next if property.empty? || value.empty? || value.casecmp?(IMPORTANT)
677
+
678
+ add_declaration!(property, value)
679
+ continuation = nil
624
680
  end
625
681
  end
626
682
 
683
+ def unmatched_open_parenthesis?(declarations)
684
+ (lparen_index = declarations.index(LPAREN)) && !declarations.index(RPAREN, lparen_index)
685
+ end
686
+
627
687
  #--
628
688
  # TODO: way too simplistic
629
689
  #++
@@ -648,18 +708,4 @@ module CssParser
648
708
  end
649
709
  end
650
710
  end
651
-
652
- class OffsetAwareRuleSet < RuleSet
653
- # File offset range
654
- attr_reader :offset
655
-
656
- # the local or remote location
657
- attr_accessor :filename
658
-
659
- def initialize(filename, offset, selectors, block, specificity = nil)
660
- super(selectors, block, specificity)
661
- @offset = offset
662
- @filename = filename
663
- end
664
- end
665
711
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CssParser
4
- VERSION = '1.11.0'.freeze
4
+ VERSION = '1.21.1'.freeze
5
5
  end
data/lib/css_parser.rb CHANGED
@@ -6,7 +6,6 @@ require 'net/https'
6
6
  require 'digest/md5'
7
7
  require 'zlib'
8
8
  require 'stringio'
9
- require 'iconv' unless String.method_defined?(:encode)
10
9
 
11
10
  require 'css_parser/version'
12
11
  require 'css_parser/rule_set'
@@ -58,7 +57,7 @@ module CssParser
58
57
  # in case called like CssParser.merge([rule_set, rule_set])
59
58
  rule_sets.flatten! if rule_sets[0].is_a?(Array)
60
59
 
61
- unless rule_sets.all? { |rs| rs.is_a?(CssParser::RuleSet) }
60
+ unless rule_sets.all?(CssParser::RuleSet)
62
61
  raise ArgumentError, 'all parameters must be CssParser::RuleSets.'
63
62
  end
64
63
 
@@ -71,17 +70,17 @@ module CssParser
71
70
  rule_set.expand_shorthand!
72
71
 
73
72
  specificity = rule_set.specificity
74
- specificity ||= rule_set.selectors.map { |s| calculate_specificity(s) }.compact.max || 0
73
+ specificity ||= rule_set.selectors.filter_map { |s| calculate_specificity(s) }.max || 0
75
74
 
76
75
  rule_set.each_declaration do |property, value, is_important|
77
76
  # Add the property to the list to be folded per http://www.w3.org/TR/CSS21/cascade.html#cascading-order
78
- if not properties.key?(property)
77
+ if !properties.key?(property)
79
78
  properties[property] = {value: value, specificity: specificity, is_important: is_important}
80
79
  elsif is_important
81
- if not properties[property][:is_important] or properties[property][:specificity] <= specificity
80
+ if !properties[property][:is_important] || properties[property][:specificity] <= specificity
82
81
  properties[property] = {value: value, specificity: specificity, is_important: is_important}
83
82
  end
84
- elsif properties[property][:specificity] < specificity or properties[property][:specificity] == specificity
83
+ elsif properties[property][:specificity] < specificity || properties[property][:specificity] == specificity
85
84
  unless properties[property][:is_important]
86
85
  properties[property] = {value: value, specificity: specificity, is_important: is_important}
87
86
  end
@@ -111,7 +110,7 @@ module CssParser
111
110
  #++
112
111
  def self.calculate_specificity(selector)
113
112
  a = 0
114
- b = selector.scan(/\#/).length
113
+ b = selector.scan('#').length
115
114
  c = selector.scan(NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX_NC).length
116
115
  d = selector.scan(ELEMENTS_AND_PSEUDO_ELEMENTS_RX_NC).length
117
116
 
@@ -139,7 +138,7 @@ module CssParser
139
138
  css.gsub(URI_RX) do
140
139
  uri = Regexp.last_match(1).to_s.gsub(/["']+/, '')
141
140
  # Don't process URLs that are already absolute
142
- unless uri.match(%r{^[a-z]+://}i)
141
+ unless uri.match?(%r{^[a-z]+://}i)
143
142
  begin
144
143
  uri = base_uri.join(uri)
145
144
  rescue
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: css_parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.11.0
4
+ version: 1.21.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Dunae
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-14 00:00:00.000000000 Z
11
+ date: 2025-03-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -24,118 +24,6 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: benchmark-ips
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: bump
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: maxitest
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: memory_profiler
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: rake
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: rubocop
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - "~>"
102
- - !ruby/object:Gem::Version
103
- version: '1.8'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - "~>"
109
- - !ruby/object:Gem::Version
110
- version: '1.8'
111
- - !ruby/object:Gem::Dependency
112
- name: rubocop-rake
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '0'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ">="
123
- - !ruby/object:Gem::Version
124
- version: '0'
125
- - !ruby/object:Gem::Dependency
126
- name: webrick
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0'
139
27
  description: A set of classes for parsing CSS in Ruby.
140
28
  email: code@dunae.ca
141
29
  executables: []
@@ -155,7 +43,8 @@ metadata:
155
43
  changelog_uri: https://github.com/premailer/css_parser/blob/master/CHANGELOG.md
156
44
  source_code_uri: https://github.com/premailer/css_parser
157
45
  bug_tracker_uri: https://github.com/premailer/css_parser/issues
158
- post_install_message:
46
+ rubygems_mfa_required: 'true'
47
+ post_install_message:
159
48
  rdoc_options: []
160
49
  require_paths:
161
50
  - lib
@@ -163,15 +52,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
163
52
  requirements:
164
53
  - - ">="
165
54
  - !ruby/object:Gem::Version
166
- version: '2.4'
55
+ version: '3.0'
167
56
  required_rubygems_version: !ruby/object:Gem::Requirement
168
57
  requirements:
169
58
  - - ">="
170
59
  - !ruby/object:Gem::Version
171
60
  version: '0'
172
61
  requirements: []
173
- rubygems_version: 3.2.16
174
- signing_key:
62
+ rubygems_version: 3.4.19
63
+ signing_key:
175
64
  specification_version: 4
176
65
  summary: Ruby CSS parser.
177
66
  test_files: []