about summary refs log tree commit diff
path: root/app/lib/command_tag/processor.rb
diff options
context:
space:
mode:
authorFire Demon <firedemon@creature.cafe>2020-07-26 03:23:54 -0500
committerFire Demon <firedemon@creature.cafe>2020-08-30 05:45:16 -0500
commit1af0cdcd02de9e1dcfca78ac0c8aff209def980f (patch)
tree1ded845b028e3dda975230863f0087e25c61930f /app/lib/command_tag/processor.rb
parent173ec3d8c765239aa54c12d8a06e7dc404a1ac9d (diff)
[Command Tags] Fix parsing of of multi-line argument; add support for variables and templating
Diffstat (limited to 'app/lib/command_tag/processor.rb')
-rw-r--r--app/lib/command_tag/processor.rb69
1 files changed, 64 insertions, 5 deletions
diff --git a/app/lib/command_tag/processor.rb b/app/lib/command_tag/processor.rb
index a0d9af66b..e6bd672c1 100644
--- a/app/lib/command_tag/processor.rb
+++ b/app/lib/command_tag/processor.rb
@@ -17,6 +17,13 @@ class CommandTag::Processor
 
   STATEMENT_RE = /^\s*#!\s*([^\n]+ (?:start|begin|do)$.*)\n\s*#!\s*(?:end|stop|done)\s*$|^\s*#!\s*(.*?)\s*$/im.freeze
   STATEMENT_STRIP_RE = /^\s*#!\s*(?:[^\n]+ (?:start|begin|do)$.*)\n\s*#!\s*(?:end|stop|done)\s*$\n?|^\s*#!\s*(?:.*?)\s*$\n?/im.freeze
+  TEMPLATE_RE = /\{%\s*(.*?)\s*%\}/.freeze
+  ESCAPE_MAP = {
+    '\n' => "\n",
+    '\r' => "\r",
+    '\t' => "\t",
+    '\\\\' => '\\',
+  }.freeze
 
   def initialize(account, status)
     @account      = account
@@ -38,7 +45,7 @@ class CommandTag::Processor
     @text = @text.gsub(STATEMENT_STRIP_RE, '').split("\n")
 
     %w(at_start once_at_start).each { |suffix| execute_statements(suffix) }
-    @text = @text.join("\n").rstrip
+    @text = parse_templates(@text.join("\n")).rstrip
     %w(before_save once_before_save).each { |suffix| execute_statements(suffix) }
 
     if @text.blank? || @text.gsub(Account::MENTION_RE, '').strip.blank?
@@ -72,13 +79,48 @@ class CommandTag::Processor
         .gsub(/^\s*((?:(?:#{Account::MENTION_RE}|#{Tag::HASHTAG_RE})\s*)+)#!/, "\\1\n#!")
   end
 
+  def parse_templates(text)
+    text.gsub(TEMPLATE_RE) do
+      return if Regexp.last_match(1).blank?
+
+      template = Regexp.last_match(1).scan(/'([^']*)'|"([^"]*)"|(\S+)/).flatten.compact
+      return if template[0].blank?
+
+      name      = normalize(template[0])
+      separator = "\n"
+
+      if template.count > 2
+        if %w(by with using sep separator delim delimiter).include?(template[-2].downcase)
+          separator = unescape_literals(template[-1])
+          template = template[0..-3]
+        elsif !template[-1].match?(/\A[-+]?[0-9]+\z/)
+          separator = unescape_literals(template[-1])
+          template.pop
+        end
+      end
+
+      index_start = to_integer(template[1])
+      index_end   = to_integer(template[2])
+
+      if ['all', '[]'].include?(template[1])
+        var(name).join(separator)
+      elsif index_end.zero?
+        var(name)[index_start].presence || ''
+      else
+        var(name)[index_start..index_end].presence || ''
+      end
+    end
+  end
+
   def parse_statements
-    @text.scan(STATEMENT_RE).flatten.each do |statement|
-      next if statement.blank? || statement[0]&.strip.blank?
+    @text.scan(STATEMENT_RE).flatten.compact.each do |statement|
+      next if statement.blank?
 
-      statement = statement.scan(/^(.*) (?:start|begin|do)$(.*)|'([^']*)'|"([^"]*)"|(\S+)/im).flatten.compact
+      statement = statement.scan(/'([^']*)'|"([^"]*)"|(\S+)|\s+(?:start|begin|do)\s*$[\r\n]+(.*)/im).flatten.compact
       statement[0] = statement[0].strip.tr(':.\- ', '_').gsub(/__+/, '_').downcase
-      add_statement_handlers_for(statement)
+      statement[-1].rstrip! if statement.count > 1
+
+      add_statement_handlers_for(statement) if statement[0].present?
     end
   end
 
@@ -132,6 +174,23 @@ class CommandTag::Processor
     @status.destroy unless @status.destroyed?
   end
 
+  def normalize(text)
+    text.to_s.strip.downcase
+  end
+
+  def to_integer(text)
+    text&.strip.to_i
+  end
+
+  def unescape_literals(text)
+    ESCAPE_MAP.each { |escaped, unescaped| text.gsub!(escaped, unescaped) }
+    text
+  end
+
+  def var(name)
+    @vars[name].presence || []
+  end
+
   def read_visibility_from(arg)
     return if arg.strip.blank?