168 lines
5.4 KiB
Ruby
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
|