middleman-blog 3.4.1 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -6
  3. data/CHANGELOG.md +32 -1
  4. data/Gemfile +5 -9
  5. data/README.md +3 -2
  6. data/features/article_cli.feature +10 -1
  7. data/features/custom_collections.feature +11 -0
  8. data/features/language.feature +82 -0
  9. data/features/multiblog.feature +4 -2
  10. data/features/permalink-data.feature +12 -0
  11. data/features/summary.feature +13 -0
  12. data/features/support/env.rb +3 -0
  13. data/features/tags.feature +18 -1
  14. data/features/time_zone.feature +1 -1
  15. data/fixtures/custom-article-template-app/config.rb +3 -0
  16. data/fixtures/custom-article-template-app/my_custom_article.tt +7 -0
  17. data/fixtures/custom-article-template-app/source/index.html.erb +9 -0
  18. data/fixtures/custom-article-template-app/source/layout.erb +30 -0
  19. data/fixtures/custom-collections-sources-app/config.rb +11 -0
  20. data/fixtures/custom-collections-sources-app/source/articles/2011-01-02-another-article.html.markdown +8 -0
  21. data/fixtures/custom-collections-sources-app/source/category.html.erb +7 -0
  22. data/fixtures/custom-collections-sources-app/source/index.html.erb +5 -0
  23. data/fixtures/custom-collections-sources-app/source/layout.erb +13 -0
  24. data/fixtures/custom-collections-sources-app/source/news/2011-01-01-new-article.html.markdown +7 -0
  25. data/fixtures/language-app/config.rb +2 -0
  26. data/fixtures/language-app/locales/en.yml +4 -0
  27. data/fixtures/language-app/locales/ru.yml +4 -0
  28. data/fixtures/language-app/source/2013-09-07-english-article-with-lang-in-frontmatter.html.erb +6 -0
  29. data/fixtures/language-app/source/2013-09-07-russian-article-with-lang-in-frontmatter.html.erb +6 -0
  30. data/fixtures/language-app/source/en/2013-09-07-english-article-with-lang-in-path.html.erb +5 -0
  31. data/fixtures/language-app/source/layouts/layout.erb +8 -0
  32. data/fixtures/language-app/source/localizable/index.html.erb +5 -0
  33. data/fixtures/language-app/source/ru/2013-09-07-russian-article-with-lang-in-path.html.erb +5 -0
  34. data/fixtures/multiblog-app/source/blog1/index.html.erb +7 -0
  35. data/fixtures/permalink-data-app/config.rb +5 -0
  36. data/fixtures/permalink-data-app/source/index.html.erb +3 -0
  37. data/fixtures/permalink-data-app/source/layout.erb +14 -0
  38. data/fixtures/permalink-data-app/source/news/2011-01-01-new-article.html.markdown +7 -0
  39. data/fixtures/time-zone-app/source/blog/2013-06-24-hello.html.erb +1 -0
  40. data/lib/middleman-blog.rb +3 -8
  41. data/lib/middleman-blog/blog_article.rb +96 -60
  42. data/lib/middleman-blog/blog_data.rb +78 -76
  43. data/lib/middleman-blog/calendar_pages.rb +87 -119
  44. data/lib/middleman-blog/commands/article.rb +20 -14
  45. data/lib/middleman-blog/custom_pages.rb +30 -64
  46. data/lib/middleman-blog/extension.rb +175 -0
  47. data/lib/middleman-blog/helpers.rb +152 -0
  48. data/lib/middleman-blog/paginator.rb +127 -123
  49. data/lib/middleman-blog/tag_pages.rb +27 -45
  50. data/lib/middleman-blog/template.rb +17 -15
  51. data/lib/middleman-blog/template/config.tt +30 -33
  52. data/lib/middleman-blog/template/source/layout.erb +1 -0
  53. data/lib/middleman-blog/uri_templates.rb +58 -0
  54. data/lib/middleman-blog/version.rb +1 -1
  55. data/middleman-blog.gemspec +4 -1
  56. metadata +75 -9
  57. data/Gemfile-3.0 +0 -27
  58. data/lib/middleman-blog/extension_3_0.rb +0 -248
  59. data/lib/middleman-blog/extension_3_1.rb +0 -278
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ </head>
5
+ <body>
6
+ <% if is_blog_article? %>
7
+ <%= current_article.url %>
8
+ <%= yield %>
9
+ <% else %>
10
+ <%= yield %>
11
+ <% end %>
12
+ </body>
13
+ </html>
@@ -0,0 +1,7 @@
1
+ ---
2
+ title: "Newer Article"
3
+ date: 2011-01-01
4
+ tags: foo, bar
5
+ ---
6
+
7
+ Newer Article Content
@@ -0,0 +1,2 @@
1
+ activate :i18n
2
+ activate :blog
@@ -0,0 +1,4 @@
1
+ en:
2
+ hello: "Hello, world!"
3
+ language: "Language"
4
+ ok: "OK"
@@ -0,0 +1,4 @@
1
+ ru:
2
+ hello: "Привет, мир!"
3
+ language: "Язык"
4
+ ok: "отлично"
@@ -0,0 +1,6 @@
1
+ ---
2
+ title: "English article with lang in frontmatter"
3
+ lang: en
4
+ ---
5
+
6
+ <p>Some text in English. All is <%= t :ok %>.</p>
@@ -0,0 +1,6 @@
1
+ ---
2
+ title: "Русская статья с меткой языка во вступлении"
3
+ lang: ru
4
+ ---
5
+
6
+ <p>Некоторый текст на русском языке. Всё <%= t :ok %>.</p>
@@ -0,0 +1,5 @@
1
+ ---
2
+ title: "English article with lang in path"
3
+ ---
4
+
5
+ <p>Some text in English. All is <%= t :ok %>.</p>
@@ -0,0 +1,8 @@
1
+ <html>
2
+ <head></head>
3
+ <body>
4
+ <h1><%= t :hello %></h1>
5
+ <p><%= t :language %>: <%= lang %></p>
6
+ <%= yield %>
7
+ </body>
8
+ </html>
@@ -0,0 +1,5 @@
1
+ <% blog.local_articles.each do |article| %>
2
+ <article>
3
+ <%= article.body %>
4
+ </article>
5
+ <% end %>
@@ -0,0 +1,5 @@
1
+ ---
2
+ title: "Русская статья с меткой языка в пути к файлу"
3
+ ---
4
+
5
+ <p>Некоторый текст на русском языке. Всё <%= t :ok %>.</p>
@@ -0,0 +1,7 @@
1
+ ---
2
+ blog: blog_number_1
3
+ pageable: true
4
+ per_page: 10
5
+ ---
6
+
7
+ Paginate: <%= paginate %>
@@ -0,0 +1,5 @@
1
+ require "middleman-blog"
2
+ activate :blog do |blog|
3
+ blog.sources = ":category/:year-:month-:day-:title.html"
4
+ blog.permalink = ":category/:custom-:year-:month-:day-:title.html"
5
+ end
@@ -0,0 +1,3 @@
1
+ <% blog.articles[0...12].each do |article| %>
2
+ <li><a href="<%= article.url %>"><%= article.title %></a> <time><%= article.date.strftime('%b %e') %></time></li>
3
+ <% end %>
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ </head>
5
+ <body>
6
+ <% if is_blog_article? %>
7
+ URL: <%= current_article.url %>
8
+ Category: <%= current_article.metadata[:page]['category'] %>
9
+ <%= yield %>
10
+ <% else %>
11
+ <%= yield %>
12
+ <% end %>
13
+ </body>
14
+ </html>
@@ -0,0 +1,7 @@
1
+ ---
2
+ title: "Newer Article"
3
+ date: 2011-01-01
4
+ custom: "A custom string"
5
+ ---
6
+
7
+ Newer Article Content
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  title: Time zone
3
3
  layout: false
