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