lib/middleman-blog/paginator.rb
# frozen_string_literal: true
require 'middleman-blog/uri_templates'
module Middleman
module Blog
# A sitemap plugin that splits indexes (including tag
# and calendar indexes) over multiple pages
class Paginator
include UriTemplates
def initialize(app, blog_controller)
@app = app
@blog_controller = blog_controller
@per_page = blog_controller.options.per_page
@page_link = blog_controller.options.page_link
end
# Update the main sitemap resource list
# @return [void]
def manipulate_resource_list(resources)
new_resources = []
resources.each do |res|
next if res.ignored?
# Avoid recomputing metadata over and over
md = res.metadata
next unless md[:page][:pageable]
# Skip other blogs' resources
next unless match_blog(res, md)
# "articles" local variable is populated by Calendar and Tag page generators
# If it's not set then use the complete list of articles
# TODO: Some way to allow the frontmatter to specify the article filter?
articles = md[:locals]['articles'] || @blog_controller.data.articles
articles.select! { |article| article.lang == md[:options][:locale] } if md.fetch(:options, false) && md[:options].fetch(:locale, false)
# Allow blog.per_page and blog.page_link to be overridden in the frontmatter
per_page = md[:page][:per_page] || @per_page
page_link = uri_template(md[:page][:page_link] || @page_link)
num_pages = (articles.length / per_page.to_f).ceil
# Add the pagination metadata to the base page (page 1)
res.add_metadata locals: page_locals(1, num_pages, per_page, nil, articles)
prev_page_res = res
# Create additional resources for the 2nd and subsequent pages.
2.upto(num_pages) do |page_num|
p = page_resource(res, page_num, page_link)
# Copy the metadata from the base page
p.add_metadata md
p.add_metadata locals: page_locals(page_num, num_pages, per_page, prev_page_res, articles)
# Add a reference in the previous page to this page
prev_page_res.add_metadata locals: { 'next_page' => p }
prev_page_res = p
new_resources << p
end
end
resources + new_resources
end
private
# Does this resource match the blog controller for this paginator?
# @return [Boolean]
def match_blog(res, md)
res_controller = md[:locals]['blog_controller'] || (res.respond_to?(:blog_controller) && res.blog_controller)
return false if res_controller && res_controller != @blog_controller
override_controller = md[:page][:blog]
return false if override_controller && override_controller.to_s != @blog_controller.name.to_s
true
end
# Generate a resource for a particular page
# @param [Sitemap::Resource] res the original resource
# @param [Integer] page_num the page number to generate a resource for
# @param [String] page_link The pagination link path component template
def page_resource(res, page_num, page_link)
path = page_sub(res, page_num, page_link)
if res.is_a? Sitemap::ProxyResource
Sitemap::ProxyResource.new(@app.sitemap, path, res.target)
else
Sitemap::Resource.new(@app.sitemap, path, res.source_file)
end
end
# @param [Integer] page_num the page number to generate a resource for
# @param [Integer] num_pages Total number of pages
# @param [Integer] per_page How many articles per page
# @param [Sitemap::Resource] prev_page_res The resource of the previous page
# @param [Array<Sitemap::Resource>] articles The list of all articles
def page_locals(page_num, num_pages, per_page, prev_page_res, articles)
# Index into articles of the first article of this page
page_start = (page_num - 1) * per_page
# Index into articles of the last article of this page
page_end = (page_num * per_page) - 1
{
# Set a flag to allow templates to be used with and without pagination
'paginate' => true,
# Include the numbers, useful for displaying "Page X of Y"
'page_number' => page_num,
'num_pages' => num_pages,
'per_page' => per_page,
# The range of article numbers on this page
# (1-based, for showing "Items X to Y of Z")
'page_start' => page_start + 1,
'page_end' => [page_end + 1, articles.length].min,
# These contain the next and previous page.
# They are set to nil if there are no more pages.
# The nils are overwritten when the later pages are generated, below.
'next_page' => nil,
'prev_page' => prev_page_res,
# The list of articles for this page.
'page_articles' => articles[page_start..page_end],
# Include the articles so that non-proxied pages can use "articles" instead
# of "blog.articles" for consistency with the calendar and tag templates.
'articles' => articles,
'blog_controller' => @blog_controller
}
end
# Substitute the page number into the resource URL.
# @param [Middleman::Sitemap::Resource] res The resource to generate pages for
# @param [Integer] page_num The page page_number
# @param [String] page_link The pagination link path component template
# @return [String]
def page_sub(res, page_num, page_link)
if page_num == 1
# First page has an unmodified URL.
res.path
else
page_url = apply_uri_template page_link, num: page_num
index_re = %r{(^|/)#{Regexp.escape(@app.config[:index_file])}$}
if res.path&.match?(index_re)
res.path.sub(index_re, "\\1#{page_url}/#{@app.config[:index_file]}")
else
res.path.sub(%r{(^|/)([^/]*)\.([^/]*)$}, "\\1\\2/#{page_url}.\\3")
end
end
end
end
end
end