dugway 0.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.
Files changed (125) hide show
  1. data/.gitignore +7 -0
  2. data/.travis.yml +3 -0
  3. data/Gemfile +2 -0
  4. data/README.md +151 -0
  5. data/Rakefile +12 -0
  6. data/bin/dugway +7 -0
  7. data/dugway.gemspec +43 -0
  8. data/lib/dugway.rb +66 -0
  9. data/lib/dugway/application.rb +147 -0
  10. data/lib/dugway/cart.rb +144 -0
  11. data/lib/dugway/cli.rb +22 -0
  12. data/lib/dugway/cli/build.rb +56 -0
  13. data/lib/dugway/cli/create.rb +71 -0
  14. data/lib/dugway/cli/server.rb +37 -0
  15. data/lib/dugway/cli/templates/Gemfile.tt +2 -0
  16. data/lib/dugway/cli/templates/config.tt +24 -0
  17. data/lib/dugway/cli/templates/gitignore.tt +9 -0
  18. data/lib/dugway/cli/templates/source/cart.html +40 -0
  19. data/lib/dugway/cli/templates/source/checkout.html +33 -0
  20. data/lib/dugway/cli/templates/source/contact.html +37 -0
  21. data/lib/dugway/cli/templates/source/home.html +25 -0
  22. data/lib/dugway/cli/templates/source/images/badge.png +0 -0
  23. data/lib/dugway/cli/templates/source/javascripts/cart.js.coffee +7 -0
  24. data/lib/dugway/cli/templates/source/javascripts/product.js.coffee +7 -0
  25. data/lib/dugway/cli/templates/source/layout.html +101 -0
  26. data/lib/dugway/cli/templates/source/maintenance.html +20 -0
  27. data/lib/dugway/cli/templates/source/product.html +65 -0
  28. data/lib/dugway/cli/templates/source/products.html +25 -0
  29. data/lib/dugway/cli/templates/source/screenshot.jpg +0 -0
  30. data/lib/dugway/cli/templates/source/scripts.js +19 -0
  31. data/lib/dugway/cli/templates/source/settings.json +71 -0
  32. data/lib/dugway/cli/templates/source/styles.css +27 -0
  33. data/lib/dugway/cli/templates/source/stylesheets/cart.css.sass +38 -0
  34. data/lib/dugway/cli/templates/source/stylesheets/layout.css.sass +103 -0
  35. data/lib/dugway/cli/templates/source/stylesheets/product.css.sass +19 -0
  36. data/lib/dugway/cli/templates/source/stylesheets/products.css.sass +20 -0
  37. data/lib/dugway/cli/templates/source/stylesheets/vendor/normalize.css +396 -0
  38. data/lib/dugway/cli/templates/source/success.html +5 -0
  39. data/lib/dugway/controller.rb +148 -0
  40. data/lib/dugway/data/locales/cs.yml +26 -0
  41. data/lib/dugway/data/locales/da.yml +26 -0
  42. data/lib/dugway/data/locales/en-AU.yml +26 -0
  43. data/lib/dugway/data/locales/en-GB.yml +26 -0
  44. data/lib/dugway/data/locales/en-PH.yml +27 -0
  45. data/lib/dugway/data/locales/en-US.yml +26 -0
  46. data/lib/dugway/data/locales/es-MX.yml +26 -0
  47. data/lib/dugway/data/locales/eu.yml +26 -0
  48. data/lib/dugway/data/locales/gsw-CH.yml +26 -0
  49. data/lib/dugway/data/locales/hu.yml +26 -0
  50. data/lib/dugway/data/locales/il.yml +26 -0
  51. data/lib/dugway/data/locales/ja.yml +30 -0
  52. data/lib/dugway/data/locales/ms-MY.yml +26 -0
  53. data/lib/dugway/data/locales/nb.yml +22 -0
  54. data/lib/dugway/data/locales/pl.yml +27 -0
  55. data/lib/dugway/data/locales/pt-BR.yml +26 -0
  56. data/lib/dugway/data/locales/sv-SE.yml +26 -0
  57. data/lib/dugway/data/locales/th.yml +26 -0
  58. data/lib/dugway/data/locales/tr.yml +29 -0
  59. data/lib/dugway/data/locales/zh-TW.yml +30 -0
  60. data/lib/dugway/data/theme_fonts.yml +151 -0
  61. data/lib/dugway/extensions/time.rb +15 -0
  62. data/lib/dugway/liquid/drops/account_drop.rb +17 -0
  63. data/lib/dugway/liquid/drops/artist_drop.rb +9 -0
  64. data/lib/dugway/liquid/drops/artists_drop.rb +13 -0
  65. data/lib/dugway/liquid/drops/base_drop.rb +59 -0
  66. data/lib/dugway/liquid/drops/cart_drop.rb +13 -0
  67. data/lib/dugway/liquid/drops/cart_item_drop.rb +21 -0
  68. data/lib/dugway/liquid/drops/categories_drop.rb +13 -0
  69. data/lib/dugway/liquid/drops/category_drop.rb +9 -0
  70. data/lib/dugway/liquid/drops/contact_drop.rb +29 -0
  71. data/lib/dugway/liquid/drops/country_drop.rb +6 -0
  72. data/lib/dugway/liquid/drops/currency_drop.rb +6 -0
  73. data/lib/dugway/liquid/drops/image_drop.rb +6 -0
  74. data/lib/dugway/liquid/drops/page_drop.rb +13 -0
  75. data/lib/dugway/liquid/drops/pages_drop.rb +9 -0
  76. data/lib/dugway/liquid/drops/product_drop.rb +101 -0
  77. data/lib/dugway/liquid/drops/product_option_drop.rb +27 -0
  78. data/lib/dugway/liquid/drops/products_drop.rb +75 -0
  79. data/lib/dugway/liquid/drops/shipping_option_drop.rb +13 -0
  80. data/lib/dugway/liquid/drops/theme_drop.rb +23 -0
  81. data/lib/dugway/liquid/filters/comparison_filters.rb +48 -0
  82. data/lib/dugway/liquid/filters/core_filters.rb +138 -0
  83. data/lib/dugway/liquid/filters/default_pagination.rb +35 -0
  84. data/lib/dugway/liquid/filters/font_filters.rb +9 -0
  85. data/lib/dugway/liquid/filters/url_filters.rb +66 -0
  86. data/lib/dugway/liquid/filters/util_filters.rb +119 -0
  87. data/lib/dugway/liquid/tags/checkout_form.rb +9 -0
  88. data/lib/dugway/liquid/tags/get.rb +58 -0
  89. data/lib/dugway/liquid/tags/paginate.rb +129 -0
  90. data/lib/dugway/liquifier.rb +122 -0
  91. data/lib/dugway/logger.rb +16 -0
  92. data/lib/dugway/request.rb +36 -0
  93. data/lib/dugway/store.rb +166 -0
  94. data/lib/dugway/template.rb +44 -0
  95. data/lib/dugway/theme.rb +145 -0
  96. data/lib/dugway/theme_font.rb +68 -0
  97. data/lib/dugway/version.rb +3 -0
  98. data/spec/fixtures/store/page/about-us.js +1 -0
  99. data/spec/fixtures/store/products.js +1 -0
  100. data/spec/fixtures/store/store.js +1 -0
  101. data/spec/fixtures/theme/cart.html +33 -0
  102. data/spec/fixtures/theme/checkout.html +31 -0
  103. data/spec/fixtures/theme/contact.html +37 -0
  104. data/spec/fixtures/theme/home.html +38 -0
  105. data/spec/fixtures/theme/images/bc_badge.png +0 -0
  106. data/spec/fixtures/theme/javascripts/one.js +3 -0
  107. data/spec/fixtures/theme/javascripts/two.js.coffee +2 -0
  108. data/spec/fixtures/theme/layout.html +77 -0
  109. data/spec/fixtures/theme/maintenance.html +17 -0
  110. data/spec/fixtures/theme/product.html +73 -0
  111. data/spec/fixtures/theme/products.html +41 -0
  112. data/spec/fixtures/theme/screenshot.jpg +0 -0
  113. data/spec/fixtures/theme/scripts.js +2 -0
  114. data/spec/fixtures/theme/settings.json +55 -0
  115. data/spec/fixtures/theme/styles.css +4 -0
  116. data/spec/fixtures/theme/stylesheets/one.css +3 -0
  117. data/spec/fixtures/theme/stylesheets/two.css.sass +4 -0
  118. data/spec/fixtures/theme/success.html +5 -0
  119. data/spec/spec_helper.rb +24 -0
  120. data/spec/units/dugway/request_spec.rb +206 -0
  121. data/spec/units/dugway/store_spec.rb +194 -0
  122. data/spec/units/dugway/template_spec.rb +145 -0
  123. data/spec/units/dugway/theme_font_spec.rb +136 -0
  124. data/spec/units/dugway_spec.rb +9 -0
  125. metadata +549 -0
