Skip to content

Commit

Permalink
code-restyling: root files and Pagy Core
Browse files Browse the repository at this point in the history
  • Loading branch information
ddnexus committed Apr 10, 2021
1 parent aac54aa commit b638764
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 75 deletions.
26 changes: 20 additions & 6 deletions .rubocop.yml
Expand Up @@ -3,24 +3,38 @@ require:
- rubocop-rake
- rubocop-minitest

# trying to be good O:)
AllCops:
TargetRubyVersion: 3.0
NewCops: enable

# No, thank you!
Layout:
Enabled: false

# affect performance
Metrics:
Enabled: false

Style:
# not cool
Style/CommentedKeyword:
Enabled: false

Bundler/OrderedGems:
Style/EmptyCaseCondition:
Enabled: false
Style/Documentation:
Enabled: false

Lint/RaiseException:
# these cops are disabled only in the test files in order to
# allow to copy and paste the failed output for test reconciliation
Style/StringLiterals:
Enabled: true

Lint/StructNewOverride:
Exclude:
- test/**/*
Style/HashSyntax:
Enabled: true
Exclude:
- test/**/*
Style/SymbolArray:
Enabled: true
Exclude:
- test/**/*
12 changes: 5 additions & 7 deletions Gemfile
Expand Up @@ -2,23 +2,22 @@ source "https://rubygems.org"

gemspec

gem 'rake'
gem 'rack'
gem 'i18n'

gem 'oj', require: false # false is for testing with or without it
gem 'rack'
gem 'rake'

gem 'puma'

group :test do
gem 'codecov', require: false
gem 'minitest'
gem 'minitest-reporters'
gem 'rubocop', '~> 1.11', require: false
gem 'rubocop-performance', require: false
gem 'rubocop-rake', require: false
gem 'rubocop-minitest', require: false
gem 'simplecov', require: false
gem 'codecov', require: false
gem 'minitest'
gem 'minitest-reporters'
end

group :apps do
Expand All @@ -29,7 +28,6 @@ group :apps do
end

group :performance do
#benchmark/profiling
gem 'benchmark-ips'
gem 'kalibera'
gem 'memory_profiler'
Expand Down
1 change: 0 additions & 1 deletion Rakefile
@@ -1,4 +1,3 @@
# encoding: utf-8
# frozen_string_literal: true

require "bundler/setup"
Expand Down
70 changes: 44 additions & 26 deletions lib/pagy.rb
@@ -1,48 +1,66 @@
# See Pagy API documentation: https://ddnexus.github.io/pagy/api/pagy
# encoding: utf-8
# frozen_string_literal: true

require 'pathname'

class Pagy ; VERSION = '4.1.0'
# main class
class Pagy
VERSION = '4.1.0'

# Root pathname to get the path of Pagy files like templates or dictionaries
def self.root = @root ||= Pathname.new(__FILE__).dirname.freeze
def self.root
@root ||= Pathname.new(__dir__).freeze
end

# default vars
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 }
VARS = { page: 1, items: 20, outset: 0, size: [1, 4, 4, 1], page_param: :page, # rubocop:disable Style/MutableConstant
params: {}, anchor: '', link_extra: '', i18n_key: 'pagy.item_name', cycle: false }

attr_reader :count, :page, :items, :vars, :pages, :last, :offset, :from, :to, :prev, :next

INSTANCE_VARS_MIN = { count: 0, items: 1, page: 1, outset: 0 }.freeze

# Merge and validate the options, do some simple arithmetic and set the instance variables
def initialize(vars)
@vars = VARS.merge(vars.delete_if{|_,v| v.nil? || v == '' }) # default vars + cleaned vars
{ count:0, items:1, outset:0, page:1 }.each do |k,min| # validate instance variables
(@vars[k] && instance_variable_set(:"@#{k}", @vars[k].to_i) >= min) \
or raise(VariableError.new(self), "expected :#{k} >= #{min}; got #{@vars[k].inspect}")
@vars = VARS.merge( vars.delete_if{|_,v| v.nil? || v == '' } )

