Skip to content
This repository
Browse code

[sprockets] continuing explanation of hike finding

  • Loading branch information...
commit 23101647cc84ab350b24c277f8f7cb7dd240cd99 1 parent 69c62ac
Ryan Bigg authored September 14, 2011
155  sprockets/sprockets.markdown
Source Rendered
@@ -1047,7 +1047,46 @@ This `@trail` instance variable is set up as the first thing in the `initialize`
1047 1047
     def initialize(root = ".")
1048 1048
       @trail = Hike::Trail.new(root)
1049 1049
 
1050  
-Where the `root` argument here is the root of the Rails application, exactly the same as `Rails.root` returns. The `find` method itself is defined within the Hike gem like this:
  1050
+Where the `root` argument here is the root of the Rails application, exactly the same as `Rails.root` returns. The `initialize` method for `Hike::Trail` is defined within Hike in `lib/hike/trail.rb` like this:
  1051
+
  1052
+
  1053
+
  1054
+**hike: lib/hike/trail.rb, 6 lines, beginning line 48**
  1055
+    
  1056
+    def initialize(root = ".")
  1057
+      @root       = Pathname.new(root).expand_path
  1058
+      @paths      = Paths.new(@root)
  1059
+      @extensions = Extensions.new
  1060
+      @aliases    = Hash.new { |h, k| h[k] = Extensions.new }
  1061
+    end
  1062
+
  1063
+The `root` method that is passed in is converted to a `Pathname` object if it isn't one already, and then `expand_path` is called on it so that it can store the absolute path for hiking. 
  1064
+
  1065
+Next up, new `Paths` object (`@paths`) is created for this root, which is used to track a collection of the paths of where to find files within the `@root`.  
  1066
+
  1067
+The `@extensions` method is much the same as the `@paths` object, but tracks the extensions which files are being found for. The `@aliases` hash will contain fallbacks for other extensions that are known to Hike. The documentation for the `attr_reader` for this setting explains it quite well:
  1068
+
  1069
+
  1070
+
  1071
+**hike: lib/hike/trail.rb, 12 lines, beginning line 31**
  1072
+    
  1073
+    # `Index#aliases` is a mutable `Hash` mapping an extension to
  1074
+    # an `Array` of aliases.
  1075
+    #
  1076
+    #   trail = Hike::Trail.new
  1077
+    #   trail.paths.push "~/Projects/hike/site"
  1078
+    #   trail.aliases['.htm']   = 'html'
  1079
+    #   trail.aliases['.xhtml'] = 'html'
  1080
+    #   trail.aliases['.php']   = 'html'
  1081
+    #
  1082
+    # Aliases provide a fallback when the primary extension is not
  1083
+    # matched. In the example above, a lookup for "foo.html" will
  1084
+    # check for the existence of "foo.htm", "foo.xhtml", or "foo.php".
  1085
+
  1086
+Each one of the `@root`, `@paths`, `@extensions` and `@aliases` methods have `attr_reader`s defined for them. As we saw near the beginning of this guide, the root, paths and extensions are set up during the initialization of the `Sprockets::Environment` class, providing the foundation for being able to find specific assets.
  1087
+
  1088
+The `find` method on this `Hike::Trail` object is defined like this:
  1089
+
1051 1090
 
1052 1091
 
1053 1092
 
@@ -1057,7 +1096,7 @@ Where the `root` argument here is the root of the Rails application, exactly the
1057 1096
       index.find(*args, &block)
1058 1097
     end
1059 1098
 
1060  
-Where the `index` method is defined like this:
  1099
+The `index` method for Hike works much like the one for sprockets, where the idea was to provide a cached lookup for the files that its found and provide a mechanism for finding files that it hasn't yet found. This method is declared like this:
1061 1100
 
1062 1101
 
1063 1102
 
@@ -1067,6 +1106,118 @@ Where the `index` method is defined like this:
1067 1106
       Index.new(root, paths, extensions, aliases)
1068 1107
     end
1069 1108
 
  1109
