diff --git a/lib/jekyll.rb b/lib/jekyll.rb index 15e7c90c8fc..a51894cc942 100644 --- a/lib/jekyll.rb +++ b/lib/jekyll.rb @@ -55,6 +55,7 @@ module Jekyll autoload :FrontmatterDefaults, "jekyll/frontmatter_defaults" autoload :Hooks, "jekyll/hooks" autoload :Layout, "jekyll/layout" + autoload :Inclusion, "jekyll/inclusion" autoload :Cache, "jekyll/cache" autoload :CollectionReader, "jekyll/readers/collection_reader" autoload :DataReader, "jekyll/readers/data_reader" diff --git a/lib/jekyll/inclusion.rb b/lib/jekyll/inclusion.rb new file mode 100644 index 00000000000..179ddb045cb --- /dev/null +++ b/lib/jekyll/inclusion.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Jekyll + class Inclusion + attr_reader :site, :name, :path + private :site + + def initialize(site, base, name) + @site = site + @name = name + @path = PathManager.join(base, name) + end + + def render(context) + @template ||= site.liquid_renderer.file(path).parse(content) + @template.render!(context) + rescue Liquid::Error => e + e.template_name = path + e.markup_context = "included " if e.markup_context.nil? + raise e + end + + def content + @content ||= File.read(path, **site.file_read_opts) + end + + def inspect + "#{self.class} #{path.inspect}" + end + alias_method :to_s, :inspect + end +end diff --git a/lib/jekyll/site.rb b/lib/jekyll/site.rb index b90999e7dca..76c1327fe77 100644 --- a/lib/jekyll/site.rb +++ b/lib/jekyll/site.rb @@ -3,7 +3,7 @@ module Jekyll class Site attr_reader :source, :dest, :cache_dir, :config - attr_accessor :layouts, :pages, :static_files, :drafts, + attr_accessor :layouts, :pages, :static_files, :drafts, :inclusions, :exclude, :include, :lsi, :highlighter, :permalink_style, :time, :future, :unpublished, :safe, :plugins, :limit_posts, :show_drafts, :keep_files, :baseurl, :data, :file_read_opts, @@ -86,6 +86,7 @@ def print_stats Jekyll.logger.info @liquid_renderer.stats_table end + # rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/MethodLength # # Reset Site details. @@ -98,6 +99,7 @@ def reset Time.now end self.layouts = {} + self.inclusions = {} self.pages = [] self.static_files = [] self.data = {} @@ -117,6 +119,7 @@ def reset Jekyll::Hooks.trigger :site, :after_reset, self end # rubocop:enable Metrics/MethodLength + # rubocop:enable Metrics/AbcSize # Load necessary libraries, plugins, converters, and generators. # diff --git a/lib/jekyll/tags/include.rb b/lib/jekyll/tags/include.rb index 49ffcb9efbf..cc6b145102f 100644 --- a/lib/jekyll/tags/include.rb +++ b/lib/jekyll/tags/include.rb @@ -18,12 +18,13 @@ class IncludeTag < Liquid::Tag def initialize(tag_name, markup, tokens) super - matched = markup.strip.match(VARIABLE_SYNTAX) + markup = markup.strip + matched = markup.match(VARIABLE_SYNTAX) if matched @file = matched["variable"].strip @params = matched["params"].strip else - @file, @params = markup.strip.split(%r!\s+!, 2) + @file, @params = markup.split(%r!\s+!, 2) end validate_params if @params @tag_name = tag_name @@ -192,6 +193,60 @@ def could_not_locate_message(file, includes_dirs, safe) end end + # Do not inherit from this class. + # TODO: Merge into the `Jekyll::Tags::IncludeTag` in v5.0 + class OptimizedIncludeTag < IncludeTag + def render(context) + @site ||= context.registers[:site] + + file = render_variable(context) || @file + validate_file_name(file) + + @site.inclusions[file] ||= locate_include_file(file) + inclusion = @site.inclusions[file] + + add_include_to_dependency(inclusion, context) if @site.incremental? + + context.stack do + context["include"] = parse_params(context) if @params + inclusion.render(context) + end + end + + private + + def locate_include_file(file) + @site.includes_load_paths.each do |dir| + path = PathManager.join(dir, file) + return Inclusion.new(@site, dir, file) if valid_include_file?(path, dir) + end + raise IOError, could_not_locate_message(file, @site.includes_load_paths, @site.safe) + end + + def valid_include_file?(path, dir) + File.file?(path) && !outside_scope?(path, dir) + end + + def outside_scope?(path, dir) + @site.safe && !realpath_prefixed_with?(path, dir) + end + + def realpath_prefixed_with?(path, dir) + File.realpath(path).start_with?(dir) + rescue StandardError + false + end + + def add_include_to_dependency(inclusion, context) + return unless context.registers[:page]&.key?("path") + + @site.regenerator.add_dependency( + @site.in_source_dir(context.registers[:page]["path"]), + inclusion.path + ) + end + end + class IncludeRelativeTag < IncludeTag def tag_includes_dirs(context) Array(page_path(context)).freeze @@ -217,5 +272,5 @@ def page_path(context) end end -Liquid::Template.register_tag("include", Jekyll::Tags::IncludeTag) +Liquid::Template.register_tag("include", Jekyll::Tags::OptimizedIncludeTag) Liquid::Template.register_tag("include_relative", Jekyll::Tags::IncludeRelativeTag)