INSTANCE_VARS_MIN.each do |name,min|
raise VariableError.new(self), "expected :#{name} >= #{min}; got #{@vars[name].inspect}" \
unless @vars[name] && instance_variable_set(:"@#{name}", @vars[name].to_i) >= min
end
@pages = @last = [(@count.to_f / @items).ceil, 1].max # cardinal and ordinal meanings
@page <= @last or raise(OverflowError.new(self), "expected :page in 1..#{@last}; got #{@page.inspect}")
@offset = @items * (@page - 1) + @outset # pagination offset + outset (initial offset)
@items = @count - ((@pages-1) * @items) if @page == @last && @count > 0 # adjust items for last non-empty page
@from = @count == 0 ? 0 : @offset + 1 - @outset # page begins from item
@to = @count == 0 ? 0 : @offset + @items - @outset # page ends to item
@prev = (@page-1 unless @page == 1) # nil if no prev page
@next = @page == @last ? (1 if @vars[:cycle]) : @page + 1 # nil if no next page, 1 if :cycle
@pages = @last = [(@count.to_f / @items).ceil, 1].max
raise OverflowError.new(self), "expected :page in 1..#{@last}; got #{@page.inspect}" if @page > @last

@offset = @items * (@page - 1) + @outset
@items = @count - ((@pages-1) * @items) if @page == @last && @count.positive?
@from = @count.zero? ? 0 : @offset + 1 - @outset
@to = @count.zero? ? 0 : @offset + @items - @outset
@prev = (@page-1 unless @page == 1)
@next = @page == @last ? (1 if @vars[:cycle]) : @page + 1
end

# Return the array of page numbers and :gap items e.g. [1, :gap, 7, 8, "9", 10, 11, :gap, 36]
def series(size=@vars[:size])
(series = []) and size.empty? and return series
4.times{|i| (size[i]>=0 rescue nil) or raise(VariableError.new(self), "expected 4 items >= 0 in :size; got #{size.inspect}")}
[*0..size[0], *@page-size[1]..@page+size[2], *@last-size[3]+1..@last+1].sort!.each_cons(2) do |a, b|
if a<0 || a==b || a>@last; next # skip out of range and duplicates
elsif a+1 == b; series.push(a) # no gap -> no additions
elsif a+2 == b; series.push(a, a+1) # 1 page gap -> fill with missing page
else series.push(a, :gap) # n page gap -> add gap
end # skip the end boundary (last+1)
end # shift the start boundary (0) and
series.shift; series[series.index(@page)] = @page.to_s; series # convert the current page to String
return [] if size.empty?
raise VariableError.new(self), "expected 4 items >= 0 in :size; got #{size.inspect}" \
unless size.size == 4 && size.all?{ |num| num >= 0 rescue false } # rubocop:disable Style/RescueModifier

[].tap do |series|
[ *0..size[0], # initial pages from 0
*@page-size[1]..@page+size[2], # around current page
*@last-size[3]+1..@last+1 # final pages till @last+1
].sort!.each_cons(2) do |left, right| # sort and loop by 2
next if left.negative? || left == right # skip out of range and duplicates
break if left > @last # break if out of @last boundary
case right
when left+1 then series.push(left) # no gap -> no additions
when left+2 then series.push(left, left+1) # 1 page gap -> fill with missing page
else series.push(left, :gap) # n page gap -> add gap
end
end
series.shift # shift the start boundary (0)
series[series.index(@page)] = @page.to_s # convert the current page to String
end
end

end
Expand Down
7 changes: 4 additions & 3 deletions lib/pagy/backend.rb
@@ -1,5 +1,4 @@
# See Pagy::Backend API documentation: https://ddnexus.github.io/pagy/api/backend
# encoding: utf-8
# frozen_string_literal: true

class Pagy
Expand All @@ -8,12 +7,14 @@ class Pagy

# See also the extras if you need specialized methods to paginate Arrays or other collections

module Backend ; private # the whole module is private so no problem with including it in a controller

module Backend
private # the whole module is private so no problem with including it in a controller

# Return Pagy object and items
def pagy(collection, vars={})
pagy = Pagy.new(pagy_get_vars(collection, vars))
return pagy, pagy_get_items(collection, pagy)
[ pagy, pagy_get_items(collection, pagy) ]
end

# Sub-method called only by #pagy: here for easy customization of variables by overriding
Expand Down
18 changes: 10 additions & 8 deletions lib/pagy/countless.rb
@@ -1,4 +1,3 @@
# encoding: utf-8
# frozen_string_literal: true

