primer_view_components 0.0.44 → 0.0.45

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,7 +7,7 @@ module ERBLint
7
7
  module ArgumentMappers
8
8
  # Maps element attributes to system arguments.
9
9
  class SystemArguments
10
- STRING_PARAETERS = %w[aria- data-].freeze
10
+ STRING_PARAMETERS = %w[aria- data-].freeze
11
11
  TEST_SELECTOR_REGEX = /test_selector\((?<selector>.+)\)$/.freeze
12
12
 
13
13
  attr_reader :attribute
@@ -29,7 +29,9 @@ module ERBLint
29
29
  { test_selector: m[:selector].tr("'", '"') }
30
30
  elsif attr_name == "data-test-selector"
31
31
  { test_selector: attribute.value.to_json }
32
- elsif attr_name.start_with?(*STRING_PARAETERS)
32
+ elsif attr_name.start_with?(*STRING_PARAMETERS)
33
+ raise ConversionError, "Cannot convert attribute \"#{attr_name}\" because its value contains an erb block" if attribute.value_node&.children&.any? { |n| n.try(:type) == :erb }
34
+
33
35
  # if attribute has no value_node, it means it is a boolean attribute.
34
36
  { "\"#{attr_name}\"" => attribute.value_node ? attribute.value.to_json : true }
35
37
  else
@@ -21,14 +21,18 @@ module ERBLint
21
21
  nil
22
22
  end
23
23
 
24
- def message(tag)
25
- args = map_arguments(tag)
24
+ def correction(args)
25
+ return nil if args.nil?
26
26
 
27
+ correction = "<%= render Primer::ButtonComponent.new"
28
+ correction += "(#{args})" if args.present?
29
+ "#{correction} do %>"
30
+ end
31
+
32
+ def message(args)
27
33
  return MESSAGE if args.nil?
28
34
 
29
- msg = "#{MESSAGE}\n\nTry using:\n\n<%= render Primer::ButtonComponent.new"
30
- msg += "(#{args})" if args.present?
31
- "#{msg} %>\n\nInstead of:\n"
35
+ "#{MESSAGE}\n\nTry using:\n\n#{correction(args)}\n\nInstead of:\n"
32
36
  end
33
37
  end
34
38
  end
@@ -7,19 +7,50 @@ module ERBLint
7
7
  module Linters
8
8
  # Helper methods for linting ERB.
9
9
  module Helpers
10
+ # from https://github.com/Shopify/erb-lint/blob/6179ee2d9d681a6ec4dd02351a1e30eefa748d3d/lib/erb_lint/linters/self_closing_tag.rb
11
+ SELF_CLOSING_TAGS = %w[
12
+ area base br col command embed hr input keygen
13
+ link menuitem meta param source track wbr img
14
+ ].freeze
15
+
10
16
  def self.included(base)
11
17
  base.include(ERBLint::LinterRegistry)
12
18
 
13
19
  define_method "run" do |processed_source|
14
- tags(processed_source).each do |tag|
20
+ @offenses_not_corrected = 0
21
+ tags = tags(processed_source)
22
+ tag_tree = build_tag_tree(tags)
23
+
24
+ tags.each do |tag|
15
25
  next if tag.closing?
16
26
  next unless self.class::TAGS&.include?(tag.name)
17
27
 
18
- classes = tag.attributes["class"]&.value&.split(" ")
28
+ classes = tag.attributes["class"]&.value&.split(" ") || []
29
+
30
+ tag_tree[tag][:offense] = false
19
31
 
20
- next if self.class::CLASSES.any? && (classes & self.class::CLASSES).blank?
32
+ next unless self.class::CLASSES.blank? || (classes & self.class::CLASSES).any?
21
33
 
22
- generate_offense(self.class, processed_source, tag, message(tag))
34
+ args = map_arguments(tag)
35
+ correction = correction(args)
36
+
37
+ tag_tree[tag][:offense] = true
38
+ tag_tree[tag][:correctable] = !correction.nil?
39
+ tag_tree[tag][:message] = message(args)
40
+ tag_tree[tag][:correction] = correction
41
+ end
42
+
43
+ tag_tree.each do |tag, h|
44
+ next unless h[:offense]
45
+
46
+ # We always fix the offenses using blocks. The closing tag corresponds to `<% end %>`.
47
+ if h[:correctable]
48
+ add_offense(tag.loc, h[:message], h[:correction])
49
+ add_offense(h[:closing].loc, h[:message], "<% end %>")
50
+ else
51
+ @offenses_not_corrected += 1
52
+ generate_offense(self.class, processed_source, tag, h[:message])
53
+ end
23
54
  end