4
+ date: 2013-06-24 23:05:52 +09:00
4
5
  ---
5
6
  <%= Time.zone.to_s %>
@@ -5,11 +5,6 @@ require "middleman-blog/template"
5
5
  require "middleman-blog/commands/article"
6
6
 
7
7
  ::Middleman::Extensions.register(:blog) do
8
- if defined?(::Middleman::Extension)
9
- require "middleman-blog/extension_3_1"
10
- ::Middleman::BlogExtension
11
- else
12
- require "middleman-blog/extension_3_0"
13
- ::Middleman::Blog
14
- end
15
- end
8
+ require "middleman-blog/extension"
9
+ ::Middleman::BlogExtension
10
+ end
@@ -1,95 +1,93 @@
1
+ # -*- coding: utf-8 -*-
1
2
  require 'active_support/time_with_zone'
2
3
  require 'active_support/core_ext/time/calculations'
3
4
 
4
5
  module Middleman
5
6
  module Blog
6
- # A module that adds blog-article methods to Resources.
7
+ # A module that adds blog-article-specific methods to Resources.
8
+ # A {BlogArticle} can be retrieved via {Blog::Helpers#current_article} or
9
+ # methods on {BlogData} (like {BlogData#articles}).
10
+ # @see http://rdoc.info/github/middleman/middleman/Middleman/Sitemap/Resource Middleman::Sitemap::Resource
7
11
  module BlogArticle
