Skip to content

Instantly share code, notes, and snippets.

@sionta
Created February 5, 2025 07:34
Show Gist options
  • Save sionta/731944b992299f57116d2a2c7cd79e1f to your computer and use it in GitHub Desktop.
Save sionta/731944b992299f57116d2a2c7cd79e1f to your computer and use it in GitHub Desktop.
Jekyll Transclude Plugin
# frozen_string_literal: true
#
# Jekyll Transclude Plugin
#
# This plugin allows you to include content from files located in the `_includes` directory
# with dynamic parameters and block content. Use the `{% transclude %}` Liquid tag to utilize this feature.
#
# Features:
# - Supports passing block content via `include.content`.
# - Allows parameters to be defined and accessed in the included content.
# - Blocks cannot override the `content` key to prevent unexpected behavior.
# - Provides error handling for invalid syntax or missing files.
#
# Installation:
# - Place this file in your Jekyll project under `<Jekyll source dir>/_plugins/transclude.rb`.
#
# Usage:
# 1. To use the `transclude` tag in your markdown or HTML files, format it like this:
#
# {% transclude snippet.html title="Important Note" %}
# This content will be available as `include.content`.
# {% endtransclude %}
#
# 2. Inside the `_includes/snippet.html` file, you can access the parameters and content:
#
# <div class="note">
# <h2>{{ include.title | strip }}</h2>
# <div>{{ include.content | markdownify }}</div>
# </div>
#
# Restrictions:
# - The `content` parameter cannot be overridden with `param=value`. An error will be raised if attempted.
#
# Error Handling:
# - Invalid file names or parameter syntax will trigger detailed error messages.
# - Included files must exist in the `_includes` directory, and symbolic links are not allowed in safe mode.
#
# Author:
# - Andre Attamimi (https://github.com/sionta)
module Jekyll
module Tags
# TranscludeTag processes the `{% transclude %}` tag in Jekyll
class TranscludeTag < Liquid::Block
# Regular expression to validate parameters
VALID_SYNTAX = /
([\w-]+)\s*=\s* # Key (parameter name)
(?:"([^"\\]*(?:\\.[^"\\]*)*)" # Value in double quotes
|'([^'\\]*(?:\\.[^'\\]*)*)' # Value in single quotes
|([\w.-]+)) # Unquoted value
/x
# Constructor: Initializes the transclude tag with template and parameters
#
# @param tag_name [String] The name of the tag (not used here)
# @param markup [String] String containing the template name and parameters
# @param tokens [Array] Tokens for the Liquid template (not used here)
def initialize(tag_name, markup, tokens)
super
# Separate the template name and parameters from the markup
@template, @params = markup.strip.split(/\s+/, 2)
# Validation: Template must be provided
raise SyntaxError, 'Invalid syntax: Template file is required.' unless @template
end
# Main function: Renders the transclude tag
#
# @param context [Liquid::Context] The Jekyll rendering context
# @return [String] The rendered HTML output
def render(context)
site = context.registers[:site]
content = super.strip
template_path = find_template_path(site, @template)
# Validate the file: Ensure the file exists and is readable
raise "File not found: #{@template} in the _includes directory." unless template_path
raise "File not readable: #{template_path}" unless File.readable?(template_path)
# Parse the template file
partial = Liquid::Template.parse(File.read(template_path))
# Merge parameters and block content into the context
context.stack do
context['include'] = parse_params(context, content)
partial.render(context)
end
rescue StandardError => e
# Error handling: Log an error message to the terminal
Jekyll.logger.error 'Transclude Error:', e.message
raise e
end
private
# Finds the template path within the `_includes` directory
#
# @param site [Jekyll::Site] The Jekyll site object
# @param template [String] The name of the template file
# @return [String, nil] Full path of the file or `nil` if not found
def find_template_path(site, template)
includes_dirs = site.includes_load_paths
# Search for the file in each registered path
includes_dirs.each do |dir|
potential_path = File.join(dir, template)
return potential_path if File.exist?(potential_path)
end
nil
end
# Processes the parameters from the markup and combines them with block content
#
# @param context [Liquid::Context] The rendering context
# @param content [String] The block content
# @return [Hash] Parsed parameters
def parse_params(context, content)
params = { 'content' => content } # Add block content
return params unless @params # Return only `content` if no parameters provided
markup = @params.dup
# Parse parameters from the markup
while (match = VALID_SYNTAX.match(markup))
markup = markup[match.end(0)..] # Continue parsing after the match
key = match[1]
value = match[2] || match[3] || context[match[4]]
# Warning: Do not override `content`
if key == 'content'
Jekyll.logger.warn 'Transclude Warning:', "Cannot override 'content' in transclude; using block content."
else
params[key] = value
end
end
params
end
end
end
end
# Register the transclude tag with Liquid
Liquid::Template.register_tag('transclude', Jekyll::Tags::TranscludeTag)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment