Skip to content

Commit

Permalink
Provide a Liquid find tag as easier alternative to where_exp (#101)
Browse files Browse the repository at this point in the history
* Provide a Liquid find tag as easier alternative to where_exp

* Rebase, allow commas inside of strings for find tag
  • Loading branch information
jaredcwhite authored Sep 10, 2020
1 parent 9603e8c commit 991a0ef
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 52 deletions.
49 changes: 1 addition & 48 deletions bridgetown-core/lib/bridgetown-core/filters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module Filters
include URLFilters
include GroupingFilters
include DateFilters
include ConditionHelpers

# Convert a Markdown string into HTML output.
#
Expand Down Expand Up @@ -423,54 +424,6 @@ def as_liquid(item)
end
end
end

# ----------- The following set of code was *adapted* from Liquid::If
# ----------- ref: https://git.io/vp6K6

# Parse a string to a Liquid Condition
def parse_condition(exp)
parser = Liquid::Parser.new(exp)
condition = parse_binary_comparison(parser)

parser.consume(:end_of_string)
condition
end

# Generate a Liquid::Condition object from a Liquid::Parser object additionally processing
# the parsed expression based on whether the expression consists of binary operations with
# Liquid operators `and` or `or`
#
# - parser: an instance of Liquid::Parser
#
# Returns an instance of Liquid::Condition
def parse_binary_comparison(parser)
condition = parse_comparison(parser)
first_condition = condition
while (binary_operator = parser.id?("and") || parser.id?("or"))
child_condition = parse_comparison(parser)
condition.send(binary_operator, child_condition)
condition = child_condition
end
first_condition
end

# Generates a Liquid::Condition object from a Liquid::Parser object based on whether the parsed
# expression involves a "comparison" operator (e.g. <, ==, >, !=, etc)
#
# - parser: an instance of Liquid::Parser
#
# Returns an instance of Liquid::Condition
def parse_comparison(parser)
left_operand = Liquid::Expression.parse(parser.expression)
operator = parser.consume?(:comparison)

# No comparison-operator detected. Initialize a Liquid::Condition using only left operand
return Liquid::Condition.new(left_operand) unless operator

# Parse what remained after extracting the left operand and the `:comparison` operator
# and initialize a Liquid::Condition object using the operands and the comparison-operator
Liquid::Condition.new(left_operand, operator, Liquid::Expression.parse(parser.expression))
end
end
end

Expand Down
56 changes: 56 additions & 0 deletions bridgetown-core/lib/bridgetown-core/filters/condition_helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

module Bridgetown
module Filters
module ConditionHelpers
# ----------- The following set of code was *adapted* from Liquid::If
# ----------- ref: https://git.io/vp6K6

# Parse a string to a Liquid Condition
def parse_condition(exp)
parser = Liquid::Parser.new(exp)
condition = parse_binary_comparison(parser)

parser.consume(:end_of_string)
condition
end

# Generate a Liquid::Condition object from a Liquid::Parser object additionally processing
# the parsed expression based on whether the expression consists of binary operations with
# Liquid operators `and` or `or`
#
# - parser: an instance of Liquid::Parser
#
# Returns an instance of Liquid::Condition
def parse_binary_comparison(parser)
condition = parse_comparison(parser)
first_condition = condition
while (binary_operator = parser.id?("and") || parser.id?("or"))
child_condition = parse_comparison(parser)
condition.send(binary_operator, child_condition)
condition = child_condition
end
first_condition
end

# Generates a Liquid::Condition object from a Liquid::Parser object based
# on whether the parsed expression involves a "comparison" operator
# (e.g. <, ==, >, !=, etc)
#
# - parser: an instance of Liquid::Parser
#
# Returns an instance of Liquid::Condition
def parse_comparison(parser)
left_operand = Liquid::Expression.parse(parser.expression)
operator = parser.consume?(:comparison)

# No comparison-operator detected. Initialize a Liquid::Condition using only left operand
return Liquid::Condition.new(left_operand) unless operator

# Parse what remained after extracting the left operand and the `:comparison` operator
# and initialize a Liquid::Condition object using the operands and the comparison-operator
Liquid::Condition.new(left_operand, operator, Liquid::Expression.parse(parser.expression))
end
end
end
end
86 changes: 86 additions & 0 deletions bridgetown-core/lib/bridgetown-core/tags/find.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# frozen_string_literal: true

module Bridgetown
module Tags
class Find < Liquid::Tag
include Bridgetown::Filters::ConditionHelpers
include Bridgetown::LiquidExtensions

SYNTAX = %r!^(.*?) (where|in) (.*?),(.*)$!.freeze
CONDITIONS_SEP = "~FINDSEP~"

def initialize(tag_name, markup, tokens)
super
if markup.strip =~ SYNTAX
@new_var_name = Regexp.last_match(1).strip
@single_or_group = Regexp.last_match(2)
@arr_name = Regexp.last_match(3).strip
@conditions = process_conditions(Regexp.last_match(4).strip)
else
raise SyntaxError, <<~MSG
Syntax Error in tag 'find' while parsing the following markup:
#{markup}
Valid syntax: find <varname> where|in <array>, <condition(s)>
MSG
end
end

def render(context)
@group = lookup_variable(context, @arr_name)
return "" unless @group.respond_to?(:select)

@group = @group.values if @group.is_a?(Hash)

expression = @conditions.split(CONDITIONS_SEP).map do |condition|
"__find_tag_item__.#{condition.strip}"
end.join(" and ")
@liquid_condition = parse_condition(expression)

context[@new_var_name] = if @single_or_group == "where"
group_evaluate(context)
else
single_evaluate(context)
end

""
end

private

def process_conditions(conditions)
processed_conditions = +""
in_quotes = false

conditions.each_char do |c|
in_quotes = !in_quotes if c == '"'

processed_conditions << (c == "," && !in_quotes ? CONDITIONS_SEP : c)
end

processed_conditions
end

def group_evaluate(context)
context.stack do
@group.select do |object|
context["__find_tag_item__"] = object
@liquid_condition.evaluate(context)
end
end || []
end

def single_evaluate(context)
context.stack do
@group.find do |object|
context["__find_tag_item__"] = object
@liquid_condition.evaluate(context)
end
end || nil
end
end
end
end

Liquid::Template.register_tag("find", Bridgetown::Tags::Find)
51 changes: 51 additions & 0 deletions bridgetown-core/test/test_tags.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def create_post(content, override = {}, converter_class = Bridgetown::Converters
@converter = site.converters.find { |c| c.class == converter_class }
payload = { "highlighter_prefix" => @converter.highlighter_prefix,
"highlighter_suffix" => @converter.highlighter_suffix, }
payload["posts"] = site.posts.docs.map(&:to_liquid) if site.posts.docs

@result = Liquid::Template.parse(content).render!(payload, info)
@result = @converter.convert(@result)
Expand Down Expand Up @@ -773,4 +774,54 @@ def highlight_block_with_opts(options_string)
end
end
end

context "find tag" do
context "can find a single post" do
setup do
content = <<~EOS
---
title: This is a test
---
{% find post in posts, title == "Category in YAML" %}
POST: {{ post.content }}
EOS
create_post(content,
"permalink" => "pretty",
"source" => source_dir,
"destination" => dest_dir,
"read_all" => true)
end

should "return the post" do
expected = "POST: Best <em>post</em> ever"
assert_match(expected, @result)
end
end

context "can find multiple posts" do
setup do
content = <<~EOS
---
title: This is a test
---
{% find found where posts, title != "Categories", layout contains "efaul" %}
POST: {{ found[1].title }}
EOS
create_post(content,
"permalink" => "pretty",
"source" => source_dir,
"destination" => dest_dir,
"read_all" => true)
end

should "return the post" do
expected = "POST: Foo Bar"
assert_match(expected, @result)
end
end
end
end
8 changes: 4 additions & 4 deletions bridgetown-website/src/_layouts/docs.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ layout: default
{% unless next_order %}
{% assign next_order = page.order | plus: 1 %}
{% endunless %}
{% assign next_page = site.docs | where_exp: "item", "item.order == next_order" %}
{% if next_page.size > 0 %}
<p class="has-text-right mt-8 mb-1"><a href="{{ next_page[0].url }}" class="button is-info is-small">
<span>Next: {{ next_page[0].title }}</span>
{% find next_page in site.docs, order == next_order %}
{% if next_page %}
<p class="has-text-right mt-8 mb-1"><a href="{{ next_page.url }}" class="button is-info is-small">
<span>Next: {{ next_page.title }}</span>
<span class="icon" aria-hidden="true">
<i class="fa fa-chevron-right"></i>
</span>
Expand Down

0 comments on commit 991a0ef

Please sign in to comment.