# frozen_string_literal: true
module Jekyll
class Renderer
attr_reader :document, :site
attr_writer :layouts, :payload
def initialize(site, document, site_payload = nil)
@site = site
@document = document
@payload = site_payload
@layouts = nil
end
# Fetches the payload used in Liquid rendering.
# It can be written with #payload=(new_payload)
# Falls back to site.site_payload if no payload is set.
#
# Returns a Jekyll::Drops::UnifiedPayloadDrop
def payload
@payload ||= site.site_payload
end
# The list of layouts registered for this Renderer.
# It can be written with #layouts=(new_layouts)
# Falls back to site.layouts if no layouts are registered.
#
# Returns a Hash of String => Jekyll::Layout identified
# as basename without the extension name.
def layouts
@layouts || site.layouts
end
# Determine which converters to use based on this document's
# extension.
#
# Returns Array of Converter instances.
def converters
@converters ||= site.converters.select { |c| c.matches(document.extname) }.tap(&:sort!)
end
# Determine the extname the outputted file should have
#
# Returns String the output extname including the leading period.
def output_ext
@output_ext ||= (permalink_ext || converter_output_ext)
end
# Prepare payload and render the document
#
# Returns String rendered document output
def run
Jekyll.logger.debug "Rendering:", document.relative_path
assign_pages!
assign_current_document!
assign_highlighter_options!
assign_layout_data!
Jekyll.logger.debug "Pre-Render Hooks:", document.relative_path
document.trigger_hooks(:pre_render, payload)
render_document
end
# Render the document.
#
# Returns String rendered document output
# rubocop: disable Metrics/AbcSize, Metrics/MethodLength
def render_document
info = {
:registers => { :site => site, :page => payload["page"] },
:strict_filters => liquid_options["strict_filters"],
:strict_variables => liquid_options["strict_variables"],
}
output = document.content
if document.render_with_liquid?
Jekyll.logger.debug "Rendering Liquid:", document.relative_path
output = render_liquid(output, payload, info, document.path)
end
Jekyll.logger.debug "Rendering Markup:", document.relative_path
output = convert(output.to_s)
document.content = output
Jekyll.logger.debug "Post-Convert Hooks:", document.relative_path
document.trigger_hooks(:post_convert)
output = document.content
if document.place_in_layout?
Jekyll.logger.debug "Rendering Layout:", document.relative_path
output = place_in_layouts(output, payload, info)
end
output
end
# rubocop: enable Metrics/AbcSize, Metrics/MethodLength
# Convert the document using the converters which match this renderer's document.
#
# Returns String the converted content.
def convert(content)
converters.reduce(content) do |output, converter|
converter.convert output
rescue StandardError => e
Jekyll.logger.error "Conversion error:",
"#{converter.class} encountered an error while " \
"converting '#{document.relative_path}':"
Jekyll.logger.error("", e.to_s)
raise e
end
end
# Render the given content with the payload and info
#
# content -
# payload -
# info -
# path - (optional) the path to the file, for use in ex
#
# Returns String the content, rendered by Liquid.
def render_liquid(content, payload, info, path = nil)
template = site.liquid_renderer.file(path).parse(content)
template.warnings.each do |e|
Jekyll.logger.warn "Liquid Warning:",
LiquidRenderer.format_error(e, path || document.relative_path)
end
template.render!(payload, info)
# rubocop: disable Lint/RescueException
rescue Exception => e
Jekyll.logger.error "Liquid Exception:",
LiquidRenderer.format_error(e, path || document.relative_path)
raise e
end
# rubocop: enable Lint/RescueException
# Checks if the layout specified in the document actually exists
#
# layout - the layout to check
#
# Returns Boolean true if the layout is invalid, false if otherwise
def invalid_layout?(layout)
!document.data["layout"].nil? && layout.nil? && !(document.is_a? Jekyll::Excerpt)
end
# Render layouts and place document content inside.
#
# Returns String rendered content
def place_in_layouts(content, payload, info)
output = content.dup
layout = layouts[document.data["layout"].to_s]
validate_layout(layout)
used = Set.new([layout])
# Reset the payload layout data to ensure it starts fresh for each page.
payload["layout"] = nil
while layout
output = render_layout(output, layout, info)
add_regenerator_dependencies(layout)
next unless (layout = site.layouts[layout.data["layout"]])
break if used.include?(layout)
used << layout
end
output
end
private
# Checks if the layout specified in the document actually exists
#
# layout - the layout to check
# Returns nothing
def validate_layout(layout)
return unless invalid_layout?(layout)
Jekyll.logger.warn "Build Warning:", "Layout '#{document.data["layout"]}' requested " \
"in #{document.relative_path} does not exist."
end
# Render layout content into document.output
#
# Returns String rendered content
def render_layout(output, layout, info)
payload["content"] = output
payload["layout"] = Utils.deep_merge_hashes(layout.data, payload["layout"] || {})
render_liquid(
layout.content,
payload,
info,
layout.path
)
end
def add_regenerator_dependencies(layout)
return unless document.write?
site.regenerator.add_dependency(
site.in_source_dir(document.path),
layout.path
)
end
# Set page content to payload and assign pager if document has one.
#
# Returns nothing
def assign_pages!
payload["page"] = document.to_liquid
payload["paginator"] = (document.pager.to_liquid if document.respond_to?(:pager))
end
# Set related posts to payload if document is a post.
#
# Returns nothing
def assign_current_document!
payload["site"].current_document = document
end
# Set highlighter prefix and suffix
#
# Returns nothing
def assign_highlighter_options!
payload["highlighter_prefix"] = converters.first.highlighter_prefix
payload["highlighter_suffix"] = converters.first.highlighter_suffix
end
def assign_layout_data!
layout = layouts[document.data["layout"]]
payload["layout"] = Utils.deep_merge_hashes(layout.data, payload["layout"] || {}) if layout
end
def permalink_ext
document_permalink = document.permalink
if document_permalink && !document_permalink.end_with?("/")
permalink_ext = File.extname(document_permalink)
permalink_ext unless permalink_ext.empty?
end
end
def converter_output_ext
if output_exts.size == 1
output_exts.last
else
output_exts[-2]
end
end
def output_exts
@output_exts ||= converters.map do |c|
c.output_ext(document.extname)
end.tap(&:compact!)
end
def liquid_options
@liquid_options ||= site.config["liquid"]
end
end
end