dugway 1.2.0 → 1.3.1

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +1 -1
  3. data/README.md +32 -7
  4. data/dugway.gemspec +12 -10
  5. data/lib/dugway/cli/server.rb +67 -7
  6. data/lib/dugway/liquid/drops/artists_drop.rb +12 -0
  7. data/lib/dugway/liquid/drops/base_drop.rb +27 -2
  8. data/lib/dugway/liquid/drops/categories_drop.rb +12 -0
  9. data/lib/dugway/liquid/drops/pages_drop.rb +30 -2
  10. data/lib/dugway/liquid/drops/product_drop.rb +11 -0
  11. data/lib/dugway/liquid/drops/products_drop.rb +39 -0
  12. data/lib/dugway/liquid/drops/theme_drop.rb +29 -6
  13. data/lib/dugway/liquid/filters/core_filters.rb +169 -7
  14. data/lib/dugway/liquid/filters/font_filters.rb +1 -0
  15. data/lib/dugway/liquid/filters/util_filters.rb +1 -10
  16. data/lib/dugway/liquid/tags/get.rb +6 -6
  17. data/lib/dugway/liquid/tags/paginate.rb +61 -11
  18. data/lib/dugway/store.rb +39 -1
  19. data/lib/dugway/theme.rb +72 -33
  20. data/lib/dugway/version.rb +1 -1
  21. data/lib/dugway.rb +24 -1
  22. data/locales/storefront.de.yml +2 -0
  23. data/locales/storefront.en-CA.yml +2 -0
  24. data/locales/storefront.en-GB.yml +2 -0
  25. data/locales/storefront.en-US.yml +2 -0
  26. data/locales/storefront.es-ES.yml +2 -0
  27. data/locales/storefront.es-MX.yml +2 -0
  28. data/locales/storefront.fr-CA.yml +2 -0
  29. data/locales/storefront.fr-FR.yml +2 -0
  30. data/locales/storefront.id.yml +2 -0
  31. data/locales/storefront.it.yml +2 -0
  32. data/locales/storefront.ja.yml +2 -0
  33. data/locales/storefront.ko.yml +2 -0
  34. data/locales/storefront.nl.yml +2 -0
  35. data/locales/storefront.pl.yml +2 -0
  36. data/locales/storefront.pt-BR.yml +2 -0
  37. data/locales/storefront.pt-PT.yml +2 -0
  38. data/locales/storefront.ro.yml +2 -0
  39. data/locales/storefront.sv.yml +2 -0
  40. data/locales/storefront.tr.yml +2 -0
  41. data/locales/storefront.zh-CN.yml +2 -0
  42. data/locales/storefront.zh-TW.yml +2 -0
  43. data/mise.toml +2 -0
  44. data/spec/spec_helper.rb +3 -0
  45. data/spec/units/dugway/liquid/drops/pages_drop_spec.rb +186 -7
  46. data/spec/units/dugway/liquid/drops/product_drop_spec.rb +17 -0
  47. data/spec/units/dugway/liquid/filters/core_filters_spec.rb +297 -3
  48. data/spec/units/dugway/store_spec.rb +18 -0
  49. data/spec/units/dugway/theme_spec.rb +246 -0
  50. metadata +56 -27
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9523e3515728a8c01fbe05baf360edad408e447b64ccb4894270029bc95b12f1
4
- data.tar.gz: 80c989204781ae28db6af3b80f74fdc834c315e27f497188bfa431caabefaa2d
3
+ metadata.gz: ff6d7b016361e10eae58787065d149c641ec6d903b43ab043ad63f5887f2839f
4
+ data.tar.gz: d1837b869e9839a0e093ed5adba26ff154cba4fbc5560f11ca0ef0da6f1e328c
5
5
  SHA512:
