Created
February 5, 2025 07:34
-
-
Save sionta/731944b992299f57116d2a2c7cd79e1f to your computer and use it in GitHub Desktop.
Jekyll Transclude Plugin
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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