Class: CssParser::RuleSet

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Defined in:
lib/css_parser/rule_set.rb

Direct Known Subclasses

OffsetAwareRuleSet

Defined Under Namespace

Classes: Declarations

Constant Summary collapse

RE_ELEMENTS_AND_PSEUDO_ELEMENTS =

Patterns for specificity calculations

/((^|[\s+>]+)\w+|:(first-line|first-letter|before|after))/i.freeze
RE_NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES =
/(\.\w+)|(\[\w+)|(:(link|first-child|lang))/i.freeze
BACKGROUND_PROPERTIES =
['background-color', 'background-image', 'background-repeat', 'background-position', 'background-size', 'background-attachment'].freeze
LIST_STYLE_PROPERTIES =
['list-style-type', 'list-style-position', 'list-style-image'].freeze
FONT_STYLE_PROPERTIES =
['font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family'].freeze
BORDER_STYLE_PROPERTIES =
['border-width', 'border-style', 'border-color'].freeze
BORDER_PROPERTIES =
['border', 'border-left', 'border-right', 'border-top', 'border-bottom'].freeze
NUMBER_OF_DIMENSIONS =
4
DIMENSIONS =
[
  ['margin', %w[margin-top margin-right margin-bottom margin-left]],
  ['padding', %w[padding-top padding-right padding-bottom padding-left]],
  ['border-color', %w[border-top-color border-right-color border-bottom-color border-left-color]],
  ['border-style', %w[border-top-style border-right-style border-bottom-style border-left-style]],
  ['border-width', %w[border-top-width border-right-width border-bottom-width border-left-width]]
].freeze
WHITESPACE_REPLACEMENT =
'___SPACE___'

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(selectors, block, specificity = nil) ⇒ RuleSet

Returns a new instance of RuleSet.



240
241
242
243
244
245
# File 'lib/css_parser/rule_set.rb', line 240

def initialize(selectors, block, specificity = nil)
  @selectors = []
  @specificity = specificity
  parse_selectors!(selectors) if selectors
  parse_declarations!(block)
end

Instance Attribute Details

#selectorsObject (readonly)

Array of selector strings.



227
228
229
# File 'lib/css_parser/rule_set.rb', line 227

def selectors
  @selectors
end

#specificityObject

Integer with the specificity to use for this RuleSet.



230
231
232
# File 'lib/css_parser/rule_set.rb', line 230

def specificity
  @specificity
end

Instance Method Details

#add_declaration!Object Also known as: []=



236
# File 'lib/css_parser/rule_set.rb', line 236

def_delegators :declarations, :add_declaration!, :delete

#create_background_shorthand!Object

Looks for long format CSS background properties (e.g. background-color) and converts them into a shorthand CSS background property.

Leaves properties declared !important alone.



498
499
500
501
502
503
504
505
506
507
508
509
# File 'lib/css_parser/rule_set.rb', line 498

def create_background_shorthand! # :nodoc:
  # When we have a background-size property we must separate it and distinguish it from
  # background-position by preceding it with a backslash. In this case we also need to
  # have a background-position property, so we set it if it's missing.
  # http://www.w3schools.com/cssref/css3_pr_background.asp
  if (declaration = declarations['background-size']) && !declaration.important
    declarations['background-position'] ||= '0% 0%'
    declaration.value = "/ #{declaration.value}"
  end

  create_shorthand_properties! BACKGROUND_PROPERTIES, 'background'
end

#create_border_shorthand!Object

Combine border-color, border-style and border-width into border Should be run after create_dimensions_shorthand!

TODO: this is extremely similar to create_background_shorthand! and should be combined



515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
# File 'lib/css_parser/rule_set.rb', line 515

def create_border_shorthand! # :nodoc:
  values = BORDER_STYLE_PROPERTIES.map do |property|
    next unless (declaration = declarations[property])
    next if declaration.important
    # can't merge if any value contains a space (i.e. has multiple values)
    # we temporarily remove any spaces after commas for the check (inside rgba, etc...)
    next if declaration.value.gsub(/,\s/, ',').strip =~ /\s/

    declaration.value
  end.compact

  return if values.size != BORDER_STYLE_PROPERTIES.size

  BORDER_STYLE_PROPERTIES.each do |property|
    declarations.delete(property)
  end

  declarations['border'] = values.join(' ')
end

#create_dimensions_shorthand!Object

Looks for long format CSS dimensional properties (margin, padding, border-color, border-style and border-width) and converts them into shorthand CSS properties.



537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
# File 'lib/css_parser/rule_set.rb', line 537

def create_dimensions_shorthand! # :nodoc:
  return if declarations.size < NUMBER_OF_DIMENSIONS

  DIMENSIONS.each do |property, dimensions|
    values = [:top, :right, :bottom, :left].each_with_index.with_object({}) do |(side, index), result|
      next unless (declaration = declarations[dimensions[index]])

      result[side] = declaration.value
    end

    # All four dimensions must be present
    next if values.size != dimensions.size

    new_value = values.values_at(*compute_dimensions_shorthand(values)).join(' ').strip
    declarations[property] = new_value unless new_value.empty?

    # Delete the longhand values
    dimensions.each { |d| declarations.delete(d) }
  end
end

#create_font_shorthand!Object

Looks for long format CSS font properties (e.g. font-weight) and tries to convert them into a shorthand CSS font property. All font properties must be present in order to create a shorthand declaration.



561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
# File 'lib/css_parser/rule_set.rb', line 561

def create_font_shorthand! # :nodoc:
  return unless FONT_STYLE_PROPERTIES.all? { |prop| declarations.key?(prop) }

  new_value = String.new
  ['font-style', 'font-variant', 'font-weight'].each do |property|
    unless declarations[property].value == 'normal'
      new_value << declarations[property].value << ' '
    end
  end

  new_value << declarations['font-size'].value

  unless declarations['line-height'].value == 'normal'
    new_value << '/' << declarations['line-height'].value
  end

  new_value << ' ' << declarations['font-family'].value

  declarations['font'] = new_value.gsub(/\s+/, ' ')

  FONT_STYLE_PROPERTIES.each { |prop| declarations.delete(prop) }
end

#create_list_style_shorthand!Object

Looks for long format CSS list-style properties (e.g. list-style-type) and converts them into a shorthand CSS list-style property.

Leaves properties declared !important alone.



588
589
590
# File 'lib/css_parser/rule_set.rb', line 588

def create_list_style_shorthand! # :nodoc:
  create_shorthand_properties! LIST_STYLE_PROPERTIES, 'list-style'
end

#create_shorthand!Object

Create shorthand declarations (e.g. margin or font) whenever possible.



464
465
466
467
468
469
470
471
# File 'lib/css_parser/rule_set.rb', line 464

def create_shorthand!
  create_background_shorthand!
  create_dimensions_shorthand!
  # border must be shortened after dimensions
  create_border_shorthand!
  create_font_shorthand!
  create_list_style_shorthand!
end

#create_shorthand_properties!(properties, shorthand_property) ⇒ Object

Combine several properties into a shorthand one



474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
# File 'lib/css_parser/rule_set.rb', line 474

def create_shorthand_properties!(properties, shorthand_property) # :nodoc:
  values = []
  properties_to_delete = []
  properties.each do |property|
    next unless (declaration = declarations[property])
    next if declaration.important

    values << declaration.value
    properties_to_delete << property
  end

  return if values.length <= 1

  properties_to_delete.each do |property|
    declarations.delete(property)
  end

  declarations[shorthand_property] = values.join(' ')
end

#declarations_to_s(options = {}) ⇒ Object

Return all declarations as a string.



281
282
283
# File 'lib/css_parser/rule_set.rb', line 281

def declarations_to_s(options = {})
  declarations.to_s(options)
end

#deleteObject Also known as: remove_declaration!



236
# File 'lib/css_parser/rule_set.rb', line 236

def_delegators :declarations, :add_declaration!, :delete

#each_declarationObject

Iterate through declarations.



274
275
276
277
278
# File 'lib/css_parser/rule_set.rb', line 274

def each_declaration # :yields: property, value, is_important
  declarations.each do |property_name, value|
    yield property_name, value.value, value.important
  end
end

#each_selector(options = {}) ⇒ Object

Iterate through selectors.

Options

  • force_important – boolean

Example

ruleset.each_selector do |sel, dec, spec|
  ...
end


264
265
266
267
268
269
270
271
# File 'lib/css_parser/rule_set.rb', line 264

def each_selector(options = {}) # :yields: selector, declarations, specificity
  decs = declarations.to_s(options)
  if @specificity
    @selectors.each { |sel| yield sel.strip, decs, @specificity }
  else
    @selectors.each { |sel| yield sel.strip, decs, CssParser.calculate_specificity(sel) }
  end
end

#expand_background_shorthand!Object

Convert shorthand background declarations (e.g. background: url("chess.png") gray 50% repeat fixed;) into their constituent parts.

See www.w3.org/TR/CSS21/colors.html#propdef-background



304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/css_parser/rule_set.rb', line 304

def expand_background_shorthand! # :nodoc:
  return unless (declaration = declarations['background'])

  value = declaration.value.dup

  replacement =
    if value.match(CssParser::RE_INHERIT)
      BACKGROUND_PROPERTIES.map { |key| [key, 'inherit'] }.to_h
    else
      {
        'background-image' => value.slice!(CssParser::RE_IMAGE),
        'background-attachment' => value.slice!(CssParser::RE_SCROLL_FIXED),
        'background-repeat' => value.slice!(CssParser::RE_REPEAT),
        'background-color' => value.slice!(CssParser::RE_COLOUR),
        'background-size' => extract_background_size_from(value),
        'background-position' => value.slice!(CssParser::RE_BACKGROUND_POSITION)
      }
    end

  declarations.replace_declaration!('background', replacement, preserve_importance: true)
end

#expand_border_shorthand!Object

Split shorthand border declarations (e.g. border: 1px red;) Additional splitting happens in expand_dimensions_shorthand!



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# File 'lib/css_parser/rule_set.rb', line 334

def expand_border_shorthand! # :nodoc:
  BORDER_PROPERTIES.each do |k|
    next unless (declaration = declarations[k])

    value = declaration.value.dup

    replacement = {
      "#{k}-width" => value.slice!(CssParser::RE_BORDER_UNITS),
      "#{k}-color" => value.slice!(CssParser::RE_COLOUR),
      "#{k}-style" => value.slice!(CssParser::RE_BORDER_STYLE)
    }

    declarations.replace_declaration!(k, replacement, preserve_importance: true)
  end
end

#expand_dimensions_shorthand!Object

Split shorthand dimensional declarations (e.g. margin: 0px auto;) into their constituent parts. Handles margin, padding, border-color, border-style and border-width.



352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
# File 'lib/css_parser/rule_set.rb', line 352

def expand_dimensions_shorthand! # :nodoc:
  DIMENSIONS.each do |property, (top, right, bottom, left)|
    next unless (declaration = declarations[property])

    value = declaration.value.dup

    # RGB and HSL values in borders are the only units that can have spaces (within params).
    # We cheat a bit here by stripping spaces after commas in RGB and HSL values so that we
    # can split easily on spaces.
    #
    # TODO: rgba, hsl, hsla
    value.gsub!(RE_COLOUR) { |c| c.gsub(/(\s*,\s*)/, ',') }

    matches = split_value_preserving_function_whitespace(value)

    case matches.length
    when 1
      values = matches.to_a * 4
    when 2
      values = matches.to_a * 2
    when 3
      values = matches.to_a
      values << matches[1] # left = right
    when 4
      values = matches.to_a
    else
      raise ArgumentError, "Cannot parse #{value}"
    end

    replacement = [top, right, bottom, left].zip(values).to_h

    declarations.replace_declaration!(property, replacement, preserve_importance: true)
  end
end

#expand_font_shorthand!Object

Convert shorthand font declarations (e.g. font: 300 italic 11px/14px verdana, helvetica, sans-serif;) into their constituent parts.



389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
# File 'lib/css_parser/rule_set.rb', line 389

def expand_font_shorthand! # :nodoc:
  return unless (declaration = declarations['font'])

  # reset properties to 'normal' per http://www.w3.org/TR/CSS21/fonts.html#font-shorthand
  font_props = {
    'font-style' => 'normal',
    'font-variant' => 'normal',
    'font-weight' => 'normal',
    'font-size' => 'normal',
    'line-height' => 'normal'
  }

  value = declaration.value.dup
  value.gsub!(%r{/\s+}, '/') # handle spaces between font size and height shorthand (e.g. 14px/ 16px)

  in_fonts = false

  matches = value.scan(/"(?:.*[^"])"|'(?:.*[^'])'|(?:\w[^ ,]+)/)
  matches.each do |m|
    m.strip!
    m.gsub!(/;$/, '')

    if in_fonts
      if font_props.key?('font-family')
        font_props['font-family'] += ", #{m}"
      else
        font_props['font-family'] = m
      end
    elsif m =~ /normal|inherit/i
      ['font-style', 'font-weight', 'font-variant'].each do |font_prop|
        font_props[font_prop] ||= m
      end
    elsif m =~ /italic|oblique/i
      font_props['font-style'] = m
    elsif m =~ /small-caps/i
      font_props['font-variant'] = m
    elsif m =~ /[1-9]00$|bold|bolder|lighter/i
      font_props['font-weight'] = m
    elsif m =~ CssParser::FONT_UNITS_RX
      if m.include?('/')
        font_props['font-size'], font_props['line-height'] = m.split('/', 2)
      else
        font_props['font-size'] = m
      end
      in_fonts = true
    end
  end

  declarations.replace_declaration!('font', font_props, preserve_importance: true)
