hanami-view 2.0.0.alpha8 → 2.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/README.md +15 -3
  4. data/hanami-view.gemspec +5 -3
  5. data/lib/hanami/view/cache.rb +16 -0
  6. data/lib/hanami/view/context.rb +15 -55
  7. data/lib/hanami/view/context_helpers/content_helpers.rb +5 -5
  8. data/lib/hanami/view/decorated_attributes.rb +2 -2
  9. data/lib/hanami/view/erb/engine.rb +27 -0
  10. data/lib/hanami/view/erb/filters/block.rb +44 -0
  11. data/lib/hanami/view/erb/filters/trimming.rb +42 -0
  12. data/lib/hanami/view/erb/parser.rb +161 -0
  13. data/lib/hanami/view/erb/template.rb +30 -0
  14. data/lib/hanami/view/errors.rb +8 -2
  15. data/lib/hanami/view/exposure.rb +23 -17
  16. data/lib/hanami/view/exposures.rb +22 -13
  17. data/lib/hanami/view/helpers/escape_helper.rb +221 -0
  18. data/lib/hanami/view/helpers/number_formatting_helper.rb +182 -0
  19. data/lib/hanami/view/helpers/tag_helper/tag_builder.rb +230 -0
  20. data/lib/hanami/view/helpers/tag_helper.rb +210 -0
  21. data/lib/hanami/view/html.rb +104 -0
  22. data/lib/hanami/view/html_safe_string_buffer.rb +46 -0
  23. data/lib/hanami/view/part.rb +13 -15
  24. data/lib/hanami/view/part_builder.rb +68 -108
  25. data/lib/hanami/view/path.rb +4 -31
  26. data/lib/hanami/view/renderer.rb +36 -44
  27. data/lib/hanami/view/rendering.rb +42 -0
  28. data/lib/hanami/view/{render_environment_missing.rb → rendering_missing.rb} +8 -13
  29. data/lib/hanami/view/scope.rb +14 -15
  30. data/lib/hanami/view/scope_builder.rb +42 -78
  31. data/lib/hanami/view/tilt/haml_adapter.rb +40 -0
  32. data/lib/hanami/view/tilt/slim_adapter.rb +40 -0
  33. data/lib/hanami/view/tilt.rb +22 -46
  34. data/lib/hanami/view/version.rb +1 -1
  35. data/lib/hanami/view.rb +53 -91
  36. metadata +64 -26
  37. data/LICENSE +0 -20
  38. data/lib/hanami/view/render_environment.rb +0 -62
  39. data/lib/hanami/view/tilt/erb.rb +0 -26
  40. data/lib/hanami/view/tilt/erbse.rb +0 -21
  41. data/lib/hanami/view/tilt/haml.rb +0 -26
@@ -1,13 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "pathname"
4
- require "dry/core/cache"
5
4
 
6
5
  module Hanami
7
6
  class View
8
7
  # @api private
9
8
  class Path
10
- extend Dry::Core::Cache
11
9
  include Dry::Equalizer(:dir, :root)
12
10
 
13
11
  attr_reader :dir, :root
@@ -25,12 +23,10 @@ module Hanami
25
23
  @root = Pathname(root)
26
24
  end
27
25
 
28
- def lookup(name, format, child_dirs: [], parent_dir: false)
29
- fetch_or_store(dir, root, name, format, child_dirs, parent_dir) do
30
- lookup_template(name, format) ||
31
- lookup_in_child_dirs(name, format, child_dirs: child_dirs) ||
32
- parent_dir && lookup_in_parent_dir(name, format, child_dirs: child_dirs)
33
- end
26
+ # Searches for a template using a wildcard for the engine extension
27
+ def lookup(prefix, name, format)
28
+ glob = dir.join(prefix, "#{name}.#{format}.*")
29
+ Dir[glob].first
34
30
  end
35
31
 
36
32
  def chdir(dirname)
@@ -40,29 +36,6 @@ module Hanami
40
36
  def to_s
41
37
  dir.to_s
42
38
  end
43
-
44
- private
45
-
46
- def root?
47
- dir == root
48
- end
49
-
50
- # Search for a template using a wildcard for the engine extension
51
- def lookup_template(name, format)
52
- glob = dir.join("#{name}.#{format}.*")
53
- Dir[glob].first
54
- end
55
-
56
- def lookup_in_child_dirs(name, format, child_dirs:)
57
- child_dirs.reduce(nil) { |_, dir|
58
- template = chdir(dir).lookup(name, format)
59
- break template if template
60
- }
61
- end
62
-
63
- def lookup_in_parent_dir(name, format, child_dirs:)
64
- !root? && chdir("..").lookup(name, format, child_dirs: child_dirs, parent_dir: true)
65
- end
66
39
  end
