lib/lookbook/config.rb



require "lookbook/markdown"
require "lookbook/theme"
require "lookbook/store"

module Lookbook
  class Config
    def initialize
      @options = Store.new

      @options.set({
        project_name: "Lookbook",
        log_level: 2,
        auto_refresh: true,

        components_path: "app/components",

        page_controller: "Lookbook::PageController",
        page_route: "pages",
        page_paths: ["test/components/docs"],
        page_options: {},
        markdown_options: Markdown::DEFAULT_OPTIONS,

        preview_paths: [],
        preview_display_params: {},
        preview_srcdoc: nil,
        preview_tags: {},
        sort_examples: false,

        listen: Rails.env.development?,
        listen_paths: [],
        listen_extensions: ["rb", "html.*"],
        listen_use_polling: false,

        cable_mount_path: "/lookbook-cable",
        cable_logger: Lookbook.logger,

        runtime_parsing: !Rails.env.production?,
        parser_registry_path: "tmp/storage/.yardoc",

        ui_theme: "indigo",
        ui_theme_overrides: {},
        ui_favicon: true,

        hooks: {
          after_initialize: [],
          before_exit: [],
          after_change: []
        },

        debug_menu: Rails.env.development?,

        experimental_features: false,

        inspector_panels: {
          preview: {
            pane: :main,
            position: 1,
            partial: "lookbook/previews/panels/preview",
            hotkey: "v",
            panel_classes: "overflow-hidden",
            padded: false,
            system: true
          },
          output: {
            pane: :main,
            position: 2,
            partial: "lookbook/previews/panels/output",
            label: "HTML",
            hotkey: "h",
            padded: false,
            system: true
          },
          source: {
            pane: :drawer,
            position: 1,
            partial: "lookbook/previews/panels/source",
            label: "Source",
            hotkey: "s",
            copy: ->(data) { data.examples.map { |e| e.source }.join("\n") },
            padded: false,
            system: true
          },
          notes: {
            pane: :drawer,
            position: 2,
            partial: "lookbook/previews/panels/notes",
            label: "Notes",
            hotkey: "n",
            disabled: ->(data) { data.examples.select { |e| e.notes.present? }.none? },
            padded: false,
            system: true
          },
          params: {
            pane: :drawer,
            position: 3,
            partial: "lookbook/previews/panels/params",
            label: "Params",
            hotkey: "p",
            disabled: ->(data) { data.preview.params.none? },
            padded: false,
            system: true
          }
        }
      })
    end

    def project_name
      @options.project_name == false ? nil : @options.project_name
    end

    def components_path
      absolute_path(@options.components_path)
    end

    def page_paths=(paths = [])
      @options.page_paths += paths if paths.is_a? Array
    end

    def page_paths
      normalize_paths(@options.page_paths)
    end

    def preview_paths=(paths = [])
      @options.preview_paths += paths if paths.is_a? Array
    end

    def preview_paths
      normalize_paths(@options.preview_paths)
    end

    def preview_srcdoc=(enable)
      Lookbook.logger.warn "The `preview_srcdoc` config option is deprecated and will be removed in v2.0"
    end

    def listen_paths
      normalize_paths(@options.listen_paths)
    end

    def listen_extensions=(extensions = [])
      @options.listen_extensions += extensions if extensions.is_a? Array
      @options.listen_extensions.uniq!
    end

    def parser_registry_path
      absolute_path(@options.parser_registry_path)
    end

    def inspector_panels(&block)
      panels = Store.new(@options.inspector_panels.select { |key, panel| panel != false })
      if block
        yield panels
      else
        panels
      end
    end

    def define_inspector_panel(name, opts = {})
      pane = opts[:pane].presence || :drawer
      siblings = inspector_panels.select { |key, panel| panel.pane == pane }
      opts[:position] ||= siblings.size + 1
      @options.inspector_panels[name] = opts
      siblings.each do |key, panel|
        if panel.position >= opts[:position]
          panel.position += 1
        end
      end
    end

    def amend_inspector_panel(name, opts = {})
      if opts == false
        @options.inspector_panels[name] = false
      else
        @options.inspector_panels[name].merge!(opts)
      end
    end

    def remove_inspector_panel(name)
      amend_inspector_panel(name, false)
    end

    def inspector_panel_defaults
      {
        id: ->(data) { "inspector-panel-#{data.name}" },
        partial: "lookbook/previews/panels/content",
        content: nil,
        label: ->(data) { data.name.titleize },
        pane: :drawer,
        position: ->(data) { data.index_position },
        hotkey: nil,
        disabled: false,
        show: true,
        copy: nil,
        panel_classes: nil,
        locals: {},
        system: false
      }
    end

    def ui_theme=(name)
      name = name.to_s
      if Theme.valid_theme?(name)
        @options.ui_theme = name
      else
        Lookbook.logger.warn "'#{name}' is not a valid Lookbook theme. Theme setting not changed."
      end
    end

    def ui_theme_overrides(&block)
      if block
        yield @options.ui_theme_overrides
      else
        @options.ui_theme_overrides
      end
    end

    def [](key)
      if respond_to? key.to_sym
        public_send(key.to_sym)
      else
        @options[key.to_sym]
      end
    end

    def []=(key, value)
      setter_key = "#{key}=".to_sym
      if respond_to? setter_key
        public_send(setter_key, value)
      else
        @options[key.to_sym] = value
      end
    end

    def to_h
      @options.to_h
    end

    def to_json(*a)
      to_h.to_json(*a)
    end

    protected

    def normalize_paths(paths)
      paths.map! do |path|
        full_path = absolute_path(path)
        full_path if Dir.exist?(full_path)
      end.compact!
      paths
    end

    def absolute_path(path)
      File.absolute_path(path.to_s, Rails.root)
    end

    def method_missing(name, *args)
      @options.send(name, *args)
    end

    def respond_to_missing?(name, *)
      to_h.key? name
    end
  end
end