8
12
  def self.extended(base)
9
13
  base.class.send(:attr_accessor, :blog_controller)
10
14
  end
11
15
 
16
+ # A reference to the {BlogData} for this article's blog.
17
+ # @return [BlogData]
12
18
  def blog_data
13
- if self.blog_controller
14
- self.blog_controller.data
15
- else
16
- app.blog
17
- end
19
+ blog_controller.data
18
20
  end
19
21
 
22
+ # The options for this article's blog.
23
+ # @return [ConfigurationManager]
20
24
  def blog_options
21
- if self.blog_controller
22
- self.blog_controller.options
23
- else
24
- app.blog.options
25
- end
25
+ blog_controller.options
26
26
  end
27
27
 
28
- # Render this resource
28
+ # Render this resource to a string with the appropriate layout.
29
+ # Called automatically by Middleman.
29
30
  # @return [String]
30
31
  def render(opts={}, locs={}, &block)
31
32
  if opts[:layout].nil?
32
- if metadata[:options] && !metadata[:options][:layout].nil?
33
- opts[:layout] = metadata[:options][:layout]
34
- end
33
+ opts[:layout] = metadata[:options][:layout]
35
34
  opts[:layout] = blog_options.layout if opts[:layout].nil?
35
+ # Convert to a string unless it's a boolean
36
36
  opts[:layout] = opts[:layout].to_s if opts[:layout].is_a? Symbol
37
37
  end
38
38
 
39
39
  content = super(opts, locs, &block)
40
40
 
41
41
  unless opts[:keep_separator]
42
- if content.match(blog_options.summary_separator)
43
- content.sub!(blog_options.summary_separator, "")
44
- end
42
+ content.sub!(blog_options.summary_separator, "")
45
43
  end
46
44
 
47
45
  content
48
46
  end
49
47
 
50
- # The title of the article, set from frontmatter
48
+ # The title of the article, set from frontmatter.
51
49
  # @return [String]
52
50
  def title
53
51
  data["title"]
54
52
  end
55
53
 
56
- # Whether or not this article has been published
54
+ # Whether or not this article has been published.
57
55
  #
58
56
  # An article is considered published in the following scenarios:
59
57
  #
60
- # 1. frontmatter does not set published to false and either
61
- # 2. published_future_dated is true or
62
- # 3. article date is after the current time
58
+ # 1. Frontmatter does not set +published+ to false and either
59
+ # 2. The blog option +published_future_dated+ is true or
60
+ # 3. The article's date is after the current time
63
61
  # @return [Boolean]
64
62
  def published?
65
- (data["published"] != false) and
66
- (blog_options.publish_future_dated || date <= Time.current)
63
+ data["published"] != false && (blog_options.publish_future_dated || date <= Time.current)
67
64
  end
68
65
 
69
- # The body of this article, in HTML. This is for
66
+ # The body of this article, in HTML (no layout). This is for
70
67
  # things like RSS feeds or lists of articles - individual
71
68
  # articles will automatically be rendered from their
72
69
  # template.
73
70
  # @return [String]
74
71
  def body
75
- render(:layout => false)
72
+ render layout: false
76
73
  end
77
74
 
78
75
  # The summary for this article, in HTML. The summary is either
79
- # everything before the summary separator (set via :summary_separator
80
- # and defaulting to "READMORE") or the first :summary_length
81
- # characters of the post.
76
+ # everything before the summary separator (set via the blog option
77
+ # +summary_separator+ and defaulting to "READMORE") or the first
78
+ # +summary_length+ characters of the post.
82
79
  #
