primer_view_components 0.0.44 → 0.0.45

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.
@@ -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