module Deface class Override include Deface::TemplateHelper cattr_accessor :actions, :_early attr_accessor :args @@_early = [] @@actions = [:remove, :replace, :replace_contents, :surround, :surround_contents, :insert_after, :insert_before, :insert_top, :insert_bottom, :set_attributes] @@sources = [:text, :partial, :template] # Initializes new override, you must supply only one Target, Action & Source # parameter for each override (and any number of Optional parameters). # # ==== Target # # * :virtual_path - The path of the template / partial where # the override should take effect eg: "shared/_person", "admin/posts/new" # this will apply to all controller actions that use the specified template # # ==== Action # # * :remove - Removes all elements that match the supplied selector # * :replace - Replaces all elements that match the supplied selector # * :replace_contents - Replaces the contents of all elements that match the supplied selector # * :surround - Surrounds all elements that match the supplied selector, expects replacement markup to contain <%= render_original %> placeholder # * :surround_contents - Surrounds the contents of all elements that match the supplied selector, expects replacement markup to contain <%= render_original %> placeholder # * :insert_after - Inserts after all elements that match the supplied selector # * :insert_before - Inserts before all elements that match the supplied selector # * :insert_top - Inserts inside all elements that match the supplied selector, before all existing child # * :insert_bottom - Inserts inside all elements that match the supplied selector, after all existing child # * :set_attributes - Sets (or adds) attributes to all elements that match the supplied selector, expects :attributes option to be passed. # # ==== Source # # * :text - String containing markup # * :partial - Relative path to partial # * :template - Relative path to template # # ==== Optional # # * :name - Unique name for override so it can be identified and modified later. # This needs to be unique within the same :virtual_path # * :disabled - When set to true the override will not be applied. # * :original - String containing original markup that is being overridden. # If supplied Deface will log when the original markup changes, which helps highlight overrides that need # attention when upgrading versions of the source application. Only really warranted for :replace overrides. # NB: All whitespace is stripped before comparsion. # * :closing_selector - A second css selector targeting an end element, allowing you to select a range # of elements to apply an action against. The :closing_selector only supports the :replace, :remove and # :replace_contents actions, and the end element must be a sibling of the first/starting element. Note the CSS # general sibling selector (~) is used to match the first element after the opening selector. # * :sequence - Used to order the application of an override for a specific virtual path, helpful when # an override depends on another override being applied first. # Supports: # :sequence => n - where n is a positive or negative integer (lower numbers get applied first, default 100). # :sequence => {:before => "override_name"} - where "override_name" is the name of an override defined for the # same virutal_path, the current override will be appplied before # the named override passed. # :sequence => {:after => "override_name") - the current override will be applied after the named override passed. # * :attributes - A hash containing all the attributes to be set on the matched elements, eg: :attributes => {:class => "green", :title => "some string"} # def initialize(args) unless Rails.application.try(:config).respond_to?(:deface) and Rails.application.try(:config).deface.try(:overrides) @@_early << args warn "[WARNING] Deface railtie has not initialized yet, override '#{args[:name]}' is being declared too early." return end raise(ArgumentError, ":name must be defined") unless args.key? :name raise(ArgumentError, ":virtual_path must be defined") if args[:virtual_path].blank? virtual_key = args[:virtual_path].to_sym name_key = args[:name].to_s.parameterize self.class.all[virtual_key] ||= {} if self.class.all[virtual_key].has_key? name_key #updating exisiting override @args = self.class.all[virtual_key][name_key].args #check if the action is being redefined, and reject old action if (@@actions & args.keys).present? @args.reject!{|key, value| (@@actions & @args.keys).include? key } end #check if the source is being redefined, and reject old action if (@@sources & args.keys).present? @args.reject!{|key, value| (@@sources & @args.keys).include? key } end @args.merge!(args) else #initializing new override @args = args raise(ArgumentError, ":action is invalid") if self.action.nil? end self.class.all[virtual_key][name_key] = self end def selector @args[self.action] end def name @args[:name] end def sequence return 100 unless @args.key?(:sequence) if @args[:sequence].is_a? Hash key = @args[:virtual_path].to_sym if @args[:sequence].key? :before ref_name = @args[:sequence][:before] if self.class.all[key].key? ref_name.to_s return self.class.all[key][ref_name.to_s].sequence - 1 else return 100 end elsif @args[:sequence].key? :after ref_name = @args[:sequence][:after] if self.class.all[key].key? ref_name.to_s return self.class.all[key][ref_name.to_s].sequence + 1 else return 100 end else #should never happen.. tut tut! return 100 end else return @args[:sequence].to_i end rescue SystemStackError if defined?(Rails) Rails.logger.error "\e[1;32mDeface: [WARNING]\e[0m Circular sequence dependency includes override named: '#{self.name}' on '#{@args[:virtual_path]}'." end return 100 end def action (@@actions & @args.keys).first end def source erb = if @args.key? :partial load_template_source(@args[:partial], true) elsif @args.key? :template load_template_source(@args[:template], false) elsif @args.key? :text @args[:text] end end def source_element Deface::Parser.convert(source.clone) end def original_source return nil unless @args[:original].present? Deface::Parser.convert(@args[:original].clone) end # logs if original source has changed def validate_original(match) return true if self.original_source.nil? valid = self.original_source.to_s.gsub(/\s/, '') == match.to_s.gsub(/\s/, '') if !valid && defined?(Rails.logger) == "constant" Rails.logger.error "\e[1;32mDeface: [WARNING]\e[0m The original source for '#{self.name}' has changed, this override should be reviewed to ensure it's still valid." end valid end def disabled? @args.key?(:disabled) ? @args[:disabled] : false end def end_selector return nil if @args[:closing_selector].blank? "#{self.selector} ~ #{@args[:closing_selector]}" end def attributes @args[:attributes] || [] end # applies all applicable overrides to given source # def self.apply(source, details, log=true) overrides = find(details) if log log = defined?(Rails.logger) end if log && overrides.size > 0 Rails.logger.info "\e[1;32mDeface:\e[0m #{overrides.size} overrides found for '#{details[:virtual_path]}'" end unless overrides.empty? doc = Deface::Parser.convert(source) overrides.each do |override| if override.disabled? Rails.logger.info("\e[1;32mDeface:\e[0m '#{override.name}' is disabled") if log next end if override.end_selector.blank? # single css selector matches = doc.css(override.selector) if log Rails.logger.send(matches.size == 0 ? :error : :info, "\e[1;32mDeface:\e[0m '#{override.name}' matched #{matches.size} times with '#{override.selector}'") end matches.each do |match| override.validate_original(match) case override.action when :remove match.replace "" when :replace match.replace override.source_element when :replace_contents match.children.remove match.add_child(override.source_element) when :surround, :surround_contents new_source = override.source_element.clone(1) if original = new_source.css("code:contains('render_original')").first if override.action == :surround original.replace match.clone(1) match.replace new_source elsif override.action == :surround_contents original.replace match.children match.children.remove match.add_child new_source end else #maybe we should log that the original wasn't found. end when :insert_before match.before override.source_element when :insert_after match.after override.source_element when :insert_top if match.children.size == 0 match.children = override.source_element else match.children.before(override.source_element) end when :insert_bottom if match.children.size == 0 match.children = override.source_element else match.children.after(override.source_element) end when :set_attributes override.attributes.each do |name, value| match.set_attribute(name.to_s, value.to_s) end end end else # targeting range of elements as end_selector is present starting = doc.css(override.selector).first if starting && starting.parent ending = starting.parent.css(override.end_selector).first else ending = doc.css(override.end_selector).first end if starting && ending if log Rails.logger.info("\e[1;32mDeface:\e[0m '#{override.name}' matched starting with '#{override.selector}' and ending with '#{override.end_selector}'") end elements = select_range(starting, ending) case override.action when :remove elements.map &:remove when :replace starting.before(override.source_element) elements.map &:remove when :replace_contents elements[1..-2].map &:remove starting.after(override.source_element) end else if starting.nil? Rails.logger.info("\e[1;32mDeface:\e[0m '#{override.name}' failed to match with starting selector '#{override.selector}'") else Rails.logger.info("\e[1;32mDeface:\e[0m '#{override.name}' failed to match with end selector '#{override.end_selector}'") end end end end #prevents any caching by rails in development mode details[:updated_at] = Time.now source = doc.to_s Deface::Parser.undo_erb_markup!(source) end source end # finds all applicable overrides for supplied template # def self.find(details) return [] if self.all.empty? || details.empty? virtual_path = details[:virtual_path] return [] if virtual_path.nil? virtual_path = virtual_path[1..-1] if virtual_path.first == '/' result = [] result << self.all[virtual_path.to_sym].try(:values) result.flatten.compact.sort_by &:sequence end def self.all Rails.application.config.deface.overrides.all end private # finds all elements upto closing sibling in nokgiri document # def self.select_range(first, last) first == last ? [first] : [first, *select_range(first.next, last)] end end end