Quellcode durchsuchen

Add new block tags `regex_replace`/`regex_replace_first` to Liquid

This allows user to perform dynamic substitution using the power of
Liquid templating itself.
Akinori MUSHA vor 9 Jahren
Ursprung
Commit
031a72fabd

+ 28 - 0
app/concerns/liquid_droppable.rb

@@ -33,6 +33,34 @@ module LiquidDroppable
     self.class::Drop.new(self)
     self.class::Drop.new(self)
   end
   end
 
 
+  class MatchDataDrop < Liquid::Drop
+    def initialize(object)
+      @object = object
+    end
+
+    %w[pre_match post_match names size].each { |attr|
+      define_method(attr) {
+        @object.__send__(attr)
+      }
+    }
+
+    def to_s
+      @object[0]
+    end
+
+    def before_method(method)
+      @object[method]
+    rescue IndexError
+      nil
+    end
+  end
+
+  class ::MatchData
+    def to_liquid
+      MatchDataDrop.new(self)
+    end
+  end
+
   require 'uri'
   require 'uri'
 
 
   class URIDrop < Drop
   class URIDrop < Drop

+ 91 - 0
app/concerns/liquid_interpolatable.rb

@@ -1,3 +1,5 @@
+# :markup: markdown
+
 module LiquidInterpolatable
 module LiquidInterpolatable
   extend ActiveSupport::Concern
   extend ActiveSupport::Concern
 
 
@@ -311,4 +313,93 @@ module LiquidInterpolatable
   end
   end
   Liquid::Template.register_tag('credential', LiquidInterpolatable::Tags::Credential)
   Liquid::Template.register_tag('credential', LiquidInterpolatable::Tags::Credential)
   Liquid::Template.register_tag('line_break', LiquidInterpolatable::Tags::LineBreak)
   Liquid::Template.register_tag('line_break', LiquidInterpolatable::Tags::LineBreak)
+
+  module Blocks
+    # Replace every occurrence of a given regex pattern in the first
+    # "in" block with the result of the "with" block in which the
+    # variable `match` is set for each iteration, which can be used as
+    # follows:
+    #
+    # - `match[0]` or just `match`: the whole matching string
+    # - `match[1]`..`match[n]`: strings matching the numbered capture groups
+    # - `match.size`: total number of the elements above (n+1)
+    # - `match.names`: array of names of named capture groups
+    # - `match[name]`..: strings matching the named capture groups
+    # - `match.pre_match`: string preceding the match
+    # - `match.post_match`: string following the match
+    # - `match.***`: equivalent to `match['***']` unless it conflicts with the existing methods above
+    #
+    # If named captures (`(?<name>...)`) are used in the pattern, they
+    # are also made accessible as variables.  Note that if numbered
+    # captures are used mixed with named captures, you could get
+    # unexpected results.
+    #
+    # Example usage:
+    #
+    #     {% regex_replace "\w+" in %}Use me like this.{% with %}{{ match | capitalize }}{% endregex_replace %}
+    #     {% assign fullname = "Doe, John A." %}
+    #     {% regex_replace_first "\A(?<name1>.+), (?<name2>.+)\z" in %}{{ fullname }}{% with %}{{ name2 }} {{ name1 }}{% endregex_replace_first %}
+    #
+    #     Use Me Like This.
+    #
+    #     John A. Doe
+    #
+    class RegexReplace < Liquid::Block
+      Syntax = /\A\s*(#{Liquid::QuotedFragment})(?:\s+in)?\s*\z/
+
+      def initialize(tag_name, markup, tokens)
+        super
+
+        case markup
+        when Syntax
+          @regexp = $1
+        else
+          raise Liquid::SyntaxError, 'Syntax Error in regex_replace tag - Valid syntax: regex_replace pattern in'
+        end
+        @nodelist = @in_block = []
+        @with_block = nil
+      end
+
+      def nodelist
+        if @with_block
+          @in_block + @with_block
+        else
+          @in_block
+        end
+      end
+
+      def unknown_tag(tag, markup, tokens)
+        return super unless tag == 'with'.freeze
+        @nodelist = @with_block = []
+      end
+
+      def render(context)
+        begin
+          regexp = Regexp.new(context[@regexp].to_s)
+        rescue ::SyntaxError => e
+          raise Liquid::SyntaxError, "Syntax Error in regex_replace tag - #{e.message}"
+        end
+
+        subject = render_all(@in_block, context)
+
+        subject.send(first? ? :sub : :gsub, regexp) {
+          next '' unless @with_block
+          m = Regexp.last_match
+          context.stack do
+            m.names.each do |name|
+              context[name] = m[name]
+            end
+            context['match'.freeze] = m
+            render_all(@with_block, context)
+          end
+        }
+      end
+
+      def first?
+        @tag_name.end_with?('_first'.freeze)
+      end
+    end
+  end
+  Liquid::Template.register_tag('regex_replace',       LiquidInterpolatable::Blocks::RegexReplace)
+  Liquid::Template.register_tag('regex_replace_first', LiquidInterpolatable::Blocks::RegexReplace)
 end
 end

+ 34 - 0
spec/concerns/liquid_interpolatable_spec.rb

@@ -220,4 +220,38 @@ describe LiquidInterpolatable::Filters do
       expect(agent.interpolated['test']).to eq("foo\\1\nfoobar\\\nfoobaz\\")
       expect(agent.interpolated['test']).to eq("foo\\1\nfoobar\\\nfoobaz\\")
     end
     end
   end
   end
+
+  describe 'regex_replace_first block' do
+    let(:agent) { Agents::InterpolatableAgent.new(name: "test") }
+
+    it 'should replace the first occurrence of a string using regex' do
+      agent.interpolation_context['something'] = 'foobar zoobar'
+      agent.options['cleaned'] = '{% regex_replace_first "(?<word>\S+)(?<suffix>bar)" in %}{{ something }}{% with %}{{ word | upcase }}{{ suffix }}{% endregex_replace_first %}'
+      expect(agent.interpolated['cleaned']).to eq('FOObar zoobar')
+    end
+
+    it 'should be able to take a pattern in a variable' do
+      agent.interpolation_context['something'] = 'foobar zoobar'
+      agent.interpolation_context['pattern'] = "(?<word>\\S+)(?<suffix>bar)"
+      agent.options['cleaned'] = '{% regex_replace_first pattern in %}{{ something }}{% with %}{{ word | upcase }}{{ suffix }}{% endregex_replace_first %}'
+      expect(agent.interpolated['cleaned']).to eq('FOObar zoobar')
+    end
+
+    it 'should define a variable named "match" in a "with" block' do
+      agent.interpolation_context['something'] = 'foobar zoobar'
+      agent.interpolation_context['pattern'] = "(?<word>\\S+)(?<suffix>bar)"
+      agent.options['cleaned'] = '{% regex_replace_first pattern in %}{{ something }}{% with %}{{ match.word | upcase }}{{ match["suffix"] }}{% endregex_replace_first %}'
+      expect(agent.interpolated['cleaned']).to eq('FOObar zoobar')
+    end
+  end
+
+  describe 'regex_replace block' do
+    let(:agent) { Agents::InterpolatableAgent.new(name: "test") }
+
+    it 'should replace the all occurrences of a string using regex' do
+      agent.interpolation_context['something'] = 'foobar zoobar'
+      agent.options['cleaned'] = '{% regex_replace "(?<word>\S+)(?<suffix>bar)" in %}{{ something }}{% with %}{{ word | upcase }}{{ suffix }}{% endregex_replace %}'
+      expect(agent.interpolated['cleaned']).to eq('FOObar ZOObar')
+    end
+  end
 end
 end