pagy 3.8.2 → 9.4.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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/apps/calendar.ru +737 -0
  4. data/apps/demo.ru +449 -0
  5. data/apps/index.rb +7 -0
  6. data/apps/keyset_ar.ru +228 -0
  7. data/apps/keyset_s.ru +220 -0
  8. data/apps/rails.ru +217 -0
  9. data/apps/repro.ru +182 -0
  10. data/bin/pagy +98 -0
  11. data/config/pagy.rb +220 -0
  12. data/javascripts/pagy.d.ts +5 -0
  13. data/javascripts/pagy.min.js +4 -0
  14. data/javascripts/pagy.min.js.map +10 -0
  15. data/javascripts/pagy.mjs +100 -0
  16. data/lib/optimist.rb +1022 -0
  17. data/lib/pagy/b64.rb +33 -0
  18. data/lib/pagy/backend.rb +30 -19
  19. data/lib/pagy/calendar/day.rb +41 -0
  20. data/lib/pagy/calendar/month.rb +42 -0
  21. data/lib/pagy/calendar/quarter.rb +49 -0
  22. data/lib/pagy/calendar/unit.rb +103 -0
  23. data/lib/pagy/calendar/week.rb +39 -0
  24. data/lib/pagy/calendar/year.rb +35 -0
  25. data/lib/pagy/calendar.rb +84 -0
  26. data/lib/pagy/console.rb +23 -0
  27. data/lib/pagy/countless.rb +27 -22
  28. data/lib/pagy/exceptions.rb +16 -13
  29. data/lib/pagy/extras/arel.rb +11 -14
  30. data/lib/pagy/extras/array.rb +12 -16
  31. data/lib/pagy/extras/bootstrap.rb +83 -41
  32. data/lib/pagy/extras/bulma.rb +79 -46
  33. data/lib/pagy/extras/calendar.rb +79 -0
  34. data/lib/pagy/extras/countless.rb +20 -25
  35. data/lib/pagy/extras/elasticsearch_rails.rb +59 -38
  36. data/lib/pagy/extras/gearbox.rb +55 -0
  37. data/lib/pagy/extras/headers.rb +38 -23
  38. data/lib/pagy/extras/i18n.rb +19 -18
  39. data/lib/pagy/extras/js_tools.rb +70 -0
  40. data/lib/pagy/extras/jsonapi.rb +88 -0
  41. data/lib/pagy/extras/keyset.rb +30 -0
  42. data/lib/pagy/extras/limit.rb +63 -0
  43. data/lib/pagy/extras/meilisearch.rb +57 -0
  44. data/lib/pagy/extras/metadata.rb +32 -27
  45. data/lib/pagy/extras/overflow.rb +61 -53
  46. data/lib/pagy/extras/pagy.rb +82 -0
  47. data/lib/pagy/extras/searchkick.rb +52 -41
  48. data/lib/pagy/extras/size.rb +40 -0
  49. data/lib/pagy/extras/standalone.rb +60 -0
  50. data/lib/pagy/extras/trim.rb +19 -13
  51. data/lib/pagy/frontend.rb +76 -51
  52. data/lib/pagy/i18n.rb +167 -0
  53. data/lib/pagy/keyset/active_record.rb +44 -0
  54. data/lib/pagy/keyset/sequel.rb +57 -0
  55. data/lib/pagy/keyset.rb +118 -0
  56. data/lib/pagy/shared_methods.rb +26 -0
  57. data/lib/pagy/url_helpers.rb +26 -0
  58. data/lib/pagy.rb +91 -37
  59. data/locales/ar.yml +29 -0
  60. data/locales/be.yml +25 -0
  61. data/locales/bg.yml +21 -0
  62. data/locales/bs.yml +25 -0
  63. data/locales/ca.yml +21 -0
  64. data/locales/ckb.yml +18 -0
  65. data/locales/cs.yml +23 -0
  66. data/locales/da.yml +21 -0
  67. data/locales/de.yml +21 -0
  68. data/locales/dz.yml +17 -0
  69. data/locales/en.yml +21 -0
  70. data/{lib/locales → locales}/es.yml +11 -12
  71. data/locales/fr.yml +21 -0
  72. data/locales/hr.yml +25 -0
  73. data/locales/id.yml +19 -0
  74. data/locales/it.yml +21 -0
  75. data/locales/ja.yml +19 -0
  76. data/locales/km.yml +19 -0
  77. data/locales/ko.yml +17 -0
  78. data/locales/nb.yml +21 -0
  79. data/locales/nl.yml +21 -0
  80. data/locales/nn.yml +21 -0
  81. data/locales/pl.yml +25 -0
  82. data/{lib/locales → locales}/pt-BR.yml +11 -12
  83. data/locales/pt.yml +21 -0
  84. data/locales/ru.yml +25 -0
  85. data/locales/sk.yml +23 -0
  86. data/locales/sr.yml +25 -0
  87. data/locales/sv-SE.yml +21 -0
  88. data/locales/sv.yml +21 -0
  89. data/locales/sw.yml +25 -0
  90. data/locales/ta.yml +21 -0
  91. data/locales/tr.yml +19 -0
  92. data/locales/uk.yml +25 -0
  93. data/locales/vi.yml +17 -0
  94. data/locales/zh-CN.yml +17 -0
  95. data/locales/zh-HK.yml +17 -0
  96. data/locales/zh-TW.yml +17 -0
  97. data/stylesheets/pagy.css +46 -0
  98. data/stylesheets/pagy.scss +48 -0
  99. data/stylesheets/pagy.tailwind.css +21 -0
  100. metadata +95 -67
  101. data/lib/config/pagy.rb +0 -170
  102. data/lib/javascripts/pagy.js +0 -106
  103. data/lib/locales/README.md +0 -35
  104. data/lib/locales/bg.yml +0 -22
  105. data/lib/locales/ca.yml +0 -22
  106. data/lib/locales/da.yml +0 -22
  107. data/lib/locales/de.yml +0 -22
  108. data/lib/locales/en.yml +0 -22
  109. data/lib/locales/fr.yml +0 -22
  110. data/lib/locales/id.yml +0 -20
  111. data/lib/locales/it.yml +0 -22
  112. data/lib/locales/ja.yml +0 -20
  113. data/lib/locales/km.yml +0 -19
  114. data/lib/locales/ko.yml +0 -20
  115. data/lib/locales/nb.yml +0 -22
  116. data/lib/locales/nl.yml +0 -22
  117. data/lib/locales/pl.yml +0 -24
  118. data/lib/locales/ru.yml +0 -24
  119. data/lib/locales/sv-SE.yml +0 -23
  120. data/lib/locales/sv.yml +0 -23
  121. data/lib/locales/tr.yml +0 -20
  122. data/lib/locales/utils/i18n.rb +0 -25
  123. data/lib/locales/utils/loader.rb +0 -34
  124. data/lib/locales/utils/p11n.rb +0 -80
  125. data/lib/locales/zh-CN.yml +0 -20
  126. data/lib/locales/zh-HK.yml +0 -20
  127. data/lib/locales/zh-TW.yml +0 -20
  128. data/lib/pagy/extras/foundation.rb +0 -57
  129. data/lib/pagy/extras/items.rb +0 -65
  130. data/lib/pagy/extras/materialize.rb +0 -59
  131. data/lib/pagy/extras/navs.rb +0 -38
  132. data/lib/pagy/extras/pagy_search.rb +0 -18
  133. data/lib/pagy/extras/semantic.rb +0 -55
  134. data/lib/pagy/extras/shared.rb +0 -53
  135. data/lib/pagy/extras/support.rb +0 -29
  136. data/lib/pagy/extras/uikit.rb +0 -62
  137. data/lib/templates/bootstrap_nav.html.erb +0 -24
  138. data/lib/templates/bootstrap_nav.html.haml +0 -34
  139. data/lib/templates/bootstrap_nav.html.slim +0 -34
  140. data/lib/templates/bulma_nav.html.erb +0 -24
  141. data/lib/templates/bulma_nav.html.haml +0 -32
  142. data/lib/templates/bulma_nav.html.slim +0 -32
  143. data/lib/templates/foundation_nav.html.erb +0 -24
  144. data/lib/templates/foundation_nav.html.haml +0 -34
  145. data/lib/templates/foundation_nav.html.slim +0 -34
  146. data/lib/templates/nav.html.erb +0 -22
  147. data/lib/templates/nav.html.haml +0 -30
  148. data/lib/templates/nav.html.slim +0 -29
  149. data/lib/templates/uikit_nav.html.erb +0 -15
  150. data/lib/templates/uikit_nav.html.haml +0 -28
  151. data/lib/templates/uikit_nav.html.slim +0 -28
  152. data/pagy.gemspec +0 -16
