nanoc2 2.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. data/ChangeLog +3 -0
  2. data/LICENSE +19 -0
  3. data/README +75 -0
  4. data/Rakefile +76 -0
  5. data/bin/nanoc2 +26 -0
  6. data/lib/nanoc2.rb +73 -0
  7. data/lib/nanoc2/base.rb +26 -0
  8. data/lib/nanoc2/base/asset.rb +117 -0
  9. data/lib/nanoc2/base/asset_defaults.rb +21 -0
  10. data/lib/nanoc2/base/asset_rep.rb +282 -0
  11. data/lib/nanoc2/base/binary_filter.rb +44 -0
  12. data/lib/nanoc2/base/code.rb +41 -0
  13. data/lib/nanoc2/base/compiler.rb +67 -0
  14. data/lib/nanoc2/base/core_ext.rb +2 -0
  15. data/lib/nanoc2/base/core_ext/hash.rb +78 -0
  16. data/lib/nanoc2/base/core_ext/string.rb +8 -0
  17. data/lib/nanoc2/base/data_source.rb +286 -0
  18. data/lib/nanoc2/base/defaults.rb +30 -0
  19. data/lib/nanoc2/base/filter.rb +93 -0
  20. data/lib/nanoc2/base/layout.rb +91 -0
  21. data/lib/nanoc2/base/notification_center.rb +66 -0
  22. data/lib/nanoc2/base/page.rb +132 -0
  23. data/lib/nanoc2/base/page_defaults.rb +20 -0
  24. data/lib/nanoc2/base/page_rep.rb +324 -0
  25. data/lib/nanoc2/base/plugin.rb +71 -0
  26. data/lib/nanoc2/base/proxies.rb +5 -0
  27. data/lib/nanoc2/base/proxies/asset_proxy.rb +29 -0
  28. data/lib/nanoc2/base/proxies/asset_rep_proxy.rb +26 -0
  29. data/lib/nanoc2/base/proxies/layout_proxy.rb +25 -0
  30. data/lib/nanoc2/base/proxies/page_proxy.rb +35 -0
  31. data/lib/nanoc2/base/proxies/page_rep_proxy.rb +28 -0
  32. data/lib/nanoc2/base/proxy.rb +37 -0
  33. data/lib/nanoc2/base/router.rb +72 -0
  34. data/lib/nanoc2/base/site.rb +274 -0
  35. data/lib/nanoc2/base/template.rb +64 -0
  36. data/lib/nanoc2/binary_filters.rb +1 -0
  37. data/lib/nanoc2/binary_filters/image_science_thumbnail.rb +28 -0
  38. data/lib/nanoc2/cli.rb +9 -0
  39. data/lib/nanoc2/cli/base.rb +132 -0
  40. data/lib/nanoc2/cli/commands.rb +10 -0
  41. data/lib/nanoc2/cli/commands/autocompile.rb +80 -0
  42. data/lib/nanoc2/cli/commands/compile.rb +312 -0
  43. data/lib/nanoc2/cli/commands/create_layout.rb +85 -0
  44. data/lib/nanoc2/cli/commands/create_page.rb +85 -0
  45. data/lib/nanoc2/cli/commands/create_site.rb +323 -0
  46. data/lib/nanoc2/cli/commands/create_template.rb +76 -0
  47. data/lib/nanoc2/cli/commands/help.rb +69 -0
  48. data/lib/nanoc2/cli/commands/info.rb +125 -0
  49. data/lib/nanoc2/cli/commands/switch.rb +141 -0
  50. data/lib/nanoc2/cli/commands/update.rb +91 -0
  51. data/lib/nanoc2/cli/logger.rb +72 -0
  52. data/lib/nanoc2/data_sources.rb +2 -0
  53. data/lib/nanoc2/data_sources/filesystem.rb +707 -0
  54. data/lib/nanoc2/data_sources/filesystem_combined.rb +495 -0
  55. data/lib/nanoc2/extra.rb +6 -0
  56. data/lib/nanoc2/extra/auto_compiler.rb +285 -0
  57. data/lib/nanoc2/extra/context.rb +22 -0
  58. data/lib/nanoc2/extra/core_ext.rb +2 -0
  59. data/lib/nanoc2/extra/core_ext/hash.rb +54 -0
  60. data/lib/nanoc2/extra/core_ext/time.rb +13 -0
  61. data/lib/nanoc2/extra/file_proxy.rb +29 -0
  62. data/lib/nanoc2/extra/vcs.rb +48 -0
  63. data/lib/nanoc2/extra/vcses.rb +5 -0
  64. data/lib/nanoc2/extra/vcses/bazaar.rb +21 -0
  65. data/lib/nanoc2/extra/vcses/dummy.rb +20 -0
  66. data/lib/nanoc2/extra/vcses/git.rb +21 -0
  67. data/lib/nanoc2/extra/vcses/mercurial.rb +21 -0
  68. data/lib/nanoc2/extra/vcses/subversion.rb +21 -0
  69. data/lib/nanoc2/filters.rb +16 -0
  70. data/lib/nanoc2/filters/bluecloth.rb +13 -0
  71. data/lib/nanoc2/filters/erb.rb +19 -0
  72. data/lib/nanoc2/filters/erubis.rb +14 -0
  73. data/lib/nanoc2/filters/haml.rb +21 -0
  74. data/lib/nanoc2/filters/markaby.rb +14 -0
  75. data/lib/nanoc2/filters/maruku.rb +14 -0
  76. data/lib/nanoc2/filters/old.rb +19 -0
  77. data/lib/nanoc2/filters/rainpress.rb +13 -0
  78. data/lib/nanoc2/filters/rdiscount.rb +13 -0
  79. data/lib/nanoc2/filters/rdoc.rb +23 -0
  80. data/lib/nanoc2/filters/redcloth.rb +14 -0
  81. data/lib/nanoc2/filters/relativize_paths.rb +16 -0
  82. data/lib/nanoc2/filters/relativize_paths_in_css.rb +16 -0
  83. data/lib/nanoc2/filters/relativize_paths_in_html.rb +16 -0
  84. data/lib/nanoc2/filters/rubypants.rb +14 -0
  85. data/lib/nanoc2/filters/sass.rb +18 -0
  86. data/lib/nanoc2/helpers.rb +9 -0
  87. data/lib/nanoc2/helpers/blogging.rb +217 -0
  88. data/lib/nanoc2/helpers/capturing.rb +63 -0
  89. data/lib/nanoc2/helpers/filtering.rb +54 -0
  90. data/lib/nanoc2/helpers/html_escape.rb +25 -0
  91. data/lib/nanoc2/helpers/link_to.rb +113 -0
  92. data/lib/nanoc2/helpers/render.rb +49 -0
  93. data/lib/nanoc2/helpers/tagging.rb +56 -0
  94. data/lib/nanoc2/helpers/text.rb +38 -0
  95. data/lib/nanoc2/helpers/xml_sitemap.rb +63 -0
  96. data/lib/nanoc2/routers.rb +3 -0
  97. data/lib/nanoc2/routers/default.rb +54 -0
  98. data/lib/nanoc2/routers/no_dirs.rb +66 -0
  99. data/lib/nanoc2/routers/versioned.rb +79 -0
  100. metadata +185 -0
