deface 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,125 @@
1
+ require 'polyglot'
2
+
3
+ require 'deface/dsl/context'
4
+
5
+ module Deface
6
+ module DSL
7
+ class Loader
8
+ def self.load(filename, options = nil, &block)
9
+ unless File.basename(filename) =~ /^[^\.]+(.html.(erb|haml)){0,1}.deface$/
10
+ raise "Deface::DSL does not know how to read '#{filename}'. Override files should end with just .deface, .html.erb.deface, or .html.haml.deface"
11
+ end
12
+
13
+ unless file_in_dir_below_overrides?(filename)
14
+ raise "Deface::DSL overrides must be in a sub-directory that matches the views virtual path. Move '#{filename}' into a sub-directory."
15
+ end
16
+
17
+ File.open(filename) do |file|
18
+ context_name = File.basename(filename).gsub('.deface', '')
19
+
20
+ file_contents = file.read
21
+
22
+ if context_name.end_with?('.html.erb')
23
+ dsl_commands, the_rest = extract_dsl_commands_from_erb(file_contents)
24
+
25
+ context_name = context_name.gsub('.html.erb', '')
26
+ context = Context.new(context_name)
27
+ context.virtual_path(determine_virtual_path(filename))
28
+ context.instance_eval(dsl_commands)
29
+ context.erb(the_rest)
30
+ context.create_override
31
+ elsif context_name.end_with?('.html.haml')
32
+ dsl_commands, the_rest = extract_dsl_commands_from_haml(file_contents)
33
+
34
+ context_name = context_name.gsub('.html.haml', '')
35
+ context = Context.new(context_name)
36
+ context.virtual_path(determine_virtual_path(filename))
37
+ context.instance_eval(dsl_commands)
38
+ context.haml(the_rest)
39
+ context.create_override
40
+ else
41
+ context = Context.new(context_name)
42
+ context.virtual_path(determine_virtual_path(filename))
43
+ context.instance_eval(file_contents)
44
+ context.create_override
45
+ end
46
+ end
47
+ end
48
+
49
+ def self.register
50
+ Polyglot.register('deface', Deface::DSL::Loader)
51
+ end
52
+
53
+ def self.extract_dsl_commands_from_erb(html_file_contents)
54
+ dsl_commands = ''
55
+
56
+ while starts_with_html_comment?(html_file_contents)
57
+ first_open_comment_index = html_file_contents.lstrip.index('<!--')
58
+ first_close_comment_index = html_file_contents.index('-->')
59
+
60
+ unless first_close_comment_index.nil?
61
+ comment = html_file_contents[first_open_comment_index..first_close_comment_index+2]
62
+ end
63
+
64
+ dsl_commands << comment.gsub('<!--', '').gsub('-->', '').strip + "\n"
65
+
66
+ html_file_contents = html_file_contents.gsub(comment, '')
67
+ end
68
+
69
+ [dsl_commands, html_file_contents]
70
+ end
71
+
72
+ def self.extract_dsl_commands_from_haml(file_contents)
73
+ dsl_commands = ''
74
+
75
+ while starts_with_haml_comment?(file_contents)
76
+ first_open_comment_index = file_contents.lstrip.index('/')
77
+ first_close_comment_index = file_contents.index("\n")
78
+
79
+ unless first_close_comment_index.nil?
80
+ comment = file_contents[first_open_comment_index..first_close_comment_index]
81
+ end
82
+
83
+ dsl_commands << comment.gsub('/', '').strip + "\n"
84
+
85
+ file_contents = file_contents.gsub(comment, '')
86
+
87
+ while file_contents.start_with?(' ')
88
+ first_newline_index = file_contents.index("\n")
89
+ comment = file_contents[0..first_newline_index]
90
+ dsl_commands << comment.gsub('/', '').strip + "\n"
91
+ file_contents = file_contents.gsub(comment, '')
92
+ end
93
+ end
94
+
95
+ [dsl_commands, file_contents]
96
+ end
97
+
98
+ private
99
+
100
+ def self.starts_with_html_comment?(line)
101
+ line.lstrip.index('<!--') == 0
102
+ end
103
+
104
+ def self.starts_with_haml_comment?(line)
105
+ line.lstrip.index('/') == 0
106
+ end
107
+
108
+ def self.file_in_dir_below_overrides?(filename)
109
+ File.fnmatch?("**/overrides/**/#{File.basename(filename)}", filename)
110
+ end
111
+
112
+ def self.determine_virtual_path(filename)
113
+ result = ''
114
+ pathname = Pathname.new(filename)
115
+ pathname.ascend do |parent|
116
+ if parent.basename.to_s == 'overrides'
117
+ result = pathname.sub(parent.to_s + '/', '').dirname.to_s
118
+ break
119
+ end
120
+ end
121
+ result
122
+ end
123
+ end
124
+ end
125
+ end
@@ -23,18 +23,14 @@ module Deface
23
23
  def load_all(app)