require 'pagy'
Expand All @@ -7,23 +6,26 @@ class Pagy

class Countless < Pagy

INSTANCE_VARS_MIN = { items: 1, page: 1, outset: 0 }.freeze

# Merge and validate the options, do some simple arithmetic and set a few instance variables
def initialize(vars={}) # rubocop:disable Lint/MissingSuper
@vars = VARS.merge(vars.delete_if{|_,v| v.nil? || v == '' }) # default vars + cleaned vars (can be overridden)
{ items:1, outset:0, page:1 }.each do |k,min| # validate instance variables
(@vars[k] && instance_variable_set(:"@#{k}", @vars[k].to_i) >= min) \
or raise(VariableError.new(self), "expected :#{k} >= #{min}; got #{@vars[k].inspect}")
INSTANCE_VARS_MIN.each do |k,min| # validate instance variables
raise VariableError.new(self), "expected :#{k} >= #{min}; got #{@vars[k].inspect}" \
unless @vars[k] && instance_variable_set(:"@#{k}", @vars[k].to_i) >= min
end
@offset = @items * (@page - 1) + @outset # pagination offset + outset (initial offset)
end

# Finalize the instance variables based on the fetched items
def finalize(fetched)
fetched == 0 && @page > 1 and raise(OverflowError.new(self), "page #{@page} got no items")
raise OverflowError.new(self), "page #{@page} got no items" \
if fetched.zero? && @page > 1
@pages = @last = (fetched > @items ? @page + 1 : @page) # set the @pages and @last
@items = fetched if fetched < @items && fetched > 0 # adjust items for last non-empty page
@from = fetched == 0 ? 0 : @offset + 1 - @outset # page begins from item
@to = fetched == 0 ? 0 : @offset + @items - @outset # page ends to item
@items = fetched if fetched < @items && fetched.positive? # adjust items for last non-empty page
@from = fetched.zero? ? 0 : @offset + 1 - @outset # page begins from item
@to = fetched.zero? ? 0 : @offset + @items - @outset # page ends to item
@prev = (@page-1 unless @page == 1) # nil if no prev page
@next = @page == @last ? (1 if @vars[:cycle]) : @page + 1 # nil if no next page, 1 if :cycle
self
Expand Down
8 changes: 6 additions & 2 deletions lib/pagy/exceptions.rb
@@ -1,5 +1,8 @@
# frozen_string_literal: true

class Pagy

# generic variable error
class VariableError < ArgumentError
attr_reader :pagy

Expand All @@ -9,13 +12,14 @@ def initialize(pagy)
end

def variable
message =~ /expected :([\w]+)/
$1.to_sym if $1
message =~ /expected :(\w+)/
Regexp.last_match(1)&.to_sym
end

def value = pagy.vars[variable]
end

# specific overflow error
class OverflowError < VariableError; end

end
66 changes: 44 additions & 22 deletions lib/pagy/frontend.rb
@@ -1,5 +1,4 @@
# See Pagy::Frontend API documentation: https://ddnexus.github.io/pagy/api/frontend
# encoding: utf-8
# frozen_string_literal: true

require 'yaml'
Expand All @@ -10,11 +9,11 @@ class Pagy

# I18n static hash loaded at startup, used as default alternative to the i18n gem.
# see https://ddnexus.github.io/pagy/api/frontend#i18n
I18n = eval(Pagy.root.join('locales', 'utils', 'i18n.rb').read) #rubocop:disable Security/Eval
I18n = eval Pagy.root.join('locales', 'utils', 'i18n.rb').read #rubocop:disable Security/Eval

module Helpers
# This works with all Rack-based frameworks (Sinatra, Padrino, Rails, ...)
def pagy_url_for(page, pagy, url=false)
def pagy_url_for(page, pagy, url=nil)
p_vars = pagy.vars
params = request.GET.merge(p_vars[:params])
params[p_vars[:page_param].to_s] = page
Expand All @@ -33,42 +32,65 @@ module Frontend

# Generic pagination: it returns the html with the series of links to the pages
def pagy_nav(pagy)
link, p_prev, p_next = pagy_link_proc(pagy), pagy.prev, pagy.next