@@ -0,0 +1,5 @@
1
+ require 'nanoc2/extra/vcses/bazaar'
2
+ require 'nanoc2/extra/vcses/dummy'
3
+ require 'nanoc2/extra/vcses/git'
4
+ require 'nanoc2/extra/vcses/mercurial'
5
+ require 'nanoc2/extra/vcses/subversion'
@@ -0,0 +1,21 @@
1
+ module Nanoc2::Extra::VCSes
2
+
3
+ class Bazaar < Nanoc2::Extra::VCS
4
+
5
+ identifiers :bazaar, :bzr
6
+
7
+ def add(filename)
8
+ system('bzr', 'add', filename)
9
+ end
10
+
11
+ def remove(filename)
12
+ system('bzr', 'rm', filename)
13
+ end
14
+
15
+ def move(src, dst)
16
+ system('bzr', 'mv', src, dst)
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,20 @@
1
+ module Nanoc2::Extra::VCSes
2
+
3
+ class Dummy < Nanoc2::Extra::VCS
4
+
5
+ identifiers :dummy
6
+
7
+ def add(filename)
8
+ end
9
+
10
+ def remove(filename)
11
+ FileUtils.rm_rf(filename)
12
+ end
13
+
14
+ def move(src, dst)
15
+ FileUtils.move(src, dst)
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,21 @@
1
+ module Nanoc2::Extra::VCSes
2
+
3
+ class Git < Nanoc2::Extra::VCS
4
+
5
+ identifiers :git
6
+
7
+ def add(filename)
8
+ system('git', 'add', filename)
9
+ end
10
+
11
+ def remove(filename)
12
+ system('git', 'rm', filename)
13
+ end
14
+
15
+ def move(src, dst)
16
+ system('git', 'mv', src, dst)
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,21 @@
1
+ module Nanoc2::Extra::VCSes
2
+
3
+ class Mercurial < Nanoc2::Extra::VCS
4
+
5
+ identifiers :mercurial, :hg
6
+
7
+ def add(filename)
8
+ system('hg', 'add', filename)
9
+ end
10
+
11
+ def remove(filename)
12
+ system('hg', 'rm', filename)
13
+ end
14
+
15
+ def move(src, dst)
16
+ system('hg', 'mv', src, dst)
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,21 @@
1
+ module Nanoc2::Extra::VCSes
2
+
3
+ class Subversion < Nanoc2::Extra::VCS
4
+
5
+ identifiers :subversion, :svn
6
+
7
+ def add(filename)
8
+ system('svn', 'add', filename)
9
+ end
10
+
11
+ def remove(filename)
12
+ system('svn', 'rm', filename)
13
+ end
14
+
15
+ def move(src, dst)
16
+ system('svn', 'mv', src, dst)
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,16 @@
1
+ require 'nanoc2/filters/bluecloth'
2
+ require 'nanoc2/filters/erb'
3
+ require 'nanoc2/filters/erubis'
4
+ require 'nanoc2/filters/haml'
5
+ require 'nanoc2/filters/markaby'
6
+ require 'nanoc2/filters/maruku'
7
+ require 'nanoc2/filters/old'
8
+ require 'nanoc2/filters/rainpress'
9
+ require 'nanoc2/filters/rdiscount'
10
+ require 'nanoc2/filters/rdoc'
11
+ require 'nanoc2/filters/redcloth'
12
+ require 'nanoc2/filters/relativize_paths'
13
+ require 'nanoc2/filters/relativize_paths_in_css'
14
+ require 'nanoc2/filters/relativize_paths_in_html'
15
+ require 'nanoc2/filters/rubypants'
16
+ require 'nanoc2/filters/sass'
@@ -0,0 +1,13 @@
1
+ module Nanoc2::Filters
2
+ class BlueCloth < Nanoc2::Filter
3
+
4
+ identifiers :bluecloth
5
+
6
+ def run(content)
7
+ require 'bluecloth'
8
+
9
+ ::BlueCloth.new(content).to_html
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ module Nanoc2::Filters
2
+ class ERB < Nanoc2::Filter
3
+
4
+ identifiers :erb
5
+
6
+ def run(content)
7
+ require 'erb'
8
+
9
+ # Create context
10
+ context = ::Nanoc2::Extra::Context.new(assigns)
11
+
12
+ # Get result
13
+ erb = ::ERB.new(content)
14
+ erb.filename = filename
15
+ erb.result(context.get_binding)
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ module Nanoc2::Filters
2
+ class Erubis < Nanoc2::Filter
3
+
4
+ identifiers :erubis
5
+
6
+ def run(content)
7
+ require 'erubis'
8
+
9
+ # Get result
10
+ ::Erubis::Eruby.new(content).evaluate(assigns)
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,21 @@
1
+ module Nanoc2::Filters
2
+ class Haml < Nanoc2::Filter
3
+
4
+ identifiers :haml
5
+
6
+ def run(content)
7
+ require 'haml'
8
+
9
+ # Get options
10
+ options = @obj_rep.attribute_named(:haml_options) || {}
11
+ options[:filename] = filename
12
+
13
+ # Create context
14
+ context = ::Nanoc2::Extra::Context.new(assigns)
15
+
16
+ # Get result
17
+ ::Haml::Engine.new(content, options).render(context, assigns)
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ module Nanoc2::Filters
2
+ class Markaby < Nanoc2::Filter
3
+
4
+ identifiers :markaby
5
+
6
+ def run(content)
7
+ require 'markaby'
8
+
9
+ # Get result
10
+ ::Markaby::Builder.new(assigns).instance_eval(content).to_s
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module Nanoc2::Filters
2
+ class Maruku < Nanoc2::Filter
3
+
4
+ identifiers :maruku
5
+
6
+ def run(content)
7
+ require 'maruku'
8
+
9
+ # Get result
10
+ ::Maruku.new(content).to_html
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ module Nanoc2::Filters
2
+ class Old < Nanoc2::Filter
3
+
4
+ identifiers :eruby, :markdown, :smartypants, :textile
5
+
6
+ def run(content)
7
+ raise Nanoc2::Error.new(
8
+ "The 'eruby', markdown', 'smartypants' and 'textile' filters no " +
9
+ "longer exist. Instead, use the following filters:\n" +
10
+ "\n" +
11
+ "* for Markdown: bluecloth, rdiscount, redcloth\n" +
12
+ "* for Textile: redcloth\n" +
13
+ "* for embedded Ruby: erb, erubis\n" +
14
+ "* for Smartypants: rubypants"
15
+ )
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ module Nanoc2::Filters
2
+ class Rainpress < Nanoc2::Filter
3
+
4
+ identifier :rainpress
5
+
6
+ def run(content)
7
+ require 'rainpress'
8
+
9
+ ::Rainpress.compress(content)
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Nanoc2::Filters
2
+ class RDiscount < Nanoc2::Filter
3
+
4
+ identifiers :rdiscount
5
+
6
+ def run(content)
7
+ require 'rdiscount'
8
+
9
+ ::RDiscount.new(content).to_html
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ module Nanoc2::Filters
2
+ class RDoc < Nanoc2::Filter
3
+
4
+ identifiers :rdoc
5
+
6
+ def run(content)
7
+ begin
8
+ # new RDoc
9
+ require 'rdoc/markup'
10
+ require 'rdoc/markup/to_html'
11
+
12
+ ::RDoc::Markup.new.convert(content, ::RDoc::Markup::ToHtml.new)
13
+ rescue LoadError
14
+ # old RDoc
15
+ require 'rdoc/markup/simple_markup'
16
+ require 'rdoc/markup/simple_markup/to_html'
17
+
18
+ ::SM::SimpleMarkup.new.convert(content, ::SM::ToHtml.new)
19
+ end
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,14 @@
1
+ module Nanoc2::Filters
2
+ class RedCloth < Nanoc2::Filter
3
+
4
+ identifiers :redcloth
5
+
6
+ def run(content)
7
+ require 'redcloth'
8
+
9
+ # Get result
10
+ ::RedCloth.new(content).to_html
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ module Nanoc2::Filters
2
+ class RelativizePaths < Nanoc2::Filter
3
+
4
+ identifier :relativize_paths
5
+
6
+ def run(content)
7
+ raise RuntimeError.new(
8
+ "The relativize_paths filter itself does not exist anymore. " +
9
+ "If you want to relativize paths in HTML, use the " +
10
+ "relativize_paths_in_html filter; if you want to relativize paths " +
11
+ "in CSS, use the relativize_paths_in_css filter."
12
+ )
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module Nanoc2::Filters
2
+ class RelativizePathsInCSS < Nanoc2::Filter
3
+
4
+ identifier :relativize_paths_in_css
5
+
6
+ require 'nanoc2/helpers/link_to'
7
+ include Nanoc2::Helpers::LinkTo
8
+
9
+ def run(content)
10
+ content.gsub(/url\((['"]?)(\/.+?)\1\)/) do
11
+ 'url(' + $1 + relative_path_to($2) + $1 + ')'
12
+ end
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module Nanoc2::Filters
2
+ class RelativizePathsInHTML < Nanoc2::Filter
3
+
4
+ identifier :relativize_paths_in_html
5
+
6
+ require 'nanoc2/helpers/link_to'
7
+ include Nanoc2::Helpers::LinkTo
8
+
9
+ def run(content)
10
+ content.gsub(/(src|href)=(['"]?)(\/.+?)\2([ >])/) do
11
+ $1 + '=' + $2 + relative_path_to($3) + $2 + $4
12
+ end
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ module Nanoc2::Filters
2
+ class SmartyPants < Nanoc2::Filter
3
+
4
+ identifiers :rubypants
5
+
6
+ def run(content)
7
+ require 'rubypants'
8
+
9
+ # Get result
10
+ ::RubyPants.new(content).to_html
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ module Nanoc2::Filters
2
+ class Sass < Nanoc2::Filter
3
+
4
+ identifiers :sass
5
+
6
+ def run(content)
7
+ require 'sass'
8
+
9
+ # Get options
10
+ options = @obj_rep.attribute_named(:sass_options) || {}
11
+ options[:filename] = filename
12
+
13
+ # Get result
14
+ ::Sass::Engine.new(content, options).render
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,9 @@
1
+ require 'nanoc2/helpers/blogging'
2
+ require 'nanoc2/helpers/capturing'
3
+ require 'nanoc2/helpers/filtering'
4
+ require 'nanoc2/helpers/html_escape'
5
+ require 'nanoc2/helpers/link_to'
6
+ require 'nanoc2/helpers/render'
7
+ require 'nanoc2/helpers/tagging'
8
+ require 'nanoc2/helpers/text'
9
+ require 'nanoc2/helpers/xml_sitemap'
@@ -0,0 +1,217 @@
1
+ module Nanoc2::Helpers
2
+
3
+ # Nanoc2::Helpers::Blogging provides some functionality for building blogs,
4
+ # such as finding articles and constructing feeds.
5
+ #
6
+ # This helper has a few requirements. First, all blog articles should have
7
+ # the following attributes:
8
+ #
9
+ # * 'kind', set to 'article'.
10
+ #
11
+ # * 'created_at', set to the creation timestamp.
12
+ #
13
+ # Some functions in this blogging helper, such as the +atom_feed+ function,
14
+ # require additional attributes to be set; these attributes are described in
15
+ # the documentation for these functions.
16
+ #
17
+ # The two main functions are sorted_articles and atom_feed.
18
+ #
19
+ # To activate this helper, +include+ it, like this:
20
+ #
21
+ # include Nanoc2::Helpers::Blogging
22
+ module Blogging
23
+
24
+ # Returns an unsorted list of articles.
25
+ def articles
26
+ @pages.select { |page| page.kind == 'article' }
27
+ end
28
+
29
+ # Returns a list of articles, sorted by descending creation date (so newer
30
+ # articles appear first).
31
+ def sorted_articles
32
+ articles.sort_by { |a| a.created_at }.reverse
33
+ end
34
+
35
+ # Returns a string representing the atom feed containing recent articles,
36
+ # sorted by descending creation date. +params+ is a hash where the
37
+ # following keys can be set:
38
+ #
39
+ # +limit+:: The maximum number of articles to show. Defaults to 5.
40
+ #
41
+ # +articles+:: A list of articles to include in the feed. Defaults to the
42
+ # list of articles returned by the articles function.
43
+ #
44
+ # +content_proc+:: A proc that returns the content of the given article,
45
+ # passed as a parameter. By default, given the argument
46
+ # +article+, this proc will return +article.content+.
47
+ # This function may not return nil.
48
+ #
49
+ # +excerpt_proc+:: A proc that returns the excerpt of the given article,
50
+ # passed as a parameter. By default, given the argument
51
+ # +article+, this proc will return +article.excerpt+.
52
+ # This function may return nil.
53
+ #
54
+ # The following attributes must be set on blog articles:
55
+ #
56
+ # * 'title', containing the title of the blog post.
57
+ #
58
+ # * all other attributes mentioned above.
59
+ #
60
+ # The following attributes can optionally be set on blog articles to
61
+ # change the behaviour of the Atom feed:
62
+ #
63
+ # * 'excerpt', containing an excerpt of the article, usually only a few
64
+ # lines long.
65
+ #
66
+ # * 'custom_path_in_feed', containing the path that will be used instead
67
+ # of the normal path in the feed. This can be useful when including
68
+ # non-outputted pages in a feed; such pages could have their custom feed
69
+ # path set to the blog path instead, for example.
70
+ #
71
+ # The feed will also include dates on which the articles were updated.
72
+ # These are generated automatically; the way this happens depends on the
73
+ # used data source (the filesystem data source checks the file mtimes, for
74
+ # instance).
75
+ #
76
+ # The feed page will need to have the following attributes:
77
+ #
78
+ # * 'base_url', containing the URL to the site, without trailing slash.
79
+ # For example, if the site is at "http://example.com/", the base_url
80
+ # would be "http://example.com". It is probably a good idea to define
81
+ # this in the page defaults, i.e. the 'meta.yaml' file (at least if the
82
+ # filesystem data source is being used, which is probably the case).
83
+ #
84
+ # * 'title', containing the title of the feed, which is usually also the
85
+ # title of the blog.
86
+ #
87
+ # * 'author_name', containing the name of the page's author. This will
88
+ # likely be a global attribute, unless the site is managed by several
89
+ # people/
90
+ #
91
+ # * 'author_uri', containing the URI for the page's author, such as the
92
+ # author's web site URL. This will also likely be a global attribute.
93
+ #
94
+ # The feed page can have the following optional attributes:
95
+ #
96
+ # * 'feed_url', containing the custom URL of the feed. This can be useful
97
+ # when the private feed URL shouldn't be exposed; for example, when
98
+ # using FeedBurner this would be set to the public FeedBurner URL.
99
+ #
100
+ # To construct a feed, create a blank page with no layout, only the 'erb'
101
+ # (or 'erubis') filter, and an 'xml' extension. It may also be useful to
102
+ # set 'is_hidden' to true, so that helpers such as the sitemap helper will
103
+ # ignore the page. The content of the feed page should be:
104
+ #
105
+ # <%= atom_feed %>
106
+ def atom_feed(params={})
107
+ require 'builder'
108
+
109
+ # Extract parameters
110
+ limit = params[:limit] || 5
111
+ relevant_articles = params[:articles] || articles || []
112
+ content_proc = params[:content_proc] || lambda { |a| a.content }
113
+ excerpt_proc = params[:excerpt_proc] || lambda { |a| a.excerpt }
114
+
115
+ # Check feed page attributes
116
+ if @page.base_url.nil?
117
+ raise RuntimeError.new('Cannot build Atom feed: feed page has no base_url')
118
+ end
119
+ if @page.title.nil?
120
+ raise RuntimeError.new('Cannot build Atom feed: feed page has no title')
121
+ end
122
+ if @page.author_name.nil?
123
+ raise RuntimeError.new('Cannot build Atom feed: feed page has no author_name')
124
+ end
125
+ if @page.author_uri.nil?
126
+ raise RuntimeError.new('Cannot build Atom feed: feed page has no author_uri')
127
+ end
128
+
129
+ # Check article attributes
130
+ if relevant_articles.empty?
131
+ raise RuntimeError.new('Cannot build Atom feed: no articles')
132
+ end
133
+ if relevant_articles.any? { |a| a.created_at.nil? }
134
+ raise RuntimeError.new('Cannot build Atom feed: one or more articles lack created_at')
135
+ end
136
+
137
+ # Get sorted relevant articles
138
+ sorted_relevant_articles = relevant_articles.sort_by { |a| a.created_at }.reverse.first(limit)
139
+
140
+ # Get most recent article
141
+ last_article = sorted_relevant_articles.first
142
+
143
+ # Create builder
144
+ buffer = ''
145
+ xml = Builder::XmlMarkup.new(:target => buffer, :indent => 2)
146
+
147
+ # Build feed
148
+ xml.instruct!
149
+ xml.feed(:xmlns => 'http://www.w3.org/2005/Atom') do
150
+ # Add primary attributes
151
+ xml.id @page.base_url + '/'
152
+ xml.title @page.title
153
+
154
+ # Add date
155
+ xml.updated last_article.created_at.to_iso8601_time
156
+
157
+ # Add links
158
+ xml.link(:rel => 'alternate', :href => @page.base_url)
159
+ xml.link(:rel => 'self', :href => feed_url)
160
+
161
+ # Add author information
162
+ xml.author do
163
+ xml.name @page.author_name
164
+ xml.uri @page.author_uri
165
+ end
166
+
167
+ # Add articles
168
+ sorted_relevant_articles.each do |a|
169
+ xml.entry do
170
+ # Add primary attributes
171
+ xml.id atom_tag_for(a)
172
+ xml.title a.title, :type => 'html'
173
+
174
+ # Add dates
175
+ xml.published a.created_at.to_iso8601_time
176
+ xml.updated a.mtime.to_iso8601_time
177
+
178
+ # Add link
179
+ xml.link(:rel => 'alternate', :href => url_for(a))
180
+
181
+ # Add content
182
+ summary = excerpt_proc.call(a)
183
+ xml.content content_proc.call(a), :type => 'html'
184
+ xml.summary summary, :type => 'html' unless summary.nil?
185
+ end
186
+ end
187
+ end
188
+
189
+ buffer
190
+ end
191
+
192
+ # Returns the URL for the given page. It will return the URL containing
193
+ # the custom path in the feed if possible, otherwise the normal path.
194
+ def url_for(page)
195
+ @page.base_url + (page.custom_path_in_feed || page.path)
196
+ end
197
+
198
+ # Returns the URL of the feed. It will return the custom feed URL if set,
199
+ # or otherwise the normal feed URL.
200
+ def feed_url
201
+ @page[:feed_url] || @page.base_url + @page.path
202
+ end
203
+
204
+ # Returns an URI containing an unique ID for the given page. This will be
205
+ # used in the Atom feed to uniquely identify articles. These IDs are
206
+ # created using a procedure suggested by Mark Pilgrim in this blog post:
207
+ # http://diveintomark.org/archives/2004/05/28/howto-atom-id.
208
+ def atom_tag_for(page)
209
+ hostname = @page.base_url.sub(/.*:\/\/(.+?)\/?$/, '\1')
210
+ formatted_date = page.created_at.to_iso8601_date
211
+
212
+ 'tag:' + hostname + ',' + formatted_date + ':' + page.path
213
+ end
214
+
215
+ end
216
+
217
+ end