class SCSSLint::Linter::GroupedPropertyOrder

Public Instance Methods

check_order( node ) { || ... } click to toggle source

Logic that actually performs order

# File lib/grouped_property_scss_linter/grouped_property_order.rb, line 28
def check_order( node )

  # 1. get a list of properties we can sort
  sortable_properties = node.children.select{ |child| child.is_a?( Sass::Tree::PropNode )}

  # 2. group things
  grouped_properties = {}
  props = sortable_properties.map do |prop|

    # simplify the name a little
    name = prop.name.join

    # attempt to match the name
    group = find_match_for_property( name )

    # if it didn’t find anything, move on
    next if group.nil?

    # if there’s no existing group
    grouped_properties[group[:name]] = { first: 1.0/0, last: 0, props: [] } unless grouped_properties.key?( group[:name] )

    # build a concat
    concat = { name: name, node: prop, group: group[:name], line: prop.line, group_idx: group[:idx] }

    # drop things on
    grouped_properties[group[:name]][:first] = [grouped_properties[group[:name]][:first], prop.line].min
    grouped_properties[group[:name]][:last]  = [grouped_properties[group[:name]][:last],  prop.line].max
    grouped_properties[group[:name]][:props] << concat

    concat

  end
  props.compact!

  # 3. call down
  check_sort_order( props, grouped_properties ) unless grouped_properties.empty?

  # 4. yield so we can process children
  yield

end
visit_if(node, &block) click to toggle source
# File lib/grouped_property_scss_linter/grouped_property_order.rb, line 76
def visit_if(node, &block)

  check_order(node, &block)
  visit(node.else) if node.else

end
visit_media( node )

alias things out

Alias for: check_order
visit_mixin( node )
Alias for: check_order
visit_prop( node )
Alias for: check_order
visit_root( node ) { || ... } click to toggle source

Called when the linter is fired up on a document. Acts as a pseudo-constructor

# File lib/grouped_property_scss_linter/grouped_property_order.rb, line 6
def visit_root( node )

  # get configured order
  @configured_groups = get_order_from_conf

  # and map things around
  @groups = []
  @property_to_group = {}
  count = 0
  @configured_groups.each_pair do |name, group|

    @groups <<  name
    group['properties'].each{ |property| @property_to_group[property] = { name: name, idx: count }}
    count += 1

  end

  yield

end
visit_rule( node )
Alias for: check_order

Private Instance Methods

check_sort_order( props, grouped ) click to toggle source
# File lib/grouped_property_scss_linter/grouped_property_order.rb, line 190
def check_sort_order( props, grouped )

  # get stats on grouped version
  grouped.each_value do |group|

    # number of properties
    group[:num]   = group[:props].length
    group[:delta] = group[:last] - group[:first]

  end

  # quick duck-type
  quick_check_order( props, grouped )

  # if we’re checking whitespace, do so
  check_whitespace( grouped ) unless grouped.length < 2

end
check_whitespace( grouped ) click to toggle source
# File lib/grouped_property_scss_linter/grouped_property_order.rb, line 250
def check_whitespace( grouped )

  # get a quick handle on all the groups we’ve found
  detected_groups = grouped.keys

  # iterate through
  curr_idx = 0
  grouped.each_pair do |name, current|

    # get a configuration
    group_conf = @configured_groups[name]

    # if we don’t care about space, bail
    ( curr_idx += 1 and next ) unless group_conf['space_around']

    # similarly, if this group is too small to trigger spacing, bounce
    ( curr_idx += 1 and next ) unless current[:props].length > group_conf['max_no_space']

    # set some easy references
    next_group = detected_groups.length > curr_idx ? grouped[detected_groups[curr_idx + 1]] : nil
    prev_group = curr_idx > 0 ? grouped[detected_groups[curr_idx - 1]] : nil

    # if there’s something after us, and there’s no space
    if !next_group.nil? and ((next_group[:first] - current[:last]) < 2)

      # raise a lint error
      add_lint( current[:props].last[:node], "Must be at least one empty line after ‘#{current[:props].last[:name]}’" )

      # also, flag the next group so we don’t catch it next time ‘round
      next_group[:raised] = true

    end

    # if there’s something before us, and there’s no space…
    if !prev_group.nil? and ((current[:first] - prev_group[:last]) < 2) and current[:raised].nil?

      # raise a lint error
      add_lint( current[:props].first[:node], "Must be at least one empty line before ‘#{current[:props].first[:name]}’" )

    end

    # finally, increment
    curr_idx += 1

  end
end
find_match_for_property( prop ) click to toggle source

Finds a matching group for a specified property

