classy-yaml 1.3.1 → 1.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 82933744cd15e116066a4004c44169d70d36b2c6e58b4fbb6654d957ecea0ed4
4
- data.tar.gz: 305cfa5b8a5a53bd50d5187fe8b2d13b0ecc52ea295a591dcca694c407567503
3
+ metadata.gz: b91dad78932d510d2299faae67a08584bad33501c3a048435603a6e673049090
4
+ data.tar.gz: c3d9d4e2c543e3f3b70b94260f2293d5d3f6a24474e228690aa5c97f5a80666f
5
5
  SHA512:
6
- metadata.gz: d9e8f6d551c9b482b0712a8b4b50734ae579f876012dd1414df37f3db8f4ad9e5030a008ebdb1172e560d517f10c8b387237c45739ffc6f68d85c9d1184674eb
7
- data.tar.gz: c6347754df73ef47fd53bc7d9ee1650a0aea5b7042d8ae08c3310fc4579281dc3763816cbe1aa4fb612f98d7d7eac165fa5566fd4a75b08f76f4c6b9ea292385
6
+ metadata.gz: 1bc0b46c0d2f9fb2a49eedada6e497c7aebf14bc6f95f75609461620f44d5df12066dd8437fe8be90240053670dad19bf6cf1a80a1d8a3f08f0a54ea8e1ce6ad
7
+ data.tar.gz: bcf8f2d58c819e91a935076eb920094e27b2855e30d13038fd26ad3942447d1c5c14f646427e18d3787a6c39774da89bc63443995cf0373935eceb28db0186b4
data/README.md CHANGED
@@ -85,6 +85,23 @@ btn:
85
85
 
86
86
  # will add the classes "px-3 py-2 text-blue-50"
