diff --git a/bridgetown-core/lib/bridgetown-core/filters.rb b/bridgetown-core/lib/bridgetown-core/filters.rb index f7dd2504f..1d84e90e0 100644 --- a/bridgetown-core/lib/bridgetown-core/filters.rb +++ b/bridgetown-core/lib/bridgetown-core/filters.rb @@ -7,6 +7,7 @@ module Filters include URLFilters include GroupingFilters include DateFilters + include ConditionHelpers # Convert a Markdown string into HTML output. # @@ -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 diff --git a/bridgetown-core/lib/bridgetown-core/filters/condition_helpers.rb b/bridgetown-core/lib/bridgetown-core/filters/condition_helpers.rb new file mode 100644 index 000000000..fa4cb2481 --- /dev/null +++ b/bridgetown-core/lib/bridgetown-core/filters/condition_helpers.rb @@ -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 diff --git a/bridgetown-core/lib/bridgetown-core/tags/find.rb b/bridgetown-core/lib/bridgetown-core/tags/find.rb new file mode 100644 index 000000000..ebac6ecf3 --- /dev/null +++ b/bridgetown-core/lib/bridgetown-core/tags/find.rb @@ -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 where|in , + 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) diff --git a/bridgetown-core/test/test_tags.rb b/bridgetown-core/test/test_tags.rb index 767f1d921..946b8e3da 100644 --- a/bridgetown-core/test/test_tags.rb +++ b/bridgetown-core/test/test_tags.rb @@ -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) @@ -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 post 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 diff --git a/bridgetown-website/src/_layouts/docs.liquid b/bridgetown-website/src/_layouts/docs.liquid index 52a430db7..c2671d09f 100644 --- a/bridgetown-website/src/_layouts/docs.liquid +++ b/bridgetown-website/src/_layouts/docs.liquid @@ -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 %} -

- Next: {{ next_page[0].title }} + {% find next_page in site.docs, order == next_order %} + {% if next_page %} +

+ Next: {{ next_page.title }}