Skip to content

Commit

Permalink
Merge pull request #239 from locomotivecms/new-attribute-parser
Browse files Browse the repository at this point in the history
New attribute parser for the with_scope tag
  • Loading branch information
did authored Oct 4, 2024
2 parents 4386db6 + 782e813 commit 25420ca
Show file tree
Hide file tree
Showing 20 changed files with 320 additions and 101 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/continuous_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1.4
ruby-version: 3.3.5
bundler-cache: true
cache-version: 1

Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ group :test do

gem 'rack-test', '~> 2.1.0'

gem 'simplecov', '~> 0.22.0', require: false
gem 'simplecov', '~> 0.22.0', require: false
end
7 changes: 5 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ PATH
morphine (~> 0.1.1)
multi_json (~> 1.15.0)
nokogiri (~> 1.15.6)
parser (~> 3.3)
pony (~> 1.12)
rack-cache (>= 1.7, < 2)
rack-rewrite (~> 1.5.1)
Expand All @@ -41,6 +42,7 @@ GEM
tzinfo (~> 2.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
ast (2.4.2)
attr_extras (7.1.0)
base64 (0.2.0)
bcrypt (3.1.20)
Expand Down Expand Up @@ -120,9 +122,10 @@ GEM
nokogiri (1.15.6)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.15.6-x86_64-darwin)
racc (~> 1.4)
origin (2.3.1)
parser (3.3.5.0)
ast (~> 2.4.1)
racc
pony (1.13.1)
mail (>= 2.0)
public_suffix (6.0.0)
Expand Down
4 changes: 3 additions & 1 deletion lib/locomotive/steam/liquid.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
require 'liquid'
require 'parser/current'
require 'ast'

require_relative 'liquid/errors'
require_relative 'liquid/patches'
Expand All @@ -7,6 +9,6 @@
require_relative 'liquid/drops/i18n_base'
require_relative 'liquid/tags/hybrid'
require_relative 'liquid/tags/concerns/section'
require_relative 'liquid/tags/concerns/attributes'
require_relative 'liquid/tags/concerns/simple_attributes_parser'
require_relative 'liquid/tags/section'
require_relative_all %w(. drops filters tags/concerns tags), 'liquid'
63 changes: 63 additions & 0 deletions lib/locomotive/steam/liquid/tags/concerns/attributes_evaluator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
module Locomotive
module Steam
module Liquid
module Tags
module Concerns

# Evaluates the attributes parsed the AttributesParser
module AttributesEvaluator
extend ActiveSupport::Concern

included do
# Regexps are allowed as strings
RegexpFragment = /\/([^\/]+)\/([imx]+)?/o.freeze
StrictRegexpFragment = /\A#{RegexpFragment}\z/o.freeze

REGEX_OPTIONS = {
'i' => Regexp::IGNORECASE,
'm' => Regexp::MULTILINE,
'x' => Regexp::EXTENDED
}.freeze
end

protected

def evaluate_attributes(context)
@attributes = context[attributes_var_name] || {} if attributes_var_name.present?

attributes.inject({}) do |memo, (key, value)|
# _slug instead of _permalink
_key = key.to_s == '_permalink' ? '_slug' : key.to_s

memo.merge({ _key => evaluate_attribute(context, value) })
end
end

def evaluate_attribute(context, value)
case value
when Array
value.map { |v| evaluate_attribute(context, v) }
when Hash
Hash[value.map { |k, v| [k.to_s, evaluate_attribute(context, v)] }]
when StrictRegexpFragment
create_regexp($1, $2)
when ::Liquid::VariableLookup
evaluated_value = context.evaluate(value)
evaluated_value.respond_to?(:_id) ? evaluated_value.send(:_source) : evaluate_attribute(context, evaluated_value)
else
value
end
end

def create_regexp(value, unparsed_options)
options = unparsed_options.blank? ? nil : unparsed_options.split('').uniq.inject(0) do |_options, letter|
_options |= REGEX_OPTIONS[letter]
end
Regexp.new(value, options)
end
end
end
end
end
end
end
156 changes: 156 additions & 0 deletions lib/locomotive/steam/liquid/tags/concerns/attributes_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
module Locomotive
module Steam
module Liquid
module Tags
module Concerns

# The with_scope liquid tag lets the developer use a Ruby syntax to
# pass options which is difficult to implement with the Liquid parsing
# approach (see the SimpleAttributesParser for instance)
module AttributesParser
extend ActiveSupport::Concern

included do
# Mongoid operators available on symbols
OPERATORS = %w(all exists gt gte in lt lte ne nin size near within)