end

#expand_list_style_shorthand!Object

Convert shorthand list-style declarations (e.g. list-style: lower-alpha outside;) into their constituent parts.

See www.w3.org/TR/CSS21/generate.html#lists



444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
# File 'lib/css_parser/rule_set.rb', line 444

def expand_list_style_shorthand! # :nodoc:
  return unless (declaration = declarations['list-style'])

  value = declaration.value.dup

  replacement =
    if value =~ CssParser::RE_INHERIT
      LIST_STYLE_PROPERTIES.map { |key| [key, 'inherit'] }.to_h
    else
      {
        'list-style-type' => value.slice!(CssParser::RE_LIST_STYLE_TYPE),
        'list-style-position' => value.slice!(CssParser::RE_INSIDE_OUTSIDE),
        'list-style-image' => value.slice!(CssParser::URI_RX_OR_NONE)
      }
    end

  declarations.replace_declaration!('list-style', replacement, preserve_importance: true)
end

#expand_shorthand!Object

Split shorthand declarations (e.g. margin or font) into their constituent parts.



291
292
293
294
295
296
297
298
# File 'lib/css_parser/rule_set.rb', line 291

def expand_shorthand!
  # border must be expanded before dimensions
  expand_border_shorthand!
  expand_dimensions_shorthand!
  expand_font_shorthand!
  expand_background_shorthand!
  expand_list_style_shorthand!
end

#extract_background_size_from(value) ⇒ Object



326
327
328
329
330
# File 'lib/css_parser/rule_set.rb', line 326

def extract_background_size_from(value)
  size = value.slice!(CssParser::RE_BACKGROUND_SIZE)

  size.sub(%r{^\s*/\s*}, '') if size
end

#get_value(property) ⇒ Object Also known as: []

Get the value of a property



248
249
250
251
252
# File 'lib/css_parser/rule_set.rb', line 248

def get_value(property)
  return '' unless (value = declarations[property])

  "#{value};"
end

#to_sObject

Return the CSS rule set as a string.



286
287
288
# File 'lib/css_parser/rule_set.rb', line 286

def to_s
  "#{@selectors.join(',')} { #{declarations} }"
end