Created
June 14, 2019 23:18
-
-
Save jeremyevans/6ed5a19b559395de63aa2cb34db091e6 to your computer and use it in GitHub Desktop.
Compiled Templates Support for Roda
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
From 4b00364417def26dd5aa941dcbd0156bbfc4cdee Mon Sep 17 00:00:00 2001 | |
From: Jeremy Evans <[email protected]> | |
Date: Fri, 14 Jun 2019 15:17:51 -0700 | |
Subject: [PATCH] Improve render performance up to 4x in the default case by | |
calling compiled template methods directly | |
This takes the UnboundMethods that Tilt prepares for templates, | |
and defines real methods using them in a module included in the | |
Roda class (so it works with frozen Roda apps). | |
When render/view is called with a single Symbol or String argument, | |
lookup that argument in the template method cache. If it exists, | |
call the method using send. If it doesn't exist, but a single | |
Symbol or String argument is given, flag to update the template | |
method cache with the compiled template method. | |
This optimizes the common case of calling render/view with a | |
single argument. It can be 2.5x faster to render and 4x | |
faster to view for simple templates, in terms of time spent | |
in Roda. | |
This depends on Ruby 2.1+ and Tilt 1.2+ in order to work correctly, | |
so only use the compilation in those cases. | |
Most plugins that use render don't benefit directly from this | |
optimization. The render_locals plugin turns the optimization | |
off completely, because it can inject locals even when none are | |
passed directly when calling render/view, and the optimization | |
does not work in that case. | |
If using the view_options plugin, the set_view_subdir and | |
append_view_subdir can work with the optimization, but | |
set_view_options and set_layout_options disables the optimization. | |
Some less commonly used tilt template engines do not support | |
compilation. For those template engines, if compiling the | |
template raises a NotImplementedError, rescue it and mark | |
to not try compiling the template in the future. | |
To avoid problems when subclassing, make sure to define methods | |
that include the class object id in the template method cache key. | |
--- | |
CHANGELOG | 4 + | |
lib/roda/plugins/render.rb | 137 +++++++++++++++++++++++--- | |
lib/roda/plugins/render_locals.rb | 10 ++ | |
lib/roda/plugins/view_options.rb | 28 ++++++ | |
spec/plugin/render_spec.rb | 157 ++++++++++++++++++++++++++++++ | |
spec/plugin/view_options_spec.rb | 18 ++++ | |
spec/views/a.rdoc | 2 + | |
spec/views/about/comp_test.erb | 1 + | |
spec/views/comp_layout.erb | 1 + | |
spec/views/comp_test.erb | 1 + | |
10 files changed, 344 insertions(+), 15 deletions(-) | |
create mode 100644 spec/views/a.rdoc | |
create mode 100644 spec/views/about/comp_test.erb | |
create mode 100644 spec/views/comp_layout.erb | |
create mode 100644 spec/views/comp_test.erb | |
diff --git a/CHANGELOG b/CHANGELOG | |
index 2ca62d9..0e7023d 100644 | |
--- a/CHANGELOG | |
+++ b/CHANGELOG | |
@@ -1,3 +1,7 @@ | |
+= master | |
+ | |
+* Improve render performance up to 4x in the default case by calling compiled template methods directly (jeremyevans) | |
+ | |
= 3.21.0 (2019-06-14) | |
* Cache compiled templates in development mode, until the template files are modified (jeremyevans) | |
diff --git a/lib/roda/plugins/render.rb b/lib/roda/plugins/render.rb | |
index 3ae541e..dd2bcf3 100644 | |
--- a/lib/roda/plugins/render.rb | |
+++ b/lib/roda/plugins/render.rb | |
@@ -139,10 +139,15 @@ class Roda | |
# rendering faster by specifying +:cache_key+ inside the +:layout_opts+ | |
# plugin option. | |
module Render | |
+ # Support for using compiled methods directly requires Ruby 2.1 for the | |
+ # method binding to work, and Tilt 1.2 for Tilt::Template#compiled_method. | |
+ COMPILED_METHOD_SUPPORT = RUBY_VERSION >= '2.1' && defined?(Tilt::VERSION) && Tilt::VERSION >= '1.2' | |
+ | |
# Setup default rendering options. See Render for details. | |
def self.configure(app, opts=OPTS) | |
if app.opts[:render] | |
orig_cache = app.opts[:render][:cache] | |
+ orig_method_cache = app.opts[:render][:template_method_cache] | |
opts = app.opts[:render][:orig_opts].merge(opts) | |
end | |
app.opts[:render] = opts.dup | |
@@ -163,6 +168,20 @@ class Roda | |
end | |
end | |
+ if opts[:check_template_mtime] | |
+ opts.delete(:template_method_cache) | |
+ elsif COMPILED_METHOD_SUPPORT | |
+ opts[:template_method_cache] = orig_method_cache || (opts[:cache_class] || RodaCache).new | |
+ begin | |
+ app.const_get(:RodaCompiledTemplates, false) | |
+ rescue NameError | |
+ compiled_templates_module = Module.new | |
+ app.send(:include, compiled_templates_module) | |
+ app.const_set(:RodaCompiledTemplates, compiled_templates_module) | |
+ end | |
+ opts[:template_method_cache] = orig_method_cache || (opts[:cache_class] || RodaCache).new | |
+ end | |
+ | |
opts[:cache] = orig_cache || (opts[:cache_class] || RodaCache).new | |
opts[:layout_opts] = (opts[:layout_opts] || {}).dup | |
@@ -182,6 +201,8 @@ class Roda | |
else | |
opts[:layout_opts][:template] = layout | |
end | |
+ | |
+ opts[:optimize_layout] = (opts[:layout_opts][:template] if opts[:layout_opts].keys.sort == [:_is_layout, :template]) | |
end | |
opts[:layout_opts].freeze | |
@@ -249,6 +270,9 @@ class Roda | |
def inherited(subclass) | |
super | |
opts = subclass.opts[:render] = subclass.opts[:render].dup | |
+ if COMPILED_METHOD_SUPPORT | |
+ opts[:template_method_cache] = (opts[:cache_class] || RodaCache).new | |
+ end | |
opts[:cache] = opts[:cache].dup | |
opts.freeze | |
end | |
@@ -261,9 +285,13 @@ class Roda | |
module InstanceMethods | |
# Render the given template. See Render for details. | |
- def render(template, opts = OPTS, &block) | |
- opts = render_template_opts(template, opts) | |
- retrieve_template(opts).render((opts[:scope]||self), (opts[:locals]||OPTS), &block) | |
+ def render(template, opts = (optimized_template = _cached_template_method(template); OPTS), &block) | |
+ if optimized_template | |
+ send(optimized_template, {}, &block) | |
+ else | |
+ opts = render_template_opts(template, opts) | |
+ retrieve_template(opts).render((opts[:scope]||self), (opts[:locals]||OPTS), &block) | |
+ end | |
end | |
# Return the render options for the instance's class. | |
@@ -274,9 +302,26 @@ class Roda | |
# Render the given template. If there is a default layout | |
# for the class, take the result of the template rendering | |
# and render it inside the layout. See Render for details. | |
- def view(template, opts=OPTS) | |
- opts = parse_template_opts(template, opts) | |
- content = opts[:content] || render_template(opts) | |
+ def view(template, opts = (optimized_template = _cached_template_method(template); OPTS)) | |
+ if optimized_template | |
+ content = send(optimized_template, {}) | |
+ | |
+ render_opts = self.class.opts[:render] | |
+ if layout_template = render_opts[:optimize_layout] | |
+ method_cache = render_opts[:template_method_cache] | |
+ unless layout_method = method_cache[:_roda_layout] | |
+ retrieve_template(:template=>layout_template, :cache_key=>nil, :template_method_cache_key => :_roda_layout) | |
+ layout_method = method_cache[:_roda_layout] | |
+ end | |
+ | |
+ if layout_method | |
+ return send(layout_method, {}){content} | |
+ end | |
+ end | |
+ else | |
+ opts = parse_template_opts(template, opts) | |
+ content = opts[:content] || render_template(opts) | |
+ end | |
if layout_opts = view_layout_opts(opts) | |
content = render_template(layout_opts){content} | |
@@ -287,6 +332,41 @@ class Roda | |
private | |
+ if COMPILED_METHOD_SUPPORT | |
+ # If there is an instance method for the template, return the instance | |
+ # method symbol. This optimization is only used for render/view calls | |
+ # with a single string or symbol argument. | |
+ def _cached_template_method(template) | |
+ case template | |
+ when String, Symbol | |
+ if (method_cache = render_opts[:template_method_cache]) | |
+ _cached_template_method_lookup(method_cache, template) | |
+ end | |
+ end | |
+ end | |
+ | |
+ # The key to use in the template method cache for the given template. | |
+ def _cached_template_method_key(template) | |
+ template | |
+ end | |
+ | |
+ # Return the instance method symbol for the template in the method cache. | |
+ def _cached_template_method_lookup(method_cache, template) | |
+ method_cache[template] | |
+ end | |
+ else | |
+ # :nocov: | |
+ def _cached_template_method(template) | |
+ nil | |
+ end | |
+ | |
+ def _cached_template_method_key(template) | |
+ nil | |
+ end | |
+ # :nocov: | |
+ end | |
+ | |
+ | |
# Convert template options to single hash when rendering templates using render. | |
def render_template_opts(template, opts) | |
parse_template_opts(template, opts) | |
@@ -313,7 +393,7 @@ class Roda | |
# Given the template name and options, set the template class, template path/content, | |
# template block, and locals to use for the render in the passed options. | |
def find_template(opts) | |
- render_opts = render_opts() | |
+ render_opts = self.class.opts[:render] | |
engine_override = opts[:engine] | |
engine = opts[:engine] ||= render_opts[:engine] | |
if content = opts[:inline] | |
@@ -332,13 +412,15 @@ class Roda | |
end | |
if cache | |
- template_block = opts[:template_block] unless content | |
- template_opts = opts[:template_opts] | |
- | |
- opts[:cache_key] ||= if template_class || engine_override || template_opts || template_block | |
- [path, template_class, engine_override, template_opts, template_block] | |
- else | |
- path | |
+ unless opts.has_key?(:cache_key) | |
+ template_block = opts[:template_block] unless content | |
+ template_opts = opts[:template_opts] | |
+ | |
+ opts[:cache_key] = if template_class || engine_override || template_opts || template_block | |
+ [path, template_class, engine_override, template_opts, template_block] | |
+ else | |
+ path | |
+ end | |
end | |
else | |
opts.delete(:cache_key) | |
@@ -353,6 +435,9 @@ class Roda | |
if template.is_a?(Hash) | |
opts.merge!(template) | |
else | |
+ if opts.empty? && (key = _cached_template_method_key(template)) | |
+ opts[:template_method_cache_key] = key | |
+ end | |
opts[:template] = template | |
opts | |
end | |
@@ -372,6 +457,7 @@ class Roda | |
end | |
cached_template(opts) do | |
opts = found_template_opts || find_template(opts) | |
+ render_opts = self.class.opts[:render] | |
template_opts = render_opts[:template_opts] | |
if engine_opts = render_opts[:engine_opts][opts[:engine]] | |
template_opts = template_opts.merge(engine_opts) | |
@@ -383,7 +469,28 @@ class Roda | |
if render_opts[:check_template_mtime] && !opts[:template_block] && !cache | |
TemplateMtimeWrapper.new(opts[:template_class], opts[:path], 1, template_opts) | |
else | |
- opts[:template_class].new(opts[:path], 1, template_opts, &opts[:template_block]) | |
+ template = opts[:template_class].new(opts[:path], 1, template_opts, &opts[:template_block]) | |
+ | |
+ if COMPILED_METHOD_SUPPORT && | |
+ (method_cache_key = opts[:template_method_cache_key]) && | |
+ (method_cache = render_opts[:template_method_cache]) && | |
+ (method_cache[method_cache_key] != false) && | |
+ !opts[:inline] && | |
+ cache != false | |
+ | |
+ begin | |
+ unbound_method = template.send(:compiled_method, OPTS) | |
+ rescue ::NotImplementedError | |
+ method_cache[method_cache_key] = false | |
+ else | |
+ method_name = :"_roda_template_#{self.class.object_id}_#{method_cache_key}" | |
+ self.class::RodaCompiledTemplates.send(:define_method, method_name, unbound_method) | |
+ self.class::RodaCompiledTemplates.send(:private, method_name) | |
+ method_cache[method_cache_key] = method_name | |
+ end | |
+ end | |
+ | |
+ template | |
end | |
end | |
end | |
diff --git a/lib/roda/plugins/render_locals.rb b/lib/roda/plugins/render_locals.rb | |
index 749366d..1927215 100644 | |
--- a/lib/roda/plugins/render_locals.rb | |
+++ b/lib/roda/plugins/render_locals.rb | |
@@ -1,5 +1,7 @@ | |
# frozen-string-literal: true | |
+require_relative 'render' | |
+ | |
# | |
class Roda | |
module RodaPlugins | |
@@ -41,6 +43,14 @@ class Roda | |
module InstanceMethods | |
private | |
+ if Render::COMPILED_METHOD_SUPPORT | |
+ # Disable use of cached templates, since it assumes a render/view call with no | |
+ # options will have no locals. | |
+ def _cached_template_method(template) | |
+ nil | |
+ end | |
+ end | |
+ | |
def render_locals | |
opts[:render_locals] | |
end | |
diff --git a/lib/roda/plugins/view_options.rb b/lib/roda/plugins/view_options.rb | |
index 540c025..8c52b42 100644 | |
--- a/lib/roda/plugins/view_options.rb | |
+++ b/lib/roda/plugins/view_options.rb | |
@@ -1,5 +1,7 @@ | |
# frozen-string-literal: true | |
+require_relative 'render' | |
+ | |
# | |
class Roda | |
module RodaPlugins | |
@@ -124,6 +126,32 @@ class Roda | |
private | |
+ if Render::COMPILED_METHOD_SUPPORT | |
+ # Return nil if using custom view or layout options. | |
+ # If using a view subdir, prefix the template key with the subdir. | |
+ def _cached_template_method_key(template) | |
+ return if @_view_options || @_layout_options | |
+ | |
+ if subdir = @_view_subdir | |
+ template = "#{subdir}\0#{template}" | |
+ end | |
+ | |
+ super | |
+ end | |
+ | |
+ # Return nil if using custom view or layout options. | |
+ # If using a view subdir, prefix the template key with the subdir. | |
+ def _cached_template_method_lookup(method_cache, template) | |
+ return if @_view_options || @_layout_options | |
+ | |
+ if subdir = @_view_subdir | |
+ template = "#{subdir}\0#{template}" | |
+ end | |
+ | |
+ super | |
+ end | |
+ end | |
+ | |
# If view options or locals have been set and this | |
# template isn't a layout template, merge the options | |
# and locals into the returned hash. | |
diff --git a/spec/plugin/render_spec.rb b/spec/plugin/render_spec.rb | |
index 50d8896..3b7c134 100644 | |
--- a/spec/plugin/render_spec.rb | |
+++ b/spec/plugin/render_spec.rb | |
@@ -1,6 +1,7 @@ | |
require_relative "../spec_helper" | |
begin | |
+ require 'tilt' | |
require 'tilt/erb' | |
require 'tilt/string' | |
rescue LoadError | |
@@ -258,6 +259,162 @@ describe "render plugin" do | |
app.render_opts[:views].must_equal File.join(Dir.pwd, 'bar') | |
end | |
+ if RUBY_VERSION >= '2.1' && defined?(Tilt::VERSION) && Tilt::VERSION >= '1.2' | |
+ it "does not cache template renders when using a template library that doesn't support it" do | |
+ begin | |
+ require 'tilt/rdoc' | |
+ rescue | |
+ next | |
+ end | |
+ | |
+ app(:bare) do | |
+ plugin :render, :views=>'spec/views', :engine=>'rdoc' | |
+ route do | |
+ render('a') | |
+ end | |
+ end | |
+ | |
+ app.render_opts[:template_method_cache]['a'].must_be_nil | |
+ body.strip.must_equal "<p># a # * b</p>" | |
+ app.render_opts[:template_method_cache]['a'].must_equal false | |
+ body.strip.must_equal "<p># a # * b</p>" | |
+ app.render_opts[:template_method_cache]['a'].must_equal false | |
+ body.strip.must_equal "<p># a # * b</p>" | |
+ app.render_opts[:template_method_cache]['a'].must_equal false | |
+ app::RodaCompiledTemplates.private_instance_methods.length.must_equal 0 | |
+ end | |
+ | |
+ ['comp_test', :comp_test].each do |template| | |
+ it "does not cache template renders when given a hash" do | |
+ app(:bare) do | |
+ plugin :render, :views=>'spec/views' | |
+ route do | |
+ render(:template=>template) | |
+ end | |
+ end | |
+ | |
+ app.render_opts[:template_method_cache][template].must_be_nil | |
+ body.strip.must_equal "ct" | |
+ app.render_opts[:template_method_cache][template].must_be_nil | |
+ body.strip.must_equal "ct" | |
+ app.render_opts[:template_method_cache][template].must_be_nil | |
+ body.strip.must_equal "ct" | |
+ app.render_opts[:template_method_cache][template].must_be_nil | |
+ app::RodaCompiledTemplates.private_instance_methods.length.must_equal 0 | |
+ end | |
+ | |
+ it "caches template renders when given a #{template.class}" do | |
+ app(:bare) do | |
+ plugin :render, :views=>'spec/views' | |
+ route do | |
+ render(template) | |
+ end | |
+ end | |
+ | |
+ app.render_opts[:template_method_cache][template].must_be_nil | |
+ body.strip.must_equal "ct" | |
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol) | |
+ body.strip.must_equal "ct" | |
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol) | |
+ body.strip.must_equal "ct" | |
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol) | |
+ app::RodaCompiledTemplates.private_instance_methods.length.must_equal 1 | |
+ end | |
+ | |
+ it "does not cache template views or layout when given a hash" do | |
+ app(:bare) do | |
+ layout = template.to_s.sub('test', 'layout') | |
+ layout = layout.to_sym if template.is_a?(Symbol) | |
+ plugin :render, :views=>'spec/views', :layout=>layout | |
+ route do | |
+ view(:template=>template) | |
+ end | |
+ end | |
+ | |
+ app.render_opts[:template_method_cache][template].must_be_nil | |
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil | |
+ body.strip.must_equal "act\nb" | |
+ app.render_opts[:template_method_cache][template].must_be_nil | |
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil | |
+ body.strip.must_equal "act\nb" | |
+ app.render_opts[:template_method_cache][template].must_be_nil | |
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil | |
+ body.strip.must_equal "act\nb" | |
+ app.render_opts[:template_method_cache][template].must_be_nil | |
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil | |
+ app::RodaCompiledTemplates.private_instance_methods.length.must_equal 0 | |
+ end | |
+ | |
+ it "caches template views with layout when given a #{template.class}" do | |
+ app(:bare) do | |
+ layout = template.to_s.sub('test', 'layout') | |
+ layout = layout.to_sym if template.is_a?(Symbol) | |
+ plugin :render, :views=>'spec/views', :layout=>layout | |
+ route do | |
+ view(template) | |
+ end | |
+ end | |
+ | |
+ app.render_opts[:template_method_cache][template].must_be_nil | |
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil | |
+ body.strip.must_equal "act\nb" | |
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol) | |
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil | |
+ body.strip.must_equal "act\nb" | |
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol) | |
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_kind_of(Symbol) | |
+ body.strip.must_equal "act\nb" | |
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol) | |
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_kind_of(Symbol) | |
+ app::RodaCompiledTemplates.private_instance_methods.length.must_equal 2 | |
+ end | |
+ | |
+ it "caches template views without layout when additional layout options given when given a #{template.class}" do | |
+ app(:bare) do | |
+ plugin :render, :views=>'spec/views', :layout=>nil | |
+ route do | |
+ view(template) | |
+ end | |
+ end | |
+ | |
+ app.render_opts[:template_method_cache][template].must_be_nil | |
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil | |
+ body.strip.must_equal "ct" | |
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol) | |
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil | |
+ body.strip.must_equal "ct" | |
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol) | |
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil | |
+ body.strip.must_equal "ct" | |
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol) | |
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil | |
+ app::RodaCompiledTemplates.private_instance_methods.length.must_equal 1 | |
+ end | |
+ | |
+ it "caches template views without layout when additional layout options given when given a #{template.class}" do | |
+ app(:bare) do | |
+ plugin :render, :views=>'spec/views', :layout_opts=>{:locals=>{:title=>"Home"}} | |
+ route do | |
+ view(template) | |
+ end | |
+ end | |
+ | |
+ app.render_opts[:template_method_cache][template].must_be_nil | |
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil | |
+ body.strip.must_equal "<title>Roda: Home</title>\nct" | |
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol) | |
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil | |
+ body.strip.must_equal "<title>Roda: Home</title>\nct" | |
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol) | |
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil | |
+ body.strip.must_equal "<title>Roda: Home</title>\nct" | |
+ app.render_opts[:template_method_cache][template].must_be_kind_of(Symbol) | |
+ app.render_opts[:template_method_cache][:_roda_layout].must_be_nil | |
+ app::RodaCompiledTemplates.private_instance_methods.length.must_equal 1 | |
+ end | |
+ end | |
+ end | |
+ | |
it "inline layouts and inline views" do | |
app(:render) do | |
view({:inline=>'bar'}, :layout=>{:inline=>'Foo: <%= yield %>'}) | |
diff --git a/spec/plugin/view_options_spec.rb b/spec/plugin/view_options_spec.rb | |
index baabe28..97e9eb0 100644 | |
--- a/spec/plugin/view_options_spec.rb | |
+++ b/spec/plugin/view_options_spec.rb | |
@@ -21,12 +21,21 @@ describe "view_options plugin view subdirs" do | |
r.on "about" do | |
append_view_subdir 'views' | |
+ r.is 'test' do | |
+ append_view_subdir 'about' | |
+ render("comp_test") | |
+ end | |
render("about", :locals=>{:title => "About Roda"}) | |
end | |
r.on "path" do | |
render('spec/views/about', :locals=>{:title => "Path"}, :layout_opts=>{:locals=>{:title=>"Home"}}) | |
end | |
+ | |
+ r.is 'test' do | |
+ set_view_subdir 'spec/views' | |
+ render("comp_test") | |
+ end | |
end | |
end | |
end | |
@@ -42,6 +51,15 @@ describe "view_options plugin view subdirs" do | |
it "should not change behavior when subdir is not set" do | |
body("/path").strip.must_equal "<h1>Path</h1>" | |
end | |
+ | |
+ it "should handle template compilation correctly" do | |
+ 3.times do | |
+ body("/test").strip.must_equal "ct" | |
+ end | |
+ 3.times do | |
+ body("/about/test").strip.must_equal "about-ct" | |
+ end | |
+ end | |
end | |
describe "view_options plugin" do | |
diff --git a/spec/views/a.rdoc b/spec/views/a.rdoc | |
new file mode 100644 | |
index 0000000..1f7d854 | |
--- /dev/null | |
+++ b/spec/views/a.rdoc | |
@@ -0,0 +1,2 @@ | |
+# a | |
+# * b | |
diff --git a/spec/views/about/comp_test.erb b/spec/views/about/comp_test.erb | |
new file mode 100644 | |
index 0000000..114a59d | |
--- /dev/null | |
+++ b/spec/views/about/comp_test.erb | |
@@ -0,0 +1 @@ | |
+about-ct | |
diff --git a/spec/views/comp_layout.erb b/spec/views/comp_layout.erb | |
new file mode 100644 | |
index 0000000..89d2654 | |
--- /dev/null | |
+++ b/spec/views/comp_layout.erb | |
@@ -0,0 +1 @@ | |
+a<%= yield %>b | |
diff --git a/spec/views/comp_test.erb b/spec/views/comp_test.erb | |
new file mode 100644 | |
index 0000000..ef616ac | |
--- /dev/null | |
+++ b/spec/views/comp_test.erb | |
@@ -0,0 +1 @@ | |
+ct | |
-- | |
2.21.0 | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment