nanoc3 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. data/ChangeLog +3 -0
  2. data/LICENSE +19 -0
  3. data/NEWS.rdoc +262 -0
  4. data/README.rdoc +80 -0
  5. data/Rakefile +11 -0
  6. data/bin/nanoc3 +16 -0
  7. data/lib/nanoc3/base/code_snippet.rb +42 -0
  8. data/lib/nanoc3/base/compiler.rb +225 -0
  9. data/lib/nanoc3/base/compiler_dsl.rb +110 -0
  10. data/lib/nanoc3/base/core_ext/array.rb +21 -0
  11. data/lib/nanoc3/base/core_ext/hash.rb +23 -0
  12. data/lib/nanoc3/base/core_ext/string.rb +14 -0
  13. data/lib/nanoc3/base/core_ext.rb +5 -0
  14. data/lib/nanoc3/base/data_source.rb +197 -0
  15. data/lib/nanoc3/base/dependency_tracker.rb +291 -0
  16. data/lib/nanoc3/base/errors.rb +95 -0
  17. data/lib/nanoc3/base/filter.rb +60 -0
  18. data/lib/nanoc3/base/item.rb +87 -0
  19. data/lib/nanoc3/base/item_rep.rb +236 -0
  20. data/lib/nanoc3/base/layout.rb +53 -0
  21. data/lib/nanoc3/base/notification_center.rb +68 -0
  22. data/lib/nanoc3/base/plugin.rb +88 -0
  23. data/lib/nanoc3/base/preprocessor_context.rb +37 -0
  24. data/lib/nanoc3/base/rule.rb +37 -0
  25. data/lib/nanoc3/base/rule_context.rb +68 -0
  26. data/lib/nanoc3/base/site.rb +334 -0
  27. data/lib/nanoc3/base.rb +25 -0
  28. data/lib/nanoc3/cli/base.rb +151 -0
  29. data/lib/nanoc3/cli/commands/autocompile.rb +89 -0
  30. data/lib/nanoc3/cli/commands/compile.rb +279 -0
  31. data/lib/nanoc3/cli/commands/create_item.rb +79 -0
  32. data/lib/nanoc3/cli/commands/create_layout.rb +94 -0
  33. data/lib/nanoc3/cli/commands/create_site.rb +320 -0
  34. data/lib/nanoc3/cli/commands/help.rb +71 -0
  35. data/lib/nanoc3/cli/commands/info.rb +114 -0
  36. data/lib/nanoc3/cli/commands/update.rb +96 -0
  37. data/lib/nanoc3/cli/commands.rb +13 -0
  38. data/lib/nanoc3/cli/logger.rb +73 -0
  39. data/lib/nanoc3/cli.rb +16 -0
  40. data/lib/nanoc3/data_sources/delicious.rb +66 -0
  41. data/lib/nanoc3/data_sources/filesystem.rb +231 -0
  42. data/lib/nanoc3/data_sources/filesystem_combined.rb +202 -0
  43. data/lib/nanoc3/data_sources/filesystem_common.rb +22 -0
  44. data/lib/nanoc3/data_sources/filesystem_compact.rb +232 -0
  45. data/lib/nanoc3/data_sources/last_fm.rb +103 -0
  46. data/lib/nanoc3/data_sources/twitter.rb +53 -0
  47. data/lib/nanoc3/data_sources.rb +20 -0
  48. data/lib/nanoc3/extra/auto_compiler.rb +97 -0
  49. data/lib/nanoc3/extra/chick.rb +119 -0
  50. data/lib/nanoc3/extra/context.rb +24 -0
  51. data/lib/nanoc3/extra/core_ext/time.rb +19 -0
  52. data/lib/nanoc3/extra/core_ext.rb +3 -0
  53. data/lib/nanoc3/extra/deployers/rsync.rb +64 -0
  54. data/lib/nanoc3/extra/deployers.rb +12 -0
  55. data/lib/nanoc3/extra/file_proxy.rb +31 -0
  56. data/lib/nanoc3/extra/validators/links.rb +0 -0
  57. data/lib/nanoc3/extra/validators/w3c.rb +71 -0
  58. data/lib/nanoc3/extra/validators.rb +12 -0
  59. data/lib/nanoc3/extra/vcs.rb +65 -0
  60. data/lib/nanoc3/extra/vcses/bazaar.rb +21 -0
  61. data/lib/nanoc3/extra/vcses/dummy.rb +20 -0
  62. data/lib/nanoc3/extra/vcses/git.rb +21 -0
  63. data/lib/nanoc3/extra/vcses/mercurial.rb +21 -0
  64. data/lib/nanoc3/extra/vcses/subversion.rb +21 -0
  65. data/lib/nanoc3/extra/vcses.rb +17 -0
  66. data/lib/nanoc3/extra.rb +16 -0
  67. data/lib/nanoc3/filters/bluecloth.rb +13 -0
  68. data/lib/nanoc3/filters/coderay.rb +17 -0
  69. data/lib/nanoc3/filters/erb.rb +19 -0
  70. data/lib/nanoc3/filters/erubis.rb +17 -0
  71. data/lib/nanoc3/filters/haml.rb +20 -0
  72. data/lib/nanoc3/filters/less.rb +13 -0
  73. data/lib/nanoc3/filters/markaby.rb +14 -0
  74. data/lib/nanoc3/filters/maruku.rb +14 -0
  75. data/lib/nanoc3/filters/rainpress.rb +13 -0
  76. data/lib/nanoc3/filters/rdiscount.rb +13 -0
  77. data/lib/nanoc3/filters/rdoc.rb +23 -0
  78. data/lib/nanoc3/filters/redcloth.rb +14 -0
  79. data/lib/nanoc3/filters/relativize_paths.rb +32 -0
  80. data/lib/nanoc3/filters/rubypants.rb +14 -0
  81. data/lib/nanoc3/filters/sass.rb +17 -0
  82. data/lib/nanoc3/filters.rb +37 -0
  83. data/lib/nanoc3/helpers/blogging.rb +226 -0
  84. data/lib/nanoc3/helpers/breadcrumbs.rb +25 -0
  85. data/lib/nanoc3/helpers/capturing.rb +71 -0
  86. data/lib/nanoc3/helpers/filtering.rb +46 -0
  87. data/lib/nanoc3/helpers/html_escape.rb +22 -0
  88. data/lib/nanoc3/helpers/link_to.rb +120 -0
  89. data/lib/nanoc3/helpers/rendering.rb +76 -0
  90. data/lib/nanoc3/helpers/tagging.rb +58 -0
  91. data/lib/nanoc3/helpers/text.rb +40 -0
  92. data/lib/nanoc3/helpers/xml_sitemap.rb +69 -0
  93. data/lib/nanoc3/helpers.rb +16 -0
  94. data/lib/nanoc3/package.rb +106 -0
  95. data/lib/nanoc3/tasks/clean.rake +16 -0
  96. data/lib/nanoc3/tasks/clean.rb +33 -0
  97. data/lib/nanoc3/tasks/deploy/rsync.rake +11 -0
  98. data/lib/nanoc3/tasks/validate.rake +35 -0
  99. data/lib/nanoc3/tasks.rb +9 -0
  100. data/lib/nanoc3.rb +19 -0
  101. data/vendor/cri/ChangeLog +0 -0
  102. data/vendor/cri/LICENSE +19 -0
  103. data/vendor/cri/NEWS +0 -0
  104. data/vendor/cri/README +4 -0
  105. data/vendor/cri/Rakefile +25 -0
  106. data/vendor/cri/lib/cri/base.rb +153 -0
  107. data/vendor/cri/lib/cri/command.rb +105 -0
  108. data/vendor/cri/lib/cri/core_ext/string.rb +41 -0
  109. data/vendor/cri/lib/cri/core_ext.rb +8 -0
  110. data/vendor/cri/lib/cri/option_parser.rb +186 -0
  111. data/vendor/cri/lib/cri.rb +12 -0
  112. data/vendor/cri/test/test_base.rb +6 -0
  113. data/vendor/cri/test/test_command.rb +6 -0
  114. data/vendor/cri/test/test_core_ext.rb +21 -0
  115. data/vendor/cri/test/test_option_parser.rb +279 -0
  116. metadata +225 -0