6
- metadata.gz: 35a6c9269ee9a256feb9abe34614ab9d815e6c429f3b0ab4f7ac765a2a568d85848a2c3c61f60a08e138c02da4b0d3b631384003864c29b12b4f55cada681549
7
- data.tar.gz: b0b2ddd105dd648fe79c51194a0fe8e10f8c475c5f1cadd3733c94ae349124bd8c7070e47d0de1ac39986b72e5f6f94e523e00424e224fb8cc4b801ecf882bf6
6
+ metadata.gz: 394df0c94d22aea6e12bd142c316ec3622e5aae27cdb0f2f883b98bb53eac0652718770162e7891fe5a0ce3d68e638e830c01d68a8f3beaaa20f62baceac9047
7
+ data.tar.gz: dcab3dd4a7db0ebf1a079e1f83f1a78f811da241abcccd2a4154bb5b2e017986022683e09f5ead00fdf5c0e45880ca5da34419fde32d91c65f09361a9be92230
@@ -8,7 +8,7 @@ jobs:
8
8
 
9
9
  strategy:
10
10
  matrix:
11
- ruby-version: [2.7.5, 2.6.7]
11
+ ruby-version: [2.7]
12
12
 
13
13
  steps:
14
14
  - uses: actions/checkout@v2
data/README.md CHANGED
@@ -28,11 +28,7 @@ using [RubyInstaller](http://rubyinstaller.org).
28
28
 
29
29
  Supported Ruby versions:
30
30
 
31
- - 2.3
32
- - 2.4
33
- - 2.5
34
- - 2.6
35
- - 2.7
31
+ - 2.7+
36
32
 
37
33
  ### Install Dugway
38
34
 
@@ -99,12 +95,12 @@ your code into multiple files in separate directories.
99
95
 
100
96
  #### Using Sprockets
101
97
 
102
- [Sprockets](https://github.com/sstephenson/sprockets) allows you to bring in
98
+ [Sprockets](https://github.com/rails/sprockets) allows you to bring in
103
99
  CSS and JavaScript from different directories into a single file. We've created
104
100
  **stylesheets** and **javascripts** directories by default that you can put
105
101
  your code in, but you could delete those and put files anywhere you'd like.
106
102
  After that, use [Sprockets
107
- directives](https://github.com/sstephenson/sprockets#the-directive-processor)
103
+ directives](https://github.com/rails/sprockets#directives)
108
104
  to package them into **theme.css** and **theme.js**.
109
105
 
110
106
  ##### theme.css
@@ -193,6 +189,35 @@ dugway server
193
189
  By default this will serve your theme at http://127.0.0.1:9292/. You can then stop
194
190
  the server by hitting CTRL+C.
195
191
 
192
+ ### Running on a custom port
193
+
194
+ To run Dugway on a different port, use the `--port` (or `-p`) option:
195
+
196
+ ```
197
+ dugway server --port 3000
198
+ dugway server -p 8080
199
+ ```
200
+
201
+ This is particularly useful for running multiple theme instances simultaneously:
202
+
203
+ ```
204
+ # Terminal 1 - Theme A
205
+ cd /path/to/theme-a
206
+ dugway server --port 3000
207
+
208
+ # Terminal 2 - Theme B
209
+ cd /path/to/theme-b
210
+ dugway server --port 3001
211
+ ```
212
+
213
+ You can also customize the host address:
214
+
215
+ ```
216
+ dugway server --host 0.0.0.0 --port 3000
217
+ ```
218
+
219
+ ### Logging
220
+
196
221
  If you want to run with logging, enable it via your theme's `settings.json`
197
222
  file with `"log": true`. When Dugway is running, it will then create a log
198
223
  in `log/dugway.log` in your theme's directory. You can then log output
data/dugway.gemspec CHANGED
@@ -16,32 +16,34 @@ Gem::Specification.new do |s|
16
16
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
17
  s.platform = Gem::Platform::RUBY
18
18
  s.require_path = 'lib'
19
- s.required_ruby_version = '>= 2.3', '< 3'
19
+ s.required_ruby_version = '>= 2.7', '< 3'
20
20
  s.executables << 'dugway'
21
21
 
22
22
  s.add_dependency('bundler', '>= 1.0.0')
23
23
  s.add_dependency('rack', '~> 1.4.1')
24
24
  s.add_dependency('rack-mount', '~> 0.8.3')
25
- s.add_dependency('activesupport', '~> 5.2')
26
- s.add_dependency('liquid', '~> 3.0.6')
25
+ s.add_dependency('activesupport', '~> 5.2.8')
26
+ s.add_dependency('liquid', '~> 5.5.1')
27
27
  s.add_dependency('bigdecimal', '~> 1.4.4')
28
28
  s.add_dependency('coffee-script', '~> 2.4.1')
29
29
  s.add_dependency('sass', '~> 3.4.25')
30
- s.add_dependency('sprockets', '~> 2.0')
30
+ s.add_dependency('sprockets', '~> 2.12')
31
31
  s.add_dependency('sprockets-sass', '~> 1.3.1')
32
32
  s.add_dependency('compass', '~> 1.0.3')
33
- s.add_dependency('httparty', '~> 0.10.0')
33
+ s.add_dependency('sassc', '~> 2.4')
34
+ s.add_dependency('httparty', '~> 0.21.0')
34
35
  s.add_dependency('better_errors', '~> 0.9.0')
35
36
  s.add_dependency('will_paginate', '~> 3.0.4')
36
- s.add_dependency('i18n', '1.6')
37
+ s.add_dependency('i18n', '~> 1.12')
37
38
  s.add_dependency('htmlentities', '~> 4.3.1')
38
- s.add_dependency('listen', '~> 3.0')
39
- s.add_dependency('thor', '~> 0.20.3')
39
+ s.add_dependency('listen', '~> 3.8')
40
+ s.add_dependency('thor', '~> 1.2')
40
41
  s.add_dependency('rubyzip', '~> 0.9.9')
41
- s.add_dependency('terser', '~> 1.1')
42
+ s.add_dependency('terser', '~> 1.2')
42
43
  s.add_dependency('thin', '~> 1.8.0')
43
44
  s.add_dependency('bigcartel-theme-fonts', '>= 1.0.0')
44
- s.add_dependency('bigcartel-currency-locales', '>= 1.0.0')
45
+ s.add_dependency('bigcartel-currency-locales', '>= 2.4.0')
46
+ s.add_dependency('multi_json', '~> 1.15.0')
45
47
 
46
48
  s.add_development_dependency('rake', '~> 10.0.3')
47
49
  s.add_development_dependency('rspec', '~> 2.14.1')
@@ -1,5 +1,6 @@
1
1
  require 'rack'
2
2
  require 'listen'
3
+ require 'rbconfig'
3
4
 
4
5
  module Dugway
5
6
  module Cli
@@ -40,13 +41,72 @@ module Dugway
40
41
 
41
42
  Thread.new { listener.start }
42
43
 
43
- Rack::Server.start({
44
- :config => File.join(Dir.pwd, 'config.ru'),
45
- :environment => 'none',
46
- :Host => options[:host],
47
- :Port => options[:port],
48
- :server => options[:server]
49
- })
44
+ begin
45
+ Rack::Server.start({
46
+ :config => File.join(Dir.pwd, 'config.ru'),
47
+ :environment => 'none',
48
+ :Host => options[:host],
49
+ :Port => options[:port],
50
+ :server => options[:server]
51
+ })
52
+ rescue Errno::EADDRINUSE
53
+ handle_port_in_use_error(options[:port])
54
+ exit 1
55
+ rescue Errno::EACCES
56
+ handle_permission_error(options[:port])
57
+ exit 1
58
+ rescue Errno::EADDRNOTAVAIL
59
+ handle_address_error(options[:host], options[:port])
60
+ exit 1
61
+ rescue StandardError => e
62
+ if e.message.include?("port is in use") || e.message.include?("no acceptor")
63
+ handle_port_in_use_error(options[:port])
64
+ elsif e.message.include?("requires root privileges")
65
+ handle_permission_error(options[:port])
66
+ else
67
+ puts "\nError starting server: #{e.message}"
68
+ puts "Try running with different options or check your theme configuration."
69
+ end
70
+ exit 1
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def handle_port_in_use_error(port)
77
+ suggested_port = port + 1
78
+ puts "\nError: Port #{port} is already in use!"
79
+ puts "Try running with a different port:"
80
+ puts " dugway server --port #{suggested_port}"
81
+ puts " dugway server -p #{suggested_port}"
82
+ puts "\nTo see what's using port #{port}, run:"
83
+ puts " #{port_check_command(port)}"
84
+ end
85
+
86
+ def handle_permission_error(port)
87
+ puts "\nError: Permission denied to bind to port #{port}!"
88
+ if port < 1024
89
+ puts "Ports below 1024 require root privileges. Try a port above 1024:"
90
+ puts " dugway server --port 3000"
91
+ puts " dugway server --port 8080"
92
+ else
93
+ puts "Check your system permissions or try a different port."
94
+ end
95
+ end
96
+
97
+ def handle_address_error(host, port)
98
+ puts "\nError: Cannot bind to address #{host}!"
99
+ puts "Try using a different host address:"
100
+ puts " dugway server --host 0.0.0.0 --port #{port}"
101
+ puts " dugway server --host 127.0.0.1 --port #{port}"
102
+ end
103
+
104
+ def port_check_command(port)
105
+ if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
106
+ "netstat -ano | findstr :#{port}"
107
+ else
108
+ "lsof -i :#{port}"
109
+ end
50
110
  end
51
111
  end
52
112
  end
@@ -1,6 +1,18 @@
1
1
  module Dugway
2
2
  module Drops
3
3
  class ArtistsDrop < BaseDrop
4
+ # Liquid 5.x compatibility: implement liquid_method_missing for method access
5
+ def liquid_method_missing(method)
6
+ # Try to find artist by permalink
7
+ result = before_method(method.to_s)
8
+ # Wrap hash results in ArtistDrop if it looks like an artist
9
+ if result.is_a?(Hash) && result['permalink']
10
+ ArtistDrop.new(result)
11
+ else
12
+ result
13
+ end
14
+ end
15
+
4
16
  def all
5
17
  @all ||= source
6
18
  end
@@ -34,9 +34,14 @@ module Dugway
34
34
  return source.send(method_or_key)
35
35
  elsif source.respond_to?('has_key?') && source.has_key?(method_or_key)
36
36
  return source[method_or_key]
37
- elsif source.is_a?(Array) && source.first.has_key?('permalink')
37
+ elsif source.is_a?(Array) && source.first
38
+ # Handle both hash items and drop items in array
38
39
  for item in source
39
- return item if item['permalink'] == method_or_key.to_s
40
+ if item.is_a?(Hash) && item['permalink'] == method_or_key.to_s
41
+ return item
42
+ elsif item.respond_to?(:source) && item.source.is_a?(Hash) && item.source['permalink'] == method_or_key.to_s
43
+ return item
44
+ end
40
45
  end
41
46
  end
42
47
 
@@ -47,6 +52,11 @@ module Dugway
47
52
  before_method(method.to_s)
48
53
  end
49
54
 
55
+ # Liquid 5.x compatibility: implement liquid_method_missing for property access
56
+ def liquid_method_missing(method_name)
57
+ before_method(method_name.to_s)
58
+ end
59
+
50
60
  def errors
51
61
  @context['errors']
52
62
  end
@@ -54,6 +64,21 @@ module Dugway
54
64
  def error(msg)
55
65
  errors << msg
56
66
  end
67
+
68
+ # Liquid 5.x compatibility: make drops JSON-serializable
69
+ # Only return source for JSON serialization, not for filter processing
70
+ def to_liquid
71
+ self
72
+ end
73
+
74
+ def as_json(*args)
75
+ if source.is_a?(Hash)
76
+ # Convert Ruby hash to proper JSON-compatible format
77
+ source.stringify_keys
78
+ else
79
+ {}
80
+ end
81
+ end
57
82
  end
58
83
  end
59
84
  end
@@ -1,6 +1,18 @@
1
1
  module Dugway
2
2
  module Drops
3
3
  class CategoriesDrop < BaseDrop
4
+ # Liquid 5.x compatibility: implement liquid_method_missing for method access
5
+ def liquid_method_missing(method)
6
+ # Try to find category by permalink
7
+ result = before_method(method.to_s)
8
+ # Wrap hash results in CategoryDrop if it looks like a category
9
+ if result.is_a?(Hash) && result['permalink']
10
+ CategoryDrop.new(result)
11
+ else
12
+ result
13
+ end
14
+ end
15
+
4
16
  def all
5
17
  @all ||= source
6
18
  end
@@ -1,12 +1,40 @@
1
1
  module Dugway
2
2
  module Drops
3
3
  class PagesDrop < BaseDrop
4
+ # Liquid 5.x compatibility: implement liquid_method_missing for method access
5
+ def liquid_method_missing(method)
6
+ # Try to find page by permalink
7
+ result = before_method(method.to_s)
8
+ # Wrap hash results in PageDrop if it looks like a page
9
+ if result.is_a?(Hash) && result['permalink']
10
+ PageDrop.new(result)
11
+ else
12
+ result
13
+ end
14
+ end
15
+
4
16
  def all
5
- @all ||= source.select { |page| page['category'] == 'custom' }
17
+ @all ||= source.select { |page| (page.source['category'] == 'custom' || page.source['category'] == 'external') && !page.source['required'] }
6
18
  end
7
19
 
8
20
  def cart
9
- @cart ||= source.find { |page| page['permalink'] == 'cart' }
21
+ @cart ||= source.find { |page| page.source['permalink'] == 'cart' }
22
+ end
23
+
24
+ def subscribe_page
25
+ @subscribe_page ||= store.subscribe_url ? source.find { |page| page.source['url'] == store.subscribe_url } : nil
26
+ end
27
+
28
+ def custom_pages
29
+ @custom_pages ||= source.select { |page| page.source['category'] == 'custom' && !page.source['required'] }
30
+ end
31
+
32
+ def external_pages
33
+ @external_pages ||= source.select { |page| page.source['category'] == 'external' && !page.source['required'] }
34
+ end
35
+
36
+ def required_pages
37
+ @required_pages ||= source.select { |page| page.source['required'] == true }
10
38
  end
11
39
  end
12
40
  end
@@ -1,6 +1,11 @@
1
1
  module Dugway
2
2
  module Drops
3
3
  class ProductDrop < BaseDrop
4
+
5
+ # Liquid 5.x compatibility: ensure proper string representation
6
+ def to_s
7
+ name || "Product ##{id}"
8
+ end
4
9
  def created_at
5
10
  Time.parse(source['created_at'])
6
11
  end
@@ -112,6 +117,12 @@ module Dugway
112
117
  end
113
118
  end
114
119
 
120
+ # Used to simulate price suffix behavior
121
+ # In prod this is a per-product setting but for dugway it needs to be global
122
+ def price_suffix
123
+ store.price_suffix
124
+ end
125
+
115
126
  private
116
127
 
117
128
  def price_min_max
@@ -3,6 +3,45 @@ require 'will_paginate/array'
3
3
  module Dugway
4
4
  module Drops
5
5
  class ProductsDrop < BaseDrop
6
+
7
+ # Liquid 5.x compatibility: implement liquid_method_missing for method access
8
+ def liquid_method_missing(method)
9
+ method_str = method.to_s
10
+
11
+ # Handle known methods
12
+ case method_str
13
+ when 'all'
14
+ all
15
+ when 'current'
16
+ current
17
+ when 'on_sale'
18
+ on_sale
19
+ else
20
+ # Try to find product by permalink
21
+ result = before_method(method_str)
22
+ # Wrap hash results in ProductDrop if it looks like a product
23
+ if result.is_a?(Hash) && result['permalink']
24
+ ProductDrop.new(result)
25
+ else
26
+ result
27
+ end
28
+ end
29
+ end
30
+
31
+ # Also ensure before_method works as fallback
32
+ def before_method(method_or_key)
33
+ case method_or_key.to_s
34
+ when 'all'
35
+ all
36
+ when 'current'
37
+ current
38
+ when 'on_sale'
39
+ on_sale
40
+ else
41
+ super
42
+ end
43
+ end
44
+
6
45
  def all
7
46
  sort_and_paginate source
8
47
  end
@@ -8,13 +8,36 @@ module Dugway
8
8
  @definitions = definitions || {}
9
9
  end
10
10
 
11
- def before_method(method_or_key)
12
- # We should try to get away from this api and use the newer one below
13
- if source.respond_to?('has_key?') && source.has_key?(method_or_key) && settings_images.find { |image| image['variable'] == method_or_key.to_s }
14
- return ImageDrop.new(source[method_or_key].stringify_keys)
11
+ # Liquid 5.x compatibility: implement liquid_method_missing for property access
12
+ def liquid_method_missing(method)
13
+ method_str = method.to_s
14
+ method_sym = method.to_sym
15
+
16
+ # Try different key formats - source might have string or symbol keys
17
+ # Use has_key? to handle false/nil values properly
18
+ if source.has_key?(method_str)
19
+ value = source[method_str]
20
+ elsif source.has_key?(method_sym)
21
+ value = source[method_sym]
22
+ elsif source.has_key?(method_str.to_sym)
23
+ value = source[method_str.to_sym]
24
+ elsif source.has_key?(method_sym.to_s)
25
+ value = source[method_sym.to_s]
26
+ else
27
+ value = nil
15
28
  end
16
-
17
- super
29
+
30
+ # Check for image settings - need to wrap in ImageDrop
31
+ if !value.nil? && settings_images.find { |image| image['variable'] == method_str }
32
+ return ImageDrop.new(value.is_a?(Hash) ? value.stringify_keys : value)
33
+ end
34
+
35
+ value
36
+ end
37
+
38
+ def before_method(method_or_key)
39
+ # Fallback for older Liquid versions
40
+ liquid_method_missing(method_or_key)
18
41
  end
19
42
 
20
43
  # Newer API for theme images.
@@ -40,21 +40,89 @@ module Dugway
40
40
  when 'sign' then money_with_sign(amount)
41
41
  when 'code' then money_with_code(amount)
42
42
  when 'sign_and_code' then money_with_sign_and_code(amount)
43
- else number_to_currency(amount, :unit => '').strip
43
+ when 'rounded' then
44
+ begin
45
+ number_to_currency(amount.ceil, { :unit => '', :format => '%n', :precision => 0 }).to_s.strip
46
+ rescue FrozenError => e
47
+ sprintf('%.0f', amount.ceil.to_f)
48
+ end
49
+ when 'sign_rounded' then money_with_sign(amount.ceil, 0)
50
+ when 'code_rounded' then money_with_code(amount.ceil, 0)
51
+ else
52
+ begin
53
+ number_to_currency(amount, { :unit => '', :format => '%n' }).to_s.strip
54
+ rescue FrozenError => e
55
+ # Fallback for frozen hash issues in newer Ruby versions
56
+ sprintf('%.2f', amount.to_f)
57
+ end
58
+ end
59
+ end
60
+
61
+ def product_price(product, money_format = nil, range_format = nil)
62
+ return unless product
63
+
64
+ if product['variable_pricing']
65
+ case range_format
66
+ when 'min_only'
67
+ money(product['min_price'], money_format)
68
+ when 'max_only'
69
+ money(product['max_price'], money_format)
70
+ else # default, nil, or any other value
71
+ format_price_range(product['min_price'], product['max_price'], money_format)
72
+ end
73
+ else
74
+ money(product['default_price'] || product['price'], money_format)
44
75
  end
45
76
  end
46
77
 
47
- def money_with_sign(amount)
48
- unit = I18n.translate('number.currency.format.unit')
49
- number_to_currency(amount).gsub(unit, %{<span class="currency_sign">#{ HTMLEntities.new.encode(unit, :named) }</span>})
78
+ def money_with_sign(amount, precision = nil)
79
+ begin
80
+ unit = I18n.translate('number.currency.format.unit')
81
+ options = { :unit => "<span class=\"currency_sign\">#{HTMLEntities.new.encode(unit, :named)}</span>" }
82
+ options[:precision] = precision if precision
83
+ number_to_currency(amount, options)
84
+ rescue FrozenError => e
85
+ # Fallback for frozen hash issues
86
+ format_string = precision == 0 ? '%.0f' : '%.2f'
87
+ "<span class=\"currency_sign\">$</span>#{sprintf(format_string, amount.to_f)}"
88
+ end
50
89
  end
51
90
 
52
- def money_with_code(amount)
53
- %{#{ money(amount) } <span class="currency_code">#{ currency['code'] }</span>}
91
+ def money_with_code(amount, precision = nil)
92
+ begin
93
+ # Format number without currency unit, respecting locale's number formatting rules
94
+ number_locale = normalize_currency_locale_for_formatting(currency['locale'])
95
+ options = { :unit => '', :format => '%n', :locale => number_locale }
96
+ options[:precision] = precision if precision
97
+ formatted_number = number_to_currency(amount, options).to_s.strip
98
+
99
+ # Use locale-specific format string to ensure proper currency code placement
100
+ # Keep original locale for bigcartel-currency-locales gem compatibility
101
+ format_string = I18n.translate('number.currency.format.format',
102
+ :locale => currency['locale'],
103
+ :default => '%u%n')
104
+
105
+ code_span = "<span class=\"currency_code\">#{currency['code']}</span>"
106
+ format_string.gsub('%n', formatted_number).gsub('%u', code_span)
107
+ rescue => e
108
+ # Fallback for frozen hash or I18n issues
109
+ format_string = precision == 0 ? '%.0f' : '%.2f'
110
+ "<span class=\"currency_code\">#{currency['code']}</span>#{sprintf(format_string, amount.to_f)}"
111
+ end
54
112
  end
55
113
 
56
114
  def money_with_sign_and_code(amount)
57
- %{#{ money_with_sign(amount) } <span class="currency_code">#{ currency['code'] }</span>}
115
+ %{#{ money_with_sign(amount).to_s } <span class="currency_code">#{ currency['code'] }</span>}
116
+ end
117
+
118
+ # Liquid 5.x compatibility: ensure proper JSON output
119
+ def to_json(input)
120
+ # Handle Liquid Drops specially - use as_json for serialization
121
+ if input.respond_to?(:as_json)
122
+ JSON.generate(input.as_json)
123
+ else
124
+ JSON.generate(input)
125
+ end
58
126
  end
59
127
 
60
128
  # Shipping Filters
@@ -137,10 +205,104 @@ module Dugway
137
205
 
138
206
  private
139
207
 
208
+ def currency
209
+ @context.registers[:currency]
210
+ end
211
+
140
212
  def contact_tab_index
141
213
  @contact_tab_index ||= 0
142
214
  @contact_tab_index += 1
143
215
  end
216
+
217
+ def format_price_range(min_price, max_price, money_format)
218
+ case money_format
219
+ when "sign"
220
+ format_range_with_sign(min_price, max_price)
221
+ when "code"
222
+ format_range_with_code(min_price, max_price)
223
+ when "sign_rounded"
224
+ format_range_with_sign(min_price.ceil, max_price.ceil, 0)
225
+ when "code_rounded"
226
+ format_range_with_code(min_price.ceil, max_price.ceil, 0)
227
+ when "rounded"
228
+ format_range_plain(min_price.ceil, max_price.ceil, 0)
229
+ else
230
+ format_range_plain(min_price, max_price)
231
+ end
232
+ end
233
+
234
+ def format_range_with_sign(min_price, max_price, precision = nil)
235
+ # Sign on both prices for clarity
236
+ # US: $10.00 - $20.00 | FR: 10,00 € - 20,00 €
237
+ min_formatted = money_with_sign(min_price, precision)
238
+ max_formatted = money_with_sign(max_price, precision)
239
+ "#{min_formatted} - #{max_formatted}"
240
+ end
241
+
242
+ def format_range_with_code(min_price, max_price, precision = nil)
243
+ # Single code in locale-appropriate position
244
+ # US: $10.00 - 20.00 USD | FR: 10,00 - 20,00 EUR
245
+ code_span = "<span class=\"currency_code\">#{currency['code']}</span>"
246
+ range_string = build_number_range(min_price, max_price, precision)
247
+ apply_currency_format(range_string, code_span)
248
+ end
249
+
250
+ def format_range_plain(min_price, max_price, precision = nil)
251
+ # No currency indicators
252
+ begin
253
+ options = { :unit => '', :format => '%n' }
254
+ options[:precision] = precision if precision
255
+ min_formatted = number_to_currency(min_price, options).to_s.strip
256
+ max_formatted = number_to_currency(max_price, options).to_s.strip
257
+ "#{min_formatted} - #{max_formatted}"
258
+ rescue FrozenError => e
259
+ # Fallback for frozen hash issues
260
+ format_string = precision == 0 ? '%.0f' : '%.2f'
261
+ "#{sprintf(format_string, min_price.to_f)} - #{sprintf(format_string, max_price.to_f)}"
262
+ end
263
+ end
264
+
265
+ def build_number_range(min_price, max_price, precision = nil)
266
+ min_formatted = format_number_plain(min_price, precision)
267
+ max_formatted = format_number_plain(max_price, precision)
268
+ "#{min_formatted} - #{max_formatted}"
269
+ end
270
+
271
+ def format_number_plain(amount, precision = nil)
272
+ begin
273
+ number_locale = normalize_currency_locale_for_formatting(currency['locale'])
274
+ options = { :unit => '', :format => '%n', :locale => number_locale }
275
+ options[:precision] = precision if precision
276
+ number_to_currency(amount, options).to_s.strip
277
+ rescue FrozenError => e
278
+ format_string = precision == 0 ? '%.0f' : '%.2f'
279
+ sprintf(format_string, amount.to_f)
280
+ end
281
+ end
282
+
283
+ def apply_currency_format(range_string, currency_span)
284
+ # Use I18n to get locale-specific format like storefront does
285
+ begin
286
+ # Keep original locale for bigcartel-currency-locales gem compatibility
287
+ format_string = I18n.translate('number.currency.format.format',
288
+ locale: currency['locale'],
289
+ default: '%u%n')
290
+ format_string.gsub('%n', range_string).gsub('%u', currency_span)
291
+ rescue
292
+ # Fallback if I18n fails
293
+ "#{range_string} #{currency_span}"
294
+ end
295
+ end
296
+
297
+ def normalize_currency_locale_for_formatting(locale)
298
+ # Handle invalid 'eu' locale from production system for number formatting only
299
+ case locale
300
+ when 'eu'
301
+ 'de-DE' # Use German locale for EUR number formatting (1.200,99 style)
302
+ else
303
+ locale
304
+ end
305
+ end
144
306
  end
145
307
  end
146
308
  end
@@ -2,6 +2,7 @@ module Dugway
2
2
  module Filters
3
3
  module FontFilters
4
4
  def font_family(font_name)
5
+ return '' if font_name.nil? || font_name.to_s.strip.empty?
5
6
  ThemeFont.find_family_by_name(font_name)
6
7
  end
7
8
  end