middleman-blog 3.4.1 → 3.5.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 (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