html = (p_prev ? %(<span class="page prev">#{link.call p_prev, pagy_t('pagy.nav.prev'), 'aria-label="previous"'}</span> )
: %(<span class="page prev disabled">#{pagy_t('pagy.nav.prev')}</span> ))
link = pagy_link_proc(pagy)
p_prev = pagy.prev
p_next = pagy.next
html = if p_prev
%(<span class="page prev">#{link.call p_prev, pagy_t('pagy.nav.prev'), 'aria-label="previous"'}</span> )
else
%(<span class="page prev disabled">#{pagy_t('pagy.nav.prev')}</span> )
end
pagy.series.each do |item| # series example: [1, :gap, 7, 8, "9", 10, 11, :gap, 36]
html << if item.is_a?(Integer); %(<span class="page">#{link.call item}</span> ) # page link
elsif item.is_a?(String) ; %(<span class="page active">#{item}</span> ) # current page
elsif item == :gap ; %(<span class="page gap">#{pagy_t('pagy.nav.gap')}</span> ) # page gap
html << case item
when Integer then %(<span class="page">#{link.call item}</span> ) # page link
when String then %(<span class="page active">#{item}</span> ) # current page
when :gap then %(<span class="page gap">#{pagy_t('pagy.nav.gap')}</span> ) # page gap
end
end
html << (p_next ? %(<span class="page next">#{link.call p_next, pagy_t('pagy.nav.next'), 'aria-label="next"'}</span>)
: %(<span class="page next disabled">#{pagy_t('pagy.nav.next')}</span>))
html << if p_next
%(<span class="page next">#{link.call p_next, pagy_t('pagy.nav.next'), 'aria-label="next"'}</span>)
else
%(<span class="page next disabled">#{pagy_t('pagy.nav.next')}</span>)
end
%(<nav class="pagy-nav pagination" role="navigation" aria-label="pager">#{html}</nav>)
end

# Return examples: "Displaying items 41-60 of 324 in total" of "Displaying Products 41-60 of 324 in total"
def pagy_info(pagy, item_name=nil)
key = if (count = pagy.count) == 0 ; 'pagy.info.no_items'
else pagy.pages == 1 ? 'pagy.info.single_page' : 'pagy.info.multiple_pages'
end
pagy_t(key, item_name: item_name || pagy_t(pagy.vars[:i18n_key], count: count), count: count, from: pagy.from, to: pagy.to)
count = pagy.count
key = if count.zero?
'pagy.info.no_items'
elsif pagy.pages == 1
'pagy.info.single_page'
else
'pagy.info.multiple_pages'
end
pagy_t key, item_name: item_name || pagy_t(pagy.vars[:i18n_key], count: count),
count: count,
from: pagy.from,
to: pagy.to
end

# Returns a performance optimized proc to generate the HTML links
# Benchmarked on a 20 link nav: it is ~22x faster and uses ~18x less memory than rails' link_to
def pagy_link_proc(pagy, link_extra='')
p_prev, p_next = pagy.prev, pagy.next
a, b = %(<a href="#{pagy_url_for(PAGE_PLACEHOLDER, pagy)}" #{pagy.vars[:link_extra]} #{link_extra}).split(PAGE_PLACEHOLDER, 2)
lambda {|n, text=n, extra=''| "#{a}#{n}#{b}#{ if n == p_prev ; ' rel="prev"'
elsif n == p_next ; ' rel="next"'
else '' end } #{extra}>#{text}</a>"}
p_prev = pagy.prev
p_next = pagy.next
left, right = %(<a href="#{pagy_url_for(PAGE_PLACEHOLDER, pagy)}" #{pagy.vars[:link_extra]} #{link_extra}).split(PAGE_PLACEHOLDER, 2)
lambda do |num, text=num, extra=''|
"#{left}#{num}#{right}#{ case num
when p_prev then ' rel="prev"'
when p_next then ' rel="next"'
else ''
end } #{extra}>#{text}</a>"
end
end

# Similar to I18n.t: just ~18x faster using ~10x less memory
# (@pagy_locale explicitly initilized in order to avoid warning)
def pagy_t(key, **opts) = Pagy::I18n.t(@pagy_locale||=nil, key, **opts)
def pagy_t(key, **opts)
Pagy::I18n.t @pagy_locale||=nil, key, **opts
end

end
end

0 comments on commit b638764

Please sign in to comment.