sass 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. checksums.yaml +7 -0
  2. data/CONTRIBUTING +1 -1
  3. data/MIT-LICENSE +2 -2
  4. data/README.md +29 -17
  5. data/Rakefile +43 -9
  6. data/VERSION +1 -1
  7. data/VERSION_DATE +1 -0
  8. data/VERSION_NAME +1 -1
  9. data/bin/sass +6 -1
  10. data/bin/sass-convert +6 -1
  11. data/bin/scss +6 -1
  12. data/ext/mkrf_conf.rb +27 -0
  13. data/lib/sass/cache_stores/base.rb +7 -3
  14. data/lib/sass/cache_stores/chain.rb +3 -2
  15. data/lib/sass/cache_stores/filesystem.rb +5 -7
  16. data/lib/sass/cache_stores/memory.rb +1 -1
  17. data/lib/sass/cache_stores/null.rb +2 -2
  18. data/lib/sass/callbacks.rb +2 -1
  19. data/lib/sass/css.rb +168 -53
  20. data/lib/sass/engine.rb +502 -174
  21. data/lib/sass/environment.rb +151 -111
  22. data/lib/sass/error.rb +7 -7
  23. data/lib/sass/exec.rb +176 -60
  24. data/lib/sass/features.rb +40 -0
  25. data/lib/sass/importers/base.rb +46 -7
  26. data/lib/sass/importers/deprecated_path.rb +51 -0
  27. data/lib/sass/importers/filesystem.rb +113 -30
  28. data/lib/sass/importers.rb +1 -0
  29. data/lib/sass/logger/base.rb +30 -0
  30. data/lib/sass/logger/log_level.rb +45 -0
  31. data/lib/sass/logger.rb +12 -0
  32. data/lib/sass/media.rb +213 -0
  33. data/lib/sass/plugin/compiler.rb +194 -104
  34. data/lib/sass/plugin/configuration.rb +18 -25
  35. data/lib/sass/plugin/merb.rb +1 -1
  36. data/lib/sass/plugin/staleness_checker.rb +37 -11
  37. data/lib/sass/plugin.rb +10 -13
  38. data/lib/sass/railtie.rb +2 -1
  39. data/lib/sass/repl.rb +5 -6
  40. data/lib/sass/script/css_lexer.rb +8 -4
  41. data/lib/sass/script/css_parser.rb +5 -2
  42. data/lib/sass/script/functions.rb +1547 -618
  43. data/lib/sass/script/lexer.rb +122 -72
  44. data/lib/sass/script/parser.rb +304 -135
  45. data/lib/sass/script/tree/funcall.rb +306 -0
  46. data/lib/sass/script/{interpolation.rb → tree/interpolation.rb} +43 -13
  47. data/lib/sass/script/tree/list_literal.rb +77 -0
  48. data/lib/sass/script/tree/literal.rb +45 -0
  49. data/lib/sass/script/tree/map_literal.rb +64 -0
  50. data/lib/sass/script/{node.rb → tree/node.rb} +30 -12
  51. data/lib/sass/script/{operation.rb → tree/operation.rb} +33 -21
  52. data/lib/sass/script/{string_interpolation.rb → tree/string_interpolation.rb} +14 -4
  53. data/lib/sass/script/{unary_operation.rb → tree/unary_operation.rb} +21 -9
  54. data/lib/sass/script/tree/variable.rb +57 -0
  55. data/lib/sass/script/tree.rb +15 -0
  56. data/lib/sass/script/value/arg_list.rb +36 -0
  57. data/lib/sass/script/value/base.rb +238 -0
  58. data/lib/sass/script/value/bool.rb +40 -0
  59. data/lib/sass/script/{color.rb → value/color.rb} +256 -74
  60. data/lib/sass/script/value/deprecated_false.rb +55 -0
  61. data/lib/sass/script/value/helpers.rb +155 -0
  62. data/lib/sass/script/value/list.rb +128 -0
  63. data/lib/sass/script/value/map.rb +70 -0
  64. data/lib/sass/script/value/null.rb +49 -0
  65. data/lib/sass/script/{number.rb → value/number.rb} +115 -62
  66. data/lib/sass/script/{string.rb → value/string.rb} +9 -11
  67. data/lib/sass/script/value.rb +12 -0
  68. data/lib/sass/script.rb +35 -9
  69. data/lib/sass/scss/css_parser.rb +2 -12
  70. data/lib/sass/scss/parser.rb +657 -230
  71. data/lib/sass/scss/rx.rb +17 -12
  72. data/lib/sass/scss/static_parser.rb +37 -6
  73. data/lib/sass/scss.rb +0 -1
  74. data/lib/sass/selector/abstract_sequence.rb +35 -3
  75. data/lib/sass/selector/comma_sequence.rb +29 -14
  76. data/lib/sass/selector/sequence.rb +371 -74
  77. data/lib/sass/selector/simple.rb +28 -13
  78. data/lib/sass/selector/simple_sequence.rb +163 -36
  79. data/lib/sass/selector.rb +138 -36
  80. data/lib/sass/shared.rb +3 -5
  81. data/lib/sass/source/map.rb +196 -0
  82. data/lib/sass/source/position.rb +39 -0
  83. data/lib/sass/source/range.rb +41 -0
  84. data/lib/sass/stack.rb +126 -0
  85. data/lib/sass/supports.rb +228 -0
  86. data/lib/sass/tree/at_root_node.rb +82 -0
  87. data/lib/sass/tree/comment_node.rb +34 -29
  88. data/lib/sass/tree/content_node.rb +9 -0
  89. data/lib/sass/tree/css_import_node.rb +60 -0
  90. data/lib/sass/tree/debug_node.rb +3 -3
  91. data/lib/sass/tree/directive_node.rb +33 -3
  92. data/lib/sass/tree/each_node.rb +9 -9
  93. data/lib/sass/tree/extend_node.rb +20 -6
  94. data/lib/sass/tree/for_node.rb +6 -6
  95. data/lib/sass/tree/function_node.rb +12 -4
  96. data/lib/sass/tree/if_node.rb +2 -15
  97. data/lib/sass/tree/import_node.rb +11 -5
  98. data/lib/sass/tree/media_node.rb +27 -11
  99. data/lib/sass/tree/mixin_def_node.rb +15 -4
  100. data/lib/sass/tree/mixin_node.rb +27 -7
  101. data/lib/sass/tree/node.rb +69 -35
  102. data/lib/sass/tree/prop_node.rb +47 -31
  103. data/lib/sass/tree/return_node.rb +4 -3
  104. data/lib/sass/tree/root_node.rb +20 -4
  105. data/lib/sass/tree/rule_node.rb +37 -26
  106. data/lib/sass/tree/supports_node.rb +38 -0
  107. data/lib/sass/tree/trace_node.rb +33 -0
  108. data/lib/sass/tree/variable_node.rb +10 -4
  109. data/lib/sass/tree/visitors/base.rb +5 -8
  110. data/lib/sass/tree/visitors/check_nesting.rb +67 -52
  111. data/lib/sass/tree/visitors/convert.rb +134 -53
  112. data/lib/sass/tree/visitors/cssize.rb +245 -51
  113. data/lib/sass/tree/visitors/deep_copy.rb +102 -0
  114. data/lib/sass/tree/visitors/extend.rb +68 -0
  115. data/lib/sass/tree/visitors/perform.rb +331 -105
  116. data/lib/sass/tree/visitors/set_options.rb +125 -0
  117. data/lib/sass/tree/visitors/to_css.rb +259 -95
  118. data/lib/sass/tree/warn_node.rb +3 -3
  119. data/lib/sass/tree/while_node.rb +3 -3
  120. data/lib/sass/util/cross_platform_random.rb +19 -0
  121. data/lib/sass/util/multibyte_string_scanner.rb +157 -0
  122. data/lib/sass/util/normalized_map.rb +130 -0
  123. data/lib/sass/util/ordered_hash.rb +192 -0
  124. data/lib/sass/util/subset_map.rb +11 -2
  125. data/lib/sass/util/test.rb +9 -0
  126. data/lib/sass/util.rb +565 -39
  127. data/lib/sass/version.rb +27 -15
  128. data/lib/sass.rb +39 -4
  129. data/test/sass/cache_test.rb +15 -0
  130. data/test/sass/compiler_test.rb +223 -0
  131. data/test/sass/conversion_test.rb +901 -107
  132. data/test/sass/css2sass_test.rb +94 -0
  133. data/test/sass/engine_test.rb +1059 -164
  134. data/test/sass/exec_test.rb +86 -0
  135. data/test/sass/extend_test.rb +933 -837
  136. data/test/sass/fixtures/test_staleness_check_across_importers.css +1 -0
  137. data/test/sass/fixtures/test_staleness_check_across_importers.scss +1 -0
  138. data/test/sass/functions_test.rb +995 -136
  139. data/test/sass/importer_test.rb +338 -18
  140. data/test/sass/logger_test.rb +58 -0
  141. data/test/sass/more_results/more_import.css +2 -2
  142. data/test/sass/plugin_test.rb +114 -30
  143. data/test/sass/results/cached_import_option.css +3 -0
  144. data/test/sass/results/filename_fn.css +3 -0
  145. data/test/sass/results/import.css +2 -2
  146. data/test/sass/results/import_charset.css +1 -0
  147. data/test/sass/results/import_charset_1_8.css +1 -0
  148. data/test/sass/results/import_charset_ibm866.css +1 -0
  149. data/test/sass/results/import_content.css +1 -0
  150. data/test/sass/results/script.css +1 -1
  151. data/test/sass/results/scss_import.css +2 -2
  152. data/test/sass/results/units.css +2 -2
  153. data/test/sass/script_conversion_test.rb +43 -1
  154. data/test/sass/script_test.rb +380 -36
  155. data/test/sass/scss/css_test.rb +257 -75
  156. data/test/sass/scss/scss_test.rb +2322 -110
  157. data/test/sass/source_map_test.rb +887 -0
  158. data/test/sass/templates/_cached_import_option_partial.scss +1 -0
  159. data/test/sass/templates/_double_import_loop2.sass +1 -0
  160. data/test/sass/templates/_filename_fn_import.scss +11 -0
  161. data/test/sass/templates/_imported_content.sass +3 -0
  162. data/test/sass/templates/_same_name_different_partiality.scss +1 -0
  163. data/test/sass/templates/bork5.sass +3 -0
  164. data/test/sass/templates/cached_import_option.scss +3 -0
  165. data/test/sass/templates/double_import_loop1.sass +1 -0
  166. data/test/sass/templates/filename_fn.scss +18 -0
  167. data/test/sass/templates/import_charset.sass +2 -0
  168. data/test/sass/templates/import_charset_1_8.sass +2 -0
  169. data/test/sass/templates/import_charset_ibm866.sass +2 -0
  170. data/test/sass/templates/import_content.sass +4 -0
  171. data/test/sass/templates/same_name_different_ext.sass +2 -0
  172. data/test/sass/templates/same_name_different_ext.scss +1 -0
  173. data/test/sass/templates/same_name_different_partiality.scss +1 -0
  174. data/test/sass/templates/single_import_loop.sass +1 -0
  175. data/test/sass/templates/subdir/import_up1.scss +1 -0
  176. data/test/sass/templates/subdir/import_up2.scss +1 -0
  177. data/test/sass/test_helper.rb +1 -1
  178. data/test/sass/util/multibyte_string_scanner_test.rb +147 -0
  179. data/test/sass/util/normalized_map_test.rb +51 -0
  180. data/test/sass/util_test.rb +183 -0
  181. data/test/sass/value_helpers_test.rb +181 -0
  182. data/test/test_helper.rb +45 -5
  183. data/vendor/listen/CHANGELOG.md +228 -0
  184. data/vendor/listen/CONTRIBUTING.md +38 -0
  185. data/vendor/listen/Gemfile +30 -0
  186. data/vendor/listen/Guardfile +8 -0
  187. data/vendor/{fssm → listen}/LICENSE +1 -1
  188. data/vendor/listen/README.md +315 -0
  189. data/vendor/listen/Rakefile +47 -0
  190. data/vendor/listen/Vagrantfile +96 -0
  191. data/vendor/listen/lib/listen/adapter.rb +214 -0
  192. data/vendor/listen/lib/listen/adapters/bsd.rb +112 -0
  193. data/vendor/listen/lib/listen/adapters/darwin.rb +85 -0
  194. data/vendor/listen/lib/listen/adapters/linux.rb +113 -0
  195. data/vendor/listen/lib/listen/adapters/polling.rb +67 -0
  196. data/vendor/listen/lib/listen/adapters/windows.rb +87 -0
  197. data/vendor/listen/lib/listen/dependency_manager.rb +126 -0
  198. data/vendor/listen/lib/listen/directory_record.rb +371 -0
  199. data/vendor/listen/lib/listen/listener.rb +225 -0
  200. data/vendor/listen/lib/listen/multi_listener.rb +143 -0
  201. data/vendor/listen/lib/listen/turnstile.rb +28 -0
  202. data/vendor/listen/lib/listen/version.rb +3 -0
  203. data/vendor/listen/lib/listen.rb +40 -0
  204. data/vendor/listen/listen.gemspec +22 -0
  205. data/vendor/listen/spec/listen/adapter_spec.rb +183 -0
  206. data/vendor/listen/spec/listen/adapters/bsd_spec.rb +36 -0
  207. data/vendor/listen/spec/listen/adapters/darwin_spec.rb +37 -0
  208. data/vendor/listen/spec/listen/adapters/linux_spec.rb +47 -0
  209. data/vendor/listen/spec/listen/adapters/polling_spec.rb +68 -0
  210. data/vendor/listen/spec/listen/adapters/windows_spec.rb +30 -0
  211. data/vendor/listen/spec/listen/dependency_manager_spec.rb +107 -0
  212. data/vendor/listen/spec/listen/directory_record_spec.rb +1225 -0
  213. data/vendor/listen/spec/listen/listener_spec.rb +169 -0
  214. data/vendor/listen/spec/listen/multi_listener_spec.rb +174 -0
  215. data/vendor/listen/spec/listen/turnstile_spec.rb +56 -0
  216. data/vendor/listen/spec/listen_spec.rb +73 -0
  217. data/vendor/listen/spec/spec_helper.rb +21 -0
  218. data/vendor/listen/spec/support/adapter_helper.rb +629 -0
  219. data/vendor/listen/spec/support/directory_record_helper.rb +55 -0
  220. data/vendor/listen/spec/support/fixtures_helper.rb +29 -0
  221. data/vendor/listen/spec/support/listeners_helper.rb +156 -0
  222. data/vendor/listen/spec/support/platform_helper.rb +15 -0
  223. metadata +344 -271
  224. data/lib/sass/less.rb +0 -382
  225. data/lib/sass/script/bool.rb +0 -18
  226. data/lib/sass/script/funcall.rb +0 -162
  227. data/lib/sass/script/list.rb +0 -76
  228. data/lib/sass/script/literal.rb +0 -245
  229. data/lib/sass/script/variable.rb +0 -54
  230. data/lib/sass/scss/sass_parser.rb +0 -11
  231. data/test/sass/less_conversion_test.rb +0 -653
  232. data/vendor/fssm/README.markdown +0 -55
  233. data/vendor/fssm/Rakefile +0 -59
  234. data/vendor/fssm/VERSION.yml +0 -5
  235. data/vendor/fssm/example.rb +0 -9
  236. data/vendor/fssm/fssm.gemspec +0 -77
  237. data/vendor/fssm/lib/fssm/backends/fsevents.rb +0 -36
  238. data/vendor/fssm/lib/fssm/backends/inotify.rb +0 -26
  239. data/vendor/fssm/lib/fssm/backends/polling.rb +0 -25
  240. data/vendor/fssm/lib/fssm/backends/rubycocoa/fsevents.rb +0 -131
  241. data/vendor/fssm/lib/fssm/monitor.rb +0 -26
  242. data/vendor/fssm/lib/fssm/path.rb +0 -91
  243. data/vendor/fssm/lib/fssm/pathname.rb +0 -502
  244. data/vendor/fssm/lib/fssm/state/directory.rb +0 -57
  245. data/vendor/fssm/lib/fssm/state/file.rb +0 -24
  246. data/vendor/fssm/lib/fssm/support.rb +0 -63
  247. data/vendor/fssm/lib/fssm/tree.rb +0 -176
  248. data/vendor/fssm/lib/fssm.rb +0 -33
  249. data/vendor/fssm/profile/prof-cache.rb +0 -40
  250. data/vendor/fssm/profile/prof-fssm-pathname.html +0 -1231
  251. data/vendor/fssm/profile/prof-pathname.rb +0 -68
  252. data/vendor/fssm/profile/prof-plain-pathname.html +0 -988
  253. data/vendor/fssm/profile/prof.html +0 -2379
  254. data/vendor/fssm/spec/path_spec.rb +0 -75
  255. data/vendor/fssm/spec/root/duck/quack.txt +0 -0
  256. data/vendor/fssm/spec/root/file.css +0 -0
  257. data/vendor/fssm/spec/root/file.rb +0 -0
  258. data/vendor/fssm/spec/root/file.yml +0 -0
  259. data/vendor/fssm/spec/root/moo/cow.txt +0 -0
  260. data/vendor/fssm/spec/spec_helper.rb +0 -14
