lib/keynote/inline.rb in keynote-1.1.1 vs lib/keynote/inline.rb in keynote-2.0.0
- old
+ new
@@ -1,54 +1,48 @@
-# encoding: UTF-8
+# frozen_string_literal: false
require "action_view"
-require "thread"
module Keynote
# The `Inline` mixin lets you write inline templates as comments inside the
# body of a presenter method. You can use any template language supported by
# Rails.
#
# ## Basic usage
#
# After extending the `Keynote::Inline` module in your presenter class, you
- # can generate HTML by calling the `erb` method and immediately following it
- # with a block of comments containing your template:
+ # can generate HTML by calling the `erb` method and passing a template as a string
+ # to it:
#
# def link
- # erb
- # # <%= link_to user_url(current_user) do %>
- # # <%= image_tag("image1.jpg") %>
- # # <%= image_tag("image2.jpg") %>
- # # <% end %>
+ # erb do
+ # <<~ERB
+ # <%= link_to user_url(current_user) do %>
+ # <%= image_tag("image1.jpg") %>
+ # <%= image_tag("image2.jpg") %>
+ # <% end %>
+ # ERB
+ # end
# end
#
# Calling this method renders the ERB template, including passing the calls
# to `link_to`, `user_url`, `current_user`, and `image_tag` back to the
# presenter object (and then to the view).
#
# ## Passing variables
#
- # There are a couple of different ways to pass local variables into an inline
- # template. The easiest is to pass the `binding` object into the template
- # method, giving access to all local variables:
+ # You can pass locals as keyword arguments to the `erb` method:
#
- # def local_binding
+ # def with_locals
# x = 1
# y = 2
#
- # erb binding
- # # <%= x + y %>
+ # erb(x:, y:) do
+ # %q(<%= x + y %>)
+ # end
# end
#
- # You can also pass a hash of variable names and values instead:
- #
- # def local_binding
- # erb x: 1, y: 2
- # # <%= x + y %>
- # end
- #
# ## The `inline` method
#
# If you want to use template languages other than ERB, you have to define
# methods for them by calling the {Keynote::Inline#inline} method on a
# presenter class:
@@ -81,114 +75,97 @@
# inline :haml
#
# def header
# full_name = "#{user.first_name} #{user.last_name}"
#
- # haml binding
- # # div#header
- # # h1= full_name
- # # h3= user.most_recent_status
+ # haml(full_name:) do
+ # <<~HAML
+ # div#header
+ # h1= full_name
+ # h3= user.most_recent_status
+ # HAML
+ # end
# end
# end
def inline(*formats)
require "action_view/context"
Array(formats).each do |format|
- define_method format do |locals = {}|
- Renderer.new(self, locals, caller(1)[0], format).render
+ define_method format do |**locals, &block|
+ raise ArgumentError, "You must pass a block to the ##{format} method" unless block
+ Renderer.new(self, locals, caller_locations(1, 1).first, block.call, format).render
end
end
end
# Extending `Keynote::Inline` automatically creates an `erb` method on the
# base class.
def self.extended(base)
base.inline :erb
+ base.include InstanceMethods
end
+ module InstanceMethods
+ # A callback for Action View: https://github.com/rails/rails/blob/a32eab53a58540b9ca57da429c5f64564db43126/actionview/lib/action_view/base.rb#L261
+ def _run(method, template, locals, buffer, add_to_stack: true, has_strict_locals: false, &block)
+ old_output_buffer, old_virtual_path, old_template = @output_buffer, @virtual_path, @current_template
+ @current_template = template if add_to_stack
+ @output_buffer = buffer
+ public_send(method, locals, buffer, &block)
+ ensure
+ @output_buffer, @virtual_path, @current_template = old_output_buffer, old_virtual_path, old_template
+ end
+ end
+
# @private
class Renderer
- def initialize(presenter, locals, caller_line, format)
+ def initialize(presenter, locals, loc, source, format)
@presenter = presenter
- @locals = extract_locals(locals)
- @template = Cache.fetch(*parse_caller(caller_line), format, @locals)
+ @locals = locals
+ @template = Cache.fetch(loc.path, loc.lineno, source, format, locals)
end
def render
@template.render(@presenter, @locals)
end
-
- private
-
- def extract_locals(locals)
- return locals unless locals.is_a?(Binding)
-
- Hash[locals.eval("local_variables").map do |local|
- [local, locals.eval(local.to_s)]
- end]
- end
-
- def parse_caller(caller_line)
- file, rest = caller_line.split ":", 2
- line, _ = rest.split " ", 2
-
- [file.strip, line.to_i]
- end
end
# @private
class Cache
- COMMENTED_LINE = /^\s*#(.*)$/
-
- def self.fetch(source_file, line, format, locals)
+ def self.fetch(source_file, line, source, format, locals)
instance = (Thread.current[:_keynote_template_cache] ||= Cache.new)
- instance.fetch(source_file, line, format, locals)
+ instance.fetch(source_file, line, source, format, locals)
end
def self.reset
Thread.current[:_keynote_template_cache] = nil
end
def initialize
@cache = {}
end
- def fetch(source_file, line, format, locals)
+ def fetch(source_file, line, source, format, locals)
local_names = locals.keys.sort
- cache_key = ["#{source_file}:#{line}", *local_names].freeze
- new_mtime = File.mtime(source_file).to_f
+ cache_key = ["#{source_file}:#{line}", *local_names].freeze
+ new_mtime = File.mtime(source_file).to_f
template, mtime = @cache[cache_key]
if new_mtime != mtime
- source = read_template(source_file, line)
-
+ source = unindent(source.chomp)
template = Template.new(source, cache_key[0],
- handler_for_format(format), locals: local_names)
+ handler_for_format(format), format:, locals: local_names)
@cache[cache_key] = [template, new_mtime]
end
template
end
private
- def read_template(source_file, line_num)
- result = ""
-
- File.foreach(source_file).drop(line_num).each do |line|
- if line =~ COMMENTED_LINE
- result << $1 << "\n"
- else
- break
- end
- end
-
- unindent result.chomp
- end
-
def handler_for_format(format)
ActionView::Template.handler_for_extension(format.to_s)
end
# Borrowed from Pry, which borrowed it from Python.
@@ -201,65 +178,30 @@
else
""
end
end
- text.gsub(/^#{margin}/, ' ' * left_padding)
+ text.gsub(/^#{margin}/, " " * left_padding)
end
end
# @private
- class TemplateFor41AndLower < ActionView::Template
- # Older versions of Rails don't have this mutex, but we probably want it,
- # so let's make sure it's there.
- def initialize(*)
- super
- @compile_mutex = Mutex.new
- end
-
+ class Template < ActionView::Template
# The only difference between this #compile! and the normal one is that
# we call `view.class` instead of `view.singleton_class`, so that the
# template method gets defined as an instance method on the presenter
# and therefore sticks around between presenter instances.
def compile!(view)
return if @compiled
@compile_mutex.synchronize do
return if @compiled
- compile(view, view.class)
-
- @source = nil if defined?(@virtual_path) && @virtual_path
- @compiled = true
- end
- end
- end
-
- # @private
- class TemplateFor42AndHigher < ActionView::Template
- # The only difference between this #compile! and the normal one is that
- # we call `view.class` instead of `view.singleton_class`, so that the
- # template method gets defined as an instance method on the presenter
- # and therefore sticks around between presenter instances.
- def compile!(view)
- return if @compiled
-
- @compile_mutex.synchronize do
- return if @compiled
-
compile(view.class)
@source = nil if defined?(@virtual_path) && @virtual_path
@compiled = true
end
end
- end
-
- if Rails.version.to_f < 4.2
- # @private
- Template = TemplateFor41AndLower
- else
- # @private
- Template = TemplateFor42AndHigher
end
end
end