# File lib/grouped_property_scss_linter/grouped_property_order.rb, line 156
def find_match_for_property( prop )

  # if it’s a property
  prop = '{VARIABLE}' if ( prop.start_with?( '--' ))

  # sanitise the name by removing any browser prefixes
  prop = prop.gsub( /^(-\w+(-osx)?-)?/, '' )

  # iteratively remove hyphens from the property…
  while prop =~ /\-/

    # if we know about this property or its splatted variety…
    if @property_to_group.key? prop or @property_to_group.key? prop+'*'

      return ( @property_to_group[prop] || @property_to_group[prop+'*'] )

    end

    # strip the leading hyphen and try again
    prop.gsub!( /\-(\w+)$/, '' )

  end

  # finally…
  if @property_to_group.key? prop or @property_to_group.key? prop+'*'

    return ( @property_to_group[prop] || @property_to_group[prop+'*'] )

  end

  nil

end
get_order_from_conf() click to toggle source

Acquires configuration and populates out a group array

# File lib/grouped_property_scss_linter/grouped_property_order.rb, line 86
def get_order_from_conf

  # 1. acquire defaults + default them, just in case
  defaults = config['defaults']
  defaults['space_around']     |= true
  defaults['max_no_space']     |= 3

  # 2, acquire groups
  groups = config['groups'] || load_groups_from_style

  # 3. if it failed, bail
  raise 'No groups configured' if groups.nil?

  # 4. if we’re worrying about CSS variables
  if config['css_variables_first']

    groups = {
      'css_vars' => {
        'properties' => [ '{VARIABLE}' ],
        'space_around' => true,
        'max_no_space' => 0
      }
    }.merge( groups )

  end

  # 5. munge
  groups.update( groups ) do |name, group|

    # a. if it’s an array, cast it
    group = { 'properties' => group } if group.is_a?( Array )

    # b. merge in some defaults
    group['space_around'] = defaults['space_around'] if group['space_around'].nil?
    group['max_no_space'] ||= defaults['max_no_space']

    # c. return
    group

  end

  groups

end
hint_text_for( prop, grouped_props, context ) click to toggle source
# File lib/grouped_property_scss_linter/grouped_property_order.rb, line 297
def hint_text_for( prop, grouped_props, context )

  # get target group
  dst_group = prop[:group]

  # if we know about the group…
  if grouped_props.key?( dst_group )

    # get the first property of the current group
    dst_prop = grouped_props[dst_group][:props].first

    # if it’s a different property, return
    return "after ‘#{dst_prop[:name].bold}’" if dst_prop != prop

  end

  # either our offending property is the sole member of a group, or it’s very lost… so look for a previous marker
  curr_idx = prop[:group_idx]
  while curr_idx > 0

    # decrement and reset
    curr_idx  = curr_idx - 1
    dst_group = @groups[curr_idx]

    # look
    if grouped_props.key? dst_group

      # get the _last_ property of the group
      dst_prop = grouped_props[dst_group][:props].last

      # and return
      return "after ‘#{dst_prop[:name].bold}’"

    end
  end

  # otherwise, it probably belongs right at the start
  "before ‘#{context.first[:name].bold}’"

end
load_groups_from_style() click to toggle source

Loads a group from a configured style

# File lib/grouped_property_scss_linter/grouped_property_order.rb, line 132
def load_groups_from_style

  # 0. if the style is blank/empty…
  ( raise 'No style specified' and return ) if config['style'].empty?

  # 1. attempt to find the file
  data_filename = File.join( GroupedPropertyScssLinter::STYLES_DIR, "#{config['style']}.yaml" )

  # 2. does it exist
  ( raise "No style ‘#{config['style']}’ found" and return ) unless File.exists?( data_filename )

  # 3. can we read it
  ( raise "Cannot read style ‘#{config['style']}’" and return ) unless File.readable?( data_filename )

  # 4. open
  style_config = YAML.load_file( data_filename )

  # 5. barf?
  ( raise "Bad style file found for ‘#{config['style']}’" and return ) if ( style_config.nil? or style_config['groups'].nil? )

  style_config['groups']
end
quick_check_order( props, grouped ) click to toggle source
# File lib/grouped_property_scss_linter/grouped_property_order.rb, line 209
def quick_check_order( props, grouped )

  current_group = 0
  good = true

  props.each do |prop|

    # if it’s the current group, move on
    next if prop[:group] == @groups[current_group]

    # find an index
    idx = @groups.index( prop[:group] )

    # if it’s less-than, error out
    if idx < current_group

      if prop[:name].start_with?( '--' )

        add_lint( prop[:node], "CSS variables should be defined at the beginning of the rule (found #{prop[:name].bold})")

      else

        ext = config['extended_hinting'] ? " (assigned group ‘#{prop[:group].bold}’, found group ‘#{@groups[current_group].bold}’)" : ""

        add_lint( prop[:node], "property ‘#{prop[:name].bold}’ should be #{hint_text_for(prop, grouped, props)}#{ext}" )

      end
      good = false

    else

      current_group = idx

    end

  end

  good

end