Merge pull request #1217 from cantino/liquid_regex_replace_block

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

Akinori MUSHA 8 years ago
parent
commit
b0bc8ca746

+ 28 - 0
app/concerns/liquid_droppable.rb

@@ -33,6 +33,34 @@ module LiquidDroppable
33 33
     self.class::Drop.new(self)
34 34
   end
35 35
 
36
+  class MatchDataDrop < Liquid::Drop
37
+    def initialize(object)
38
+      @object = object
39
+    end
40
+
41
+    %w[pre_match post_match names size].each { |attr|
42
+      define_method(attr) {
43
+        @object.__send__(attr)
44
+      }
45
+    }
46
+
47
+    def to_s
48
+      @object[0]
49
+    end
50
+
51
+    def before_method(method)
52
+      @object[method]
53
+    rescue IndexError
54
+      nil
55
+    end
56
+  end
57
+
58
+  class ::MatchData
59
+    def to_liquid
60
+      MatchDataDrop.new(self)
61
+    end
62
+  end
63
+
36 64
   require 'uri'
37 65
 
38 66
   class URIDrop < Drop

+ 91 - 0
app/concerns/liquid_interpolatable.rb

@@ -1,3 +1,5 @@
1
+# :markup: markdown
2
+
1 3
 module LiquidInterpolatable
2 4
   extend ActiveSupport::Concern
3 5
 
@@ -311,4 +313,93 @@ module LiquidInterpolatable
311 313
   end
312 314
   Liquid::Template.register_tag('credential', LiquidInterpolatable::Tags::Credential)
313 315
   Liquid::Template.register_tag('line_break', LiquidInterpolatable::Tags::LineBreak)