+The `root`, `paths`, `extensions` and `aliases` methods are dealt with in the `initialize` method for this `Hike::Index` and frozen:
  1110
+
  1111
+
  1112
+
  1113
+
  1114
+**hike: lib/hike/index.rb, 17 lines, beginning line 20**
  1115
+    
  1116
+    def initialize(root, paths, extensions, aliases)
  1117
+      @root = root
  1118
+    
  1119
+      # Freeze is used here so an error is throw if a mutator method
  1120
+      # is called on the array. Mutating `@paths`, `@extensions`, or
  1121
+      # `@aliases` would have unpredictable results.
  1122
+      @paths      = paths.dup.freeze
  1123
+      @extensions = extensions.dup.freeze
  1124
+      @aliases    = aliases.inject({}) { |h, (k, a)|
  1125
+                      h[k] = a.dup.freeze; h
  1126
+                   }.freeze
  1127
+      @pathnames  = paths.map { |path| Pathname.new(path) }
  1128
+    
  1129
+      @stats    = {}
  1130
+      @entries  = {}
  1131
+      @patterns = {}
  1132
+    end
  1133
+
  1134
+The `find` method on this `Index` object begins like this:
  1135
+
  1136
+
  1137
+
  1138
+**hike: lib/hike/index.rb, 4 lines, beginning line 52**
  1139
+    
  1140
+    def find(*logical_paths, &block)
  1141
+      if block_given?
  1142
+        options = extract_options!(logical_paths)
  1143
+        base_path = Pathname.new(options[:base_path] || @root)
  1144
+
  1145
+The `find` method can be passed an infinite number of arguments, which are defined as the `logical_paths` argument in the method. At the end of these arguments can be an options hash, and that's extracted with the `extract_options!` method, which stores it as `options`. Now, if this `options` hash contains a `:base_path` key, then that will be the `base_path` for this method. If there isn't one, then `@root` will be the `base_path` instead.
  1146
+
  1147
+In the case of the `resolve` method, there are no options passed in and so this will default to `@root`. Next, this method iterates through each of the `logical_paths` and does this:
  1148
+
  1149
+
  1150
+
  1151
+**hike: lib/hike/index.rb, 9 lines, beginning line 57**
  1152
+    
  1153
+    logical_paths.each do |logical_path|
  1154
