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/apps/demo.ru ADDED
@@ -0,0 +1,449 @@
1
+ # frozen_string_literal: true
2
+
3
+ # DESCRIPTION
4
+ # Showcase all the helpers and styles
5
+ #
6
+ # DOC
7
+ # https://ddnexus.github.io/pagy/playground/#3-demo-app
8
+ #
9
+ # BIN HELP
10
+ # bundle exec pagy -h
11
+ #
12
+ # DEMO USAGE
13
+ # bundle exec pagy demo
14
+ #
15
+ # DEV USAGE
16
+ # bundle exec pagy clone demo
17
+ # bundle exec pagy ./demo.ru
18
+ #
19
+ # URL
20
+ # http://0.0.0.0:8000
21
+
22
+ VERSION = '9.4.0'
23
+
24
+ # Bundle
25
+ require 'bundler/inline'
26
+ require 'bundler'
27
+ Bundler.configure
28
+ gemfile(ENV['PAGY_INSTALL_BUNDLE'] == 'true') do
29
+ source 'https://rubygems.org'
30
+ gem 'oj'
31
+ gem 'puma'
32
+ gem 'rouge'
33
+ gem 'sinatra'
34
+ end
35
+
36
+ # pagy initializer
37
+ STYLES = { pagy: { extra: 'pagy', prefix: '', css_anchor: 'pagy-scss' },
38
+ bootstrap: {},
39
+ bulma: {},
40
+ tailwind: { extra: 'pagy', prefix: '', css_anchor: 'pagy-tailwind-css' } }.freeze
41
+
42
+ STYLES.each_key do |style|
43
+ require "pagy/extras/#{STYLES[style][:extra] || style}"
44
+ end
45
+ require 'pagy/extras/limit'
46
+ require 'pagy/extras/trim'
47
+ Pagy::DEFAULT[:trim_extra] = false # opt-in trim
48
+
49
+ # Sinatra setup
50
+ require 'sinatra/base'
51
+
52
+ # Sinatra application
53
+ class PagyDemo < Sinatra::Base
54
+ include Pagy::Backend
55
+
56
+ get '/' do
57
+ redirect '/pagy'
58
+ end
59
+
60
+ get '/template' do
61
+ collection = MockCollection.new
62
+ @pagy, @records = pagy(collection, trim_extra: params['trim'])
63
+
64
+ erb :template, locals: { pagy: @pagy, style: 'pagy' }
65
+ end
66
+
67
+ get('/javascripts/:file') do
68
+ format = params[:file].split('.').last
69
+ if format == 'js'
70
+ content_type 'application/javascript'
71
+ elsif format == 'map'
72
+ content_type 'application/json'
73
+ end
74
+ send_file Pagy.root.join('javascripts', params[:file])
75
+ end
76
+
77
+ get('/stylesheets/:file') do
78
+ content_type 'text/css'
79
+ send_file Pagy.root.join('stylesheets', params[:file])
80
+ end
81
+
82
+ # One route/action per style
83
+ STYLES.each_key do |style|
84
+ prefix = STYLES[style][:prefix] || "_#{style}"
85
+
86
+ get("/#{style}/?:trim?") do
87
+ collection = MockCollection.new
88
+ @pagy, @records = pagy(collection, trim_extra: params['trim'])
89
+
90
+ erb :helpers, locals: { style:, prefix: }
91
+ end
92
+ end
93
+
94
+ helpers do
95
+ include Pagy::Frontend
96
+
97
+ def style_menu
98
+ html = +%(<div id="style-menu"> )
99
+ STYLES.each_key { |style| html << %(<a href="/#{style}">#{style}</a>) }
100
+ html << %(<a href="/template">template</a>)
101
+ html << %(</div>)
102
+ end
103
+
104
+ def highlight(html, format: :html)
105
+ if format == :html
106
+ html = html.gsub(/>[\s]*</, '><').strip # template single line no spaces around/between tags
107
+ html = Formatter.new.format(html)
108
+ end
109
+ lexer = Rouge::Lexers::ERB.new
110
+ formatter = Rouge::Formatters::HTMLInline.new('monokai.sublime')
111
+ summary = { html: 'Served HTML (pretty formatted)', erb: 'ERB Template' }
112
+ %(<details><summary>#{summary[format]}</summary><pre>\n#{
113
+ formatter.format(lexer.lex(html))
114
+ }</pre></details>)
115
+ end
116
+ end
117
+
118
+ # Views
119
+ template :layout do
120
+ <<~'ERB'
121
+ <!DOCTYPE html>
122
+ <html lang="en">
123
+ <head>
124
+ <title>Pagy Demo App</title>
125
+ <script src="/javascripts/pagy.min.js"></script>
126
+ <script>
127
+ window.addEventListener("load", Pagy.init);
128
+ </script>
129
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
130
+ <%= erb :"#{style}_head" if defined?(style) %>
131
+ <style type="text/css">
132
+ @media screen { html, body {
133
+ font-size: 1rem;
134
+ line-height: 1.2s;
135
+ padding: 0;
136
+ margin: 0;
137
+ } }
138
+ body {
139
+ background: white !important;
140
+ margin: 0 !important;
141
+ font-family: sans-serif !important;
142
+ }
143
+ h1, h2 {
144
+ font-size: 1.8rem !important;
145
+ font-weight: 600 !important;
146
+ margin-top: 1rem !important;
147
+ margin-bottom: 0.7rem !important;
148
+ line-height: 1.5 !important;
149
+ color: rgb(90 90 90) !important;
150
+ }
151
+ h2 {
152
+ font-family: monospace;
153
+ font-size: .9rem !important;
154
+ margin-top: 1.6rem !important;
155
+ }
156
+ summary, .notes {
157
+ font-size: .8rem;
158
+ color: gray;
159
+ margin-top: .6rem;
160
+ font-style: italic;
161
+ cursor: pointer;
162
+ }
163
+ .notes {
164
+ font-family: sans-serif;
165
+ font-weight: normal;
166
+ }
167
+ .notes code{
168
+ background-color: #E8E8E8;
169
+ padding: 0 0.3rem;
170
+ font-style: normal;
171
+ border-radius: 3px;
172
+ }
173
+ .description {
174
+ margin: 1rem 0;
175
+ }
176
+ .description a {
177
+ color: blue;
178
+ text-decoration: underline;
179
+ }
180
+ pre, pre code {
181
+ display: block;
182
+ margin-top: .3rem;
183
+ margin-bottom: 1rem;
184
+ font-size: .8rem !important;
185
+ line-height: 1rem !important;
186
+ color: white;
187
+ background-color: rgb(30 30 30);
188
+ padding: 1rem;
189
+ overflow: auto;
190
+ }
191
+ .content {
192
+ padding: 0 1.5rem 2rem !important;
193
+ }
194
+
195
+ #style-menu {
196
+ flex;
197
+ font-family: sans-serif;
198
+ font-size: 1.1rem;
199
+ line-height: 1.5rem;
200
+ white-space: nowrap;
201
+ color: white;
202
+ background-color: gray;
203
+ padding: .2rem 1.5rem;
204
+ }
205
+ #style-menu > :not([hidden]) ~ :not([hidden]) {
206
+ --space-reverse: 0;
207
+ margin-right: calc(0.5rem * var(--space-reverse));
208
+ margin-left: calc(0.5rem * calc(1 - var(--space-reverse)));
209
+ }
210
+ #style-menu a {
211
+ color: inherit;
212
+ text-decoration: none;
213
+ }
214
+ /* Quick demo for overriding the element style attribute of certain pagy helpers
215
+ .pagy input[style] {
216
+ width: 5rem !important;
217
+ }
218
+ */
219
+ </style>
220
+ </head>
221
+ <body>
222
+ <!-- each different class used by each style -->
223
+ <%= style_menu %>
224
+ <div class="content">
225
+ <%= yield %>
226
+ </div>
227
+ </body>
228
+ </html>
229
+ ERB
230
+ end
231
+
232
+ template :pagy_head do
233
+ <<~ERB
234
+ <!-- copy and paste the pagy style in order to edit it -->
235
+ <link rel="stylesheet" href="/stylesheets/pagy.css">
236
+ ERB
237
+ end
238
+
239
+ template :bootstrap_head do
240
+ <<~ERB
241
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
242
+ ERB
243
+ end
244
+
245
+ template :bulma_head do
246
+ <<~ERB
247
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
248
+ ERB
249
+ end
250
+
251
+ template :tailwind_head do
252
+ <<~ERB
253
+ <script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script>
254
+ <!-- copy and paste the pagy.tailwind style in order to edit it -->
255
+ <style type="text/tailwindcss">
256
+ <%= Pagy.root.join('stylesheets', 'pagy.tailwind.css').read %>
257
+ </style>
258
+ ERB
259
+ end
260
+
261
+ template :helpers do
262
+ <<~'ERB'
263
+ <h1><%= style %></h1>
264
+ <% extra = STYLES[style][:extra] || "#{style}" %>
265
+ <% css_anchor = STYLES[style][:css_anchor] %>
266
+
267
+ <p class="description">See the <a href="http://ddnexus.github.io/pagy/docs/extras/<%= extra %>" target="blank"><%= extra %> extra</a>
268
+ documentation
269
+ <% if css_anchor %>
270
+ and the <a href="http://ddnexus.github.io/pagy/docs/api/stylesheets/#<%= css_anchor %>" target="blank"><%= css_anchor.gsub('-', '.') %></a>
271
+ <% end %>
272
+ for details</p>
273
+
274
+ <h2>Collection</h2>
275
+ <p id="records">@records: <%= @records.join(',') %></p>
276
+
277
+ <h2>pagy<%= prefix %>_nav <span class="notes">Simple nav <code>size: 5</code></span></h2>
278
+ <%= html = send(:"pagy#{prefix}_nav", @pagy, id: 'simple-nav', aria_label: 'Pages simple-nav', size: 5) %>
279
+ <%= highlight(html) %>
280
+
281
+ <h2>pagy<%= prefix %>_nav <span class="notes">Fast nav <code>size: 7</code></span></h2>
282
+ <%= html = send(:"pagy#{prefix}_nav", @pagy, id: 'nav', aria_label: 'Pages nav') %>
283
+ <%= highlight(html) %>
284
+
285
+ <h2>pagy<%= prefix %>_nav_js <span class="notes">Fast nav <code>size: 7</code></span></h2>
286
+ <%= html = send(:"pagy#{prefix}_nav_js", @pagy, id: 'nav-js', aria_label: 'Pages nav_js') %>
287
+ <%= highlight(html) %>
288
+
289
+ <h2>pagy<%= prefix %>_nav_js <span class="notes">Responsive nav <code>steps: {...}</code> (Resize the window to see)</span></h2>
290
+ <%= html = send(:"pagy#{prefix}_nav_js", @pagy, id: 'nav-js-responsive',
291
+ aria_label: 'Pages nav_js_responsive',
292
+ steps: { 0 => 5, 500 => 7, 750 => 9, 1000 => 11 }) %>
293
+ <%= highlight(html) %>
294
+
295
+ <h2>pagy<%= prefix %>_combo_nav_js</h2>
296
+ <%= html = send(:"pagy#{prefix}_combo_nav_js", @pagy, id: 'combo-nav-js', aria_label: 'Pages combo_nav_js') %>
297
+ <%= highlight(html) %>
298
+
299
+ <h2>pagy_info</h2>
300
+ <%= html = pagy_info(@pagy, id: 'pagy-info') %>
301
+ <%= highlight(html) %>
302
+
303
+ <% if style.match(/pagy|tailwind/) %>
304
+ <h2>pagy_limit_selector_js</h2>
305
+ <%= html = pagy_limit_selector_js(@pagy, id: 'limit-selector-js') %>
306
+ <%= highlight(html) %>
307
+
308
+ <h2>pagy_prev_a / pagy_next_a</h2>
309
+ <%= html = '<nav class="pagy" id="prev-next" aria-label="Pagy prev-next">' << pagy_prev_a(@pagy) << pagy_next_a(@pagy) << '</nav>' %>
310
+ <%= highlight(html) %>
311
+
312
+ <h2>pagy_prev_link / pagy_next_link <span class="notes">Link not rendered<span></h2>
313
+ <% html = '<head>' << (pagy_prev_link(@pagy)||'') << (pagy_next_link(@pagy)||'') << '</head>' %>
314
+ <%= highlight(html) %>
315
+ <% end %>
316
+ ERB
317
+ end
318
+
319
+ template :template do
320
+ <<~ERB
321
+ <h1>Pagy Template Demo</h1>
322
+
323
+ <p class="description">
324
+ See the <a href="https://ddnexus.github.io/pagy/docs/how-to/#using-your-pagination-templates">
325
+ Custom Templates</a> documentation.
326
+ </p>
327
+ <h2>Collection</h2>
328
+ <p id="records">@records: <%= @records.join(',') %></p>
329
+
330
+ <h2>Rendered ERB template</h2>
331
+
332
+ <%# We don't inline the template here, so we can highlight it more easily %>
333
+ <%= html = ERB.new(TEMPLATE).result(binding) %>
334
+ <%= highlight(TEMPLATE, format: :erb) %>
335
+ <%= highlight(html) %>
336
+ ERB
337
+ end
338
+
339
+ # Easier code highlighting
340
+ TEMPLATE = <<~ERB
341
+ <%# IMPORTANT: replace '<%=' with '<%==' if you run this in rails %>
342
+
343
+ <%# The a variable below is set to a lambda that generates the a tag %>
344
+ <%# Usage: a_tag = a.(page_number, text, classes: nil, aria_label: nil) %>
345
+ <% a = pagy_anchor(pagy) %>
346
+ <nav class="pagy nav" aria-label="Pages">
347
+ <%# Previous page link %>
348
+ <% if pagy.prev %>
349
+ <%= a.(pagy.prev, '&lt;', aria_label: 'Previous') %>
350
+ <% else %>
351
+ <a role="link" aria-disabled="true" aria-label="Previous">&lt;</a>
352
+ <% end %>
353
+ <%# Page links (series example: [1, :gap, 7, 8, "9", 10, 11, :gap, 36]) %>
354
+ <% pagy.series.each do |item| %>
355
+ <% if item.is_a?(Integer) %>
356
+ <%= a.(item) %>
357
+ <% elsif item.is_a?(String) %>
358
+ <a role="link" aria-disabled="true" aria-current="page" class="current"><%= item %></a>
359
+ <% elsif item == :gap %>
360
+ <a role="link" aria-disabled="true" class="gap">&hellip;</a>
361
+ <% end %>
362
+ <% end %>
363
+ <%# Next page link %>
364
+ <% if pagy.next %>
365
+ <%= a.(pagy.next, '&gt;', aria_label: 'Next') %>
366
+ <% else %>
367
+ <a role="link" aria-disabled="true" aria-label="Next">&lt;</a>
368
+ <% end %>
369
+ </nav>
370
+ ERB
371
+ end
372
+
373
+ # Cheap pagy formatter for helpers output
374
+ class Formatter
375
+ INDENT = ' '
376
+ TEXT_SPACE = "\u00B7"
377
+ TEXT = /^([^<>]+)(.*)/
378
+ UNPAIRED = /^(<(input|link).*?>)(.*)/
379
+ PAIRED = %r{^(<(head|nav|div|span|p|a|b|label|ul|li).*?>)(.*?)(</\2>)(.*)}
380
+ WRAPPER = /<.*?>/
381
+ DATA_PAGY = /(data-pagy="([^"]+)")/
382
+
383
+ def initialize
384
+ @formatted = +''
385
+ end
386
+
387
+ def format(input, level = 0)
388
+ process(input, level)
389
+ @formatted
390
+ end
391
+
392
+ private
393
+
394
+ def process(input, level)
395
+ push = ->(content) { @formatted << ((INDENT * level) << content << "\n") }
396
+ rest = nil
397
+ if (match = input.match(TEXT))
398
+ text, rest = match.captures
399
+ push.(text.gsub(' ', TEXT_SPACE))
400
+ elsif (match = input.match(UNPAIRED))
401
+ tag, _name, rest = match.captures
402
+ push.(tag)
403
+ elsif (match = input.match(PAIRED))
404
+ tag_start, name, block, tag_end, rest = match.captures
405
+ ## Handle incomplete same-tag nesting
406
+ while block.scan(/<#{name}.*?>/).size > block.scan(tag_end).size
407
+ more, rest = rest.split(tag_end, 2)
408
+ block << tag_end << more
409
+ end
410
+ if (match = tag_start.match(DATA_PAGY))
411
+ search, data = match.captures
412
+ formatted = data.scan(/.{1,76}/).join("\n")
413
+ replace = %(\n#{INDENT * (level + 1)}data-pagy="#{formatted}")
414
+ tag_start.sub!(search, replace)
415
+ end
416
+ if block.match(WRAPPER)
417
+ push.(tag_start)
418
+ process(block, level + 1)
419
+ push.(tag_end)
420
+ else
421
+ push.(tag_start << block << tag_end)
422
+ end
423
+ end
424
+ process(rest, level) if rest
425
+ end
426
+ end
427
+
428
+ # Simple array-based collection that acts as a standard DB collection.
429
+ class MockCollection < Array
430
+ def initialize(arr = Array(1..1000))
431
+ super
432
+ @collection = clone
433
+ end
434
+
435
+ def offset(value)
436
+ @collection = self[value..]
437
+ self
438
+ end
439
+
440
+ def limit(value)
441
+ @collection[0, value]
442
+ end
443
+
444
+ def count(*)
445
+ size
446
+ end
447
+ end
448
+
449
+ run PagyDemo
data/apps/index.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # PagyApp module
4
+ module PagyApps
5
+ # Return the hash of app name/path
6
+ INDEX = Dir[File.expand_path('./*.ru', __dir__)].to_h { |f| [File.basename(f, '.ru'), f] }.freeze
7
+ end
data/apps/keyset_ar.ru ADDED
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ # DESCRIPTION
4
+ # Showcase the keyset ActiveRecord pagination
5
+ #
6
+ # DOC
7
+ # https://ddnexus.github.io/pagy/playground/#5-keyset-apps
8
+ #
9
+ # BIN HELP
10
+ # bundle exec pagy -h
11
+ #
12
+ # DEV USAGE
13
+ # bundle exec pagy clone keyset_ar
14
+ # bundle exec pagy ./keyset_ar.ru
15
+ #
16
+ # URL
17
+ # http://0.0.0.0:8000
18
+
19
+ VERSION = '9.4.0'
20
+
21
+ # Bundle
22
+ require 'bundler/inline'
23
+ require 'bundler'
24
+ Bundler.configure
25
+ gemfile(ENV['PAGY_INSTALL_BUNDLE'] == 'true') do
26
+ source 'https://rubygems.org'
27
+ gem 'activerecord'
28
+ gem 'puma'
29
+ gem 'sinatra'
30
+ gem 'sqlite3'
31
+ end
32
+
33
+ # Pagy initializer
34
+ require 'pagy/extras/limit'
35
+ require 'pagy/extras/keyset'
36
+ require 'pagy/extras/pagy'
37
+ Pagy::DEFAULT[:limit] = 10
38
+ Pagy::DEFAULT.freeze
39
+
40
+ # Sinatra setup
41
+ require 'sinatra/base'
42
+ # Sinatra application
43
+ class PagyKeyset < Sinatra::Base
44
+ include Pagy::Backend
45
+
46
+ # Root route/action
47
+ get '/' do
48
+ Time.zone = 'UTC'
49
+
50
+ @order = { animal: :asc, name: :asc, birthdate: :desc, id: :asc }
51
+ @pagy, @pets = pagy_keyset(Pet.order(@order))
52
+ erb :main
53
+ end
54
+
55
+ helpers do
56
+ include Pagy::Frontend
57
+
58
+ def order_symbol(dir)
59
+ { asc: '&#x2197;', desc: '&#x2198;' }[dir]
60
+ end
61
+ end
62
+
63
+ # Views
64
+ template :layout do
65
+ <<~ERB
66
+ <!DOCTYPE html>
67
+ <html lang="en">
68
+ <html>
69
+ <head>
70
+ <title>Pagy Keyset App</title>
71
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
72
+ <style type="text/css">
73
+ @media screen { html, body {
74
+ font-size: 1rem;
75
+ line-height: 1.2s;
76
+ padding: 0;
77
+ margin: 0;
78
+ } }
79
+ body {
80
+ background: white !important;
81
+ margin: 0 !important;
82
+ font-family: sans-serif !important;
83
+ }
84
+ .content {
85
+ padding: 1rem 1.5rem 2rem !important;
86
+ }
87
+
88
+ <%= Pagy.root.join('stylesheets', 'pagy.css').read %>
89
+ </style>
90
+ </head>
91
+ <body>
92
+ <%= yield %>
93
+ </body>
94
+ </html>
95
+ ERB
96
+ end
97
+
98
+ template :main do
99
+ <<~ERB
100
+ <div class="content">
101
+ <h1>Pagy Keyset App</h1>
102
+ <p>Self-contained, standalone app usable to easily reproduce any keyset related pagy issue with ActiveRecord sets.</p>
103
+ <p>Please, report the following versions in any new issue.</p>
104
+ <h2>Versions</h2>
105
+ <ul>
106
+ <li>Ruby: <%= RUBY_VERSION %></li>
107
+ <li>Rack: <%= Rack::RELEASE %></li>
108
+ <li>Sinatra: <%= Sinatra::VERSION %></li>
109
+ <li>Pagy: <%= Pagy::VERSION %></li>
110
+ </ul>
111
+
112
+ <h3>Collection</h3>
113
+ <div id="records" class="collection">
114
+ <table border="1" cellspacing="0" cellpadding="3">
115
+ <tr>
116
+ <th>animal <%= order_symbol(@order[:animal]) %></th>
117
+ <th>name <%= order_symbol(@order[:name]) %></th>
118
+ <th>birthdate <%= order_symbol(@order[:birthdate]) %></th>
119
+ <th>id <%= order_symbol(@order[:id]) %></th>
120
+ </tr>
121
+ <% @pets.each do |pet| %>
122
+ <tr>
123
+ <td><%= pet.animal %></td>
124
+ <td><%= pet.name %></td>
125
+ <td><%= pet.birthdate %></td>
126
+ <td><%= pet.id %></td>
127
+ </tr>
128
+ <% end %>
129
+ </table>
130
+ </div>
131
+ <p>
132
+ <nav class="pagy" id="next" aria-label="Pagy next">
133
+ <%= pagy_next_a(@pagy, text: 'Next page &gt;') %>
134
+ </nav>
135
+ </div>
136
+ ERB
137
+ end
138
+ end
139
+
140
+ # ActiveRecord setup
141
+ require 'active_record'
142
+
143
+ # Match the microsecods with the strings stored into the time columns of SQLite
144
+ # ActiveSupport::JSON::Encoding.time_precision = 6
145
+
146
+ # Log
147
+ output = ENV['APP_ENV'].equal?('showcase') ? IO::NULL : $stdout
148
+ ActiveRecord::Base.logger = Logger.new(output)
149
+ # SQLite DB files
150
+ dir = ENV['APP_ENV'].equal?('development') ? '.' : Dir.pwd # app dir in dev or pwd otherwise
151
+ abort "ERROR: Cannot create DB files: the directory #{dir.inspect} is not writable." \
152
+ unless File.writable?(dir)
153
+ # Connection
154
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: "#{dir}/tmp/pagy-keyset-ar.sqlite3")
155
+ # Schema
156
+ ActiveRecord::Schema.define do
157
+ create_table :pets, force: true do |t|
158
+ t.string :animal
159
+ t.string :name
160
+ t.date :birthdate
161
+ end
162
+ end
163
+
164
+ # Models
165
+ class Pet < ActiveRecord::Base; end
166
+
167
+ data = <<~DATA
168
+ Luna | dog | 2018-03-10
169
+ Coco | cat | 2019-05-15
170
+ Dodo | dog | 2020-06-25
171
+ Wiki | bird | 2018-03-12
172
+ Baby | rabbit | 2020-01-13
173
+ Neki | horse | 2021-07-20
174
+ Tino | donkey | 2019-06-18
175
+ Plot | cat | 2022-09-21
176
+ Riki | cat | 2018-09-14
177
+ Susi | horse | 2018-10-26
178
+ Coco | pig | 2020-08-29
179
+ Momo | bird | 2023-08-25
180
+ Lili | cat | 2021-07-22
181
+ Beli | pig | 2020-07-26
182
+ Rocky | bird | 2022-08-19
183
+ Vyvy | dog | 2018-05-16
184
+ Susi | horse | 2024-01-25
185
+ Ella | cat | 2020-02-20
186
+ Rocky | dog | 2019-09-19
187
+ Juni | rabbit | 2020-08-24
188
+ Coco | bird | 2021-03-17
189
+ Susi | dog | 2021-07-28
190
+ Luna | horse | 2023-05-14
191
+ Gigi | pig | 2022-05-19
192
+ Coco | cat | 2020-02-20
193
+ Nino | donkey | 2019-06-17
194
+ Luna | cat | 2022-02-09
195
+ Popi | dog | 2020-09-26
196
+ Lili | pig | 2022-06-18
197
+ Mina | horse | 2021-04-21
198
+ Susi | rabbit | 2023-05-18
199
+ Toni | donkey | 2018-06-22
200
+ Rocky | horse | 2019-09-28
201
+ Lili | cat | 2019-03-18
202
+ Roby | cat | 2022-06-19
203
+ Anto | horse | 2022-08-18
204
+ Susi | pig | 2021-04-21
205
+ Boly | bird | 2020-03-29
206
+ Sky | cat | 2023-07-19
207
+ Lili | dog | 2020-01-28
208
+ Fami | snake | 2023-04-27
209
+ Lopi | pig | 2019-06-19
210
+ Rocky | snake | 2022-03-13
211
+ Denis | dog | 2022-06-19
212
+ Maca | cat | 2022-06-19
213
+ Luna | dog | 2022-08-15
214
+ Jeme | horse | 2019-08-08
215
+ Sary | bird | 2023-04-29
216
+ Rocky | bird | 2023-05-14
217
+ Coco | dog | 2023-05-27
218
+ DATA
219
+
220
+ # DB seed
221
+ pets = []
222
+ data.each_line(chomp: true) do |pet|
223
+ name, animal, birthdate = pet.split('|').map(&:strip)
224
+ pets << { name:, animal:, birthdate: }
225
+ end
226
+ Pet.insert_all(pets)
227
+
228
+ run PagyKeyset