24
55
 
25
56
  counter_correct?(processed_source)
@@ -29,12 +60,10 @@ module ERBLint
29
60
  return unless offense.context
30
61
 
31
62
  lambda do |corrector|
32
- if processed_source.file_content.include?("erblint:counter #{self.class.name.demodulize}")
33
- # update the counter if exists
34
- corrector.replace(offense.source_range, offense.context)
63
+ if offense.context.include?(counter_disable)
64
+ correct_counter(corrector, processed_source, offense)
35
65
  else
36
- # add comment with counter if none
37
- corrector.insert_before(processed_source.source_buffer.source_range, "#{offense.context}\n")
66
+ corrector.replace(offense.source_range, offense.context)
38
67
  end
39
68
  end
40
69
  end
@@ -42,10 +71,77 @@ module ERBLint
42
71
 
43
72
  private
44
73
 
74
+ # Override this function to convert the HTML element attributes to argument for a component.
75
+ #
76
+ # @return [Hash] if possible to map all attributes to arguments.
77
+ # @return [Nil] if cannot map to arguments.
78
+ def map_arguments(_tag)
79
+ nil
80
+ end
81
+
82
+ # Override this function to define how to autocorrect an element to a component.
83
+ #
84
+ # @return [String] with the text to replace the HTML element if possible to correct.
85
+ # @return [Nil] if cannot correct element.
86
+ def correction(_tag)
87
+ nil
88
+ end
89
+
90
+ # Override this function to customize the linter message.
91
+ #
92
+ # @return [String] message to show on linter error.
45
93
  def message(_tag)
46
94
  self.class::MESSAGE
47
95
  end
48
96
 
97
+ def counter_disable
98
+ "erblint:counter #{self.class.name.demodulize}"
99
+ end
100
+
101
+ def correct_counter(corrector, processed_source, offense)
102
+ if processed_source.file_content.include?(counter_disable)
103
+ # update the counter if exists
104
+ corrector.replace(offense.source_range, offense.context)
105
+ else
106
+ # add comment with counter if none
107
+ corrector.insert_before(processed_source.source_buffer.source_range, "#{offense.context}\n")
108
+ end
109
+ end
110
+
111
+ # This assumes that the AST provided represents valid HTML, where each tag has a corresponding closing tag.
112
+ # From the tags, we build a structured tree which represents the tag hierarchy.
113
+ # With this, we are able to know where the tags start and end.
114
+ def build_tag_tree(tags)
115
+ tag_tree = {}
116
+ current_opened_tag = nil
117
+
118
+ tags.each do |tag|
119
+ if tag.closing?
120
+ if current_opened_tag && tag.name == current_opened_tag.name
121
+ tag_tree[current_opened_tag][:closing] = tag
122
+ current_opened_tag = tag_tree[current_opened_tag][:parent]
123
+ end
124
+
125
+ next
126
+ end
127
+
128
+ self_closing = self_closing?(tag)
129
+
130
+ tag_tree[tag] = {
131
+ closing: self_closing ? tag : nil,
132
+ parent: current_opened_tag
133
+ }
134
+
135
+ current_opened_tag = tag unless self_closing
136
+ end
137
+
138
+ tag_tree
139
+ end
140
+
141
+ def self_closing?(tag)
142
+ tag.self_closing? || SELF_CLOSING_TAGS.include?(tag.name)
143
+ end
144
+
49
145
  def tags(processed_source)
50
146
  processed_source.parser.nodes_with_type(:tag).map { |tag_node| BetterHtml::Tree::Tag.from_node(tag_node) }
51
147
  end
@@ -54,7 +150,6 @@ module ERBLint
54
150
  comment_node = nil
55
151
  expected_count = 0