83
- # :summary_generator can be set to a Proc in order to provide
84
- # custom summary generation. The Proc is provided a parameter
85
- # which is the rendered content of the article (without layout), the
80
+ # The blog option +summary_generator+ can be set to a +Proc+ in order to provide
81
+ # custom summary generation. The +Proc+ is provided
82
+ # the rendered content of the article (without layout), the
86
83
  # desired length to trim the summary to, and the ellipsis string to use.
84
+ # Otherwise the {#default_summary_generator} will be used.
87
85
  #
88
86
  # @param [Number] length How many characters to trim the summary to.
89
87
  # @param [String] ellipsis The ellipsis string to use when content is trimmed.
90
88
  # @return [String]
91
89
  def summary(length=blog_options.summary_length, ellipsis='...')
92
- rendered = render(:layout => false, :keep_separator => true)
90
+ rendered = render layout: false, keep_separator: true
93
91
 
94
92
  if blog_options.summary_separator && rendered.match(blog_options.summary_separator)
95
93
  rendered.split(blog_options.summary_separator).first
@@ -100,12 +98,18 @@ module Middleman
100
98
  end
101
99
  end
102
100
 
101
+ # The default summary generator first tries to find the +summary_separator+ and
102
+ # take the text before it. If that doesn't work, it will truncate text without splitting
103
+ # the middle of an HTML tag, using a Nokogiri-based {TruncateHTML} utility.
104
+ #
105
+ # @param [String] rendered The rendered blog article
106
+ # @param [Integer] length The length in characters to truncate to.
107
+ # -1 or +nil+ will return the whole article.
103
108
  def default_summary_generator(rendered, length, ellipsis)
104
- require 'middleman-blog/truncate_html'
105
-
106
- if rendered =~ blog_options.summary_separator
109
+ if blog_options.summary_separator && rendered =~ blog_options.summary_separator
107
110
  rendered.split(blog_options.summary_separator).first
108
- elsif length
111
+ elsif length && length >= 0
112
+ require 'middleman-blog/truncate_html'
109
113
  TruncateHTML.truncate_html(rendered, length, ellipsis)
110
114
  else
111
115
  rendered
@@ -113,23 +117,39 @@ module Middleman
113
117
  end
114
118
 
115
119
  # A list of tags for this article, set from frontmatter.
116
- # @return [Array<String>] (never nil)
120
+ # @return [Array<String>] (never +nil+)
117
121
  def tags
118
122
  article_tags = data["tags"]
119
123
 
120
124
  if article_tags.is_a? String
121
125
  article_tags.split(',').map(&:strip)
122
126
  else
123
- article_tags || []
127
+ Array(article_tags)
124
128
  end
125
129
  end
126
130
 
127
- # Retrieve a section of the source path
128
- # @param [String] The part of the path, e.g. "year", "month", "day", "title"
129
- # @return [String]
130
- def path_part(part)
131
- @_path_parts ||= blog_data.path_matcher.match(path).captures
132
- @_path_parts[blog_data.matcher_indexes[part]]
131
+ # The language of the article. The language can be present in the
132
+ # frontmatter or in the source path. If both are present, they
133
+ # must match. If neither specifies a lang, I18n's default_locale will
134
+ # be used. If +lang+ is set to nil, or the +:i18n+ extension is not
135
+ # activated at all, +nil+ will be returned.
136
+ #
137
+ # @return [Symbol] Language code (for example, +:en+ or +:de+)
138
+ def lang
139
+ frontmatter_lang = data["lang"]
140
+
141
+ if blog_options.sources.include? ":lang"
142
+ filename_lang = path_part("lang")
143
+ end
144
+
145
+ if frontmatter_lang && filename_lang && frontmatter_lang != filename_lang
146
+ raise "The lang in #{path}'s filename (#{filename_lang.inspect}) doesn't match the lang in its frontmatter (#{frontmatter_lang.inspect})"
147
+ end
148
+
149
+ locale_lang = I18n.default_locale if defined? I18n
150
+
151
+ lang = frontmatter_lang || filename_lang || locale_lang
152
+ lang && lang.to_sym
133
153
  end
134
154
 
135
155
  # Attempt to figure out the date of the post. The date should be
@@ -141,7 +161,7 @@ module Middleman
141
161
  def date
142
162
  return @_date if @_date
143
163
 
144
- frontmatter_date = data["date"]
164
+ frontmatter_date = data['date']
145
165
 
146
166
  # First get the date from frontmatter