data/lib/pagy/i18n.rb ADDED
@@ -0,0 +1,167 @@
1
+ # See Pagy::I18n API documentation https://ddnexus.github.io/pagy/docs/api/i18n
2
+ # frozen_string_literal: true
3
+
4
+ require 'yaml'
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
+ # Pluralization rules
12
+ module P11n
13
+ # Pluralization variables
14
+ from0to1 = (0..1).to_a.freeze
15
+ from2to4 = (2..4).to_a.freeze
16
+ from3to10 = (3..10).to_a.freeze
17
+ from5to9 = (5..9).to_a.freeze
18
+ from11to14 = (11..14).to_a.freeze
19
+ from11to99 = (11..99).to_a.freeze
20
+ from12to14 = (12..14).to_a.freeze
21
+
22
+ from0to1_from5to9 = from0to1 + from5to9
23
+
24
+ # Store the proc defining each pluralization RULE
25
+ # Logic adapted from https://github.com/svenfuchs/rails-i18n
26
+ RULE = {
27
+ arabic:
28
+ lambda do |n = 0|
29
+ mod100 = n % 100
30
+ case
31
+ when n == 0 then 'zero' # rubocop:disable Style/NumericPredicate
32
+ when n == 1 then 'one'
33
+ when n == 2 then 'two'
34
+ when from3to10.include?(mod100) then 'few'
35
+ when from11to99.include?(mod100) then 'many'
36
+ else 'other'
37
+ end
38
+ end,
39
+
40
+ east_slavic:
41
+ lambda do |n = 0|
42
+ mod10 = n % 10
43
+ mod100 = n % 100
44
+ case
45
+ when mod10 == 1 && mod100 != 11 then 'one'
46
+ when from2to4.include?(mod10) && !from12to14.include?(mod100) then 'few'
47
+ when mod10 == 0 || from5to9.include?(mod10) || from11to14.include?(mod100) then 'many' # rubocop:disable Style/NumericPredicate
48
+ else 'other'
49
+ end
50
+ end,
51
+
52
+ one_other:
53
+ ->(n) { n == 1 ? 'one' : 'other' }, # default RULE
54
+
55
+ one_two_other:
56
+ lambda do |n|
57
+ case n
58
+ when 1 then 'one'
59
+ when 2 then 'two'
60
+ else 'other'
61
+ end
62
+ end,
63
+
64
+ one_upto_two_other:
65
+ ->(n) { n && n >= 0 && n < 2 ? 'one' : 'other' },
66
+
67
+ other:
68
+ ->(*) { 'other' },
69
+
70
+ polish:
71
+ lambda do |n = 0|
72
+ mod10 = n % 10
73
+ mod100 = n % 100
74
+ case
75
+ when n == 1 then 'one'
76
+ when from2to4.include?(mod10) && !from12to14.include?(mod100) then 'few'
77
+ when from0to1_from5to9.include?(mod10) || from12to14.include?(mod100) then 'many'
78
+ else 'other'
79
+ end
80
+ end,
81
+
82
+ west_slavic:
83
+ lambda do |n|
84
+ case n
85
+ when 1 then 'one'
86
+ when *from2to4 then 'few'
87
+ else 'other'
88
+ end
89
+ end
90
+
91
+ }.freeze
92
+
93
+ # Store the RULE to apply to each LOCALE
94
+ # the :one_other RULE is the default for locales missing from this list
95
+ LOCALE = Hash.new(RULE[:one_other]).tap do |hash|
96
+ hash['ar'] = RULE[:arabic]
97
+ hash['be'] = RULE[:east_slavic]
98
+ hash['bs'] = RULE[:east_slavic]
99
+ hash['ckb'] = RULE[:other]
100
+ hash['cs'] = RULE[:west_slavic]
101
+ hash['id'] = RULE[:other]
102
+ hash['fr'] = RULE[:one_upto_two_other]
103
+ hash['hr'] = RULE[:east_slavic]
104
+ hash['ja'] = RULE[:other]
105
+ hash['km'] = RULE[:other]
106
+ hash['ko'] = RULE[:other]
107
+ hash['pl'] = RULE[:polish]
108
+ hash['ru'] = RULE[:east_slavic]
109
+ hash['sk'] = RULE[:west_slavic]
110
+ hash['sr'] = RULE[:east_slavic]
111
+ hash['tr'] = RULE[:other]
112
+ hash['uk'] = RULE[:east_slavic]
113
+ hash['vi'] = RULE[:other]
114
+ hash['zh-CN'] = RULE[:other]
115
+ hash['zh-HK'] = RULE[:other]
116
+ hash['zh-TW'] = RULE[:other]
117
+ hash['dz'] = RULE[:other]
118
+ end.freeze
119
+ end
120
+
121
+ # Stores the i18n DATA structure for each loaded locale
122
+ # default on the first locale DATA
123
+ DATA = Hash.new { |hash,| hash.first[1] }
124
+
125
+ private
126
+
127
+ # Create a flat hash with dotted notation keys
128
+ def flatten(initial, prefix = '')
129
+ initial.each.reduce({}) do |hash, (key, value)|
130
+ hash.merge!(value.is_a?(Hash) ? flatten(value, "#{prefix}#{key}.") : { "#{prefix}#{key}" => value })
131
+ end
132
+ end
133
+
134
+ # Build the DATA hash out of the passed locales
135
+ def build(*locales)
136
+ locales.each do |locale|
137
+ locale[:filepath] ||= Pagy.root.join('locales', "#{locale[:locale]}.yml")
138
+ locale[:pluralize] ||= P11n::LOCALE[locale[:locale]]
139
+ dictionary = YAML.safe_load(File.read(locale[:filepath], encoding: 'UTF-8'))
140
+ raise I18nError, %(expected :locale "#{locale[:locale]}" not found in :filepath "#{locale[:filepath].inspect}") \
141
+ unless dictionary.key?(locale[:locale])
142
+
143
+ DATA[locale[:locale]] = [flatten(dictionary[locale[:locale]]), locale[:pluralize]]
144
+ end
145
+ end
146
+ # Build the default at require time
147
+ build(locale: 'en')
148
+
149
+ public
150
+
151
+ # Public method to configure the locales: overrides the default, build the DATA and freezes it
152
+ def load(*locales)
153
+ DATA.clear
154
+ build(*locales)
155
+ DATA.freeze
156
+ end
157
+
158
+ # Translate and pluralize the key with the locale DATA
159
+ def translate(locale, key, **opts)
160
+ data, pluralize = DATA[locale]
161
+ translation = data[key] || (opts[:count] && data[key += ".#{pluralize.call(opts[:count])}"]) \
162
+ or return %([translation missing: "#{key}"])
163
+ translation.gsub(/%{[^}]+?}/) { |match| opts[:"#{match[2..-2]}"] || match }
164
+ end
165
+ alias t translate
166
+ end
167
+ end
@@ -0,0 +1,44 @@
1
+ # See Pagy API documentation: https://ddnexus.github.io/pagy/docs/api/keyset
2
+ # frozen_string_literal: true
3
+
4
+ class Pagy
5
+ class Keyset
6
+ # Keyset adapter for ActiveRecord
7
+ class ActiveRecord < Keyset
8
+ protected
9
+
10
+ # Get the keyset attributes from the record
11
+ def keyset_attributes_from(record) = record.slice(*@keyset.keys)
12
+
13
+ # Get the hash of quoted keyset identifiers
14
+ def quoted_identifiers(table)
15
+ connection = @set.connection
16
+ @keyset.to_h { |column| [column, "#{connection.quote_table_name(table)}.#{connection.quote_column_name(column)}"] }
17
+ end
18
+
19
+ # Extract the keyset from the set
20
+ def extract_keyset
21
+ @set.order_values.each_with_object({}) do |node, keyset|
22
+ keyset[node.value.name.to_sym] = node.direction
23
+ end
24
+ end
25
+
26
+ # Filter the newest records
27
+ def filter_newest = @set.where(filter_newest_query, **@latest)
28
+
29
+ # Append the missing keyset keys if the set is restricted by select
30
+ def apply_select
31
+ @set.select(*@keyset.keys)
32
+ end
33
+
34
+ # Set with selected columns?
35
+ def select? = !@set.select_values.empty?
36
+
37
+ # Typecast the latest attributes
38
+ def typecast_latest(latest)
39
+ @set.model.new(latest).slice(latest.keys)
40
+ .to_hash.transform_keys(&:to_sym)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,57 @@
1
+ # See Pagy API documentation: https://ddnexus.github.io/pagy/docs/api/keyset
2
+ # frozen_string_literal: true
3
+
4
+ class Pagy
5
+ class Keyset
6
+ # Keyset adapter for sequel
7
+ class Sequel < Keyset
8
+ protected
9
+
10
+ # Get the keyset attributes from the record
11
+ def keyset_attributes_from(record) = record.to_hash.slice(*@keyset.keys)
12
+
13
+ # Get the hash of quoted keyset identifiersAdd commentMore actions
14
+ def quoted_identifiers(table)
15
+ db = @set.db
16
+ @keyset.to_h { |column| [column, "#{db.quote_identifier(table)}.#{db.quote_identifier(column)}"] }
17
+ end
18
+
19
+ # Extract the keyset from the set
20
+ def extract_keyset
21
+ return {} unless @set.opts[:order]
22
+
23
+ @set.opts[:order].each_with_object({}) do |item, keyset|
24
+ case item
25
+ when Symbol
26
+ keyset[item] = :asc
27
+ when ::Sequel::SQL::OrderedExpression
28
+ keyset[item.expression] = item.descending ? :desc : :asc
29
+ else
30
+ raise TypeError, "#{item.class.inspect} is not a supported Sequel::SQL::OrderedExpression"
31
+ end
32
+ end
33
+ end
34
+
35
+ # Filter the newest records
36
+ def filter_newest = @set.where(::Sequel.lit(filter_newest_query, **@latest))
37
+
38
+ # Append the missing keyset keys if the set is restricted by select
39
+ def apply_select
40
+ selected = @set.opts[:select]
41
+ @set.select_append(*@keyset.keys.reject { |c| selected.include?(c) })
42
+ end
43
+
44
+ # Set with selected columns?
45
+ def select? = !@set.opts[:select].nil?
46
+
47
+ # Typecast the latest attributes
48
+ def typecast_latest(latest)
49
+ model = @set.opts[:model]
50
+ model.unrestrict_primary_key if (restricted_pk = model.restrict_primary_key?)
51
+ latest = model.new(latest).to_hash.slice(*latest.keys.map(&:to_sym))
52
+ model.restrict_primary_key if restricted_pk
53
+ latest
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,118 @@
1
+ # See Pagy API documentation: https://ddnexus.github.io/pagy/docs/api/keyset
2
+ # frozen_string_literal: true
3
+
4
+ require 'json'
5
+ require_relative 'b64'
6
+ require_relative 'shared_methods'
7
+
8
+ class Pagy
9
+ # Implement wicked-fast keyset pagination for big data
10
+ class Keyset
11
+ class TypeError < ::TypeError; end
12
+
13
+ include SharedMethods
14
+
15
+ # Pick the right adapter for the set
16
+ def self.new(set, **vars)
17
+ if self == Pagy::Keyset
18
+ if defined?(::ActiveRecord) && set.is_a?(::ActiveRecord::Relation)
19
+ ActiveRecord
20
+ elsif defined?(::Sequel) && set.is_a?(::Sequel::Dataset)
21
+ Sequel
22
+ else
23
+ raise TypeError, "expected set to be an instance of ActiveRecord::Relation or Sequel::Dataset; got #{set.class}"
24
+ end.new(set, **vars)
25
+ else
26
+ allocate.tap { |instance| instance.send(:initialize, set, **vars) }
27
+ end
28
+ end
29
+
30
+ attr_reader :latest # Other readers from SharedMethods
31
+
32
+ def initialize(set, **vars)
33
+ default = DEFAULT.slice(:limit, :page_param, # from pagy
34
+ :headers, # from headers extra
35
+ :jsonapi, # from jsonapi extra
36
+ :limit_param, :limit_max, :limit_extra) # from limit_extra
37
+ assign_vars({ **default, page: nil }, vars)
38
+ assign_limit
39
+ @set = set
40
+ @page = @vars[:page]
41
+ @keyset = extract_keyset
42
+ raise InternalError, 'the set must be ordered' if @keyset.empty?
43
+ return unless @page
44
+
45
+ latest = JSON.parse(B64.urlsafe_decode(@page)).transform_keys(&:to_sym)
46
+ @latest = typecast_latest(latest)
47
+ raise InternalError, 'page and keyset are not consistent' \
48
+ unless @latest.keys == @keyset.keys
49
+ end
50
+
51
+ # Return the next page
52
+ def next
53
+ records
54
+ return unless @more
55
+
56
+ @next ||= begin
57
+ hash = keyset_attributes_from(@records.last)
58
+ json = @vars[:jsonify_keyset_attributes]&.(hash) || hash.to_json
59
+ B64.urlsafe_encode(json)
60
+ end
61
+ end
62
+
63
+ # Fetch the array of records for the current page
64
+ def records
65
+ @records ||= begin
66
+ @set = apply_select if select?
67
+ if @latest
68
+ # :nocov:
69
+ @set = @vars[:after_latest]&.(@set, @latest) || # deprecated
70
+ # :nocov:
71
+ @vars[:filter_newest]&.(@set, @latest, @keyset) ||
72
+ filter_newest
73
+ end
74
+ records = @set.limit(@limit + 1).to_a
75
+ @more = records.size > @limit && !records.pop.nil?
76
+ records
77
+ end
78
+ end
79
+
80
+ protected
81
+
82
+ # Prepare the literal query string (complete with the placeholders for value interpolation)
83
+ # used to filter the newest records.
84
+ # For example:
85
+ # With a set like Pet.order(animal: :asc, name: :desc, id: :asc) it returns the following string:
86
+ # ( "pets"."animal" = :animal AND "pets"."name" = :name AND "pets"."id" > :id ) OR
87
+ # ( "pets"."animal" = :animal AND "pets"."name" < :name ) OR
88
+ # ( "pets"."animal" > :animal )
89
+ # When :tuple_comparison is enabled, and if the order is all :asc or all :desc,
90
+ # with a set like Pet.order(:animal, :name, :id) it returns the following string:
91
+ # ( "pets"."animal", "pets"."name", "pets"."id" ) > ( :animal, :name, :id )
92
+ def filter_newest_query
93
+ operator = { asc: '>', desc: '<' }
94
+ directions = @keyset.values
95
+ name = quoted_identifiers(@set.model.table_name)
96
+
97
+ if @vars[:tuple_comparison] && (directions.all?(:asc) || directions.all?(:desc))
98
+ placeholders = @keyset.keys.map { |column| ":#{column}" }.join(', ')
99
+ "( #{name.values.join(', ')} ) #{operator[directions.first]} ( #{placeholders} )"
100
+ else
101
+ keyset = @keyset.to_a
102
+ where = []
103
+ until keyset.empty?
104
+ last_column, last_direction = keyset.pop
105
+ query = +'( '
106
+ query << (keyset.map { |column, _d| "#{name[column]} = :#{column}" } \
107
+ << "#{name[last_column]} #{operator[last_direction]} :#{last_column}").join(' AND ')
108
+ query << ' )'
109
+ where << query
110
+ end
111
+ where.join(' OR ')
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ require_relative 'keyset/active_record'
118
+ require_relative 'keyset/sequel'
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ # Shared with Keyset
5
+ module SharedMethods
6
+ attr_reader :page, :limit, :vars
7
+
8
+ # Validates and assign the passed vars: var must be present and value.to_i must be >= to min
9
+ def assign_and_check(name_min)
10
+ name_min.each do |name, min|
11
+ raise VariableError.new(self, name, ">= #{min}", @vars[name]) \
12
+ unless @vars[name]&.respond_to?(:to_i) && instance_variable_set(:"@#{name}", @vars[name].to_i) >= min
13
+ end
14
+ end
15
+
16
+ # Assign @limit (overridden by the gearbox extra)
17
+ def assign_limit
18
+ assign_and_check(limit: 1)
19
+ end
20
+
21
+ # Assign @vars
22
+ def assign_vars(default, vars)
23
+ @vars = { **default, **vars.delete_if { |k, v| default.key?(k) && (v.nil? || v == '') } }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ # Provide the helpers to handle the url in frontend and backend
5
+ module UrlHelpers
6
+ # Return the URL for the page, relying on the params method and Rack by default.
7
+ # It supports all Rack-based frameworks (Sinatra, Padrino, Rails, ...).
8
+ # For non-rack environments you can use the standalone extra
9
+ def pagy_url_for(pagy, page, absolute: false, fragment: nil, **_)
10
+ vars = pagy.vars
11
+ query_params = request.GET.clone(freeze: false)
12
+ query_params.merge!(vars[:params].transform_keys(&:to_s)) if vars[:params].is_a?(Hash)
13
+ pagy_set_query_params(page, vars, query_params)
14
+ query_params = vars[:params].(query_params) if vars[:params].is_a?(Proc)
15
+ query_string = "?#{Rack::Utils.build_nested_query(query_params)}"
16
+ "#{request.base_url if absolute}#{vars[:request_path] || request.path}#{query_string}#{fragment}"
17
+ end
18
+
19
+ # Add the page and limit params
20
+ # Overridable by the jsonapi extra
21
+ def pagy_set_query_params(page, vars, query_params)
22
+ query_params[vars[:page_param].to_s] = page
23
+ query_params[vars[:limit_param].to_s] = vars[:limit] if vars[:limit_extra]
24
+ end
25
+ end
26
+ end
data/lib/pagy.rb CHANGED
@@ -1,52 +1,106 @@
1
- # See Pagy API documentation: https://ddnexus.github.io/pagy/api/pagy
2
- # encoding: utf-8
1
+ # See Pagy API documentation: https://ddnexus.github.io/pagy/docs/api/pagy
3
2
  # frozen_string_literal: true
4
3
 
5
4
  require 'pathname'
5
+ require_relative 'pagy/shared_methods'
6
6
 
7
- class Pagy ; VERSION = '3.8.2'
7
+ # Top superclass: it should define only what's common to all the subclasses
8
+ class Pagy
9
+ VERSION = '9.4.0'
8
10
 
9
- # Root pathname to get the path of Pagy files like templates or dictionaries
10
- def self.root; @root ||= Pathname.new(__FILE__).dirname.freeze end
11
+ # Core default: constant for easy access, but mutable for customizable defaults
12
+ DEFAULT = { count_args: [:all], # rubocop:disable Style/MutableConstant
13
+ ends: true,
14
+ limit: 20,
15
+ outset: 0,
16
+ page: 1,
17
+ page_param: :page,
18
+ size: 7 } # AR friendly
11
19
 
12
- # default vars
13
- VARS = { page:1, items:20, outset:0, size:[1,4,4,1], page_param: :page, params:{}, anchor:'', link_extra:'', i18n_key:'pagy.item_name', cycle:false }
20
+ # Gem root pathname to get the path of Pagy files stylesheets, javascripts, apps, locales, etc.
21
+ def self.root
22
+ @root ||= Pathname.new(__dir__).parent.freeze
23
+ end
24
+
25
+ include SharedMethods
14
26
 