316
+
317
+  module Blocks
318
+    # Replace every occurrence of a given regex pattern in the first
319
+    # "in" block with the result of the "with" block in which the
320
+    # variable `match` is set for each iteration, which can be used as
321
+    # follows:
322
+    #
323
+    # - `match[0]` or just `match`: the whole matching string
324
+    # - `match[1]`..`match[n]`: strings matching the numbered capture groups
325
+    # - `match.size`: total number of the elements above (n+1)
326
+    # - `match.names`: array of names of named capture groups
327
+    # - `match[name]`..: strings matching the named capture groups
328
+    # - `match.pre_match`: string preceding the match
329
+    # - `match.post_match`: string following the match
330
+    # - `match.***`: equivalent to `match['***']` unless it conflicts with the existing methods above
331
+    #
332
+    # If named captures (`(?<name>...)`) are used in the pattern, they
333
+    # are also made accessible as variables.  Note that if numbered
334
+    # captures are used mixed with named captures, you could get
335
+    # unexpected results.
336
+    #
337
+    # Example usage:
338
+    #
339
+    #     {% regex_replace "\w+" in %}Use me like this.{% with %}{{ match | capitalize }}{% endregex_replace %}
340
+    #     {% assign fullname = "Doe, John A." %}
341
+    #     {% regex_replace_first "\A(?<name1>.+), (?<name2>.+)\z" in %}{{ fullname }}{% with %}{{ name2 }} {{ name1 }}{% endregex_replace_first %}
342
+    #
343
+    #     Use Me Like This.
344
+    #
345
+    #     John A. Doe
346
+    #
347
+    class RegexReplace < Liquid::Block
348
+      Syntax = /\A\s*(#{Liquid::QuotedFragment})(?:\s+in)?\s*\z/
349
+
350
+      def initialize(tag_name, markup, tokens)
351
+        super
352
+
353
+        case markup
354
+        when Syntax
355
+          @regexp = $1
356
+        else
357
+          raise Liquid::SyntaxError, 'Syntax Error in regex_replace tag - Valid syntax: regex_replace pattern in'
358
+        end
359
+        @nodelist = @in_block = []
360
+        @with_block = nil
361
+      end
362
+
363
+      def nodelist
364
+        if @with_block
365
+          @in_block + @with_block
366
+        else
367
+          @in_block
368
+        end
369
+      end
370
+
371
+      def unknown_tag(tag, markup, tokens)
372
+        return super unless tag == 'with'.freeze
373
+        @nodelist = @with_block = []
374
+      end
375
+
376
+      def render(context)
377
+        begin
378
+          regexp = Regexp.new(context[@regexp].to_s)
379
+        rescue ::SyntaxError => e
380
+          raise Liquid::SyntaxError, "Syntax Error in regex_replace tag - #{e.message}"
381
+        end
382
+
383
+        subject = render_all(@in_block, context)
384
+
385
+        subject.send(first? ? :sub : :gsub, regexp) {
386
+          next '' unless @with_block
387
+          m = Regexp.last_match
388
+          context.stack do
389
+            m.names.each do |name|
390
+              context[name] = m[name]
391
+            end
392
+            context['match'.freeze] = m
393
+            render_all(@with_block, context)
394
+          end
395
+        }
396
+      end
397
+
398
+      def first?
399
+        @tag_name.end_with?('_first'.freeze)
400
+      end
401
+    end
402
+  end
403
+  Liquid::Template.register_tag('regex_replace',       LiquidInterpolatable::Blocks::RegexReplace)
404
+  Liquid::Template.register_tag('regex_replace_first', LiquidInterpolatable::Blocks::RegexReplace)
314 405
 end

+ 34 - 0
spec/concerns/liquid_interpolatable_spec.rb

@@ -220,4 +220,38 @@ describe LiquidInterpolatable::Filters do
220 220
       expect(agent.interpolated['test']).to eq("foo\\1\nfoobar\\\nfoobaz\\")
221 221
     end
222 222
   end
223
+
224
+  describe 'regex_replace_first block' do
225
+    let(:agent) { Agents::InterpolatableAgent.new(name: "test") }
226
+
227
+    it 'should replace the first occurrence of a string using regex' do
228
+      agent.interpolation_context['something'] = 'foobar zoobar'
229
+      agent.options['cleaned'] = '{% regex_replace_first "(?<word>\S+)(?<suffix>bar)" in %}{{ something }}{% with %}{{ word | upcase }}{{ suffix }}{% endregex_replace_first %}'
230
+      expect(agent.interpolated['cleaned']).to eq('FOObar zoobar')
231
+    end
232
+
233
+    it 'should be able to take a pattern in a variable' do
234
+      agent.interpolation_context['something'] = 'foobar zoobar'
235
+      agent.interpolation_context['pattern'] = "(?<word>\\S+)(?<suffix>bar)"
236
+      agent.options['cleaned'] = '{% regex_replace_first pattern in %}{{ something }}{% with %}{{ word | upcase }}{{ suffix }}{% endregex_replace_first %}'
237
+      expect(agent.interpolated['cleaned']).to eq('FOObar zoobar')
238
+    end
239
+
240
+    it 'should define a variable named "match" in a "with" block' do
241
+      agent.interpolation_context['something'] = 'foobar zoobar'
242
+      agent.interpolation_context['pattern'] = "(?<word>\\S+)(?<suffix>bar)"
243
+      agent.options['cleaned'] = '{% regex_replace_first pattern in %}{{ something }}{% with %}{{ match.word | upcase }}{{ match["suffix"] }}{% endregex_replace_first %}'
244
+      expect(agent.interpolated['cleaned']).to eq('FOObar zoobar')
245
+    end
246
+  end
247
+
248
+  describe 'regex_replace block' do
249
+    let(:agent) { Agents::InterpolatableAgent.new(name: "test") }
250
+
251
+    it 'should replace the all occurrences of a string using regex' do
252
+      agent.interpolation_context['something'] = 'foobar zoobar'
253
+      agent.options['cleaned'] = '{% regex_replace "(?<word>\S+)(?<suffix>bar)" in %}{{ something }}{% with %}{{ word | upcase }}{{ suffix }}{% endregex_replace %}'
254
+      expect(agent.interpolated['cleaned']).to eq('FOObar ZOObar')
255
+    end
256
+  end
223 257
 end