nanoc 3.2.4 → 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 (230) hide show
  1. data/.gemtest +0 -0
  2. data/ChangeLog +3 -0
  3. data/Gemfile +32 -0
  4. data/LICENSE +19 -0
  5. data/NEWS.md +470 -0
  6. data/README.md +114 -0
  7. data/Rakefile +14 -0
  8. data/bin/nanoc +7 -27
  9. data/bin/nanoc3 +3 -0
  10. data/doc/yardoc_templates/default/layout/html/footer.erb +10 -0
  11. data/lib/nanoc.rb +41 -0
  12. data/lib/nanoc/base.rb +49 -0
  13. data/lib/nanoc/base/compilation/checksum_store.rb +57 -0
  14. data/lib/nanoc/base/compilation/compiled_content_cache.rb +62 -0
  15. data/lib/nanoc/base/compilation/compiler.rb +458 -0
  16. data/lib/nanoc/base/compilation/compiler_dsl.rb +214 -0
  17. data/lib/nanoc/base/compilation/dependency_tracker.rb +200 -0
  18. data/lib/nanoc/base/compilation/filter.rb +165 -0
  19. data/lib/nanoc/base/compilation/item_rep_proxy.rb +103 -0
  20. data/lib/nanoc/base/compilation/item_rep_recorder_proxy.rb +102 -0
  21. data/lib/nanoc/base/compilation/outdatedness_checker.rb +223 -0
  22. data/lib/nanoc/base/compilation/outdatedness_reasons.rb +46 -0
  23. data/lib/nanoc/base/compilation/rule.rb +73 -0
  24. data/lib/nanoc/base/compilation/rule_context.rb +84 -0
  25. data/lib/nanoc/base/compilation/rule_memory_calculator.rb +40 -0
  26. data/lib/nanoc/base/compilation/rule_memory_store.rb +53 -0
  27. data/lib/nanoc/base/compilation/rules_collection.rb +243 -0
  28. data/lib/nanoc/base/context.rb +47 -0
  29. data/lib/nanoc/base/core_ext.rb +6 -0
  30. data/lib/nanoc/base/core_ext/array.rb +62 -0
  31. data/lib/nanoc/base/core_ext/hash.rb +63 -0
  32. data/lib/nanoc/base/core_ext/pathname.rb +26 -0
  33. data/lib/nanoc/base/core_ext/string.rb +46 -0
  34. data/lib/nanoc/base/directed_graph.rb +275 -0
  35. data/lib/nanoc/base/errors.rb +211 -0
  36. data/lib/nanoc/base/memoization.rb +67 -0
  37. data/lib/nanoc/base/notification_center.rb +84 -0
  38. data/lib/nanoc/base/ordered_hash.rb +200 -0
  39. data/lib/nanoc/base/plugin_registry.rb +181 -0
  40. data/lib/nanoc/base/result_data/item_rep.rb +492 -0
  41. data/lib/nanoc/base/source_data/code_snippet.rb +58 -0
  42. data/lib/nanoc/base/source_data/configuration.rb +24 -0
  43. data/lib/nanoc/base/source_data/data_source.rb +234 -0
  44. data/lib/nanoc/base/source_data/item.rb +301 -0
  45. data/lib/nanoc/base/source_data/layout.rb +130 -0
  46. data/lib/nanoc/base/source_data/site.rb +361 -0
  47. data/lib/nanoc/base/store.rb +135 -0
  48. data/lib/nanoc/cli.rb +137 -0
  49. data/lib/nanoc/cli/command_runner.rb +104 -0
  50. data/lib/nanoc/cli/commands/autocompile.rb +58 -0
  51. data/lib/nanoc/cli/commands/compile.rb +297 -0
  52. data/lib/nanoc/cli/commands/create_item.rb +60 -0
  53. data/lib/nanoc/cli/commands/create_layout.rb +73 -0
  54. data/lib/nanoc/cli/commands/create_site.rb +411 -0
  55. data/lib/nanoc/cli/commands/debug.rb +117 -0
  56. data/lib/nanoc/cli/commands/deploy.rb +79 -0
  57. data/lib/nanoc/cli/commands/info.rb +98 -0
  58. data/lib/nanoc/cli/commands/nanoc.rb +38 -0
  59. data/lib/nanoc/cli/commands/prune.rb +50 -0
  60. data/lib/nanoc/cli/commands/update.rb +70 -0
  61. data/lib/nanoc/cli/commands/view.rb +82 -0
  62. data/lib/nanoc/cli/commands/watch.rb +124 -0
  63. data/lib/nanoc/cli/error_handler.rb +199 -0
  64. data/lib/nanoc/cli/logger.rb +92 -0
  65. data/lib/nanoc/data_sources.rb +29 -0
  66. data/lib/nanoc/data_sources/deprecated/delicious.rb +42 -0
  67. data/lib/nanoc/data_sources/deprecated/last_fm.rb +87 -0
  68. data/lib/nanoc/data_sources/deprecated/twitter.rb +38 -0
  69. data/lib/nanoc/data_sources/filesystem.rb +299 -0
  70. data/lib/nanoc/data_sources/filesystem_unified.rb +121 -0
  71. data/lib/nanoc/data_sources/filesystem_verbose.rb +91 -0
  72. data/lib/nanoc/extra.rb +24 -0
  73. data/lib/nanoc/extra/auto_compiler.rb +103 -0
  74. data/lib/nanoc/extra/chick.rb +125 -0
  75. data/lib/nanoc/extra/core_ext.rb +6 -0
  76. data/lib/nanoc/extra/core_ext/enumerable.rb +33 -0
  77. data/lib/nanoc/extra/core_ext/pathname.rb +30 -0
  78. data/lib/nanoc/extra/core_ext/time.rb +19 -0
  79. data/lib/nanoc/extra/deployer.rb +47 -0
  80. data/lib/nanoc/extra/deployers.rb +15 -0
  81. data/lib/nanoc/extra/deployers/fog.rb +98 -0
  82. data/lib/nanoc/extra/deployers/rsync.rb +70 -0
  83. data/lib/nanoc/extra/file_proxy.rb +40 -0
  84. data/lib/nanoc/extra/pruner.rb +86 -0
  85. data/lib/nanoc/extra/validators.rb +12 -0
  86. data/lib/nanoc/extra/validators/links.rb +268 -0
  87. data/lib/nanoc/extra/validators/w3c.rb +95 -0
  88. data/lib/nanoc/extra/vcs.rb +66 -0
  89. data/lib/nanoc/extra/vcses.rb +17 -0
  90. data/lib/nanoc/extra/vcses/bazaar.rb +25 -0
  91. data/lib/nanoc/extra/vcses/dummy.rb +24 -0
  92. data/lib/nanoc/extra/vcses/git.rb +25 -0
  93. data/lib/nanoc/extra/vcses/mercurial.rb +25 -0
  94. data/lib/nanoc/extra/vcses/subversion.rb +25 -0
  95. data/lib/nanoc/filters.rb +59 -0
  96. data/lib/nanoc/filters/asciidoc.rb +38 -0
  97. data/lib/nanoc/filters/bluecloth.rb +19 -0
  98. data/lib/nanoc/filters/coderay.rb +21 -0
  99. data/lib/nanoc/filters/coffeescript.rb +20 -0
  100. data/lib/nanoc/filters/colorize_syntax.rb +298 -0
  101. data/lib/nanoc/filters/erb.rb +38 -0
  102. data/lib/nanoc/filters/erubis.rb +34 -0
  103. data/lib/nanoc/filters/haml.rb +27 -0
  104. data/lib/nanoc/filters/kramdown.rb +20 -0
  105. data/lib/nanoc/filters/less.rb +53 -0
  106. data/lib/nanoc/filters/markaby.rb +20 -0
  107. data/lib/nanoc/filters/maruku.rb +20 -0
  108. data/lib/nanoc/filters/mustache.rb +24 -0
  109. data/lib/nanoc/filters/rainpress.rb +19 -0
  110. data/lib/nanoc/filters/rdiscount.rb +22 -0
  111. data/lib/nanoc/filters/rdoc.rb +33 -0
  112. data/lib/nanoc/filters/redcarpet.rb +62 -0
  113. data/lib/nanoc/filters/redcloth.rb +47 -0
  114. data/lib/nanoc/filters/relativize_paths.rb +94 -0
  115. data/lib/nanoc/filters/rubypants.rb +20 -0
  116. data/lib/nanoc/filters/sass.rb +74 -0
  117. data/lib/nanoc/filters/slim.rb +25 -0
  118. data/lib/nanoc/filters/typogruby.rb +23 -0
  119. data/lib/nanoc/filters/uglify_js.rb +42 -0
  120. data/lib/nanoc/filters/xsl.rb +46 -0
  121. data/lib/nanoc/filters/yui_compressor.rb +23 -0
  122. data/lib/nanoc/helpers.rb +16 -0
  123. data/lib/nanoc/helpers/blogging.rb +319 -0
  124. data/lib/nanoc/helpers/breadcrumbs.rb +40 -0
  125. data/lib/nanoc/helpers/capturing.rb +138 -0
  126. data/lib/nanoc/helpers/filtering.rb +50 -0
  127. data/lib/nanoc/helpers/html_escape.rb +55 -0
  128. data/lib/nanoc/helpers/link_to.rb +151 -0
  129. data/lib/nanoc/helpers/rendering.rb +140 -0
  130. data/lib/nanoc/helpers/tagging.rb +71 -0
  131. data/lib/nanoc/helpers/text.rb +44 -0
  132. data/lib/nanoc/helpers/xml_sitemap.rb +76 -0
  133. data/lib/nanoc/tasks.rb +10 -0
  134. data/lib/nanoc/tasks/clean.rake +16 -0
  135. data/lib/nanoc/tasks/clean.rb +29 -0
  136. data/lib/nanoc/tasks/deploy/rsync.rake +16 -0
  137. data/lib/nanoc/tasks/validate.rake +92 -0
  138. data/nanoc.gemspec +49 -0
  139. data/tasks/doc.rake +16 -0
  140. data/tasks/test.rake +46 -0
  141. data/test/base/core_ext/array_spec.rb +73 -0
  142. data/test/base/core_ext/hash_spec.rb +98 -0
  143. data/test/base/core_ext/pathname_spec.rb +27 -0
  144. data/test/base/core_ext/string_spec.rb +37 -0
  145. data/test/base/test_checksum_store.rb +35 -0
  146. data/test/base/test_code_snippet.rb +31 -0
  147. data/test/base/test_compiler.rb +403 -0
  148. data/test/base/test_compiler_dsl.rb +161 -0
  149. data/test/base/test_context.rb +31 -0
  150. data/test/base/test_data_source.rb +46 -0
  151. data/test/base/test_dependency_tracker.rb +262 -0
  152. data/test/base/test_directed_graph.rb +288 -0
  153. data/test/base/test_filter.rb +83 -0
  154. data/test/base/test_item.rb +179 -0
  155. data/test/base/test_item_rep.rb +579 -0
  156. data/test/base/test_layout.rb +59 -0
  157. data/test/base/test_memoization.rb +90 -0
  158. data/test/base/test_notification_center.rb +34 -0
  159. data/test/base/test_outdatedness_checker.rb +394 -0
  160. data/test/base/test_plugin.rb +30 -0
  161. data/test/base/test_rule.rb +19 -0
  162. data/test/base/test_rule_context.rb +65 -0
  163. data/test/base/test_site.rb +190 -0
  164. data/test/cli/commands/test_compile.rb +33 -0
  165. data/test/cli/commands/test_create_item.rb +14 -0
  166. data/test/cli/commands/test_create_layout.rb +28 -0
  167. data/test/cli/commands/test_create_site.rb +24 -0
  168. data/test/cli/commands/test_deploy.rb +74 -0
  169. data/test/cli/commands/test_help.rb +12 -0
  170. data/test/cli/commands/test_info.rb +11 -0
  171. data/test/cli/commands/test_prune.rb +98 -0
  172. data/test/cli/commands/test_update.rb +10 -0
  173. data/test/cli/test_cli.rb +102 -0
  174. data/test/cli/test_error_handler.rb +29 -0
  175. data/test/cli/test_logger.rb +10 -0
  176. data/test/data_sources/test_filesystem.rb +433 -0
  177. data/test/data_sources/test_filesystem_unified.rb +536 -0
  178. data/test/data_sources/test_filesystem_verbose.rb +357 -0
  179. data/test/extra/core_ext/test_enumerable.rb +30 -0
  180. data/test/extra/core_ext/test_pathname.rb +17 -0
  181. data/test/extra/core_ext/test_time.rb +15 -0
  182. data/test/extra/deployers/test_fog.rb +67 -0
  183. data/test/extra/deployers/test_rsync.rb +100 -0
  184. data/test/extra/test_auto_compiler.rb +417 -0
  185. data/test/extra/test_file_proxy.rb +19 -0
  186. data/test/extra/test_vcs.rb +22 -0
  187. data/test/extra/validators/test_links.rb +62 -0
  188. data/test/extra/validators/test_w3c.rb +47 -0
  189. data/test/filters/test_asciidoc.rb +22 -0
  190. data/test/filters/test_bluecloth.rb +18 -0
  191. data/test/filters/test_coderay.rb +44 -0
  192. data/test/filters/test_coffeescript.rb +18 -0
  193. data/test/filters/test_colorize_syntax.rb +379 -0
  194. data/test/filters/test_erb.rb +105 -0
  195. data/test/filters/test_erubis.rb +78 -0
  196. data/test/filters/test_haml.rb +96 -0
  197. data/test/filters/test_kramdown.rb +18 -0
  198. data/test/filters/test_less.rb +113 -0
  199. data/test/filters/test_markaby.rb +24 -0
  200. data/test/filters/test_maruku.rb +18 -0
  201. data/test/filters/test_mustache.rb +25 -0
  202. data/test/filters/test_rainpress.rb +29 -0
  203. data/test/filters/test_rdiscount.rb +31 -0
  204. data/test/filters/test_rdoc.rb +18 -0
  205. data/test/filters/test_redcarpet.rb +73 -0
  206. data/test/filters/test_redcloth.rb +33 -0
  207. data/test/filters/test_relativize_paths.rb +533 -0
  208. data/test/filters/test_rubypants.rb +18 -0
  209. data/test/filters/test_sass.rb +229 -0
  210. data/test/filters/test_slim.rb +35 -0
  211. data/test/filters/test_typogruby.rb +21 -0
  212. data/test/filters/test_uglify_js.rb +30 -0
  213. data/test/filters/test_xsl.rb +105 -0
  214. data/test/filters/test_yui_compressor.rb +44 -0
  215. data/test/gem_loader.rb +11 -0
  216. data/test/helper.rb +207 -0
  217. data/test/helpers/test_blogging.rb +754 -0
  218. data/test/helpers/test_breadcrumbs.rb +81 -0
  219. data/test/helpers/test_capturing.rb +41 -0
  220. data/test/helpers/test_filtering.rb +106 -0
  221. data/test/helpers/test_html_escape.rb +32 -0
  222. data/test/helpers/test_link_to.rb +249 -0
  223. data/test/helpers/test_rendering.rb +89 -0
  224. data/test/helpers/test_tagging.rb +87 -0
  225. data/test/helpers/test_text.rb +24 -0
  226. data/test/helpers/test_xml_sitemap.rb +103 -0
  227. data/test/tasks/test_clean.rb +67 -0
  228. metadata +327 -15
  229. data/bin/nanoc-select +0 -86
  230. data/lib/nanoc-select.rb +0 -11
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc::DataSources
4
+
5
+ autoload 'Filesystem', 'nanoc/data_sources/filesystem'
6
+ autoload 'FilesystemUnified', 'nanoc/data_sources/filesystem_unified'
7
+ autoload 'FilesystemVerbose', 'nanoc/data_sources/filesystem_verbose'
8
+
9
+ Nanoc::DataSource.register '::Nanoc::DataSources::FilesystemVerbose', :filesystem_verbose
10
+ Nanoc::DataSource.register '::Nanoc::DataSources::FilesystemUnified', :filesystem_unified
11
+
12
+ # Deprecated; fetch data from online data sources manually instead
13
+ # TODO [in nanoc 4.0] remove me
14
+ autoload 'Delicious', 'nanoc/data_sources/deprecated/delicious'
15
+ autoload 'LastFM', 'nanoc/data_sources/deprecated/last_fm'
16
+ autoload 'Twitter', 'nanoc/data_sources/deprecated/twitter'
17
+ Nanoc::DataSource.register '::Nanoc::DataSources::Delicious', :delicious
18
+ Nanoc::DataSource.register '::Nanoc::DataSources::LastFM', :last_fm
19
+ Nanoc::DataSource.register '::Nanoc::DataSources::Twitter', :twitter
20
+
21
+ # Deprecated; use `filesystem_verbose` or `filesystem_unified` instead
22
+ # TODO [in nanoc 4.0] remove me
23
+ Nanoc::DataSource.register '::Nanoc::DataSources::FilesystemVerbose', :filesystem
24
+ Nanoc::DataSource.register '::Nanoc::DataSources::FilesystemUnified', :filesystem_combined
25
+ Nanoc::DataSource.register '::Nanoc::DataSources::FilesystemUnified', :filesystem_compact
26
+ FilesystemCombined = FilesystemUnified
27
+ FilesystemCompact = FilesystemUnified
28
+
29
+ end
@@ -0,0 +1,42 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc::DataSources
4
+
5
+ # @deprecated Fetch data from online data sources manually instead
6
+ class Delicious < Nanoc::DataSource
7
+
8
+ def items
9
+ @items ||= begin
10
+ require 'json'
11
+
12
+ # Get data
13
+ @http_client ||= Nanoc::Extra::CHiCk::Client.new
14
+ status, headers, data = *@http_client.get("http://feeds.delicious.com/v2/json/#{self.config[:username]}")
15
+
16
+ # Parse as JSON
17
+ raw_items = JSON.parse(data)
18
+
19
+ # Convert to items
20
+ raw_items.enum_with_index.map do |raw_item, i|
21
+ # Get data
22
+ content = raw_item['n']
23
+ attributes = {
24
+ :url => raw_item['u'],
25
+ :description => raw_item['d'],
26
+ :tags => raw_item['t'],
27
+ :date => Time.parse(raw_item['dt']),
28
+ :note => raw_item['n'],
29
+ :author => raw_item['a']
30
+ }
31
+ identifier = "/#{i}/"
32
+ mtime = nil
33
+
34
+ # Build item
35
+ Nanoc::Item.new(content, attributes, identifier, mtime)
36
+ end
37
+ end
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,87 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc::DataSources
4
+
5
+ # @deprecated Fetch data from online data sources manually instead
6
+ class LastFM < Nanoc::DataSource
7
+
8
+ def items
9
+ @items ||= begin
10
+ require 'json'
11
+ require 'uri'
12
+
13
+ # Check configuration
14
+ if self.config[:username].nil?
15
+ raise RuntimeError, "LastFM data source requires a username in the configuration"
16
+ end
17
+ if self.config[:api_key].nil?
18
+ raise RuntimeError, "LastFM data source requires an API key in the configuration"
19
+ end
20
+
21
+ # Get data
22
+ @http_client ||= Nanoc::Extra::CHiCk::Client.new
23
+ status, headers, data = *@http_client.get(
24
+ 'http://ws.audioscrobbler.com/2.0/' +
25
+ '?method=user.getRecentTracks' +
26
+ '&format=json' +
27
+ '&user=' + URI.escape(self.config[:username]) +
28
+ '&api_key=' + URI.escape(self.config[:api_key])
29
+ )
30
+
31
+ # Parse as JSON
32
+ parsed_data = JSON.parse(data)
33
+ raw_items = parsed_data['recenttracks']['track']
34
+
35
+ # Convert to items
36
+ raw_items.enum_with_index.map do |raw_item, i|
37
+ # Get artist data
38
+ artist_status, artist_headers, artist_data = *@http_client.get(
39
+ 'http://ws.audioscrobbler.com/2.0/' +
40
+ '?method=artist.getInfo' +
41
+ '&format=json' +
42
+ (
43
+ raw_item['artist']['mbid'].empty? ?
44
+ '&artist=' + URI.escape(raw_item['artist']['#text']) :
45
+ '&mbid=' + URI.escape(raw_item['artist']['mbid'])
46
+ ) +
47
+ '&api_key=' + URI.escape(self.config[:api_key])
48
+ )
49
+
50
+ # Parse as JSON
51
+ parsed_artist_data = JSON.parse(artist_data)
52
+ raw_artist_info = parsed_artist_data['artist']
53
+
54
+ # Build data
55
+ content = ''
56
+
57
+ # Handle track dates
58
+ if raw_item['@attr'] && raw_item['@attr']['nowplaying'] == 'true'
59
+ track_played_at = Time.now
60
+ now_playing = true
61
+ else
62
+ played_at = Time.parse(raw_item['date']['#text'])
63
+ now_playing = false
64
+ end
65
+
66
+ attributes = {
67
+ :name => raw_item['name'],
68
+ :artist => {
69
+ :name => raw_artist_info['name'],
70
+ :url => raw_artist_info['url']
71
+ },
72
+ :url => raw_item['url'],
73
+ :played_at => track_played_at,
74
+ :now_playing => now_playing
75
+ }
76
+ identifier = "/#{i}/"
77
+ mtime = nil
78
+
79
+ # Build item
80
+ Nanoc::Item.new(content, attributes, identifier, mtime)
81
+ end
82
+ end
83
+ end
84
+
85
+ end
86
+
87
+ end
@@ -0,0 +1,38 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc::DataSources
4
+
5
+ # @deprecated Fetch data from online data sources manually instead
6
+ class Twitter < Nanoc::DataSource
7
+
8
+ def items
9
+ @item ||= begin
10
+ require 'json'
11
+
12
+ # Get data
13
+ @http_client ||= Nanoc::Extra::CHiCk::Client.new
14
+ status, headers, data = *@http_client.get("http://twitter.com/statuses/user_timeline/#{self.config[:username]}.json")
15
+
16
+ # Parse as JSON
17
+ raw_items = JSON.parse(data)
18
+
19
+ # Convert to items
20
+ raw_items.enum_with_index.map do |raw_item, i|
21
+ # Get data
22
+ content = raw_item['text']
23
+ attributes = {
24
+ :created_at => raw_item['created_at'],
25
+ :source => raw_item['source']
26
+ }
27
+ identifier = "/#{raw_item['id']}/"
28
+ mtime = Time.parse(raw_item['created_at'])
29
+
30
+ # Build item
31
+ Nanoc::Item.new(content, attributes, identifier, mtime)
32
+ end
33
+ end
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -0,0 +1,299 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc::DataSources
4
+
5
+ # Provides functionality common across all filesystem data sources.
6
+ module Filesystem
7
+
8
+ # The VCS that will be called when adding, deleting and moving files. If
9
+ # no VCS has been set, or if the VCS has been set to `nil`, a dummy VCS
10
+ # will be returned.
11
+ #
12
+ # @return [Nanoc::Extra::VCS, nil] The VCS that will be used.
13
+ def vcs
14
+ @vcs ||= Nanoc::Extra::VCSes::Dummy.new
15
+ end
16
+ attr_writer :vcs
17
+
18
+ # See {Nanoc::DataSource#up}.
19
+ def up
20
+ end
21
+
22
+ # See {Nanoc::DataSource#down}.
23
+ def down
24
+ end
25
+
26
+ # See {Nanoc::DataSource#setup}.
27
+ def setup
28
+ # Create directories
29
+ %w( content layouts ).each do |dir|
30
+ FileUtils.mkdir_p(dir)
31
+ vcs.add(dir)
32
+ end
33
+ end
34
+
35
+ # See {Nanoc::DataSource#items}.
36
+ def items
37
+ load_objects('content', 'item', Nanoc::Item)
38
+ end
39
+
40
+ # See {Nanoc::DataSource#layouts}.
41
+ def layouts
42
+ load_objects('layouts', 'layout', Nanoc::Layout)
43
+ end
44
+
45
+ # See {Nanoc::DataSource#create_item}.
46
+ def create_item(content, attributes, identifier, params={})
47
+ create_object('content', content, attributes, identifier, params)
48
+ end
49
+
50
+ # See {Nanoc::DataSource#create_layout}.
51
+ def create_layout(content, attributes, identifier, params={})
52
+ create_object('layouts', content, attributes, identifier, params)
53
+ end
54
+
55
+ private
56
+
57
+ # Creates a new object (item or layout) on disk in dir_name according to
58
+ # the given identifier. The file will have its attributes taken from the
59
+ # attributes hash argument and its content from the content argument.
60
+ def create_object(dir_name, content, attributes, identifier, params={})
61
+ raise NotImplementedError.new(
62
+ "#{self.class} does not implement ##{name}"
63
+ )
64
+ end
65
+
66
+ # Creates instances of klass corresponding to the files in dir_name. The
67
+ # kind attribute indicates the kind of object that is being loaded and is
68
+ # used solely for debugging purposes.
69
+ #
70
+ # This particular implementation loads objects from a filesystem-based
71
+ # data source where content and attributes can be spread over two separate
72
+ # files. The content and meta-file are optional (but at least one of them
73
+ # needs to be present, obviously) and the content file can start with a
74
+ # metadata section.
75
+ #
76
+ # @see Nanoc::DataSources::Filesystem#load_objects
77
+ def load_objects(dir_name, kind, klass)
78
+ all_split_files_in(dir_name).map do |base_filename, (meta_ext, content_ext)|
79
+ # Get filenames
80
+ meta_filename = filename_for(base_filename, meta_ext)
81
+ content_filename = filename_for(base_filename, content_ext)
82
+
83
+ # Read content and metadata
84
+ is_binary = !!(content_filename && !@site.config[:text_extensions].include?(File.extname(content_filename)[1..-1]))
85
+ if is_binary && klass == Nanoc::Item
86
+ meta = (meta_filename && YAML.load_file(meta_filename)) || {}
87
+ content_or_filename = content_filename
88
+ else
89
+ meta, content_or_filename = parse(content_filename, meta_filename, kind)
90
+ end
91
+
92
+ # Get attributes
93
+ attributes = {
94
+ :filename => content_filename,
95
+ :content_filename => content_filename,
96
+ :meta_filename => meta_filename,
97
+ :extension => content_filename ? ext_of(content_filename)[1..-1] : nil,
98
+ # WARNING :file is deprecated; please create a File object manually
99
+ # using the :content_filename or :meta_filename attributes.
100
+ # TODO [in nanoc 4.0] remove me
101
+ :file => content_filename ? Nanoc::Extra::FileProxy.new(content_filename) : nil
102
+ }.merge(meta)
103
+
104
+ # Get identifier
105
+ if meta_filename
106
+ identifier = identifier_for_filename(meta_filename[(dir_name.length+1)..-1])
107
+ elsif content_filename
108
+ identifier = identifier_for_filename(content_filename[(dir_name.length+1)..-1])
109
+ else
110
+ raise RuntimeError, "meta_filename and content_filename are both nil"
111
+ end
112
+
113
+ # Get modification times
114
+ meta_mtime = meta_filename ? File.stat(meta_filename).mtime : nil
115
+ content_mtime = content_filename ? File.stat(content_filename).mtime : nil
116
+ if meta_mtime && content_mtime
117
+ mtime = meta_mtime > content_mtime ? meta_mtime : content_mtime
118
+ elsif meta_mtime
119
+ mtime = meta_mtime
120
+ elsif content_mtime
121
+ mtime = content_mtime
122
+ else
123
+ raise RuntimeError, "meta_mtime and content_mtime are both nil"
124
+ end
125
+
126
+ # Create layout object
127
+ klass.new(
128
+ content_or_filename, attributes, identifier,
129
+ :binary => is_binary, :mtime => mtime
130
+ )
131
+ end
132
+ end
133
+
134
+ # Finds all items/layouts/... in the given base directory. Returns a hash
135
+ # in which the keys are the file's dirname + basenames, and the values a
136
+ # pair consisting of the metafile extension and the content file
137
+ # extension. The meta file extension or the content file extension can be
138
+ # nil, but not both. Backup files are ignored. For example:
139
+ #
140
+ # {
141
+ # 'content/foo' => [ 'yaml', 'html' ],
142
+ # 'content/bar' => [ 'yaml', nil ],
143
+ # 'content/qux' => [ nil, 'html' ]
144
+ # }
145
+ def all_split_files_in(dir_name)
146
+ # Get all good file names
147
+ filenames = Dir[dir_name + '/**/*'].select { |i| File.file?(i) }
148
+ filenames.reject! { |fn| fn =~ /(~|\.orig|\.rej|\.bak)$/ }
149
+
150
+ # Group by identifier
151
+ grouped_filenames = filenames.group_by { |fn| basename_of(fn) }
152
+
153
+ # Convert values into metafile/content file extension tuple
154
+ grouped_filenames.each_pair do |key, filenames|
155
+ # Divide
156
+ meta_filenames = filenames.select { |fn| ext_of(fn) == '.yaml' }
157
+ content_filenames = filenames.select { |fn| ext_of(fn) != '.yaml' }
158
+
159
+ # Check number of files per type
160
+ if ![ 0, 1 ].include?(meta_filenames.size)
161
+ raise RuntimeError, "Found #{meta_filenames.size} meta files for #{key}; expected 0 or 1"
162
+ end
163
+ if ![ 0, 1 ].include?(content_filenames.size)
164
+ raise RuntimeError, "Found #{content_filenames.size} content files for #{key}; expected 0 or 1"
165
+ end
166
+
167
+ # Reorder elements and convert to extnames
168
+ filenames[0] = meta_filenames[0] ? 'yaml' : nil
169
+ filenames[1] = content_filenames[0] ? ext_of(content_filenames[0])[1..-1] || '': nil
170
+ end
171
+
172
+ # Done
173
+ grouped_filenames
174
+ end
175
+
176
+ # Returns the filename for the given base filename and the extension.
177
+ #
178
+ # If the extension is nil, this function should return nil as well.
179
+ #
180
+ # A simple implementation would simply concatenate the base filename, a
181
+ # period and an extension (which is what the
182
+ # {Nanoc::DataSources::FilesystemCompact} data source does), but other
183
+ # data sources may prefer to implement this differently (for example,
184
+ # {Nanoc::DataSources::FilesystemVerbose} doubles the last part of the
185
+ # basename before concatenating it with a period and the extension).
186
+ def filename_for(base_filename, ext)
187
+ raise NotImplementedError.new(
188
+ "#{self.class} does not implement #filename_for"
189
+ )
190
+ end
191
+
192
+ # Returns the identifier that corresponds with the given filename, which
193
+ # can be the content filename or the meta filename.
194
+ def identifier_for_filename(filename)
195
+ raise NotImplementedError.new(
196
+ "#{self.class} does not implement #identifier_for_filename"
197
+ )
198
+ end
199
+
200
+ # Returns the base name of filename, i.e. filename with the first or all
201
+ # extensions stripped off. By default, all extensions are stripped off,
202
+ # but when allow_periods_in_identifiers is set to true in the site
203
+ # configuration, only the last extension will be stripped .
204
+ def basename_of(filename)
205
+ filename.sub(extension_regex, '')
206
+ end
207
+
208
+ # Returns the extension(s) of filename. Supports multiple extensions.
209
+ # Includes the leading period.
210
+ def ext_of(filename)
211
+ filename =~ extension_regex ? $1 : ''
212
+ end
213
+
214
+ # Returns a regex that is used for determining the extension of a file
215
+ # name. The first match group will be the entire extension, including the
216
+ # leading period.
217
+ def extension_regex
218
+ if @config && @config[:allow_periods_in_identifiers]
219
+ /(\.[^\/\.]+$)/
220
+ else
221
+ /(\.[^\/]+$)/
222
+ end
223
+ end
224
+
225
+ # Parses the file named `filename` and returns an array with its first
226
+ # element a hash with the file's metadata, and with its second element the
227
+ # file content itself.
228
+ def parse(content_filename, meta_filename, kind)
229
+ # Read content and metadata from separate files
230
+ if meta_filename
231
+ content = content_filename ? read(content_filename) : ''
232
+ meta = YAML.load(read(meta_filename)) || {}
233
+
234
+ return [ meta, content ]
235
+ end
236
+
237
+ # Read data
238
+ data = read(content_filename)
239
+
240
+ # Check presence of metadata section
241
+ if data !~ /\A-{3,5}\s*$/
242
+ return [ {}, data ]
243
+ end
244
+
245
+ # Split data
246
+ pieces = data.split(/^(-{5}|-{3})\s*$/)
247
+ if pieces.size < 4
248
+ raise RuntimeError.new(
249
+ "The file '#{content_filename}' appears to start with a metadata section (three or five dashes at the top) but it does not seem to be in the correct format."
250
+ )
251
+ end
252
+
253
+ # Parse
254
+ meta = YAML.load(pieces[2]) || {}
255
+ content = pieces[4..-1].join.strip
256
+
257
+ # Done
258
+ [ meta, content ]
259
+ end
260
+
261
+ # Reads the content of the file with the given name and returns a string
262
+ # in UTF-8 encoding. The original encoding of the string is derived from
263
+ # the default external encoding, but this can be overridden by the
264
+ # “encoding” configuration attribute in the data source configuration.
265
+ def read(filename)
266
+ # Read
267
+ begin
268
+ data = File.read(filename)
269
+ rescue => e
270
+ raise RuntimeError.new("Could not read #{filename}: #{e.inspect}")
271
+ end
272
+
273
+ # Fix
274
+ if data.respond_to?(:encode!)
275
+ if @config && @config[:encoding]
276
+ original_encoding = Encoding.find(@config[:encoding])
277
+ data.force_encoding(@config[:encoding])
278
+ else
279
+ original_encoding = data.encoding
280
+ end
281
+
282
+ data.encode!('UTF-8') rescue raise_encoding_error(filename, original_encoding)
283
+ raise_encoding_error(filename, original_encoding) if !data.valid_encoding?
284
+ end
285
+
286
+ # Remove UTF-8 BOM (ugly)
287
+ data.gsub!("\xEF\xBB\xBF", '')
288
+
289
+ data
290
+ end
291
+
292
+ # Raises an invalid encoding error for the given filename and encoding.
293
+ def raise_encoding_error(filename, encoding)
294
+ raise RuntimeError.new("Could not read #{filename} because the file is not valid #{encoding}.")
295
+ end
296
+
297
+ end
298
+
299
+ end