15
- attr_reader :count, :page, :items, :vars, :pages, :last, :offset, :from, :to, :prev, :next
27
+ attr_reader :count, :from, :in, :last, :next, :offset, :prev, :to
28
+ alias pages last
16
29
 
17
30
  # Merge and validate the options, do some simple arithmetic and set the instance variables
18
- def initialize(vars)
19
- @vars = VARS.merge(vars.delete_if{|_,v| v.nil? || v == '' }) # default vars + cleaned vars
20
- { count:0, items:1, outset:0, page:1 }.each do |k,min| # validate instance variables
21
- (@vars[k] && instance_variable_set(:"@#{k}", @vars[k].to_i) >= min) \
22
- or raise(VariableError.new(self), "expected :#{k} >= #{min}; got #{@vars[k].inspect}")
23
- end
24
- @pages = @last = [(@count.to_f / @items).ceil, 1].max # cardinal and ordinal meanings
25
- @page <= @last or raise(OverflowError.new(self), "expected :page in 1..#{@last}; got #{@page.inspect}")
26
- @offset = @items * (@page - 1) + @outset # pagination offset + outset (initial offset)
27
- @items = @count - ((@pages-1) * @items) if @page == @last && @count > 0 # adjust items for last non-empty page
28
- @from = @count == 0 ? 0 : @offset+1 - @outset # page begins from item
29
- @to = @count == 0 ? 0 : @offset + @items - @outset # page ends to item
30
- @prev = (@page-1 unless @page == 1) # nil if no prev page
31
- @next = @page == @last ? (1 if @vars[:cycle]) : @page+1 # nil if no next page, 1 if :cycle
31
+ def initialize(**vars)
32
+ assign_vars(DEFAULT, vars)
33
+ assign_and_check(count: 0, page: 1, outset: 0)
34
+ assign_limit
35
+ assign_offset
36
+ assign_last
37
+ check_overflow
38
+ @from = [@offset - @outset + 1, @count].min
39
+ @to = [@offset - @outset + @limit, @count].min
40
+ @in = [@to - @from + 1, @count].min
41
+ assign_prev_and_next
42
+ end
43
+
44
+ # Setup @last (overridden by the gearbox extra)
45
+ def assign_last
46
+ @last = [(@count.to_f / @limit).ceil, 1].max
47
+ @last = @vars[:max_pages] if @vars[:max_pages] && @last > vars[:max_pages]
48
+ end
49
+
50
+ # Assign @offset (overridden by the gearbox extra)
51
+ def assign_offset
52
+ @offset = (@limit * (@page - 1)) + @outset # may be already set from gear_box
32
53
  end