SYMBOL_OPERATORS_REGEXP = /(\w+\.(#{OPERATORS.join('|')})){1}\s*\:/o
end

def parse_markup(markup)
parser = self.class.current_parser

# 'liquid_code.rb' is purely arbitrary
source_buffer = ::Parser::Source::Buffer.new('liquid_code.rb')
source_buffer.source = "{%s}" % clean_markup(markup)

ast = parser.parse(source_buffer)
AstProcessor.new.process(ast)
end

private

def clean_markup(markup)
# convert symbol operators into valid ruby code
markup.gsub(SYMBOL_OPERATORS_REGEXP, ':"\1" =>')
end

class_methods do
def current_parser
(@current_parser ||= build_parser).tap do |parser|
parser.reset
end
end

def build_parser
::Parser::CurrentRuby.new.tap do |parser|
# Silent the error instead of logging them to STDERR (default behavior of the parser)
parser.diagnostics.consumer = ->(message) { true }
end
end
end

class AstProcessor
include AST::Processor::Mixin

def on_hash(node)
nodes = process_all(node)
nodes.inject({}) { |memo, sub_hash| memo.merge(sub_hash) }
end

def on_pair(node)
key_expr, right_expr = *node
{ process(key_expr) => process(right_expr) }
end

def on_sym(node)
node.children.first.to_sym
end

def on_array(node)
process_all(node)
end

def on_int(node)
node.children.first.to_i
end

def on_float(node)
node.children.first.to_f
end

def on_str(node)
node.children.first.to_s
end

def on_true(node)
true
end

def on_false(node)
false
end

def on_regexp(node)
regexp_expr, opts_expr = *node
Regexp.new(process(regexp_expr), process(opts_expr))
end

def on_regopt(node)
node.children ? node.children.join('') : nil
end

def on_deep_send(node)
source_expr, name_expr = *node

if source_expr.nil?
[name_expr.to_s]
elsif source_expr.type == :send
process(source_expr.updated(:deep_send, nil)) << name_expr.to_s
else
raise 'NOT IMPLEMENTED [DEEP_SEND]' # TODO
end
end

def on_send(node)
source_expr, name_expr = *node

if source_expr.nil?
::Liquid::Expression.parse(name_expr.to_s)
elsif name_expr == :+
process(source_expr)
elsif source_expr.type == :send
::Liquid::Expression.parse(
(process(source_expr.updated(:deep_send, nil)) << name_expr.to_s).join('.')
)
else
raise 'NOT IMPLEMENTED [SEND]' # TODO
end
end

# HACK: override the default process implementation
def process(node)
return if node.nil?

node = node.to_ast

# Invoke a specific handler
on_handler = :"on_#{node.type}"
if respond_to? on_handler
new_node = send on_handler, node
else
new_node = handler_missing(node)
end

# fix: the original method considered false as nil which is incorrect
node = new_node unless new_node.nil?

node
end
end
end
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ module Concerns
# Many of Liquid tags have attributes (like options)
# This module makes sure we use the same reliable way to
# extract and evaluate them.

module Attributes
module SimpleAttributesParser

attr_reader :attributes, :raw_attributes

Expand Down
2 changes: 1 addition & 1 deletion lib/locomotive/steam/liquid/tags/consume.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module Tags
#
class Consume < ::Liquid::Block

include Concerns::Attributes
include Concerns::SimpleAttributesParser

Syntax = /(#{::Liquid::VariableSignature}+)\s*from\s*(#{::Liquid::QuotedFragment}+),?(.+)?/o.freeze

Expand Down
2 changes: 1 addition & 1 deletion lib/locomotive/steam/liquid/tags/editable/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module Tags
module Editable
class Base < ::Liquid::Block

include Concerns::Attributes
include Concerns::SimpleAttributesParser

Syntax = /(#{::Liquid::QuotedFragment})(\s*,\s*#{::Liquid::Expression}+)?/o

Expand Down
2 changes: 1 addition & 1 deletion lib/locomotive/steam/liquid/tags/inherited_block.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module Tags
#
class InheritedBlock < ::Liquid::Block

include Concerns::Attributes
include Concerns::SimpleAttributesParser

SYNTAX = /(#{::Liquid::QuotedFragment}+)/o

Expand Down
2 changes: 1 addition & 1 deletion lib/locomotive/steam/liquid/tags/link_to.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module Liquid
module Tags
class LinkTo < Hybrid

include Concerns::Attributes
include Concerns::SimpleAttributesParser
include Concerns::I18nPage
include Concerns::Path

Expand Down
3 changes: 1 addition & 2 deletions lib/locomotive/steam/liquid/tags/locale_switcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ module Tags
# - "iso" is the default choice for label
# - " | " is the default separating code
#

class LocaleSwitcher < ::Liquid::Tag

include Concerns::Attributes
include Concerns::SimpleAttributesParser
include Concerns::I18nPage

attr_reader :attributes, :site, :page, :current_locale, :url_builder
Expand Down
2 changes: 1 addition & 1 deletion lib/locomotive/steam/liquid/tags/model_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module Tags
#
class ModelForm < ::Liquid::Block

include Concerns::Attributes
include Concerns::SimpleAttributesParser

Syntax = /(#{::Liquid::QuotedFragment})\s*,*(.*)?/o.freeze

Expand Down
2 changes: 1 addition & 1 deletion lib/locomotive/steam/liquid/tags/paginate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module Tags
#
class Paginate < ::Liquid::Block

include Concerns::Attributes
include Concerns::SimpleAttributesParser

Syntax = /(#{::Liquid::QuotedFragment}+)\s+by\s+(#{::Liquid::QuotedFragment}+)/o

Expand Down
2 changes: 1 addition & 1 deletion lib/locomotive/steam/liquid/tags/path_to.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module Liquid
module Tags
class PathTo < ::Liquid::Tag

include Concerns::Attributes
include Concerns::SimpleAttributesParser
include Concerns::I18nPage
include Concerns::Path

Expand Down
2 changes: 1 addition & 1 deletion lib/locomotive/steam/liquid/tags/redirect_to.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module Tags

class RedirectTo < ::Liquid::Tag

include Concerns::Attributes
include Concerns::SimpleAttributesParser
include Concerns::I18nPage
include Concerns::Path

Expand Down
2 changes: 1 addition & 1 deletion lib/locomotive/steam/liquid/tags/section.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module Tags
class Section < ::Liquid::Include

include Concerns::Section
include Concerns::Attributes
include Concerns::SimpleAttributesParser

Syntax = /(#{::Liquid::QuotedString}|#{::Liquid::VariableSignature}+)\s*,*(.*)?/o.freeze

Expand Down
Loading

0 comments on commit 25420ca

Please sign in to comment.