lib/rouge/lexers/robot_framework.rb



# -*- coding: utf-8 -*- #
# frozen_string_literal: true

module Rouge
  module Lexers
    class RobotFramework < RegexLexer
      tag 'robot_framework'
      aliases 'robot', 'robot-framework'

      title "Robot Framework"
      desc 'Robot Framework is a generic open source automation testing framework (robotframework.org)'

      filenames '*.robot'
      mimetypes 'text/x-robot'

      def initialize(opts = {})
        super(opts)
        @col = 0
        @next = nil
        @is_template = false
      end

      def self.settings_with_keywords
        @settings_with_keywords ||= Set.new [
          "library", "resource", "setup", "teardown", "template", "suite setup",
          "suite teardown", "task setup", "task teardown", "task template",
          "test setup", "test teardown", "test template", "variables"
        ]
      end
      
      def self.settings_with_args
        @settings_with_args ||= Set.new [
          "arguments", "default tags", "documentation", "force tags",
          "metadata", "return", "tags", "timeout", "task timeout",
          "test timeout"
        ]
      end

      id = %r/(?:\\|[^|$@&% \t\n])+(?: (?:\\.|[^|$@&% \t\n])+)*/
      bdd = %r/(?:Given|When|Then|And|But) /i
      sep = %r/ +\| +|[ ]{2,}|\t+/

      start do
        push :prior_text
      end

      state :prior_text do
        rule %r/^[^*].*/, Text
        rule(//) { pop! }
      end

      # Mixins

      state :whitespace do
        rule %r/\s+/, Text::Whitespace
      end

      state :section_include do
        mixin :end_section
        mixin :sep
        mixin :newline
      end

      state :end_section do
        rule(/(?=^(?:\| )?\*)/) { pop! }
      end

      state :return do
        rule(//) { pop! }
      end

      state :sep do
        rule %r/\| /, Text::Whitespace

        rule sep do
          token Text::Whitespace
          @col = @col + 1
          if @next
            push @next
          elsif @is_template
            push :args
          elsif @col == 1
            @next = :keyword
            push :keyword
          else
            push :args
          end
          push :cell_start
        end

        rule %r/\.\.\. */ do
          token Text::Whitespace
          @col = @col + 1
          push :args
        end
        
        rule %r/ ?\|/, Text::Whitespace
      end

      state :newline do
        rule %r/\n/ do
          token Text::Whitespace
          @col = 0
          @next = nil
          push :cell_start
        end
      end

      # States

      state :root do
        mixin :whitespace

        rule %r/^(?:\| )?\*[* ]*([A-Z]+(?: [A-Z]+)?).*/i do |m|
          token Generic::Heading, m[0]
          case m[1].chomp("s").downcase
          when "setting" then push :section_settings
          when "test case" then push :section_tests
          when "task" then push :section_tasks
          when "keyword" then push :section_keywords
          when "variable" then push :section_variables
          end
        end
      end

      state :section_settings do
        mixin :section_include

        rule %r/([A-Z]+(?: [A-Z]+)?)(:?)/i do |m|
          match = m[1].downcase
          @next = if self.class.settings_with_keywords.include? match
                    :keyword
                  elsif self.class.settings_with_args.include? match
                    :args
                  end
          groups Name::Builtin::Pseudo, Punctuation
        end
      end

      state :section_tests do
        mixin :section_include
        
        rule %r/[$@&%{}]+/, Name::Label
        rule %r/( )(?![ |])/, Name::Label

        rule id do
          @is_template = false
          token Name::Label
        end
      end

      state :section_tasks do
        mixin :section_tests
      end

      state :section_keywords do
        mixin :section_include
  
        rule %r/[$@&%]\{/ do
          token Name::Variable
          push :var
        end
        
        rule %r/[$@&%{}]+/, Name::Label
        rule %r/( )(?![ |])/, Name::Label
        
        rule id, Name::Label
      end

      state :section_variables do
        mixin :section_include

        rule %r/[$@&%]\{/ do
          token Name::Variable
          @next = :args
          push :var
        end
      end

      state :cell_start do
        rule %r/#.*/, Comment
        mixin :return
      end

      state :keyword do
        rule %r/(\[)([A-Z]+(?: [A-Z]+)?)(\])/i do |m|
          groups Punctuation, Name::Builtin::Pseudo, Punctuation
          
          match = m[2].downcase
          @is_template = true if match == "template"
          if self.class.settings_with_keywords.include? match
            @next = :keyword
          elsif self.class.settings_with_args.include? match
            @next = :args
          end

          pop!
        end

        rule %r/[$@&%]\{/ do
          token Name::Variable
          @next = :keyword unless @next.nil?
          push :var
        end

        rule %r/FOR/i do
          token Name::Function
          @next = :keyword unless @next.nil?
        end

        rule %r/( )(?![ |])/, Name::Function

        rule bdd, Name::Builtin
        rule id do
          token Name::Function
          @next = nil
        end

        mixin :return
      end

      state :args do
        rule %r/[$@&%]\{/ do
          token Name::Variable
          @next = :keyword unless @next.nil?
          push :var
        end

        rule %r/[$@&%]+/, Str
        rule %r/( )(?![ |])/, Str
        rule id, Str
        
        mixin :return
      end

      state :var do
        rule %r/(\})( )(=)/ do
          groups Name::Variable, Text::Whitespace, Punctuation
          pop!
        end
        rule %r/[$@&%]\{/, Name::Variable, :var
        rule %r/[{\[]/, Name::Variable, :var
        rule %r/[}\]]/, Name::Variable, :pop!
        rule %r/[^$@&%{}\[\]]+/, Name::Variable
        rule %r/\}\[/, Name::Variable
      end
    end
  end
end