Merge pull request #1307 from middleman/collections

Collections
This commit is contained in:
Thomas Reynolds 2014-10-15 17:19:43 -05:00
commit 127fba17ef
36 changed files with 964 additions and 57 deletions

View file

@ -55,3 +55,5 @@ CaseIndentation:
IndentWhenRelativeTo: end IndentWhenRelativeTo: end
TrivialAccessors: TrivialAccessors:
ExactNameMatch: true ExactNameMatch: true
PerceivedComplexity:
Enabled: false

View 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'

View file

@ -1,8 +1,9 @@
Feature: Console Feature: Console
Scenario: Enter and exit the console Scenario: Enter and exit the console
Given I run `middleman console` interactively Given a fixture app "large-build-app"
When I type "puts 'Hello from the console.'" When I run `middleman console` interactively
And I type "puts 'Hello from the console.'"
And I type "exit" And I type "exit"
Then it should pass with: Then it should pass with:
""" """

View 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"

View 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
}

View file

@ -0,0 +1,7 @@
---
title: "Blog1 Newer Article"
date: 2011-01-01
tags: foo, bar
---
Newer Article Content

View file

@ -0,0 +1,9 @@
---
title: "Blog1 Another Article"
date: 2011-01-02
tags:
- foo
- 120
---
Another Article Content

View file

@ -0,0 +1,7 @@
---
title: "Blog2 Newer Article"
date: 2011-01-01
tags: foo, bar
---
Newer Article Content

View file

@ -0,0 +1,8 @@
---
title: "Blog2 Another Article"
date: 2011-01-02
tags:
- foo
---
Another Article Content

View file

@ -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>

View 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 %>

View file

@ -0,0 +1,6 @@
---
title: "Test Article"
date: 2011-01-01
tags: foo
---
Test Article Content

View file

@ -0,0 +1,6 @@
---
title: "Test Article"
date: 2011-01-02
tags: foo
---
Test Article Content

View file

@ -0,0 +1,6 @@
---
title: "Test Article"
date: 2011-01-03
tags: bar
---
Test Article Content

View file

@ -0,0 +1,6 @@
---
title: "Test Article"
date: 2011-01-04
tags: bar
---
Test Article Content

View file

@ -0,0 +1,6 @@
---
title: "Test Article"
date: 2011-01-05
tags: bar
---
Test Article Content

View file

@ -0,0 +1,6 @@
---
title: "Test Article"
date: 2011-02-01
tags: bar
---
Test Article Content

View file

@ -0,0 +1,6 @@
---
title: "Test Article"
date: 2011-02-02
tags: bar
---
Test Article Content

View 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 %>

View 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 %>

View file

@ -59,6 +59,21 @@ if ENV['TEST'] || ENV['CONTRACTS'] == 'true'
end end
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']] ResourceList = Contracts::ArrayOf[IsA['Middleman::Sitemap::Resource']]
end end
else else
@ -141,6 +156,9 @@ else
class Frozen < Callable class Frozen < Callable
end end
# class MethodDefined < Callable
# end
end end
end end

View file

@ -52,6 +52,11 @@ Middleman::Extensions.register :routing, auto_activate: :before_configuration do
Middleman::CoreExtensions::Routing Middleman::CoreExtensions::Routing
end end
Middleman::Extensions.register :collections, auto_activate: :before_configuration do
require 'middleman-core/core_extensions/collections'
Middleman::CoreExtensions::Collections::CollectionsExtension
end
### ###
# Setup Optional Extensions # Setup Optional Extensions
### ###

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -12,7 +12,7 @@ module Middleman::CoreExtensions
end end
def after_configuration 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 end
end end

View file

@ -1,4 +1,5 @@
require 'middleman-core/sitemap/resource' require 'middleman-core/sitemap/resource'
require 'middleman-core/core_extensions/collections/step_context'
module Middleman module Middleman
module Sitemap module Sitemap
@ -13,6 +14,13 @@ module Middleman
@app.define_singleton_method(:proxy, &method(:create_proxy)) @app.define_singleton_method(:proxy, &method(:create_proxy))
@proxy_configs = Set.new @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 end
# Setup a proxy from a path to a target # Setup a proxy from a path to a target
@ -27,71 +35,49 @@ module Middleman
Contract String, String, Maybe[Hash] => Any Contract String, String, Maybe[Hash] => Any
def create_proxy(path, target, opts={}) def create_proxy(path, target, opts={})
options = opts.dup options = opts.dup
@app.ignore(target) if options.delete(:ignore) @app.ignore(target) if options.delete(:ignore)
metadata = { @proxy_configs << create_anonymous_proxy(path, target, options)
options: options,
locals: options.delete(:locals) || {},
page: options.delete(:data) || {}
}
@proxy_configs << ProxyConfiguration.new(path: path, target: target, metadata: metadata)
@app.sitemap.rebuild_resource_list!(:added_proxy) @app.sitemap.rebuild_resource_list!(:added_proxy)
end 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 # Update the main sitemap resource list
# @return Array<Middleman::Sitemap::Resource> # @return Array<Middleman::Sitemap::Resource>
Contract ResourceList => ResourceList Contract ResourceList => ResourceList
def manipulate_resource_list(resources) def manipulate_resource_list(resources)
resources + @proxy_configs.map do |config| resources + @proxy_configs.map { |c| c.to_resource(@app) }
p = ProxyResource.new( end
@app.sitemap, end
config.path,
config.target 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
) )
p.add_metadata(config.metadata)
p
end end
end end
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
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 end
class ProxyResource < ::Middleman::Sitemap::Resource class ProxyResource < ::Middleman::Sitemap::Resource

View file

@ -165,7 +165,11 @@ module Middleman
# Ignore based on the source path (without template extensions) # Ignore based on the source path (without template extensions)
return true if @app.sitemap.ignored?(path) return true if @app.sitemap.ignored?(path)
# This allows files to be ignored by their source file name (with template extensions) # 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 end
# The preferred MIME content type for this resource based on extension or metadata # The preferred MIME content type for this resource based on extension or metadata
@ -174,6 +178,31 @@ module Middleman
def content_type def content_type
options[:content_type] || ::Rack::Mime.mime_type(ext, nil) options[:content_type] || ::Rack::Mime.mime_type(ext, nil)
end 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 end
end end

View file

@ -49,11 +49,15 @@ module Middleman
# @return [Middleman::Application] # @return [Middleman::Application]
attr_reader :app attr_reader :app
attr_reader :update_count
# Initialize with parent app # Initialize with parent app
# @param [Middleman::Application] app # @param [Middleman::Application] app
def initialize(app) def initialize(app)
@app = app @app = app
@resources = [] @resources = []
@update_count = 0
# TODO: Should this be a set or hash? # TODO: Should this be a set or hash?
@resource_list_manipulators = [] @resource_list_manipulators = []
@needs_sitemap_rebuild = true @needs_sitemap_rebuild = true
@ -187,6 +191,7 @@ module Middleman
end end
invalidate_resources_not_ignored_cache! invalidate_resources_not_ignored_cache!
@update_count += 1
end end
end end

View file

@ -172,7 +172,7 @@ module Middleman
.lazy .lazy
.select { |d| d.type == type } .select { |d| d.type == type }
.map { |d| d.find(path, glob) } .map { |d| d.find(path, glob) }
.reject { |d| d.nil? } .reject(&:nil?)
.first .first
end end

View file

@ -102,7 +102,7 @@ module Middleman
partial_file = locate_partial(name) 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 raise ::Middleman::TemplateRenderer::TemplateNotFound, "Could not locate partial: #{name}" unless partial_file
source_path = sitemap.file_to_path(partial_file) source_path = sitemap.file_to_path(partial_file)

View file

@ -14,6 +14,11 @@ require 'rack/mime'
# DbC # DbC
require 'middleman-core/contracts' require 'middleman-core/contracts'
# For URI templating
require 'addressable/template'
require 'active_support/inflector'
require 'active_support/inflector/transliterate'
module Middleman module Middleman
module Util module Util
include Contracts include Contracts
@ -384,5 +389,93 @@ module Middleman
resource_url resource_url
end end
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
end end

View file

@ -29,6 +29,7 @@ Gem::Specification.new do |s|
# Helpers # Helpers
s.add_dependency('activesupport', ['~> 4.1.0']) s.add_dependency('activesupport', ['~> 4.1.0'])
s.add_dependency('padrino-helpers', ['~> 0.12.3']) s.add_dependency('padrino-helpers', ['~> 0.12.3'])
s.add_dependency("addressable", ["~> 2.3.5"])
# Watcher # Watcher
s.add_dependency('listen', ['>= 2.7.9', '< 3.0']) s.add_dependency('listen', ['>= 2.7.9', '< 3.0'])