lib/lookbook/tag_options.rb



module Lookbook
  class TagOptions
    SYMBOL_MATCH = /(:([a-zA-Z_\d]+))$/
    EVAL_OPTION_MATCH = /(\{\{\s?(.*)\s?\}\})$/
    YAML_HASH_MATCH = /(\{(.*?)\})$/m
    YAML_ARRAY_MATCH = /(\[(.*?)\])$/m
    FILE_PATH_MATCH = /(\S+\.(json|yml))$/

    MATCHERS = [YAML_ARRAY_MATCH, YAML_HASH_MATCH, SYMBOL_MATCH, EVAL_OPTION_MATCH, FILE_PATH_MATCH]

    def initialize(options_str, eval_scope: nil, base_dir: nil)
      @options_str = options_str.is_a?(String) ? options_str.strip : ""
      @eval_scope = eval_scope
      @base_dir = base_dir
    end

    def options
      resolve.is_a?(Hash) ? resolve.symbolize_keys : resolve
    end

    def resolve
      @resolved_options ||= begin
        if @options_str.present?
          if @options_str.end_with?(".json", ".yml")
            file_path = resolve_file_path
            if file_path
              options_file_content = File.read(file_path)
            else
              raise "Tag options data file not found"
            end
          end
          if file_path&.extname == ".json"
            JSON.parse(options_file_content)
          elsif file_path&.extname == ".yml"
            YAML.safe_load(options_file_content)
          elsif evaluatable?
            evaluate
          else
            YAML.safe_load(@options_str || "{}")
          end
        else
          {}
        end
      rescue => exception
        Lookbook.logger.warn Lookbook::Error.new(exception)
        {}
      end
    end

    def resolve_file_path
      path = if @options_str.start_with?(".") && @base_dir.present?
        File.expand_path(@options_str, @base_dir)
      else
        Rails.root.join(@options_str)
      end
      Pathname.new path
    end

    def evaluate
      if Lookbook.config.preview_params_options_eval == true
        if @eval_scope.nil?
          raise "Preview params eval must be scoped to an object"
        else
          @eval_scope.instance_eval(statement)
        end
      else
        raise "The config option `preview_params_options_eval` must be set to `true` before param options can be evaluated at runtime"
      end
    end

    def self.extract_options(str)
      str ||= ""
      str.strip!
      opts_str = ""

      MATCHERS.each do |regexp|
        match_data = str.match(regexp)
        unless match_data.nil?
          str.gsub!(regexp, "").strip!
          opts_str = match_data[1]
          break
        end
      end

      [str, opts_str]
    end

    def self.resolveable?(str)
      return unless str.is_a?(String)
      str.strip!
      MATCHERS.each do |regexp|
        return true if str.match?(regexp)
      end
      false
    end

    private

    def statement
      evaluatable? ? eval_match_data[2].strip : "{}"
    end

    def evaluatable?
      eval_match_data.present?
    end

    def eval_match_data
      @eval_match_data ||= @options_str.match(EVAL_OPTION_MATCH) || @options_str.match(SYMBOL_MATCH)
    end
  end
end