This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
plugins:
|
||||
# - rubocop-jekyll
|
||||
- rubocop-md
|
||||
- rubocop-performance
|
||||
- rubocop-rake
|
||||
- rubocop-rspec
|
||||
|
||||
AllCops:
|
||||
Exclude:
|
||||
- binstub/**/*
|
||||
- demo/_site/*
|
||||
- vendor/**/*
|
||||
- Gemfile*
|
||||
NewCops: enable
|
||||
|
||||
Gemspec/DeprecatedAttributeAssignment:
|
||||
Enabled: false
|
||||
|
||||
Gemspec/RequireMFA:
|
||||
Enabled: false
|
||||
|
||||
Gemspec/RequiredRubyVersion:
|
||||
Enabled: false
|
||||
|
||||
Layout/HashAlignment:
|
||||
EnforcedColonStyle: table
|
||||
EnforcedHashRocketStyle: table
|
||||
|
||||
Layout/IndentationConsistency:
|
||||
Exclude:
|
||||
- "*.md"
|
||||
|
||||
Layout/InitialIndentation:
|
||||
Exclude:
|
||||
- "*.md"
|
||||
|
||||
Layout/LineLength:
|
||||
Max: 150
|
||||
|
||||
Metrics/AbcSize:
|
||||
Max: 55
|
||||
|
||||
Metrics/BlockLength:
|
||||
Exclude:
|
||||
- jekyll_plugin_support.gemspec
|
||||
Max: 30
|
||||
|
||||
Metrics/ClassLength:
|
||||
Exclude:
|
||||
- "**/all_collections_tag.rb"
|
||||
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 25
|
||||
|
||||
Metrics/MethodLength:
|
||||
Max: 50
|
||||
|
||||
Metrics/ModuleLength:
|
||||
Exclude:
|
||||
- "**/jekyll_plugin_support_class.rb"
|
||||
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 25
|
||||
|
||||
Naming/FileName:
|
||||
Exclude:
|
||||
- Rakefile
|
||||
- "*.md"
|
||||
|
||||
RSpec/ExampleLength:
|
||||
Max: 30
|
||||
|
||||
RSpec/SpecFilePathFormat:
|
||||
Enabled: false
|
||||
IgnoreMethods: true
|
||||
|
||||
RSpec/SpecFilePathSuffix:
|
||||
Enabled: false
|
||||
|
||||
RSpec/IndexedLet:
|
||||
Enabled: false
|
||||
|
||||
RSpec/MultipleExpectations:
|
||||
Max: 15
|
||||
|
||||
Style/ClassVars:
|
||||
Enabled: false
|
||||
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
|
||||
Style/FrozenStringLiteralComment:
|
||||
Enabled: false
|
||||
|
||||
Style/StringConcatenation:
|
||||
Enabled: false
|
||||
|
||||
Style/TrailingCommaInHashLiteral:
|
||||
EnforcedStyleForMultiline: comma
|
||||
@@ -0,0 +1,4 @@
|
||||
# Notes on Attribution
|
||||
|
||||
The following additional public methods could be supported for the attribution string:
|
||||
`full_name`, `conflicts`, `dependencies`, `platform`, `specification_version`, `summary`, `cert_chain`, `description`, `email`, `extensions`, `licenses`, `metadata`, `post_install_message`, `rg_full_gem_path`, `location`, `git_version`, `rg_extension_dir`, `nondevelopment_dependencies`, `default_executable`, `original_platform`, `gems_dir`, `groups`, `requirements`, `bin_file`, `executables`, `bindir`, `loaded_from`, `hash`, `files`, `spec_file`, `abbreviate`, `bin_dir`, `default_value`, `doc_dir`, `mark_version`, `executable`, `file_name`, `rubygems_version`, `lib_files`, `license`, `name_tuple`, `original_name`, `require_path`, `ri_dir`, `spec_dir`, `spec_name`, `source`, `validate_permissions`, `extension_dir`, `full_gem_path`, `remote`, `base_dir`, `gem_dir`, `normalize`, `match_platform`, `plugins`, `source_paths`, `datadir`, `require_paths`, and `this`.
|
||||
@@ -0,0 +1,216 @@
|
||||
# Change Log
|
||||
|
||||
## 3.0.0 / 2025-02-09
|
||||
|
||||
* Numbered as v3.0.0 because `jekyll_draft` was at v2.1.0 and the two projects should have similar version numbers.
|
||||
* `Jekyll_all_collections` was folded into this project.
|
||||
* Now requires [Jekyll 4.4.1](https://jekyllrb.com/news/2025/01/29/jekyll-4-4-1-released/) or later,
|
||||
and Ruby 3.2.0 or later
|
||||
|
||||
|
||||
## 1.1.0 / 2025-02-07
|
||||
|
||||
* Now replaces include and layout variables with their values
|
||||
* Renamed `@helper.markup` to `@helper.argument_string`
|
||||
* Improved `nil` handling throughout
|
||||
* Added the **Debug Demo production** launch configuration to debug the demo website in production mode
|
||||
* Added the `bin/release_port` script that kills hung processes being debugged
|
||||
* Added the `cleanDemo` task for the **Debug Demo production** and **Debug Demo development** tasks
|
||||
* Many minor improvements to the demo.
|
||||
* `JekyllSupport::JekyllPluginHelper::register` now accepts a `quiet` option, to suppress the signon message.
|
||||
Use it like this:
|
||||
|
||||
```ruby
|
||||
JekyllPluginHelper.register(self, 'tag_name', quiet: true)
|
||||
```
|
||||
|
||||
|
||||
## 1.0.3 / 2024-08-19
|
||||
|
||||
* Still fussing with error handling
|
||||
|
||||
|
||||
## 1.0.2 / 2024-08-19
|
||||
|
||||
* Computes line_number and path properly
|
||||
* Refactored demo CSS
|
||||
* Improved custom plugin error handling and sample code.
|
||||
The following seems to be optimal for custom plugins; it suppresses the ridiculously long stack trace that used to be generated:
|
||||
|
||||
```ruby
|
||||
begin
|
||||
# ...
|
||||
rescue DemoInlineTagError => e # jekyll_plugin_support handles StandardError
|
||||
@logger.error { e.logger_message }
|
||||
exit! 1 if @die_on_demo_tag_error
|
||||
|
||||
e.html_message
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
## 1.0.1 / 2024-07-27
|
||||
|
||||
* Moved `warn_short_trace`, `maybe_reraise_error`, `remove_ansi_color`,
|
||||
`format_error_message` and `exit_without_stack_trace` into `JekyllSupportError`.
|
||||
* Added missing `shared_include.css` and `clippy.svg` to `demo/`.
|
||||
* Fixed missing module name when invoking `format_error_message`.
|
||||
|
||||
|
||||
## 1.0.0 / 2024-07-23
|
||||
|
||||
* Added the `redef_without_warning` method so tag and block plugins can be subclassed.
|
||||
* Put most of the functionality into the `JekyllSupport` module.
|
||||
* Made tag and block code more consistent
|
||||
|
||||
|
||||
## 0.8.6 / 2024-06-11
|
||||
|
||||
* Fixup version, containing what was supposed to be in v0.8.5
|
||||
|
||||
|
||||
## 0.8.5 / 2024-03-25
|
||||
|
||||
* Empty block tags now return a value. See https://talk.jekyllrb.com/t/empty-liquid-block-not-rendering
|
||||
|
||||
|
||||
## 0.8.4 / 2024-02-27
|
||||
|
||||
* Problem in error handler fixed.
|
||||
* A warning is logged if an environment variable is undefined and `die_if_undefined` is not set.
|
||||
|
||||
|
||||
## 0.8.3 / 2024-01-05
|
||||
|
||||
* Variables defined in front matter of layouts and pages are now handled.
|
||||
|
||||
|
||||
## 0.8.2 / 2024-01-03
|
||||
|
||||
* `JekyllSupport::JekyllBlock` and `JekyllSupport::JekyllTag` subclasses now have automatically created error classes,
|
||||
named after the subclass.
|
||||
* Error class methods have been provided for standardized and convenient error handling:
|
||||
* `shorten_backtrace`
|
||||
* `logger_message`
|
||||
* `html_message`
|
||||
* Tags now self-report when registered.
|
||||
|
||||
|
||||
## 0.8.1 / 2023-12-10
|
||||
|
||||
* Added the `JekyllPluginHelper.remove_html_tags` method.
|
||||
|
||||
|
||||
## 0.8.0 / 2023-11-14
|
||||
|
||||
* Restructured for better support of Liquid variables.
|
||||
* Removed redundant class name in error message.
|
||||
* Improved handling of StandardError.
|
||||
* Demo site now shows how to define and handle custom errors.
|
||||
* Added `@tag_config` variable, which can be inspected to determine whether to die on various types of exceptions.
|
||||
* Added `format_error_message`, `maybe_reraise_error`, and `raise_error` to `JekyllBlock` and `JekyllTag`
|
||||
* Adds a method called `shorten_backtrace` to `StandardError` and all custom error
|
||||
classes defined by `JekyllSupport.define_error`.
|
||||
See the `demo/` directory for examples of how to use this optional feature.
|
||||
* The `plugin-vars` section in `_config.yml` was renamed to `liquid_vars`.
|
||||
|
||||
|
||||
## 0.7.2 / 2023-08-14
|
||||
|
||||
* Hopefully takes care of issue [#4](../../issues/4).
|
||||
|
||||
|
||||
## 0.7.1 / 2023-08-11
|
||||
|
||||
* Suppressed stack trace from issue [#4](../../issues/4).
|
||||
* Now using Shopify `ruby-lsp` instead of `rebornix.Ruby` by Peng Lv
|
||||
* Debug configurations changed to `rdbg` instead of `ruby`, so the `debase` development dependency was changed to
|
||||
[`debug`](https://marketplace.visualstudio.com/items?itemName=KoichiSasada.vscode-rdbg),
|
||||
which provides the `rdgb` command.
|
||||
The `ruby-debug-ide` development dependency is also no longer required.
|
||||
|
||||
|
||||
## 0.7.0 / 2023-05-22
|
||||
|
||||
* Added `error_short_trace` and `warn_short_trace` methods for displaying non-fatal errors on the console.
|
||||
|
||||
## 0.6.2 / 2023-05-15
|
||||
|
||||
* Removed annoying blank line output when attribute was invoked.
|
||||
|
||||
## 0.6.1 / 2023-04-12
|
||||
|
||||
* Added `CallChain.excerpt_caller` to detect if an excerpt is being generated.
|
||||
|
||||
|
||||
## 0.6.0 / 2023-04-05
|
||||
|
||||
* Added attribution support
|
||||
* Renamed `jekyll_plugin_support_helper.rb` to `jekyll_plugin_helper.rb`.
|
||||
* Now returns `undefined` instead of `false'` when `@keys_values.empty?`
|
||||
and parameter is not specified.
|
||||
|
||||
|
||||
## 0.5.3 / 2023-03-19
|
||||
|
||||
* Added properties `argv_original`, `keys_values_original`, `remaining_markup_original`,
|
||||
and `params_original`, which are the original values of `argv`, `keys_values`,
|
||||
`remaining_markup`, and `params`, before environment variable expansion.
|
||||
|
||||
|
||||
## 0.5.2 / 2023-03-17
|
||||
|
||||
* Added `@helper.remaining_markup` public method, which returns remaining markup passed to your tag,
|
||||
after keyword and name/value parsing is complete.
|
||||
* Finally wrote proper `rspec` tests.
|
||||
* Finally documented argument parsing.
|
||||
* Fixed bug introduced in v0.5.1 which did not remove elements from `@params`.
|
||||
|
||||
|
||||
## 0.5.1 / 2023-02-17
|
||||
|
||||
* `no_arg_parsing` optimization added.
|
||||
|
||||
|
||||
## 0.5.0 / 2023-02-15
|
||||
|
||||
* Plugins now register themselves
|
||||
* Plugins now report their name and version
|
||||
* `@layout`, `@paginator`, and `@theme` have values if supported by the version of Jekyll, and they are active.
|
||||
(See [Jekyll docs](https://jekyllrb.com/docs/variables/).)
|
||||
|
||||
|
||||
## 0.4.1 / 2023-02-14
|
||||
|
||||
* Fixed several problems
|
||||
* Added demo site
|
||||
|
||||
|
||||
## 0.4.0 / 2023-02-12
|
||||
|
||||
* `render_impl` for tags and blocks now predefines more instance variables:
|
||||
|
||||
* `@liquid_context` – passed to `render`
|
||||
* `@config` Jekyll configuration
|
||||
* `@envs` Environment variables
|
||||
* `@mode` ('development', 'test' or 'production')
|
||||
|
||||
|
||||
## 0.3.1 / 2023-02-07
|
||||
|
||||
* `JekyllBlock.render_impl` and `JekyllTag.render_impl` now define `@page` and `@site`.
|
||||
|
||||
|
||||
## 0.3.0 / 2023-01-14
|
||||
|
||||
* Added support for tags, which should subclass JekyllSupport::JekyllTag
|
||||
|
||||
|
||||
## 0.2.0 / 2023-01-12
|
||||
|
||||
* Refactored
|
||||
|
||||
|
||||
## 0.1.0 / 2023-01-10
|
||||
|
||||
* Initial version; only supports Jekyll block tags
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2020 Mike Slinn
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
require 'bundler/gem_tasks'
|
||||
require 'rspec/core/rake_task'
|
||||
|
||||
RSpec::Core::RakeTask.new(:spec)
|
||||
task default: :spec
|
||||
@@ -0,0 +1,38 @@
|
||||
require_relative 'lib/jekyll_plugin_support/version'
|
||||
|
||||
Gem::Specification.new do |spec|
|
||||
github = 'https://github.com/mslinn/jekyll_plugin_support'
|
||||
|
||||
spec.bindir = 'exe'
|
||||
spec.authors = ['Mike Slinn']
|
||||
spec.email = ['mslinn@mslinn.com']
|
||||
spec.files = Dir['.rubocop.yml', 'LICENSE.*', 'Rakefile', '{lib,spec}/**/*', '*.gemspec', '*.md']
|
||||
spec.homepage = 'https://www.mslinn.com/jekyll_plugins/jekyll_plugin_support.html'
|
||||
spec.license = 'MIT'
|
||||
spec.metadata = {
|
||||
'allowed_push_host' => 'https://rubygems.org',
|
||||
'bug_tracker_uri' => "#{github}/issues",
|
||||
'changelog_uri' => "#{github}/CHANGELOG.md",
|
||||
'homepage_uri' => spec.homepage,
|
||||
'source_code_uri' => github,
|
||||
}
|
||||
spec.name = 'jekyll_plugin_support'
|
||||
spec.platform = Gem::Platform::RUBY
|
||||
spec.post_install_message = <<~END_MESSAGE
|
||||
|
||||
Thanks for installing #{spec.name}!
|
||||
|
||||
END_MESSAGE
|
||||
spec.require_paths = ['lib']
|
||||
spec.required_ruby_version = '>= 2.6.0'
|
||||
spec.summary = 'Provides a framework for writing and testing Jekyll plugins'
|
||||
spec.test_files = spec.files.grep %r{^(test|spec|features)/}
|
||||
spec.version = JekyllPluginSupportVersion::VERSION
|
||||
|
||||
spec.add_dependency 'facets'
|
||||
spec.add_dependency 'jekyll', '>= 4.4.1'
|
||||
spec.add_dependency 'jekyll_plugin_logger'
|
||||
spec.add_dependency 'key-value-parser'
|
||||
spec.add_dependency 'pry'
|
||||
spec.add_dependency 'sorted_set'
|
||||
end
|
||||
@@ -0,0 +1,118 @@
|
||||
module JekyllSupport
|
||||
# Base class for Jekyll block tags
|
||||
class JekyllBlock < Liquid::Block
|
||||
attr_reader :argument_string, :helper, :line_number, :logger, :page, :site, :text
|
||||
|
||||
# See https://github.com/Shopify/liquid/wiki/Liquid-for-Programmers#create-your-own-tags
|
||||
# @param tag_name [String] the name of the tag, which we usually know.
|
||||
# @param argument_string [String] the arguments passed to the tag, as a single string.
|
||||
# @param parse_context [Liquid::ParseContext] contains the following attributes:
|
||||
# @depth might have the value 0
|
||||
# @error_mode might have the value `:strict`
|
||||
# @line_number duplicates @ptions[:line_number]
|
||||
# @locale duplicates @ptions[:locale]
|
||||
# @options is a hash with the following two keys that holds Liquid options:
|
||||
# :locale is a Liquid::I18n object, used to display localized error messages on Liquid built-in tags and filters.
|
||||
# :line_number is the line number containing the plugin invocation.
|
||||
# See https://github.com/Shopify/liquid/wiki/Liquid-for-Programmers#create-your-own-tags
|
||||
# @partial Boolean, unclear what this indicates
|
||||
# @template_options Replicates @options
|
||||
# @trim_whitespace might have the value `false`
|
||||
# @warnings array
|
||||
# @return [void]
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
@tag_name = tag_name
|
||||
raise JekyllPluginSupportError, "markup is a #{markup.class} with value '#{markup}'." unless markup.instance_of? String
|
||||
|
||||
# Lookup variable names with values in markup in render because site and config are not available here
|
||||
@argument_string = markup # Replace variable names with values in markup in render because site and config are not available here
|
||||
|
||||
@logger = PluginMetaLogger.instance.new_logger(self, PluginMetaLogger.instance.config)
|
||||
@logger.debug { "#{self.class}: respond_to?(:no_arg_parsing) #{respond_to?(:no_arg_parsing) ? 'yes' : 'no'}." }
|
||||
@helper = JekyllPluginHelper.new tag_name, markup, @logger, respond_to?(:no_arg_parsing)
|
||||
|
||||
@error_name = "#{tag_name.camelcase(:upper)}Error"
|
||||
::JekyllSupport::CustomError.factory @error_name
|
||||
end
|
||||
|
||||
# Liquid::Block subclasses do not render if there is no content within the tag
|
||||
# This override fixes that
|
||||
def blank?
|
||||
false
|
||||
end
|
||||
|
||||
# Method prescribed by the Jekyll plugin lifecycle.
|
||||
# Defines @config, @envs, @mode, @page and @site
|
||||
# @return [String]
|
||||
def render(liquid_context)
|
||||
@helper.liquid_context = ::JekyllSupport.inject_config_vars liquid_context # modifies liquid_context
|
||||
text = super # Liquid variable values in content are looked up and substituted
|
||||
|
||||
@envs = liquid_context.environments.first
|
||||
@page = liquid_context.registers[:page]
|
||||
@scopes = liquid_context.scopes
|
||||
@site = liquid_context.registers[:site]
|
||||
|
||||
@config = @site.config
|
||||
@tag_config = @config[@tag_name]
|
||||
@jps = @config['jekyll_plugin_support']
|
||||
@pry_on_standard_error = @jps['pry_on_standard_error'] || false if @jps
|
||||
|
||||
set_error_context
|
||||
|
||||
# @envs.keys are :content, :highlighter_prefix, :highlighter_suffix, :jekyll, :layout, :page, :paginator, :site, :theme
|
||||
@layout = @envs[:layout]
|
||||
@paginator = @envs[:paginator]
|
||||
@theme = @envs[:theme]
|
||||
|
||||
env = @config['env']
|
||||
@mode = env&.key?('JEKYLL_ENV') ? env['JEKYLL_ENV'] : 'development'
|
||||
|
||||
@argument_string = JekyllSupport.lookup_liquid_variables @logger, @helper.liquid_context, @argument_string.to_s.strip
|
||||
@helper.reinitialize @argument_string.to_s.strip
|
||||
|
||||
@attribution = @helper.parameter_specified?('attribution') || false unless @no_arg_parsing
|
||||
@logger.debug { "@keys_values='#{@keys_values}'" }
|
||||
|
||||
# @argument_string = JekyllSupport.lookup_liquid_variables @logger, liquid_context, @argument_string # Is this redundant?
|
||||
# @argument_string.strip! # Is this redundant?
|
||||
# @helper.reinitialize @argument_string # Is this redundant?
|
||||
|
||||
render_impl(text)
|
||||
rescue StandardError => e
|
||||
e.shorten_backtrace
|
||||
file_name = e.backtrace[0]&.split(':')&.first
|
||||
in_file_name = "in '#{file_name}' " if file_name
|
||||
of_page = "of '#{@page['path']}'" if @page
|
||||
@logger.error { "#{e.class} on line #{@line_number} #{of_page} while processing #{tag_name} #{in_file_name}- #{e.message}" }
|
||||
binding.pry if @pry_on_standard_error # rubocop:disable Lint/Debugger
|
||||
raise e if @die_on_standard_error
|
||||
|
||||
<<~END_MSG
|
||||
<div class='standard_error'>
|
||||
#{e.class} on line #{@line_number} of #{e.backtrace[0].split(':').first} by #{@tag_name}: #{e.message}
|
||||
</div>
|
||||
END_MSG
|
||||
end
|
||||
|
||||
# Jekyll plugins should override this method, not render,
|
||||
# so they can be tested more easily.
|
||||
# The following variables are predefined:
|
||||
# @argument_string, @config, @envs, @helper, @layout, @logger, @mode, @page, @paginator, @site, @tag_name and @theme
|
||||
# @return [String] The result to be rendered to the invoking page
|
||||
def render_impl(text)
|
||||
text
|
||||
end
|
||||
|
||||
def set_error_context
|
||||
return unless Object.const_defined? @error_name
|
||||
|
||||
error_class = Object.const_get @error_name
|
||||
error_class.class_variable_set(:@@argument_string, @argument_string)
|
||||
error_class.class_variable_set(:@@line_number, @line_number)
|
||||
error_class.class_variable_set(:@@path, @page['path'])
|
||||
error_class.class_variable_set(:@@tag_name, @tag_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
module JekyllSupport
|
||||
class JekyllBlockNoArgParsing < JekyllBlock
|
||||
attr_reader :argument_string, :helper, :line_number, :logger, :page, :site
|
||||
|
||||
def initialize(tag_name, argument_string, parse_context)
|
||||
class << self
|
||||
include NoArgParsing
|
||||
end
|
||||
|
||||
super
|
||||
@logger.debug { "#{self.class}: respond_to?(:o_arg_parsing) #{respond_to?(:no_arg_parsing) ? 'yes' : 'no'}." }
|
||||
rescue StandardError => e
|
||||
e.shorten_backtrace
|
||||
@logger.error { e.full_message }
|
||||
JekyllSupport.error_short_trace(@logger, e)
|
||||
end
|
||||
|
||||
# Liquid::Block subclasses do not render if there is no content within the tag
|
||||
# This override fixes that
|
||||
def blank?
|
||||
false
|
||||
end
|
||||
|
||||
# Jekyll plugins must override this method, not render, so their plugin can be tested more easily
|
||||
# The following variables are predefined:
|
||||
# @argument_string, @config, @envs, @helper, @layout, @logger, @mode, @page, @paginator, @site, @tag_name and @theme
|
||||
def render_impl
|
||||
abort "#{self.class}.render_impl for tag #{@tag_name} must be overridden, but it was not."
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,52 @@
|
||||
require 'facets/string/camelcase'
|
||||
require 'facets/string/snakecase'
|
||||
|
||||
module JekyllSupport
|
||||
# Use like this:
|
||||
# CustomError.new(:MyError, 'blah', 'asdf')
|
||||
class CustomError < StandardError
|
||||
def self.factory(error_class_name)
|
||||
return if Object.const_defined? "::#{error_class_name}"
|
||||
|
||||
Object.const_set error_class_name, Class.new(CustomError)
|
||||
end
|
||||
|
||||
def error_name
|
||||
self.class.name.split('::').last
|
||||
end
|
||||
|
||||
def calling_file
|
||||
file_fq, _line_number, _extra = backtrace[0]&.split(':')
|
||||
file_fq
|
||||
end
|
||||
|
||||
# @return HTML <div> tag with class set to the snake_case version of the error class name.
|
||||
def html_message
|
||||
shorten_backtrace
|
||||
path, line_number, _caller = backtrace[1]&.split(':')
|
||||
<<~END_MSG
|
||||
<div class='#{error_name.snakecase}'>
|
||||
#{self.class} raised in #{calling_file} while processing line #{line_number} (after front matter) of #{path}
|
||||
#{message}
|
||||
</div>
|
||||
END_MSG
|
||||
end
|
||||
|
||||
def logger_message
|
||||
shorten_backtrace
|
||||
kaller = caller(1..1).first
|
||||
path, line_number, _caller = backtrace[1]&.split(':')
|
||||
<<~END_MSG
|
||||
#{error_name} raised in #{kaller} while processing line #{line_number} (after front matter) of #{path}
|
||||
#{message}
|
||||
END_MSG
|
||||
end
|
||||
|
||||
def shorten_backtrace(backtrace_element_count = 3)
|
||||
b = backtrace[0..backtrace_element_count - 1].map do |x|
|
||||
x.gsub(Dir.pwd + '/', './')
|
||||
end
|
||||
set_backtrace b
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,48 @@
|
||||
module JekyllSupportError
|
||||
attr_reader :logger, :page
|
||||
|
||||
# If a Jekyll plugin needs to crash exit, and stop Jekyll, call this method.
|
||||
# It does not generate a stack trace.
|
||||
# This method does not return because the process is abruptly terminated.
|
||||
#
|
||||
# @param error StandardError or a subclass of StandardError is required
|
||||
#
|
||||
# Do not raise the error before calling this method, just create it via 'new', like this:
|
||||
# exit_without_stack_trace StandardError.new('This is my error message')
|
||||
#
|
||||
# If you want to call this method from a handler method, the default index for the backtrace array must be specified.
|
||||
# The default backtrace index is 1, which means the calling method.
|
||||
# To specify the calling method's caller, pass in 2, like this:
|
||||
# exit_without_stack_trace StandardError.new('This is my error message'), 2
|
||||
def exit_without_stack_trace(error, caller_index = 1)
|
||||
raise error
|
||||
rescue StandardError => e
|
||||
file, line_number, caller = e.backtrace[caller_index].split(':')
|
||||
caller = caller.tr('`', "'")
|
||||
warn "#{error.message} #{caller} on line #{line_number} (after front matter) of #{file}".red
|
||||
# Process.kill('HUP', Process.pid) # generates huge stack trace
|
||||
exec "echo ''"
|
||||
end
|
||||
|
||||
def format_error_message(message)
|
||||
page = " of #{@page['path']}" if @page
|
||||
remove_ansi_color "on line #{line_number} (after front matter)#{page}.\n#{message}"
|
||||
end
|
||||
|
||||
def remove_ansi_color(string)
|
||||
string.gsub(/\e\[([;\d]+)?m/, '')
|
||||
end
|
||||
|
||||
def maybe_reraise_error(error, throw_error: true)
|
||||
fmsg = format_error_message "#{error.class}: #{error.message.strip}"
|
||||
@logger.error { fmsg }
|
||||
return "<span class='jekyll_plugin_support_error'>#{fmsg}</span>" unless throw_error
|
||||
|
||||
error.set_backtrace error.backtrace[0..9]
|
||||
raise error
|
||||
end
|
||||
|
||||
def warn_short_trace(error)
|
||||
JekyllSupport.warn_short_trace(@logger, error)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,69 @@
|
||||
module JekyllSupport
|
||||
# Base class for Jekyll generators.
|
||||
# PluginMetaLogger.instance.config is populated with the contents of `_config.yml` before Jekyll::Generator instances run.
|
||||
class JekyllGenerator < Jekyll::Generator
|
||||
attr_reader :helper, :line_number, :logger, :site
|
||||
|
||||
# Method prescribed by the Jekyll plugin lifecycle.
|
||||
# Defines @config, @envs, @mode and @site
|
||||
# @return [void]
|
||||
def generate(site)
|
||||
@logger ||= PluginMetaLogger.instance.new_logger(self, PluginMetaLogger.instance.config)
|
||||
|
||||
@error_name = "#{self.class.name}Error"
|
||||
|
||||
@site = site
|
||||
@config = @site.config
|
||||
@envs = site.config['env']
|
||||
@theme = @site.theme
|
||||
|
||||
@mode = ENV['JEKYLL_ENV'] || 'development'
|
||||
|
||||
generate_impl
|
||||
rescue StandardError => e
|
||||
e.shorten_backtrace
|
||||
@logger.error { "#{e.class} on line #{@line_number} of #{e.backtrace[0].split(':').first} by #{self.class.name} - #{e.message}" }
|
||||
binding.pry if @pry_on_standard_error # rubocop:disable Lint/Debugger
|
||||
raise e if @die_on_standard_error
|
||||
|
||||
<<~END_MSG
|
||||
<div class='standard_error'>
|
||||
#{e.class} on line #{@line_number} of #{e.backtrace[0].split(':').first} by #{self.class.name}: #{e.message}
|
||||
</div>
|
||||
END_MSG
|
||||
end
|
||||
|
||||
# Jekyll plugins should override this method, not `generate`, so they can be tested more easily.
|
||||
# The following variables are predefined:
|
||||
# @config, @envs, @helper, @logger, @mode, @paginator, @site and @theme
|
||||
# @return [void]
|
||||
def generate_impl; end
|
||||
|
||||
def self.register(klass)
|
||||
abort("Error: The #{klass.name} plugin does not define VERSION") \
|
||||
unless klass.const_defined?(:VERSION)
|
||||
|
||||
version = klass.const_get(:VERSION)
|
||||
error_name_stub = klass.name.include?('::') ? klass.name.split('::')[1] : klass.name
|
||||
error_ruby_class_name = "#{error_name_stub.camelcase(:upper)}Error"
|
||||
error_css_class_name = error_ruby_class_name.split('::').last.snakecase
|
||||
msg = <<~END_MSG
|
||||
Loaded generator plugin #{klass.name} v#{version}. It has:
|
||||
Error class: #{@error_name}
|
||||
CSS class for error messages: #{error_css_class_name}
|
||||
END_MSG
|
||||
|
||||
PluginMetaLogger.instance.info { msg }
|
||||
end
|
||||
|
||||
def set_error_context(klass) # rubocop:disable Naming/AccessorMethodName
|
||||
return unless Object.const_defined? @error_name
|
||||
|
||||
error_class = Object.const_get @error_name
|
||||
error_class.class_variable_set(:@@argument_string, @argument_string)
|
||||
error_class.class_variable_set(:@@line_number, @line_number)
|
||||
error_class.class_variable_set(:@@path, @page['path'])
|
||||
error_class.class_variable_set(:@@tag_name, klass.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,112 @@
|
||||
require 'facets/string/interpolate'
|
||||
require 'key_value_parser'
|
||||
require 'shellwords'
|
||||
|
||||
module JekyllSupport
|
||||
class JekyllPluginHelper
|
||||
attr_accessor :liquid_context
|
||||
attr_reader :argument_string, :argv, :argv_original, :attribution, :excerpt_caller, :keys_values_original,
|
||||
:keys_values, :jpsh_subclass_caller, :logger, :markup, :no_arg_parsing, :params, :params_original,
|
||||
:tag_name
|
||||
|
||||
# See https://github.com/Shopify/liquid/wiki/Liquid-for-Programmers#create-your-own-tags
|
||||
# @param tag_name [String] the name of the tag, which we already know.
|
||||
# @param argument_string [String] the arguments from the tag, as a single string.
|
||||
# @param parse_context [Liquid::ParseContext] hash that stores Liquid options.
|
||||
# By default it has two keys: :locale and :line_numbers, the first is a Liquid::I18n object, and the second,
|
||||
# a boolean parameter that determines if error messages should display the line number the error occurred.
|
||||
# This argument is used mostly to display localized error messages on Liquid built-in Tags and Filters.
|
||||
# See https://github.com/Shopify/liquid/wiki/Liquid-for-Programmers#create-your-own-tags
|
||||
# @return [void]
|
||||
def initialize(tag_name, markup, logger, no_arg_parsing)
|
||||
@tag_name = tag_name
|
||||
@logger = logger
|
||||
@markup = markup
|
||||
@argument_string = markup
|
||||
@no_arg_parsing = no_arg_parsing
|
||||
rescue StandardError => e
|
||||
e.shorten_backtrace
|
||||
@logger.error { e.message }
|
||||
end
|
||||
|
||||
# @return undefined if parameter was specified, removes it from the available tokens and returns value
|
||||
def parameter_specified?(name, delete_param: true)
|
||||
return false if @keys_values.to_s.empty?
|
||||
|
||||
key = name
|
||||
key = name.to_sym if @keys_values&.first&.first.instance_of?(Symbol)
|
||||
value = @keys_values[key]
|
||||
delete_parameter(name) if delete_param
|
||||
value
|
||||
end
|
||||
|
||||
def reinitialize(markup)
|
||||
# @keys_values was a Hash[Symbol, String|Boolean] but now it is Hash[String, String|Boolean]
|
||||
@argument_string = markup
|
||||
if @no_arg_parsing
|
||||
define_singleton_method(:argv) { warn_fetch :argv }
|
||||
define_singleton_method(:keys_values) { warn_fetch :keys_values }
|
||||
define_singleton_method(:params) { warn_fetch :params }
|
||||
define_singleton_method(:parameter_specified?) { |_name| warn_parse(:parameter_specified?) }
|
||||
define_singleton_method(:delete_parameter) { |_name| warn_parse(:delete_parameter) }
|
||||
@attribution = false
|
||||
else
|
||||
parse markup
|
||||
@attribution = parameter_specified?('attribution') || false
|
||||
@logger.debug { "@keys_values='#{@keys_values}'" }
|
||||
end
|
||||
end
|
||||
|
||||
# Call this method to return the remaining markup after `parameter_specified?` has been invoked.
|
||||
def remaining_markup
|
||||
@argv&.join(' ')
|
||||
end
|
||||
|
||||
def remaining_markup_original
|
||||
@argv_original&.join(' ')
|
||||
end
|
||||
|
||||
def warn_fetch(variable)
|
||||
abort "Error: Argument parsing was suppressed, but an attempt to obtain the value of #{variable} was made"
|
||||
end
|
||||
|
||||
def warn_parse(meth)
|
||||
abort "Error: Argument parsing was suppressed, but an attempt to invoke #{meth} was made"
|
||||
end
|
||||
|
||||
def delete_parameter(key)
|
||||
return if @keys_values.empty? || @params.nil?
|
||||
|
||||
@params.delete(key)
|
||||
@argv.delete_if { |x| x == key or x.start_with?("#{key}=") }
|
||||
@keys_values.delete(key)
|
||||
|
||||
@params_original.delete(key)
|
||||
@argv_original.delete_if { |x| x == key or x.start_with?("#{key}=") }
|
||||
@keys_values_original.delete(key)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def page
|
||||
@liquid_context.registers[:page]
|
||||
end
|
||||
|
||||
def parse(markup)
|
||||
@argv_original = Shellwords.split(markup)
|
||||
@keys_values_original = KeyValueParser
|
||||
.new({}, { array_values: false, normalize_keys: false, separator: /=/ })
|
||||
.parse(@argv_original)
|
||||
@params_original = @keys_values_original unless respond_to?(:no_arg_parsing) && no_arg_parsing
|
||||
|
||||
@argv = Shellwords.split(self.class.expand_env(markup, logger))
|
||||
@keys_values = KeyValueParser
|
||||
.new({}, { array_values: false, normalize_keys: false, separator: /=/ })
|
||||
.parse(@argv)
|
||||
|
||||
return if respond_to?(:no_arg_parsing) && no_arg_parsing
|
||||
|
||||
@params = @keys_values # TODO: @keys_values should be deleted
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,74 @@
|
||||
module JekyllSupport
|
||||
# Attribution aspect of JekyllPluginHelper
|
||||
class JekyllPluginHelper
|
||||
# @param file must be a fully qualified file name that points to a file within a gem.
|
||||
# @return Gem::Specification of gem that file points into, or nil if not called from a gem
|
||||
# See https://stackoverflow.com/a/75890279/553865
|
||||
def self.current_spec(file)
|
||||
abort 'JekyllPluginHelper::current_spec: file is nil' if file.nil?
|
||||
return nil unless File.exist?(file)
|
||||
|
||||
searcher = if Gem::Specification.respond_to?(:find)
|
||||
Gem::Specification
|
||||
elsif Gem.respond_to?(:searcher)
|
||||
Gem.searcher.init_gemspecs
|
||||
end
|
||||
|
||||
searcher&.find do |spec|
|
||||
file.start_with? spec.full_gem_path
|
||||
end
|
||||
end
|
||||
|
||||
def attribute
|
||||
return unless @current_gem
|
||||
|
||||
<<~END_OUTPUT
|
||||
<div id="jps_attribute_#{rand(999_999)}" class="jps_attribute">
|
||||
<div>
|
||||
<a href="#{@homepage}" target="_blank" rel="nofollow">
|
||||
#{attribution_string}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
END_OUTPUT
|
||||
end
|
||||
|
||||
def default_attribution
|
||||
authors = @authors&.join(', ')
|
||||
result = "Generated by the \#{@name} v\#{@version} Jekyll plugin"
|
||||
result << ", written by #{authors}" if authors
|
||||
result << " \#{@published_date}" if @published_date
|
||||
result << '.'
|
||||
result
|
||||
end
|
||||
|
||||
# Sets @current_gem if file points at a uniquely named file within a gem.
|
||||
# @param file must be a fully qualified file name in a gem, for example: __FILE__
|
||||
def gem_file(file)
|
||||
@current_gem = JekyllPluginHelper.current_spec file
|
||||
@logger.debug "No gem found for '#{file} was found." unless @current_gem
|
||||
annotate_globals if @attribution && @current_gem
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def annotate_globals
|
||||
return unless @current_gem
|
||||
|
||||
@name = @current_gem.name
|
||||
@authors = @current_gem.authors
|
||||
@homepage = @current_gem.homepage
|
||||
@published_date = @current_gem.date.to_date.to_s
|
||||
@version = @current_gem.version
|
||||
end
|
||||
|
||||
def attribution_string
|
||||
string = if @attribution == true
|
||||
default_attribution
|
||||
else
|
||||
@attribution
|
||||
end
|
||||
String.interpolate { string }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,88 @@
|
||||
require 'facets/string/camelcase'
|
||||
require 'facets/string/snakecase'
|
||||
require 'yaml'
|
||||
|
||||
module JekyllSupport
|
||||
# Class methods for JekyllPluginHelper
|
||||
class JekyllPluginHelper
|
||||
# Expand an environment variable reference
|
||||
def self.expand_env(str, logger = nil, die_if_undefined: false)
|
||||
str&.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}/) do
|
||||
envar = Regexp.last_match(1)
|
||||
unless ENV.key? envar
|
||||
msg = "jekyll_plugin_support error: environment variable #{envar} is undefined"
|
||||
raise JekyllPluginSupportError, msg.red, [] if die_if_undefined
|
||||
|
||||
if logger
|
||||
logger.warn msg
|
||||
else
|
||||
puts msg.red
|
||||
end
|
||||
end
|
||||
ENV.fetch(envar, nil)
|
||||
end
|
||||
end
|
||||
|
||||
def self.generate_message(klass, tag_name, version)
|
||||
error_name_stub = klass.name.include?('::') ? klass.name.split('::')[1] : klass.name
|
||||
error_ruby_class_name = "#{error_name_stub.camelcase(:upper)}Error"
|
||||
config_die_key = "die_on_#{error_ruby_class_name.snakecase}"
|
||||
error_css_class_name = error_ruby_class_name.split('::').last.snakecase
|
||||
config = YAML.load_file('_config.yml')
|
||||
tag_config = config[tag_name]
|
||||
tag_config_msg = if tag_config.nil?
|
||||
<<~END_MSG
|
||||
_config.yml does not contain configuration information for this plugin.
|
||||
You could add a section containing default values by specifying a section for the tag name,
|
||||
and an entry whose name starts with `die_on_`, followed by a snake_case version of the error name.
|
||||
|
||||
#{tag_name}:
|
||||
#{config_die_key}: false
|
||||
END_MSG
|
||||
else
|
||||
<<~END_MSG
|
||||
_config.yml contains the following configuration for this plugin:
|
||||
#{tag_config}
|
||||
END_MSG
|
||||
end
|
||||
|
||||
<<~END_MSG
|
||||
Loaded plugin #{tag_name} v#{version}. It has:
|
||||
Error class: #{error_ruby_class_name}
|
||||
CSS class for error messages: #{error_css_class_name}
|
||||
|
||||
#{tag_config_msg}
|
||||
END_MSG
|
||||
end
|
||||
|
||||
# @param klass [Class] class instance to register
|
||||
# @param tag_name [String] name of plugin defined by klass to register as
|
||||
# @param quiet [Boolean] suppress registration message if truthy
|
||||
def self.register(klass, tag_name, quiet: false)
|
||||
abort("Error: The #{tag_name} plugin does not define VERSION") \
|
||||
unless klass.const_defined?(:VERSION)
|
||||
|
||||
version = klass.const_get(:VERSION)
|
||||
|
||||
abort("Error: The #{tag_name} plugin is not an instance of JekyllSupport::JekyllBlock or JekyllSupport::JekyllTag") \
|
||||
unless klass.instance_of?(Class) &&
|
||||
(klass.ancestors.include?(::JekyllSupport::JekyllBlock) ||
|
||||
klass.ancestors.include?(::JekyllSupport::JekyllTag))
|
||||
|
||||
Liquid::Template.register_tag(tag_name, klass)
|
||||
return if quiet
|
||||
|
||||
msg = generate_message(klass, tag_name, version)
|
||||
PluginMetaLogger.instance.info { msg }
|
||||
end
|
||||
|
||||
def self.remove_html_tags(string)
|
||||
string.gsub(/<[^>]*>/, '').strip
|
||||
end
|
||||
|
||||
# strip leading and trailing quotes if present
|
||||
def self.remove_quotes(string)
|
||||
string.strip.gsub(/\A'|\A"|'\Z|"\Z/, '').strip if string
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,69 @@
|
||||
module AllCollectionsHooks
|
||||
class APage
|
||||
attr_reader :content, :data, :date, :description, :destination, :draft, :excerpt, :ext, :extname, :href,
|
||||
:label, :last_modified, :layout, :origin, :path, :relative_path, :tags, :title, :type, :url
|
||||
|
||||
def initialize(obj, origin)
|
||||
@origin = origin
|
||||
data_field_init obj
|
||||
obj_field_init obj
|
||||
@draft = Jekyll::Draft.draft? obj
|
||||
@href = @url if @href.nil?
|
||||
# @href = "/#{@href}" if @origin == 'individual_page'
|
||||
@href = "#{@href}index.html" if @href.end_with? '/'
|
||||
@name = File.basename(@href)
|
||||
@title = if @data&.key?('title')
|
||||
@data['title']
|
||||
elsif obj.respond_to?(:title)
|
||||
obj.title
|
||||
else
|
||||
"<code>#{@href}</code>"
|
||||
end
|
||||
rescue StandardError => e
|
||||
JekyllSupport.error_short_trace(@logger, e)
|
||||
# JekyllSupport.warn_short_trace(@logger, e)
|
||||
end
|
||||
|
||||
def to_s
|
||||
@label || @date.to_s
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def data_field_init(obj)
|
||||
return unless obj.respond_to? :data
|
||||
|
||||
@data = obj.data
|
||||
|
||||
@categories = @data['categories'] if @data.key? 'categories'
|
||||
@date = (@data['date'].to_date if @data&.key?('date')) || Date.today
|
||||
@description = @data['description'] if @data.key? 'description'
|
||||
@excerpt = @data['excerpt'] if @data.key? 'excerpt'
|
||||
@ext ||= @data['ext'] if @data.key? 'ext'
|
||||
@last_modified = @data['last_modified'] || @data['last_modified_at'] || @date
|
||||
@last_modified_field = case @data
|
||||
when @data.key?('last_modified')
|
||||
'last_modified'
|
||||
when @data.key?('last_modified_at')
|
||||
'last_modified_at'
|
||||
end
|
||||
@layout = @data['layout'] if @data.key? 'layout'
|
||||
@tags = @data['tags'] if @data.key? 'tags'
|
||||
end
|
||||
|
||||
def obj_field_init(obj)
|
||||
@content = obj.content if obj.respond_to? :content
|
||||
|
||||
# TODO: What _config.yml setting should be passed to destination()?
|
||||
@destination = obj.destination('') if obj.respond_to? :destination
|
||||
@ext = obj.extname
|
||||
@extname = @ext # For compatibility with previous versions of all_collections
|
||||
@label = obj.collection.label if obj.respond_to?(:collection) && obj.collection.respond_to?(:label)
|
||||
@path = obj.path if obj.respond_to? :path
|
||||
@relative_path = obj.relative_path if obj.respond_to? :relative_path
|
||||
@type = obj.type if obj.respond_to? :type
|
||||
@url = obj.url
|
||||
@url = "#{@url}index.html" if @url.end_with? '/'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,61 @@
|
||||
module JekyllAllCollections
|
||||
module AllCollectionsHooks
|
||||
class << self
|
||||
attr_accessor :logger
|
||||
end
|
||||
@logger = PluginMetaLogger.instance.new_logger(self, PluginMetaLogger.instance.config)
|
||||
|
||||
# No, all_collections is not defined for this hook
|
||||
# Jekyll::Hooks.register(:site, :after_init, priority: :normal) do |site|
|
||||
# defined_msg = ::AllCollectionsHooks.all_collections_defined?(site)
|
||||
# @logger.debug { "Jekyll::Hooks.register(:site, :after_init: #{defined_msg}" }
|
||||
# end
|
||||
|
||||
# Creates a `Array[APage]` property called site.all_collections if it does not already exist.
|
||||
# The array is available from :site, :pre_render onwards
|
||||
# Each `APage` entry is one document or page.
|
||||
Jekyll::Hooks.register(:site, :post_read, priority: :normal) do |site|
|
||||
@site = site
|
||||
unless site.class.method_defined? :all_collections
|
||||
defined_msg = ::AllCollectionsHooks.all_collections_defined? site
|
||||
@logger.debug { "Jekyll::Hooks.register(:site, :post_read, :normal: #{defined_msg}" }
|
||||
::AllCollectionsHooks.compute(site) if !@site.class.method_defined?(:all_documents) || @site.all_documents.nil?
|
||||
end
|
||||
rescue StandardError => e
|
||||
JekyllSupport.error_short_trace(@logger, e)
|
||||
# JekyllSupport.warn_short_trace(@logger, e)
|
||||
end
|
||||
|
||||
# Yes, all_collections is defined for this hook
|
||||
# Jekyll::Hooks.register(:site, :post_read, priority: :low) do |site|
|
||||
# defined_msg = ::AllCollectionsHooks.all_collections_defined?(site)
|
||||
# @logger.debug { "Jekyll::Hooks.register(:site, :post_read, :low: #{defined_msg}" }
|
||||
# rescue StandardError => e
|
||||
# JekyllSupport.error_short_trace(@logger, e)
|
||||
# # JekyllSupport.warn_short_trace(@logger, e)
|
||||
# end
|
||||
|
||||
# Yes, all_collections is defined for this hook
|
||||
# Jekyll::Hooks.register(:site, :post_read, priority: :normal) do |site|
|
||||
# defined_msg = ::AllCollectionsHooks.all_collections_defined?(site)
|
||||
# @logger.debug { "Jekyll::Hooks.register(:site, :post_read, :normal: #{defined_msg}" }
|
||||
# rescue StandardError => e
|
||||
# JekyllSupport.error_short_trace(@logger, e)
|
||||
# # JekyllSupport.warn_short_trace(@logger, e)
|
||||
# end
|
||||
|
||||
# Yes, all_collections is defined for this hook
|
||||
# Jekyll::Hooks.register(:site, :pre_render, priority: :normal) do |site, _payload|
|
||||
# defined_msg = ::AllCollectionsHooks.all_collections_defined?(site)
|
||||
# @logger.debug { "Jekyll::Hooks.register(:site, :pre_render: #{defined_msg}" }
|
||||
# rescue StandardError => e
|
||||
# JekyllSupport.error_short_trace(@logger, e)
|
||||
# # JekyllSupport.warn_short_trace(@logger, e)
|
||||
# end
|
||||
end
|
||||
end
|
||||
|
||||
PluginMetaLogger.instance.logger.info do
|
||||
"Loaded AllCollectionsHooks v#{JekyllPluginSupportVersion::VERSION} :site, :pre_render, :normal hook plugin."
|
||||
end
|
||||
Liquid::Template.register_filter(JekyllAllCollections::AllCollectionsHooks)
|
||||
@@ -0,0 +1,48 @@
|
||||
# Insert the url of a Jekyll::Page into each LruFile instance,
|
||||
# along with the Page reference
|
||||
# todo: replace references to url and :url with reverse_url and :reverse_url
|
||||
LruFile = Struct.new(:url, :page) do
|
||||
include SendChain
|
||||
|
||||
def <=>(other)
|
||||
url <=> other.url
|
||||
end
|
||||
|
||||
def href
|
||||
url.reverse
|
||||
end
|
||||
end
|
||||
|
||||
# Matches suffixes of an array of urls
|
||||
# Converts suffixes to prefixes
|
||||
class SortedLruFiles
|
||||
attr_reader :msbs
|
||||
|
||||
def initialize
|
||||
@msbs = MSlinnBinarySearch.new %i[url start_with?]
|
||||
end
|
||||
|
||||
# @param apages [Array[APage]]
|
||||
def add_pages(apages)
|
||||
apages.each { |apage| insert apage.href, apage }
|
||||
@msbs.enable_search
|
||||
end
|
||||
|
||||
def enable_search
|
||||
@msbs.enable_search
|
||||
end
|
||||
|
||||
def find(suffix)
|
||||
@msbs.find suffix
|
||||
end
|
||||
|
||||
def insert(url, file)
|
||||
lru_file = LruFile.new(url.reverse, file)
|
||||
lru_file.new_chain [:url, %i[start_with? placeholder]]
|
||||
@msbs.insert(lru_file)
|
||||
end
|
||||
|
||||
def select(suffix)
|
||||
@msbs.select_pages suffix
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
module AllCollectionsHooks
|
||||
class << self
|
||||
attr_accessor :all_collections, :all_documents, :everything, :sorted_lru_files
|
||||
end
|
||||
|
||||
# @sort_by = ->(apages, criteria) { [apages.sort(criteria)] } # todo delete this
|
||||
|
||||
# @return [String] indicating if :all_collections is defined or not
|
||||
def self.all_collections_defined?(site)
|
||||
"site.all_collections #{site.class.method_defined?(:all_collections) ? 'IS' : 'IS NOT'} defined"
|
||||
end
|
||||
|
||||
# Create Array of AllCollectionsHooks::APage from objects
|
||||
# @param objects [Array] An array of Jekyll::Document, Jekyll::Page or file names
|
||||
# @param origin [String] Indicates type of objects being passed
|
||||
def self.apages_from_objects(objects, origin)
|
||||
pages = []
|
||||
objects.each do |object|
|
||||
page = APage.new(object, origin)
|
||||
pages << page unless page.data['exclude_from_all'] || page.path == 'redirect.html'
|
||||
end
|
||||
pages
|
||||
end
|
||||
|
||||
# Called by early, high-priority hook.
|
||||
# Computes site.all_collections, site.all_documents, site.everything, and site.sorted_lru_files
|
||||
def self.compute(site)
|
||||
site.class.module_eval { attr_accessor :all_collections, :all_documents, :everything, :sorted_lru_files }
|
||||
|
||||
documents = site.collections
|
||||
.values
|
||||
.map { |x| x.class.method_defined?(:docs) ? x.docs : x }
|
||||
.flatten
|
||||
.compact
|
||||
@all_collections = AllCollectionsHooks.apages_from_objects(documents, 'collection')
|
||||
@all_documents = @all_collections +
|
||||
AllCollectionsHooks.apages_from_objects(site.pages, 'individual_page')
|
||||
@everything = @all_documents +
|
||||
AllCollectionsHooks.apages_from_objects(site.static_files, 'static_file')
|
||||
@sorted_lru_files = SortedLruFiles.new.add_pages @everything
|
||||
|
||||
site.all_collections = @all_collections
|
||||
site.all_documents = @all_documents
|
||||
site.everything = @everything
|
||||
site.sorted_lru_files = @sorted_lru_files
|
||||
rescue StandardError => e
|
||||
JekyllSupport.error_short_trace(::JekyllAllCollections::AllCollectionsHooks.logger, e)
|
||||
# JekyllSupport.warn_short_trace(::JekyllAllCollections::AllCollectionsHooks.logger, e)
|
||||
end
|
||||
end
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
# require 'jekyll_draft'
|
||||
require 'securerandom'
|
||||
|
||||
# @author Copyright 2020 Michael Slinn
|
||||
# @license SPDX-License-Identifier: Apache-2.0
|
||||
module JekyllAllCollections
|
||||
PLUGIN_NAME = 'all_collections'.freeze unless defined?(PLUGIN_NAME)
|
||||
CRITERIA = %w[date destination draft label last_modified last_modified_at path relative_path title type url].freeze unless defined?(CRITERIA)
|
||||
DRAFT_HTML = '<i class="jekyll_draft">Draft</i>'.freeze unless defined?(DRAFT_HTML)
|
||||
|
||||
class AllCollectionsTag < ::JekyllSupport::JekyllTag
|
||||
include ::JekyllPluginSupportVersion
|
||||
|
||||
# Method prescribed by JekyllTag.
|
||||
# @return [String]
|
||||
def render_impl
|
||||
parse_arguments # Defines instance variables like @sort_by
|
||||
sort_lambda = init_sort_by @sort_by, @sort_by_param
|
||||
@heading = @helper.parameter_specified?('heading') || default_head(@sort_by)
|
||||
generate_output sort_lambda
|
||||
rescue StandardError => e
|
||||
JekyllSupport.error_short_trace @logger, e
|
||||
# JekyllSupport.warn_short_trace @logger, e
|
||||
end
|
||||
|
||||
# Descending sort keys reverse the order of comparison
|
||||
# Example return values:
|
||||
# "->(a, b) { [a.last_modified] <=> [b.last_modified] }"
|
||||
# "->(a, b) { [b.last_modified] <=> [a.last_modified] }" (descending)
|
||||
# "->(a, b) { [a.last_modified, a.date] <=> [b.last_modified, b.date] }" (descending last_modified, ascending date)
|
||||
# "->(a, b) { [a.last_modified, b.date] <=> [b.last_modified, a.date] }" (ascending last_modified, descending date)
|
||||
def self.create_lambda_string(criteria)
|
||||
criteria_lhs_array = []
|
||||
criteria_rhs_array = []
|
||||
verify_sort_by_type(criteria).each do |c|
|
||||
descending_sort = c.start_with? '-'
|
||||
c.delete_prefix! '-'
|
||||
abort("Error: '#{c}' is not a valid sort field. Valid field names are: #{CRITERIA.join ', '}") \
|
||||
unless CRITERIA.include?(c)
|
||||
criteria_lhs_array << (descending_sort ? "b.#{c}" : "a.#{c}")
|
||||
criteria_rhs_array << (descending_sort ? "a.#{c}" : "b.#{c}")
|
||||
end
|
||||
"->(a, b) { [#{criteria_lhs_array.join(', ')}] <=> [#{criteria_rhs_array.join(', ')}] }"
|
||||
end
|
||||
|
||||
def self.verify_sort_by_type(sort_by)
|
||||
case sort_by
|
||||
when Array
|
||||
sort_by
|
||||
when Enumerable
|
||||
sort_by.to_a
|
||||
when Date
|
||||
[sort_by.to_i]
|
||||
when String
|
||||
[sort_by]
|
||||
else
|
||||
abort "Error: @sort_by was specified as '#{sort_by}'; it must either be a string or an array of strings"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_head(sort_by)
|
||||
criteria = (sort_by.map do |x|
|
||||
reverse = x.start_with? '-'
|
||||
criterion = x.delete_prefix('-').capitalize
|
||||
criterion += reverse ? ' (Newest to Oldest)' : ' (Oldest to Newest)'
|
||||
criterion
|
||||
end).join(', ')
|
||||
"All Posts in All Categories Sorted By #{criteria}"
|
||||
end
|
||||
|
||||
def evaluate(string)
|
||||
self.eval string, binding, __FILE__, __LINE__
|
||||
rescue StandardError => e
|
||||
warn_short_trace e.red
|
||||
end
|
||||
|
||||
def last_modified_value(apage)
|
||||
@logger.debug do
|
||||
" apage.last_modified='#{apage.last_modified}'; " \
|
||||
"apage.last_modified_at='#{apage.last_modified_at}'; " \
|
||||
"@date_column='#{@date_column}'"
|
||||
end
|
||||
last_modified = if @date_column == 'last_modified' && apage.respond_to?(:last_modified)
|
||||
apage.last_modified
|
||||
elsif apage.respond_to? :last_modified_at
|
||||
apage.last_modified_at
|
||||
else
|
||||
apage.date
|
||||
end
|
||||
last_modified ||= apage.date || Date.today
|
||||
last_modified
|
||||
end
|
||||
|
||||
def generate_output(sort_lambda)
|
||||
id = @id.to_s.strip.empty? ? '' : " id='#{@id}'"
|
||||
heading = @heading.strip.to_s.empty? ? '' : "<h2#{id}>#{@heading}</h2>"
|
||||
data = case @data_selector
|
||||
when 'all_collections'
|
||||
@site.all_collections
|
||||
when 'all_documents'
|
||||
@site.all_documents
|
||||
when 'everything'
|
||||
@site.everything
|
||||
else
|
||||
raise AllCollectionsError, "Invalid value for @data_selector (#{data_selector})"
|
||||
end
|
||||
collection = data.sort(&sort_lambda)
|
||||
posts = collection.map do |x|
|
||||
last_modified = last_modified_value x
|
||||
date = last_modified.strftime '%Y-%m-%d'
|
||||
draft = x.draft ? DRAFT_HTML : ''
|
||||
href = "<a href='#{x.href}'>#{x.title}</a>"
|
||||
@logger.debug { " date='#{date}' #{x.title}\n" }
|
||||
" <span>#{date}</span><span>#{href}#{draft}</span>"
|
||||
end
|
||||
<<~END_TEXT
|
||||
#{heading}
|
||||
<div class="posts">
|
||||
#{posts.join "\n"}
|
||||
</div>
|
||||
END_TEXT
|
||||
rescue ArgumentError => e
|
||||
warn_short_trace e
|
||||
end
|
||||
|
||||
# See https://stackoverflow.com/a/75377832/553865
|
||||
def init_sort_by(sort_by, sort_by_param)
|
||||
sort_lambda_string = AllCollectionsTag.create_lambda_string sort_by
|
||||
|
||||
@logger.debug do
|
||||
"#{@page['path']} sort_by_param=#{sort_by_param} " \
|
||||
"sort_lambda_string = #{sort_lambda_string}\n"
|
||||
end
|
||||
|
||||
evaluate sort_lambda_string
|
||||
end
|
||||
|
||||
def parse_arguments
|
||||
@data_selector = @helper.parameter_specified?('data_selector') || 'all_collections'
|
||||
abort "Invalid data_selector #{@data_selector}" unless %w[all_collections all_documents everything].include? @data_selector
|
||||
|
||||
@date_column = @helper.parameter_specified?('date_column') || 'date'
|
||||
unless %w[date last_modified].include?(@date_column)
|
||||
raise AllCollectionsError "The date_column attribute must either have value 'date' or 'last_modified', " \
|
||||
"but '#{@date_column}' was specified"
|
||||
end
|
||||
|
||||
@id = @helper.parameter_specified?('id') || SecureRandom.hex(10)
|
||||
@sort_by_param = @helper.parameter_specified? 'sort_by'
|
||||
@sort_by = (@sort_by_param&.delete(' ')&.split(',') if @sort_by_param != false) || ['-date']
|
||||
end
|
||||
|
||||
::JekyllSupport::JekyllPluginHelper.register(self, PLUGIN_NAME)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,53 @@
|
||||
def require_directory(dir)
|
||||
Dir[File.join(dir, '*.rb')]&.sort&.each do |file|
|
||||
require file unless file == __FILE__
|
||||
end
|
||||
end
|
||||
|
||||
require 'colorator'
|
||||
require 'jekyll'
|
||||
require 'jekyll_plugin_logger'
|
||||
require 'pry'
|
||||
require 'sorted_set'
|
||||
|
||||
# require_directory __dir__
|
||||
require_directory "#{__dir__}/util"
|
||||
require_directory "#{__dir__}/error"
|
||||
require_directory "#{__dir__}/block"
|
||||
require_directory "#{__dir__}/generator"
|
||||
require_directory "#{__dir__}/helper"
|
||||
require_directory "#{__dir__}/jekyll_plugin_support"
|
||||
require_directory "#{__dir__}/tag"
|
||||
require_directory "#{__dir__}/jekyll_all_collections"
|
||||
require_directory "#{__dir__}/hooks"
|
||||
|
||||
module JekyllSupport
|
||||
def self.redef_without_warning(const, value)
|
||||
send(:remove_const, const) if const_defined?(const)
|
||||
const_set const, value
|
||||
end
|
||||
end
|
||||
|
||||
module NoArgParsing
|
||||
attr_accessor :no_arg_parsing
|
||||
|
||||
@no_arg_parsing = true
|
||||
end
|
||||
|
||||
module JekyllSupport
|
||||
class JekyllTag
|
||||
include JekyllSupportError
|
||||
end
|
||||
|
||||
class JekyllTagNoArgParsing
|
||||
include JekyllSupportError
|
||||
end
|
||||
|
||||
class JekyllBlock
|
||||
include JekyllSupportError
|
||||
end
|
||||
|
||||
class JekyllBlockNoArgParsing
|
||||
include JekyllSupportError
|
||||
end
|
||||
end
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
# Monkey patch StandardError so a new method called shorten_backtrace is added.
|
||||
class StandardError
|
||||
def shorten_backtrace(backtrace_element_count = 3)
|
||||
# self.backtrace = backtrace[0..backtrace_element_count].map do |x|
|
||||
# raise JekyllPluginSupportError, "backtrace contains a #{x.class} with value '#{x}'." unless x.instance_of? String
|
||||
|
||||
# x.gsub(Dir.pwd + '/', './')
|
||||
# end
|
||||
end
|
||||
end
|
||||
|
||||
module JekyllSupport
|
||||
DISPLAYED_CALLS = 8 unless defined?(DISPLAYED_CALLS)
|
||||
|
||||
def self.error_short_trace(logger, error)
|
||||
error.set_backtrace error.backtrace[0..DISPLAYED_CALLS]
|
||||
logger.error { error }
|
||||
error
|
||||
end
|
||||
|
||||
# @return a new StandardError subclass containing the shorten_backtrace method
|
||||
def define_error
|
||||
Class.new ::JekyllSupport::CustomError
|
||||
end
|
||||
module_function :define_error
|
||||
|
||||
JekyllPluginSupportError = define_error unless defined?(JekyllPluginSupportError)
|
||||
|
||||
def self.dump_vars(_logger, liquid_context)
|
||||
page = liquid_context.registers[:page]
|
||||
vars = liquid_context.scopes.map do |scope|
|
||||
scope.map { |name, value| " #{name} = #{value}" }.join("\n")
|
||||
end.join("\n")
|
||||
puts <<~END_MSG
|
||||
#{page['name']} variables after injecting any defined in _config.yml:
|
||||
#{vars}
|
||||
END_MSG
|
||||
end
|
||||
|
||||
# Inject variable definitions from _config.yml into liquid_context
|
||||
# Modifies liquid_context.scopes in the caller
|
||||
# (call by object reference, see https://stackoverflow.com/a/1872159/553865)
|
||||
# @return modified liquid_context
|
||||
# See README.md#configuration-variable-definitions
|
||||
# See demo/variables.html
|
||||
def self.inject_config_vars(liquid_context)
|
||||
site = liquid_context.registers[:site]
|
||||
|
||||
plugin_variables = site.config['liquid_vars']
|
||||
|
||||
scope = liquid_context.scopes.last
|
||||
|
||||
env = site.config['env']
|
||||
@mode = env&.key?('JEKYLL_ENV') ? env['JEKYLL_ENV'] : 'development'
|
||||
|
||||
# Set default values
|
||||
plugin_variables&.each do |name, value|
|
||||
scope[name] = value if value.instance_of? String
|
||||
end
|
||||
|
||||
# Override with environment-specific values
|
||||
plugin_variables&.[](@mode)&.each do |name, value|
|
||||
scope[name] = value if value.instance_of? String
|
||||
end
|
||||
|
||||
liquid_context
|
||||
end
|
||||
|
||||
def self.dump_stack(stack, cycles, interval)
|
||||
stack_depth = stack.length
|
||||
puts "Stack depth is #{stack_depth}"
|
||||
num_entries = cycles * interval
|
||||
return unless stack_depth > interval * 5
|
||||
|
||||
stack.last(num_entries).each_with_index do |x, i|
|
||||
msg = " #{i}: #{x}"
|
||||
(i % interval).zero? ? puts(msg.yellow) : puts(msg)
|
||||
end
|
||||
end
|
||||
|
||||
# Modifies a clone of markup_original so variable references are replaced by their values
|
||||
# @param markup_original to be cloned
|
||||
# @return modified markup_original
|
||||
def self.lookup_liquid_variables(logger, liquid_context, markup_original)
|
||||
markup = markup_original.clone
|
||||
page = liquid_context.registers[:page]
|
||||
envs = liquid_context.environments.first
|
||||
layout = envs[:layout]
|
||||
|
||||
markup = process_layout_variables logger, layout, markup
|
||||
markup = process_page_variables logger, page, markup
|
||||
liquid_context.scopes&.each do |scope|
|
||||
markup = process_included_variables logger, scope, markup
|
||||
markup = process_liquid_variables logger, scope, markup
|
||||
end
|
||||
markup
|
||||
rescue StandardError => e
|
||||
logger.error { e.full_message }
|
||||
end
|
||||
|
||||
def self.process_included_variables(logger, scope, markup)
|
||||
scope['include']&.each do |name, value|
|
||||
if value.nil?
|
||||
value = ''
|
||||
logger.warn { "include.#{name} is undefined." }
|
||||
end
|
||||
markup.gsub!("{{include.#{name}}}", value)
|
||||
end
|
||||
markup
|
||||
rescue StandardError => e
|
||||
logger.error { e.full_message }
|
||||
end
|
||||
|
||||
def self.process_layout_variables(logger, layout, markup)
|
||||
layout&.each do |name, value|
|
||||
if value.nil?
|
||||
value = ''
|
||||
logger.warn { "layout.#{value} is undefined." }
|
||||
end
|
||||
markup.gsub!("{{layout.#{name}}}", value.to_s)
|
||||
end
|
||||
markup
|
||||
rescue StandardError => e
|
||||
logger.error { e.full_message }
|
||||
end
|
||||
|
||||
# Process assigned, captured and injected variables
|
||||
def self.process_liquid_variables(logger, scope, markup)
|
||||
scope&.each do |name, value|
|
||||
next if name.nil?
|
||||
|
||||
value = '' if value.nil?
|
||||
markup.gsub!("{{#{name}}}", value&.to_s)
|
||||
end
|
||||
markup
|
||||
rescue StandardError => e
|
||||
logger.error { e.full_message }
|
||||
end
|
||||
|
||||
def self.process_page_variables(logger, page, markup)
|
||||
page&.each_key do |key|
|
||||
next if %w[content excerpt next previous output].include? key # Skip problem attributes
|
||||
|
||||
markup.gsub!("{{page.#{key}}}", page[key].to_s)
|
||||
end
|
||||
markup
|
||||
rescue StandardError => e
|
||||
logger.error { e.full_message }
|
||||
end
|
||||
|
||||
def self.warn_short_trace(logger, error)
|
||||
remaining = error.backtrace.length - DISPLAYED_CALLS
|
||||
logger.warn do
|
||||
error.message + "\n" +
|
||||
error.backtrace.take(DISPLAYED_CALLS).join("\n") +
|
||||
"\n...Remaining #{remaining} call sites elided.\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
Registers = Struct.new(:page, :site) unless defined?(Registers)
|
||||
|
||||
# Mock for Collections
|
||||
class Collections
|
||||
def values
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
# Mock for Site
|
||||
class SiteMock
|
||||
attr_reader :config
|
||||
|
||||
def collections
|
||||
Collections.new
|
||||
end
|
||||
end
|
||||
|
||||
# Mock for Liquid::ParseContent
|
||||
class TestParseContext < Liquid::ParseContext
|
||||
attr_reader :line_number, :registers
|
||||
|
||||
# rubocop:disable Layout/CommentIndentation
|
||||
# def initialize
|
||||
# super
|
||||
# @line_number = 123
|
||||
|
||||
# @registers = Registers.new(
|
||||
# { 'path' => 'https://feeds.soundcloud.com/users/soundcloud:users:7143896/sounds.rss' },
|
||||
# SiteMock.new
|
||||
# )
|
||||
# end
|
||||
# rubocop:enable Layout/CommentIndentation
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
module JekyllPluginSupportVersion
|
||||
VERSION = '3.0.0'.freeze unless defined?(VERSION)
|
||||
end
|
||||
@@ -0,0 +1,106 @@
|
||||
module JekyllSupport
|
||||
# Base class for Jekyll tags
|
||||
class JekyllTag < Liquid::Tag
|
||||
attr_reader :argument_string, :helper, :line_number, :logger, :page, :site
|
||||
|
||||
# See https://github.com/Shopify/liquid/wiki/Liquid-for-Programmers#create-your-own-tags
|
||||
# @param tag_name [String] the name of the tag, which we usually know.
|
||||
# @param argument_string [String] the arguments passed to the tag, as a single string.
|
||||
# @param parse_context [Liquid::ParseContext] contains the following attributes:
|
||||
# @depth might have the value 0
|
||||
# @error_mode might have the value `:strict`
|
||||
# @line_number duplicates @ptions[:line_number]
|
||||
# @locale duplicates @ptions[:locale]
|
||||
# @options is a hash with the following two keys that holds Liquid options:
|
||||
# :locale is a Liquid::I18n object, used to display localized error messages on Liquid built-in tags and filters.
|
||||
# :line_number is the line number containing the plugin invocation.
|
||||
# See https://github.com/Shopify/liquid/wiki/Liquid-for-Programmers#create-your-own-tags
|
||||
# @partial Boolean, unclear what this indicates
|
||||
# @template_options Replicates @options
|
||||
# @trim_whitespace might have the value `false`
|
||||
# @warnings array
|
||||
# @return [void]
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
@tag_name = tag_name
|
||||
raise JekyllPluginSupportError, "markup is a #{markup.class} with value '#{markup}'." unless markup.instance_of? String
|
||||
|
||||
# Vars in plugin parameters cannot be replaced yet
|
||||
@argument_string = markup.to_s # Lookup variable names with values in markup in render because site and config are not available here
|
||||
|
||||
@logger = PluginMetaLogger.instance.new_logger(self, PluginMetaLogger.instance.config)
|
||||
@logger.debug { "#{self.class}: respond_to?(:no_arg_parsing) #{respond_to?(:no_arg_parsing) ? 'yes' : 'no'}." }
|
||||
@helper = JekyllPluginHelper.new(tag_name, @argument_string, @logger, respond_to?(:no_arg_parsing))
|
||||
|
||||
@error_name = "#{tag_name.camelcase(:upper)}Error"
|
||||
::JekyllSupport::CustomError.factory @error_name
|
||||
end
|
||||
|
||||
# Method prescribed by the Jekyll plugin lifecycle.
|
||||
def render(liquid_context)
|
||||
return if @helper.excerpt_caller
|
||||
|
||||
@helper.liquid_context = JekyllSupport.inject_config_vars liquid_context # modifies liquid_context
|
||||
|
||||
@envs = liquid_context.environments.first
|
||||
@page = liquid_context.registers[:page]
|
||||
@scopes = liquid_context.scopes
|
||||
@site = liquid_context.registers[:site]
|
||||
|
||||
@config = @site.config
|
||||
@tag_config = @config[@tag_name]
|
||||
@jps = @config['jekyll_plugin_support']
|
||||
@pry_on_standard_error = @jps['pry_on_standard_error'] || false if @jps
|
||||
|
||||
set_error_context
|
||||
|
||||
# @envs.keys are :content, :highlighter_prefix, :highlighter_suffix, :jekyll, :layout, :page, :paginator, :site, :theme
|
||||
@layout = @envs[:layout]
|
||||
@paginator = @envs[:paginator]
|
||||
@theme = @envs[:theme]
|
||||
|
||||
env = @config['env']
|
||||
@mode = env&.key?('JEKYLL_ENV') ? env['JEKYLL_ENV'] : 'development'
|
||||
|
||||
@argument_string = JekyllSupport.lookup_liquid_variables @logger, @helper.liquid_context, @argument_string.to_s.strip
|
||||
@helper.reinitialize @argument_string.to_s.strip
|
||||
|
||||
# @argument_string = JekyllSupport.lookup_liquid_variables @logger, liquid_context, @argument_string # Is this redundant?
|
||||
# @argument_string.strip! # Is this redundant?
|
||||
# @helper.reinitialize @argument_string # Is this redundant?
|
||||
|
||||
render_impl
|
||||
rescue StandardError => e
|
||||
e.shorten_backtrace
|
||||
file_name = e.backtrace[0]&.split(':')&.first
|
||||
in_file_name = "in '#{file_name}' " if file_name
|
||||
of_page = "of '#{@page['path']}'" if @page
|
||||
@logger.error { "#{e.class} on line #{@line_number} #{of_page} while processing #{tag_name} #{in_file_name}- #{e.message}" }
|
||||
binding.pry if @pry_on_standard_error # rubocop:disable Lint/Debugger
|
||||
raise e if @die_on_standard_error
|
||||
|
||||
<<~END_MSG
|
||||
<div class='standard_error'>
|
||||
#{e.class} on line #{@line_number} #{of_page} while processing #{tag_name} #{in_file_name} - #{JekyllPluginHelper.remove_html_tags e.message}
|
||||
</div>
|
||||
END_MSG
|
||||
end
|
||||
|
||||
# Jekyll plugins must override this method, not render, so their plugin can be tested more easily
|
||||
# The following variables are predefined:
|
||||
# @argument_string, @config, @envs, @helper, @layout, @logger, @mode, @page, @paginator, @site, @tag_name and @theme
|
||||
def render_impl
|
||||
abort "#{self.class}.render_impl for tag #{@tag_name} must be overridden, but it was not."
|
||||
end
|
||||
|
||||
def set_error_context
|
||||
return unless Object.const_defined? @error_name
|
||||
|
||||
error_class = Object.const_get @error_name
|
||||
error_class.class_variable_set(:@@argument_string, @argument_string)
|
||||
error_class.class_variable_set(:@@line_number, @line_number)
|
||||
error_class.class_variable_set(:@@path, @page['path'])
|
||||
error_class.class_variable_set(:@@tag_name, @tag_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
module JekyllSupport
|
||||
class JekyllTagNoArgParsing < JekyllTag
|
||||
attr_reader :argument_string, :helper, :line_number, :logger, :page, :site
|
||||
|
||||
def initialize(tag_name, argument_string, parse_context)
|
||||
class << self
|
||||
include NoArgParsing
|
||||
end
|
||||
|
||||
super
|
||||
@logger.debug { "#{self.class}: respond_to?(:no_arg_parsing) #{respond_to?(:no_arg_parsing) ? 'yes' : 'no'}." }
|
||||
end
|
||||
|
||||
# Jekyll plugins must override this method, not render, so their plugin can be tested more easily
|
||||
# The following variables are predefined:
|
||||
# @argument_string, @config, @envs, @helper, @layout, @logger, @mode, @page, @paginator, @site, @tag_name and @theme
|
||||
def render_impl
|
||||
abort "#{self.class}.render_impl for tag #{@tag_name} must be overridden, but it was not."
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,152 @@
|
||||
unless defined?(MSlinnBinarySearchError)
|
||||
class MSlinnBinarySearchError < StandardError
|
||||
end
|
||||
end
|
||||
|
||||
# Ruby's binary search is unsuitable because the value to be searched for changes the required ordering for String compares
|
||||
class MSlinnBinarySearch
|
||||
attr_reader :accessor_chain, :array # For testing only
|
||||
|
||||
def initialize(accessor_chain)
|
||||
@array = SortedSet.new # [LruFile] Ordered highest to lowest
|
||||
@accessor_chain = accessor_chain
|
||||
end
|
||||
|
||||
# Convert the SortedSet to an Array
|
||||
def enable_search
|
||||
@array = @array.to_a
|
||||
end
|
||||
|
||||
# A match is found when the Array[LruFile] has an href which starts with the given stem
|
||||
# @param stem [String]
|
||||
# @return first item from @array.url that matches, or nil if no match
|
||||
def find(stem)
|
||||
raise MSlinnBinarySearchError, 'Invalid find because stem to search for is nil.' if stem.nil?
|
||||
|
||||
index = find_index(stem)
|
||||
return nil if index.nil?
|
||||
|
||||
@array[index]
|
||||
end
|
||||
|
||||
# @param stem [String]
|
||||
# @return index of first matching stem, or nil if @array is empty, or 0 if no stem specified
|
||||
def find_index(stem)
|
||||
return nil if @array.empty?
|
||||
return 0 if stem.nil? || stem.empty?
|
||||
|
||||
mets = stem.reverse
|
||||
return nil if @array[0].url[0...mets.size] > mets # TODO: use chain eval for item
|
||||
return nil if @array[0].url[0] != mets[0]
|
||||
|
||||
_find_index(mets, 0, @array.length - 1)
|
||||
end
|
||||
|
||||
# @param stem [String]
|
||||
# @return [index] of matching values, or [] if @array is empty, or entire array if no stem specified
|
||||
def find_indices(stem)
|
||||
return [] if @array.empty?
|
||||
return @array if stem.nil? || stem.empty?
|
||||
|
||||
first_index = _find_index(stem, 0, @array.length - 1)
|
||||
last_index = first_index
|
||||
last_index += 1 while @array[last_index].url.start_with? stem
|
||||
[first_index..last_index]
|
||||
end
|
||||
|
||||
# @param item [LruFile]
|
||||
# @return [int] index of matching LruFile in @array, or nil if not found
|
||||
def index_of(lru_file)
|
||||
raise MSlinnBinarySearchError, 'Invalid index_of lru_file (nil).' if lru_file.nil?
|
||||
|
||||
find_index lru_file.url
|
||||
end
|
||||
|
||||
# @return [LruFile] item at given index in @array
|
||||
def item_at(index)
|
||||
if index > @array.length - 1
|
||||
raise MSlinnBinarySearchError,
|
||||
"Invalid item_at index (#{index}) is greater than maximum stem (#{@array.length - 1})."
|
||||
end
|
||||
raise MSlinnBinarySearchError, "Invalid item_at index (#{index}) is less than zero." if index.negative?
|
||||
|
||||
@array[index]
|
||||
end
|
||||
|
||||
# @param lru_file [LruFile]
|
||||
def insert(lru_file)
|
||||
raise MSlinnBinarySearchError, 'Invalid insert because new item is nil.' if lru_file.nil?
|
||||
raise MSlinnBinarySearchError, "Invalid insert because new item has no chain (#{lru_file})" if lru_file.chain.nil?
|
||||
|
||||
@array.add lru_file
|
||||
end
|
||||
|
||||
# TODO: Cache this method
|
||||
# @param suffix [String] to use stem search on
|
||||
# @return nil if @array is empty
|
||||
# @return the first item in @array if suffix is nil or an empty string
|
||||
def prefix_search(suffix)
|
||||
return nil if @array.empty?
|
||||
return @array[0] if suffix.empty? || suffix.nil?
|
||||
|
||||
low = search_index { |x| x.evaluate_with suffix }
|
||||
return [] if low.nil?
|
||||
|
||||
high = low
|
||||
high += 1 while high < @array.length &&
|
||||
@array[high].evaluate_with(suffix)
|
||||
@array[low..high]
|
||||
end
|
||||
|
||||
# @param stem [String]
|
||||
# @return [APage] matching APages, or [] if @array is empty, or entire array if no stem specified
|
||||
def select_pages(stem)
|
||||
first_index = find_index stem
|
||||
return [] if first_index.nil?
|
||||
|
||||
last_index = first_index
|
||||
while last_index < @array.length - 1
|
||||
# LruFile.url is reversed, bug LruFile.page is not
|
||||
break unless @array[last_index + 1].url.start_with?(stem.reverse)
|
||||
|
||||
last_index += 1
|
||||
end
|
||||
Range.new(first_index, last_index).map { |i| @array[i].page }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# A match is found when the Array[LruFile] has an href which starts with the given stem
|
||||
# @param stem [String]
|
||||
# @return [int] first index in @array that matches, or nil if no match
|
||||
def _find_index(mets, min_index, max_index)
|
||||
raise MSlinnBinarySearchError, "_find_index min_index(#{min_index})<0" if min_index.negative?
|
||||
raise MSlinnBinarySearchError, "_find_index min_index(#{min_index})>max_index(#{max_index})" if min_index > max_index
|
||||
raise MSlinnBinarySearchError, "_find_index max_index(#{max_index})>=@array.length(#{@array.length})" if max_index >= @array.length
|
||||
|
||||
return min_index if (min_index == max_index) && @array[min_index].url.start_with?(mets)
|
||||
|
||||
while min_index < max_index
|
||||
mid_index = (min_index + max_index) / 2
|
||||
mid_value = @array[mid_index].url[0...(mets.size)] # TODO: use chain eval for item
|
||||
|
||||
if mid_value == mets # back up until the first match is found
|
||||
index = mid_index
|
||||
loop do
|
||||
return 0 if index.zero?
|
||||
|
||||
return index unless @array[index - 1].url.start_with?(mets)
|
||||
|
||||
index -= 1
|
||||
end
|
||||
elsif mid_value > mets
|
||||
max_index = mid_index - 1
|
||||
return _find_index(mets, min_index, max_index)
|
||||
else
|
||||
min_index = mid_index + 1
|
||||
return _find_index(mets, min_index, max_index)
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,56 @@
|
||||
# Supports one chain at a time
|
||||
module SendChain
|
||||
# See https://stackoverflow.com/a/79333706/553865
|
||||
# This method can be called directly if no methods in the chain require arguments
|
||||
# Does not use any external state
|
||||
def send_chain(chain)
|
||||
Array(chain).inject(self) { |o, a| o.send(*a) }
|
||||
end
|
||||
|
||||
# Saves @chain structure containing :placeholders for arguments to be supplied later
|
||||
# Call when a different chain with :placeholders is desired
|
||||
def new_chain(chain)
|
||||
abort "new_chain error: chain must be an array ('#{chain}' was an #{chain.class.name})" \
|
||||
unless chain.instance_of?(Array)
|
||||
@chain = chain
|
||||
end
|
||||
|
||||
# Call after new_chain, to evaluate @chain with values
|
||||
def substitute_and_send_chain_with(values)
|
||||
send_chain substitute_chain_with values
|
||||
end
|
||||
|
||||
alias evaluate_with substitute_and_send_chain_with
|
||||
|
||||
# Call this method after calling new_chain to perform error checking and replace :placeholders with values.
|
||||
# @chain is not modified.
|
||||
# @return [Array] Modified chain
|
||||
def substitute_chain_with(values)
|
||||
values = [values] unless values.instance_of?(Array)
|
||||
|
||||
placeholder_count = @chain.flatten.count { |x| x == :placeholder }
|
||||
if values.length != placeholder_count
|
||||
abort "with_values error: number of values (#{values.length}) does not match the number of placeholders (#{placeholder_count})"
|
||||
end
|
||||
|
||||
eval_chain @chain, values
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Replaces :placeholders with values
|
||||
# Does not use any external state
|
||||
# @return modified chain
|
||||
def eval_chain(chain, values)
|
||||
chain.map do |c|
|
||||
case c
|
||||
when :placeholder
|
||||
values.shift
|
||||
when Array
|
||||
eval_chain c, values
|
||||
else
|
||||
c
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
require 'spec_helper'
|
||||
require_relative '../../lib/jekyll_all_collections'
|
||||
|
||||
class APageStub
|
||||
attr_reader :date, :last_modified, :label
|
||||
|
||||
def initialize(date, last_modified, label = '')
|
||||
@date = Date.parse date
|
||||
@last_modified = Date.parse last_modified
|
||||
@label = label
|
||||
end
|
||||
|
||||
def to_s
|
||||
@label
|
||||
end
|
||||
end
|
||||
|
||||
def show(lambda_string, result, expected)
|
||||
p "For lambda_string: #{lambda_string}"
|
||||
p " result: #{result.map(&:label).join(', ')} <==> expected: #{expected.map(&:label).join(', ')}"
|
||||
end
|
||||
|
||||
# See https://stackoverflow.com/a/75388137/553865
|
||||
RSpec.describe(AllCollectionsTag::AllCollectionsTag) do
|
||||
let(:o1) { APageStub.new('2020-01-01', '2020-01-01', 'a_A') }
|
||||
let(:o2) { APageStub.new('2021-01-01', '2020-01-01', 'b_A') }
|
||||
let(:o3) { APageStub.new('2021-01-01', '2023-01-01', 'b_B') }
|
||||
let(:o4) { APageStub.new('2022-01-01', '2023-01-01', 'c_B') }
|
||||
let(:objs) { [o1, o2, o3, o4] }
|
||||
|
||||
it 'defines sort_by lambda with last_modified' do
|
||||
sort_lambda = ->(a, b) { [a.last_modified] <=> [b.last_modified] }
|
||||
result = objs.sort(&sort_lambda)
|
||||
expect(result).to eq([o1, o2, o3, o4])
|
||||
end
|
||||
|
||||
it 'makes sort_by lambdas from stringified date' do
|
||||
sort_lambda = eval '->(a, b) { a.last_modified <=> b.last_modified }',
|
||||
NullBinding.new.min_binding, __FILE__, __LINE__ - 1
|
||||
result = objs.sort(&sort_lambda)
|
||||
expect(result).to eq([o1, o2, o3, o4])
|
||||
end
|
||||
|
||||
it 'makes sort_by lambdas from stringified array of last_modified' do
|
||||
sort_lambda = eval '->(a, b) { [a.last_modified] <=> [b.last_modified] }',
|
||||
NullBinding.new.min_binding, __FILE__, __LINE__ - 1
|
||||
result = objs.sort(&sort_lambda)
|
||||
expect(result).to eq([o1, o2, o3, o4])
|
||||
end
|
||||
|
||||
it 'makes sort_by lambdas with descending keys from stringified array of last_modified' do
|
||||
sort_lambda = eval '->(a, b) { [b.last_modified] <=> [a.last_modified] }',
|
||||
NullBinding.new.min_binding, __FILE__, __LINE__ - 1
|
||||
result = objs.sort(&sort_lambda)
|
||||
expected = [o3, o4, o1, o2]
|
||||
expect(result).to eq(expected)
|
||||
end
|
||||
|
||||
it 'create_lambda with 1 date key, descending' do
|
||||
lambda_string = described_class.create_lambda_string('-last_modified')
|
||||
sort_lambda = described_class.evaluate(lambda_string)
|
||||
result = objs.sort(&sort_lambda)
|
||||
expected = [o3, o4, o1, o2]
|
||||
# show(lambda_string, result, expected)
|
||||
expect(result).to eq(expected)
|
||||
end
|
||||
|
||||
it 'create_lambda with 1 date key, ascending' do
|
||||
lambda_string = described_class.create_lambda_string('date')
|
||||
sort_lambda = described_class.evaluate(lambda_string)
|
||||
result = objs.sort(&sort_lambda)
|
||||
expected = [o1, o2, o3, o4]
|
||||
# show(lambda_string, result, expected)
|
||||
expect(result).to eq(expected)
|
||||
end
|
||||
|
||||
it 'create_lambda with 2 date keys, both ascending' do
|
||||
lambda_string = described_class.create_lambda_string(%w[date last_modified])
|
||||
sort_lambda = described_class.evaluate(lambda_string)
|
||||
result = objs.sort(&sort_lambda)
|
||||
expected = [o1, o2, o3, o4]
|
||||
# show(lambda_string, result, expected)
|
||||
expect(result).to eq(expected)
|
||||
end
|
||||
|
||||
it 'create_lambda with 2 date keys, both descending' do
|
||||
lambda_string = described_class.create_lambda_string(['-date', '-last_modified'])
|
||||
sort_lambda = described_class.evaluate(lambda_string)
|
||||
result = objs.sort(&sort_lambda)
|
||||
expected = [o4, o3, o2, o1]
|
||||
# show(lambda_string, result, expected)
|
||||
expect(result).to eq(expected)
|
||||
end
|
||||
|
||||
it 'create_lambda with 2 date keys, first descending and second ascending' do
|
||||
lambda_string = described_class.create_lambda_string(['-date', 'last_modified'])
|
||||
sort_lambda = described_class.evaluate(lambda_string)
|
||||
result = objs.sort(&sort_lambda)
|
||||
expected = [o4, o2, o3, o1]
|
||||
# show(lambda_string, result, expected)
|
||||
expect(result).to eq(expected)
|
||||
end
|
||||
|
||||
it 'create_lambda with 2 date keys, first ascending and second descending' do
|
||||
lambda_string = described_class.create_lambda_string(['date', '-last_modified'])
|
||||
sort_lambda = described_class.evaluate(lambda_string)
|
||||
result = objs.sort(&sort_lambda)
|
||||
expected = [o1, o3, o2, o4]
|
||||
# show(lambda_string, result, expected)
|
||||
expect(result).to eq(expected)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
require 'spec_helper'
|
||||
|
||||
# Ruby's binary search is unsuitable because ordering requirements are not stable.
|
||||
# the value to be searched for changes the required ordering
|
||||
|
||||
RSpec.describe(Array) do
|
||||
before { skip('Never gonna give you up/Never gonna let you down') }
|
||||
|
||||
sorted_ints = [0, 4, 7, 10, 12]
|
||||
sorted_strings = %w[aaa aab aac bbb bbc bbd ccc ccd cce].sort.reverse
|
||||
|
||||
it 'returns index of first int match' do
|
||||
actual = sorted_ints.bsearch_index { |x| x >= 4 }
|
||||
expect(actual).to eq(1)
|
||||
|
||||
actual = sorted_ints.bsearch_index { |x| x >= 6 }
|
||||
expect(actual).to eq(2)
|
||||
|
||||
actual = sorted_ints.bsearch_index { |x| x >= -1 }
|
||||
expect(actual).to eq(0)
|
||||
|
||||
actual = sorted_ints.bsearch_index { |x| x >= 100 }
|
||||
expect(actual).to be_nil
|
||||
end
|
||||
|
||||
# See https://stackoverflow.com/q/79333097/553865
|
||||
it 'returns index of first string match' do
|
||||
puts(sorted_strings.map { |x| x.start_with? 'a' })
|
||||
index = sorted_strings.bsearch_index { |x| x.start_with? 'a' }
|
||||
expect(sorted_strings[index]).to eq('aac')
|
||||
|
||||
index = sorted_strings.bsearch_index { |x| x.start_with? 'aa' }
|
||||
expect(sorted_strings[index]).to eq('aac')
|
||||
|
||||
index = sorted_strings.bsearch_index { |x| x.start_with? 'aaa' }
|
||||
expect(sorted_strings[index]).to eq('aaa')
|
||||
|
||||
index = sorted_strings.bsearch_index { |x| x.start_with? 'b' }
|
||||
expect(sorted_strings[index]).to eq('bbd')
|
||||
|
||||
index = sorted_strings.bsearch_index { |x| x.start_with? 'bb' }
|
||||
expect(sorted_strings[index]).to eq('bbd')
|
||||
|
||||
index = sorted_strings.bsearch_index { |x| x.start_with? 'bbc' }
|
||||
expect(sorted_strings[index]).to eq('bbc')
|
||||
|
||||
index = sorted_strings.bsearch_index { |x| x.start_with? 'cc' }
|
||||
expect(sorted_strings[index]).to eq('cce')
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
require_relative '../lib/error/jekyll_custom_error'
|
||||
require_relative '../lib/jekyll_plugin_support/jekyll_plugin_support_class'
|
||||
|
||||
class Dummy
|
||||
def just_for_testing; end
|
||||
end
|
||||
|
||||
class CustomErrorSpec
|
||||
tag_name = 'test_tag'
|
||||
argument_string = 'This is the argument string'
|
||||
AnError = ::JekyllSupport.define_error
|
||||
AnError.class_variable_set(:@@tag_name, tag_name)
|
||||
AnError.class_variable_set(:@@argument_string, argument_string)
|
||||
|
||||
puts "AnError is a #{AnError.class}; StandardError is a #{StandardError.class}"
|
||||
begin
|
||||
raise AnError, 'Oops'
|
||||
rescue AnError => e
|
||||
puts "Caught AnError: #{e.message}"
|
||||
rescue ::JekyllSupport::CustomError => e
|
||||
puts "Caught CustomError: #{e.message}"
|
||||
end
|
||||
|
||||
RSpec.describe JekyllSupport::JekyllPluginHelper do
|
||||
it 'generates messages' do
|
||||
msg = described_class.generate_message(Dummy, tag_name, '0.1.0')
|
||||
puts msg
|
||||
expect(msg).to include('Error class: DummyError')
|
||||
expect(msg).to include('CSS class for error messages: dummy_error')
|
||||
expect(msg).to include('die_on_dummy_error: false')
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe ::JekyllSupport::CustomError do
|
||||
it 'can create custom errors' do
|
||||
expect { raise AnError, 'Oops' }.to raise_error(AnError)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,84 @@
|
||||
require 'spec_helper'
|
||||
require_relative '../lib/jekyll_plugin_support'
|
||||
|
||||
# Verifies how data comparisons work
|
||||
|
||||
class Obj
|
||||
# `last_modified` is primary sort key
|
||||
# `date` (when specified) is secondary sort key
|
||||
attr_reader :date, :last_modified
|
||||
|
||||
def initialize(param1, param2)
|
||||
@last_modified = Date.parse(param1)
|
||||
@date = Date.parse(param2)
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe(Obj) do
|
||||
let(:o1) { described_class.new('2000-01-01', '2001-01-01') }
|
||||
let(:o2) { described_class.new('2010-01-01', '2001-01-01') }
|
||||
let(:o3) { described_class.new('2010-01-01', '2011-01-01') }
|
||||
let(:o4) { described_class.new('2020-01-01', '2011-01-01') }
|
||||
let(:objs) { [o1, o2, o3, o4] }
|
||||
|
||||
# See https://ruby-doc.org/3.2.0/Comparable.html
|
||||
it 'compares one key with ascending dates' do
|
||||
expect([o1.last_modified] <=> [o2.last_modified]).to eq(-1)
|
||||
expect([o2.last_modified] <=> [o3.last_modified]).to eq(0)
|
||||
expect([o3.last_modified] <=> [o4.last_modified]).to eq(-1)
|
||||
end
|
||||
|
||||
it 'compares two keys with ascending dates' do
|
||||
expect([o1.last_modified, o1.date] <=> [o2.last_modified, o2.date]).to eq(-1)
|
||||
expect([o2.last_modified, o2.date] <=> [o3.last_modified, o3.date]).to eq(-1)
|
||||
expect([o3.last_modified, o3.date] <=> [o4.last_modified, o4.date]).to eq(-1)
|
||||
end
|
||||
|
||||
it 'compares one key with descending dates' do
|
||||
expect([o1.last_modified] <=> [o2.last_modified]).to eq(-1)
|
||||
expect([o2.last_modified] <=> [o3.last_modified]).to eq(0)
|
||||
end
|
||||
|
||||
it 'compares two keys with descending dates' do
|
||||
expect([o2.last_modified, o2.date] <=> [o1.last_modified, o1.date]).to eq(1)
|
||||
expect([o3.last_modified, o3.date] <=> [o2.last_modified, o2.date]).to eq(1)
|
||||
expect([o4.last_modified, o4.date] <=> [o3.last_modified, o3.date]).to eq(1)
|
||||
end
|
||||
|
||||
# See https://ruby-doc.org/3.2.0/Enumerable.html#method-i-sort
|
||||
it 'sort with one key ascending' do
|
||||
sort_lambda = ->(a, b) { [a.last_modified] <=> [b.last_modified] }
|
||||
result = objs.sort(&sort_lambda)
|
||||
expect(result).to eq([o1, o2, o3, o4])
|
||||
end
|
||||
|
||||
it 'sort with one key descending' do
|
||||
sort_lambda = ->(a, b) { [b.last_modified] <=> [a.last_modified] }
|
||||
result = objs.sort(&sort_lambda)
|
||||
expect(result).to eq([o4, o2, o3, o1])
|
||||
end
|
||||
|
||||
it 'sort with two keys ascending' do
|
||||
sort_lambda = ->(a, b) { [a.last_modified, a.date] <=> [b.last_modified, b.date] }
|
||||
result = objs.sort(&sort_lambda)
|
||||
expect(result).to eq([o1, o2, o3, o4])
|
||||
end
|
||||
|
||||
it 'sort with both keys descending' do
|
||||
sort_lambda = ->(a, b) { [b.last_modified, b.date] <=> [a.last_modified, a.date] }
|
||||
result = objs.sort(&sort_lambda)
|
||||
expect(result).to eq([o4, o3, o2, o1])
|
||||
end
|
||||
|
||||
it 'sort with last_modified descending and date ascending' do
|
||||
sort_lambda = ->(a, b) { [b.last_modified, a.date] <=> [a.last_modified, b.date] }
|
||||
result = objs.sort(&sort_lambda)
|
||||
expect(result).to eq([o4, o2, o3, o1])
|
||||
end
|
||||
|
||||
it 'sort with last_modified ascending and date descending' do
|
||||
sort_lambda = ->(a, b) { [a.last_modified, b.date] <=> [b.last_modified, a.date] }
|
||||
result = objs.sort(&sort_lambda)
|
||||
expect(result).to eq([o1, o3, o2, o4])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,64 @@
|
||||
require 'jekyll_plugin_logger'
|
||||
# require 'rspec/match_ignoring_whitespace'
|
||||
require_relative '../lib/jekyll_plugin_support'
|
||||
require_relative '../lib/jekyll_plugin_support/jekyll_plugin_support_spec_support'
|
||||
|
||||
class JekyllPluginHelperOptionsTest
|
||||
RSpec.describe ::JekyllSupport::JekyllPluginHelper do
|
||||
logger = PluginMetaLogger.instance.new_logger(self, PluginMetaLogger.instance.config)
|
||||
|
||||
it 'parses quoted string options' do
|
||||
helper = described_class.new('my_tag', "colors='blue or green' blah ick", logger, false)
|
||||
helper.reinitialize helper.markup
|
||||
expect(helper.keys_values.keys).to eq(%w[colors blah ick])
|
||||
|
||||
colors = helper.parameter_specified? 'colors'
|
||||
expect(colors).to eq('blue or green')
|
||||
|
||||
expect(helper.keys_values.keys).to eq(%w[blah ick])
|
||||
end
|
||||
|
||||
it 'parses unquoted string options' do
|
||||
helper = described_class.new('my_tag', 'color=blue blah ick', logger, false)
|
||||
helper.reinitialize helper.markup
|
||||
expect(helper.keys_values.keys).to eq(%w[color blah ick])
|
||||
|
||||
color = helper.parameter_specified? 'color'
|
||||
expect(color).to eq('blue')
|
||||
|
||||
expect(helper.keys_values.keys).to eq(%w[blah ick])
|
||||
end
|
||||
|
||||
it 'parses quoted booleans' do
|
||||
helper = described_class.new('my_tag', "bool1='true' bool2='false' blah ick", logger, false)
|
||||
helper.reinitialize helper.markup
|
||||
expect(helper.keys_values.keys).to eq(%w[bool1 bool2 blah ick])
|
||||
|
||||
bool1 = helper.parameter_specified? 'bool1'
|
||||
expect(bool1).to be true
|
||||
|
||||
bool2 = helper.parameter_specified? 'bool2'
|
||||
expect(bool2).to be false
|
||||
|
||||
expect(helper.keys_values.keys).to eq(%w[blah ick])
|
||||
|
||||
expect(helper.remaining_markup).to eq('blah ick')
|
||||
end
|
||||
|
||||
it 'parses unquoted booleans' do
|
||||
helper = described_class.new('my_tag', 'bool1=true bool2=false blah ick', logger, false)
|
||||
helper.reinitialize helper.markup
|
||||
expect(helper.keys_values.keys).to eq(%w[bool1 bool2 blah ick])
|
||||
|
||||
bool1 = helper.parameter_specified? 'bool1'
|
||||
expect(bool1).to be true
|
||||
|
||||
bool2 = helper.parameter_specified? 'bool2'
|
||||
expect(bool2).to be false
|
||||
|
||||
expect(helper.keys_values.keys).to eq(%w[blah ick])
|
||||
|
||||
expect(helper.remaining_markup).to eq('blah ick')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
require 'jekyll_plugin_logger'
|
||||
# require 'rspec/match_ignoring_whitespace'
|
||||
require_relative '../lib/jekyll_plugin_support'
|
||||
require_relative '../lib/jekyll_plugin_support/jekyll_plugin_support_spec_support'
|
||||
|
||||
class LiquidVariableParsing
|
||||
# @return copy of str with references to defined variables replaced by the values of the variables
|
||||
def self.variable_replace(str, scopes)
|
||||
result = str.clone
|
||||
match_data_list = str.to_enum(:scan, /{{[a-z_][a-zA-Z_0-9]*}}/).map { Regexp.last_match }.reverse
|
||||
match_data_list&.each do |md|
|
||||
from = md.begin(0)
|
||||
to = md.end(0) - 1
|
||||
ref = str[from..to]
|
||||
name = ref[2..-3]
|
||||
scopes&.each do |scope|
|
||||
value = scope.key?(name) ? scope[name] : ref
|
||||
# puts "str=#{str}; from=#{from}; to=#{to}; name=#{name} value=#{value}"
|
||||
result[from..to] = value
|
||||
end
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
RSpec.describe ::JekyllSupport::JekyllPluginHelper do
|
||||
it 'substitutes variable references for values without recursion' do
|
||||
scopes = [{ 'a' => '{{', 'b' => 'asdf', 'c' => '}}' }]
|
||||
str = '{{a}}{{b}}{{c}} This should be unchanged: {{d}}'
|
||||
new_str = LiquidVariableParsing.variable_replace(str, scopes)
|
||||
expect(str).to start_with('{{a}}')
|
||||
expect(new_str).to eq('{{asdf}} This should be unchanged: {{d}}')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
require 'spec_helper'
|
||||
require_relative '../lib/hooks/all_files'
|
||||
|
||||
def build_msbs(sorted_strings)
|
||||
sorted_strings.each do |string|
|
||||
$msbs.insert LruFile.new(string.reverse, "Page #{string}") # { |x| x.url.start_with? string }
|
||||
end
|
||||
$msbs.enable_search
|
||||
end
|
||||
|
||||
RSpec.describe(MSlinnBinarySearch) do
|
||||
$msbs = described_class.new [:url, %i[start_with? placeholder]]
|
||||
build_msbs %w[aaa.html baa.html caa.html bbb.html cbb.html dbb.html ccc.html ccd.html cce.html]
|
||||
|
||||
it 'handles an empty search string by returning the index of the first item (0)' do
|
||||
index = $msbs.find_index '' # { |x| x.url.start_with? '' }
|
||||
expect(index).to be_zero
|
||||
end
|
||||
|
||||
it 'returns the index of the first partial match' do
|
||||
index = $msbs.find_index 'a.html' # { |x| x.url.start_with? 'a.html' }
|
||||
expect(index).to eq(0)
|
||||
|
||||
index = $msbs.find_index 'baa.html' # { |x| x.url.start_with? 'baa.html' }
|
||||
expect(index).to eq(1)
|
||||
|
||||
index = $msbs.find_index 'c.html' # { |x| x.url.start_with? 'c.html' }
|
||||
expect(index).to eq(6)
|
||||
|
||||
index = $msbs.find_index 'cce.html' # { |x| x.url.start_with? 'cce.html' }
|
||||
expect(index).to eq(8)
|
||||
end
|
||||
|
||||
it 'returns the item of the first match' do
|
||||
item = $msbs.find 'a.html' # { |x| x.url.start_with? 'a.html' }
|
||||
expect(item.url.reverse).to eq('aaa.html')
|
||||
|
||||
item = $msbs.find 'baa.html' # { |x| x.url.start_with? 'baa.html' }
|
||||
expect(item.url.reverse).to eq('baa.html')
|
||||
|
||||
item = $msbs.find 'c.html' # { |x| x.url.start_with? 'c.html' }
|
||||
expect(item.url.reverse).to eq('ccc.html')
|
||||
|
||||
item = $msbs.find 'cce.html' # { |x| x.url.start_with? 'cce.html' }
|
||||
expect(item.url.reverse).to eq('cce.html')
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,72 @@
|
||||
require 'spec_helper'
|
||||
require_relative '../lib/util/send_chain'
|
||||
|
||||
unless defined? LruFile
|
||||
LruFile = Struct.new(:url, :page) do
|
||||
include SendChain
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe(LruFile) do
|
||||
lru_file = described_class.new 'abc', 'def'
|
||||
|
||||
it 'can perform a simple call if no arguments are required' do
|
||||
# Equivalent to: lru_file.url.reverse
|
||||
actual = lru_file.send_chain %i[url reverse]
|
||||
expect(actual).to eq('cba')
|
||||
end
|
||||
|
||||
it 'can accept a scalar argument in stages' do
|
||||
lru_file.new_chain [:url, %i[end_with? placeholder]]
|
||||
# Equivalent to: lru_file.url.end_with?('bc')
|
||||
substituted_chain = lru_file.substitute_chain_with 'bc'
|
||||
actual = lru_file.send_chain substituted_chain
|
||||
expect(actual).to be true
|
||||
end
|
||||
|
||||
it 'can accept a vector argument in stages' do
|
||||
lru_file.new_chain [:url, %i[end_with? placeholder]]
|
||||
# Next 2 lines are equivalent to: lru_file.url.end_with?('bc')
|
||||
substituted_chain = lru_file.substitute_chain_with ['bc']
|
||||
actual = lru_file.send_chain substituted_chain
|
||||
expect(actual).to be true
|
||||
end
|
||||
|
||||
it 'can accept a scalar argument in one stage' do
|
||||
lru_file.new_chain [:url, %i[end_with? placeholder]]
|
||||
# Equivalent to: lru_file.url.end_with?('bc')
|
||||
actual = lru_file.substitute_and_send_chain_with 'bc'
|
||||
expect(actual).to be true
|
||||
end
|
||||
|
||||
it 'can accept an array argument in one stage' do
|
||||
lru_file.new_chain [:url, %i[end_with? placeholder]]
|
||||
# Equivalent to: lru_file.url.end_with?('bc')
|
||||
actual = lru_file.substitute_and_send_chain_with ['bc']
|
||||
expect(actual).to be true
|
||||
end
|
||||
|
||||
it 'can use the evaluate_with alias' do
|
||||
lru_file.new_chain [:url, %i[end_with? placeholder]]
|
||||
# Equivalent to: lru_file.url.end_with?('bc')
|
||||
actual = lru_file.evaluate_with ['bc']
|
||||
expect(actual).to be true
|
||||
end
|
||||
|
||||
it 'can reuse the chain with different values' do
|
||||
lru_file.new_chain [:url, %i[end_with? placeholder]]
|
||||
|
||||
# Equivalent to: lru_file.url.end_with?('bc')
|
||||
actual = lru_file.substitute_and_send_chain_with 'bc'
|
||||
expect(actual).to be true
|
||||
|
||||
# Equivalent to: lru_file.url.end_with?('abc')
|
||||
substituted_chain = lru_file.substitute_chain_with ['abc']
|
||||
actual = lru_file.send_chain substituted_chain
|
||||
expect(actual).to be true
|
||||
|
||||
# Equivalent to: lru_file.url.end_with?('de')
|
||||
actual = lru_file.substitute_and_send_chain_with 'de'
|
||||
expect(actual).to be false
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
require 'spec_helper'
|
||||
|
||||
# Exploring how to set date attributes in an object
|
||||
|
||||
class Obj
|
||||
# `last_modified` is primary sort key
|
||||
# `date` (when specified) is secondary sort key
|
||||
attr_reader :date, :last_modified
|
||||
|
||||
def initialize(param1, param2)
|
||||
@last_modified = Date.parse(param1)
|
||||
@date = Date.parse(param2)
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe(Obj) do
|
||||
let(:obj) { described_class.new('2000-01-01', '2001-01-01') }
|
||||
|
||||
it 'can send date' do
|
||||
date = obj.send :date
|
||||
expect(obj.date).to eq(date)
|
||||
end
|
||||
|
||||
it 'can send last_modified' do
|
||||
last_modified = obj.send :last_modified
|
||||
expect(obj.last_modified).to eq(last_modified)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,82 @@
|
||||
require 'spec_helper'
|
||||
require_relative '../lib/hooks/all_files'
|
||||
|
||||
RSpec.describe(SortedLruFiles) do
|
||||
expected1 = 'first-page.html'
|
||||
expected2 = 'second-page.html'
|
||||
expected3 = 'third-page.html'
|
||||
|
||||
it 'can read back an inserted item' do
|
||||
sorted_files = described_class.new
|
||||
sorted_files.insert(expected1, "https://mslinn.com/#{expected1}") # insert reverses expected1
|
||||
sorted_files.enable_search
|
||||
|
||||
actual = sorted_files.select expected1
|
||||
expect(actual.length).to eq(1)
|
||||
expect(actual.first&.reverse&.start_with?(expected1.reverse)).to be true
|
||||
|
||||
actual = sorted_files.select expected1[5..]
|
||||
expect(actual.length).to eq(1)
|
||||
expect(actual.first&.reverse&.start_with?(expected1[5..].reverse)).to be true
|
||||
|
||||
actual = sorted_files.select 'should_not_match'
|
||||
expect(actual).to be_empty
|
||||
end
|
||||
|
||||
it 'works with 2 items and one match' do
|
||||
sorted_files = described_class.new
|
||||
sorted_files.insert(expected1, "https://mslinn.com/#{expected1}") # insert reverses expected1
|
||||
sorted_files.insert(expected2, "https://mslinn.com/#{expected2}")
|
||||
sorted_files.enable_search
|
||||
|
||||
expect(sorted_files.msbs.array[0].url).to be <= sorted_files.msbs.array[1].url
|
||||
|
||||
actual = sorted_files.select expected1
|
||||
expect(actual.length).to eq(1)
|
||||
expect(actual.first&.end_with?(expected1)).to be true
|
||||
|
||||
actual = sorted_files.select expected2
|
||||
expect(actual.length).to eq(1)
|
||||
expect(actual.first&.end_with?(expected2)).to be true
|
||||
end
|
||||
|
||||
it 'handles multiple matches' do
|
||||
sorted_files = described_class.new
|
||||
sorted_files.insert(expected1, "https://mslinn.com/#{expected1}") # insert reverses expected1
|
||||
sorted_files.insert(expected2, "https://mslinn.com/#{expected2}")
|
||||
sorted_files.enable_search
|
||||
|
||||
expected = '.html'
|
||||
actual = sorted_files.select expected
|
||||
expect(actual.length).to eq(2)
|
||||
expect(actual.first&.end_with?(expected)).to be true
|
||||
end
|
||||
|
||||
it 'works with 3 items' do
|
||||
sorted_files = described_class.new
|
||||
sorted_files.insert(expected1, "https://mslinn.com/#{expected1}") # insert reverses expected1
|
||||
sorted_files.insert(expected2, "https://mslinn.com/#{expected2}")
|
||||
sorted_files.insert(expected3, "https://mslinn.com/#{expected3}")
|
||||
sorted_files.enable_search
|
||||
|
||||
expect(sorted_files.msbs.array[0].url).to be <= sorted_files.msbs.array[1].url
|
||||
expect(sorted_files.msbs.array[1].url).to be <= sorted_files.msbs.array[2].url
|
||||
|
||||
actual = sorted_files.select expected1
|
||||
expect(actual.length).to eq(1)
|
||||
expect(actual.first&.end_with?(expected1)).to be true
|
||||
|
||||
actual = sorted_files.select expected2
|
||||
expect(actual.length).to eq(1)
|
||||
expect(actual.first&.end_with?(expected2)).to be true
|
||||
|
||||
actual = sorted_files.select expected3
|
||||
expect(actual.length).to eq(1)
|
||||
expect(actual.first&.end_with?(expected3)).to be true
|
||||
|
||||
expected = '.html'
|
||||
actual = sorted_files.select expected
|
||||
expect(actual.length).to eq(3)
|
||||
expect(actual.first&.end_with?(expected)).to be true
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
require 'jekyll'
|
||||
require_relative '../lib/jekyll_plugin_support'
|
||||
|
||||
Jekyll.logger.log_level = :info
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.filter_run_when_matching focus: true
|
||||
# config.order = 'random'
|
||||
|
||||
# See https://relishapp.com/rspec/rspec-core/docs/command-line/only-failures
|
||||
config.example_status_persistence_file_path = 'spec/status_persistence.txt'
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
example_id | status | run_time |
|
||||
----------------------------------------------- | ------ | --------------- |
|
||||
./spec/jekyll_block_plugin_support_spec.rb[1:1] | passed | 0.00062 seconds |
|
||||
@@ -0,0 +1,38 @@
|
||||
require 'spec_helper'
|
||||
|
||||
# Exploring how to invoke methods from classes and modules
|
||||
# See https://mslinn.com/jekyll/10700-designing-for-testability.html
|
||||
|
||||
module TestModule
|
||||
def a_method
|
||||
'a_method says Hi!'
|
||||
end
|
||||
end
|
||||
|
||||
class TestClass
|
||||
extend TestModule # Defines class methods
|
||||
|
||||
def initialize(param1, param2)
|
||||
super()
|
||||
@param1 = param1
|
||||
@param2 = param2
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe(TestModule) do
|
||||
extend described_class
|
||||
|
||||
it 'Invokes a_method from module' do
|
||||
result = self.class.a_method
|
||||
expect(result).to eq('a_method says Hi!')
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe(TestClass) do
|
||||
let(:o1) { described_class.new('value1', 'value2') }
|
||||
|
||||
it 'Invokes a_method from class' do
|
||||
result = o1.class.a_method
|
||||
expect(result).to eq('a_method says Hi!')
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user