147
167
  if frontmatter_date.is_a? Time
@@ -151,11 +171,12 @@ module Middleman
151
171
  end
152
172
 
153
173
  # Next figure out the date from the filename
154
- if blog_options.sources.include?(":year") &&
155
- blog_options.sources.include?(":month") &&
156
- blog_options.sources.include?(":day")
174
+ source_vars = blog_data.source_template.variables
175
+ if source_vars.include?('year') &&
176
+ source_vars.include?('month') &&
177
+ source_vars.include?('day')
157
178
 
158
- filename_date = Time.zone.local(path_part("year").to_i, path_part("month").to_i, path_part("day").to_i)
179
+ filename_date = Time.zone.local(path_part('year').to_i, path_part('month').to_i, path_part('day').to_i)
159
180
  if @_date
160
181
  raise "The date in #{path}'s filename doesn't match the date in its frontmatter" unless @_date.to_date == filename_date.to_date
161
182
  else
@@ -168,37 +189,52 @@ module Middleman
168
189
  @_date
169
190
  end
170
191
 
171
- # The "slug" of the article that shows up in its URL.
192
+ # The "slug" of the article that shows up in its URL. The article slug
193
+ # is a parameterized version of the {#title} (lowercase, spaces replaced
194
+ # with dashes, etc) and can be used in the blog +permalink+ as +:title+.
195
+ #
172
196
  # @return [String]
173
197
  def slug
174
- @_slug ||= data["slug"]
175
-
176
- @_slug ||= if blog_options.sources.include?(":title")
177
- path_part("title")
198
+ if data['slug']
199
+ data['slug']
200
+ elsif blog_data.source_template.variables.include?('title')
201
+ path_part('title')
178
202
  elsif title
179
- title.parameterize
203
+ Blog::UriTemplates.safe_parameterize(title)
180
204
  else
181
205
  raise "Can't generate a slug for #{path} because it has no :title in its path pattern or title/slug in its frontmatter."
182
206
  end
183
207
  end
184
208
 
185
209
  # The previous (chronologically earlier) article before this one
186
- # or nil if this is the first article.
187
- # @return [Middleman::Sitemap::Resource]
210
+ # or +nil+ if this is the first article.
211
+ # @return [BlogArticle]
188
212
  def previous_article
189
213
  blog_data.articles.find {|a| a.date < self.date }
190
214
  end
191
215
 
192
216
  # The next (chronologically later) article after this one
193
- # or nil if this is the most recent article.
217
+ # or +nil+ if this is the most recent article.
194
218
  # @return [Middleman::Sitemap::Resource]
195
219
  def next_article
196
220
  blog_data.articles.reverse.find {|a| a.date > self.date }
197
221
  end
198
222
 
223
+ # This is here to prevent out-of-memory on exceptions.
224
+ # @private
199
225
  def inspect
200
226
  "#<Middleman::Blog::BlogArticle: #{data.inspect}>"
201
227
  end
228
+
229
+ private
230
+
231
+ # Retrieve a section of the source path template.
232
+ # @param [String] part The part of the path, e.g. "lang", "year", "month", "day", "title"
233
+ # @return [String]
234
+ def path_part(part)
235
+ @_path_parts ||= blog_data.source_template.extract(path)
236
+ @_path_parts[part.to_s]
237
+ end
202
238
  end
203
239
  end
204
240
  end
@@ -1,16 +1,16 @@
1
+ require 'middleman-blog/uri_templates'
2
+
1
3
  module Middleman
2
4
  module Blog
3
5
  # A store of all the blog articles in the site, with accessors
4
6
  # for the articles by various dimensions. Accessed via "blog" in
5
7
  # templates.
6
8
  class BlogData
7
- # A regex for matching blog article source paths
8
- # @return [Regex]
9
- attr_reader :path_matcher
9
+ include UriTemplates
10
10
 
11
- # A hash of indexes into the path_matcher captures
12
- # @return [Hash]
13
- attr_reader :matcher_indexes
11
+ # A URITemplate for the source file path relative to :source_dir
12
+ # @return [URITemplate]
13
+ attr_reader :source_template
14
14
 
15
15
  # The configured options for this blog
16
16
  # @return [Thor::CoreExt::HashWithIndifferentAccess]
@@ -18,10 +18,8 @@ module Middleman
18
18
 
19
19
  attr_reader :controller
20
20
 