24
24
  #clear overrides before reloading them
25
25
  app.config.deface.overrides.all.clear
26
+ Deface::DSL::Loader.register
26
27
 
27
- # check application for specified overrides paths
28
- override_paths = app.paths["app/overrides"]
29
- enumerate_and_load(override_paths, app.root)
30
-
31
- # check all railties / engines / extensions for overrides
32
- app.railties.all.each do |railtie|
28
+ # check all railties / engines / extensions / application for overrides
29
+ app.railties.all.dup.push(app).each do |railtie|
33
30
  next unless railtie.respond_to? :root
34
-
35
- override_paths = railtie.respond_to?(:paths) ? railtie.paths["app/overrides"] : nil
36
- enumerate_and_load(override_paths, railtie.root)
31
+ load_overrides(railtie)
37
32
  end
33
+
38
34
  end
39
35
 
40
36
  def early_check
@@ -46,15 +42,30 @@ module Deface
46
42
  end
47
43
 
48
44
  private
45
+
46
+ def load_overrides(railtie)
47
+ Override.current_railtie = railtie
48
+ paths = railtie.respond_to?(:paths) ? railtie.paths["app/overrides"] : nil
49
+ enumerate_and_load(paths, railtie.root)
50
+ end
51
+
49
52
  def enumerate_and_load(paths, root)
50
53
  paths ||= ["app/overrides"]
51
54
 
52
55
  paths.each do |path|
53
- Dir.glob(root.join path, "*.rb") do |c|
56
+ if Rails.version[0..2] == "3.2"
57
+ # add path to watchable_dir so Rails will call to_prepare on file changes
58
+ # allowing overrides to be updated / reloaded in development mode.
59
+ Rails.application.config.watchable_dirs[root.join(path).to_s] = [:rb, :deface]
60
+ end
61
+
62
+ Dir.glob(root.join path, "**/*.rb") do |c|
54
63
  Rails.application.config.cache_classes ? require(c) : load(c)
55
64
  end
65
+ Dir.glob(root.join path, "**/*.deface") do |c|
66
+ Rails.application.config.cache_classes ? require(c) : Deface::DSL::Loader.load(c)
67
+ end
56
68
  end
57
-
58
69
  end
59
70
  end
60
71
  end
@@ -40,25 +40,37 @@ module Deface
40
40
  # coverts { attributes into deface compatibily attributes
41
41
  def deface_attributes(attrs)
42
42
  return if attrs.nil?
43
+
43
44
  attrs.gsub! /\{|\}/, ''
44
- attrs = attrs.split(',')
45
45
 
