diff --git a/actionpack/lib/action_view/dependency_tracker.rb b/actionpack/lib/action_view/dependency_tracker.rb new file mode 100644 index 0000000000..3de5cd150b --- /dev/null +++ b/actionpack/lib/action_view/dependency_tracker.rb @@ -0,0 +1,91 @@ +require 'thread_safe' + +module ActionView + class DependencyTracker + @trackers = ThreadSafe::Cache.new + + def self.find_dependencies(name, template) + tracker = @trackers[template.handler] + + if tracker.present? + tracker.call(name, template) + else + [] + end + end + + def self.register_tracker(extension, tracker) + handler = Template.handler_for_extension(extension) + @trackers[handler] = tracker + end + + def self.remove_tracker(handler) + @trackers.delete(handler) + end + + class ERBTracker + EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/ + + # Matches: + # render partial: "comments/comment", collection: commentable.comments + # render "comments/comments" + # render 'comments/comments' + # render('comments/comments') + # + # render(@topic) => render("topics/topic") + # render(topics) => render("topics/topic") + # render(message.topics) => render("topics/topic") + RENDER_DEPENDENCY = / + render\s* # render, followed by optional whitespace + \(? # start an optional parenthesis for the render call + (partial:|:partial\s+=>)?\s* # naming the partial, used with collection -- 1st capture + ([@a-z"'][@a-z_\/\."']+) # the template name itself -- 2nd capture + /x + + def self.call(name, template) + new(name, template).dependencies + end + + def initialize(name, template) + @name, @template = name, template + end + + def dependencies + render_dependencies + explicit_dependencies + end + + private + attr_reader :name, :template + + def source + template.source + end + + def directory + name.split("/")[0..-2].join("/") + end + + def render_dependencies + source.scan(RENDER_DEPENDENCY). + collect(&:second).uniq. + + # render(@topic) => render("topics/topic") + # render(topics) => render("topics/topic") + # render(message.topics) => render("topics/topic") + collect { |name| name.sub(/\A@?([a-z]+\.)*([a-z_]+)\z/) { "#{$2.pluralize}/#{$2.singularize}" } }. + + # render("headline") => render("message/headline") + collect { |name| name.include?("/") ? name : "#{directory}/#{name}" }. + + # replace quotes from string renders + collect { |name| name.gsub(/["']/, "") } + end + + def explicit_dependencies + source.scan(EXPLICIT_DEPENDENCY).flatten.uniq + end + end + + register_tracker :erb, ERBTracker + end +end diff --git a/actionpack/lib/action_view/digestor.rb b/actionpack/lib/action_view/digestor.rb index 4507861dcc..9324a1ac50 100644 --- a/actionpack/lib/action_view/digestor.rb +++ b/actionpack/lib/action_view/digestor.rb @@ -1,25 +1,8 @@ require 'thread_safe' +require 'action_view/dependency_tracker' module ActionView class Digestor - EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/ - - # Matches: - # render partial: "comments/comment", collection: commentable.comments - # render "comments/comments" - # render 'comments/comments' - # render('comments/comments') - # - # render(@topic) => render("topics/topic") - # render(topics) => render("topics/topic") - # render(message.topics) => render("topics/topic") - RENDER_DEPENDENCY = / - render\s* # render, followed by optional whitespace - \(? # start an optional parenthesis for the render call - (partial:|:partial\s+=>)?\s* # naming the partial, used with collection -- 1st capture - ([@a-z"'][@a-z_\/\."']+) # the template name itself -- 2nd capture - /x - cattr_reader(:cache) @@cache = ThreadSafe::Cache.new @@ -47,7 +30,7 @@ module ActionView end def dependencies - render_dependencies + explicit_dependencies + DependencyTracker.find_dependencies(name, template) rescue ActionView::MissingTemplate [] # File doesn't exist, so no dependencies end @@ -69,16 +52,16 @@ module ActionView name.gsub(%r|/_|, "/") end - def directory - name.split("/")[0..-2].join("/") - end - def partial? false end + def template + @template ||= finder.find(logical_name, [], partial?, formats: [ format ]) + end + def source - @source ||= finder.find(logical_name, [], partial?, formats: [ format ]).source + template.source end def dependency_digest @@ -89,26 +72,6 @@ module ActionView (template_digests + injected_dependencies).join("-") end - def render_dependencies - source.scan(RENDER_DEPENDENCY). - collect(&:second).uniq. - - # render(@topic) => render("topics/topic") - # render(topics) => render("topics/topic") - # render(message.topics) => render("topics/topic") - collect { |name| name.sub(/\A@?([a-z]+\.)*([a-z_]+)\z/) { "#{$2.pluralize}/#{$2.singularize}" } }. - - # render("headline") => render("message/headline") - collect { |name| name.include?("/") ? name : "#{directory}/#{name}" }. - - # replace quotes from string renders - collect { |name| name.gsub(/["']/, "") } - end - - def explicit_dependencies - source.scan(EXPLICIT_DEPENDENCY).flatten.uniq - end - def injected_dependencies Array.wrap(options[:dependencies]) end diff --git a/actionpack/test/template/dependency_tracker_test.rb b/actionpack/test/template/dependency_tracker_test.rb new file mode 100644 index 0000000000..9c68afbdbd --- /dev/null +++ b/actionpack/test/template/dependency_tracker_test.rb @@ -0,0 +1,46 @@ +require 'abstract_unit' +require 'action_view/dependency_tracker' + +class DependencyTrackerTest < ActionView::TestCase + Neckbeard = lambda {|template| template.source } + Bowtie = lambda {|template| template.source } + + class NeckbeardTracker + def self.call(name, template) + ["foo/#{name}"] + end + end + + class FakeTemplate + attr_reader :source, :handler + + def initialize(source, handler = Neckbeard) + @source, @handler = source, handler + end + end + + def tracker + ActionView::DependencyTracker + end + + def setup + ActionView::Template.register_template_handler :neckbeard, Neckbeard + tracker.register_tracker(:neckbeard, NeckbeardTracker) + end + + def teardown + tracker.remove_tracker(:neckbeard) + end + + def test_finds_tracker_by_template_handler + template = FakeTemplate.new("boo/hoo") + dependencies = tracker.find_dependencies("boo/hoo", template) + assert_equal ["foo/boo/hoo"], dependencies + end + + def test_returns_empty_array_if_no_tracker_is_found + template = FakeTemplate.new("boo/hoo", Bowtie) + dependencies = tracker.find_dependencies("boo/hoo", template) + assert_equal [], dependencies + end +end diff --git a/actionpack/test/template/digestor_test.rb b/actionpack/test/template/digestor_test.rb index 849e2981a6..e29cecabc0 100644 --- a/actionpack/test/template/digestor_test.rb +++ b/actionpack/test/template/digestor_test.rb @@ -2,10 +2,11 @@ require 'abstract_unit' require 'fileutils' class FixtureTemplate - attr_reader :source + attr_reader :source, :handler def initialize(template_path) @source = File.read(template_path) + @handler = ActionView::Template.handler_for_extension(:erb) rescue Errno::ENOENT raise ActionView::MissingTemplate.new([], "", [], true, []) end