lib/hanami/view.rb in hanami-view-2.0.0.alpha8 vs lib/hanami/view.rb in hanami-view-2.1.0.beta1

- old
+ new

@@ -1,22 +1,14 @@ # frozen_string_literal: true require "dry/configurable" -require "dry/core/cache" require "dry/core/equalizer" require "dry/inflector" +require "zeitwerk" -require_relative "view/application_view" -require_relative "view/context" -require_relative "view/exposures" require_relative "view/errors" -require_relative "view/part_builder" -require_relative "view/path" -require_relative "view/render_environment" -require_relative "view/rendered" -require_relative "view/renderer" -require_relative "view/scope_builder" +require_relative "view/html" module Hanami # A standalone, template-based view rendering system that offers everything # you need to write well-factored view code. # @@ -29,17 +21,38 @@ # # @see https://dry-rb.org/gems/dry-view/ # # @api public class View + # @since 2.1.0 # @api private + def self.gem_loader + @gem_loader ||= Zeitwerk::Loader.new.tap do |loader| + root = File.expand_path("..", __dir__) + loader.tag = "hanami-view" + loader.push_dir(root) + loader.ignore( + "#{root}/hanami-view.rb", + "#{root}/hanami/view/version.rb", + "#{root}/hanami/view/errors.rb", + ) + loader.inflector = Zeitwerk::GemInflector.new("#{root}/hanami-view.rb") + loader.inflector.inflect( + "erb" => "ERB", + "html" => "HTML", + "html_safe_string_buffer" => "HTMLSafeStringBuffer", + ) + end + end + + gem_loader.setup + + # @api private DEFAULT_RENDERER_OPTIONS = {default_encoding: "utf-8"}.freeze include Dry::Equalizer(:config, :exposures) - extend Dry::Core::Cache - extend Dry::Configurable # @!group Configuration # @overload config.paths=(paths) @@ -140,10 +153,12 @@ # @param format [Symbol] # @api public # @!scope class setting :default_format, default: :html + setting :part_class, default: Part + # @overload config.scope_namespace=(namespace) # Set a namespace that will be searched when building scope classes. # # @param namespace [Module, Class] # @@ -161,10 +176,12 @@ # @param part_builder [Class] # @api public # @!scope class setting :part_builder, default: PartBuilder + setting :scope_class, default: Scope + # @overload config.scope_namespace=(namespace) # Set a namespace that will be searched when building scope classes. # # @param namespace [Module, Class] # @@ -223,11 +240,11 @@ # @see https://github.com/rtomayko/tilt # # @param mapping [Hash<Symbol, Class>] engine mapping # @api public # @!scope class - setting :renderer_engine_mapping + setting :renderer_engine_mapping, default: {} # @!endgroup # @api private def self.inherited(klass) @@ -449,91 +466,40 @@ # # my_view.html.erb # # <%= greeting %> # # <%= copyright(Time.now.utc) %> # # MyView.new.(message: "Hello") # => "HELLO!" - def self.scope(base: config.scope || Hanami::View::Scope, &block) - config.scope = Class.new(base, &block) + def self.scope(scope_class = nil, &block) + scope_class ||= config.scope || config.scope_class + + config.scope = Class.new(scope_class, &block) end # @!endgroup - # @!group Render environment - - # Returns a render environment for the view and the given options. This - # environment isn't chdir'ed into any particular directory. - # - # @param format [Symbol] template format to use (defaults to the `default_format` setting) - # @param context [Context] context object to use (defaults to the `default_context` setting) - # - # @see View.template_env render environment for the view's template - # @see View.layout_env render environment for the view's layout - # - # @return [RenderEnvironment] - # @api public - def self.render_env(format: config.default_format, context: config.default_context) - RenderEnvironment.prepare(renderer(format), config, context) - end - - # @overload template_env(format: config.default_format, context: config.default_context) - # Returns a render environment for the view and the given options, - # chdir'ed into the view's template directory. This is the environment - # used when rendering the template, and is useful to to fetch - # independently when unit testing Parts and Scopes. - # - # @param format [Symbol] template format to use (defaults to the `default_format` setting) - # @param context [Context] context object to use (defaults to the `default_context` setting) - # - # @return [RenderEnvironment] - # @api public - def self.template_env(**args) - render_env(**args).chdir(config.template) - end - - # @overload layout_env(format: config.default_format, context: config.default_context) - # Returns a render environment for the view and the given options, - # chdir'ed into the view's layout directory. This is the environment used - # when rendering the view's layout. - # - # @param format [Symbol] template format to use (defaults to the `default_format` setting) - # @param context [Context] context object to use (defaults to the `default_context` setting) - # - # @return [RenderEnvironment] @api public - def self.layout_env(**args) - render_env(**args).chdir(layout_path) - end - - # Returns renderer for the view and provided format - # # @api private - def self.renderer(format) - fetch_or_store(:renderer, config, format) { - Renderer.new( - config.paths, - format: format, - engine_mapping: config.renderer_engine_mapping, - **config.renderer_options - ) - } + def self.layout_path + File.join(*[config.layouts_dir, config.layout].compact) end # @api private - def self.layout_path - File.join(*[config.layouts_dir, config.layout].compact) + def self.cache + Cache end - # @!endgroup - # Returns an instance of the view. This binds the defined exposures to the # view instance. # # Subclasses can define their own `#initialize` to accept injected # dependencies, but must call `super()` to ensure the standard view # initialization can proceed. # # @api public def initialize + self.class.config.finalize! + ensure_config + @exposures = self.class.exposures.bind(self) end # The view's configuration # @@ -557,59 +523,55 @@ # @param input input data for preparing exposure values # # @return [Rendered] rendered view object # @api public def call(format: config.default_format, context: config.default_context, **input) - ensure_config + rendering = self.rendering(format: format, context: context) - env = self.class.render_env(format: format, context: context) - template_env = self.class.template_env(format: format, context: context) + locals = locals(rendering, input) + output = rendering.template(config.template, rendering.scope(config.scope, locals)) - locals = locals(template_env, input) - output = env.template(config.template, template_env.scope(config.scope, locals)) - if layout? - layout_env = self.class.layout_env(format: format, context: context) begin - output = env.template( + output = rendering.template( self.class.layout_path, - layout_env.scope(config.scope, layout_locals(locals)) + rendering.scope(config.scope, layout_locals(locals)) ) { output } rescue TemplateNotFoundError raise LayoutNotFoundError.new(config.layout, config.paths) end end Rendered.new(output: output, locals: locals) end + def rendering(format: config.default_format, context: config.default_context) + Rendering.new(config: config, format: format, context: context) + end + private - # @api private def ensure_config raise UndefinedConfigError, :paths unless Array(config.paths).any? raise UndefinedConfigError, :template unless config.template end - # @api private - def locals(render_env, input) - exposures.(context: render_env.context, **input) do |value, exposure| + def locals(rendering, input) + exposures.(context: rendering.context, **input) do |value, exposure| if exposure.decorate? && value - render_env.part(exposure.name, value, **exposure.options) + rendering.part(exposure.name, value, as: exposure.options[:as]) else value end end end - # @api private def layout_locals(locals) locals.each_with_object({}) do |(key, value), layout_locals| layout_locals[key] = value if exposures[key].for_layout? end end - # @api private def layout? !!config.layout # rubocop:disable Style/DoubleNegation end end end