This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user