21
- DEFAULT_PERMALINK_COMPONENTS = [:year, :month, :day, :title]
22
-
23
21
  # @private
24
- def initialize(app, options={}, controller=nil)
22
+ def initialize(app, controller, options)
25
23
  @app = app
26
24
  @options = options
27
25
  @controller = controller
@@ -29,26 +27,10 @@ module Middleman
29
27
  # A list of resources corresponding to blog articles
30
28
  @_articles = []
31
29
 
32
- matcher = Regexp.escape(options.sources).
33
- sub(/^\//, "").
34
- gsub(":year", "(\\d{4})").
35
- gsub(":month", "(\\d{2})").
36
- gsub(":day", "(\\d{2})").
37
- sub(":title", "([^/]+)")
38
-
39
- subdir_matcher = matcher.sub(/\\\.[^.]+$/, "(/.*)$")
40
-
41
- @path_matcher = /^#{matcher}/
42
- @subdir_matcher = /^#{subdir_matcher}/
43
-
44
- # Build a hash of part name to capture index, e.g. {"year" => 0}
45
- @matcher_indexes = {}
46
- options.sources.scan(/:year|:month|:day|:title/).
47
- each_with_index do |key, i|
48
- @matcher_indexes[key[1..-1]] = i
49
- end
50
- # The path always appears at the end.
51
- @matcher_indexes["path"] = @matcher_indexes.size
30
+ @source_template = uri_template options.sources
31
+ @permalink_template = uri_template options.permalink
32
+ @subdir_template = uri_template options.sources.sub(/\.[^.]+$/, "/{+path}")
33
+ @subdir_permalink_template = uri_template options.permalink.sub(/\.[^.]+$/, "/{+path}")
52
34
  end
53
35
 
54
36
  # A list of all blog articles, sorted by descending date
@@ -57,15 +39,15 @@ module Middleman
57
39
  @_articles.sort_by(&:date).reverse
58
40
  end
59
41
 
60
- # The BlogArticle for the given path, or nil if one doesn't exist.
61
- # @return [Middleman::Sitemap::Resource]
62
- def article(path)
63
- article = @app.sitemap.find_resource_by_path(path.to_s)
64
- if article && article.is_a?(BlogArticle)
65
- article
66
- else
67
- nil
68
- end
42
+ # A list of all blog articles with the given language,
43
+ # sorted by descending date
44
+ #
45
+ # @param [Symbol] lang Language to match (optional, defaults to I18n.locale).
46
+ # @return [Array<Middleman::Sitemap::Resource>]
47
+ def local_articles(lang=nil)
48
+ lang ||= I18n.locale
49
+ lang = lang.to_sym if lang.kind_of? String
50
+ articles.select {|article| article.lang == lang }
69
51
  end
70
52
 
71
53
  # Returns a map from tag name to an array
@@ -80,6 +62,7 @@ module Middleman
80
62
  end
81
63
  end
82
64
 
65
+ # Sort each tag's list of articles
83
66
  tags.each do |tag, articles|
84
67
  tags[tag] = articles.sort_by(&:date).reverse
85
68
  end
@@ -95,43 +78,40 @@ module Middleman
95
78
  used_resources = []
96
79
 
97
80
  resources.each do |resource|
98
- if resource.path =~ path_matcher
99
- resource.extend BlogArticle
81
+ if (params = @source_template.extract(resource.path))
82
+ article = convert_to_article(resource)
83
+ next unless publishable?(article)
100
84
 
101
- if @controller
102
- resource.blog_controller = controller
103
- end
104
-
105
- # Skip articles that are not published (in non-development environments)
106
- next unless @app.environment == :development || resource.published?
85
+ # Add extra parameters from the URL to the page metadata
86
+ extra_data = params.except *%w(year month day title lang)
87
+ article.add_metadata page: extra_data unless extra_data.empty?
107
88
 
108
89
  # compute output path:
109
90
  # substitute date parts to path pattern
110
- resource.destination_path = Middleman::Util.normalize_path parse_permalink_options(resource)
91
+ article.destination_path = template_path @permalink_template, article
111
92
 
112
- @_articles << resource
93
+ @_articles << article
113
94
 
114
- elsif resource.path =~ @subdir_matcher
115
- match = $~.captures
95
+ elsif (params = @subdir_template.extract(resource.path))
96
+ # It's not an article, but it's thhe companion files for an article
97
+ # (in a subdirectory named after the article)
98
+ # figure out the matching article for this subdirectory file
116
99
 
117
- article_path = options.sources
118
- %w(year month day title).each do |token|
119
- article_path = article_path.sub(":#{token}", match[@matcher_indexes[token]]) if @matcher_indexes[token]
120
- end
100
+ article_path = @source_template.expand(params).to_s
121
101
 
122
102
  article = @app.sitemap.find_resource_by_path(article_path)
123
103
  raise "Article for #{resource.path} not found" if article.nil?
124
- article.extend BlogArticle
125
104
 
126
- # Skip files that belong to articles that are not published (in non-development environments)
127
- next unless @app.environment == :development || article.published?
105
+ # The article may not yet have been processed, so convert it here.
106
+ article = convert_to_article(article)
107
+ next unless publishable?(article)
128
108
 
129
109
  # The subdir path is the article path with the index file name
130
110
  # or file extension stripped off.
131
- resource.destination_path = parse_permalink_options(article).
132
- sub(/(\/#{@app.index_file}$)|(\.[^.]+$)|(\/$)/, match[@matcher_indexes["path"]])
111
+ path = params.fetch('path')
112
+ new_destination_path = template_path @subdir_permalink_template, article, path: path
133
113
 
134
- resource.destination_path = Middleman::Util.normalize_path(resource.destination_path)
114
+ resource.destination_path = Middleman::Util.normalize_path(new_destination_path)
135
115
  end
136
116
 
137
117
  used_resources << resource
@@ -140,30 +120,52 @@ module Middleman
140
120
  used_resources
141
121
  end
142
122
 
143
- def parse_permalink_options(resource)
144
- permalink = options.permalink.
145
- sub(':year', resource.date.year.to_s).
146
- sub(':month', resource.date.month.to_s.rjust(2, '0')).
147
- sub(':day', resource.date.day.to_s.rjust(2, '0')).
148
- sub(':title', resource.slug)
123
+ def inspect
124
+ "#<Middleman::Blog::BlogData: #{articles.inspect}>"
125
+ end
126
+
127
+ # Whether or not a given article should be included in the sitemap.
128
+ # Skip articles that are not published unless the environment is +:development+.
129
+ # @param [BlogArticle] article A blog article
130
+ # @return [Boolean] whether it should be published
131
+ def publishable?(article)
132
+ @app.environment == :development || article.published?
133
+ end
149
134
 
150
- custom_permalink_components.each do |component|
151
- permalink = permalink.sub(":#{component}", resource.data[component].parameterize)
135
+ private
136
+
137
+ # Generate a hash of options for substituting into the permalink URL template.
138
+ # @param [Sitemap::Resource] resource The resource to generate options for.
139
+ # @param [Hash] extra More options to be merged in on top.
140
+ # @return [Hash] options
141
+ def permalink_options(resource, extra={})
142
+ # Allow any frontmatter data to be substituted into the permalink URL
143
+ params = resource.metadata[:page].slice *@permalink_template.variables
144
+ params.each do |k, v|
145
+ params[k] = safe_parameterize(v)
152
146
  end
153
147
 
154
- permalink
148
+ params.
149
+ merge(date_to_params(resource.date)).
150
+ merge(lang: resource.lang.to_s, title: resource.slug).
151
+ merge(extra)
155
152
  end
156
153
 
157
- def custom_permalink_components
158
- permalink_url_components.reject { |component| DEFAULT_PERMALINK_COMPONENTS.include? component.to_sym }
159
- end
154
+ def convert_to_article(resource)
155
+ return resource if resource.is_a?(BlogArticle)
160
156
 
161
- def permalink_url_components
162
- options.permalink.scan(/:([A-Za-z0-9]+)/).flatten
157
+ resource.extend BlogArticle
158
+ resource.blog_controller = controller
159
+
160
+ if !options.preserve_locale && (lang = resource.lang)
161
+ resource.add_metadata options: { lang: lang }, locals: { lang: lang }
162
+ end
163
+
164
+ resource
163
165
  end
164
166
 
165
- def inspect
166
- "#<Middleman::Blog::BlogData: #{articles.inspect}>"
167
+ def template_path(template, article, extras={})
168
+ apply_uri_template template, permalink_options(article, extras)
167
169
  end
168
170
  end
169
171
  end