46
- if attrs.join.include? "=>"
47
- attrs.map!{|a| a.split("=>").map(&:strip) }
48
- else
49
- attrs.map!{|a| a.split(": ").map(&:strip) }
46
+ attributes = {}
47
+ scanner = StringScanner.new(attrs)
48
+ scanner.scan(/\s+/)
49
+
50
+ until scanner.eos?
51
+ return unless key = scanner.scan(/:(\w*)|(["'])((?![\\#]|\2).|\\.)*\2|(\w*):/) #matches :key, 'key', "key" or key:
52
+ return unless scanner.scan(/\s*(=>)?\s*/) #match => or just white space
53
+ return unless value = scanner.scan(/(["'])((?![\\#]|\1).|\\.)*\1|[^\s,]*/) #match 'value', "value", value, @value, some-value
54
+ return unless scanner.scan(/\s*(?:,|$)\s*/)
55
+ attributes[key.to_s] = value
50
56
  end
51
57
 
52
- attrs.map! do |a|
53
- if a[1][0] != ?' && a[1][0] != ?"
54
- a[0] = %Q{"data-erb-#{a[0].gsub(/:|'|"/,'')}"}
55
- a[1] = %Q{"<%= #{a[1]} %>"}
58
+ attrs = []
59
+ attributes.each do |key, value|
60
+ #only need to convert non-literal values
61
+ if value[0] != ?' && value[0] != ?" && value[0] != ?:
62
+ key = %Q{"data-erb-#{key.gsub(/:|'|"/,'')}"}
63
+ value = %Q{"<%= #{value} %>"}
56
64
  end
57
65
 
58
- a
66
+ if key[-1] == ?:
67
+ attrs << "#{key} #{value}"
68
+ else
69
+ attrs << "#{key} => #{value}"
70
+ end
59
71
  end
60
72
 
61
- attrs.map{ |a| a.join " => " }.join(', ')
73
+ attrs.join(', ')
62
74
  end
63
75
 
64
76
  end
@@ -5,66 +5,17 @@ module Deface
5
5
  extend Applicator::ClassMethods
6
6
  extend Search::ClassMethods
7
7
 
8
- cattr_accessor :actions, :_early
9
- attr_accessor :args
8
+ cattr_accessor :actions, :sources, :_early, :current_railtie
9
+ attr_accessor :args, :parsed_document
10
10
 
11
11
  @@_early = []
12
12
  @@actions = [:remove, :replace, :replace_contents, :surround, :surround_contents, :insert_after, :insert_before, :insert_top, :insert_bottom, :set_attributes, :add_to_attributes, :remove_from_attributes]
13
- @@sources = [:text, :erb, :haml, :partial, :template]
13
+ @@sources = [:text, :erb, :haml, :partial, :template, :cut, :copy]
14
14
 
15
15
  # Initializes new override, you must supply only one Target, Action & Source
16
16
  # parameter for each override (and any number of Optional parameters).
17
17
  #
18
- # ==== Target
19
- #
20
- # * <tt>:virtual_path</tt> - The path of the template / partial where
21
- # the override should take effect eg: "shared/_person", "admin/posts/new"
22
- # this will apply to all controller actions that use the specified template
23
- #
24
- # ==== Action
25
- #
26
- # * <tt>:remove</tt> - Removes all elements that match the supplied selector
27
- # * <tt>:replace</tt> - Replaces all elements that match the supplied selector
28
- # * <tt>:replace_contents</tt> - Replaces the contents of all elements that match the supplied selector
29
- # * <tt>:surround</tt> - Surrounds all elements that match the supplied selector, expects replacement markup to contain <%= render_original %> placeholder
30
- # * <tt>:surround_contents</tt> - Surrounds the contents of all elements that match the supplied selector, expects replacement markup to contain <%= render_original %> placeholder
31
- # * <tt>:insert_after</tt> - Inserts after all elements that match the supplied selector
32
- # * <tt>:insert_before</tt> - Inserts before all elements that match the supplied selector
33
- # * <tt>:insert_top</tt> - Inserts inside all elements that match the supplied selector, before all existing child
34
- # * <tt>:insert_bottom</tt> - Inserts inside all elements that match the supplied selector, after all existing child
35
- # * <tt>:set_attributes</tt> - Sets attributes on all elements that match the supplied selector, replacing existing attribute value if present or adding if not. Expects :attributes option to be passed.
36
- # * <tt>:add_to_attributes</tt> - Appends value to attributes on all elements that match the supplied selector, adds attribute if not present. Expects :attributes option to be passed.
37
- # * <tt>:remove_from_attributes</tt> - Removes value from attributes on all elements that match the supplied selector. Expects :attributes option to be passed.
38
- #
39
- # ==== Source
40
- #
41
- # * <tt>:text</tt> - String containing markup
42
- # * <tt>:partial</tt> - Relative path to partial
43
- # * <tt>:template</tt> - Relative path to template
44
- #
45
- # ==== Optional
46
- #
47
- # * <tt>:name</tt> - Unique name for override so it can be identified and modified later.
48
- # This needs to be unique within the same :virtual_path
49
- # * <tt>:disabled</tt> - When set to true the override will not be applied.
50
- # * <tt>:original</tt> - String containing original markup that is being overridden.
51
- # If supplied Deface will log when the original markup changes, which helps highlight overrides that need
52
- # attention when upgrading versions of the source application. Only really warranted for :replace overrides.
53
- # NB: All whitespace is stripped before comparsion.
54
- # * <tt>:closing_selector</tt> - A second css selector targeting an end element, allowing you to select a range
55
- # of elements to apply an action against. The :closing_selector only supports the :replace, :remove and
56
- # :replace_contents actions, and the end element must be a sibling of the first/starting element. Note the CSS
57
- # general sibling selector (~) is used to match the first element after the opening selector.
58
- # * <tt>:sequence</tt> - Used to order the application of an override for a specific virtual path, helpful when
59
- # an override depends on another override being applied first.
60
- # Supports:
61
- # :sequence => n - where n is a positive or negative integer (lower numbers get applied first, default 100).
62
- # :sequence => {:before => "override_name"} - where "override_name" is the name of an override defined for the
63
- # same virutal_path, the current override will be appplied before
64
- # the named override passed.
65
- # :sequence => {:after => "override_name") - the current override will be applied after the named override passed.
66
- # * <tt>:attributes</tt> - A hash containing all the attributes to be set on the matched elements, eg: :attributes => {:class => "green", :title => "some string"}
67
- #
18
+ # See READme for more!
68
19
  def initialize(args, &content)
69
20
  if Rails.application.try(:config).try(:deface).try(:enabled)
70
21
  unless Rails.application.config.deface.try(:overrides)
@@ -110,6 +61,10 @@ module Deface
110
61
  raise(ArgumentError, ":action is invalid") if self.action.nil?
111
62
  end
112
63
 
64
+ #set loaded time (if not already present) for hash invalidation
65
+ @args[:updated_at] ||= Time.zone.now.to_f
66
+ @args[:railtie] = self.class.current_railtie
67
+
113
68
  self.class.all[virtual_key][name_key] = self
114
69
 
115
70
  expire_compiled_template
@@ -125,6 +80,10 @@ module Deface
125
80
  @args[:name]
126
81
  end
127
82
 
83
+ def railtie
84
+ @args[:railtie]
85
+ end
86
+
128
87
  def sequence
129
88
  return 100 unless @args.key?(:sequence)
130
89
  if @args[:sequence].is_a? Hash
@@ -167,22 +126,61 @@ module Deface
167
126
  end
168
127
 
169
128
  def source
170
- erb = if @args.key? :partial
129
+ erb = case source_argument
130
+ when :partial
171
131
  load_template_source(@args[:partial], true)
172
- elsif @args.key? :template
132
+ when :template
173
133
  load_template_source(@args[:template], false)
174
- elsif @args.key? :text
134
+ when :text
175
135
  @args[:text]
176
- elsif @args.key? :erb
136
+ when :erb
177
137
  @args[:erb]
178
- elsif @args.key?(:haml) && Rails.application.config.deface.haml_support
179
- haml_engine = Deface::HamlConverter.new(@args[:haml])
180
- haml_engine.render
138
+ when :cut
139
+ cut = @args[:cut]
140
+
141
+ if cut.is_a? Hash
142
+ starting, ending = self.class.select_endpoints(self.parsed_document, cut[:start], cut[:end])
143
+
144
+ range = self.class.select_range(starting, ending)
145
+ range.map &:remove
146
+
147
+ Deface::Parser.undo_erb_markup! range.map(&:to_s).join
148
+
149
+ else
150
+ Deface::Parser.undo_erb_markup! self.parsed_document.css(cut).first.remove.to_s.clone
151
+ end
152
+
153
+ when :copy
154
+ copy = @args[:copy]
155
+
156
+ if copy.is_a? Hash
157
+ starting, ending = self.class.select_endpoints(self.parsed_document, copy[:start], copy[:end])
158
+
159
+ range = self.class.select_range(starting, ending)
160
+
161
+ Deface::Parser.undo_erb_markup! range.map(&:to_s).join
162
+ else
163
+ Deface::Parser.undo_erb_markup! parsed_document.css(copy).first.to_s.clone
164
+ end
165
+
166
+ when :haml
167
+ if Rails.application.config.deface.haml_support
168
+ haml_engine = Deface::HamlConverter.new(@args[:haml])
169
+ haml_engine.render
170
+ else
171
+ raise Deface::NotSupportedError, "`#{self.name}` supplies :haml source, but haml_support is not detected."
172
+ end
181
173
  end
182
174
 
183
175
  erb
184
176
  end
185
177
 
178
+ # Returns a :symbol for the source argument present
179
+ #
180
+ def source_argument
181
+ @@sources.detect { |source| @args.key? source }
182
+ end
183
+
186
184
  def source_element
187
185
  Deface::Parser.convert(source.clone)
188
186
  end
@@ -193,27 +191,42 @@ module Deface
193
191
 
194
192
  def end_selector
195
193
  return nil if @args[:closing_selector].blank?
196
- "#{self.selector} ~ #{@args[:closing_selector]}"
194
+ @args[:closing_selector]
197
195
  end
198
196
 
199
197
  def attributes
200
198
  @args[:attributes] || []
201
199
  end
202
200
 
203
- def digest
204
- Digest::MD5.new.update(@args.to_s).hexdigest
201
+ # Alters digest of override to force view method
202
+ # recompilation (when source template/partial changes)
203
+ #
204
+ def touch
205
+ @args[:updated_at] = Time.zone.now.to_f
205
206
  end
206
207
 
207
- def self.all
208
- Rails.application.config.deface.overrides.all
208
+ # Creates MD5 hash of args sorted keys and values
209
+ # used to determine if an override has changed
210
+ #
211
+ def digest
212
+ Digest::MD5.new.update(@args.keys.map(&:to_s).sort.concat(@args.values.map(&:to_s).sort).join).hexdigest
209
213
  end
210
214
 
215
+ # Creates MD5 of all overrides that apply to a particular
216
+ # virtual_path, used in CompiledTemplates method name
217
+ # so we can support re-compiling of compiled method
218
+ # when overrides change. Only of use in production mode.
219
+ #
211
220
  def self.digest(details)
212
221
  overrides = self.find(details)
213
222
 
214
223
  Digest::MD5.new.update(overrides.inject('') { |digest, override| digest << override.digest }).hexdigest
215
224
  end
216
225
 
226
+ def self.all
227
+ Rails.application.config.deface.overrides.all
228
+ end
229
+
217
230
  private
218
231
 
219
232
  # check if method is compiled for the current virtual path
@@ -65,6 +65,10 @@ module Deface
65
65
 
66
66
  # catchs any overrides that we required manually
67
67
  app.config.deface.overrides.early_check
68
+
69
+ if Dir.glob(app.root.join("app/compiled_views", "**/*.erb")).present?
70
+ puts "[WARNING] Precompiled views present and Deface is enabled, this can result in overrides being applied twice."
71
+ end
68
72
  else
69
73
  # deface is disabled so clear any overrides
70
74
  # that might have been manually required