67
40
  end
68
41
  end
@@ -1,9 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/core/cache"
4
- require "dry/core/equalizer"
5
3
  require_relative "errors"
6
- require_relative "tilt"
7
4
 
8
5
  module Hanami
9
6
  class View
@@ -11,67 +8,62 @@ module Hanami
11
8
  class Renderer
12
9
  PARTIAL_PREFIX = "_"
13
10
  PATH_DELIMITER = "/"
11
+ CURRENT_PATH_PREFIX = "."
14
12
 
15
- extend Dry::Core::Cache
13
+ attr_reader :config, :prefixes
16
14
 
17
- include Dry::Equalizer(:paths, :format, :engine_mapping, :options)
18
-
19
- attr_reader :paths, :format, :engine_mapping, :options
20
-
21
- def initialize(paths, format:, engine_mapping: nil, **options)
22
- @paths = paths
23
- @format = format
24
- @engine_mapping = engine_mapping || {}
25
- @options = options
15
+ def initialize(config)
16
+ @config = config
17
+ @prefixes = [CURRENT_PATH_PREFIX]
26
18
  end
27
19
 
28
- def template(name, scope, **lookup_options, &block)
29
- path = lookup(name, **lookup_options)
20
+ def template(name, format, scope, &block)
21
+ old_prefixes = @prefixes.dup
30
22
 
31
- if path
32
- render(path, scope, &block)
33
- else
34
- raise TemplateNotFoundError.new(name, paths)
35
- end
36
- end
23
+ template_path = lookup(name, format)
37
24
 
38
- def partial(name, scope, &block)
39
- template(
40
- name_for_partial(name),
41
- scope,
42
- child_dirs: %w[shared],
43
- parent_dir: true,
44
- &block
45
- )
46
- end
25
+ raise TemplateNotFoundError.new(name, format, config.paths) unless template_path
47
26
 
48
- def render(path, scope, &block)
49
- tilt(path).render(scope, {locals: scope._locals}, &block)
50
- end
27
+ new_prefix = File.dirname(name)
28
+ @prefixes << new_prefix unless @prefixes.include?(new_prefix)
51
29
 
52
- def chdir(dirname)
53
- new_paths = paths.map { |path| path.chdir(dirname) }
30
+ render(template_path, scope, &block)
31
+ ensure
32
+ @prefixes = old_prefixes
33
+ end
54
34
 
55
- self.class.new(new_paths, format: format, **options)
35
+ def partial(name, format, scope, &block)
36
+ template(name_for_partial(name), format, scope, &block)
56
37
  end
57
38
 
58
39
  private
59
40
 
