# To Do: # 1. Move the test tags into the testing domain # # Would be nice if: # I could create an extension to the eval_symbol routine so that extensions # (like Vars) could handle the evaluation of their own creations. module IfTags include Radiant::Taggable class TagError < StandardError; end desc %{ Renders the contents of the tag if the @condition@ attribute evaluates as TRUE. The @condition@ attribute (or simply @cond@) may include up to three parts: * *Primary Symbol* - A value declaration or reference to a stored value (required) * *Comparison Test* - Comparison operator (required pretty often) * If there is only a Primary Symbol, this value may be: 'exists?', 'blank?', 'empty?' (or may be omitted -- implies 'exists?'). * If there are both Primary and Comparison Symbols, may be '=', '!=', 'gt', 'lt', 'gte', 'lte', or 'matches'. * *Comparison Symbol* - Same rules as Primary Symbol (may be ommitted in certain cases) Allowable Symbol Formats: * *String* - 'my unique string' or stringWithNoSpaces (note: if your string-with-spaces also includes a single quote, use a second single quote as an escape character. So, 'my unique string''s value' produces: "my unique string's value"). * *Number* - 1234 or -123.4 * *Boolean* - false or False or true or TRUE (boolean) * *Compound Symbol* - page[title] or page.part[My Page Part] refrencing stored values *Usage:*
*Examples:*
...
       TRUE if this page has a page part named 'My Page Part'

    ...
       Shorter notation - same as the previous example

    ...
       TRUE if a page part named 'My Page Part' exists and is blank

    ...
       TRUE if the page title is not 'My Page Title'

    ...
       TRUE if the page's URL matches the Regexp: '*/about/.*'

    ...
       TRUE if the number of children is less than or equal to 10

    ...
       TRUE if Page Title is not blank
} tag 'if' do |tag| if (condition_string = tag.attr['condition']) || (condition_string = tag.attr['cond']) then tag.expand if parse_and_eval_condition(condition_string, tag) else raise TagError.new("'if' tag must contain 'condition' attribute") end end desc %{ Opposite of the @if@ tag. *Usage:*
} tag 'unless' do |tag| if (condition_string = tag.attr['condition']) || (condition_string = tag.attr['cond']) then tag.expand unless parse_and_eval_condition(condition_string, tag) else raise TagError.new("'unless' tag must contain 'condition' attribute") end end desc %{ Used for rendering values. The @value_for@ attribute follows the same rules as symbols in the @condition@ attribute of the @if@ and @unless@ tags. This means that this tag can be used for accessing page, page part, and even custom symbols added by other extensions. *Usage:*
} tag 'puts' do |tag| if tag.attr['value_for'] then compound_symbol = parse_for_compound_symbol(tag.attr['value_for'].strip) if compound_symbol.is_a?(Array) then eval_compound_symbol(compound_symbol, tag) else raise TagError.new("invalid compound symbol in `value_for' attribute") end else raise TagError.new("`puts' tag must contain `value_for' attribute") end end #### NEEDS HELP... ### # I created these two tags to test some of the subroutines used by if/unless # directly. These tags are used by various tests. # # How do I create them as part of the test - so they don't also appear in # production? Ideally, I'd just add them to the test.rb files somehow but # there's a whole includes taggable issue there. ################## tag 'test_parse_condition' do |tag| if (condition_string = tag.attr['condition']) || (condition_string = tag.attr['cond']) then parse_condition_string(condition_string, tag).inspect else raise TagError.new("parse_condition' tag must contain `condtition/cond' attribute") end end tag 'test_eval_symbol' do |tag| if tag.attr['symbol'] then evaluated_symbol = eval_symbol(tag.attr['symbol'], tag) evaluated_symbol.inspect + ' (' + evaluated_symbol.class.to_s + ')' else raise TagError.new("eval_symbol' tag must contain `symbol' attribute") end end private # # Sends the condition_string off to be parsed and then evaluates the condition # (controls evaluation depending on whether its a one-sided condition or full) # def parse_and_eval_condition(condition_string, tag) parsed_condition = parse_condition_string(condition_string, tag) raise TagError.new("Invalid `condition' attribute in '#{tag.name}' tag - no elements found") if parsed_condition.nil? primary_symbol_value = eval_symbol(parsed_condition[0], tag) # if no comparison test defined, assume existence test comparison_test = parsed_condition[1] || 'exists' if parsed_condition[2].nil? then # we're dealing with 2 element conditions (i.e. cond="a exists") case comparison_test when 'exists', 'exists?' return !(primary_symbol_value.nil?) when 'blank', 'is_blank', 'blank?' return primary_symbol_value.blank? when 'empty', 'is_empty', 'empty?' return primary_symbol_value.empty? else raise TagError.new("`#{tag.name}' tag `condition' attribute was not understood - `#{comparison_test}' is not valid.") end else # so, we're dealing with a full 3 element condition (i.e. "if a = b") comparison_symbol_value = eval_symbol(parsed_condition[2], tag) # A symbol value must first exist before a comparison can be made and # there is no provision in this tag system to declare a nil value. # So, if content(bogus page part) returns nil, none of these comparisons # can possibly be true. We'll just return false to prevent things like: # if content(bogus page part) = content(another bogus page part) # from returning true (i.e. nil = nil). return false if comparison_symbol_value.nil? case comparison_test when 'equals', '=', '==' return (primary_symbol_value == comparison_symbol_value) when 'not', '!=' return primary_symbol_value != comparison_symbol_value when 'lt' return primary_symbol_value < comparison_symbol_value when 'lte' return primary_symbol_value <= comparison_symbol_value when 'gt' return primary_symbol_value > comparison_symbol_value when 'gte' return primary_symbol_value >= comparison_symbol_value when 'match', 'matches' return comparison_symbol_value.match(primary_symbol_value) else raise TagError.new("`#{tag.name}' tag `condition' attribute was not understood - #{primary_symbol_value} #{comparison_test} #{comparison_symbol_value} is not valid.") end end end # # Splits the condition into the Primary-Symbol, Comparison-Test, and Comparison-Symbol # strings and combines them into an array. # # Example input: "compound(symbol) equals 'my string'" # produces: ["compound(symbol)", "equals", "'my string'"] # def parse_condition_string(condition_string, tag) symbol_regexp = '(' + '(?:[\w\.]+\[(?:[^\]]|\]\])+\])' + # x.xx.x[yyy] format (escaped ']' is ok) "|(?:'(?:[^']|'')*')" + # or 'xxx' format (escaped ' is ok) '|(?:[^\s\[\]]+)' + # or xxx format (non-space, non-[] characters) ')' test_regexp = '([^\s]+)' parsed_condition = condition_string.scan(/^\s*#{symbol_regexp}(?:\s+#{test_regexp}(?:\s+#{symbol_regexp})?)?\s*$/)[0] parsed_condition.compact! unless parsed_condition.nil? return parsed_condition end # # Evaluates a symbol (first retriving the value if a compound symbol) # and casts it into the appropriate type (string, float, etc). # def eval_symbol(symbol, tag) # test it to see if it's a compound symbol (split into array, if so) symbol = parse_for_compound_symbol(symbol) if symbol.is_a?(Array) then # if the symbol is a compound symbol (array), then hand it off return eval_compound_symbol(symbol, tag) elsif (symbol[0] == 39) && (symbol[-1] == 39) then # starts & ends with ' - strip off the quotes and return the remainder return symbol[1..(symbol.length-2)].gsub("''", "'") elsif (symbol[0] == 47) && (symbol[-1] == 47) then # starts & ends with \ - strip off the slashes and cast it as a regexp begin regexp = Regexp.new(symbol[1..(symbol.length-2)]) rescue RegexpError => e raise TagError.new("Malformed regular expression in `condition' argument of `#{tag.name}' tag: #{e.message}") end return regexp elsif (symbol == 'false') || (symbol == 'False') || (symbol == 'FALSE') then return false elsif (symbol == 'true') || (symbol == 'True') || (symbol == 'TRUE') then return true elsif symbol.to_f.to_s == symbol then return symbol.to_f elsif symbol.to_i.to_s == symbol then return symbol.to_i else # default is a string (already cast) so just return it as is return symbol end end # # If the symbol_string matches the format of a compound symbol -- xxx(yyy) -- # then convert it into an array -- [xxx, yyy]. Otherwise, return the original # symbol_string # def parse_for_compound_symbol(symbol_string) split_symbol_regex = '([\w|\.]+)\[((?:[^\]]|\]\])+)\]' if (symbol_string) && (symbol_array = symbol_string.scan(/^#{split_symbol_regex}$/)[0]) then symbol_array.compact! symbol_array[1].gsub!(']]', ']') end return symbol_array || symbol_string end # # Retieves the value referenced by a compound symbol. Ideally, extension # writers should be able to hook into this method to add their own compound # elements, the logic required to return the appropriate value, and cast # the value's type). # # So, only Radiant core elements like content, page, children, etc. should # be in here. Var, then, would go in its own separate extension along with # the tag definitions. And, as mentioned above, the Var extension would be # tasked with its own tests to confirm how this runs. # def eval_compound_symbol(symbol, tag) # with good parsing elsewhere, this error should never fire - but just in case. raise TagError.new("`#{tag.name}' tag could not be evaluated - malformed compound symbol") if symbol.length != 2 page = tag.locals.page case symbol[0] when 'page' case symbol[1] when 'title', 'url', 'slug', 'breadcrumb' if result = page.send(symbol[1]) then return result end end when 'page.part' symbol[1] ||= 'body' ### Is there a more ruby-esque way of writing this conditional... if page.part(symbol[1]) then return page.part(symbol[1]).content else return nil end # just an attempt to get a value where additional params were required # in this case, I accept the format: # page.date[published_at(format values)] # # sure, it works but it doesn't read right and doesn't really allow for # more than one parameter. Perhaps a better notation would be: # page[date(for: for_param; format: string of format params)] # or, more ruby-style: # page[date{'for' => 'for_param', 'format' => 'string of format params'}] # anyway, seems like it needs a different direction when 'page.date' sub_symbols = symbol[1].scan(/^([^\(]+)(?:\(([^\)]*)\))?$/)[0] format = (sub_symbols[1] || '%A, %B %d, %Y') case sub_symbols[0] when 'now' return Time.now.strftime(format) when 'published_at', 'created_at', 'updated_at' return page[sub_symbols[0]].strftime(format) end when 'children' if symbol[1] = 'count' then return page.children.count end when 'vars' if symbol[1] == '*show all*' then # show all the vars return current_vars(tag).inspect else if current_vars(tag).key?(symbol[1]) then return current_vars(tag)[symbol[1]] else return nil end end end # if it hasn't returned a value by now, it must not be valid... raise TagError.new("`#{tag.name}' tag could not be evaluated - `#{symbol[0]}[#{symbol[1]}]' is not valid") end end