@@ -0,0 +1,129 @@
1
+ module Dugway
2
+ module Tags
3
+ class Paginate < ::Liquid::Block
4
+ Syntax = /(\w+)\s+from\s+(#{ Liquid::QuotedFragment })\s*(by\s*(#{ Liquid::QuotedFragment }))?/
5
+
6
+ def initialize(tag_name, markup, tokens)
7
+ if markup =~ Syntax
8
+ @variable_name = $1
9
+ @collection_name = $2
10
+ @per_page = $3.present? ? $4 : nil
11
+
12
+ @attributes = { 'inner_window' => 3, 'outer_window' => 1 }
13
+
14
+ markup.scan(Liquid::TagAttributes) { |key, value|
15
+ @attributes[key] = value
16
+ }
17
+
18
+ @limit = @attributes['limit']
19
+ else
20
+ raise SyntaxError.new("Syntax Error in tag 'paginate' - Valid syntax: paginate [variable] from [collection] by [number]")
21
+ end
22
+
23
+ super
24
+ end
25
+
26
+ def render(context)
27
+ @context = context
28
+ @limit = context[@limit].present? ? context[@limit] : (@limit.present? ? @limit.to_i : nil)
29
+ @per_page = context[@per_page].present? ? context[@per_page] : (@per_page.present? ? @per_page.to_i : nil)
30
+ @order = context[@attributes['order']].present? ? context[@attributes['order']] : @attributes['order']
31
+
32
+ context.stack do
33
+ context['internal'] = {
34
+ 'per_page' => @per_page,
35
+ 'order' => @order,
36
+ 'page' => @context.registers[:params][:page] && @context.registers[:params][:page].to_i,
37
+ 'limit' => @limit
38
+ }
39
+
40
+ collection = context[@collection_name]
41
+ context[@variable_name] = collection
42
+ current_page = collection.current_page
43
+
44
+ pagination = {
45
+ 'page_size' => collection.per_page,
46
+ 'current_page' => current_page,
47
+ 'current_offset' => collection.offset
48
+ }
49
+
50
+ context['paginate'] = pagination
51
+
52
+ collection_size = collection.total_entries
53
+
54
+ raise ArgumentError.new("Cannot paginate array '#{ @collection_name }'. Not found.") if collection_size.nil?
55
+
56
+ page_count = collection.total_pages
57
+
58
+ pagination['items'] = collection_size
59
+ pagination['pages'] = page_count
60
+
61
+ pagination['previous'] = collection.previous_page.blank? ? no_link('&laquo; Previous') : link('&laquo; Previous', collection.previous_page)
62
+
63
+ pagination['next'] = collection.next_page.blank? ? no_link('Next &raquo;') : link('Next &raquo;', collection.next_page)
64
+
65
+ pagination['parts'] = []
66
+
67
+ if page_count > 1
68
+ inner_window = @attributes['inner_window'].to_i
69
+ outer_window = @attributes['outer_window'].to_i
70
+
71
+ min = current_page - inner_window
72
+ max = current_page + inner_window
73
+
74
+ # Adjust lower or upper limit if other is out of bounds
75
+ if max > page_count
76
+ min -= max - page_count
77
+ elsif min < 1
78
+ max += 1 - min
79
+ end
80
+
81
+ current = min..max
82
+ beginning = 1..(1 + outer_window)
83
+ tail = (page_count - outer_window)..page_count
84
+ visible = [current, beginning, tail].map(&:to_a).flatten
85
+ visible &= (1..page_count).to_a
86
+
87
+ hellip_break = false
88
+
89
+ 1.upto(page_count) { |page|
90
+ if page == current_page
91
+ pagination['parts'] << no_link(page)
92
+ elsif visible.include?(page)
93
+ pagination['parts'] << link(page, page)
94
+ elsif page == beginning.last + 1 || page == tail.first - 1
95
+ next if hellip_break
96
+
97
+ pagination['parts'] << no_link('&hellip;')
98
+ hellip_break = true
99
+ next
100
+ end
101
+
102
+ hellip_break = false
103
+ }
104
+ end
105
+
106
+ render_all @nodelist, context
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ def no_link(title)
113
+ { 'title' => title.to_s, 'is_link' => false }
114
+ end
115
+
116
+ def link(title, page)
117
+ { 'title' => title.to_s, 'url' => current_url + "?page=#{ page }#{ search }", 'is_link' => true }
118
+ end
119
+
120
+ def search
121
+ @context.registers[:params]['search'].present? ? "&search=#{ @context.registers[:params]['search'] }" : nil
122
+ end
123
+
124
+ def current_url
125
+ @context.registers[:path]
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,122 @@
1
+ require 'liquid'
2
+ require "#{ File.dirname(__FILE__) }/liquid/drops/base_drop"
3
+ Dir.glob("#{ File.dirname(__FILE__) }/liquid/**/*.rb").each { |file| require file }
4
+
5
+ Liquid::Template.register_filter(Dugway::Filters::UtilFilters)
6
+ Liquid::Template.register_filter(Dugway::Filters::CoreFilters)
7
+ Liquid::Template.register_filter(Dugway::Filters::DefaultPagination)
8
+ Liquid::Template.register_filter(Dugway::Filters::UrlFilters)
9
+ Liquid::Template.register_filter(Dugway::Filters::FontFilters)
10
+
11
+ Liquid::Template.register_tag(:checkoutform, Dugway::Tags::CheckoutForm)
12
+ Liquid::Template.register_tag(:get, Dugway::Tags::Get)
13
+ Liquid::Template.register_tag(:paginate, Dugway::Tags::Paginate)
14
+
15
+ module Dugway
16
+ class Liquifier
17
+ ESCAPE_CSS = {
18
+ '{{' => '"<<',
19
+ '}}' => '>>"',
20
+ '{%' => '"<',
21
+ '%}' => '">'
22
+ }
23
+
24
+ def initialize(request)
25
+ @request = request
26
+ end
27
+
28
+ def render(content, variables={})
29
+ variables.symbolize_keys!
30
+
31
+ assigns = shared_assigns
32
+ assigns['page_content'] = variables[:page_content]
33
+ assigns['page'] = Drops::PageDrop.new(variables[:page])
34
+ assigns['product'] = Drops::ProductDrop.new(variables[:product])
35
+
36
+ registers = shared_registers
37
+ registers[:category] = variables[:category]
38
+ registers[:artist] = variables[:artist]
39
+
40
+ if errors = variables.delete(:errors)
41
+ shared_context['errors'] << errors
42
+ end
43
+
44
+ context = Liquid::Context.new([ assigns, shared_context ], {}, registers)
45
+ Liquid::Template.parse(content).render!(context)
46
+ end
47
+
48
+ def self.render_styles(css)
49
+ Liquid::Template.parse(css).render!(
50
+ { 'theme' => Drops::ThemeDrop.new(Dugway.theme.customization) },
51
+ :registers => { :settings => Dugway.theme.settings }
52
+ )
53
+ end
54
+
55
+ def self.escape_styles(css)
56
+ ESCAPE_CSS.each_pair { |k,v| css.gsub!(k,v) }
57
+ css
58
+ end
59
+
60
+ def self.unescape_styles(css)
61
+ ESCAPE_CSS.each_pair { |k,v| css.gsub!(v,k) }
62
+ css
63
+ end
64
+
65
+ private
66
+
67
+ def store
68
+ Dugway.store
69
+ end
70
+
71
+ def theme
72
+ Dugway.theme
73
+ end
74
+
75
+ def cart
76
+ Dugway.cart
77
+ end
78
+
79
+ def shared_context
80
+ @shared_context ||= { 'errors' => [] }
81
+ end
82
+
83
+ def shared_assigns
84
+ {
85
+ 'store' => Drops::AccountDrop.new(store.account),
86
+ 'cart' => Drops::CartDrop.new(cart),
87
+ 'theme' => Drops::ThemeDrop.new(theme.customization),
88
+ 'pages' => Drops::PagesDrop.new(store.pages.map { |p| Drops::PageDrop.new(p) }),
89
+ 'categories' => Drops::CategoriesDrop.new(store.categories.map { |c| Drops::CategoryDrop.new(c) }),
90
+ 'artists' => Drops::ArtistsDrop.new(store.artists.map { |a| Drops::ArtistDrop.new(a) }),
91
+ 'products' => Drops::ProductsDrop.new(store.products.map { |p| Drops::ProductDrop.new(p) }),
92
+ 'contact' => Drops::ContactDrop.new,
93
+ 'head_content' => head_content,
94
+ 'bigcartel_credit' => bigcartel_credit
95
+ }
96
+ end
97
+
98
+ def shared_registers
99
+ {
100
+ :request => @request,
101
+ :path => @request.path,
102
+ :params => @request.params.with_indifferent_access,
103
+ :currency => store.currency,
104
+ :settings => theme.settings
105
+ }
106
+ end
107
+
108
+ def head_content
109
+ content = %{<meta name="generator" content="Big Cartel">}
110
+
111
+ if google_font_url = ThemeFont.google_font_url_for_theme
112
+ content << %{\n<link rel="stylesheet" type="text/css" href="#{ google_font_url }">}
113
+ end
114
+
115
+ content
116
+ end
117
+
118
+ def bigcartel_credit
119
+ '<a href="http://bigcartel.com/" title="Start your own store at Big Cartel now">Online Store by Big Cartel</a>'
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,16 @@
1
+ require 'logger'
2
+
3
+ module Dugway
4
+ class Logger < ::Logger
5
+ def initialize
6
+ Dir.mkdir(dir) unless File.exists?(dir)
7
+ super(File.join(dir, 'dugway.log'))
8
+ end
9
+
10
+ def dir
11
+ File.join(Dir.pwd, 'log')
12
+ end
13
+
14
+ alias write <<
15
+ end
16
+ end
@@ -0,0 +1,36 @@
1
+ module Dugway
2
+ class Request < Rack::Request
3
+ def params
4
+ super.update(env['rack.routing_args']).symbolize_keys
5
+ end
6
+
7
+ def page_permalink
8
+ case path
9
+ when %r{^/$}
10
+ 'home'
11
+ when %r{^/(products|category|artist)/}
12
+ 'products'
13
+ when %r{^/product/}
14
+ 'product'
15
+ else
16
+ File.basename(path[1..-1], '.*')
17
+ end
18
+ end
19
+
20
+ def extension
21
+ File.extname(path).present? ? File.extname(path) : '.html'
22
+ end
23
+
24
+ def format
25
+ params[:format] || extension[1..-1]
26
+ end
27
+
28
+ def html?
29
+ format == 'html'
30
+ end
31
+
32
+ def js?
33
+ format == 'js'
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,166 @@
1
+ require 'httparty'
2
+
3
+ module Dugway
4
+ class Store
5
+ include HTTParty
6
+
7
+ format :json
8
+ default_timeout 5
9
+ headers 'User-Agent' => "Dugway #{ Dugway::VERSION }"
10
+
11
+ def initialize(subdomain)
12
+ self.class.base_uri "http://api.bigcartel.com/#{ subdomain }"
13
+ end
14
+
15
+ def account
16
+ @account ||= get('/store.js')
17
+ end
18
+
19
+ def theme_pages
20
+ [
21
+ { 'name' => 'Home', 'permalink' => 'home', 'url' => '/', 'category' => 'theme' },
22
+ { 'name' => 'Products', 'permalink' => 'products', 'url' => '/products', 'category' => 'theme' },
23
+ { 'name' => 'Product', 'permalink' => 'product', 'url' => '/product', 'category' => 'theme' },
24
+ { 'name' => 'Cart', 'permalink' => 'cart', 'url' => '/cart', 'category' => 'theme' },
25
+ { 'name' => 'Checkout', 'permalink' => 'checkout', 'url' => '/checkout', 'category' => 'theme' },
26
+ { 'name' => 'Success', 'permalink' => 'success', 'url' => '/success', 'category' => 'theme' },
27
+ { 'name' => 'Contact', 'permalink' => 'contact', 'url' => '/contact', 'category' => 'theme' },
28
+ { 'name' => 'Maintenance', 'permalink' => 'maintenance', 'url' => '/maintenance', 'category' => 'theme' }
29
+ ]
30
+ end
31
+
32
+ def custom_pages
33
+ @custom_pages ||= begin
34
+ custom_pages = account.has_key?('pages') ? account['pages'] : []
35
+ custom_pages = custom_pages.map { |page| get("/page/#{ page['permalink'] }.js") }
36
+ end
37
+ end
38
+
39
+ def pages
40
+ @pages ||= theme_pages + custom_pages
41
+ end
42
+
43
+ def page(permalink)
44
+ lookup(permalink, pages)
45
+ end
46
+
47
+ def categories
48
+ account.has_key?('categories') ? account['categories'] : []
49
+ end
50
+
51
+ def category(permalink)
52
+ lookup(permalink, categories)
53
+ end
54
+
55
+ def category_products(permalink)
56
+ lookup_products(permalink, 'categories')
57
+ end
58
+
59
+ def artists
60
+ account.has_key?('artists') ? account['artists'] : []
61
+ end
62
+
63
+ def artist(permalink)
64
+ lookup(permalink, artists)
65
+ end
66
+
67
+ def artist_products(permalink)
68
+ lookup_products(permalink, 'artists')
69
+ end
70
+
71
+ def products
72
+ @products ||= get('/products.js')
73
+ end
74
+
75
+ def product(permalink)
76
+ lookup(permalink, products)
77
+ end
78
+
79
+ def product_and_option(option_id)
80
+ products.each { |product|
81
+ product['options'].each { |option|
82
+ if option['id'] == option_id
83
+ return product, option
84
+ end
85
+ }
86
+ }
87
+
88
+ nil
89
+ end
90
+
91
+ def previous_product(permalink)
92
+ products.each_with_index { |product, index|
93
+ if product['permalink'] == permalink && index > 0 && previous_product = products[index - 1]
94
+ return previous_product
95
+ end
96
+ }
97
+
98
+ nil
99
+ end
100
+
101
+ def next_product(permalink)
102
+ products.each_with_index { |product, index|
103
+ if product['permalink'] == permalink && (index + 1) < products.size && next_product = products[index + 1]
104
+ return next_product
105
+ end
106
+ }
107
+
108
+ nil
109
+ end
110
+
111
+ def search_products(search_terms)
112
+ products.select { |p| p['name'].downcase.include?(search_terms.downcase) || p['description'].downcase.include?(search_terms.downcase) }
113
+ end
114
+
115
+ def country
116
+ account['country']
117
+ end
118
+
119
+ def currency
120
+ account['currency']
121
+ end
122
+
123
+ def locale
124
+ case currency['code']
125
+ when 'AUD' then 'en-AU'
126
+ when 'BRL' then 'pt-BR'
127
+ when 'CAD' then 'en-US'
128
+ when 'CZK' then 'cs'
129
+ when 'DKK' then 'da'
130
+ when 'EUR' then 'eu'
131
+ when 'HKD' then 'en-US'
132
+ when 'HUF' then 'hu'
133
+ when 'ILS' then 'il'
134
+ when 'JPY' then 'ja'
135
+ when 'MYR' then 'ms-MY'
136
+ when 'MXN' then 'es-MX'
137
+ when 'TWD' then 'zh-TW'
138
+ when 'NZD' then 'en-US'
139
+ when 'NOK' then 'nb'
140
+ when 'PHP' then 'en-PH'
141
+ when 'PLN' then 'pl'
142
+ when 'GBP' then 'en-GB'
143
+ when 'SGD' then 'en-US'
144
+ when 'SEK' then 'sv-SE'
145
+ when 'CHF' then 'gsw-CH'
146
+ when 'THB' then 'th'
147
+ when 'TRY' then 'tr'
148
+ else 'en-US'
149
+ end
150
+ end
151
+
152
+ private
153
+
154
+ def get(path)
155
+ self.class.get(path).parsed_response
156
+ end
157
+
158
+ def lookup(permalink, array)
159
+ array.find { |item| item['permalink'] == permalink }.try(:dup)
160
+ end
161
+
162
+ def lookup_products(permalink, type)
163
+ products.select { |p| p[type].any? { |c| c['permalink'] == permalink }}
164
+ end
165
+ end
166
+ end