module Deface class Override include Deface::TemplateHelper cattr_accessor :all, :actions attr_accessor :args @@all ||= {} @@actions = [:remove, :replace, :insert_after, :insert_before, :insert_top, :insert_bottom] # 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 # * :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 # # ==== 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. def initialize(args) @args = args raise(ArgumentError, "Invalid action") if self.action.nil? raise(ArgumentError, ":virtual_path must be defined") if args[:virtual_path].blank? key = args[:virtual_path].to_sym @@all[key] ||= {} @@all[key][args[:name].to_s.parameterize] = self end def selector @args[self.action] end def name @args[:name] 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) == "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 @args[:closing_selector] end # applies all applicable overrides to given source # def self.apply(source, details) overrides = find(details) @enable_logging ||= defined?(Rails) == "constant" if @enable_logging && 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 @enable_logging next end if override.end_selector.blank? # single css selector matches = doc.css(override.selector) if @enable_logging 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 :insert_before match.before override.source_element when :insert_after match.after override.source_element when :insert_top match.children.before(override.source_element) when :insert_bottom match.children.after(override.source_element) 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 elements = select_range(starting, ending) if override.action == :replace starting.before(override.source_element) end #now remove all matched elements elements.map &:remove 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 @@all.empty? || details.empty? virtual_path = details[:virtual_path] return [] if virtual_path.nil? result = [] result << @@all[virtual_path.to_sym].try(:values) result.flatten.compact 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