33
54
 
34
- # Return the array of page numbers and :gap items e.g. [1, :gap, 7, 8, "9", 10, 11, :gap, 36]
35
- def series(size=@vars[:size])
36
- (series = []) and size.empty? and return series
37
- 4.times{|i| (size[i]>=0 rescue nil) or raise(VariableError.new(self), "expected 4 items >= 0 in :size; got #{size.inspect}")}
38
- [*0..size[0], *@page-size[1]..@page+size[2], *@last-size[3]+1..@last+1].sort!.each_cons(2) do |a, b|
39
- if a<0 || a==b || a>@last # skip out of range and duplicates
40
- elsif a+1 == b; series.push(a) # no gap -> no additions
41
- elsif a+2 == b; series.push(a, a+1) # 1 page gap -> fill with missing page
42
- else series.push(a, :gap) # n page gap -> add gap
43
- end # skip the end boundary (last+1)
44
- end # shift the start boundary (0) and
45
- series.shift; series[series.index(@page)] = @page.to_s; series # convert the current page to String
55
+ # Assign @prev and @next
56
+ def assign_prev_and_next
57
+ @prev = (@page - 1 unless @page == 1)
58
+ @next = @page == @last ? (1 if @vars[:cycle]) : @page + 1
46
59
  end
47
60
 