+      logical_path = Pathname.new(logical_path.sub(/^\//, ''))
  1155
+    
  1156
+      if relative?(logical_path)
  1157
+        find_in_base_path(logical_path, base_path, &block)
  1158
+      else
  1159
+        find_in_paths(logical_path, &block)
  1160
+      end
  1161
+    end
  1162
+
  1163
+The `logical_paths` in the case of "application.css" will be "application.css" and "application/index.css", as defined back in the `resolve` method. Any forward-slashes at the beginning of these paths are stripped and then they are checked for relativeness. If these paths contain either a single dot or a double dot at the beginning of their filenames, they are determined to be relative. It's the `relative?` function defined further down in `lib/hike/index.rb` that determines this:
  1164
+
  1165
+
  1166
+
  1167
+**hike: lib/hike/index.rb, 3 lines, beginning line 105**
  1168
+    
  1169
+    def relative?(logical_path)
  1170
+      logical_path.to_s =~ /^\.\.?\//
  1171
+    end
  1172
+
  1173
+The paths that have been given to `find` in this situation are not relative, and so the `else` for this `if` will be referenced, resulting in the `find_in_paths` method being called with the argument of the `logical_path` and the block which is passed to `find` from `resolve`. The `find_in_paths` method is defined like this:
  1174
+
  1175
+
  1176
+
  1177
+**hike: lib/hike/index.rb, 6 lines, beginning line 110**
  1178
+    
  1179
+    def find_in_paths(logical_path, &block)
  1180
+      dirname, basename = logical_path.split
  1181
+      @pathnames.each do |base_path|
  1182
+        match(base_path.join(dirname), basename, &block)
  1183
+      end
  1184
+    end
  1185
+
  1186
+By calling `split` on `logical_path` here, the `Pathname` object that is `logical_path` is split into the two parts: the first contains the directories of the pathname, while the `basename` contains just the filename. After this split, the `@pathnames` collection (which contains all the directory paths for our assets) is iterated through, with the `match` method being called for each of the path names.
  1187
+
  1188
+This `match` method is defined further down in this file and begins like this:
  1189
+
  1190
+
  1191
+
  1192
+**hike: lib/hike/index.rb, 6 lines, beginning line 127**
  1193
+    
  1194
+    def match(dirname, basename)
  1195
+      # Potential `entries` syscall
  1196
+      matches = entries(dirname)
  1197
+    
  1198
+      pattern = pattern_for(basename)
  1199
+      matches = matches.select { |m| m.to_s =~ pattern }
  1200
+
  1201
+This method begins by calling the `entries` method which will get a list of "entries" from within the specified directory. It does this with the following code:
  1202
+
  1203
+
  1204
+
  1205
+**hike: lib/hike/index.rb, 6 lines, beginning line 78**
  1206
+    
  1207
+    def entries(path)
  1208
+      key = path.to_s
  1209
+      @entries[key] ||= Pathname.new(path).entries.reject { |entry| entry.to_s =~ /^\.|~$|^\#.*\#$/ }.sort
  1210
+    rescue Errno::ENOENT
  1211
+      @entries[key] = []
  1212
+    end
  1213
+
  1214
+In this method, the `path` object, which is a `Pathname` object, is converted into a string and this value is assigned to the `key` local variable. This key is used for the `@entries` Hash. If there has been a lookup performed for this directory before, this value would have been cached inside `@entries[key]`, but because this is the first time, the lookup is performed as normal.
  1215
+
  1216
+The `path` object is converted into a `Pathname` object just to make extra sure that it well and truly is a `Pathname` and then the `entries` method is called on that. The `entries` method for a `Pathname` will return an array of directories and files for this path. From this array, entries containing dot characters, `~` characters, or those beginning and ending with the pound (#) character are excluded. This is then sorted.
  1217
+
  1218
+If this directory doesn't exist, then the `Errno::ENOENT` exception will be raised, and Hike will set the entries for this path to be an empty array.
  1219
+
  1220
+
1070 1221
 ## Rake tasks
1071 1222
 
1072 1223
 Cover assets:precompile and assets:clean here.
137  sprockets/sprockets.muse
@@ -905,7 +905,42 @@ def initialize(root = ".")
905 905
   @trail = Hike::Trail.new(root)
906 906
 </div>
907 907
 
908  
-Where the `root` argument here is the root of the Rails application, exactly the same as `Rails.root` returns. The `find` method itself is defined within the Hike gem like this:
  908
+Where the `root` argument here is the root of the Rails application, exactly the same as `Rails.root` returns. The `initialize` method for `Hike::Trail` is defined within Hike in `lib/hike/trail.rb` like this:
  909
+
  910
+<div class="example" data-repo='hike' data-file='lib/hike/trail.rb' data-start='48'>
  911
+def initialize(root = ".")
  912
+  @root       = Pathname.new(root).expand_path
  913
+  @paths      = Paths.new(@root)
  914
+  @extensions = Extensions.new
  915
+  @aliases    = Hash.new { |h, k| h[k] = Extensions.new }
  916
+end
  917
+</div>
  918
+
  919
+The `root` method that is passed in is converted to a `Pathname` object if it isn't one already, and then `expand_path` is called on it so that it can store the absolute path for hiking. 
  920
+
  921
+Next up, new `Paths` object (`@paths`) is created for this root, which is used to track a collection of the paths of where to find files within the `@root`.  
  922
+
  923
+The `@extensions` method is much the same as the `@paths` object, but tracks the extensions which files are being found for. The `@aliases` hash will contain fallbacks for other extensions that are known to Hike. The documentation for the `attr_reader` for this setting explains it quite well:
  924
+
  925
+<div class="example" data-repo='hike' data-file='lib/hike/trail.rb' data-start='31'>
  926
+# `Index#aliases` is a mutable `Hash` mapping an extension to
  927
+# an `Array` of aliases.
  928
+#
  929
+#   trail = Hike::Trail.new
  930
+#   trail.paths.push "~/Projects/hike/site"
  931
+#   trail.aliases['.htm']   = 'html'
  932
+#   trail.aliases['.xhtml'] = 'html'
  933
+#   trail.aliases['.php']   = 'html'
  934
+#
  935
+# Aliases provide a fallback when the primary extension is not
  936
+# matched. In the example above, a lookup for "foo.html" will
  937
+# check for the existence of "foo.htm", "foo.xhtml", or "foo.php".
  938
+</div>
  939
+
  940
+Each one of the `@root`, `@paths`, `@extensions` and `@aliases` methods have `attr_reader`s defined for them. As we saw near the beginning of this guide, the root, paths and extensions are set up during the initialization of the `Sprockets::Environment` class, providing the foundation for being able to find specific assets.
  941
+
  942
+The `find` method on this `Hike::Trail` object is defined like this:
  943
+
909 944
 
910 945
 <div class="example" data-repo='hike' data-file='lib/hike/trail.rb' data-start='138'>
911 946
 def find(*args, &block)
@@ -913,7 +948,7 @@ def find(*args, &block)
913 948
 end
914 949
 </div>
915 950
 
916  
-Where the `index` method is defined like this:
  951
+The `index` method for Hike works much like the one for sprockets, where the idea was to provide a cached lookup for the files that its found and provide a mechanism for finding files that it hasn't yet found. This method is declared like this:
917 952
 
918 953
 <div class="example" data-repo='hike' data-file='lib/hike/trail.rb' data-start='152'>
919 954
 def index
@@ -921,6 +956,104 @@ def index
921 956
 end
922 957
 </div>
923 958
 
  959
+The `root`, `paths`, `extensions` and `aliases` methods are dealt with in the `initialize` method for this `Hike::Index` and frozen:
  960
+
  961
+
  962
+<div class="example" data-repo='hike' data-file='lib/hike/index.rb' data-start='20'>
  963
+def initialize(root, paths, extensions, aliases)
  964
+  @root = root
  965
+
  966
+  # Freeze is used here so an error is throw if a mutator method
  967
+  # is called on the array. Mutating `@paths`, `@extensions`, or
  968
+  # `@aliases` would have unpredictable results.
  969
+  @paths      = paths.dup.freeze
  970
+  @extensions = extensions.dup.freeze
  971
+  @aliases    = aliases.inject({}) { |h, (k, a)|
  972
+                  h[k] = a.dup.freeze; h
  973
+               }.freeze
  974
+  @pathnames  = paths.map { |path| Pathname.new(path) }
  975
+
  976
+  @stats    = {}
  977
+  @entries  = {}
  978
+  @patterns = {}
  979
+end
  980
+</div>
  981
+
  982
+The `find` method on this `Index` object begins like this:
  983
+
  984
+<div class="example" data-repo='hike' data-file='lib/hike/index.rb' data-start='52'>
  985
+def find(*logical_paths, &block)
  986
+  if block_given?
  987
+    options = extract_options!(logical_paths)
  988
+    base_path = Pathname.new(options[:base_path] || @root)
  989
+</div>
  990
+
  991
+The `find` method can be passed an infinite number of arguments, which are defined as the `logical_paths` argument in the method. At the end of these arguments can be an options hash, and that's extracted with the `extract_options!` method, which stores it as `options`. Now, if this `options` hash contains a `:base_path` key, then that will be the `base_path` for this method. If there isn't one, then `@root` will be the `base_path` instead.
  992
+
  993
+In the case of the `resolve` method, there are no options passed in and so this will default to `@root`. Next, this method iterates through each of the `logical_paths` and does this:
  994
+
  995
+<div class="example" data-repo='hike' data-file='lib/hike/index.rb' data-start='57'>
  996
+logical_paths.each do |logical_path|
  997
+  logical_path = Pathname.new(logical_path.sub(/^\//, ''))
  998
+
  999
+  if relative?(logical_path)
  1000
+    find_in_base_path(logical_path, base_path, &block)
  1001
+  else
  1002
+    find_in_paths(logical_path, &block)
  1003
+  end
  1004
+end
  1005
+</div>
  1006
+
  1007
+The `logical_paths` in the case of "application.css" will be "application.css" and "application/index.css", as defined back in the `resolve` method. Any forward-slashes at the beginning of these paths are stripped and then they are checked for relativeness. If these paths contain either a single dot or a double dot at the beginning of their filenames, they are determined to be relative. It's the `relative?` function defined further down in `lib/hike/index.rb` that determines this:
  1008
+
  1009
+<div class="example" data-repo='hike' data-file='lib/hike/index.rb' data-start='105'>
  1010
+def relative?(logical_path)
  1011
+  logical_path.to_s =~ /^\.\.?\//
  1012
+end
  1013
+</div>
  1014
+
  1015
+The paths that have been given to `find` in this situation are not relative, and so the `else` for this `if` will be referenced, resulting in the `find_in_paths` method being called with the argument of the `logical_path` and the block which is passed to `find` from `resolve`. The `find_in_paths` method is defined like this:
  1016
+
  1017
+<div class="example" data-repo='hike' data-file='lib/hike/index.rb' data-start='110'>
  1018
+def find_in_paths(logical_path, &block)
  1019
+  dirname, basename = logical_path.split
  1020
+  @pathnames.each do |base_path|
  1021
+    match(base_path.join(dirname), basename, &block)
  1022
+  end
  1023
+end
  1024
+</div>
  1025
+
  1026
+By calling `split` on `logical_path` here, the `Pathname` object that is `logical_path` is split into the two parts: the first contains the directories of the pathname, while the `basename` contains just the filename. After this split, the `@pathnames` collection (which contains all the directory paths for our assets) is iterated through, with the `match` method being called for each of the path names.
  1027
+
  1028
+This `match` method is defined further down in this file and begins like this:
  1029
+
  1030
+<div class="example" data-repo='hike' data-file='lib/hike/index.rb' data-start='127'>
  1031
+def match(dirname, basename)
  1032
+  # Potential `entries` syscall
  1033
+  matches = entries(dirname)
  1034
+
  1035
+  pattern = pattern_for(basename)
  1036
+  matches = matches.select { |m| m.to_s =~ pattern }
  1037
+</div>
  1038
+
  1039
+This method begins by calling the `entries` method which will get a list of "entries" from within the specified directory. It does this with the following code:
  1040
+
  1041
+<div class="example" data-repo='hike' data-file='lib/hike/index.rb' data-start='78'>
  1042
+def entries(path)
  1043
+  key = path.to_s
  1044
+  @entries[key] ||= Pathname.new(path).entries.reject { |entry| entry.to_s =~ /^\.|~$|^\#.*\#$/ }.sort
  1045
+rescue Errno::ENOENT
  1046
+  @entries[key] = []
  1047
+end
  1048
+</div>
  1049
+
  1050
+In this method, the `path` object, which is a `Pathname` object, is converted into a string and this value is assigned to the `key` local variable. This key is used for the `@entries` Hash. If there has been a lookup performed for this directory before, this value would have been cached inside `@entries[key]`, but because this is the first time, the lookup is performed as normal.
  1051
+
  1052
+The `path` object is converted into a `Pathname` object just to make extra sure that it well and truly is a `Pathname` and then the `entries` method is called on that. The `entries` method for a `Pathname` will return an array of directories and files for this path. From this array, entries containing dot characters, `~` characters, or those beginning and ending with the pound (#) character are excluded. This is then sorted.
  1053
+
  1054
+If this directory doesn't exist, then the `Errno::ENOENT` exception will be raised, and Hike will set the entries for this path to be an empty array.
  1055
+
  1056
+
924 1057
 ## Rake tasks
925 1058
 
926 1059
 Cover assets:precompile and assets:clean here.

0 notes on commit 2310164

Please sign in to comment.
Something went wrong with that request. Please try again.