css_class_duplicates 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|