Last active
August 29, 2015 14:20
-
-
Save jbgutierrez/62f71e099bde5a6ab263 to your computer and use it in GitHub Desktop.
Tiny little scripts to lint scss and ejs files
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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