60
- def lookup(name, **options)
61
- paths.inject(nil) { |_, path|
62
- result = path.lookup(name, format, **options)
63
- break result if result
41
+ def lookup(name, format)
42
+ View.cache.fetch_or_store(:lookup, name, format, config, prefixes) {
43
+ catch :found do
44
+ config.paths.reduce(nil) do |_, path|
45
+ prefixes.reduce(nil) do |_, prefix|
46
+ result = path.lookup(prefix, name, format)
47
+ throw :found, result if result
48
+ end
49
+ end
50
+ end
64
51
  }
65
52
  end
66
53
 
67
54
  def name_for_partial(name)
68
- name_segments = name.to_s.split(PATH_DELIMITER)
69
- name_segments[0..-2].push("#{PARTIAL_PREFIX}#{name_segments[-1]}").join(PATH_DELIMITER)
55
+ segments = name.to_s.split(PATH_DELIMITER)
56
+ segments[-1] = "#{PARTIAL_PREFIX}#{segments[-1]}"
57
+ segments.join(PATH_DELIMITER)
58
+ end
59
+
60
+ def render(path, scope, &block)
61
+ tilt(path).render(scope, {locals: scope._locals}, &block).html_safe
70
62
  end
71
63
 
72
64
  def tilt(path)
73
- fetch_or_store(:engine, path, engine_mapping, options) {
74
- Tilt[path, engine_mapping, **options]
65
+ View.cache.fetch_or_store(:tilt, path, config) {
66
+ Hanami::View::Tilt[path, config.renderer_engine_mapping, config.renderer_options]
75
67
  }
76
68
  end
77
69
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class View
5
+ # @api private
6
+ class Rendering
7
+ attr_reader :config, :format
8
+
9
+ attr_reader :inflector, :part_builder, :scope_builder
10
+
11
+ attr_reader :context, :renderer
12
+
13
+ def initialize(config:, format:, context:)
14
+ @config = config
15
+ @format = format
16
+
17
+ @inflector = config.inflector
18
+ @part_builder = config.part_builder
19
+ @scope_builder = config.scope_builder
20
+
21
+ @context = context.dup_for_rendering(self)
22
+ @renderer = Renderer.new(config)
23
+ end
24
+
25
+ def template(name, scope, &block)
26
+ renderer.template(name, format, scope, &block)
27
+ end
28
+
29
+ def partial(name, scope, &block)
30
+ renderer.partial(name, format, scope, &block)
31
+ end
32
+
33
+ def part(name, value, as: nil)
34
+ part_builder.(name, value, as: as, rendering: self)
35
+ end
36
+
37
+ def scope(name = nil, locals) # rubocop:disable Style/OptionalArguments
38
+ scope_builder.(name, locals: locals, rendering: self)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,39 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "dry/inflector"
4
+ require_relative "errors"
4
5
 
5
6
  module Hanami
6
7
  class View
7
8
  # @api private
8
- class RenderEnvironmentMissing
9
- class MissingEnvironmentError < StandardError
10
- def message
11
- "a +render_env+ must be provided"
12
- end
13
- end
14
-
9
+ class RenderingMissing
15
10
  def format
16
- raise MissingEnvironmentError
11
+ raise RenderingMissingError
17
12
  end
18
13
 
19
14
  def context
20
- raise MissingEnvironmentError
15
+ raise RenderingMissingError
21
16
  end
22
17
 
23
18
  def part(_name, _value, **_options)
24
- raise MissingEnvironmentError
19
+ raise RenderingMissingError
25
20
  end
26
21
 
27
22
  def scope(_name = nil, _locals) # rubocop:disable Style/OptionalArguments
28
- raise MissingEnvironmentError
23
+ raise RenderingMissingError
29
24
  end
30
25
 
31
26
  def template(_name, _scope)
32
- raise MissingEnvironmentError
27
+ raise RenderingMissingError
33
28
  end
34
29
 
35
30
  def partial(_name, _scope)
36
- raise MissingEnvironmentError
31
+ raise RenderingMissingError
37
32
  end
38
33
 
39
34
  def inflector
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "dry/core/equalizer"
4
4
  require "dry/core/constants"
5
- require_relative "render_environment_missing"
6
5
 
7
6
  module Hanami
8
7
  class View
@@ -21,7 +20,7 @@ module Hanami
21
20
  # @api private
22
21
  CONVENIENCE_METHODS = %i[format context locals].freeze
23
22
 
24
- include Dry::Equalizer(:_name, :_locals, :_render_env)
23
+ include Dry::Equalizer(:_name, :_locals, :_rendering)
25
24
 
26
25
  # The scope's name
27
26
  #
@@ -43,18 +42,18 @@ module Hanami
43
42
  # @api public
44
43
  attr_reader :_locals
45
44
 
46
- # The current render environment
45
+ # The current rendering
47
46
  #
48
- # @return [RenderEnvironment] render environment
47
+ # @return [Rendering]
49
48
  #
50
49
  # @api private
51
- attr_reader :_render_env
50
+ attr_reader :_rendering
52
51
 
53
52
  # Returns a new Scope instance
54
53
  #
55
54
  # @param name [Symbol, nil] scope name
56
55
  # @param locals [Hash<Symbol, Object>] template locals
57
- # @param render_env [RenderEnvironment] render environment
56
+ # @param rendering [Rendering] the current rendering
58
57
  #
59
58
  # @return [Scope]
60
59
  #
@@ -62,11 +61,11 @@ module Hanami
62
61
  def initialize(
63
62
  name: nil,
64
63
  locals: Dry::Core::Constants::EMPTY_HASH,
65
- render_env: RenderEnvironmentMissing.new
64
+ rendering: RenderingMissing.new
66
65
  )
67
66
  @_name = name
68
67
  @_locals = locals
69
- @_render_env = render_env
68
+ @_rendering = rendering
70
69
  end
71
70
 
72
71
  # @overload render(partial_name, **locals, &block)
@@ -96,7 +95,7 @@ module Hanami
96
95
  partial_name = _inflector.underscore(_inflector.demodulize(partial_name.to_s))
97
96
  end
98
97
 
99
- _render_env.partial(partial_name, _render_scope(**locals), &block)
98
+ _rendering.partial(partial_name, _render_scope(**locals), &block)
100
99
  end
101
100
 
102
101
  # Build a new scope using a scope class matching the provided name
@@ -108,7 +107,7 @@ module Hanami
108
107
  #
109
108
  # @api public
110
109
  def scope(name = nil, **locals)
111
- _render_env.scope(name, locals)
110
+ _rendering.scope(name, locals)
112
111
  end
113
112
 
114
113
  # The template format for the current render environment.
@@ -123,7 +122,7 @@ module Hanami
123
122
  #
124
123
  # @api public
125
124
  def _format
126
- _render_env.format
125
+ _rendering.format
127
126
  end
128
127
 
129
128
  # The context object for the current render environment
@@ -138,7 +137,7 @@ module Hanami
138
137
  #
139
138
  # @api public
140
139
  def _context
141
- _render_env.context
140
+ _rendering.context
142
141
  end
143
142
 
144
143
  private
@@ -164,7 +163,7 @@ module Hanami
164
163
 
165
164
  def respond_to_missing?(name, include_private = false)
166
165
  _locals.key?(name) ||
167
- _render_env.context.respond_to?(name) ||
166
+ _rendering.context.respond_to?(name) ||
168
167
  CONVENIENCE_METHODS.include?(name) ||
169
168
  super
170
169
  end
@@ -176,13 +175,13 @@ module Hanami
176
175
  self.class.new(
177
176
  # FIXME: what about `name`?
178
177
  locals: locals,
179
- render_env: _render_env
178
+ rendering: _rendering
180
179
  )
181
180
  end
182
181
  end
183
182
 
184
183
  def _inflector
185
- _render_env.inflector
184
+ _rendering.inflector
186
185
  end
187
186
  end
188
187
  end
@@ -1,97 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/core/cache"
4
- require "dry/core/equalizer"
5
- require_relative "scope"
6
-
7
3
  module Hanami
8
4
  class View
9
5
  # Builds scope objects via matching classes
10
6
  #
11
7
  # @api private
12
8
  class ScopeBuilder
13
- extend Dry::Core::Cache
14
- include Dry::Equalizer(:namespace)
15
-
16
- # The view's configured `scope_namespace`
17
- #
18
- # @api private
19
- attr_reader :namespace
20
-
21
- # @return [RenderEnvironment]
22
- #
23
- # @api private
24
- attr_reader :render_env
25
-
26
- # Returns a new instance of ScopeBuilder
27
- #
28
- # @api private
29
- def initialize(namespace: nil, render_env: nil)
30
- @namespace = namespace
31
- @render_env = render_env
32
- end
33
-
34
- # @api private
35
- def for_render_env(render_env)
36
- return self if render_env == self.render_env
37
-
38
- self.class.new(namespace: namespace, render_env: render_env)
39
- end
40
-
41
- # Returns a new scope using a class matching the name
42
- #
43
- # @param name [Symbol, Class] scope name
44
- # @param locals [Hash<Symbol, Object>] locals hash
45
- #
46
- # @return [Hanami::View::Scope]
47
- #
48
- # @api private
49
- def call(name = nil, locals) # rubocop:disable Style/OptionalArguments
50
- scope_class(name).new(
51
- name: name,
52
- locals: locals,
53
- render_env: render_env
54
- )
55
- end
56
-
57
- private
58
-
59
- DEFAULT_SCOPE_CLASS = Scope
9
+ class << self
10
+ # Returns a new scope using a class matching the name
11
+ #
12
+ # @param name [Symbol, Class] scope name
13
+ # @param locals [Hash<Symbol, Object>] locals hash
14
+ #
15
+ # @return [Hanami::View::Scope]
16
+ #
17
+ # @api private
18
+ def call(name = nil, locals:, rendering:) # rubocop:disable Style/OptionalArguments
19
+ klass = scope_class(name, rendering: rendering)
20
+
21
+ klass.new(name: name, locals: locals, rendering: rendering)
22
+ end
60
23
 
61
- def scope_class(name = nil)
62
- if name.nil?
63
- DEFAULT_SCOPE_CLASS
64
- elsif name.is_a?(Class)
65
- name
66
- else
67
- fetch_or_store(namespace, name) do
68
- resolve_scope_class(name: name)
24
+ private
25
+
26
+ def scope_class(name = nil, rendering:)
27
+ if name.nil?
28
+ rendering.config.scope_class
29
+ elsif name.is_a?(Class)
30
+ name
31
+ else
32
+ View.cache.fetch_or_store(:scope_class, rendering.config) do
33
+ resolve_scope_class(name: name, rendering: rendering)
34
+ end
69
35
  end
70
36
  end
71
- end
72
37
 
73
- def resolve_scope_class(name:)
74
- name = inflector.camelize(name.to_s)
38
+ def resolve_scope_class(name:, rendering:)
39
+ name = rendering.inflector.camelize(name.to_s)
75
40
 
76
- # Give autoloaders a chance to act
77
- begin
78
- klass = namespace.const_get(name)
79
- rescue NameError # rubocop:disable Lint/HandleExceptions
80
- end
41
+ namespace = rendering.config.scope_namespace
81
42
 
82
- if !klass && namespace.const_defined?(name, false)
83
- klass = namespace.const_get(name)
84
- end
43
+ # Give autoloaders a chance to act
44
+ begin
45
+ klass = namespace.const_get(name)
46
+ rescue NameError # rubocop:disable Lint/HandleExceptions
47
+ end
85
48
 
86
- if klass && klass < Scope
87
- klass
88
- else
89
- DEFAULT_SCOPE_CLASS
90
- end
91
- end
49
+ if !klass && namespace.const_defined?(name, false)
50
+ klass = namespace.const_get(name)
51
+ end
92
52
 
93
- def inflector
94
- render_env.inflector
53
+ if klass && klass < Scope
54
+ klass
55
+ else
56
+ rendering.config.scope_class
57
+ end
58
+ end
95
59
  end
96
60
  end
97
61
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "haml"
4
+
5
+ module Hanami
6
+ class View
7
+ module Tilt
8
+ # @api private
9
+ module HamlAdapter
10
+ # Add options to Haml::Engine to match the options from its default generator.
11
+ #
12
+ # The default generator for Haml::Engine is configurable via an engine option, like so:
13
+ #
14
+ # use :Generator, -> { options[:generator] }
15
+ #
16
+ # Because this Temple filter is set as a proc, the resulting effect within Temple's EngineDSL
17
+ # is that the generator's valid options are not merged into the full set of options available
18
+ # on Haml::Engine itself. This means we receive a "Option :capture_generator is invalid"
19
+ # warning when we set our `capture_generator:` below.
20
+ #
21
+ # However, this option is perfectly valid, so here we merge all the options for Haml's default
22
+ # generator into the top-level engine's options, avoiding the warning.
23
+ ::Haml::Engine.define_options(::Haml::Engine.options[:generator].options.valid_keys)
24
+
25
+ # Haml template renderer for Hanami::View.
26
+ #
27
+ # This differs from the standard Haml::Template by automatically escaping HTML based on a
28
+ # given string's `#html_safe?`, regardless of when "hanami/view/html" is required.
29
+ #
30
+ # @see Hanami::View::Tilt
31
+ # @api private
32
+ Template = Temple::Templates::Tilt(
33
+ ::Haml::Engine,
34
+ use_html_safe: true,
35
+ capture_generator: HTMLSafeStringBuffer,
36
+ )
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "slim"
4
+
5
+ module Hanami
6
+ class View
7
+ module Tilt
8
+ # @api private
9
+ module SlimAdapter
10
+ # Add options to Slim::Engine to match the options from its default generator.
11
+ #
12
+ # The default generator for Slim::Engine is configurable via an engine option, like so:
13
+ #
14
+ # use(:Generator) { options[:generator] }
15
+ #
16
+ # Because this Temple filter is set as a proc, the resulting effect within Temple's EngineDSL
17
+ # is that the generator's valid options are not merged into the full set of options available
18
+ # on Slim::Engine itself. This means we receive a "Option :capture_generator is invalid"
19
+ # warning when we set our `capture_generator:` below.
20
+ #
21
+ # However, this option is perfectly valid, so here we merge all the options for Slim's default
22
+ # generator into the top-level engine's options, avoiding the warning.
23
+ ::Slim::Engine.define_options(::Slim::Engine.options[:generator].options.valid_keys)
24
+
25
+ # Slim template renderer for Hanami::View.
26
+ #
27
+ # This differs from the standard Slim::Template by automatically escaping HTML based on a
28
+ # given string's `#html_safe?`, regardless of when "hanami/view/html" is required.
29
+ #
30
+ # @see Hanami::View::Tilt
31
+ # @api private
32
+ Template = Temple::Templates::Tilt(
33
+ ::Slim::Engine,
34
+ use_html_safe: true,
35
+ capture_generator: HTMLSafeStringBuffer,
36
+ )
37
+ end
38
+ end
39
+ end
40
+ end