# frozen_string_literal: true require 'sprockets/http_utils' require 'sprockets/processor_utils' require 'sprockets/utils' module Sprockets module Transformers include HTTPUtils, ProcessorUtils, Utils # Public: Two level mapping of a source mime type to a target mime type. # # environment.transformers # # => { 'text/coffeescript' => { # 'application/javascript' => ConvertCoffeeScriptToJavaScript # } # } # def transformers config[:transformers] end Transformer = Struct.new :from, :to, :proc # Public: Register a transformer from and to a mime type. # # from - String mime type # to - String mime type # proc - Callable block that accepts an input Hash. # # Examples # # register_transformer 'text/coffeescript', 'application/javascript', # ConvertCoffeeScriptToJavaScript # # register_transformer 'image/svg+xml', 'image/png', ConvertSvgToPng # # Returns nothing. def register_transformer(from, to, proc) self.config = hash_reassoc(config, :registered_transformers) do |transformers| transformers << Transformer.new(from, to, proc) end compute_transformers!(self.config[:registered_transformers]) end # Internal: Register transformer for existing type adding a suffix. # # types - Array of existing mime type Strings # type_format - String suffix formatting string # extname - String extension to append # processor - Callable block that accepts an input Hash. # # Returns nothing. def register_transformer_suffix(types, type_format, extname, processor) Array(types).each do |type| extensions, charset = mime_types[type].values_at(:extensions, :charset) parts = type.split('/') suffix_type = type_format.sub('\1', parts[0]).sub('\2', parts[1]) extensions = extensions.map { |ext| "#{ext}#{extname}" } register_mime_type(suffix_type, extensions: extensions, charset: charset) register_transformer(suffix_type, type, processor) end end # Internal: Resolve target mime type that the source type should be # transformed to. # # type - String from mime type # accept - String accept type list (default: '*/*') # # Examples # # resolve_transform_type('text/plain', 'text/plain') # # => 'text/plain' # # resolve_transform_type('image/svg+xml', 'image/png, image/*') # # => 'image/png' # # resolve_transform_type('text/css', 'image/png') # # => nil # # Returns String mime type or nil is no type satisfied the accept value. def resolve_transform_type(type, accept) find_best_mime_type_match(accept || '*/*', [type].compact + config[:transformers][type].keys) end # Internal: Expand accept type list to include possible transformed types. # # parsed_accepts - Array of accept q values # # Examples # # expand_transform_accepts([['application/javascript', 1.0]]) # # => [['application/javascript', 1.0], ['text/coffeescript', 0.8]] # # Returns an expanded Array of q values. def expand_transform_accepts(parsed_accepts) accepts = [] parsed_accepts.each do |(type, q)| accepts.push([type, q]) config[:inverted_transformers][type].each do |subtype| accepts.push([subtype, q * 0.8]) end end accepts end # Internal: Compose multiple transformer steps into a single processor # function. # # transformers - Two level Hash of a source mime type to a target mime type # types - Array of mime type steps # # Returns Processor. def compose_transformers(transformers, types, preprocessors, postprocessors) if types.length < 2 raise ArgumentError, "too few transform types: #{types.inspect}" end processors = types.each_cons(2).map { |src, dst| unless processor = transformers[src][dst] raise ArgumentError, "missing transformer for type: #{src} to #{dst}" end processor } compose_transformer_list processors, preprocessors, postprocessors end private def compose_transformer_list(transformers, preprocessors, postprocessors) processors = [] transformers.each do |processor| processors.concat postprocessors[processor.from] processors << processor.proc processors.concat preprocessors[processor.to] end if processors.size > 1 compose_processors(*processors.reverse) elsif processors.size == 1 processors.first end end def compute_transformers!(registered_transformers) preprocessors = self.config[:preprocessors] postprocessors = self.config[:postprocessors] transformers = Hash.new { {} } inverted_transformers = Hash.new { Set.new } incoming_edges = registered_transformers.group_by(&:from) registered_transformers.each do |t| traversals = dfs_paths([t]) { |k| incoming_edges.fetch(k.to, []) } traversals.each do |nodes| src, dst = nodes.first.from, nodes.last.to processor = compose_transformer_list nodes, preprocessors, postprocessors transformers[src] = {} unless transformers.key?(src) transformers[src][dst] = processor inverted_transformers[dst] = Set.new unless inverted_transformers.key?(dst) inverted_transformers[dst] << src end end self.config = hash_reassoc(config, :transformers) { transformers } self.config = hash_reassoc(config, :inverted_transformers) { inverted_transformers } end end end