@@ -0,0 +1,371 @@
1
+ require 'set'
2
+ require 'find'
3
+ require 'digest/sha1'
4
+
5
+ module Listen
6
+
7
+ # The directory record stores information about
8
+ # a directory and keeps track of changes to
9
+ # the structure of its childs.
10
+ #
11
+ class DirectoryRecord
12
+ attr_reader :directory, :paths, :sha1_checksums
13
+
14
+ DEFAULT_IGNORED_DIRECTORIES = %w[.rbx .bundle .git .svn log tmp vendor]
15
+
16
+ DEFAULT_IGNORED_EXTENSIONS = %w[.DS_Store]
17
+
18
+ # Defines the used precision based on the type of mtime returned by the
19
+ # system (whether its in milliseconds or just seconds)
20
+ #
21
+ begin
22
+ HIGH_PRECISION_SUPPORTED = File.mtime(__FILE__).to_f.to_s[-2..-1] != '.0'
23
+ rescue
24
+ HIGH_PRECISION_SUPPORTED = false
25
+ end
26
+
27
+ # Data structure used to save meta data about a path
28
+ #
29
+ MetaData = Struct.new(:type, :mtime)
30
+
31
+ # Class methods
32
+ #
33
+ class << self
34
+
35
+ # Creates the ignoring patterns from the default ignored
36
+ # directories and extensions. It memoizes the generated patterns
37
+ # to avoid unnecessary computation.
38
+ #
39
+ def generate_default_ignoring_patterns
40
+ @@default_ignoring_patterns ||= Array.new.tap do |default_patterns|
41
+ # Add directories
42
+ ignored_directories = DEFAULT_IGNORED_DIRECTORIES.map { |d| Regexp.escape(d) }
43
+ default_patterns << %r{^(?:#{ignored_directories.join('|')})/}
44
+
45
+ # Add extensions
46
+ ignored_extensions = DEFAULT_IGNORED_EXTENSIONS.map { |e| Regexp.escape(e) }
47
+ default_patterns << %r{(?:#{ignored_extensions.join('|')})$}
48
+ end
49
+ end
50
+ end
51
+
52
+ # Initializes a directory record.
53
+ #
54
+ # @option [String] directory the directory to keep track of
55
+ #
56
+ def initialize(directory)
57
+ raise ArgumentError, "The path '#{directory}' is not a directory!" unless File.directory?(directory)
58
+
59
+ @directory = directory
60
+ @ignoring_patterns = Set.new
61
+ @filtering_patterns = Set.new
62
+ @sha1_checksums = Hash.new
63
+
64
+ @ignoring_patterns.merge(DirectoryRecord.generate_default_ignoring_patterns)
65
+ end
66
+
67
+ # Returns the ignoring patterns in the record
68
+ #
69
+ # @return [Array<Regexp>] the ignoring patterns
70
+ #
71
+ def ignoring_patterns
72
+ @ignoring_patterns.to_a
73
+ end
74
+
75
+ # Returns the filtering patterns used in the record to know
76
+ # which paths should be stored.
77
+ #
78
+ # @return [Array<Regexp>] the filtering patterns
79
+ #
80
+ def filtering_patterns
81
+ @filtering_patterns.to_a
82
+ end
83
+
84
+ # Adds ignoring patterns to the record.
85
+ #
86
+ # @example Ignore some paths
87
+ # ignore %r{^ignored/path/}, /man/
88
+ #
89
+ # @param [Regexp] regexp a pattern for ignoring paths
90
+ #
91
+ def ignore(*regexps)
92
+ @ignoring_patterns.merge(regexps)
93
+ end
94
+
95
+ # Replaces ignoring patterns in the record.
96
+ #
97
+ # @example Ignore only these paths
98
+ # ignore! %r{^ignored/path/}, /man/
99
+ #
100
+ # @param [Regexp] regexp a pattern for ignoring paths
101
+ #
102
+ def ignore!(*regexps)
103
+ @ignoring_patterns.replace(regexps)
104
+ end
105
+
106
+ # Adds filtering patterns to the listener.
107
+ #
108
+ # @example Filter some files
109
+ # ignore /\.txt$/, /.*\.zip/
110
+ #
111
+ # @param [Regexp] regexp a pattern for filtering paths
112
+ #
113
+ def filter(*regexps)
114
+ @filtering_patterns.merge(regexps)
115
+ end
116
+
117
+ # Replaces filtering patterns in the listener.
118
+ #
119
+ # @example Filter only these files
120
+ # ignore /\.txt$/, /.*\.zip/
121
+ #
122
+ # @param [Regexp] regexp a pattern for filtering paths
123
+ #
124
+ def filter!(*regexps)
125
+ @filtering_patterns.replace(regexps)
126
+ end
127
+
128
+ # Returns whether a path should be ignored or not.
129
+ #
130
+ # @param [String] path the path to test.
131
+ #
132
+ # @return [Boolean]
133
+ #
134
+ def ignored?(path)
135
+ path = relative_to_base(path)
136
+ @ignoring_patterns.any? { |pattern| pattern =~ path }
137
+ end
138
+
139
+ # Returns whether a path should be filtered or not.
140
+ #
141
+ # @param [String] path the path to test.
142
+ #
143
+ # @return [Boolean]
144
+ #
145
+ def filtered?(path)
146
+ # When no filtering patterns are set, ALL files are stored.
147
+ return true if @filtering_patterns.empty?
148
+
149
+ path = relative_to_base(path)
150
+ @filtering_patterns.any? { |pattern| pattern =~ path }
151
+ end
152
+
153
+ # Finds the paths that should be stored and adds them
154
+ # to the paths' hash.
155
+ #
156
+ def build
157
+ @paths = Hash.new { |h, k| h[k] = Hash.new }
158
+ important_paths { |path| insert_path(path) }
159
+ end
160
+
161
+ # Detects changes in the passed directories, updates
162
+ # the record with the new changes and returns the changes
163
+ #
164
+ # @param [Array] directories the list of directories scan for changes
165
+ # @param [Hash] options
166
+ # @option options [Boolean] recursive scan all sub-directories recursively
167
+ # @option options [Boolean] relative_paths whether or not to use relative paths for changes
168
+ #
169
+ # @return [Hash<Array>] the changes
170
+ #
171
+ def fetch_changes(directories, options = {})
172
+ @changes = { :modified => [], :added => [], :removed => [] }
173
+ directories = directories.sort_by { |el| el.length }.reverse # diff sub-dir first
174
+
175
+ directories.each do |directory|
176
+ next unless directory[@directory] # Path is or inside directory
177
+ detect_modifications_and_removals(directory, options)
178
+ detect_additions(directory, options)
179
+ end
180
+
181
+ @changes
182
+ end
183
+
184
+ # Converts an absolute path to a path that's relative to the base directory.
185
+ #
186
+ # @param [String] path the path to convert
187
+ #
188
+ # @return [String] the relative path
189
+ #
190
+ def relative_to_base(path)
191
+ return nil unless path[@directory]
192
+ path = path.force_encoding("BINARY") if path.respond_to?(:force_encoding)
193
+ path.sub(%r{^#{Regexp.quote(@directory)}#{File::SEPARATOR}?}, '')
194
+ end
195
+
196
+ private
197
+
198
+ # Detects modifications and removals recursively in a directory.
199
+ #
200
+ # @note Modifications detection begins by checking the modification time (mtime)
201
+ # of files and then by checking content changes (using SHA1-checksum)
202
+ # when the mtime of files is not changed.
203
+ #
204
+ # @param [String] directory the path to analyze
205
+ # @param [Hash] options
206
+ # @option options [Boolean] recursive scan all sub-directories recursively
207
+ # @option options [Boolean] relative_paths whether or not to use relative paths for changes
208
+ #
209
+ def detect_modifications_and_removals(directory, options = {})
210
+ @paths[directory].each do |basename, meta_data|
211
+ path = File.join(directory, basename)
212
+
213
+ case meta_data.type
214
+ when 'Dir'
215
+ if File.directory?(path)
216
+ detect_modifications_and_removals(path, options) if options[:recursive]
217
+ else
218
+ detect_modifications_and_removals(path, { :recursive => true }.merge(options))
219
+ @paths[directory].delete(basename)
220
+ @paths.delete("#{directory}/#{basename}")
221
+ end
222
+ when 'File'
223
+ if File.exist?(path)
224
+ new_mtime = mtime_of(path)
225
+
226
+ # First check if we are in the same second (to update checksums)
227
+ # before checking the time difference
228
+ if (meta_data.mtime.to_i == new_mtime.to_i && content_modified?(path)) || meta_data.mtime < new_mtime
229
+ # Update the sha1 checksum of the file
230
+ insert_sha1_checksum(path)
231
+
232
+ # Update the meta data of the file
233
+ meta_data.mtime = new_mtime
234
+ @paths[directory][basename] = meta_data
235
+
236
+ @changes[:modified] << (options[:relative_paths] ? relative_to_base(path) : path)
237
+ end
238
+ else
239
+ @paths[directory].delete(basename)
240
+ @sha1_checksums.delete(path)
241
+ @changes[:removed] << (options[:relative_paths] ? relative_to_base(path) : path)
242
+ end
243
+ end
244
+ end
245
+ end
246
+
247
+ # Detects additions in a directory.
248
+ #
249
+ # @param [String] directory the path to analyze
250
+ # @param [Hash] options
251
+ # @option options [Boolean] recursive scan all sub-directories recursively
252
+ # @option options [Boolean] relative_paths whether or not to use relative paths for changes
253
+ #
254
+ def detect_additions(directory, options = {})
255
+ # Don't process removed directories
256
+ return unless File.exist?(directory)
257
+
258
+ Find.find(directory) do |path|
259
+ next if path == @directory
260
+
261
+ if File.directory?(path)
262
+ # Add a trailing slash to directories when checking if a directory is
263
+ # ignored to optimize finding them as Find.find doesn't.
264
+ if ignored?(path + File::SEPARATOR) || (directory != path && (!options[:recursive] && existing_path?(path)))
265
+ Find.prune # Don't look any further into this directory.
266
+ else
267
+ insert_path(path)
268
+ end
269
+ elsif !ignored?(path) && filtered?(path) && !existing_path?(path)
270
+ if File.file?(path)
271
+ @changes[:added] << (options[:relative_paths] ? relative_to_base(path) : path)
272
+ insert_path(path)
273
+ end
274
+ end
275
+ end
276
+ end
277
+
278
+ # Returns whether or not a file's content has been modified by
279
+ # comparing the SHA1-checksum to a stored one.
280
+ # Ensure that the SHA1-checksum is inserted to the sha1_checksums
281
+ # array for later comparaison if false.
282
+ #
283
+ # @param [String] path the file path
284
+ #
285
+ def content_modified?(path)
286
+ @sha1_checksum = sha1_checksum(path)
287
+ if @sha1_checksums[path] == @sha1_checksum || !@sha1_checksums.key?(path)
288
+ insert_sha1_checksum(path)
289
+ false
290
+ else
291
+ true
292
+ end
293
+ end
294
+
295
+ # Inserts a SHA1-checksum path in @SHA1-checksums hash.
296
+ #
297
+ # @param [String] path the SHA1-checksum path to insert in @sha1_checksums.
298
+ #
299
+ def insert_sha1_checksum(path)
300
+ if @sha1_checksum ||= sha1_checksum(path)
301
+ @sha1_checksums[path] = @sha1_checksum
302
+ @sha1_checksum = nil
303
+ end
304
+ end
305
+
306
+ # Returns the SHA1-checksum for the file path.
307
+ #
308
+ # @param [String] path the file path
309
+ #
310
+ def sha1_checksum(path)
311
+ Digest::SHA1.file(path).to_s
312
+ rescue Errno::EACCES, Errno::ENOENT, Errno::ENXIO, Errno::EOPNOTSUPP
313
+ nil
314
+ end
315
+
316
+ # Traverses the base directory looking for paths that should
317
+ # be stored; thus paths that are filters or not ignored.
318
+ #
319
+ # @yield [path] an important path
320
+ #
321
+ def important_paths
322
+ Find.find(@directory) do |path|
323
+ next if path == @directory
324
+
325
+ if File.directory?(path)
326
+ # Add a trailing slash to directories when checking if a directory is
327
+ # ignored to optimize finding them as Find.find doesn't.
328
+ if ignored?(path + File::SEPARATOR)
329
+ Find.prune # Don't look any further into this directory.
330
+ else
331
+ yield(path)
332
+ end
333
+ elsif !ignored?(path) && filtered?(path)
334
+ yield(path)
335
+ end
336
+ end
337
+ end
338
+
339
+ # Inserts a path with its type (Dir or File) in paths hash.
340
+ #
341
+ # @param [String] path the path to insert in @paths.
342
+ #
343
+ def insert_path(path)
344
+ meta_data = MetaData.new
345
+ meta_data.type = File.directory?(path) ? 'Dir' : 'File'
346
+ meta_data.mtime = mtime_of(path) unless meta_data.type == 'Dir' # mtimes of dirs are not used yet
347
+ @paths[File.dirname(path)][File.basename(path)] = meta_data
348
+ rescue Errno::ENOENT
349
+ end
350
+
351
+ # Returns whether or not a path exists in the paths hash.
352
+ #
353
+ # @param [String] path the path to check
354
+ #
355
+ # @return [Boolean]
356
+ #
357
+ def existing_path?(path)
358
+ @paths[File.dirname(path)][File.basename(path)] != nil
359
+ end
360
+
361
+ # Returns the modification time of a file based on the precision defined by the system
362
+ #
363
+ # @param [String] file the file for which the mtime must be returned
364
+ #
365
+ # @return [Fixnum, Float] the mtime of the file
366
+ #
367
+ def mtime_of(file)
368
+ File.lstat(file).mtime.send(HIGH_PRECISION_SUPPORTED ? :to_f : :to_i)
369
+ end
370
+ end
371
+ end
@@ -0,0 +1,225 @@
1
+ require 'pathname'
2
+
3
+ module Listen
4
+ class Listener
5
+ attr_reader :directory, :directory_record, :adapter
6
+
7
+ # The default value for using relative paths in the callback.
8
+ DEFAULT_TO_RELATIVE_PATHS = false
9
+
10
+ # Initializes the directory listener.
11
+ #
12
+ # @param [String] directory the directory to listen to
13
+ # @param [Hash] options the listen options
14
+ # @option options [Regexp] ignore a pattern for ignoring paths
15
+ # @option options [Regexp] filter a pattern for filtering paths
16
+ # @option options [Float] latency the delay between checking for changes in seconds
17
+ # @option options [Boolean] relative_paths whether or not to use relative-paths in the callback
18
+ # @option options [Boolean] force_polling whether to force the polling adapter or not
19
+ # @option options [String, Boolean] polling_fallback_message to change polling fallback message or remove it
20
+ #
21
+ # @yield [modified, added, removed] the changed files
22
+ # @yieldparam [Array<String>] modified the list of modified files
23
+ # @yieldparam [Array<String>] added the list of added files
24
+ # @yieldparam [Array<String>] removed the list of removed files
25
+ #
26
+ def initialize(directory, options = {}, &block)
27
+ @block = block
28
+ @directory = Pathname.new(directory).realpath.to_s
29
+ @directory_record = DirectoryRecord.new(@directory)
30
+ @use_relative_paths = DEFAULT_TO_RELATIVE_PATHS
31
+
32
+ @use_relative_paths = options.delete(:relative_paths) if options[:relative_paths]
33
+ @directory_record.ignore(*options.delete(:ignore)) if options[:ignore]
34
+ @directory_record.filter(*options.delete(:filter)) if options[:filter]
35
+
36
+ @adapter_options = options
37
+ end
38
+
39
+ # Starts the listener by initializing the adapter and building
40
+ # the directory record concurrently, then it starts the adapter to watch
41
+ # for changes.
42
+ #
43
+ # @param [Boolean] blocking whether or not to block the current thread after starting
44
+ #
45
+ def start(blocking = true)
46
+ t = Thread.new { @directory_record.build }
47
+ @adapter = initialize_adapter
48
+ t.join
49
+ @adapter.start(blocking)
50
+ end
51
+
52
+ # Stops the listener.
53
+ #
54
+ def stop
55
+ @adapter.stop
56
+ end
57
+
58
+ # Pauses the listener.
59
+ #
60
+ # @return [Listen::Listener] the listener
61
+ #
62
+ def pause
63
+ @adapter.paused = true
64
+ self
65
+ end
66
+
67
+ # Unpauses the listener.
68
+ #
69
+ # @return [Listen::Listener] the listener
70
+ #
71
+ def unpause
72
+ @directory_record.build
73
+ @adapter.paused = false
74
+ self
75
+ end
76
+
77
+ # Returns whether the listener is paused or not.
78
+ #
79
+ # @return [Boolean] adapter paused status
80
+ #
81
+ def paused?
82
+ !!@adapter && @adapter.paused == true
83
+ end
84
+
85
+ # Adds ignoring patterns to the listener.
86
+ #
87
+ # @param (see Listen::DirectoryRecord#ignore)
88
+ #
89
+ # @return [Listen::Listener] the listener
90
+ #
91
+ def ignore(*regexps)
92
+ @directory_record.ignore(*regexps)
93
+ self
94
+ end
95
+
96
+ # Replaces ignoring patterns in the listener.
97
+ #
98
+ # @param (see Listen::DirectoryRecord#ignore!)
99
+ #
100
+ # @return [Listen::Listener] the listener
101
+ #
102
+ def ignore!(*regexps)
103
+ @directory_record.ignore!(*regexps)
104
+ self
105
+ end
106
+
107
+ # Adds filtering patterns to the listener.
108
+ #
109
+ # @param (see Listen::DirectoryRecord#filter)
110
+ #
111
+ # @return [Listen::Listener] the listener
112
+ #
113
+ def filter(*regexps)
114
+ @directory_record.filter(*regexps)
115
+ self
116
+ end
117
+
118
+ # Replacing filtering patterns in the listener.
119
+ #
120
+ # @param (see Listen::DirectoryRecord#filter!)
121
+ #
122
+ # @return [Listen::Listener] the listener
123
+ #
124
+ def filter!(*regexps)
125
+ @directory_record.filter!(*regexps)
126
+ self
127
+ end
128
+
129
+ # Sets the latency for the adapter. This is a helper method
130
+ # to simplify changing the latency directly from the listener.
131
+ #
132
+ # @example Wait 0.5 seconds each time before checking changes
133
+ # latency 0.5
134
+ #
135
+ # @param [Float] seconds the amount of delay, in seconds
136
+ #
137
+ # @return [Listen::Listener] the listener
138
+ #
139
+ def latency(seconds)
140
+ @adapter_options[:latency] = seconds
141
+ self
142
+ end
143
+
144
+ # Sets whether the use of the polling adapter
145
+ # should be forced or not.
146
+ #
147
+ # @example Forcing the use of the polling adapter
148
+ # force_polling true
149
+ #
150
+ # @param [Boolean] value whether to force the polling adapter or not
151
+ #
152
+ # @return [Listen::Listener] the listener
153
+ #
154
+ def force_polling(value)
155
+ @adapter_options[:force_polling] = value
156
+ self
157
+ end
158
+
159
+ # Sets whether the paths in the callback should be
160
+ # relative or absolute.
161
+ #
162
+ # @example Enabling relative paths in the callback
163
+ # relative_paths true
164
+ #
165
+ # @param [Boolean] value whether to enable relative paths in the callback or not
166
+ #
167
+ # @return [Listen::Listener] the listener
168
+ #
169
+ def relative_paths(value)
170
+ @use_relative_paths = value
171
+ self
172
+ end
173
+
174
+ # Defines a custom polling fallback message of disable it.
175
+ #
176
+ # @example Disabling the polling fallback message
177
+ # polling_fallback_message false
178
+ #
179
+ # @param [String, Boolean] value to change polling fallback message or remove it
180
+ #
181
+ # @return [Listen::Listener] the listener
182
+ #
183
+ def polling_fallback_message(value)
184
+ @adapter_options[:polling_fallback_message] = value
185
+ self
186
+ end
187
+
188
+ # Sets the callback that gets called on changes.
189
+ #
190
+ # @example Assign a callback to be called on changes
191
+ # callback = lambda { |modified, added, removed| ... }
192
+ # change &callback
193
+ #
194
+ # @param [Proc] block the callback proc
195
+ #
196
+ # @return [Listen::Listener] the listener
197
+ #
198
+ def change(&block) # modified, added, removed
199
+ @block = block
200
+ self
201
+ end
202
+
203
+ # Runs the callback passing it the changes if there are any.
204
+ #
205
+ # @param (see Listen::DirectoryRecord#fetch_changes)
206
+ #
207
+ def on_change(directories, options = {})
208
+ changes = @directory_record.fetch_changes(directories, options.merge(
209
+ :relative_paths => @use_relative_paths
210
+ ))
211
+ unless changes.values.all? { |paths| paths.empty? }
212
+ @block.call(changes[:modified],changes[:added],changes[:removed])
213
+ end
214
+ end
215
+
216
+ private
217
+
218
+ # Initializes an adapter passing it the callback and adapters' options.
219
+ #
220
+ def initialize_adapter
221
+ callback = lambda { |changed_dirs, options| self.on_change(changed_dirs, options) }
222
+ Adapter.select_and_initialize(@directory, @adapter_options, &callback)
223
+ end
224
+ end
225
+ end