lib/opal/nodes/literal.rb



# frozen_string_literal: true
require 'opal/nodes/base'

module Opal
  module Nodes
    class ValueNode < Base
      handle :true, :false, :self, :nil

      def compile
        push type.to_s
      end

      def self.truthy_optimize?
        true
      end
    end

    class NumericNode < Base
      handle :int, :float

      children :value

      def compile
        push value.to_s
        wrap '(', ')' if recv?
      end

      def self.truthy_optimize?
        true
      end
    end

    class StringNode < Base
      handle :str

      children :value

      ESCAPE_CHARS = {
        ?a => '\\u0007',
        ?e => '\\u001b'
      }

      ESCAPE_REGEX = /(\\+)([#{ ESCAPE_CHARS.keys.join('') }])/

      def translate_escape_chars(inspect_string)
        inspect_string.gsub(ESCAPE_REGEX) do |original|
          if $1.length.even?
            original
          else
            $1.chop + ESCAPE_CHARS[$2]
          end
        end
      end

      def compile
        string_value = value
        encoding = string_value.encoding
        should_encode = encoding != Encoding::UTF_8

        if should_encode
          string_value = string_value.force_encoding('UTF-8')
        end

        sanitized_value = string_value.inspect.gsub(/\\u\{([0-9a-f]+)\}/) do |match|
          code_point = $1.to_i(16)
          to_utf16(code_point)
        end
        push translate_escape_chars(sanitized_value)

        if should_encode && RUBY_ENGINE != 'opal'
          push '.$force_encoding("', encoding.name, '")'
        end
      end

      # http://www.2ality.com/2013/09/javascript-unicode.html
      def to_utf16(code_point)
        ten_bits = 0b1111111111
        u = -> (code_unit) { '\\u'+code_unit.to_s(16).upcase }

        return u.(code_point) if code_point <= 0xFFFF

        code_point -= 0x10000

        # Shift right to get to most significant 10 bits
        lead_surrogate = 0xD800 + (code_point >> 10)

        # Mask to get least significant 10 bits
        tail_surrogate = 0xDC00 + (code_point & ten_bits)

        u.(lead_surrogate) + u.(tail_surrogate)
      end
    end

    class SymbolNode < Base
      handle :sym

      children :value

      def compile
        push value.to_s.inspect
      end
    end

    class RegexpNode < Base
      handle :regexp

      attr_accessor :value, :flags

      # https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp
      SUPPORTED_FLAGS = /[gimuy]/

      def initialize(*)
        super
        extract_flags_and_value
      end

      def compile
        flags.select! do |flag|
          if SUPPORTED_FLAGS =~ flag
            true
          else
            compiler.warning "Skipping the '#{flag}' Regexp flag as it's not widely supported by JavaScript vendors."
            false
          end
        end

        case value.type
        when :dstr, :begin
          compile_dynamic_regexp
        when :str
          compile_static_regexp
        end
      end

      def compile_dynamic_regexp
        if flags.any?
          push "new RegExp(", expr(value), ", '#{flags.join}')"
        else
          push "new RegExp(", expr(value), ')'
        end
      end

      def compile_static_regexp
        value = self.value.children[0]
        case value
        when ''
          push('/(?:)/')
        when %r{\?<\w+\>}
          message = "named captures are not supported in javascript: #{value.inspect}"
          push "self.$raise(new SyntaxError('#{message}'))"
        else
          push "#{Regexp.new(value).inspect}#{flags.join}"
        end
      end

      def extract_flags_and_value
        *values, flags_sexp = *children
        self.flags = flags_sexp.children.map(&:to_s)

        case values.length
        when 0
          # empty regexp, we can process it inline
          self.value = s(:str, '')
        when 1
          # simple plain regexp, we can put it inline
          self.value = values[0]
        else
          self.value = s(:dstr, *values)
        end

        # trimming when //x provided
        # required by parser gem, but JS doesn't support 'x' flag
        if flags.include?('x')
          parts = value.children.map do |part|
            if part.is_a?(::Opal::AST::Node) && part.type == :str
              trimmed_value = part.children[0].gsub(/\A\s*\#.*/, '').gsub(/\s/, '')
              s(:str, trimmed_value)
            else
              part
            end
          end

          self.value = value.updated(nil, parts)
          flags.delete('x')
        end

        if value.type == :str
          # Replacing \A -> ^, \z -> $, required for the parser gem
          self.value = s(:str, value.children[0].gsub("\\A", "^").gsub("\\z", "$"))
        end
      end

      def raw_value
        self.value = @sexp.loc.expression.source
      end
    end

    # $_ = 'foo'; call if /foo/
    # s(:if, s(:match_current_line, /foo/, true))
    class MatchCurrentLineNode < Base
      handle :match_current_line

      children :regexp

      # Here we just convert it to
      # ($_ =~ regexp)
      # and let :send node to handle it
      def compile
        gvar_sexp = s(:gvar, :$_)
        send_node = s(:send, gvar_sexp, :=~, regexp)
        push expr(send_node)
      end
    end

    class XStringNode < Base
      handle :xstr

      def compile
        children.each do |child|
          case child.type
          when :str
            value = child.loc.expression.source
            push Fragment.new(value, nil)
          when :begin
            push expr(child)
          when :gvar, :ivar
            push expr(child)
          when :js_return
            # A case for manually created :js_return statement in Compiler#returns
            # Since we need to take original source of :str
            # we have to use raw source
            # so we need to combine "return" with "raw_source"
            push "return "
            str = child.children.first
            value = str.loc.expression.source
            push Fragment.new(value, nil)
          else
            raise "Unsupported xstr part: #{child.type}"
          end
        end

        wrap '(', ')' if recv?
      end
    end

    class DynamicStringNode < Base
      handle :dstr

      def compile
        push '""'

        children.each_with_index do |part, idx|
          push " + "

          if part.type == :str
            push part.children[0].inspect
          else
            push "(", expr(part), ")"
          end

          wrap '(', ')' if recv?
        end
      end
    end

    class DynamicSymbolNode < DynamicStringNode
      handle :dsym
    end

    class RangeNode < Base
      children :start, :finish

      SIMPLE_CHILDREN_TYPES = [:int, :float, :str, :sym]

      def compile
        if compile_inline?
          helper :range
          compile_inline
        else
          compile_range_initialize
        end
      end

      def compile_inline?
        start.type == finish.type &&
          SIMPLE_CHILDREN_TYPES.include?(start.type) &&
          SIMPLE_CHILDREN_TYPES.include?(finish.type)
      end

      def compile_inline
        raise NotImplementedError
      end

      def compile_range_initialize
        raise NotImplementedError
      end
    end

    class InclusiveRangeNode < RangeNode
      handle :irange

      def compile_inline
        push '$range(', expr(start), ', ', expr(finish), ', false)'
      end

      def compile_range_initialize
        push 'Opal.Range.$new(', expr(start), ', ', expr(finish), ', false)'
      end
    end

    class ExclusiveRangeNode < RangeNode
      handle :erange

      def compile_inline
        push '$range(', expr(start), ', ', expr(finish), ', true)'
      end

      def compile_range_initialize
        push 'Opal.Range.$new(', expr(start), ',' , expr(finish), ', true)'
      end
    end

    # 0b1111r -> s(:rational, (15/1))
    # -0b1111r -> s(:rational, (-15/1))
    class RationalNode < Base
      handle :rational

      children :value

      def compile
        push "Opal.Rational.$new(#{value.numerator}, #{value.denominator})"
      end
    end

    # 0b1110i -> s(:complex, (0+14i))
    # -0b1110i -> s(:complex, (0-14i))
    class ComplexNode < Base
      handle :complex

      children :value

      def compile
        push "Opal.Complex.$new(#{value.real}, #{value.imag})"
      end
    end
  end
end