css_class_duplicates 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/config/default.yml +22 -0
- data/exe/css_class_duplicates +15 -0
- data/lib/css_class_duplicates/cli.rb +32 -0
- data/lib/css_class_duplicates/config.rb +50 -0
- data/lib/css_class_duplicates/filter.rb +17 -0
- data/lib/css_class_duplicates/globber.rb +13 -0
- data/lib/css_class_duplicates/match.rb +5 -0
- data/lib/css_class_duplicates/merger.rb +34 -0
- data/lib/css_class_duplicates/parser.rb +56 -0
- data/lib/css_class_duplicates/printer.rb +16 -0
- data/lib/css_class_duplicates/scanner.rb +43 -0
- data/lib/css_class_duplicates/version.rb +5 -0
- data/lib/css_class_duplicates.rb +16 -0
- metadata +63 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ed329f3f9209fa5ff1ff79b79714d651327162fbf6c99c5f1c9619f843c60c6c
|
4
|
+
data.tar.gz: aceea4196b63d6bd9343baf34a193c5b65d64f39f7423b8f549c38296cf4d889
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2588d4c48f3ff26c61b227265fdf3365a6eb4adc272b9839b070a7f7ca51bd96c686489124cff2b32781b77d0e1d9d674d6d2f9a2d3ebcb7b68e7e22cd5a2e43
|
7
|
+
data.tar.gz: 4b24ca2142a1ca85c0f6dbb44970619efebc25b3f0f119906a1f24cb3353d15d7558625fac1f90f846a463586961c7a745f4573dc6790a40cf8b1df892cf65af
|
data/config/default.yml
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
files:
|
2
|
+
include:
|
3
|
+
- "**/*.html.erb"
|
4
|
+
# exclude:
|
5
|
+
# - "app/views/layouts/**"
|
6
|
+
|
7
|
+
matches:
|
8
|
+
type: 'exact'
|
9
|
+
# type: 'partial'
|
10
|
+
|
11
|
+
filters:
|
12
|
+
classes:
|
13
|
+
min: 2
|
14
|
+
# include:
|
15
|
+
# - '*' # Include the match only if it contains the class
|
16
|
+
# exclude:
|
17
|
+
# - 'first:*' # Exclude the match if it contains the class
|
18
|
+
# - 'btn btn-primary' # Exclude the match if it contains the classes
|
19
|
+
# delete:
|
20
|
+
# - 'hidden' # Deletes the class from match
|
21
|
+
matches:
|
22
|
+
min: 2
|
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift("#{__dir__}/../lib")
|
5
|
+
|
6
|
+
require 'benchmark'
|
7
|
+
require 'css_class_duplicates'
|
8
|
+
|
9
|
+
cli = CssClassDuplicates::CLI.new
|
10
|
+
result = 0
|
11
|
+
|
12
|
+
time = Benchmark.realtime { result = cli.run }
|
13
|
+
|
14
|
+
puts "Finished in #{time} seconds"
|
15
|
+
exit result
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CssClassDuplicates
|
4
|
+
# The CLI is a class responsible of handling all the command line interface logic.
|
5
|
+
class CLI
|
6
|
+
STATUS_SUCCESS = 0
|
7
|
+
STATUS_ERROR = 0
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@options = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
entities = []
|
15
|
+
|
16
|
+
config = Config.new
|
17
|
+
config.files.each do |file_name|
|
18
|
+
entities.concat(config.parser.call(file_name))
|
19
|
+
end
|
20
|
+
|
21
|
+
entities = config.merger.call(entities)
|
22
|
+
entities = config.filter.call(entities)
|
23
|
+
config.printer.call(entities)
|
24
|
+
|
25
|
+
STATUS_SUCCESS
|
26
|
+
rescue StandardError, SyntaxError, LoadError => e
|
27
|
+
warn e.message
|
28
|
+
warn e.backtrace
|
29
|
+
STATUS_ERROR
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'rake/file_list'
|
5
|
+
|
6
|
+
module CssClassDuplicates
|
7
|
+
# This class represents the configuration of the gem.
|
8
|
+
# A Config is associated with a YAML configuration
|
9
|
+
# file from which it was read.
|
10
|
+
class Config
|
11
|
+
PROJECT_CONFIG = '.css_class_duplicates.yml'
|
12
|
+
DEFAULT_CONFIG = File.expand_path('../../config/default.yml', __dir__)
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
filename = File.exist?(PROJECT_CONFIG) ? PROJECT_CONFIG : DEFAULT_CONFIG
|
16
|
+
@config = YAML.load_file(filename)
|
17
|
+
end
|
18
|
+
|
19
|
+
def files
|
20
|
+
Rake::FileList.new.tap do |file_list|
|
21
|
+
if (files = @config['files'])
|
22
|
+
if (include = files['include'])
|
23
|
+
file_list.include(include)
|
24
|
+
end
|
25
|
+
if (exclude = files['exclude'])
|
26
|
+
file_list.exclude(exclude)
|
27
|
+
end
|
28
|
+
else
|
29
|
+
file_list.include('**/*')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def parser
|
35
|
+
@parser ||= Parser.new(@config['filters'])
|
36
|
+
end
|
37
|
+
|
38
|
+
def merger
|
39
|
+
@merger ||= Merger.new(@config['matches'])
|
40
|
+
end
|
41
|
+
|
42
|
+
def filter
|
43
|
+
@filter ||= Filter.new(@config['filters'])
|
44
|
+
end
|
45
|
+
|
46
|
+
def printer
|
47
|
+
@printer ||= Printer.new
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CssClassDuplicates
|
4
|
+
# This class filters found results to exclude CSS classes that aren't duplicated
|
5
|
+
class Filter
|
6
|
+
def initialize(config)
|
7
|
+
@min_classes = config.dig('classes', 'min') || 2
|
8
|
+
@min_matches = config.dig('matches', 'min') || 2
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(entities)
|
12
|
+
entities.select do |entity|
|
13
|
+
entity.classes.size >= @min_classes && entity.files.size >= @min_matches
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CssClassDuplicates
|
4
|
+
# The class compares strings using wildcard symbols
|
5
|
+
class Globber
|
6
|
+
def self.for(value)
|
7
|
+
return value if value.is_a?(Regexp)
|
8
|
+
|
9
|
+
escaped = Regexp.escape(value).gsub('\*', '.*?')
|
10
|
+
Regexp.new("^#{escaped}$", Regexp::IGNORECASE)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CssClassDuplicates
|
4
|
+
# This class merges found results
|
5
|
+
class Merger
|
6
|
+
def initialize(config)
|
7
|
+
@type = config['type']
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(entities)
|
11
|
+
matches = entities.group_by(&:classes).map do |classes, items|
|
12
|
+
Match.new(classes, items.map(&:files).flatten)
|
13
|
+
end
|
14
|
+
matches = add_partial_matches(matches) if @type == 'partial'
|
15
|
+
matches
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def add_partial_matches(matches)
|
21
|
+
match_idx = matches.to_h { |match| [match.classes, match] }
|
22
|
+
|
23
|
+
matches.combination(2) do |match1, match2|
|
24
|
+
partial = match1.classes.intersection(match2.classes)
|
25
|
+
next if partial.empty?
|
26
|
+
|
27
|
+
existing = (match_idx[partial] ||= Match.new(partial, []))
|
28
|
+
existing.files = (existing.files + match1.files + match2.files).uniq
|
29
|
+
end
|
30
|
+
|
31
|
+
match_idx.values
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CssClassDuplicates
|
4
|
+
# This class searches for CSS classes
|
5
|
+
class Parser
|
6
|
+
MATCHER = /
|
7
|
+
\s+class\s*=\s*["']?([\w\s\-_:]+)["']?| # class = "hello world"
|
8
|
+
class:\s*["']?([\w\s\-_:]+)["']?| # class: 'hello world'
|
9
|
+
["']?class["']?\s=>\s*["']?([\w\s\-_:]+)["']? # 'class' => "hello world"
|
10
|
+
/x.freeze
|
11
|
+
WHITESPACE = /\s/.freeze
|
12
|
+
|
13
|
+
def initialize(config)
|
14
|
+
classes = config.dig('classes')
|
15
|
+
@include = to_regex_array(classes&.dig('include'))
|
16
|
+
@exclude = to_regex_array(classes&.dig('exclude'))
|
17
|
+
@delete = to_regex_array(classes&.dig('delete'))
|
18
|
+
end
|
19
|
+
|
20
|
+
def call(file_name)
|
21
|
+
content = File.read(file_name)
|
22
|
+
|
23
|
+
matches = Scanner.new(content).matches(MATCHER)
|
24
|
+
matches.map do |match, line|
|
25
|
+
classes = match.split(WHITESPACE).reject(&:empty?)
|
26
|
+
next if classes.empty?
|
27
|
+
|
28
|
+
classes = filter(classes.map(&:downcase).sort)
|
29
|
+
next if classes.empty?
|
30
|
+
|
31
|
+
Match.new(classes, ["#{file_name}:#{line}"])
|
32
|
+
end.compact
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def filter(classes)
|
38
|
+
return [] if @include.any? && !any?(@include, classes)
|
39
|
+
return [] if @exclude.any? && any?(@exclude, classes)
|
40
|
+
|
41
|
+
classes.reject { |value| include?(@delete, value) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def any?(list, values)
|
45
|
+
values.any? { |value| include?(list, value) } || include?(list, values.join(' '))
|
46
|
+
end
|
47
|
+
|
48
|
+
def include?(list, value)
|
49
|
+
list.any? { |regex| value =~ regex }
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_regex_array(array)
|
53
|
+
array&.map { |pattern| Globber.for(pattern) } || []
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CssClassDuplicates
|
4
|
+
# This class renders the result in console.
|
5
|
+
class Printer
|
6
|
+
def call(entities)
|
7
|
+
entities.sort_by { |entity| [-entity.files.size, -entity.classes.size] }.each do |entity|
|
8
|
+
puts "#{entity.files.size} matches for \"#{entity.classes.join(' ')}\""
|
9
|
+
entity.files.uniq.sort.each do |file_name|
|
10
|
+
puts " #{file_name}"
|
11
|
+
end
|
12
|
+
puts
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'English'
|
4
|
+
|
5
|
+
module CssClassDuplicates
|
6
|
+
# This class scans string and returns all found matches with positions
|
7
|
+
class Scanner
|
8
|
+
NEW_LINE = /(\r\n|\r|\n)/.freeze
|
9
|
+
|
10
|
+
def initialize(content)
|
11
|
+
@content = content
|
12
|
+
end
|
13
|
+
|
14
|
+
def matches(regex)
|
15
|
+
matches_with_positions(regex).map do |match, start_position|
|
16
|
+
[match, file_line(start_position)]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def matches_with_positions(regex)
|
23
|
+
matches = []
|
24
|
+
@content.scan(regex) do |captures|
|
25
|
+
captures.each_with_index do |capture, index|
|
26
|
+
next unless capture
|
27
|
+
|
28
|
+
start_position, end_position = $LAST_MATCH_INFO.offset(index + 1)
|
29
|
+
matches << [capture, start_position, end_position]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
matches
|
33
|
+
end
|
34
|
+
|
35
|
+
def file_line(position)
|
36
|
+
file_lines.index { |line_length| (position -= line_length) <= 0 } + 1
|
37
|
+
end
|
38
|
+
|
39
|
+
def file_lines
|
40
|
+
@file_lines ||= @content.split(NEW_LINE).map(&:length).each_slice(2).map(&:sum)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'css_class_duplicates/cli'
|
4
|
+
require_relative 'css_class_duplicates/config'
|
5
|
+
require_relative 'css_class_duplicates/filter'
|
6
|
+
require_relative 'css_class_duplicates/globber'
|
7
|
+
require_relative 'css_class_duplicates/match'
|
8
|
+
require_relative 'css_class_duplicates/merger'
|
9
|
+
require_relative 'css_class_duplicates/parser'
|
10
|
+
require_relative 'css_class_duplicates/printer'
|
11
|
+
require_relative 'css_class_duplicates/scanner'
|
12
|
+
require_relative 'css_class_duplicates/version'
|
13
|
+
|
14
|
+
module CssClassDuplicates
|
15
|
+
class Error < StandardError; end
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: css_class_duplicates
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andrew Kushnir
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-07-05 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: |2
|
14
|
+
The gem searches for CSS class set duplicates, so they can be replaced with something meaningful,
|
15
|
+
like "border border-gray-400 bg-gray-100 mt-1 ..." can be replaced with "widget".
|
16
|
+
Supports Tailwind CSS and its alternatives
|
17
|
+
email:
|
18
|
+
- akushnir2009@gmail.com
|
19
|
+
executables:
|
20
|
+
- css_class_duplicates
|
21
|
+
extensions: []
|
22
|
+
extra_rdoc_files: []
|
23
|
+
files:
|
24
|
+
- config/default.yml
|
25
|
+
- exe/css_class_duplicates
|
26
|
+
- lib/css_class_duplicates.rb
|
27
|
+
- lib/css_class_duplicates/cli.rb
|
28
|
+
- lib/css_class_duplicates/config.rb
|
29
|
+
- lib/css_class_duplicates/filter.rb
|
30
|
+
- lib/css_class_duplicates/globber.rb
|
31
|
+
- lib/css_class_duplicates/match.rb
|
32
|
+
- lib/css_class_duplicates/merger.rb
|
33
|
+
- lib/css_class_duplicates/parser.rb
|
34
|
+
- lib/css_class_duplicates/printer.rb
|
35
|
+
- lib/css_class_duplicates/scanner.rb
|
36
|
+
- lib/css_class_duplicates/version.rb
|
37
|
+
homepage: https://github.com/a-kushnir/css_class_duplicates
|
38
|
+
licenses:
|
39
|
+
- MIT
|
40
|
+
metadata:
|
41
|
+
homepage_uri: https://github.com/a-kushnir/css_class_duplicates
|
42
|
+
source_code_uri: https://github.com/a-kushnir/css_class_duplicates
|
43
|
+
bug_tracker_uri: https://github.com/a-kushnir/css_class_duplicates/issues
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 2.5.0
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '0'
|
58
|
+
requirements: []
|
59
|
+
rubygems_version: 3.3.10
|
60
|
+
signing_key:
|
61
|
+
specification_version: 4
|
62
|
+
summary: CSS class duplication search tool.
|
63
|
+
test_files: []
|