Skip to content

Instantly share code, notes, and snippets.

@jbgutierrez
Last active August 29, 2015 14:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jbgutierrez/62f71e099bde5a6ab263 to your computer and use it in GitHub Desktop.
Save jbgutierrez/62f71e099bde5a6ab263 to your computer and use it in GitHub Desktop.
Tiny little scripts to lint scss and ejs files
# coding: UTF-8
require 'nokogiri'
require 'tempfile'
require 'json'
VALID_NODE_NAMES = %w[title a abbr acronym address applet area article aside audio b base basefont bdi bdo bgsound big blink blockquote body br button canvas caption center cite code col colgroup command data datalist dd del details dfn dir div dl dt em embed fieldset figcaption figure font footer form frame frameset h1 h2 h3 h4 h5 h6 head header hgroup hr html i iframe img input ins isindex kbd keygen label legend li link listing main map mark marquee menu meta meter nav nobr noframes noscript object ol optgroup option output p param plaintext pre progress q rp rt ruby s samp script section select small source spacer span strike strong style sub summary sup table tbody td textarea tfoot th thead time title tr track tt u ul var video wbr xmp]
class TidyHTML
class << self
def scan glob
Dir[glob].each do |file|
next if file =~ /vendor|common-js-templates|worldwide|product|bundle|product|examples|category|popup|share|application|_mixin|_buttons|patterns/
puts "Checking ... #{file}"
@filename = file
@contents = File.read file
fix_doble_quotes_on_attributes
add_default_attribute 'href', '#'
remove_empty_attributes
set_id_after_tag
set_class_after_tag_or_id
remove_two_spaces
File.open(file, 'w') { |f| f.puts @contents }
@contents.gsub!(/<%[-=][^%^>]+?%>/) { |token| "#{token.gsub(' ', '_')[2, -2]}" }
@contents.gsub!(/<%[^%^>]+?%>/) { |token| "#{token.gsub(/[ -]/, '*')}" }
@contents.gsub!("<%", "<!--")
@contents.gsub!("%>", "-->")
check_style
validate
end
end
def validate
offset, html_contents = 0, @contents
if @contents !~ /DOCTYPE/
html_contents = <<-eos
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
#{@contents}
</body>
</html>
eos
offset = -6
end
response = nu html_contents
json = JSON.parse response
json['messages'].each do |item|
if item['type'] != 'info' and !item['lastLine'].nil?
line = item['lastLine'] + offset
puts "#{@filename}:#{line}:(#{item['type']}) #{item['message']}"
end
end
end
def nu document
# Use the array form to enforce an extension in the filename
# validator.nu requires html extension
file = Tempfile.new ['check-style', '.html']
file.write document
file.close
result = `curl -s -F parser=html5 -F out=json -F content=@#{file.path} http://html5.validator.nu`
file.unlink
result
end
def fix_doble_quotes_on_attributes
@contents.gsub!(%r{='(.+?)'}, '="\1"')
end
def add_default_attribute attr, default
@contents.gsub!(/ #{attr}=""/, " #{attr}=\"#{default}\"")
end
def remove_attribute attr
@contents.gsub!(/(?<!meta) #{attr}="(.*?)"/, '')
end
def remove_empty_attributes
@contents.gsub!(/\S+=(' *'|" *")/, '')
end
def set_id_after_tag
@contents.gsub!(/(<\S+) (.*) (id=".+?")/, '\1 \2 \3')
end
def set_class_after_tag_or_id
@contents.gsub!(/(<\S+)( id=".+?")(.*) (class=".+?")/, '\1\2 \3 \4')
end
def remove_two_spaces
@contents.gsub!(/ +(.*?=".*?")/, ' \1')
@contents.gsub!(/ >/, '>')
end
def check_style
root = Nokogiri::HTML @contents do |config|
config.noblanks
end
Visitor::HTML.visit root, @filename
end
end
end
class Visitor
class Base
attr_accessor :filename
def self.visit(root, filename)
new(root, filename).send(:visit, root)
end
protected
def initialize root, filename
@root = root
@filename = filename
end
def visit(node)
method = "visit_#{node_name node}"
if self.respond_to?(method, true)
self.send(method, node) {visit_children(node)}
else
visit_children(node)
end
end
def visit_children(parent)
parent.children.map {|c| visit(c)}
end
NODE_NAME_RE = /.*::(.*?)$/
def node_name(node)
@@node_names ||= {}
@@node_names[node.class.name] ||= node.class.name.gsub(NODE_NAME_RE, '\\1').downcase
end
end
class HTML < Base
attr_accessor :node, :selector
def visit_element(node)
@node = node
clazz = node['class']
safe_id = id && id !~ /%/
@selector = name
@selector += "##{id}" if safe_id
@selector += ".#{clazz.gsub(' ', '.')}" if clazz
# puts "#{filename}:#{line}:(error) Id duplicado ##{id}" if safe_id and @root.css("##{id}").length > 1
puts "#{filename}:#{line}:(error) Estilos en línea '#{style}'" if inline_styles?
puts "#{filename}:#{line}:(error) Nomenclatura incorrecta '#{selector}'" if wrong_naming?
# puts "#{filename}:#{line}:(error) El nombre del nodo no es válido '#{name}'" unless VALID_NODE_NAMES.include? name
puts "#{filename}:#{line}:(error) Nodo vacío '#{selector}'" if empty_tag?
puts "#{filename}:#{line}:(warning) Elemento de bloque anidadado '#{selector}'" if nested_block_element?
puts "#{filename}:#{line}:(warning) No existe label asociada '#{selector}'" if input_without_label?
puts "#{filename}:#{line}:(warning) No tiene la coletilla form '#{selector}'" if input_without_form_pattern?
puts "#{filename}:#{line}:(warning) Submit sin form asociado" if submit_without_form?
node.children.each { |child| visit child }
end
def submit_without_form?
node['type'] == 'submit' and !node.ancestors.any?{ |p| p.name == 'form' }
end
def empty_tag?
return false if node.name == 'span' and node.parent['class'] =~ /label-check|label-radio/
%w[span p].include?(node.name) and node.children.empty?
end
def wrong_naming?
exceptions = %w[FL FR AMEX CVV2 VISA]
selector_without_exceptions = selector
exceptions.each { |exception| selector_without_exceptions.gsub!(/\b#{exception}\b/, '')}
selector_without_exceptions !~ /^[a-z\-\d\.\#]+$/
end
def inline_styles?
if style
style_without_exceptions = style.gsub('display: none;', '')
!style_without_exceptions.empty?
end
end
def nested_block_element?
return unless parent = node.parent
node.name == 'div' and parent.name == 'div' and (parent['class'] || 'form-control') !~ /form-control$/
end
def input_without_label?
node.name == 'input' and @root.at_css("label[for=#{id}]").nil?
end
def input_without_form_pattern?
node.name == 'input' and id !~ /form/
end
def method_missing(method, *args, &block)
if @node.respond_to?(method)
@node.send(method, *args, &block)
else
@node[method]
end
end
end
end
TidyHTML.scan 'templates/**/*.html'
# coding: UTF-8
require 'sass'
require 'yaml'
class Visitor < Sass::Tree::Visitors::Base
attr_accessor :filename
def initialize(filename)
super()
@filename = filename
@stack = @props_count_stack = []
@nesting_level = 0
end
def visit(node)
rules = node.parsed_rules rescue ''
if @props_count_stack.empty?
@nesting_level = rules.to_s[/-page$/] ? -1 : 0
end
super
end
def visit_rule node
rules = node.parsed_rules
rules.to_s.squeeze.split(', ').each do |rule|
indents = rule.count(' ')
indents += 1 if rule[0] != '&'
@nesting_level += indents
unless rule == '#page-container #main'
puts "#{filename}:#{node.line}:(error) Id anidado #{rule}" if rule =~ /#/ and @nesting_level > 1
end
if @nesting_level_quiet and @nesting_level > 3
puts "#{filename}:#{node.line}:(warning) Se superó el limite de anidamiento"
@nesting_level_quiet = true
end
@props_count_stack.push 0 if indents > 0
node.children.each { |child| visit child }
@nesting_level -= indents
@nesting_level_quiet = false if @nesting_level < 3
if indents > 0
count = @props_count_stack.pop
puts "#{filename}:#{node.line}:(warning) #{count} reglas en el selector #{rule}" if count > 25
end
end
end
def visit_prop node
if node.name.to_s =~ /background/
rule = node.to_scss
puts "#{filename}:#{node.line}:(error) debería estar en el sprite '#{rule.chomp}'" if rule =~ /no-repeat/ && rule !~ /icon/
end
count = @props_count_stack.pop
return unless count
count += 1
@props_count_stack.push count
end
end
class TidySCSS
class << self
def scan glob
Dir[glob].each do |file|
@contents = File.read file
check_style file
File.open(file, 'w') { |f| f.puts @contents }
end
end
def check_style file
parser = Sass::SCSS::Parser.new(@contents, file)
root = parser.parse
visitor = Visitor.new file
visitor.visit root
end
end
end
TidySCSS.scan 'public/sass/**/*.scss'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment