Collections
This commit is contained in:
parent
445443cffc
commit
a95dbb6367
|
@ -55,3 +55,5 @@ CaseIndentation:
|
|||
IndentWhenRelativeTo: end
|
||||
TrivialAccessors:
|
||||
ExactNameMatch: true
|
||||
PerceivedComplexity:
|
||||
Enabled: false
|
145
middleman-core/features/collections.feature
Normal file
145
middleman-core/features/collections.feature
Normal file
|
@ -0,0 +1,145 @@
|
|||
Feature: Collections
|
||||
Scenario: Lazy query
|
||||
Given a fixture app "collections-app"
|
||||
And a file named "config.rb" with:
|
||||
"""
|
||||
articles1 = collection :articles1, resources.select { |r|
|
||||
matcher = ::Middleman::Util::UriTemplates.uri_template('blog1/{year}-{month}-{day}-{title}.html')
|
||||
::Middleman::Util::UriTemplates.extract_params(matcher, ::Middleman::Util.normalize_path(r.url))
|
||||
}
|
||||
|
||||
everything = resources.select do |r|
|
||||
true
|
||||
end
|
||||
|
||||
def get_tags(resource)
|
||||
if resource.data.tags.is_a? String
|
||||
resource.data.tags.split(',').map(&:strip)
|
||||
else
|
||||
resource.data.tags
|
||||
end
|
||||
end
|
||||
|
||||
def group_lookup(resource, sum)
|
||||
results = Array(get_tags(resource)).map(&:to_s).map(&:to_sym)
|
||||
|
||||
results.each do |k|
|
||||
sum[k] ||= []
|
||||
sum[k] << resource
|
||||
end
|
||||
end
|
||||
|
||||
tags = everything
|
||||
.select { |resource| resource.data.tags }
|
||||
.each_with_object({}, &method(:group_lookup))
|
||||
|
||||
class Wrapper
|
||||
attr_reader :stuff
|
||||
|
||||
def initialize
|
||||
@stuff = Set.new
|
||||
end
|
||||
|
||||
def <<((k, v))
|
||||
@stuff << k
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
collection :wrapped, tags.reduce(Wrapper.new, :<<)
|
||||
|
||||
set :tags, tags # Expose to templates
|
||||
|
||||
collection :first_tag, tags.keys.sort.first
|
||||
"""
|
||||
And a file named "source/index.html.erb" with:
|
||||
"""
|
||||
<% collection(:articles1).each do |article| %>
|
||||
Article1: <%= article.data.title %>
|
||||
<% end %>
|
||||
|
||||
Tag Count: <%= collection(:wrapped).stuff.length %>
|
||||
|
||||
<% config[:tags].value.each do |k, items| %>
|
||||
Tag: <%= k %> (<%= items.length %>)
|
||||
<% items.each do |article| %>
|
||||
Article (<%= k %>): <%= article.data.title %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
First Tag: <%= collection(:first_tag) %>
|
||||
"""
|
||||
Given the Server is running at "collections-app"
|
||||
When I go to "index.html"
|
||||
Then I should see 'Article1: Blog1 Newer Article'
|
||||
And I should see 'Article1: Blog1 Another Article'
|
||||
And I should see 'Tag: foo (4)'
|
||||
And I should see 'Article (foo): Blog1 Newer Article'
|
||||
And I should see 'Article (foo): Blog1 Another Article'
|
||||
And I should see 'Article (foo): Blog2 Newer Article'
|
||||
And I should see 'Article (foo): Blog2 Another Article'
|
||||
And I should see 'Tag: bar (2)'
|
||||
And I should see 'Article (bar): Blog1 Newer Article'
|
||||
And I should see 'Article (bar): Blog2 Newer Article'
|
||||
And I should see 'Tag: 120 (1)'
|
||||
And I should see 'Article (120): Blog1 Another Article'
|
||||
And I should see 'First Tag: 120'
|
||||
And I should see 'Tag Count: 3'
|
||||
|
||||
Scenario: Collected resources update with file changes
|
||||
Given a fixture app "collections-app"
|
||||
And a file named "config.rb" with:
|
||||
"""
|
||||
collection :articles, resources.select { |r|
|
||||
matcher = ::Middleman::Util::UriTemplates.uri_template('blog2/{year}-{month}-{day}-{title}.html')
|
||||
::Middleman::Util::UriTemplates.extract_params(matcher, ::Middleman::Util.normalize_path(r.url))
|
||||
}
|
||||
"""
|
||||
And a file named "source/index.html.erb" with:
|
||||
"""
|
||||
<% collection(:articles).each do |article| %>
|
||||
Article: <%= article.data.title || article.source_file[:relative_path] %>
|
||||
<% end %>
|
||||
"""
|
||||
Given the Server is running at "collections-app"
|
||||
When I go to "index.html"
|
||||
Then I should not see "Article: index.html.erb"
|
||||
Then I should see 'Article: Blog2 Newer Article'
|
||||
And I should see 'Article: Blog2 Another Article'
|
||||
|
||||
And the file "source/blog2/2011-01-02-another-article.html.markdown" has the contents
|
||||
"""
|
||||
---
|
||||
title: "Blog3 Another Article"
|
||||
date: 2011-01-02
|
||||
tags:
|
||||
- foo
|
||||
---
|
||||
|
||||
Another Article Content
|
||||
|
||||
"""
|
||||
When I go to "index.html"
|
||||
Then I should see "Article: Blog2 Newer Article"
|
||||
And I should not see "Article: Blog2 Another Article"
|
||||
And I should see 'Article: Blog3 Another Article'
|
||||
|
||||
And the file "source/blog2/2011-01-01-new-article.html.markdown" is removed
|
||||
When I go to "index.html"
|
||||
Then I should not see "Article: Blog2 Newer Article"
|
||||
And I should see 'Article: Blog3 Another Article'
|
||||
|
||||
And the file "source/blog2/2014-01-02-yet-another-article.html.markdown" has the contents
|
||||
"""
|
||||
---
|
||||
title: "Blog2 Yet Another Article"
|
||||
date: 2011-01-02
|
||||
tags:
|
||||
- foo
|
||||
---
|
||||
|
||||
Yet Another Article Content
|
||||
"""
|
||||
When I go to "index.html"
|
||||
And I should see 'Article: Blog3 Another Article'
|
||||
And I should see 'Article: Blog2 Yet Another Article'
|
|
@ -1,8 +1,9 @@
|
|||
Feature: Console
|
||||
|
||||
Scenario: Enter and exit the console
|
||||
Given I run `middleman console` interactively
|
||||
When I type "puts 'Hello from the console.'"
|
||||
Given a fixture app "large-build-app"
|
||||
When I run `middleman console` interactively
|
||||
And I type "puts 'Hello from the console.'"
|
||||
And I type "exit"
|
||||
Then it should pass with:
|
||||
"""
|
||||
|
|
204
middleman-core/features/paginate.feature
Normal file
204
middleman-core/features/paginate.feature
Normal file
|
@ -0,0 +1,204 @@
|
|||
Feature: Pagination
|
||||
Scenario: Basic configuration
|
||||
Given a fixture app "paginate-app"
|
||||
And a file named "config.rb" with:
|
||||
"""
|
||||
articles = resources.select { |r|
|
||||
matcher = ::Middleman::Util::UriTemplates.uri_template('blog/2011-{remaining}')
|
||||
::Middleman::Util::UriTemplates.extract_params(matcher, ::Middleman::Util.normalize_path(r.url))
|
||||
}
|
||||
|
||||
articles.sort { |a, b|
|
||||
b.data.date <=> a.data.date
|
||||
}.per_page(5) do |items, num, meta, is_last|
|
||||
page_path = num == 1 ? '/2011/index.html' : "/2011/page/#{num}.html"
|
||||
|
||||
prev_page = case num
|
||||
when 1
|
||||
nil
|
||||
when 2
|
||||
'/2011/index.html'
|
||||
when 3
|
||||
"/2011/page/#{num-1}.html"
|
||||
end
|
||||
|
||||
next_page = is_last ? nil : "/2011/page/#{num+1}.html"
|
||||
|
||||
proxy page_path, "/archive/2011/index.html", locals: {
|
||||
items: items,
|
||||
pagination: meta,
|
||||
prev_page: prev_page,
|
||||
next_page: next_page
|
||||
}
|
||||
end
|
||||
|
||||
def get_tags(resource)
|
||||
if resource.data.tags.is_a? String
|
||||
resource.data.tags.split(',').map(&:strip)
|
||||
else
|
||||
resource.data.tags
|
||||
end
|
||||
end
|
||||
|
||||
def group_lookup(resource, sum)
|
||||
results = Array(get_tags(resource)).map(&:to_s).map(&:to_sym)
|
||||
|
||||
results.each do |k|
|
||||
sum[k] ||= []
|
||||
sum[k] << resource
|
||||
end
|
||||
end
|
||||
|
||||
tags = articles
|
||||
.select { |resource| resource.data.tags }
|
||||
.each_with_object({}, &method(:group_lookup))
|
||||
|
||||
tags.each do |k, articles_in_tag|
|
||||
articles_in_tag.sort { |a, b|
|
||||
b.data.date <=> a.data.date
|
||||
}.per_page(2).each do |items, num, meta, is_last|
|
||||
page_path = num == 1 ? "/tags/#{k}.html" : "/tags/#{k}/page/#{num}.html"
|
||||
|
||||
prev_page = case num
|
||||
when 1
|
||||
nil
|
||||
when 2
|
||||
"/tags/#{k}.html"
|
||||
when 3
|
||||
"/tags/#{k}/page/#{num-1}.html"
|
||||
end
|
||||
|
||||
next_page = is_last ? nil : "/tags/#{k}/page/#{num+1}.html"
|
||||
|
||||
proxy page_path, "/archive/2011/index.html", locals: {
|
||||
items: items,
|
||||
pagination: meta,
|
||||
prev_page: prev_page,
|
||||
next_page: next_page
|
||||
}
|
||||
end
|
||||
end
|
||||
"""
|
||||
And the Server is running
|
||||
When I go to "/2011/index.html"
|
||||
Then I should see "Paginate: true"
|
||||
Then I should see "Article Count: 5"
|
||||
Then I should see "Page Num: 1"
|
||||
Then I should see "Num Pages: 2"
|
||||
Then I should see "Per Page: 5"
|
||||
Then I should see "Page Start: 1"
|
||||
Then I should see "Page End: 5"
|
||||
Then I should see "Next Page: '/2011/page/2.html'"
|
||||
Then I should see "Prev Page: ''"
|
||||
Then I should not see "/blog/2011-01-01-test-article.html"
|
||||
Then I should not see "/blog/2011-01-02-test-article.html"
|
||||
Then I should see "/blog/2011-01-03-test-article.html"
|
||||
Then I should see "/blog/2011-01-04-test-article.html"
|
||||
Then I should see "/blog/2011-01-05-test-article.html"
|
||||
Then I should see "/blog/2011-02-01-test-article.html"
|
||||
Then I should see "/blog/2011-02-02-test-article.html"
|
||||
|
||||
When I go to "/2011/page/2.html"
|
||||
Then I should see "Article Count: 2"
|
||||
Then I should see "Page Num: 2"
|
||||
Then I should see "Page Start: 6"
|
||||
Then I should see "Page End: 7"
|
||||
Then I should see "Next Page: ''"
|
||||
Then I should see "Prev Page: '/2011/'"
|
||||
Then I should see "/2011-01-01-test-article.html"
|
||||
Then I should see "/2011-01-02-test-article.html"
|
||||
Then I should not see "/2011-01-03-test-article.html"
|
||||
Then I should not see "/2011-01-04-test-article.html"
|
||||
Then I should not see "/2011-01-05-test-article.html"
|
||||
Then I should not see "/2011-02-01-test-article.html"
|
||||
Then I should not see "/2011-02-02-test-article.html"
|
||||
|
||||
When I go to "/tags/bar.html"
|
||||
Then I should see "Paginate: true"
|
||||
Then I should see "Article Count: 2"
|
||||
Then I should see "Page Num: 1"
|
||||
Then I should see "Num Pages: 3"
|
||||
Then I should see "Per Page: 2"
|
||||
Then I should see "Page Start: 1"
|
||||
Then I should see "Page End: 2"
|
||||
Then I should see "Next Page: '/tags/bar/page/2.html'"
|
||||
Then I should see "Prev Page: ''"
|
||||
Then I should see "/2011-02-02-test-article.html"
|
||||
Then I should see "/2011-02-01-test-article.html"
|
||||
Then I should not see "/2011-02-05-test-article.html"
|
||||
Then I should not see "/2011-01-04-test-article.html"
|
||||
Then I should not see "/2011-01-03-test-article.html"
|
||||
|
||||
Scenario: Custom pager method
|
||||
Given a fixture app "paginate-app"
|
||||
And a file named "config.rb" with:
|
||||
"""
|
||||
def items_per_page(all_items)
|
||||
[
|
||||
all_items.shift(2),
|
||||
all_items
|
||||
]
|
||||
end
|
||||
|
||||
articles = resources.select { |r|
|
||||
matcher = ::Middleman::Util::UriTemplates.uri_template('blog/2011-{remaining}')
|
||||
::Middleman::Util::UriTemplates.extract_params(matcher, ::Middleman::Util.normalize_path(r.url))
|
||||
}
|
||||
|
||||
articles.sort { |a, b|
|
||||
b.data.date <=> a.data.date
|
||||
}.per_page(method(:items_per_page).to_proc).each do |items, num, meta, is_last|
|
||||
page_path = num == 1 ? '/2011/index.html' : "/2011/page/#{num}.html"
|
||||
|
||||
prev_page = case num
|
||||
when 1
|
||||
nil
|
||||
when 2
|
||||
'/2011/index.html'
|
||||
when 3
|
||||
"/2011/page/#{num-1}.html"
|
||||
end
|
||||
|
||||
next_page = is_last ? nil : "/2011/page/#{num+1}.html"
|
||||
|
||||
proxy page_path, "/archive/2011/index.html", locals: {
|
||||
items: items,
|
||||
pagination: meta,
|
||||
prev_page: prev_page,
|
||||
next_page: next_page
|
||||
}
|
||||
end
|
||||
"""
|
||||
And the Server is running
|
||||
When I go to "/2011/index.html"
|
||||
Then I should see "Paginate: true"
|
||||
Then I should see "Article Count: 2"
|
||||
Then I should see "Page Num: 1"
|
||||
Then I should see "Num Pages: 2"
|
||||
Then I should see "Per Page: 2"
|
||||
Then I should see "Page Start: 1"
|
||||
Then I should see "Page End: 2"
|
||||
Then I should see "Next Page: '/2011/page/2.html'"
|
||||
Then I should see "Prev Page: ''"
|
||||
Then I should not see "/blog/2011-01-01-test-article.html"
|
||||
Then I should not see "/blog/2011-01-02-test-article.html"
|
||||
Then I should not see "/blog/2011-01-03-test-article.html"
|
||||
Then I should not see "/blog/2011-01-04-test-article.html"
|
||||
Then I should not see "/blog/2011-01-05-test-article.html"
|
||||
Then I should see "/blog/2011-02-01-test-article.html"
|
||||
Then I should see "/blog/2011-02-02-test-article.html"
|
||||
|
||||
When I go to "/2011/page/2.html"
|
||||
Then I should see "Article Count: 5"
|
||||
Then I should see "Page Num: 2"
|
||||
Then I should see "Page Start: 3"
|
||||
Then I should see "Page End: 7"
|
||||
Then I should see "Next Page: ''"
|
||||
Then I should see "Prev Page: '/2011/'"
|
||||
Then I should see "/2011-01-01-test-article.html"
|
||||
Then I should see "/2011-01-02-test-article.html"
|
||||
Then I should see "/2011-01-03-test-article.html"
|
||||
Then I should see "/2011-01-04-test-article.html"
|
||||
Then I should see "/2011-01-05-test-article.html"
|
||||
Then I should not see "/2011-02-01-test-article.html"
|
||||
Then I should not see "/2011-02-02-test-article.html"
|
16
middleman-core/fixtures/collections-app/config.rb
Normal file
16
middleman-core/fixtures/collections-app/config.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
collection :articles,
|
||||
where: proc { |resource|
|
||||
uri_match resource.url, 'blog/{year}-{month}-{day}-{title}.html'
|
||||
}
|
||||
|
||||
collection :tags,
|
||||
where: proc { |resource|
|
||||
resource.data.tags
|
||||
},
|
||||
group_by: proc { |resource|
|
||||
if resource.data.tags.is_a? String
|
||||
resource.data.tags.split(',').map(&:strip)
|
||||
else
|
||||
resource.data.tags
|
||||
end
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
title: "Blog1 Newer Article"
|
||||
date: 2011-01-01
|
||||
tags: foo, bar
|
||||
---
|
||||
|
||||
Newer Article Content
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
title: "Blog1 Another Article"
|
||||
date: 2011-01-02
|
||||
tags:
|
||||
- foo
|
||||
- 120
|
||||
---
|
||||
|
||||
Another Article Content
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
title: "Blog2 Newer Article"
|
||||
date: 2011-01-01
|
||||
tags: foo, bar
|
||||
---
|
||||
|
||||
Newer Article Content
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
title: "Blog2 Another Article"
|
||||
date: 2011-01-02
|
||||
tags:
|
||||
- foo
|
||||
---
|
||||
|
||||
Another Article Content
|
|
@ -0,0 +1,26 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ARTICLES -->
|
||||
<% collected.articles.each do |article| %>
|
||||
<li>
|
||||
<a href="<%= article.url %>">Article: <%= article.data.title %></a>
|
||||
<time><%= article.data.date.strftime('%b %e') %></time>
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
<!-- TAGS -->
|
||||
<% collected[:tags].each do |k, items| %>
|
||||
<li>
|
||||
<%= k %>
|
||||
<% items.each do |article| %>
|
||||
<%= article.data.title %>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
</body>
|
||||
</html>
|
0
middleman-core/fixtures/paginate-app/config.rb
Normal file
0
middleman-core/fixtures/paginate-app/config.rb
Normal file
|
@ -0,0 +1,20 @@
|
|||
Year: '<%#= year %>'
|
||||
|
||||
Paginate: <%= !!pagination %>
|
||||
Article Count: <%= items.length %>
|
||||
<% if pagination %>
|
||||
Page Num: <%= pagination.page_number %>
|
||||
Num Pages: <%= pagination.num_pages %>
|
||||
Per Page: <%= pagination.per_page %>
|
||||
Page Start: <%= pagination.page_start %>
|
||||
Page End: <%= pagination.page_end %>
|
||||
Next Page: '<%= sitemap.find_resource_by_destination_path(next_page).url if next_page %>'
|
||||
Prev Page: '<%= sitemap.find_resource_by_destination_path(prev_page).url if prev_page %>'
|
||||
<% end %>
|
||||
|
||||
<% items.each do |article| %>
|
||||
<article>
|
||||
<%= article.data.title %>
|
||||
<%= article.url %>
|
||||
</article>
|
||||
<% end %>
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: "Test Article"
|
||||
date: 2011-01-01
|
||||
tags: foo
|
||||
---
|
||||
Test Article Content
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: "Test Article"
|
||||
date: 2011-01-02
|
||||
tags: foo
|
||||
---
|
||||
Test Article Content
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: "Test Article"
|
||||
date: 2011-01-03
|
||||
tags: bar
|
||||
---
|
||||
Test Article Content
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: "Test Article"
|
||||
date: 2011-01-04
|
||||
tags: bar
|
||||
---
|
||||
Test Article Content
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: "Test Article"
|
||||
date: 2011-01-05
|
||||
tags: bar
|
||||
---
|
||||
Test Article Content
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: "Test Article"
|
||||
date: 2011-02-01
|
||||
tags: bar
|
||||
---
|
||||
Test Article Content
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: "Test Article"
|
||||
date: 2011-02-02
|
||||
tags: bar
|
||||
---
|
||||
Test Article Content
|
15
middleman-core/fixtures/paginate-app/source/index.html.erb
Normal file
15
middleman-core/fixtures/paginate-app/source/index.html.erb
Normal file
|
@ -0,0 +1,15 @@
|
|||
Paginate: <%= paginate %>
|
||||
Article Count: <%= page_articles.size %>
|
||||
<% if paginate %>
|
||||
Page Num: <%= page_number %>
|
||||
Num Pages: <%= num_pages %>
|
||||
Per Page: <%= per_page %>
|
||||
Page Start: <%= page_start %>
|
||||
Page End: <%= page_end %>
|
||||
Next Page: '<%= next_page.url if next_page %>'
|
||||
Prev Page: '<%= prev_page.url if prev_page %>'
|
||||
<% end %>
|
||||
|
||||
<% page_articles.each do |article| %>
|
||||
<li><a href="<%= article.url %>"><%= article.title %></a> <time><%= article.date.strftime('%b %e') %></time></li>
|
||||
<% end %>
|
23
middleman-core/fixtures/paginate-app/source/tag.html.erb
Normal file
23
middleman-core/fixtures/paginate-app/source/tag.html.erb
Normal file
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
pageable: true
|
||||
per_page: 2
|
||||
---
|
||||
Tag: <%= tagname %>
|
||||
|
||||
Paginate: <%= paginate %>
|
||||
Article Count: <%= page_articles.size %>
|
||||
<% if paginate %>
|
||||
Page Num: <%= page_number %>
|
||||
Num Pages: <%= num_pages %>
|
||||
Per Page: <%= per_page %>
|
||||
Page Start: <%= page_start %>
|
||||
Page End: <%= page_end %>
|
||||
Next Page: '<%= next_page.url if next_page %>'
|
||||
Prev Page: '<%= prev_page.url if prev_page %>'
|
||||
<% end %>
|
||||
|
||||
<% if page_articles %>
|
||||
<% page_articles.each do |article| %>
|
||||
<li><a href="<%= article.url %>"><%= article.title %></a> <time><%= article.date.strftime('%b %e') %></time></li>
|
||||
<% end %>
|
||||
<% end %>
|
|
@ -59,6 +59,21 @@ if ENV['TEST'] || ENV['CONTRACTS'] == 'true'
|
|||
end
|
||||
end
|
||||
|
||||
# class MethodDefined
|
||||
# def self.[](val)
|
||||
# @lookup ||= {}
|
||||
# @lookup[val] ||= new(val)
|
||||
# end
|
||||
|
||||
# def initialize(val)
|
||||
# @val = val
|
||||
# end
|
||||
|
||||
# def valid?(val)
|
||||
# val.method_defined? @val
|
||||
# end
|
||||
# end
|
||||
|
||||
ResourceList = Contracts::ArrayOf[IsA['Middleman::Sitemap::Resource']]
|
||||
end
|
||||
else
|
||||
|
@ -141,6 +156,9 @@ else
|
|||
|
||||
class Frozen < Callable
|
||||
end
|
||||
|
||||
# class MethodDefined < Callable
|
||||
# end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -52,6 +52,11 @@ Middleman::Extensions.register :routing, auto_activate: :before_configuration do
|
|||
Middleman::CoreExtensions::Routing
|
||||
end
|
||||
|
||||
Middleman::Extensions.register :collections, auto_activate: :before_configuration do
|
||||
require 'middleman-core/core_extensions/collections'
|
||||
Middleman::CoreExtensions::Collections::CollectionsExtension
|
||||
end
|
||||
|
||||
###
|
||||
# Setup Optional Extensions
|
||||
###
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
require 'middleman-core/core_extensions/collections/pagination'
|
||||
require 'middleman-core/core_extensions/collections/step_context'
|
||||
require 'middleman-core/core_extensions/collections/lazy_root'
|
||||
require 'middleman-core/core_extensions/collections/lazy_step'
|
||||
|
||||
# Super "class-y" injection of array helpers
|
||||
class Array
|
||||
include Middleman::Pagination::ArrayHelpers
|
||||
end
|
||||
|
||||
module Middleman
|
||||
module CoreExtensions
|
||||
module Collections
|
||||
class CollectionsExtension < Extension
|
||||
# This should run after most other sitemap manipulators so that it
|
||||
# gets a chance to modify any new resources that get added.
|
||||
self.resource_list_manipulator_priority = 110
|
||||
|
||||
attr_accessor :root_collector, :leaves
|
||||
|
||||
def initialize(app, options_hash={}, &block)
|
||||
super
|
||||
|
||||
@leaves = Set.new
|
||||
@collectors_by_name = {}
|
||||
@values_by_name = {}
|
||||
|
||||
@root_collector = LazyCollectorRoot.new(self)
|
||||
end
|
||||
|
||||
Contract None => Any
|
||||
def before_configuration
|
||||
@leaves.clear
|
||||
|
||||
app.add_to_config_context :resources, &method(:root_collector)
|
||||
app.add_to_config_context :collection, &method(:register_collector)
|
||||
end
|
||||
|
||||
Contract Symbol, LazyCollectorStep => Any
|
||||
def register_collector(label, endpoint)
|
||||
@collectors_by_name[label] = endpoint
|
||||
end
|
||||
|
||||
Contract Symbol => Any
|
||||
def collector_value(label)
|
||||
@values_by_name[label]
|
||||
end
|
||||
|
||||
Contract ResourceList => ResourceList
|
||||
def manipulate_resource_list(resources)
|
||||
@root_collector.realize!(resources)
|
||||
|
||||
ctx = StepContext.new
|
||||
leaves = @leaves.dup
|
||||
|
||||
@collectors_by_name.each do |k, v|
|
||||
@values_by_name[k] = v.value(ctx)
|
||||
leaves.delete v
|
||||
end
|
||||
|
||||
# Execute code paths
|
||||
leaves.each do |v|
|
||||
v.value(ctx)
|
||||
end
|
||||
|
||||
# Inject descriptors
|
||||
resources + ctx.descriptors.map { |d| d.to_resource(app) }
|
||||
end
|
||||
|
||||
helpers do
|
||||
def collection(label)
|
||||
extensions[:collections].collector_value(label)
|
||||
end
|
||||
|
||||
def pagination
|
||||
current_resource.data.pagination
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,30 @@
|
|||
require 'middleman-core/core_extensions/collections/lazy_step'
|
||||
|
||||
module Middleman
|
||||
module CoreExtensions
|
||||
module Collections
|
||||
class LazyCollectorRoot < BasicObject
|
||||
def initialize(parent)
|
||||
@data = nil
|
||||
@parent = parent
|
||||
end
|
||||
|
||||
def realize!(data)
|
||||
@data = data
|
||||
end
|
||||
|
||||
def value(_ctx=nil)
|
||||
@data
|
||||
end
|
||||
|
||||
def leaves
|
||||
@parent.leaves
|
||||
end
|
||||
|
||||
def method_missing(name, *args, &block)
|
||||
LazyCollectorStep.new(name, args, block, self)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,48 @@
|
|||
module Middleman
|
||||
module CoreExtensions
|
||||
module Collections
|
||||
class LazyCollectorStep < BasicObject
|
||||
DELEGATE = [:hash, :eql?]
|
||||
|
||||
def initialize(name, args, block, parent=nil)
|
||||
@name = name
|
||||
@args = args
|
||||
@block = block
|
||||
|
||||
@parent = parent
|
||||
@result = nil
|
||||
|
||||
leaves << self
|
||||
end
|
||||
|
||||
def leaves
|
||||
@parent.leaves
|
||||
end
|
||||
|
||||
def value(ctx=nil)
|
||||
data = @parent.value(ctx)
|
||||
|
||||
original_block = @block
|
||||
|
||||
b = if ctx
|
||||
::Proc.new do |*args|
|
||||
ctx.instance_exec(*args, &original_block)
|
||||
end
|
||||
else
|
||||
original_block
|
||||
end if original_block
|
||||
|
||||
data.send(@name, *@args.deep_dup, &b)
|
||||
end
|
||||
|
||||
def method_missing(name, *args, &block)
|
||||
return ::Kernel.send(name, *args, &block) if DELEGATE.include? name
|
||||
|
||||
leaves.delete self
|
||||
|
||||
LazyCollectorStep.new(name, args, block, self)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,59 @@
|
|||
require 'active_support/core_ext/object/deep_dup'
|
||||
require 'middleman-core/util'
|
||||
|
||||
module Middleman
|
||||
module Pagination
|
||||
module ArrayHelpers
|
||||
def per_page(per_page)
|
||||
return enum_for(:per_page, per_page) unless block_given?
|
||||
|
||||
parts = if per_page.respond_to? :call
|
||||
per_page.call(dup)
|
||||
else
|
||||
each_slice(per_page).reduce([]) do |sum, items|
|
||||
sum << items
|
||||
end
|
||||
end
|
||||
|
||||
num_pages = parts.length
|
||||
collection = self
|
||||
|
||||
current_start_i = 0
|
||||
parts.each_with_index do |items, i|
|
||||
num = i + 1
|
||||
|
||||
meta = ::Middleman::Pagination.page_locals(
|
||||
num,
|
||||
num_pages,
|
||||
collection,
|
||||
items,
|
||||
current_start_i
|
||||
)
|
||||
|
||||
yield items, num, meta, num >= num_pages
|
||||
|
||||
current_start_i += items.length
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.page_locals(page_num, num_pages, collection, items, page_start)
|
||||
per_page = items.length
|
||||
|
||||
# Index into collection of the last item of this page
|
||||
page_end = (page_start + per_page) - 1
|
||||
|
||||
::Middleman::Util.recursively_enhance(page_number: page_num,
|
||||
num_pages: num_pages,
|
||||
per_page: per_page,
|
||||
|
||||
# The range of item numbers on this page
|
||||
# (1-based, for showing "Items X to Y of Z")
|
||||
page_start: page_start + 1,
|
||||
page_end: [page_end + 1, collection.length].min,
|
||||
|
||||
# Use "collection" in templates.
|
||||
collection: collection)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,26 @@
|
|||
module Middleman
|
||||
module CoreExtensions
|
||||
module Collections
|
||||
class StepContext
|
||||
def self.add_to_context(name, &func)
|
||||
send(:define_method, :"_internal_#{name}", &func)
|
||||
end
|
||||
|
||||
attr_reader :descriptors
|
||||
|
||||
def initialize
|
||||
@descriptors = []
|
||||
end
|
||||
|
||||
def method_missing(name, *args, &block)
|
||||
internal = :"_internal_#{name}"
|
||||
if respond_to?(internal)
|
||||
@descriptors << send(internal, *args, &block)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -12,7 +12,7 @@ module Middleman::CoreExtensions
|
|||
end
|
||||
|
||||
def after_configuration
|
||||
app.use ::Rack::ShowExceptions if app.config[:show_exceptions]
|
||||
app.use ::Rack::ShowExceptions if !app.build? && app.config[:show_exceptions]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
require 'middleman-core/sitemap/resource'
|
||||
require 'middleman-core/core_extensions/collections/step_context'
|
||||
|
||||
module Middleman
|
||||
module Sitemap
|
||||
|
@ -13,6 +14,13 @@ module Middleman
|
|||
@app.define_singleton_method(:proxy, &method(:create_proxy))
|
||||
|
||||
@proxy_configs = Set.new
|
||||
@post_config = false
|
||||
end
|
||||
|
||||
def after_configuration
|
||||
@post_config = true
|
||||
|
||||
::Middleman::CoreExtensions::Collections::StepContext.add_to_context(:proxy, &method(:create_anonymous_proxy))
|
||||
end
|
||||
|
||||
# Setup a proxy from a path to a target
|
||||
|
@ -27,70 +35,48 @@ module Middleman
|
|||
Contract String, String, Maybe[Hash] => Any
|
||||
def create_proxy(path, target, opts={})
|
||||
options = opts.dup
|
||||
|
||||
@app.ignore(target) if options.delete(:ignore)
|
||||
|
||||
metadata = {
|
||||
options: options,
|
||||
locals: options.delete(:locals) || {},
|
||||
page: options.delete(:data) || {}
|
||||
}
|
||||
|
||||
@proxy_configs << ProxyConfiguration.new(path: path, target: target, metadata: metadata)
|
||||
|
||||
@proxy_configs << create_anonymous_proxy(path, target, options)
|
||||
@app.sitemap.rebuild_resource_list!(:added_proxy)
|
||||
end
|
||||
|
||||
# Setup a proxy from a path to a target
|
||||
# @param [String] path The new, proxied path to create
|
||||
# @param [String] target The existing path that should be proxied to. This must be a real resource, not another proxy.
|
||||
# @option opts [Boolean] ignore Ignore the target from the sitemap (so only the new, proxy resource ends up in the output)
|
||||
# @option opts [Symbol, Boolean, String] layout The layout name to use (e.g. `:article`) or `false` to disable layout.
|
||||
# @option opts [Boolean] directory_indexes Whether or not the `:directory_indexes` extension applies to these paths.
|
||||
# @option opts [Hash] locals Local variables for the template. These will be available when the template renders.
|
||||
# @option opts [Hash] data Extra metadata to add to the page. This is the same as frontmatter, though frontmatter will take precedence over metadata defined here. Available via {Resource#data}.
|
||||
# @return [void]
|
||||
def create_anonymous_proxy(path, target, options={})
|
||||
ProxyDescriptor.new(
|
||||
::Middleman::Util.normalize_path(path),
|
||||
::Middleman::Util.normalize_path(target),
|
||||
options
|
||||
)
|
||||
end
|
||||
|
||||
# Update the main sitemap resource list
|
||||
# @return Array<Middleman::Sitemap::Resource>
|
||||
Contract ResourceList => ResourceList
|
||||
def manipulate_resource_list(resources)
|
||||
resources + @proxy_configs.map do |config|
|
||||
p = ProxyResource.new(
|
||||
@app.sitemap,
|
||||
config.path,
|
||||
config.target
|
||||
)
|
||||
|
||||
p.add_metadata(config.metadata)
|
||||
p
|
||||
end
|
||||
resources + @proxy_configs.map { |c| c.to_resource(@app) }
|
||||
end
|
||||
end
|
||||
|
||||
# Configuration for a proxy instance
|
||||
class ProxyConfiguration
|
||||
# The path that this proxy will appear at in the sitemap
|
||||
attr_reader :path
|
||||
def path=(p)
|
||||
@path = ::Middleman::Util.normalize_path(p)
|
||||
end
|
||||
|
||||
# The existing sitemap path that this will proxy to
|
||||
attr_reader :target
|
||||
def target=(t)
|
||||
@target = ::Middleman::Util.normalize_path(t)
|
||||
end
|
||||
|
||||
# Additional metadata like locals to apply to the proxy
|
||||
attr_accessor :metadata
|
||||
|
||||
# Create a new proxy configuration from hash options
|
||||
def initialize(options={})
|
||||
options.each do |key, value|
|
||||
send "#{key}=", value
|
||||
ProxyDescriptor = Struct.new(:path, :target, :metadata) do
|
||||
def to_resource(app)
|
||||
ProxyResource.new(app.sitemap, path, target).tap do |p|
|
||||
md = metadata.dup
|
||||
p.add_metadata(
|
||||
locals: md.delete(:locals) || {},
|
||||
page: md.delete(:data) || {},
|
||||
options: md
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Two configurations are equal if they reference the same path
|
||||
def eql?(other)
|
||||
other.path == path
|
||||
end
|
||||
|
||||
# Two configurations are equal if they reference the same path
|
||||
def hash
|
||||
path.hash
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -165,7 +165,11 @@ module Middleman
|
|||
# Ignore based on the source path (without template extensions)
|
||||
return true if @app.sitemap.ignored?(path)
|
||||
# This allows files to be ignored by their source file name (with template extensions)
|
||||
!self.is_a?(ProxyResource) && @app.sitemap.ignored?(source_file[:relative_path].to_s)
|
||||
if !self.is_a?(ProxyResource) && source_file && @app.sitemap.ignored?(source_file[:relative_path].to_s)
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# The preferred MIME content type for this resource based on extension or metadata
|
||||
|
@ -174,6 +178,31 @@ module Middleman
|
|||
def content_type
|
||||
options[:content_type] || ::Rack::Mime.mime_type(ext, nil)
|
||||
end
|
||||
|
||||
def to_s
|
||||
"#<Middleman::Sitemap::Resource path=#{@path}>"
|
||||
end
|
||||
alias_method :inspect, :to_s # Ruby 2.0 calls inspect for NoMethodError instead of to_s
|
||||
end
|
||||
|
||||
class StringResource < Resource
|
||||
def initialize(store, path, contents=nil, &block)
|
||||
@request_path = path
|
||||
@contents = block_given? ? block : contents
|
||||
super(store, path)
|
||||
end
|
||||
|
||||
def template?
|
||||
true
|
||||
end
|
||||
|
||||
def render(*)
|
||||
@contents.respond_to?(:call) ? @contents.call : @contents
|
||||
end
|
||||
|
||||
def binary?
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -49,11 +49,15 @@ module Middleman
|
|||
# @return [Middleman::Application]
|
||||
attr_reader :app
|
||||
|
||||
attr_reader :update_count
|
||||
|
||||
# Initialize with parent app
|
||||
# @param [Middleman::Application] app
|
||||
def initialize(app)
|
||||
@app = app
|
||||
@resources = []
|
||||
@update_count = 0
|
||||
|
||||
# TODO: Should this be a set or hash?
|
||||
@resource_list_manipulators = []
|
||||
@needs_sitemap_rebuild = true
|
||||
|
@ -187,6 +191,7 @@ module Middleman
|
|||
end
|
||||
|
||||
invalidate_resources_not_ignored_cache!
|
||||
@update_count += 1
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -172,7 +172,7 @@ module Middleman
|
|||
.lazy
|
||||
.select { |d| d.type == type }
|
||||
.map { |d| d.find(path, glob) }
|
||||
.reject { |d| d.nil? }
|
||||
.reject(&:nil?)
|
||||
.first
|
||||
end
|
||||
|
||||
|
|
|
@ -102,7 +102,7 @@ module Middleman
|
|||
|
||||
partial_file = locate_partial(name)
|
||||
|
||||
return "" unless partial_file
|
||||
return '' unless partial_file
|
||||
raise ::Middleman::TemplateRenderer::TemplateNotFound, "Could not locate partial: #{name}" unless partial_file
|
||||
|
||||
source_path = sitemap.file_to_path(partial_file)
|
||||
|
|
|
@ -14,6 +14,11 @@ require 'rack/mime'
|
|||
# DbC
|
||||
require 'middleman-core/contracts'
|
||||
|
||||
# For URI templating
|
||||
require 'addressable/template'
|
||||
require 'active_support/inflector'
|
||||
require 'active_support/inflector/transliterate'
|
||||
|
||||
module Middleman
|
||||
module Util
|
||||
include Contracts
|
||||
|
@ -384,5 +389,93 @@ module Middleman
|
|||
resource_url
|
||||
end
|
||||
end
|
||||
|
||||
# Handy methods for dealing with URI templates. Mix into whatever class.
|
||||
module UriTemplates
|
||||
module_function
|
||||
|
||||
# Given a URI template string, make an Addressable::Template
|
||||
# This supports the legacy middleman-blog/Sinatra style :colon
|
||||
# URI templates as well as RFC6570 templates.
|
||||
#
|
||||
# @param [String] tmpl_src URI template source
|
||||
# @return [Addressable::Template] a URI template
|
||||
def uri_template(tmpl_src)
|
||||
# Support the RFC6470 templates directly if people use them
|
||||
if tmpl_src.include?(':')
|
||||
tmpl_src = tmpl_src.gsub(/:([A-Za-z0-9]+)/, '{\1}')
|
||||
end
|
||||
|
||||
Addressable::Template.new ::Middleman::Util.normalize_path(tmpl_src)
|
||||
end
|
||||
|
||||
# Apply a URI template with the given data, producing a normalized
|
||||
# Middleman path.
|
||||
#
|
||||
# @param [Addressable::Template] template
|
||||
# @param [Hash] data
|
||||
# @return [String] normalized path
|
||||
def apply_uri_template(template, data)
|
||||
::Middleman::Util.normalize_path Addressable::URI.unencode(template.expand(data)).to_s
|
||||
end
|
||||
|
||||
# Use a template to extract parameters from a path, and validate some special (date)
|
||||
# keys. Returns nil if the special keys don't match.
|
||||
#
|
||||
# @param [Addressable::Template] template
|
||||
# @param [String] path
|
||||
def extract_params(template, path)
|
||||
template.extract(path, BlogTemplateProcessor)
|
||||
end
|
||||
|
||||
# Parameterize a string preserving any multibyte characters
|
||||
def safe_parameterize(str)
|
||||
sep = '-'
|
||||
|
||||
# Reimplementation of http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-parameterize that preserves un-transliterate-able multibyte chars.
|
||||
parameterized_string = ActiveSupport::Inflector.transliterate(str.to_s).downcase
|
||||
parameterized_string.gsub!(/[^a-z0-9\-_\?]+/, sep)
|
||||
|
||||
parameterized_string.chars.to_a.each_with_index do |char, i|
|
||||
next unless char == '?' && str[i].bytes.count != 1
|
||||
parameterized_string[i] = str[i]
|
||||
end
|
||||
|
||||
re_sep = Regexp.escape(sep)
|
||||
# No more than one of the separator in a row.
|
||||
parameterized_string.gsub!(/#{re_sep}{2,}/, sep)
|
||||
# Remove leading/trailing separator.
|
||||
parameterized_string.gsub!(/^#{re_sep}|#{re_sep}$/, '')
|
||||
|
||||
parameterized_string
|
||||
end
|
||||
|
||||
# Convert a date into a hash of components to strings
|
||||
# suitable for using in a URL template.
|
||||
# @param [DateTime] date
|
||||
# @return [Hash] parameters
|
||||
def date_to_params(date)
|
||||
{
|
||||
year: date.year.to_s,
|
||||
month: date.month.to_s.rjust(2, '0'),
|
||||
day: date.day.to_s.rjust(2, '0')
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# A special template processor that validates date fields
|
||||
# and has an extra-permissive default regex.
|
||||
#
|
||||
# See https://github.com/sporkmonger/addressable/blob/master/lib/addressable/template.rb#L279
|
||||
class BlogTemplateProcessor
|
||||
def self.match(name)
|
||||
case name
|
||||
when 'year' then '\d{4}'
|
||||
when 'month' then '\d{2}'
|
||||
when 'day' then '\d{2}'
|
||||
else '.*?'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,6 +29,7 @@ Gem::Specification.new do |s|
|
|||
# Helpers
|
||||
s.add_dependency('activesupport', ['~> 4.1.0'])
|
||||
s.add_dependency('padrino-helpers', ['~> 0.12.3'])
|
||||
s.add_dependency("addressable", ["~> 2.3.5"])
|
||||
|
||||
# Watcher
|
||||
s.add_dependency('listen', ['>= 2.7.9', '< 3.0'])
|
||||
|
|
Loading…
Reference in a new issue