Skip to content

Commit

Permalink
working POC
Browse files Browse the repository at this point in the history
  • Loading branch information
did committed Sep 17, 2024
1 parent 4386db6 commit a6c93c3
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 36 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ source 'https://rubygems.org'

gemspec

gem 'parser'

group :development do
# gem 'locomotivecms_common', github: 'locomotivecms/common', ref: '4d1bd56'
# gem 'locomotivecms_common', path: '../common'
Expand Down
5 changes: 5 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,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 @@ -123,6 +124,9 @@ GEM
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 Expand Up @@ -190,6 +194,7 @@ DEPENDENCIES
memory_profiler
mongo (~> 2.18.2)
origin (~> 2.3.1)
parser
puma (~> 6.4.0)
rack (~> 3.0)
rack-mini-profiler (~> 0.10.1)
Expand Down
2 changes: 2 additions & 0 deletions 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 Down
233 changes: 201 additions & 32 deletions lib/locomotive/steam/liquid/tags/with_scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,120 @@ module Tags
# {{ project.name }}
# {% endfor %}
# {% endwith_scope %}
#
#

# s(:hash,
# s(:pair,
# s(:sym, :key),
# s(:array,
# s(:int, 1),
# s(:int, 2),
# s(:int, 3))))
class WithScope < ::Liquid::Block

class HashProcessor
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)
pp node.location
pp node.location.expression.source

::Liquid::Expression.parse(node.location.expression.source)

# raise 'TODO'

# source_expr, name_expr = *node

# if source_expr.nil?
# ::Liquid::Expression.parse(name_expr.to_s)
# 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

# TODO: create our own mixin
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

node = new_node unless new_node.nil?

node
end
end

include Concerns::Attributes

# Regexps and Arrays are allowed
Expand All @@ -25,31 +135,60 @@ class WithScope < ::Liquid::Block

# a slight different from the Shopify implementation because we allow stuff like `started_at.le`
TagAttributes = /([a-zA-Z_0-9\.]+)\s*\:\s*(#{ArrayFragment}|#{RegexpFragment}|#{::Liquid::QuotedFragment})/o.freeze
SingleVariable = /(#{::Liquid::VariableSignature}+)/om.freeze
# SingleVariable = /(#{::Liquid::VariableSignature}+)/om.freeze
SingleVariable = /\A\w*([a-zA-Z_0-9]+)\w*\z/om.freeze

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

attr_reader :attributes, :attributes_var_name
OPERATORS = %w(all exists gt gte in lt lte ne nin size near within)

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

attr_reader :attributes, :attributes_var_name, :ast

def initialize(tag_name, markup, options)
super

# simple hash?
parse_attributes(markup) { |value| parse_attribute(value) }
# convert symbol operators into valid ruby code
markup.gsub!(SYMBOL_OPERATORS_REGEXP, ':"\1" =>')

if attributes.empty? && markup =~ SingleVariable
pp markup

if markup =~ SingleVariable
puts "HERE?"
# alright, maybe we'vot got a single variable built
# with the Action liquid tag instead?
@attributes_var_name = Regexp.last_match(1)
elsif markup.present?
ast = Parser::CurrentRuby.parse("{%s}" % markup)
pp ast
@attributes = HashProcessor.new.process(ast)
puts "-----"
pp @attributes
end

if attributes.empty? && attributes_var_name.blank?
if attributes.blank? && attributes_var_name.blank?
raise ::Liquid::SyntaxError.new("Syntax Error in 'with_scope' - Valid syntax: with_scope <name_1>: <value_1>, ..., <name_n>: <value_n>")
end

# raise 'TODO'

# # simple hash?
# parse_attributes(markup) { |value| parse_attribute(value) }

# if attributes.empty? && markup =~ SingleVariable
# # alright, maybe we'vot got a single variable built
# # with the Action liquid tag instead?
# @attributes_var_name = Regexp.last_match(1)
# end

# if attributes.empty? && attributes_var_name.blank?
# raise ::Liquid::SyntaxError.new("Syntax Error in 'with_scope' - Valid syntax: with_scope <name_1>: <value_1>, ..., <name_n>: <value_n>")
# end
end

def render(context)
Expand All @@ -65,44 +204,74 @@ def render(context)

protected

def parse_attribute(value)
case value
when StrictRegexpFragment
# let the cast_value attribute create the Regexp (done during the rendering phase)
value
when ArrayFragment
$1.split(',').map { |_value| parse_attribute(_value) }
else
::Liquid::Expression.parse(value)
end
end
# def parse_attribute(value)
# case value
# when StrictRegexpFragment
# # let the cast_value attribute create the Regexp (done during the rendering phase)
# value
# when ArrayFragment
# $1.split(',').map { |_value| parse_attribute(_value) }
# else
# ::Liquid::Expression.parse(value)
# end
# end

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

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

# evaluate the value if possible before casting it
_value = value.is_a?(::Liquid::VariableLookup) ? context.evaluate(value) : value
# puts [_key, evaluate_attribute(context, value)]

hash[_key] = cast_value(context, _value)
end
memo.merge({ _key => evaluate_attribute(context, value) })
end
end

def cast_value(context, value)
def evaluate_attribute(context, value)
pp "evaluate_attribute = #{value}"
case value
when Array then value.map { |_value| cast_value(context, _value) }
when StrictRegexpFragment then create_regexp($1, $2)
else
_value = context.evaluate(value)
_value.respond_to?(:_id) ? _value.send(:_source) : _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 ::Liquid::VariableLookup
evaluated_value = context.evaluate(value)
evaluated_value.respond_to?(:_id) ? evaluated_value.send(:_source) : evaluate_attribute(context, evaluated_value)
when StrictRegexpFragment
create_regexp($1, $2)
else
value
end
end

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

# HashWithIndifferentAccess.new.tap do |hash|
# attributes.each do |key, value|
# # _slug instead of _permalink
# _key = key.to_s == '_permalink' ? '_slug' : key.to_s

# # evaluate the value if possible before casting it
# _value = value.is_a?(::Liquid::VariableLookup) ? context.evaluate(value) : value

# hash[_key] = cast_value(context, _value)
# end
# end
# end

# def cast_value(context, value)
# case value
# when Array then value.map { |_value| cast_value(context, _value) }
# when StrictRegexpFragment then create_regexp($1, $2)
# else
# _value = context.evaluate(value)
# _value.respond_to?(:_id) ? _value.send(:_source) : _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]
Expand Down
Loading

0 comments on commit a6c93c3

Please sign in to comment.