61
+ # Checks the @page <= @last
62
+ def check_overflow
63
+ raise OverflowError.new(self, :page, "in 1..#{@last}", @page) if @page > @last
64
+ end
65
+
66
+ # Label for the current page. Allow the customization of the output (overridden by the calendar extra)
67
+ def label = @page.to_s
68
+
69
+ # Label for any page. Allow the customization of the output (overridden by the calendar extra)
70
+ def label_for(page) = page.to_s
71
+
72
+ # Return the array of page numbers and :gap e.g. [1, :gap, 8, "9", 10, :gap, 36]
73
+ def series(size: @vars[:size], **_)
74
+ raise VariableError.new(self, :size, 'to be an Integer >= 0', size) \
75
+ unless size.is_a?(Integer) && size >= 0
76
+ return [] if size.zero?
77
+
78
+ [].tap do |series|
79
+ if size >= @last
80
+ series.push(*1..@last)
81
+ else
82
+ left = ((size - 1) / 2.0).floor # left half might be 1 page shorter for even size
83
+ start = if @page <= left # beginning pages
84
+ 1
85
+ elsif @page > (@last - size + left) # end pages
86
+ @last - size + 1
87
+ else # intermediate pages
88
+ @page - left
89
+ end
90
+ series.push(*start...(start + size))
91
+ # Set first and last pages plus gaps when needed, respecting the size
92
+ if vars[:ends] && size >= 7
93
+ series[0] = 1
94
+ series[1] = :gap unless series[1] == 2
95
+ series[-2] = :gap unless series[-2] == @last - 1
96
+ series[-1] = @last
97
+ end
98
+ end
99
+ series[series.index(@page)] = @page.to_s
100
+ end
101
+ end
48
102
  end