@@ -0,0 +1,232 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc3::DataSources
4
+
5
+ # The filesystem_combined data source is the default data source for a new
6
+ # nanoc site. It stores all data as files on the hard disk.
7
+ #
8
+ # None of the methods are documented in this file. See Nanoc3::DataSource
9
+ # for documentation on the overridden methods instead.
10
+ #
11
+ # = Items
12
+ #
13
+ # Items are stored as pairs of two files: a content file, containing the
14
+ # actual item content, and a meta file, containing the item's attributes,
15
+ # formatted as YAML. The content file and the corresponding meta file have
16
+ # the same filename but not the same extension; the meta file's extension is
17
+ # .yaml.
18
+ #
19
+ # Items are stored in the "content" directory of the nanoc site.
20
+ #
21
+ # The home page item, located at /, is represented by an index.yaml meta
22
+ # file, along with its corresponding content file.
23
+ #
24
+ # Subitems of other pages can be achieved in two ways: they can either be
25
+ # nested in directories and named "index" such as the home page item, or
26
+ # they can simply be given a non-"index" name.
27
+ #
28
+ # For example, this directory structure:
29
+ #
30
+ # content/
31
+ # index.html
32
+ # index.yaml
33
+ # about.html
34
+ # about.yaml
35
+ # journal.html
36
+ # journal.yaml
37
+ # journal/
38
+ # 2005.html
39
+ # 2005.yaml
40
+ # 2005/
41
+ # a-very-old-post.html
42
+ # a-very-old-post.yaml
43
+ # another-very-old-post.html
44
+ # another-very-old-post.yaml
45
+ # myst/
46
+ # index.html
47
+ # index.yaml
48
+ #
49
+ # … corresponds with the following items:
50
+ #
51
+ # /
52
+ # /about/
53
+ # /journal/
54
+ # /journal/2005/
55
+ # /journal/2005/a-very-old-post/
56
+ # /journal/2005/another-very-old-post/
57
+ # /myst/
58
+ #
59
+ # = Layouts
60
+ #
61
+ # Layouts are stored the same way as items, except that they are stored in
62
+ # the "layouts" directory instead of the "content" directory.
63
+ #
64
+ # = Code Snippets
65
+ #
66
+ # Code snippets are stored in '.rb' files in the 'lib' directory. Code
67
+ # snippets can reside in sub-directories.
68
+ class FilesystemCompact < Nanoc3::DataSource
69
+
70
+ include Nanoc3::DataSources::FilesystemCommon
71
+
72
+ ########## VCSes ##########
73
+
74
+ attr_accessor :vcs
75
+
76
+ def vcs
77
+ @vcs ||= Nanoc3::Extra::VCSes::Dummy.new
78
+ end
79
+
80
+ ########## Preparation ##########
81
+
82
+ def setup
83
+ # Create directories
84
+ %w( content layouts lib ).each do |dir|
85
+ FileUtils.mkdir_p(dir)
86
+ vcs.add(dir)
87
+ end
88
+ end
89
+
90
+ ########## Loading data ##########
91
+
92
+ def items
93
+ meta_filenames('content').map do |meta_filename|
94
+ # Read metadata
95
+ meta = YAML.load_file(meta_filename) || {}
96
+
97
+ # Get content
98
+ content_filename = content_filename_for_meta_filename(meta_filename)
99
+ content = File.read(content_filename)
100
+
101
+ # Get attributes
102
+ attributes = meta.merge(:file => Nanoc3::Extra::FileProxy.new(content_filename))
103
+
104
+ # Get identifier
105
+ identifier = identifier_for_meta_filename(meta_filename.sub(/^content/, ''))
106
+
107
+ # Get modification times
108
+ meta_mtime = File.stat(meta_filename).mtime
109
+ content_mtime = File.stat(content_filename).mtime
110
+ mtime = meta_mtime > content_mtime ? meta_mtime : content_mtime
111
+
112
+ # Create item object
113
+ Nanoc3::Item.new(content, attributes, identifier, mtime)
114
+ end
115
+ end
116
+
117
+ def layouts
118
+ meta_filenames('layouts').map do |meta_filename|
119
+ # Get content
120
+ content_filename = content_filename_for_meta_filename(meta_filename)
121
+ content = File.read(content_filename)
122
+
123
+ # Get attributes
124
+ attributes = YAML.load_file(meta_filename) || {}
125
+
126
+ # Get identifier
127
+ identifier = identifier_for_meta_filename(meta_filename.sub(/^layouts\//, ''))
128
+
129
+ # Get modification times
130
+ meta_mtime = File.stat(meta_filename).mtime
131
+ content_mtime = File.stat(content_filename).mtime
132
+ mtime = meta_mtime > content_mtime ? meta_mtime : content_mtime
133
+
134
+ # Create layout object
135
+ Nanoc3::Layout.new(content, attributes, identifier, mtime)
136
+ end
137
+ end
138
+
139
+ ########## Creating data ##########
140
+
141
+ # Creates a new item with the given content, attributes and identifier.
142
+ def create_item(content, attributes, identifier)
143
+ # Get filenames
144
+ base_path = 'content' + (identifier == '/' ? '/index' : identifier[0..-2])
145
+ meta_filename = base_path + '.yaml'
146
+ content_filename = base_path + '.html'
147
+
148
+ # Notify
149
+ Nanoc3::NotificationCenter.post(:file_created, meta_filename)
150
+ Nanoc3::NotificationCenter.post(:file_created, content_filename)
151
+
152
+ # Create files
153
+ FileUtils.mkdir_p(File.dirname(meta_filename))
154
+ File.open(meta_filename, 'w') { |io| io.write(YAML.dump(attributes.stringify_keys)) }
155
+ File.open(content_filename, 'w') { |io| io.write(content) }
156
+ end
157
+
158
+ # Creates a new layout with the given content, attributes and identifier.
159
+ def create_layout(content, attributes, identifier)
160
+ # Get filenames
161
+ base_path = 'layouts' + identifier[0..-2]
162
+ meta_filename = base_path + '.yaml'
163
+ content_filename = base_path + '.html'
164
+
165
+ # Notify
166
+ Nanoc3::NotificationCenter.post(:file_created, meta_filename)
167
+ Nanoc3::NotificationCenter.post(:file_created, content_filename)
168
+
169
+ # Create files
170
+ FileUtils.mkdir_p(File.dirname(meta_filename))
171
+ File.open(meta_filename, 'w') { |io| io.write(YAML.dump(attributes.stringify_keys)) }
172
+ File.open(content_filename, 'w') { |io| io.write(content) }
173
+ end
174
+
175
+ private
176
+
177
+ ########## Custom functions ##########
178
+
179
+ # Returns the identifier for the given meta filename. This method assumes
180
+ # that the base is already stripped.
181
+ #
182
+ # For example:
183
+ #
184
+ # /foo.yaml -> /foo/
185
+ # /foo/index.yaml -> /foo/
186
+ # /foo/foo.yaml -> /foo/foo/
187
+ # /foo/bar.yaml -> /foo/bar/
188
+ def identifier_for_meta_filename(meta_filename)
189
+ # Split into components
190
+ components = meta_filename.gsub(%r{(^/|/$)}, '').split('/')
191
+ components[-1].sub!(/\.yaml$/, '')
192
+
193
+ if components[-1] == 'index'
194
+ components[0..-2].join('/').cleaned_identifier
195
+ else
196
+ components.join('/').cleaned_identifier
197
+ end
198
+ end
199
+
200
+ # Returns the list of all meta files in the given base directory as well
201
+ # as its subdirectories.
202
+ def meta_filenames(base)
203
+ Dir[base + '/**/*.yaml']
204
+ end
205
+
206
+ # Returns the filename of the content file corresponding to the given meta
207
+ # file, ignoring any unwanted files (files that end with '~', '.orig',
208
+ # '.rej' or '.bak')
209
+ def content_filename_for_meta_filename(meta_filename)
210
+ # Find all files
211
+ filenames = Dir[meta_filename.sub(/\.yaml$/, '.*')]
212
+
213
+ # Reject meta files
214
+ filenames.reject! { |f| f =~ /\.yaml$/ }
215
+
216
+ # Reject backups
217
+ filenames.reject! { |f| f =~ /(~|\.orig|\.rej|\.bak)$/ }
218
+
219
+ # Make sure there is only one content file
220
+ if filenames.size != 1
221
+ raise RuntimeError.new(
222
+ "Expected 1 content file for the metafile #{meta_filename} but found #{filenames.size}"
223
+ )
224
+ end
225
+
226
+ # Return content filename
227
+ filenames.first
228
+ end
229
+
230
+ end
231
+
232
+ end
@@ -0,0 +1,103 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc3::DataSources
4
+
5
+ # Nanoc3::DataSources::LastFM provides data about recently played tracks
6
+ # from from a single Last.fm user as items (Nanoc3::Item instances).
7
+ #
8
+ # The configuration must have a "username" attribute containing the username
9
+ # of the account from which to fetch the data, and an "api_key" attribute
10
+ # containing the API key (which can be obtained from the Last.fm site).
11
+ #
12
+ # The items returned by this data source will be mounted at {root}/{id},
13
+ # where +id+ is a sequence number that is not necessarily unique for this
14
+ # bookmark (because delicious.com unfortunately does not provide unique IDs
15
+ # for each track).
16
+ #
17
+ # The items returned by this data source will have the following attributes:
18
+ #
19
+ # +:name+:: The name of the track.
20
+ #
21
+ # +played_at+:: The timestamp when the track was played (a Time instance).
22
+ #
23
+ # +url+:: The Last.fm URL corresponding to the track (a String instance).
24
+ #
25
+ # +artist+:: A hash containing information about the track's artist.
26
+ #
27
+ # The +artist+ hash consists of the following keys:
28
+ #
29
+ # +name+:: The name of the artist.
30
+ #
31
+ # +url+:: The Last.fm URL corresponding to the artist (a String instance).
32
+ class LastFM < Nanoc3::DataSource
33
+
34
+ def items
35
+ @items ||= begin
36
+ require 'json'
37
+ require 'time'
38
+ require 'uri'
39
+
40
+ # Check configuration
41
+ if self.config[:username].nil?
42
+ raise RuntimeError, "LastFM data source requires a username in the configuration"
43
+ end
44
+ if self.config[:api_key].nil?
45
+ raise RuntimeError, "LastFM data source requires an API key in the configuration"
46
+ end
47
+
48
+ # Get data
49
+ @http_client ||= Nanoc3::Extra::CHiCk::Client.new
50
+ status, headers, data = *@http_client.get(
51
+ 'http://ws.audioscrobbler.com/2.0/' +
52
+ '?method=user.getRecentTracks' +
53
+ '&format=json' +
54
+ '&user=' + URI.escape(self.config[:username]) +
55
+ '&api_key=' + URI.escape(self.config[:api_key])
56
+ )
57
+
58
+ # Parse as JSON
59
+ parsed_data = JSON.parse(data)
60
+ raw_items = parsed_data['recenttracks']['track']
61
+
62
+ # Convert to items
63
+ raw_items.enum_with_index.map do |raw_item, i|
64
+ # Get artist data
65
+ artist_status, artist_headers, artist_data = *@http_client.get(
66
+ 'http://ws.audioscrobbler.com/2.0/' +
67
+ '?method=artist.getInfo' +
68
+ '&format=json' +
69
+ (
70
+ raw_item['artist']['mbid'].empty? ?
71
+ '&artist=' + URI.escape(raw_item['artist']['#text']) :
72
+ '&mbid=' + URI.escape(raw_item['artist']['mbid'])
73
+ ) +
74
+ '&api_key=' + URI.escape(self.config[:api_key])
75
+ )
76
+
77
+ # Parse as JSON
78
+ parsed_artist_data = JSON.parse(artist_data)
79
+ raw_artist_info = parsed_artist_data['artist']
80
+
81
+ # Build data
82
+ content = ''
83
+ attributes = {
84
+ :name => raw_item['name'],
85
+ :artist => {
86
+ :name => raw_artist_info['name'],
87
+ :url => raw_artist_info['url']
88
+ },
89
+ :url => raw_item['url'],
90
+ :played_at => Time.parse(raw_item['date']['#text'])
91
+ }
92
+ identifier = "/#{i}/"
93
+ mtime = nil
94
+
95
+ # Build item
96
+ Nanoc3::Item.new(content, attributes, identifier, mtime)
97
+ end
98
+ end
99
+ end
100
+
101
+ end
102
+
103
+ end
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc3::DataSources
4
+
5
+ # Nanoc3::DataSources::Twitter provides tweets from a single user as items
6
+ # (Nanoc3::Item instances).
7
+ #
8
+ # The configuration must have a "username" attribute containing the username
9
+ # of the account from which to fetch the tweets.
10
+ #
11
+ # The items returned by this data source will be mounted at {root}/{id},
12
+ # where +id+ is the unique identifier of the tweet.
13
+ #
14
+ # The items returned by this data source will have the following attributes:
15
+ #
16
+ # +:created_at+:: The timestamp when this tweet was posted (a string).
17
+ #
18
+ # +source+:: The client used to tweet this message (HTML-encoded).
19
+ class Twitter < Nanoc3::DataSource
20
+
21
+ def items
22
+ @item ||= begin
23
+ require 'json'
24
+ require 'time'
25
+
26
+ # Get data
27
+ @http_client ||= Nanoc3::Extra::CHiCk::Client.new
28
+ status, headers, data = *@http_client.get("http://twitter.com/statuses/user_timeline/#{self.config[:username]}.json")
29
+
30
+ # Parse as JSON
31
+ raw_items = JSON.parse(data)
32
+
33
+ # Convert to items
34
+ raw_items.enum_with_index.map do |raw_item, i|
35
+ # Get data
36
+ content = raw_item['text']
37
+ attributes = {
38
+ :created_at => raw_item['created_at'],
39
+ :source => raw_item['source']
40
+ # TODO add more
41
+ }
42
+ identifier = "/#{raw_item['id']}/"
43
+ mtime = Time.parse(raw_item['created_at'])
44
+
45
+ # Build item
46
+ Nanoc3::Item.new(content, attributes, identifier, mtime)
47
+ end
48
+ end
49
+ end
50
+
51
+ end
52
+
53
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc3::DataSources
4
+
5
+ autoload 'Delicious', 'nanoc3/data_sources/delicious'
6
+ autoload 'Filesystem', 'nanoc3/data_sources/filesystem'
7
+ autoload 'FilesystemCombined', 'nanoc3/data_sources/filesystem_combined'
8
+ autoload 'FilesystemCommon', 'nanoc3/data_sources/filesystem_common'
9
+ autoload 'FilesystemCompact', 'nanoc3/data_sources/filesystem_compact'
10
+ autoload 'LastFM', 'nanoc3/data_sources/last_fm'
11
+ autoload 'Twitter', 'nanoc3/data_sources/twitter'
12
+
13
+ Nanoc3::DataSource.register '::Nanoc3::DataSources::Delicious', :delicious
14
+ Nanoc3::DataSource.register '::Nanoc3::DataSources::Filesystem', :filesystem
15
+ Nanoc3::DataSource.register '::Nanoc3::DataSources::FilesystemCombined', :filesystem_combined
16
+ Nanoc3::DataSource.register '::Nanoc3::DataSources::FilesystemCompact', :filesystem_compact
17
+ Nanoc3::DataSource.register '::Nanoc3::DataSources::LastFM', :last_fm
18
+ Nanoc3::DataSource.register '::Nanoc3::DataSources::Twitter', :twitter
19
+
20
+ end
@@ -0,0 +1,97 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc3::Extra
4
+
5
+ # Nanoc3::Extra::AutoCompiler is a web server that will automatically compile
6
+ # items as they are requested. It also serves static files such as
7
+ # stylesheets and images.
8
+ class AutoCompiler
9
+
10
+ attr_reader :site
11
+
12
+ # Creates a new autocompiler for the given site.
13
+ def initialize(site_path)
14
+ require 'rack'
15
+ require 'mime/types'
16
+
17
+ # Set site
18
+ @site_path = site_path
19
+
20
+ # Create mutex to prevent parallel requests
21
+ @mutex = Mutex.new
22
+ end
23
+
24
+ def call(env)
25
+ @mutex.synchronize do
26
+ # Start with a new site
27
+ build_site
28
+
29
+ # Find rep
30
+ path = env['PATH_INFO']
31
+ reps = site.items.map { |i| i.reps }.flatten
32
+ rep = reps.find { |r| r.path == path }
33
+
34
+ if rep
35
+ serve(rep)
36
+ else
37
+ # Get paths by appending index filenames
38
+ if path =~ /\/$/
39
+ possible_paths = site.config[:index_filenames].map { |f| path + f }
40
+ else
41
+ possible_paths = [ path ]
42
+ end
43
+
44
+ # Find matching file
45
+ modified_path = possible_paths.find { |f| File.file?(site.config[:output_dir] + f) }
46
+ modified_path ||= path
47
+
48
+ # Serve using Rack::File
49
+ file_server.call(env.merge('PATH_INFO' => modified_path))
50
+ end
51
+ end
52
+ rescue StandardError, ScriptError => e
53
+ # Add compilation stack to env
54
+ env['nanoc.stack'] = []
55
+ site.compiler.stack.reverse.each do |obj|
56
+ if obj.is_a?(Nanoc3::ItemRep) # item rep
57
+ env['nanoc.stack'] << "[item] #{obj.item.identifier} (rep #{obj.name})"
58
+ else # layout
59
+ env['nanoc.stack'] << "[layout] #{obj.identifier}"
60
+ end
61
+ end
62
+
63
+ # Re-raise error
64
+ raise e
65
+ end
66
+
67
+ private
68
+
69
+ def build_site
70
+ @site = Nanoc3::Site.new(@site_path)
71
+ @site.load_data
72
+ end
73
+
74
+ def mime_type_of(path, fallback)
75
+ mime_type = MIME::Types.of(path).first
76
+ mime_type = mime_type.nil? ? fallback : mime_type.simplified
77
+ end
78
+
79
+ def file_server
80
+ @file_server ||= ::Rack::File.new(site.config[:output_dir])
81
+ end
82
+
83
+ def serve(rep)
84
+ # Recompile rep
85
+ site.compiler.run(rep.item, :force => true)
86
+
87
+ # Build response
88
+ [
89
+ 200,
90
+ { 'Content-Type' => mime_type_of(rep.raw_path, 'text/html') },
91
+ [ rep.content_at_snapshot(:last) ]
92
+ ]
93
+ end
94
+
95
+ end
96
+
97
+ end
@@ -0,0 +1,119 @@
1
+ require 'net/http'
2
+ require 'rack'
3
+ require 'rack/cache'
4
+
5
+ module Nanoc3::Extra
6
+
7
+ # CHiCk is a caching HTTP client that uses Rack::Cache.
8
+ module CHiCk
9
+
10
+ # CHiCk::Client provides a simple API for issuing HTTP requests.
11
+ class Client
12
+
13
+ DEFAULT_OPTIONS = {
14
+ :cache => {
15
+ :metastore => 'file:tmp/rack/cache.meta',
16
+ :entitystore => 'file:tmp/rack/cache.body'
17
+ },
18
+ :cache_controller => {
19
+ :max_age => 60
20
+ }
21
+ }
22
+
23
+ def initialize(options={})
24
+ # Get options
25
+ @options = DEFAULT_OPTIONS.merge(options)
26
+ @options[:cache] = DEFAULT_OPTIONS[:cache].merge(@options[:cache])
27
+ @options[:cache_controller] = DEFAULT_OPTIONS[:cache_controller].merge(@options[:cache_controller])
28
+ end
29
+
30
+ def get(url)
31
+ # Build app
32
+ options = @options
33
+ @app ||= Rack::Builder.new {
34
+ use Rack::Cache, options[:cache].merge(:verbose => true)
35
+ use Nanoc3::Extra::CHiCk::CacheController, options[:cache_controller]
36
+ run Nanoc3::Extra::CHiCk::RackClient
37
+ }
38
+
39
+ # Build environment for request
40
+ env = Rack::MockRequest.env_for(url, :method => 'GET')
41
+
42
+ # Fetch
43
+ puts "[CHiCk] Fetching #{url}..." if $DEBUG
44
+ status, headers, body_parts = @app.call(env)
45
+ puts "[CHiCk] #{url}: #{headers['X-Rack-Cache']}" if $DEBUG
46
+
47
+ # Join body
48
+ body = ''
49
+ body_parts.each { |part| body << part }
50
+
51
+ # Done
52
+ [ status, headers, body ]
53
+ end
54
+
55
+ end
56
+
57
+ # CHiCk::CacheController sets the Cache-Control header (and more
58
+ # specifically, max-age) to limit the number of necessary requests.
59
+ class CacheController
60
+
61
+ def initialize(app, options={})
62
+ @app = app
63
+ @options = options
64
+ end
65
+
66
+ def call(env)
67
+ res = @app.call(env)
68
+ unless res[1].has_key?('Cache-Control') || res[1].has_key?('Expires')
69
+ res[1]['Cache-Control'] = "max-age=#{@options[:max_age]}"
70
+ end
71
+ res
72
+ end
73
+
74
+ end
75
+
76
+ # CHiCk::RackClient performs the actual HTTP requests. It does not perform
77
+ # any caching.
78
+ class RackClient
79
+
80
+ METHOD_TO_CLASS_MAPPING = {
81
+ 'DELETE' => Net::HTTP::Delete,
82
+ 'GET' => Net::HTTP::Get,
83
+ 'HEAD' => Net::HTTP::Head,
84
+ 'POST' => Net::HTTP::Post,
85
+ 'PUT' => Net::HTTP::Put
86
+ }
87
+
88
+ def self.call(env)
89
+ # Build request
90
+ request = Rack::Request.new(env)
91
+
92
+ # Build headers and strip HTTP_
93
+ request_headers = env.inject({}) do |m,(k,v)|
94
+ k =~ /^HTTP_(.*)$/ && v ? m.merge($1.gsub(/_/, '-') => v) : m
95
+ end
96
+
97
+ # Build Net::HTTP request
98
+ http = Net::HTTP.new(request.host, request.port)
99
+ net_http_request_class = METHOD_TO_CLASS_MAPPING[request.request_method]
100
+ raise ArgumentError, "Unsupported method: #{request.request_method}" if net_http_request_class.nil?
101
+ net_http_request = net_http_request_class.new(request.fullpath, request_headers)
102
+ net_http_request.body = env['rack.input'].read if [ 'POST', 'PUT' ].include?(request.request_method)
103
+
104
+ # Perform request
105
+ http.request(net_http_request) do |response|
106
+ # Build Rack response triplet
107
+ return [
108
+ response.code.to_i,
109
+ response.to_hash.inject({}) { |m,(k,v)| m.merge(k => v[0]) },
110
+ [ response.body ]
111
+ ]
112
+ end
113
+ end
114
+
115
+ end
116
+
117
+ end
118
+
119
+ end
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc3::Extra
4
+
5
+ # Nanoc3::Extra::Context provides a context and a Binding for use in various
6
+ # filters, such as the ERB and Haml one.
7
+ class Context
8
+
9
+ # Creates a new context based off the contents of the hash. Each pair in
10
+ # the hash will be converted to an instance variable. For example, passing
11
+ # the hash { :foo => 'bar' } will cause @foo to have the value "bar".
12
+ def initialize(hash)
13
+ hash.each_pair do |key, value|
14
+ instance_variable_set('@' + key.to_s, value)
15
+ end
16
+ end
17
+
18
+ # Returns a binding for this context.
19
+ def get_binding
20
+ binding
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc3::Extra::TimeExtensions
4
+
5
+ # Returns a string with the time in an ISO-8601 date format.
6
+ def to_iso8601_date
7
+ self.strftime("%Y-%m-%d")
8
+ end
9
+
10
+ # Returns a string with the time in an ISO-8601 time format.
11
+ def to_iso8601_time
12
+ self.gmtime.strftime("%Y-%m-%dT%H:%M:%SZ")
13
+ end
14
+
15
+ end
16
+
17
+ class Time
18
+ include Nanoc3::Extra::TimeExtensions
19
+ end
@@ -0,0 +1,3 @@
1
+ # encoding: utf-8
2
+
3
+ require 'nanoc3/extra/core_ext/time'