56
152
  rule_name = self.class.name.match(/:?:?(\w+)\Z/)[1]
57
- offenses_count = @offenses.length
58
153
 
59
154
  processed_source.parser.ast.descendants(:erb).each do |node|
60
155
  indicator_node, _, code_node, = *node
@@ -62,29 +157,32 @@ module ERBLint
62
157
  comment = code_node&.loc&.source&.strip
63
158
 
64
159
  if indicator == "#" && comment.start_with?("erblint:count") && comment.match(rule_name)
65
- comment_node = code_node
160
+ comment_node = node
66
161
  expected_count = comment.match(/\s(\d+)\s?$/)[1].to_i
67
162
  end
68
163
  end
69
164
 
70
- if offenses_count.zero?
71
- add_offense(processed_source.to_source_range(comment_node.loc), "Unused erblint:count comment for #{rule_name}") if comment_node
165
+ if @offenses_not_corrected.zero?
166
+ # have to adjust to get `\n` so we delete the whole line
167
+ add_offense(processed_source.to_source_range(comment_node.loc.adjust(end_pos: 1)), "Unused erblint:count comment for #{rule_name}", "") if comment_node
72
168
  return
73
169
  end
74
170
 
75
171
  first_offense = @offenses[0]
76
172
 
77
173
  if comment_node.nil?
78
- add_offense(processed_source.to_source_range(first_offense.source_range), "#{rule_name}: If you must, add <%# erblint:counter #{rule_name} #{offenses_count} %> to bypass this check.", "<%# erblint:counter #{rule_name} #{offenses_count} %>")
79
- else
174
+ add_offense(processed_source.to_source_range(first_offense.source_range), "#{rule_name}: If you must, add <%# erblint:counter #{rule_name} #{@offenses_not_corrected} %> to bypass this check.", "<%# erblint:counter #{rule_name} #{@offenses_not_corrected} %>")
175
+ elsif expected_count != @offenses_not_corrected
176
+ add_offense(processed_source.to_source_range(comment_node.loc), "Incorrect erblint:counter number for #{rule_name}. Expected: #{expected_count}, actual: #{@offenses_not_corrected}.", "<%# erblint:counter #{rule_name} #{@offenses_not_corrected} %>")
177
+ # the only offenses remaining are not autocorrectable, so we can ignore them
178
+ elsif expected_count == @offenses_not_corrected && @offenses.size == @offenses_not_corrected
80
179
  clear_offenses
81
- add_offense(processed_source.to_source_range(comment_node.loc), "Incorrect erblint:counter number for #{rule_name}. Expected: #{expected_count}, actual: #{offenses_count}.", " erblint:counter #{rule_name} #{offenses_count} ") if expected_count != offenses_count
82
180
  end
83
181
  end
84
182
 
85
183
  def generate_offense(klass, processed_source, tag, message = nil, replacement = nil)
86
184
  message ||= klass::MESSAGE
87
- klass_name = klass.name.split("::")[-1]
185
+ klass_name = klass.name.demodulize
88
186
  offense = ["#{klass_name}:#{message}", tag.node.loc.source].join("\n")
89
187
  add_offense(processed_source.to_source_range(tag.loc), offense, replacement)
90
188
  end
@@ -5,7 +5,7 @@ module Primer
5
5
  module VERSION
6
6
  MAJOR = 0
7
7
  MINOR = 0
8
- PATCH = 44
8
+ PATCH = 45
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH].join(".")
11
11
  end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :utilities do