49
103
 
50
- require 'pagy/backend'
51
- require 'pagy/frontend'
52
- require 'pagy/exceptions'
104
+ require_relative 'pagy/backend'
105
+ require_relative 'pagy/frontend'
106
+ require_relative 'pagy/exceptions'
data/locales/ar.yml ADDED
@@ -0,0 +1,29 @@
1
+ # :arabic pluralization (see https://github.com/ddnexus/pagy/blob/master/gem/lib/pagy/i18n.rb)
2
+ ar:
3
+ pagy:
4
+ aria_label:
5
+ nav:
6
+ zero: "لا يوجد صفحات"
7
+ one: "صفحة"
8
+ two: "صفحتين"
9
+ few: "صفحات"
10
+ many: "صفحات"
11
+ other: "صفحات"
12
+ prev: "السابق"
13
+ next: "التالي"
14
+ prev: "&lt;"
15
+ next: "&gt;"
16
+ gap: "&hellip;"
17
+ item_name:
18
+ zero: "صفر"
19
+ one: "عنصر"
20
+ two: "عنصرين"
21
+ few: "قليل"
22
+ many: "كثير"
23
+ other: "عناصر"
24
+ info:
25
+ no_items: "لا يوجد %{item_name}"
26
+ single_page: "عرض %{count} %{item_name}"
27
+ multiple_pages: "عرض %{item_name} %{from}-%{to} من اجمالي %{count}"
28
+ combo_nav_js: "الصفحة %{page_input} من %{pages}"
29
+ limit_selector_js: "عرض %{limit_input} %{item_name} لكل صفحة"
data/locales/be.yml ADDED
@@ -0,0 +1,25 @@
1
+ # :east_slavic pluralization (see https://github.com/ddnexus/pagy/blob/master/gem/lib/pagy/i18n.rb)
2
+ be:
3
+ pagy:
4
+ aria_label:
5
+ nav:
6
+ one: "Старонка"
7
+ few: "Старонкi"
8
+ many: "Старонкi"
9
+ other: "Старонкi"
10
+ prev: "Назад"
11
+ next: "Наперад"
12
+ prev: "&lt;"
13
+ next: "&gt;"
14
+ gap: "&hellip;"
15
+ item_name:
16
+ one: "запіс"
17
+ few: "запісы"
18
+ many: "запісаў"
19
+ other: "запісаў"
20
+ info:
21
+ no_items: "Пакуль %{item_name} няма"
22
+ single_page: "%{count} %{item_name}"
23
+ multiple_pages: "Усяго %{count} %{item_name}, паказаны з %{from} па %{to}"
24
+ combo_nav_js: "Старонка %{page_input} з %{pages}"
25
+ limit_selector_js: "Паказаць %{limit_input} %{item_name} на старонцы"
data/locales/bg.yml ADDED
@@ -0,0 +1,21 @@
1
+ # :one_other pluralization (see https://github.com/ddnexus/pagy/blob/master/gem/lib/pagy/i18n.rb)
2
+ bg:
3
+ pagy:
4
+ aria_label:
5
+ nav:
6
+ one: "Страница"
7
+ other: "Страници"
8
+ prev: "Предишна"
9
+ next: "Следваща"
10
+ prev: "&lt;"
11
+ next: "&gt;"
12
+ gap: "&hellip;"
13
+ item_name:
14
+ one: "резултат"
15
+ other: "резултати"
16
+ info:
17
+ no_items: "Няма намерени %{item_name}"
18
+ single_page: "Показани са %{count} %{item_name}"
19
+ multiple_pages: "Показани са %{item_name} %{from}-%{to} от %{count} общо"
20
+ combo_nav_js: "Страница %{page_input} от %{pages}"
21
+ limit_selector_js: "Покажи %{limit_input} %{item_name} на страница"