pagy 9.3.5 → 43.0.0.rc2

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 (168) hide show
  1. checksums.yaml +4 -4
  2. data/apps/calendar.ru +547 -551
  3. data/apps/demo.ru +221 -178
  4. data/apps/index.rb +3 -1
  5. data/apps/keynav.ru +258 -0
  6. data/apps/{keyset_ar.ru → keyset.ru} +27 -32
  7. data/apps/{keyset_s.ru → keyset_sequel.ru} +24 -29
  8. data/apps/rails.ru +51 -48
  9. data/apps/repro.ru +46 -43
  10. data/bin/pagy +20 -21
  11. data/config/pagy.rb +35 -207
  12. data/javascripts/ai_widget.js +76 -0
  13. data/javascripts/pagy.js +151 -0
  14. data/javascripts/pagy.js.map +10 -0
  15. data/javascripts/pagy.min.js +1 -4
  16. data/javascripts/pagy.mjs +111 -63
  17. data/javascripts/wand.js +1166 -0
  18. data/lib/pagy/classes/calendar/calendar.rb +98 -0
  19. data/lib/pagy/{calendar → classes/calendar}/day.rb +7 -11
  20. data/lib/pagy/{calendar → classes/calendar}/month.rb +5 -10
  21. data/lib/pagy/{calendar → classes/calendar}/quarter.rb +10 -15
  22. data/lib/pagy/classes/calendar/unit.rb +92 -0
  23. data/lib/pagy/{calendar → classes/calendar}/week.rb +5 -9
  24. data/lib/pagy/{calendar → classes/calendar}/year.rb +5 -6
  25. data/lib/pagy/classes/exceptions.rb +33 -0
  26. data/lib/pagy/classes/keyset/active_record.rb +11 -0
  27. data/lib/pagy/classes/keyset/adapters/active_record.rb +50 -0
  28. data/lib/pagy/classes/keyset/adapters/sequel.rb +63 -0
  29. data/lib/pagy/classes/keyset/keynav/active_record.rb +13 -0
  30. data/lib/pagy/classes/keyset/keynav/sequel.rb +13 -0
  31. data/lib/pagy/classes/keyset/keynav.rb +77 -0
  32. data/lib/pagy/classes/keyset/keyset.rb +126 -0
  33. data/lib/pagy/classes/keyset/sequel.rb +11 -0
  34. data/lib/pagy/classes/offset/countless.rb +56 -0
  35. data/lib/pagy/classes/offset/offset.rb +54 -0
  36. data/lib/pagy/classes/offset/search.rb +38 -0
  37. data/lib/pagy/classes/request.rb +36 -0
  38. data/lib/pagy/modules/abilities/configurable.rb +36 -0
  39. data/lib/pagy/modules/abilities/linkable.rb +59 -0
  40. data/lib/pagy/modules/abilities/rangeable.rb +15 -0
  41. data/lib/pagy/modules/abilities/shiftable.rb +12 -0
  42. data/lib/pagy/{b64.rb → modules/b64.rb} +3 -7
  43. data/lib/pagy/modules/console.rb +38 -0
  44. data/lib/pagy/modules/i18n/i18n.rb +48 -0
  45. data/lib/pagy/modules/i18n/p11n/arabic.rb +29 -0
  46. data/lib/pagy/modules/i18n/p11n/east_slavic.rb +26 -0
  47. data/lib/pagy/modules/i18n/p11n/one_other.rb +15 -0
  48. data/lib/pagy/modules/i18n/p11n/one_upto_two_other.rb +15 -0
  49. data/lib/pagy/modules/i18n/p11n/other.rb +13 -0
  50. data/lib/pagy/modules/i18n/p11n/polish.rb +26 -0
  51. data/lib/pagy/modules/i18n/p11n/west_slavic.rb +22 -0
  52. data/lib/pagy/modules/i18n/p11n.rb +16 -0
  53. data/lib/pagy/modules/searcher.rb +20 -0
  54. data/lib/pagy/toolbox/helpers/anchor_tags.rb +25 -0
  55. data/lib/pagy/toolbox/helpers/bootstrap/input_nav_js.rb +24 -0
  56. data/lib/pagy/toolbox/helpers/bootstrap/previous_next_html.rb +18 -0
  57. data/lib/pagy/toolbox/helpers/bootstrap/series_nav.rb +29 -0
  58. data/lib/pagy/toolbox/helpers/bootstrap/series_nav_js.rb +21 -0
  59. data/lib/pagy/toolbox/helpers/bulma/input_nav_js.rb +21 -0
  60. data/lib/pagy/toolbox/helpers/bulma/previous_next_html.rb +19 -0
  61. data/lib/pagy/toolbox/helpers/bulma/series_nav.rb +28 -0
  62. data/lib/pagy/toolbox/helpers/bulma/series_nav_js.rb +20 -0
  63. data/lib/pagy/toolbox/helpers/data_hash.rb +26 -0
  64. data/lib/pagy/toolbox/helpers/headers_hash.rb +20 -0
  65. data/lib/pagy/toolbox/helpers/info_tag.rb +26 -0
  66. data/lib/pagy/toolbox/helpers/input_nav_js.rb +19 -0
  67. data/lib/pagy/toolbox/helpers/limit_tag_js.rb +23 -0
  68. data/lib/pagy/toolbox/helpers/loader.rb +33 -0
  69. data/lib/pagy/toolbox/helpers/page_url.rb +23 -0
  70. data/lib/pagy/toolbox/helpers/series_nav.rb +29 -0
  71. data/lib/pagy/toolbox/helpers/series_nav_js.rb +19 -0
  72. data/lib/pagy/toolbox/helpers/support/a_lambda.rb +34 -0
  73. data/lib/pagy/toolbox/helpers/support/data_pagy_attribute.rb +15 -0
  74. data/lib/pagy/toolbox/helpers/support/nav_aria_label_attribute.rb +12 -0
  75. data/lib/pagy/toolbox/helpers/support/series.rb +38 -0
  76. data/lib/pagy/toolbox/helpers/support/wrap_input_nav_js.rb +20 -0
  77. data/lib/pagy/toolbox/helpers/support/wrap_series_nav.rb +17 -0
  78. data/lib/pagy/toolbox/helpers/support/wrap_series_nav_js.rb +36 -0
  79. data/lib/pagy/toolbox/helpers/urls_hash.rb +13 -0
  80. data/lib/pagy/toolbox/paginators/calendar.rb +30 -0
  81. data/lib/pagy/toolbox/paginators/countless.rb +25 -0
  82. data/lib/pagy/toolbox/paginators/elasticsearch_rails.rb +32 -0
  83. data/lib/pagy/toolbox/paginators/keynav_js.rb +29 -0
  84. data/lib/pagy/toolbox/paginators/keyset.rb +18 -0
  85. data/lib/pagy/toolbox/paginators/meilisearch.rb +32 -0
  86. data/lib/pagy/toolbox/paginators/method.rb +26 -0
  87. data/lib/pagy/toolbox/paginators/offset.rb +33 -0
  88. data/lib/pagy/toolbox/paginators/searchkick.rb +32 -0
  89. data/lib/pagy.rb +59 -97
  90. data/locales/ar.yml +9 -6
  91. data/locales/be.yml +9 -6
  92. data/locales/bg.yml +9 -6
  93. data/locales/bs.yml +9 -6
  94. data/locales/ca.yml +9 -6
  95. data/locales/ckb.yml +8 -6
  96. data/locales/cs.yml +9 -6
  97. data/locales/da.yml +9 -6
  98. data/locales/de.yml +9 -6
  99. data/locales/dz.yml +9 -6
  100. data/locales/en.yml +9 -6
  101. data/locales/es.yml +9 -6
  102. data/locales/fr.yml +9 -6
  103. data/locales/hr.yml +9 -6
  104. data/locales/id.yml +9 -6
  105. data/locales/it.yml +9 -6
  106. data/locales/ja.yml +9 -6
  107. data/locales/km.yml +9 -6
  108. data/locales/ko.yml +9 -6
  109. data/locales/nb.yml +9 -6
  110. data/locales/nl.yml +9 -6
  111. data/locales/nn.yml +9 -6
  112. data/locales/pl.yml +9 -6
  113. data/locales/pt-BR.yml +10 -7
  114. data/locales/pt.yml +10 -7
  115. data/locales/ru.yml +9 -6
  116. data/locales/sk.yml +26 -0
  117. data/locales/sr.yml +9 -6
  118. data/locales/sv-SE.yml +9 -6
  119. data/locales/sv.yml +9 -6
  120. data/locales/sw.yml +12 -9
  121. data/locales/ta.yml +12 -13
  122. data/locales/tr.yml +9 -6
  123. data/locales/uk.yml +9 -6
  124. data/locales/vi.yml +9 -6
  125. data/locales/zh-CN.yml +9 -6
  126. data/locales/zh-HK.yml +9 -6
  127. data/locales/zh-TW.yml +9 -6
  128. data/stylesheets/pagy-tailwind.css +64 -0
  129. data/stylesheets/pagy.css +63 -27
  130. metadata +113 -51
  131. data/javascripts/pagy.min.js.map +0 -10
  132. data/lib/pagy/backend.rb +0 -44
  133. data/lib/pagy/calendar/unit.rb +0 -103
  134. data/lib/pagy/calendar.rb +0 -84
  135. data/lib/pagy/console.rb +0 -23
  136. data/lib/pagy/countless.rb +0 -38
  137. data/lib/pagy/exceptions.rb +0 -25
  138. data/lib/pagy/extras/arel.rb +0 -28
  139. data/lib/pagy/extras/array.rb +0 -19
  140. data/lib/pagy/extras/bootstrap.rb +0 -97
  141. data/lib/pagy/extras/bulma.rb +0 -93
  142. data/lib/pagy/extras/calendar.rb +0 -79
  143. data/lib/pagy/extras/countless.rb +0 -32
  144. data/lib/pagy/extras/elasticsearch_rails.rb +0 -71
  145. data/lib/pagy/extras/gearbox.rb +0 -55
  146. data/lib/pagy/extras/headers.rb +0 -54
  147. data/lib/pagy/extras/i18n.rb +0 -26
  148. data/lib/pagy/extras/js_tools.rb +0 -70
  149. data/lib/pagy/extras/jsonapi.rb +0 -88
  150. data/lib/pagy/extras/keyset.rb +0 -30
  151. data/lib/pagy/extras/limit.rb +0 -63
  152. data/lib/pagy/extras/meilisearch.rb +0 -57
  153. data/lib/pagy/extras/metadata.rb +0 -42
  154. data/lib/pagy/extras/overflow.rb +0 -81
  155. data/lib/pagy/extras/pagy.rb +0 -82
  156. data/lib/pagy/extras/searchkick.rb +0 -59
  157. data/lib/pagy/extras/size.rb +0 -40
  158. data/lib/pagy/extras/standalone.rb +0 -60
  159. data/lib/pagy/extras/trim.rb +0 -29
  160. data/lib/pagy/frontend.rb +0 -99
  161. data/lib/pagy/i18n.rb +0 -166
  162. data/lib/pagy/keyset/active_record.rb +0 -44
  163. data/lib/pagy/keyset/sequel.rb +0 -57
  164. data/lib/pagy/keyset.rb +0 -118
  165. data/lib/pagy/shared_methods.rb +0 -26
  166. data/lib/pagy/url_helpers.rb +0 -26
  167. data/stylesheets/pagy.scss +0 -48
  168. data/stylesheets/pagy.tailwind.css +0 -21
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative '../../modules/b64'
5
+
6
+ class Pagy
7
+ # Implement wicked-fast keyset pagination for big data
8
+ class Keyset < Pagy
9
+ path = Pathname.new(__dir__)
10
+ autoload :ActiveRecord, path.join('active_record')
11
+ autoload :Sequel, path.join('sequel')
12
+ autoload :Keynav, path.join('keynav')
13
+
14
+ class TypeError < ::TypeError; end
15
+
16
+ # Initialize Keyset* and Keyset::Keynav* classes and subclasses
17
+ def self.new(set, **)
18
+ # Subclass instances run only the initializer
19
+ if /::(?:ActiveRecord|Sequel)$/.match?(name) # check without triggering autoload
20
+ return allocate.tap { |instance| instance.send(:initialize, set, **) }
21
+ end
22
+
23
+ subclass = if defined?(::ActiveRecord) && set.is_a?(::ActiveRecord::Relation)
24
+ self::ActiveRecord
25
+ elsif defined?(::Sequel) && set.is_a?(::Sequel::Dataset)
26
+ self::Sequel
27
+ else
28
+ raise TypeError, "expected an ActiveRecord::Relation or Sequel::Dataset; got #{set.class}"
29
+ end
30
+ subclass.new(set, **)
31
+ end
32
+
33
+ def initialize(set, **) # rubocop:disable Lint/MissingSuper
34
+ assign_options(**)
35
+ assign_and_check(limit: 1)
36
+ @set = set
37
+ @keyset = @options[:keyset] || extract_keyset
38
+ raise InternalError, 'the set must be ordered' if @keyset.empty?
39
+
40
+ assign_page
41
+ self.next
42
+ end
43
+
44
+ # Return the array of records for the current page
45
+ def records
46
+ @records ||= begin
47
+ ensure_select
48
+ fetch_records
49
+ end
50
+ end
51
+
52
+ # Return the next page (i.e., the cutoff of the current page)
53
+ def next
54
+ records
55
+ return unless @more
56
+
57
+ @next ||= B64.urlsafe_encode(extract_cutoff.to_json)
58
+ end
59
+
60
+ protected
61
+
62
+ def keyset? = true
63
+
64
+ def assign_page
65
+ return unless (@page = @options[:page])
66
+
67
+ @prior_cutoff = JSON.parse(B64.urlsafe_decode(@page))
68
+ end
69
+
70
+ def fetch_records
71
+ apply_where(compose_predicate, arguments_from(@prior_cutoff)) if @prior_cutoff
72
+ @set.limit(@limit + 1).to_a.tap do |records|
73
+ @more = records.size > @limit && !records.pop.nil?
74
+ end
75
+ end
76
+
77
+ # Compose the parameterized predicate used to extract the page records.
78
+ #
79
+ # For example, with a set like Pet.order(animal: :asc, name: :desc, id: :asc)
80
+ # it returns a union of intersections:
81
+ #
82
+ # ("pets"."animal" = :animal AND "pets"."name" = :name AND "pets"."id" > :id) OR
83
+ # ("pets"."animal" = :animal AND "pets"."name" < :name) OR
84
+ # ("pets"."animal" > :animal)
85
+ #
86
+ # When :tuple_comparison is enabled, and if the order is all :asc or all :desc,
87
+ # with a set like Pet.order(:animal, :name, :id) it returns the following string:
88
+ #
89
+ # ("pets"."animal", "pets"."name", "pets"."id") > (:animal, :name, :id)
90
+ #
91
+ def compose_predicate(prefix = nil)
92
+ operator = { asc: '>', desc: '<' }
93
+ directions = @keyset.values
94
+ identifier = quoted_identifiers(@set.model.table_name)
95
+ placeholder = @keyset.to_h { |column| [column, ":#{prefix}#{column}"] }
96
+ if @options[:tuple_comparison] && (directions.all?(:asc) || directions.all?(:desc))
97
+ "(#{identifier.values.join(', ')}) #{operator[directions.first]} (#{placeholder.values.join(', ')})"
98
+ else
99
+ keyset = @keyset.to_a
100
+ union = []
101
+ until keyset.empty?
102
+ last_column, last_direction = keyset.pop
103
+ intersection = +'('
104
+ intersection << (keyset.map { |column, _d| "#{identifier[column]} = #{placeholder[column]}" } \
105
+ << "#{identifier[last_column]} #{operator[last_direction]} #{placeholder[last_column]}").join(' AND ')
106
+ intersection << ')'
107
+ union << intersection
108
+ end
109
+ union.join(' OR ')
110
+ end
111
+ end
112
+
113
+ # Return the prefixed arguments from a cutoff
114
+ def arguments_from(cutoff, prefix = nil)
115
+ attributes = typecast(@keyset.keys.zip(cutoff).to_h)
116
+ prefix ? attributes.transform_keys { |key| :"#{prefix}#{key}" } : attributes
117
+ end
118
+
119
+ # Extract the cutoff from the last record (only called if @more)
120
+ def extract_cutoff
121
+ attributes = keyset_attributes_from(@records.last)
122
+ @options[:pre_serialize]&.(attributes)
123
+ attributes.values
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'adapters/sequel'
4
+
5
+ class Pagy
6
+ class Keyset
7
+ class Sequel < Keyset
8
+ include Adapters::Sequel
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ class Offset
5
+ # Offset pagination without a count
6
+ class Countless < Offset
7
+ def initialize(**) # rubocop:disable Lint/MissingSuper
8
+ assign_options(**)
9
+ assign_and_check(limit: 1, page: 1)
10
+ @page = upto_max_pages(@page)
11
+ assign_last
12
+ assign_offset
13
+ end
14
+
15
+ def records(collection)
16
+ return super if @options[:headless]
17
+
18
+ fetched = collection.offset(@offset).limit(@limit + 1).to_a # eager load limit + 1
19
+ finalize(fetched.size) # finalize the pagy object
20
+ fetched[0, @limit] # ignore the extra item
21
+ end
22
+
23
+ protected
24
+
25
+ def countless? = true
26
+
27
+ def upto_max_pages(value)
28
+ return value unless @options[:max_pages]
29
+
30
+ [value, @options[:max_pages]].min
31
+ end
32
+
33
+ def assign_last
34
+ return unless @options[:last]
35
+
36
+ @last = upto_max_pages(@options[:last].to_i)
37
+ end
38
+
39
+ # Finalize the instance variables based on the fetched size
40
+ def finalize(fetched_size)
41
+ return self unless in_range? { fetched_size.positive? || @page == 1 }
42
+
43
+ if @last && @page < @last # visited page
44
+ @last = @page unless fetched_size > @limit # set last if last page
45
+ else
46
+ @last = upto_max_pages(fetched_size > @limit ? @page + 1 : @page)
47
+ end
48
+ @in = [fetched_size, @limit].min
49
+ @from = @in.zero? ? 0 : @offset + 1
50
+ @to = @offset + @in
51
+ assign_previous_and_next
52
+ self
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../modules/abilities/shiftable'
4
+ require_relative '../../modules/abilities/rangeable'
5
+
6
+ class Pagy
7
+ # Implements Offset Pagination
8
+ class Offset < Pagy
9
+ DEFAULT = { page: 1 }.freeze
10
+
11
+ autoload :Countless, Pathname.new(__dir__).join('countless')
12
+
13
+ include Rangeable
14
+ include Shiftable
15
+
16
+ def initialize(**) # rubocop:disable Lint/MissingSuper
17
+ assign_options(**)
18
+ assign_and_check(limit: 1, count: 0, page: 1)
19
+ assign_last
20
+ assign_offset
21
+ return unless in_range? { @page <= @last }
22
+
23
+ @from = [@offset + 1, @count].min
24
+ @to = [@offset + @limit, @count].min
25
+ @in = [@to - @from + 1, @count].min
26
+ assign_previous_and_next
27
+ end
28
+
29
+ attr_reader :offset, :count, :from, :to, :in, :previous, :last
30
+
31
+ def records(collection)
32
+ collection.offset(@offset).limit(@limit)
33
+ end
34
+
35
+ protected
36
+
37
+ def offset? = true
38
+
39
+ def assign_last
40
+ @last = [(@count.to_f / @limit).ceil, 1].max
41
+ @last = @options[:max_pages] if @options[:max_pages] && @last > @options[:max_pages]
42
+ end
43
+
44
+ def assign_offset
45
+ @offset = (@limit * (@page - 1))
46
+ end
47
+
48
+ # Called by false in_range?
49
+ def assign_empty_page_variables
50
+ @in = @from = @to = 0 # options relative to the actual page
51
+ @previous = @last # @previous relative to the actual page
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ module Search
5
+ class Arguments < Array
6
+ def respond_to_missing? = true
7
+
8
+ def method_missing(*) = push(*)
9
+ end
10
+
11
+ # Collect the search arguments to pass to the actual search
12
+ def pagy_search(term = nil, **options, &block)
13
+ Arguments.new([self, term, options, block])
14
+ end
15
+ end
16
+
17
+ # Search classes do not use OFFSET for querying a DB;
18
+ # however, they use the same positional technique used by Offset
19
+ class SearchBase < Offset
20
+ DEFAULT = { search_method: :search }.freeze
21
+
22
+ def search? = true
23
+ end
24
+
25
+ class ElasticsearchRails < SearchBase
26
+ # Get the count from different versions of ElasticsearchRails
27
+ def self.total_count(results)
28
+ total = results.instance_eval { respond_to?(:raw_response) ? raw_response['hits']['total'] : response['hits']['total'] }
29
+ total.is_a?(Hash) ? total['value'] : total
30
+ end
31
+ end
32
+
33
+ class Meilisearch < SearchBase
34
+ DEFAULT = { search_method: :ms_search }.freeze
35
+ end
36
+
37
+ class Searchkick < SearchBase; end
38
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ # Decouple the reuest from the env, allowing non-rack apps to use pagy by passing a hash.
5
+ # Resolve :page and :limit, supporting the :jsonapi option. Support for URL composition.
6
+ class Request
7
+ def initialize(request, options = {})
8
+ @base_url, @path, @query, @cookie =
9
+ if request.is_a?(Hash)
10
+ request.values_at(:base_url, :path, :query, :cookie)
11
+ else
12
+ [request.base_url, request.path, request.GET, request.cookies['pagy']]
13
+ end
14
+ @jsonapi = @query['page'] && options[:jsonapi]
15
+ raise JsonapiReservedParamError, @query['page'] if @jsonapi && !@query['page'].respond_to?(:fetch)
16
+ end
17
+
18
+ attr_reader :base_url, :path, :query, :cookie
19
+
20
+ def resolve_page(options, force_integer: true)
21
+ page_key = options[:page_key] || DEFAULT[:page_key]
22
+ page = @jsonapi ? @query['page'][page_key] : @query[page_key]
23
+ page = nil if page == '' # fix for app-generated queries like ?page=
24
+ force_integer ? (page || 1).to_i : page
25
+ end
26
+
27
+ def resolve_limit(options)
28
+ limit_key = options[:limit_key] || DEFAULT[:limit_key]
29
+ return options[:limit] || DEFAULT[:limit] \
30
+ unless options[:client_max_limit] &&
31
+ (requested_limit = @jsonapi ? @query['page'][limit_key] : @query[limit_key])
32
+
33
+ [requested_limit.to_i, options[:client_max_limit]].min
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ # Add configurstion methods
5
+ module Configurable
6
+ # Sync the pagy javascript targets
7
+ def sync_javascript(destination, *targets)
8
+ names = %w[pagy.mjs pagy.js pagy.js.map pagy.min.js]
9
+ targets = names if targets.empty?
10
+ targets.each { |filename| FileUtils.cp(ROOT.join('javascripts', filename), destination) }
11
+ (names - targets).each { |filename| FileUtils.rm_f(File.join(destination, filename)) }
12
+ end
13
+
14
+ # Generate the script and style tags to help development
15
+ def dev_tools(wand_scale: 1)
16
+ <<~HTML
17
+ <script id="pagy-ai-widget">
18
+ document.addEventListener('wand-positioned', #{ROOT.join('javascripts/ai_widget.js').read});
19
+ </script>
20
+ <script id="pagy-wand" data-scale="#{wand_scale}">
21
+ #{ROOT.join('javascripts/wand.js').read}
22
+ </script>
23
+ <style id="pagy-wand-default">
24
+ #{ROOT.join('stylesheets/pagy.css').read}
25
+ </style>
26
+ HTML
27
+ end
28
+
29
+ # Setup pagy for using the i18n gem
30
+ def translate_with_the_slower_i18n_gem!
31
+ send(:remove_const, :I18n)
32
+ send(:const_set, :I18n, ::I18n)
33
+ ::I18n.load_path += Dir[ROOT.join('locales/*.yml')]
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ class Pagy
6
+ # Provide the helpers to handle the url and anchor
7
+ module Linkable
8
+ module QueryUtils
9
+ module_function
10
+
11
+ # Extracted from Rack::Utils and reformatted for rubocop
12
+ # Add the 'unescaped' param, and use it for simple and safe url-templating.
13
+ # All string keyed hashes
14
+ def build_nested_query(value, prefix = nil, unescaped = [])
15
+ case value
16
+ when Array
17
+ value.map { |v| build_nested_query(v, "#{prefix}[]", unescaped) }.join('&')
18
+ when Hash
19
+ value.map do |k, v|
20
+ new_k = prefix ? "#{prefix}[#{escape(k)}]" : escape(k)
21
+ unescaped[unescaped.find_index(k)] = new_k if unescaped.size.positive? && new_k != k && unescaped.include?(k)
22
+ build_nested_query(v, new_k, unescaped)
23
+ end.delete_if(&:empty?).join('&')
24
+ when nil
25
+ escape(prefix)
26
+ else
27
+ raise ArgumentError, 'value must be a Hash' if prefix.nil?
28
+ return "#{escape(prefix)}=#{value}" if unescaped.include?(prefix)
29
+
30
+ "#{escape(prefix)}=#{escape(value)}"
31
+ end
32
+ end
33
+
34
+ def escape(str)
35
+ URI.encode_www_form_component(str)
36
+ end
37
+ end
38
+
39
+ protected
40
+
41
+ # Return the URL for the page, relying on the Pagy::Request
42
+ def compose_page_url(page, limit_token: nil, **options)
43
+ jsonapi, page_key, limit_key, limit, client_max_limit, querify, absolute, path, fragment =
44
+ @options.merge(options)
45
+ .values_at(:jsonapi, :page_key, :limit_key, :limit, :client_max_limit, :querify, :absolute, :path, :fragment)
46
+ query = @request.query.clone(freeze: false)
47
+ query.delete(jsonapi ? 'page' : page_key)
48
+ factors = {}.tap do |h|
49
+ h[page_key] = countless? ? "#{page || 1}+#{@last}" : page
50
+ h[limit_key] = limit_token || limit if client_max_limit
51
+ end.compact # No empty params
52
+ query.merge!(jsonapi ? { 'page' => factors } : factors) if factors.size.positive?
53
+ querify&.(query) # Must modify the query: the returned value is ignored
54
+ query_string = QueryUtils.build_nested_query(query, nil, [page_key, limit_key])
55
+ query_string = "?#{query_string}" unless query_string.empty?
56
+ "#{@request.base_url if absolute}#{path || @request.path}#{query_string}#{fragment}"
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ # Add method supporting range checking, range error and rescue
5
+ module Rangeable
6
+ # Check if in range
7
+ def in_range?
8
+ return @in_range if defined?(@in_range) || (@in_range = yield)
9
+ raise RangeError.new(self, :page, "in 1..#{@last}", @page) if @options[:raise_range_error]
10
+
11
+ assign_empty_page_variables
12
+ false
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ module Shiftable
5
+ protected
6
+
7
+ def assign_previous_and_next
8
+ @previous = @page - 1 unless @page == 1
9
+ @next = @page + 1 unless @page == @last
10
+ end
11
+ end
12
+ end
@@ -1,17 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Pagy # :nodoc:
3
+ class Pagy
4
4
  # Cheap Base64 specialized methods to avoid dependencies
5
5
  module B64
6
6
  module_function
7
7
 
8
- def encode(bin)
9
- [bin].pack('m0')
10
- end
8
+ def encode(bin) = [bin].pack('m0')
11
9
 
12
- def decode(str)
13
- str.unpack1('m0')
14
- end
10
+ def decode(str) = str.unpack1('m0')
15
11
 
16
12
  def urlsafe_encode(bin)
17
13
  str = encode(bin)
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ # Provide a ready to use pagy environment when included in irb/rails console
5
+ module Console
6
+ class Request
7
+ attr_accessor :base_url, :path, :params
8
+
9
+ def initialize
10
+ @base_url = 'http://www.example.com'
11
+ @path = '/path'
12
+ @params = { example: '123' }
13
+ end
14
+
15
+ def GET = @params # rubocop:disable Naming/MethodName
16
+
17
+ def cookies = {}
18
+ end
19
+
20
+ class Collection < Array
21
+ def initialize(arr = Array(1..1000))
22
+ super
23
+ @collection = clone
24
+ end
25
+
26
+ def offset(value) = tap { @collection = self[value..] }
27
+ def limit(value) = @collection[0, value]
28
+ def count(*) = size
29
+ end
30
+
31
+ include Method
32
+
33
+ # Direct reference to request.params via a method
34
+ def params = request.params
35
+ def request = @request ||= Request.new
36
+ def collection = Collection
37
+ end
38
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require_relative 'p11n'
5
+
6
+ class Pagy
7
+ # Pagy i18n implementation, compatible with the I18n gem, just a lot faster and lighter
8
+ module I18n
9
+ extend self
10
+
11
+ def pathnames = @pathnames ||= [ROOT.join('locales')]
12
+ def locales = @locales ||= {}
13
+
14
+ # Store the variable for the duration of a single request
15
+ def locale=(value)
16
+ Thread.current[:pagy_locale] = value
17
+ end
18
+
19
+ def locale = Thread.current[:pagy_locale] || 'en'
20
+
21
+ # Translate and pluralize the key with the locale entries
22
+ def translate(key, **options)
23
+ data, p11n = locales[locale] || self.load
24
+ translation = data[key] || (options[:count] && data[key += ".#{p11n.plural_for(options[:count])}"]) \
25
+ or return %([translation missing: "#{key}"])
26
+ translation.gsub(/%{[^}]+?}/) { |match| options[:"#{match[2..-2]}"] || match }
27
+ end
28
+
29
+ private
30
+
31
+ def load
32
+ path = pathnames.reverse.map { |p| p.join("#{locale}.yml") }.find(&:exist?)
33
+ raise Errno::ENOENT, "missing dictionary file for #{locale.inspect} locale" unless path
34
+
35
+ dictionary = YAML.load_file(path)[locale]
36
+ p11n = dictionary['pagy'].delete('p11n')
37
+ locales[locale] = [flatten_to_dot_keys(dictionary), Object.const_get("Pagy::I18n::P11n::#{p11n}")]
38
+ end
39
+
40
+ # Create a flat hash with dotted notation keys
41
+ # e.g. { 'a' => { 'b' => {'c' => 3, 'd' => 4 }}} -> { 'a.b.c' => 3, 'a.b.d' => 4 }
42
+ def flatten_to_dot_keys(initial, prefix = '')
43
+ initial.reduce({}) do |hash, (key, value)|
44
+ hash.merge!(value.is_a?(Hash) ? flatten_to_dot_keys(value, "#{prefix}#{key}.") : { "#{prefix}#{key}" => value })
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ module I18n
5
+ module P11n
6
+ module Arabic
7
+ module_function
8
+
9
+ def plural_for(n = 0)
10
+ mod100 = n % 100
11
+ case
12
+ when n == 0 # rubocop:disable Style/NumericPredicate
13
+ :zero
14
+ when n == 1
15
+ :one
16
+ when n == 2
17
+ :two
18
+ when (3..10).to_a.include?(mod100)
19
+ :few
20
+ when (11..99).to_a.include?(mod100)
21
+ :many
22
+ else
23
+ :other
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ module I18n
5
+ module P11n
6
+ module EastSlavic
7
+ module_function
8
+
9
+ def plural_for(n = 0)
10
+ mod10 = n % 10
11
+ mod100 = n % 100
12
+ case
13
+ when mod10 == 1 && mod100 != 11
14
+ :one
15
+ when (2..4).to_a.include?(mod10) && !(12..14).to_a.include?(mod100)
16
+ :few
17
+ when mod10 == 0 || (5..9).to_a.include?(mod10) || (11..14).to_a.include?(mod100) # rubocop:disable Style/NumericPredicate
18
+ :many
19
+ else
20
+ :other
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ module I18n
5
+ module P11n
6
+ module OneOther
7
+ module_function
8
+
9
+ def plural_for(n = 0)
10
+ n == 1 ? :one : :other
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ module I18n
5
+ module P11n
6
+ module OneUptoTwoOther
7
+ module_function
8
+
9
+ def plural_for(n = 0)
10
+ n >= 0 && n < 2 ? :one : :other
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ module I18n
5
+ module P11n
6
+ module Other
7
+ module_function
8
+
9
+ def plural_for(*) = :other
10
+ end
11
+ end
12
+ end
13
+ end