4
+ task :build do
5
+ require "yaml"
6
+ require "json"
7
+ require File.expand_path("./../../demo/config/environment.rb", __dir__)
8
+
9
+ # Keys that are looked for to be included in the utilities.yml file
10
+ SUPPORTED_KEYS = %i[
11
+ hide
12
+ float
13
+ m mt mr mb ml mx my
14
+ p pt pr pb pl px py
15
+ ].freeze
16
+
17
+ # Replacements for some classnames that end up being a different argument key
18
+ REPLACEMENT_KEYS = {
19
+ "^v-align" => "vertical_align",
20
+ "^d" => "display",
21
+ "^wb" => "word_break",
22
+ "^v" => "visibility"
23
+ }.freeze
24
+
25
+ BREAKPOINTS = [nil, "sm", "md", "lg", "xl"].freeze
26
+
27
+ css_data =
28
+ JSON.parse(
29
+ File.read(
30
+ File.join(
31
+ __FILE__.split("lib/tasks/utilities.rake")[0], "/node_modules/@primer/css/dist/stats/utilities.json"
32
+ )
33
+ )
34
+ )["selectors"]["values"]
35
+
36
+ output = {}
37
+
38
+ css_data.each do |selector|
39
+ selector.sub!(/^./, "")
40
+ # Next if selector has ancestors or sibling selectors
41
+ next if selector.match?(/[:><~\[\.]/)
42
+ next unless SUPPORTED_KEYS.any? { |key| selector.start_with?("#{key}-") }
43
+
44
+ # Dupe so we still have the selector at the end of slicing it up
45
+ classname = selector.dup
46
+ key = ""
47
+
48
+ # Look for a replacement key
49
+ REPLACEMENT_KEYS.each do |k, v|
50
+ next unless classname.match?(Regexp.new(k))
51
+
52
+ key = v
53
+ classname.sub!(Regexp.new(k + "-"), "")
54
+ end
55
+
56
+ # If we didn't find a replacement, grab the first text before hyphen
57
+ if classname == selector
58
+ key = classname.split("-").first
59
+ classname.sub!(/^[^-]+-/, "")
60
+ end
61
+
62
+ # Check if the next bit of the classname is a breakpoint
63
+ if classname.match?(/^(sm-|md-|lg-|xl-)/)
64
+ breakpoint = classname.split("-").first
65
+ classname.sub!(/^[^-]+-/, "")
66
+ end
67
+
68
+ # Change the rest from hypens to underscores
69
+ classname.sub!(/\-/, "_")
70
+
71
+ # convert padding/margin negative values ie n7 to -7
72
+ classname.sub!(/^n/, "-") if classname.match?(/^n[0-9]/)
73
+
74
+ key = key.to_sym
75
+
76
+ classname = if classname.match?(/\A[-+]?[0-9]+\z/)
77
+ classname.to_i
78
+ else
79
+ classname.to_sym
80
+ end
81
+
82
+ if output[key].nil?
83
+ output[key] = { classname => Array.new(5, nil) }
84
+ elsif output[key][classname].nil?
85
+ output[key][classname] = Array.new(5, nil)
86
+ end
87
+
88
+ output[key][classname][BREAKPOINTS.index(breakpoint)] = selector
89
+ end
90
+
91
+ output.transform_values! do |x|
92
+ x.transform_values { |y| y.reverse.drop_while(&:nil?).reverse }
93
+ end
94
+
95
+ File.open("app/lib/primer/classify/utilities.yml", "w") do |f|
96
+ f.puts YAML.dump(output)
97
+ end
98
+ end
99
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: primer_view_components
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.44
4
+ version: 0.0.45
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub Open Source
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-06-10 00:00:00.000000000 Z
11
+ date: 2021-06-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionview
@@ -356,9 +356,9 @@ files:
356
356
  - app/components/primer/auto_complete/auto_complete.html.erb
357
357
  - app/components/primer/auto_complete/auto_complete.js
358
358
  - app/components/primer/auto_complete/auto_complete.ts
359
- - app/components/primer/auto_complete/auto_component.d.ts
360
- - app/components/primer/auto_complete/auto_component.js
361
359
  - app/components/primer/auto_complete/item.rb
360
+ - app/components/primer/auto_complete_component.d.ts
361
+ - app/components/primer/auto_complete_component.js
362
362
  - app/components/primer/avatar_component.rb
363
363
  - app/components/primer/avatar_stack_component.html.erb
364
364
  - app/components/primer/avatar_stack_component.rb
@@ -386,6 +386,8 @@ files:
386
386
  - app/components/primer/counter_component.rb
387
387
  - app/components/primer/details_component.html.erb
388
388
  - app/components/primer/details_component.rb
389
+ - app/components/primer/details_menu_component.d.ts
390
+ - app/components/primer/details_menu_component.js
389
391
  - app/components/primer/dropdown.d.ts
390
392
  - app/components/primer/dropdown.html.erb
391
393
  - app/components/primer/dropdown.js
@@ -465,7 +467,8 @@ files:
465
467
  - app/lib/primer/classify/functional_colors.rb
466
468
  - app/lib/primer/classify/functional_text_colors.rb
467
469
  - app/lib/primer/classify/grid.rb
468
- - app/lib/primer/classify/spacing.rb
470
+ - app/lib/primer/classify/utilities.rb
471
+ - app/lib/primer/classify/utilities.yml
469
472
  - app/lib/primer/fetch_or_fallback_helper.rb
470
473
  - app/lib/primer/join_style_arguments_helper.rb
471
474
  - app/lib/primer/octicon/cache.rb
@@ -486,6 +489,7 @@ files:
486
489
  - lib/tasks/coverage.rake
487
490
  - lib/tasks/docs.rake
488
491
  - lib/tasks/statuses.rake
492
+ - lib/tasks/utilities.rake
489
493
  - lib/yard/docs_helper.rb
490
494
  - lib/yard/renders_many_handler.rb
491
495
  - lib/yard/renders_one_handler.rb
@@ -510,7 +514,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
510
514
  - !ruby/object:Gem::Version
511
515
  version: '0'
512
516
  requirements: []
513
- rubygems_version: 3.0.3
517
+ rubygems_version: 3.1.2
514
518
  signing_key:
515
519
  specification_version: 4
516
520
  summary: ViewComponents for the Primer Design System
@@ -1,63 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Primer
4
- class Classify
5
- # Handler for PrimerCSS spacing classes.
6
- class Spacing
7
- BASE_OPTIONS = (0..6).to_a.freeze
8
- BASE_MAPPINGS = {
9
- my: BASE_OPTIONS,
10
- pb: BASE_OPTIONS,
11
- pl: BASE_OPTIONS,
12
- pr: BASE_OPTIONS,
13
- pt: BASE_OPTIONS,
14
- px: BASE_OPTIONS,
15
- py: BASE_OPTIONS
16
- }.freeze
17
-
18
- MARGIN_DIRECTION_OPTIONS = [*(-6..-1), *BASE_OPTIONS].freeze
19
- MARGIN_DIRECTION_MAPPINGS = {
20
- mb: MARGIN_DIRECTION_OPTIONS,
21
- ml: MARGIN_DIRECTION_OPTIONS,
22
- mr: MARGIN_DIRECTION_OPTIONS,
23
- mt: MARGIN_DIRECTION_OPTIONS
24
- }.freeze
25
-
26
- AUTO_OPTIONS = [*BASE_OPTIONS, :auto].freeze
27
- AUTO_MAPPINGS = {
28
- m: AUTO_OPTIONS,
29
- mx: AUTO_OPTIONS
30
- }.freeze
31
-
32
- RESPONSIVE_OPTIONS = [*BASE_OPTIONS, :responsive].freeze
33
- RESPONSIVE_MAPPINGS = {
34
- p: RESPONSIVE_OPTIONS
35
- }.freeze
36
-
37
- MAPPINGS = {
38
- **BASE_MAPPINGS,
39
- **MARGIN_DIRECTION_MAPPINGS,
40
- **AUTO_MAPPINGS,
41
- **RESPONSIVE_MAPPINGS
42
- }.freeze
43
- KEYS = MAPPINGS.keys.freeze
44
-
45
- class << self
46
- def spacing(key, val, breakpoint)
47
- validate(key, val) unless Rails.env.production?
48
-
49
- return "#{key.to_s.dasherize}#{breakpoint}-n#{val.abs}" if val.is_a?(Numeric) && val.negative?
50
-
51
- "#{key.to_s.dasherize}#{breakpoint}-#{val.to_s.dasherize}"
52
- end
53
-
54
- private
55
-
56
- def validate(key, val)
57
- raise ArgumentError, "#{key} is not a spacing key" unless KEYS.include?(key)
58
- raise ArgumentError, "#{val} is not a valid value for :#{key}. Use one of #{MAPPINGS[key]}" unless MAPPINGS[key].include?(val)
59
- end
60
- end
61
- end
62
- end
63
- end