Files

168 lines
5.4 KiB
Ruby

# frozen_string_literal: true
module Jekyll
# @see {_docs/router.markdown}
class Router
# Generate pages (routes)
#
# @yield A user defined block, with page as optional first parameter.
def self.draw(&)
Jekyll::Hooks.register :site, :post_read, priority: 29 do |site|
Jekyll::Router.new(site).instance_eval(&)
end
end
# Current Jekyll site
#
# @return [Jekyll::Site]
attr_reader :site
# @param site [Jekyll::Site]
def initialize(site)
@site = site
end
# Draws a route by generating a combination of all arguments given.
#
# @see {https://jekyllrb.com/docs/permalinks/#global}
# @param url_template [String] Jekyll URL template
# @param args [Hash] Symbolized hash of options
# @yieldparam page [Jekyll::Page] Generated page
# @yieldparam args [Hash] Arguments for this page
# @yieldreturn [Jekyll::Page] Same page
def get(url_template, **args, &block)
paginate_by = args.delete(:paginate_by)
args[:layout] ||= args.keys.sort.map(&:to_s).join('_') unless args.empty?
args[:layout] ||= url_template.gsub(/:[a-z_]+/, '').tr('.', '_').tr('/', '_').squeeze('_').sub(/\A_/, '').sub(
/_\z/, ''
)
args[:ext] ||= File.extname(url_template)
args[:ext] = '.html' if args[:ext].empty?
# Turn values into arrays so we can combine them
args = args.transform_values do |value|
case value
when Array then value
when String, Integer, Float, TrueClass, FalseClass, NilClass then [value]
else
raise ArgumentError, "#{value.class} parameters are not supported yet!"
end
end
# Generate every possible combination of arguments. Every
# combination is a single page.
#
# TODO: Set a hard limit to pages instead of warning?
page_count = args.values.map(&:count).reduce(&:*)
Jekyll.logger.info 'Router:', "Generating #{page_count} pages for #{url_template}..."
Jekyll.logger.warn('Router:', 'This route creates too many pages!') if page_count > 1_000
# Generate every combination of values
combination_of_args = args.values.reduce(&:product).map(&:flatten).map do |x|
args.keys.zip(x)
end.map(&:to_h)
# And create a page for each
combination_of_args.each do |page_args|
create_page(page_args).tap do |page|
# Set the URL from the template
page.data['permalink'] = URL.new(template: url_template, placeholders: page_args).to_s
if block_given?
# Support different parameters styles, but `page` must be
# always the first parameter.
if block.arity.zero?
yield
else
params = page_args.slice(*block_params(block))
style = block_params_style(block)
case style
when :opt
yield(page, *params.values)
when :keyreq
yield(page, **params)
when :keyrest
yield(page, **page_args)
else
yield(page)
end
end
end
# we paginate after the block yields
next unless paginate_by
unless page.data[paginate_by].is_a? Array
Jekyll.logger.warn 'Router:', "#{paginate_by} needs to be an array! Skipping"
next
end
prev_page = page
page_url_template = (page.url + '/p/:page/').squeeze('/')
# TODO: make page size configurable
page.data[paginate_by].each_slice(20).each_with_index do |slice, current_page|
case current_page
# replace first page with first slice, order is assumed
when 0 then page.data[paginate_by] = slice
else
placeholders = { page: current_page + 1 }
create_page(page_args).tap do |next_page|
# Set the URL from the template
next_page.data['permalink'] = URL.new(template: page_url_template, placeholders:).to_s
next_page.data['prev_page'] = prev_page
prev_page.data['next_page'] = next_page
next_page.data[paginate_by] = slice
prev_page = next_page
end
end
end
end
end
end
# @param page_args [Hash]
# @return [Jekyll::PageWithoutAFile]
def create_page(page_args)
Jekyll::PageWithoutAFile.new(site, '', '', '').tap do |page|
# Remove the default proc
page.data.default_proc = nil
# Add to the pages list so it's rendered!
site.pages << page
# Set the extension
page.ext = page_args[:ext]
# Set data
page.data.merge! page_args.transform_keys(&:to_s)
end
end
private
# @param block [Proc]
# @return [Array<Symbol>]
def block_params(block)
@block_params ||= {}
@block_params[block.hash] ||= block.parameters.map(&:last)
end
# @param block [Proc]
# @return [Symbol]
def block_params_style(block)
@block_params_style ||= {}
@block_params_style[block.hash] ||=
begin
param_styles = block.parameters.map(&:first).tap(&:shift).uniq
raise ArgumentError, 'Mixing block parameters is not supported' if param_styles.count > 1
param_styles.first
end
end
end
end