87
87
  ```
88
+
89
+ yass(:button, :primary) # => "btn btn-primary"
90
+ yass(:button, :secondary) # => "btn btn-secondary"
91
+ yass(:button, :large) # => "btn btn-lg"
92
+ yass(:button, :primary, :large) # => "btn btn-primary btn-lg"
93
+
94
+ ### Array style definition
95
+
96
+ ```
97
+ array:
98
+ - "px-3 py-2"
99
+ - "bg-gray"
100
+ - "text-purple"
101
+ ```
102
+
103
+ yass(:array) # => "px-3 py-2 bg-gray text-purple"
104
+
88
105
  ## Configuration
89
106
  You can configure the gem by creating an initializer in your Rails app. The following options are available:
90
107
 
@@ -2,13 +2,13 @@ module Classy
2
2
  module Yaml
3
3
  module ComponentHelpers
4
4
  def yass(*args)
5
- calling_path = Object.const_source_location(self.class.name).first.split('/')[0...-1].join('/')
6
- calling_file = Object.const_source_location(self.class.name).first.split('/').last.split('.').first
7
- component_name = self.class.name.underscore.split('/').last.split('.').first
5
+ calling_path = Object.const_source_location(self.class.name).first.split("/")[0...-1].join("/")
6
+ calling_file = Object.const_source_location(self.class.name).first.split("/").last.split(".").first
7
+ component_name = self.class.name.underscore.split("/").last.split(".").first
8
8
 
9
- classy_files = ["#{calling_path}/#{component_name}.yml",
9
+ classy_files = [ "#{calling_path}/#{component_name}.yml",
10
10
  "#{calling_path}/#{calling_file}/#{calling_file}.yml",
11
- "#{calling_path}/#{calling_file}/#{component_name}.yml"]
11
+ "#{calling_path}/#{calling_file}/#{component_name}.yml" ]
12
12
 
13
13
  if args.first.is_a?(Hash)
14
14
  args.first.merge!({ classy_files: classy_files.uniq })
@@ -1,38 +1,69 @@
1
1
  module Classy
2
2
  module Yaml
3
3
  module Helpers
4
+ # Fetches utility classes from YAML files based on the provided keys.
5
+ # The method follows a priority order:
6
+ # 1. Extra files (highest priority)
7
+ # 2. Default YAML
8
+ # 3. ViewComponent YAMLs (lowest priority)
9
+ #
10
+ # @param args [Array] Array of keys to look up in the YAML files
11
+ # @return [String] Space-separated list of CSS classes
4
12
  def yass(*args)
5
- classy_yamls = []
13
+ # Start with ViewComponent YAMLs (lowest priority)
14
+ classy_yamls = Classy::Yaml.cached_engine_yamls.dup
6
15
 
7
- Classy::Yaml.engine_files.each do |file|
8
- if File.exist?(file) && YAML.load_file(file)
9
- file = YAML.load_file(file)
10
- classy_yamls << file if file
11
- end
12
- end
16
+ # Add default YAML (next priority)
17
+ default_yaml = Classy::Yaml.cached_default_yaml
18
+ classy_yamls << default_yaml if default_yaml
13
19
 
14
- classy_yamls << YAML.load_file(Rails.root.join(Classy::Yaml.default_file)) if File.exist?(Rails.root.join(Classy::Yaml.default_file))
20
+ # Add extra files (highest priority)
21
+ Classy::Yaml.extra_files.each do |file_path|
22
+ load_yaml_file(file_path, classy_yamls, "extra")
23
+ end
15
24
 
25
+ # Add classy_files (highest priority)
16
26
  classy_files_hash = args.find { |arg| arg.is_a?(Hash) && arg.keys.include?(:classy_files) } || { classy_files: [] }
17
-
18
- (classy_files_hash[:classy_files] + Classy::Yaml.extra_files).each do |file|
19
- if File.exist?(file) && YAML.load_file(file)
20
- file = YAML.load_file(file)
21
- classy_yamls << file if file
22
- end
27
+ classy_files_hash[:classy_files].each do |file_path|
28
+ load_yaml_file(file_path, classy_yamls, "classy")
23
29
  end
24
30
 
25
- return if classy_yamls.blank?
31
+ return "" if classy_yamls.blank?
26
32
 
27
33
  skip_base_hash = args.find { |arg| arg.is_a?(Hash) && arg.keys.include?(:skip_base) } || {}
28
34
  keys, classes = flatten_args(values: args)
29
35
  classes += fetch_classes(keys, classy_yamls: classy_yamls, skip_base: skip_base_hash[:skip_base])
30
36
 
31
- return classes.flatten.join(" ")
37
+ classes.flatten.uniq.join(" ")
32
38
  end
33
39
 
34
40
  private
35
41
 
42
+ # Loads a YAML file and adds its contents to the classy_yamls array
43
+ # @param file_path [String, Pathname] Path to the YAML file
44
+ # @param classy_yamls [Array] Array to add the parsed YAML to
45
+ # @param file_type [String] Type of file being loaded (for error messages)
46
+ def load_yaml_file(file_path, classy_yamls, file_type)
47
+ begin
48
+ path_obj = file_path.is_a?(Pathname) ? file_path : Rails.root.join(file_path)
49
+ if File.exist?(path_obj)
50
+ content = File.read(path_obj, encoding: "UTF-8")
51
+ parsed_yaml = YAML.safe_load(content, permitted_classes: [ Symbol, String, Array, Hash ], aliases: true)
52
+ classy_yamls << parsed_yaml if parsed_yaml && parsed_yaml.is_a?(Hash)
53
+ end
54
+ rescue Psych::SyntaxError => e
55
+ Rails.logger.error "Classy::Yaml (yass helper): Failed to parse #{file_type} YAML file #{file_path}: #{e.message}"
56
+ rescue => e
57
+ Rails.logger.error "Classy::Yaml (yass helper): Error loading #{file_type} YAML file #{file_path}: #{e.message}"
58
+ end
59
+ end
60
+
61
+ # Flattens the arguments into keys and classes
62
+ # @param root [Array] Current root path in the argument tree
63
+ # @param values [Array] Values to process
64
+ # @param keys [Array] Array to store found keys
65
+ # @param added_classes [Array] Array to store found classes
66
+ # @return [Array] Tuple of [keys, added_classes]
36
67
  def flatten_args(root: [], values: [], keys: [], added_classes: [])
37
68
  values.each do |value|
38
69
  if value.is_a?(Hash)
@@ -42,14 +73,14 @@ module Classy
42
73
  end
43
74
 
44
75
  value.keys.each do |key|
45
- values << (root + [key.to_s])
46
- flatten_args(root: root + [key.to_s], values: [value[key]], keys: keys, added_classes: added_classes)
76
+ values << (root + [ key.to_s ])
77
+ flatten_args(root: root + [ key.to_s ], values: [ value[key] ], keys: keys, added_classes: added_classes)
47
78
  end
48
79
  else
49
80
  if value.is_a?(Array)
50
81
  flatten_args(root: root, values: value, keys: keys, added_classes: added_classes)
51
82
  else
52
- keys << (root + [value.to_s])
83
+ keys << (root + [ value.to_s ])
53
84
  end
54
85
  end
55
86
  end
@@ -57,6 +88,11 @@ module Classy
57
88
  return keys, added_classes
58
89
  end
59
90
 
91
+ # Fetches classes from the YAML files based on the provided keys
92
+ # @param keys [Array] Array of keys to look up
93
+ # @param classy_yamls [Array] Array of YAML files to search in
94
+ # @param skip_base [Boolean] Whether to skip base classes
95
+ # @return [Array] Array of found classes
60
96
  def fetch_classes(keys, classy_yamls: [], skip_base: false)
61
97
  classes = []
62
98
 
@@ -65,36 +101,58 @@ module Classy
65
101
  fetched_classes = nil
66
102
 
67
103
  classy_yamls.reverse_each do |classy_yaml|
104
+ # Base Class Lookup
68
105
  unless skip_base == true
69
106
  begin
70
- base_classes ||= if classy_yaml.send(:dig, *key).is_a?(Hash)
71
- classy_yaml.send(:dig, *(key + ['base'])).try(:split, " ")
72
- else
73
- classy_yaml.send(:dig, *(key[0...-1] + ['base'])).try(:split, " ")
74
- end
107
+ value_at_key = classy_yaml.send(:dig, *key)
108
+ base_value = if value_at_key.is_a?(Hash)
109
+ classy_yaml.send(:dig, *(key + [ "base" ]))
110
+ elsif key.length > 1
111
+ classy_yaml.send(:dig, *(key[0...-1] + [ "base" ]))
112
+ else
113
+ nil
114
+ end
115
+ normalized_base = normalize_original(base_value)
116
+ base_classes ||= normalized_base
75
117
  rescue
76
118
  Rails.logger.warn(Classy::Yaml::InvalidKeyError.new(data: key))
77
119
  end
78
120
  end
79
121
 
122
+ # Specific Key Class Lookup
80
123
  begin
81
- fetched_classes ||= unless classy_yaml.send(:dig, *key).is_a?(Hash)
82
- classy_yaml.send(:dig, *key).try(:split, " ")
83
- end
84
-
85
- base_classes = nil if base_classes.blank?
86
- fetched_classes = nil if fetched_classes.blank?
124
+ value = classy_yaml.send(:dig, *key)
125
+ unless value.is_a?(Hash)
126
+ normalized_fetched = normalize_original(value)
127
+ fetched_classes ||= normalized_fetched
128
+ end
87
129
  rescue
88
130
  Rails.logger.warn(Classy::Yaml::InvalidKeyError.new(data: key))
89
131
  end
132
+
133
+ base_classes = nil if base_classes.blank?
134
+ fetched_classes = nil if fetched_classes.blank?
90
135
  end
91
136
 
92
137
  classes << base_classes unless base_classes.blank?
93
138
  classes << fetched_classes unless fetched_classes.blank?
94
139
  end
95
140
 
96
- classes.reject!(&:blank?)
97
- return classes.flatten.uniq
141
+ classes.flatten.uniq
142
+ end
143
+
144
+ # Normalizes a value into an array of classes
145
+ # @param value [String, Array, nil] Value to normalize
146
+ # @return [Array, nil] Array of classes or nil if value is invalid
147
+ def normalize_original(value)
148
+ case value
149
+ when String
150
+ value.split(" ").reject(&:blank?)
151
+ when Array
152
+ value.flatten.map(&:to_s).reject(&:blank?)
153
+ else
154
+ nil
155
+ end
98
156
  end
99
157
  end
100
158
  end
@@ -1,5 +1,5 @@
1
1
  module Classy
2
2
  module Yaml
3
- VERSION = '1.3.1'
3
+ VERSION = "1.5.0"
4
4
  end
5
5
  end
data/lib/classy/yaml.rb CHANGED
@@ -3,6 +3,7 @@ require "classy/yaml/engine"
3
3
 
4
4
  module Classy
5
5
  module Yaml
6
+ # -- Configuration Accessors --
6
7
  mattr_accessor :default_file
7
8
  @@default_file = "config/utility_classes.yml"
8
9
 
@@ -12,20 +13,135 @@ module Classy
12
13
  mattr_accessor :extra_files
13
14
  @@extra_files = []
14
15
 
16
+ mattr_accessor :override_tag_helpers
17
+ @@override_tag_helpers = false
18
+
19
+ # -- Autoloads --
15
20
  autoload :Helpers, "classy/yaml/helpers"
16
21
  autoload :ComponentHelpers, "classy/yaml/component_helpers"
17
22
  autoload :InvalidKeyError, "classy/yaml/invalid_key_error"
18
23
 
24
+ # -- Class Instance Variables for Caching --
25
+ @cached_engine_yamls = nil
26
+ @cached_default_yaml = nil
27
+ @load_lock = Mutex.new # Prevent race conditions during lazy loading
28
+
29
+ # -- Configuration Setters with Path Resolution --
19
30
  def self.engine_files=(value)
20
31
  @@engine_files = Array.wrap(value).reject(&:blank?).map { |file| Rails.root.join(file) }
32
+ @cached_engine_yamls = nil # Clear cache on reassignment
21
33
  end
22
34
 
23
35
  def self.extra_files=(value)
24
36
  @@extra_files = Array.wrap(value).reject(&:blank?).map { |file| Rails.root.join(file) }
37
+ # Note: extra_files are not cached globally by default
38
+ end
39
+
40
+ def self.default_file=(value)
41
+ @@default_file = value
42
+ @cached_default_yaml = nil # Clear cache on reassignment
43
+ end
44
+
45
+ def self.override_tag_helpers=(value)
46
+ @@override_tag_helpers = value
47
+ apply_tag_helper_override if value
25
48
  end
26
49
 
50
+ # -- Cached Data Accessors (Lazy Loading) --
51
+ def self.cached_engine_yamls
52
+ # Bypass cache in development and test environments
53
+ return load_engine_yamls if Rails.env.development? || Rails.env.test?
54
+
55
+ # Use cache in other environments
56
+ return @cached_engine_yamls if @cached_engine_yamls
57
+
58
+ @load_lock.synchronize do
59
+ # Double-check idiom to ensure loading happens only once
60
+ return @cached_engine_yamls if @cached_engine_yamls
61
+ @cached_engine_yamls = load_engine_yamls
62
+ end
63
+ end
64
+
65
+ def self.cached_default_yaml
66
+ # Bypass cache in development and test environments
67
+ return load_default_yaml if Rails.env.development? || Rails.env.test?
68
+
69
+ # Use cache in other environments
70
+ return @cached_default_yaml if @cached_default_yaml
71
+
72
+ @load_lock.synchronize do
73
+ return @cached_default_yaml if @cached_default_yaml
74
+ @cached_default_yaml = load_default_yaml
75
+ end
76
+ end
77
+
78
+ # -- Setup Method --
27
79
  def self.setup
28
80
  yield self
81
+ # Clear all caches when configuration changes
82
+ @cached_engine_yamls = nil
83
+ @cached_default_yaml = nil
84
+ # Apply tag helper override if enabled
85
+ apply_tag_helper_override if @@override_tag_helpers
86
+ end
87
+
88
+ private
89
+
90
+ def self.apply_tag_helper_override
91
+ return unless defined?(ActionView::Helpers::TagHelper)
92
+
93
+ # Override the TagBuilder class which is what the tag method returns
94
+ ActionView::Helpers::TagHelper::TagBuilder.class_eval do
95
+ unless method_defined?(:classy_yaml_original_tag_options)
96
+ alias_method :classy_yaml_original_tag_options, :tag_options
97
+
98
+ def tag_options(options, escape = true)
99
+ if options
100
+ class_key = options.key?(:class) ? :class : "class"
101
+ options = options.dup
102
+ val = options[class_key]
103
+ if val.is_a?(Symbol) || val.is_a?(Hash) || val.is_a?(Array)
104
+ options[:class] = yass(val)
105
+ end
106
+ end
107
+ classy_yaml_original_tag_options(options, escape)
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ def self.load_engine_yamls
114
+ yamls = []
115
+ self.engine_files.each do |file_path|
116
+ begin
117
+ if File.exist?(file_path)
118
+ content = File.read(file_path, encoding: "UTF-8")
119
+ parsed_yaml = YAML.safe_load(content, permitted_classes: [ Symbol, String, Array, Hash ], aliases: true)
120
+ yamls << parsed_yaml if parsed_yaml && parsed_yaml.is_a?(Hash)
121
+ end
122
+ rescue Psych::SyntaxError => e
123
+ Rails.logger.error "Classy::Yaml: Failed to parse engine YAML file #{file_path}: #{e.message}"
124
+ rescue => e
125
+ Rails.logger.error "Classy::Yaml: Error loading engine YAML file #{file_path}: #{e.message}"
126
+ end
127
+ end
128
+ yamls
129
+ end
130
+
131
+ def self.load_default_yaml
132
+ default_path = Rails.root.join(self.default_file)
133
+ begin
134
+ if File.exist?(default_path)
135
+ content = File.read(default_path, encoding: "UTF-8")
136
+ parsed_yaml = YAML.safe_load(content, permitted_classes: [ Symbol, String, Array, Hash ], aliases: true)
137
+ return parsed_yaml if parsed_yaml && parsed_yaml.is_a?(Hash)
138
+ end
139
+ rescue Psych::SyntaxError => e
140
+ Rails.logger.error "Classy::Yaml: Failed to parse default YAML file #{default_path}: #{e.message}"
141
+ rescue => e
142
+ Rails.logger.error "Classy::Yaml: Error loading default YAML file #{default_path}: #{e.message}"
143
+ end
144
+ nil # Return nil if file doesn't exist or fails to load/parse
29
145
  end
30
146
  end
31
147
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: classy-yaml
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tonksthebear
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-01-30 00:00:00.000000000 Z
11
+ date: 2025-08-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -80,6 +80,48 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '3.36'
83
+ - !ruby/object:Gem::Dependency
84
+ name: appraisal
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.60'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.60'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-rails-omakase
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
83
125
  description: Rails helper to allow yaml configuration of utility css classes
84
126
  email:
85
127
  - ''
@@ -120,7 +162,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
120
162
  - !ruby/object:Gem::Version
121
163
  version: '0'
122
164
  requirements: []
123
- rubygems_version: 3.4.1
165
+ rubygems_version: 3.5.16
124
166
  signing_key:
125
167
  specification_version: 4
126
168
  summary: Rails helper to allow yaml configuration of utility css classes