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 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
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CssClassDuplicates
4
+ Match = Struct.new(:classes, :files)
5
+ 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CssClassDuplicates
4
+ VERSION = '0.1.0'
5
+ 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: []