module ThemePartials # TODO This needs to be configurable now that we've moved this file upstream. # TODO Also, this feels almost like it should be set on a per-request basis, since you can have more than one theme # in an app at a time. THEME_DIRECTORY_ORDER = [ "light", "tailwind", "base", ] INCLUDE_TARGETS = [ # ❌ This path is included for legacy purposes, but you shouldn't reference partials like this in new code. "account/shared", # ✅ This is the correct path to generically reference theme component partials with. "shared" ] # i.e. Changes "account/shared/box" to "account/shared/_box" def convert_to_literal_partial(path) path.sub(/.*\K\//, "/_") end # i.e. Changes "account/shared/_box" to "_box" def remove_hierarchy_base(path, include_target) path.sub(/^#{include_target}\//, "") end # i.e. Get "app/views/themes/light/_box.html.erb" from "_box" def get_full_debased_file_path(path, theme_directory) "app/views/themes/#{theme_directory}/#{path}.html.erb" end # Adds a hierarchy with a specific theme to a partial. # i.e. Changes "workflow/box" to "themes/light/workflow/box" def add_hierarchy_to_path(file_path, theme_directory) "themes/#{theme_directory}/#{file_path}" end class Resolver extend ThemePartials # This global variable is created once per application boot. # We're not using the Rails caching system because we want everything in local memory for this. # If we use the Rails caching system, we end up querying it over the wire from Redis or memcached. $resolved_theme_partials = {} def self.base_path_for(theme) begin "BulletTrain::Themes::#{theme.to_s.classify}::PathSnitch".constantize.method(:confess).source_location.first.split("/lib/bullet_train/themes/").first rescue NameError => _ nil end end def self.resolve(options) INCLUDE_TARGETS .filter { |include_target| options.start_with? include_target } .each do |include_target| # If the partial path has already been resolved since boot, just return that value. # This caching is not enabled in development so people can introduce new files without restarting. unless Rails.env.development? if $resolved_theme_partials[options] return $resolved_theme_partials[options] end end # Otherwise, we need to traverse the inheritance structure of the themes to find the right partial. debased_file_path = remove_hierarchy_base(options, include_target) normal_file_path = convert_to_literal_partial(options) # TODO this is a hack because the main menu is still in this directory # and other people might also add stuff there. unless File.exist?("#{Rails.root}/app/views/#{normal_file_path}.html.erb") THEME_DIRECTORY_ORDER.each do |theme_directory| # First we check whether it's defined in the actual application. This takes precedence. full_debased_file_path = convert_to_literal_partial(get_full_debased_file_path(debased_file_path, theme_directory)) actual_file_path = [ Rails.root, # This will return nil if the theme isn't distributed as a Ruby gem. base_path_for(theme_directory), ].compact.map { |path| "#{path}/#{full_debased_file_path}" }.detect { |file| File.exist?(file) } if actual_file_path # Once we've found it, ensure we don't do this again for the same partial. $resolved_theme_partials[options] = add_hierarchy_to_path(debased_file_path, theme_directory) return $resolved_theme_partials[options] end end end end nil end end end