context.rb 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. require 'base64'
  2. require 'rack/utils'
  3. require 'sprockets/errors'
  4. require 'sprockets/utils'
  5. require 'pathname'
  6. require 'set'
  7. module Sprockets
  8. # `Context` provides helper methods to all `Tilt` processors. They
  9. # are typically accessed by ERB templates. You can mix in custom
  10. # helpers by injecting them into `Environment#context_class`. Do not
  11. # mix them into `Context` directly.
  12. #
  13. # environment.instance_eval do
  14. # include MyHelper
  15. # def asset_url; end
  16. # end
  17. #
  18. # <%= asset_url "foo.png" %>
  19. #
  20. # The `Context` also collects dependencies declared by
  21. # assets. See `DirectiveProcessor` for an example of this.
  22. class Context
  23. attr_reader :environment, :pathname
  24. attr_reader :_required_paths, :_dependency_paths, :_dependency_assets
  25. attr_writer :__LINE__
  26. def initialize(environment, logical_path, pathname)
  27. @environment = environment
  28. @logical_path = logical_path
  29. @pathname = pathname
  30. @__LINE__ = nil
  31. @_required_paths = []
  32. @_dependency_paths = Set.new
  33. @_dependency_assets = Set.new([pathname.to_s])
  34. end
  35. # Returns the environment path that contains the file.
  36. #
  37. # If `app/javascripts` and `app/stylesheets` are in your path, and
  38. # current file is `app/javascripts/foo/bar.js`, `root_path` would
  39. # return `app/javascripts`.
  40. def root_path
  41. environment.paths.detect { |path| pathname.to_s[path] }
  42. end
  43. # Returns logical path without any file extensions.
  44. #
  45. # 'app/javascripts/application.js'
  46. # # => 'application'
  47. #
  48. def logical_path
  49. @logical_path[/^([^.]+)/, 0]
  50. end
  51. # Returns content type of file
  52. #
  53. # 'application/javascript'
  54. # 'text/css'
  55. #
  56. def content_type
  57. environment.content_type_of(pathname)
  58. end
  59. # Given a logical path, `resolve` will find and return the fully
  60. # expanded path. Relative paths will also be resolved. An optional
  61. # `:content_type` restriction can be supplied to restrict the
  62. # search.
  63. #
  64. # resolve("foo.js")
  65. # # => "/path/to/app/javascripts/foo.js"
  66. #
  67. # resolve("./bar.js")
  68. # # => "/path/to/app/javascripts/bar.js"
  69. #
  70. def resolve(path, options = {}, &block)
  71. pathname = Pathname.new(path)
  72. attributes = environment.attributes_for(pathname)
  73. if pathname.absolute?
  74. pathname
  75. elsif content_type = options[:content_type]
  76. content_type = self.content_type if content_type == :self
  77. if attributes.format_extension
  78. if content_type != attributes.content_type
  79. raise ContentTypeMismatch, "#{path} is " +
  80. "'#{attributes.content_type}', not '#{content_type}'"
  81. end
  82. end
  83. resolve(path) do |candidate|
  84. if self.content_type == environment.content_type_of(candidate)
  85. return candidate
  86. end
  87. end
  88. raise FileNotFound, "couldn't find file '#{path}'"
  89. else
  90. environment.resolve(path, :base_path => self.pathname.dirname, &block)
  91. end
  92. end
  93. # `depend_on` allows you to state a dependency on a file without
  94. # including it.
  95. #
  96. # This is used for caching purposes. Any changes made to
  97. # the dependency file with invalidate the cache of the
  98. # source file.
  99. def depend_on(path)
  100. @_dependency_paths << resolve(path).to_s
  101. nil
  102. end
  103. # `depend_on_asset` allows you to state an asset dependency
  104. # without including it.
  105. #
  106. # This is used for caching purposes. Any changes that would
  107. # invalidate the dependency asset will invalidate the source
  108. # file. Unlike `depend_on`, this will include recursively include
  109. # the target asset's dependencies.
  110. def depend_on_asset(path)
  111. filename = resolve(path).to_s
  112. @_dependency_assets << filename
  113. nil
  114. end
  115. # `require_asset` declares `path` as a dependency of the file. The
  116. # dependency will be inserted before the file and will only be
  117. # included once.
  118. #
  119. # If ERB processing is enabled, you can use it to dynamically
  120. # require assets.
  121. #
  122. # <%= require_asset "#{framework}.js" %>
  123. #
  124. def require_asset(path)
  125. pathname = resolve(path, :content_type => :self)
  126. depend_on_asset(pathname)
  127. @_required_paths << pathname.to_s
  128. nil
  129. end
  130. # Tests if target path is able to be safely required into the
  131. # current concatenation.
  132. def asset_requirable?(path)
  133. pathname = resolve(path)
  134. content_type = environment.content_type_of(pathname)
  135. stat = environment.stat(path)
  136. return false unless stat && stat.file?
  137. self.content_type.nil? || self.content_type == content_type
  138. end
  139. # Reads `path` and runs processors on the file.
  140. #
  141. # This allows you to capture the result of an asset and include it
  142. # directly in another.
  143. #
  144. # <%= evaluate "bar.js" %>
  145. #
  146. def evaluate(path, options = {})
  147. pathname = resolve(path)
  148. attributes = environment.attributes_for(pathname)
  149. processors = options[:processors] || attributes.processors
  150. if options[:data]
  151. result = options[:data]
  152. else
  153. result = Sprockets::Utils.read_unicode(pathname)
  154. end
  155. processors.each do |processor|
  156. begin
  157. template = processor.new(pathname.to_s) { result }
  158. result = template.render(self, {})
  159. rescue Exception => e
  160. annotate_exception! e
  161. raise
  162. end
  163. end
  164. result
  165. end
  166. # Returns a Base64-encoded `data:` URI with the contents of the
  167. # asset at the specified path, and marks that path as a dependency
  168. # of the current file.
  169. #
  170. # Use `asset_data_uri` from ERB with CSS or JavaScript assets:
  171. #
  172. # #logo { background: url(<%= asset_data_uri 'logo.png' %>) }
  173. #
  174. # $('<img>').attr('src', '<%= asset_data_uri 'avatar.jpg' %>')
  175. #
  176. def asset_data_uri(path)
  177. depend_on_asset(path)
  178. asset = environment.find_asset(path)
  179. base64 = Base64.encode64(asset.to_s).gsub(/\s+/, "")
  180. "data:#{asset.content_type};base64,#{Rack::Utils.escape(base64)}"
  181. end
  182. private
  183. # Annotates exception backtrace with the original template that
  184. # the exception was raised in.
  185. def annotate_exception!(exception)
  186. location = pathname.to_s
  187. location << ":#{@__LINE__}" if @__LINE__
  188. exception.extend(Sprockets::EngineError)
  189. exception.sprockets_annotation = " (in #{location})"
  190. end
  191. def logger
  192. environment.logger
  193. end
  194. end
  195. end