vanilla 1.17.1 → 1.17.2

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -1,10 +1,10 @@
1
1
  --~::{ Vanilla.rb }::~---
2
2
  =========================
3
3
 
4
- A wandering soul; a repo clone'd -
5
- his meta_klass and methods honed.
6
- But tarry he 'ponst what dark endeavour
7
- with code of such unknowable terror?
4
+ A wandering soul; a repo clone'd -
5
+ his meta_klass and methods honed.
6
+ But tarry he 'ponst what dark endeavour
7
+ with code of such unknowable terror?
8
8
 
9
9
  - H.P. Gemcraft, 1914
10
10
 
data/Rakefile CHANGED
@@ -18,7 +18,6 @@ namespace :test do
18
18
  t.verbose = true
19
19
  end
20
20
 
21
- require "rake/testtask"
22
21
  Rake::TestTask.new(:app) do |t|
23
22
  t.libs << "test/pristine_app"
24
23
  t.ruby_opts << "-rubygems"
@@ -28,6 +27,20 @@ namespace :test do
28
27
  end
29
28
 
30
29
  if Object.const_defined?(:Gem)
30
+
31
+ def files_for_inclusion
32
+ base_files = %w(Rakefile README .gemtest) + Dir.glob("{test,lib,bin,pristine_app}/**/*")
33
+ files_to_ignore = File.readlines(".gitignore").inject([]) do |a,p|
34
+ path = p.strip
35
+ a += Dir.glob(path)
36
+ if File.directory?(path)
37
+ a += Dir.glob(path + "/*")
38
+ end
39
+ a
40
+ end
41
+ base_files - files_to_ignore
42
+ end
43
+
31
44
  # This builds the actual gem. For details of what all these options
32
45
  # mean, and other ones you can add, check the documentation here:
33
46
  #
@@ -48,13 +61,13 @@ if Object.const_defined?(:Gem)
48
61
  s.rdoc_options = %w(--main README)
49
62
 
50
63
  # Add any extra files to include in the gem
51
- s.files = %w(Rakefile README .gemtest) + Dir.glob("{test,lib,bin,pristine_app}/**/*")
64
+ s.files = files_for_inclusion
52
65
  s.executables = ['vanilla']
53
66
  s.require_paths = ["lib"]
54
67
 
55
68
  # All the other gems we need.
56
69
  s.add_dependency("rack", ">= 0.9.1")
57
- s.add_dependency("soup", ">= 1.0.8")
70
+ s.add_dependency("soup", ">= 1.0.9")
58
71
  s.add_dependency("ratom", ">= 0.3.5")
59
72
  s.add_dependency("RedCloth", ">= 4.1.1")
60
73
  s.add_dependency("BlueCloth", ">= 1.0.0")
@@ -62,7 +75,7 @@ if Object.const_defined?(:Gem)
62
75
  s.add_dependency("parslet", ">= 1.2.0")
63
76
  s.add_dependency("rack-test", ">=0.5.7")
64
77
 
65
- s.add_development_dependency("kintama", ">= 0.1.6") # add any other gems for testing/development
78
+ s.add_development_dependency("kintama", ">= 0.1.7") # add any other gems for testing/development
66
79
  s.add_development_dependency("mocha")
67
80
  s.add_development_dependency("capybara")
68
81
  s.add_development_dependency("launchy")
@@ -111,9 +124,7 @@ if Object.const_defined?(:Gem)
111
124
  end
112
125
 
113
126
  desc 'Clear out RDoc and generated packages'
114
- task :clean => [:clobber_rdoc, :clobber_package] do
115
- rm "#{spec.name}.gemspec"
116
- end
127
+ task :clean => [:clobber_rdoc, :clobber_package]
117
128
 
118
129
  desc 'Tag the repository in git with gem version number'
119
130
  task :tag => [:gemspec, :package] do
@@ -1,5 +1,5 @@
1
1
  module Vanilla
2
- VERSION = "1.17.1"
2
+ VERSION = "1.17.2"
3
3
 
4
4
  autoload :Renderers, "vanilla/renderers"
5
5
  autoload :App, "vanilla/app"
@@ -9,8 +9,11 @@ module Vanilla
9
9
  autoload :Routing, "vanilla/routing"
10
10
  autoload :Static, "vanilla/static"
11
11
  autoload :SnipReferenceParser, "vanilla/snip_reference_parser"
12
+ autoload :AtomFeed, "vanilla/atom_feed"
12
13
  autoload :TestHelper, "vanilla/test_helper"
13
14
 
15
+ class MissingRendererError < RuntimeError; end
16
+
14
17
  class << self
15
18
  # The set of currently loaded Vanilla::App subclasses
16
19
  def apps
@@ -68,7 +68,13 @@ module Vanilla
68
68
  # Returns the renderer class for a given snip
69
69
  def renderer_for(snip)
70
70
  if snip
71
- find_renderer(snip.render_as || snip.extension)
71
+ renderer_name = snip.render_as || snip.extension
72
+ renderer_name = nil if renderer_name == ''
73
+ else
74
+ renderer_name = nil
75
+ end
76
+ if renderer_name
77
+ find_renderer(renderer_name)
72
78
  else
73
79
  config.default_renderer
74
80
  end
@@ -87,6 +93,10 @@ module Vanilla
87
93
  end
88
94
  end
89
95
 
96
+ def atom_feed(options={})
97
+ AtomFeed.new(options.merge(:app => self))
98
+ end
99
+
90
100
  private
91
101
 
92
102
  def prepare_renderers
@@ -94,13 +104,18 @@ module Vanilla
94
104
  end
95
105
 
96
106
  def find_renderer(name)
97
- @renderers[(name ? name.downcase : nil)]
107
+ if @renderers.has_key?(name.downcase)
108
+ @renderers[name.downcase]
109
+ else
110
+ raise MissingRendererError.new(name)
111
+ end
98
112
  end
99
113
 
100
114
  def rendering_and_handling_errors(snip)
101
115
  renderer_instance = renderer_for(snip).new(self)
102
116
  yield renderer_instance
103
117
  rescue Exception => e
118
+ response.status = 500
104
119
  snip_name = snip ? snip.name : nil
105
120
  "<pre>[Error rendering '#{snip_name}' - \"" +
106
121
  e.message.gsub("<", "&lt;").gsub(">", "&gt;") + "\"]\n" +
@@ -0,0 +1,77 @@
1
+ require "atom"
2
+
3
+ module Vanilla
4
+ class AtomFeed
5
+ attr_reader :domain, :app, :title
6
+
7
+ def initialize(params={})
8
+ @domain = params[:domain] || "yourdomain.example.com"
9
+ @title = params[:title] || domain
10
+ @app = params[:app]
11
+ @criteria = params[:matching]
12
+ @snips = params[:snips]
13
+ if @snips.nil?
14
+ if @criteria.nil?
15
+ @snips = app.soup.all_snips
16
+ else
17
+ @snips = app.soup[@criteria]
18
+ end
19
+ end
20
+ end
21
+
22
+ def to_s
23
+ Atom::Feed.new do |f|
24
+ f.title = title
25
+ f.updated = most_recent_updated_at
26
+ f.id = "tag:x,2008-06-01:kind/x"
27
+ f.entries = entries
28
+ end.to_xml
29
+ end
30
+
31
+ private
32
+
33
+ def snips
34
+ @snips.sort_by { |s| atom_time(s.updated_at) }.reverse
35
+ end
36
+
37
+ def most_recent_updated_at
38
+ if snips.first
39
+ atom_time(snips.first.updated_at)
40
+ else
41
+ atom_time(nil)
42
+ end
43
+ end
44
+
45
+ def entries
46
+ snips.map do |snip|
47
+ Atom::Entry.new do |e|
48
+ e.published = atom_time(snip.created_at)
49
+ e.updated = atom_time(snip.updated_at || snip.created_at)
50
+ e.content = Atom::Content::Html.new(externalise_links(app.render(snip)))
51
+ e.title = snip.title || snip.name
52
+ e.authors = [Atom::Person.new(:name => snip.author || domain)]
53
+ e.links << Atom::Link.new(:href => "http://#{domain}#{app.url_to(snip.name)}")
54
+ e.id = "tag:#{domain},#{atom_time(snip.created_at || Time.now).split("T")[0]}:/#{snip.name}"
55
+ end
56
+ end
57
+ end
58
+
59
+ def atom_time(time)
60
+ return Time.at(0) if time.nil?
61
+ time = Time.parse(time) unless time.respond_to?(:strftime)
62
+ time.strftime("%Y-%m-%dT%H:%M:%S%z").insert(-3, ":")
63
+ end
64
+
65
+ def externalise_links(content)
66
+ content.gsub(/(href|src)=(["'])(\/?.*)\2/) do
67
+ type, quote, link = $1, $2, $3
68
+ if link =~ /^http/
69
+ "#{type}=#{quote}#{link}#{quote}"
70
+ else
71
+ absolute_link = "http://#{domain}" + (link =~ /^\// ? "" : "/") + link
72
+ "#{type}=#{quote}#{absolute_link}#{quote}"
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -2,40 +2,53 @@ require 'vanilla/renderers/base'
2
2
  require 'enumerator'
3
3
 
4
4
  class Dynasnip < Vanilla::Renderers::Base
5
-
5
+
6
6
  def self.all
7
7
  ObjectSpace.enum_for(:each_object, class << self; self; end).to_a - [self]
8
8
  end
9
-
10
- def self.snip_name(new_name=nil)
11
- if new_name
12
- @snip_name = new_name.to_s
13
- else
14
- # borrowed from ActiveSupport
15
- @snip_name ||= self.name.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
16
- gsub(/([a-z\d])([A-Z])/,'\1_\2').
17
- tr("-", "_").
18
- downcase
19
- end
20
- end
21
-
9
+
22
10
  def self.attribute(attribute_name, attribute_value=nil)
23
11
  @attributes ||= {}
24
12
  @attributes[attribute_name.to_sym] = attribute_value if attribute_value
25
13
  @attributes[attribute_name.to_sym]
26
14
  end
27
-
28
- def self.usage(str)
29
- attribute :usage, escape_curly_braces(str).strip
15
+
16
+ def self.default_snip_name
17
+ # borrowed from ActiveSupport
18
+ formatted_name = self.name.
19
+ split("::").last.
20
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
21
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
22
+ tr("-", "_").
23
+ downcase
24
+ end
25
+
26
+ def self.snip_name(new_name=nil)
27
+ if new_name.nil?
28
+ if name = attribute(:snip_name)
29
+ name
30
+ else
31
+ name = default_snip_name
32
+ attribute :snip_name, name
33
+ name
34
+ end
35
+ else
36
+ attribute :snip_name, new_name
37
+ new_name
38
+ end
30
39
  end
31
-
40
+
41
+ def self.usage(str=nil)
42
+ attribute :usage, str
43
+ end
44
+
32
45
  def self.snip_attributes
33
46
  full_snip_attributes = {:name => snip_name, :content => self.name, :render_as => "Ruby"}
34
47
  @attributes ? full_snip_attributes.merge!(@attributes) : full_snip_attributes
35
48
  end
36
-
49
+
37
50
  attr_accessor :enclosing_snip
38
-
51
+
39
52
  def method_missing(method, *args)
40
53
  if snip
41
54
  snip.__send__(method)
@@ -45,28 +58,29 @@ class Dynasnip < Vanilla::Renderers::Base
45
58
  super
46
59
  end
47
60
  end
48
-
61
+
49
62
  # dynasnips gain access to the app in the same way as Render::Base
50
63
  # subclasses
51
-
64
+
52
65
  protected
53
-
66
+
67
+ def requesting_this_snip?
68
+ app.request.snip_name == snip_name
69
+ end
70
+
54
71
  def snip_name
55
72
  self.class.snip_name
56
73
  end
57
-
74
+
75
+ def usage
76
+ str = self.class.attribute(:usage)
77
+ self.class.escape_curly_braces(str).strip if str
78
+ end
79
+
58
80
  def snip
59
81
  app.soup[snip_name]
60
82
  end
61
-
62
- def show_usage
63
- if snip.usage
64
- Vanilla::Renderers::Markdown.render(snip_name, :usage)
65
- else
66
- "No usage information for #{snip_name}"
67
- end
68
- end
69
-
83
+
70
84
  def cleaned_params
71
85
  p = app.request.params.dup
72
86
  p.delete(:snip)
@@ -26,8 +26,12 @@ module Vanilla
26
26
  rule(:spaces?) { spaces.maybe }
27
27
  rule(:comma) { match(',') }
28
28
  rule(:dot) { str(".") }
29
- rule(:squote) { str("'") }
30
- rule(:dquote) { str('"') }
29
+ rule(:normal_squote) { str("'") }
30
+ rule(:html_squote) { str("&lsquo;") | str('&rsquo;') }
31
+ rule(:squote) { normal_squote | html_squote }
32
+ rule(:normal_dquote) { str('"') }
33
+ rule(:html_dquote) { str('&ldquo;') | str('&rdquo;') }
34
+ rule(:dquote) { normal_dquote | html_dquote }
31
35
  rule(:escaped_dquote) { str('"') }
32
36
  rule(:left_brace) { str("{") }
33
37
  rule(:right_brace) { str("}") }
@@ -2,10 +2,12 @@ require "vanilla"
2
2
  require "rack/test"
3
3
  require "tmpdir"
4
4
  require "fileutils"
5
+ require "soup"
5
6
 
6
7
  module Vanilla
7
8
  module TestHelper
8
9
  include Rack::Test::Methods
10
+ include Soup::TestHelper
9
11
 
10
12
  def app(klass=Vanilla.apps.first)
11
13
  unless @__app
@@ -54,5 +56,9 @@ module Vanilla
54
56
  def vanilla_teardown
55
57
  FileUtils.rm_rf(test_soup_path)
56
58
  end
59
+
60
+ def stub_app_soup(*snips)
61
+ app.stubs(:soup).returns(stub_soup(*snips))
62
+ end
57
63
  end
58
64
  end
@@ -1,6 +1,7 @@
1
+ require "application"
2
+
1
3
  # If you're running your site under a proper webserver, you probably don't need this.
2
4
  require 'vanilla/static'
3
5
  use Vanilla::Static, File.join(File.dirname(__FILE__), 'public')
4
6
 
5
- require "application"
6
7
  run Application.new
@@ -0,0 +1,12 @@
1
+ require "vanilla/dynasnip"
2
+
3
+ class Feed < Dynasnip
4
+ def handle(*args)
5
+ app.atom_feed({
6
+ :domain => "yourdomain.example.com", # change this
7
+ :title => "My Feed", # and this,
8
+ :matching => {:kind => "blog"}, # but probably not this, although you can if you like.
9
+ })
10
+ end
11
+ self
12
+ end
@@ -14,6 +14,7 @@ class CurrentSnip < Dynasnip
14
14
  |
15
15
 
16
16
  def handle(attribute=nil)
17
+ return usage if app.request.snip_name == snip_name
17
18
  if app.request.snip
18
19
  if attribute ||= app.request.part
19
20
  %|{"#{app.request.snip_name}"."#{attribute}"}|
@@ -3,7 +3,7 @@ require 'vanilla/dynasnip'
3
3
  class Index < Dynasnip
4
4
  def get(*args)
5
5
  list = app.soup.all_snips.sort_by { |a| a.updated_at || Time.at(0) }.reverse.map { |snip|
6
- "<li>{link_to #{snip.name}}</li>"
6
+ %{<li>{link_to "#{snip.name}"}</li>}
7
7
  }
8
8
  %{<ol id="index">#{list}</ol>}
9
9
  end
@@ -8,11 +8,13 @@ The link_to dyna lets you create links between snips:
8
8
 
9
9
  would insert a link to the blah snip.|
10
10
 
11
- def handle(snip_name, link_text=snip_name, part=nil)
12
- if app.soup[snip_name]
13
- %{<a href="#{url_to(snip_name, part)}">#{link_text}</a>}
11
+ def handle(name=nil, link_text=name, part=nil)
12
+ return usage if requesting_this_snip?
13
+ return "You must provide a snip name" unless name
14
+ if app.soup[name]
15
+ %{<a href="#{url_to(name, part)}">#{link_text}</a>}
14
16
  else
15
- %{<a class="missing" href="#{url_to(snip_name, part)}">#{link_text}</a>}
17
+ %{<a class="missing" href="#{url_to(name, part)}">#{link_text}</a>}
16
18
  end
17
19
  end
18
20
 
@@ -13,7 +13,8 @@ class ShowContentInPreTag < Dynasnip
13
13
  {pre my_snip,specific_part}
14
14
  |
15
15
 
16
- def handle(snip_name, part=:content)
16
+ def handle(snip_name=nil, part=:content)
17
+ return usage if requesting_this_snip?
17
18
  %{<pre>#{app.soup[snip_name].__send__(part || :content)}</pre>}
18
19
  end
19
20
 
@@ -14,7 +14,8 @@ class ShowRawContent < Dynasnip
14
14
  {raw my_snip,specific_part}
15
15
  |
16
16
 
17
- def handle(snip_name, part=:content)
17
+ def handle(snip_name=nil, part=:content)
18
+ return usage if requesting_this_snip?
18
19
  %{#{Dynasnip.escape_curly_braces(CGI.escapeHTML(app.soup[snip_name].__send__(part || :content)))}}
19
20
  end
20
21
 
@@ -23,19 +23,23 @@ Here's the source of `current_snip`:
23
23
 
24
24
  {raw current_snip}
25
25
 
26
- The default case, as in our layout, is `app.render(app.request.snip, app.request.part)` - it delegates rendering back to the application, which then takes care of processing `start` using the right renderer and so on. This method call returns the fully rendered string, and vanilla replaces the call to the dynasnip with that output, placing our snip in the appropriate place in the layout.
26
+ The default case just renders a string corresponding to snip inclusion of whichever snip was actually requested. In the case of `/start`, this will generate the string "&#123;start&#125;", which Vanilla itself then deals with, including the contents of that snip using the right renderer and so on, placing our snip in the appropriate place in the layout.
27
27
 
28
28
 
29
29
  Other dynas
30
30
  -----------
31
31
 
32
- Of course, you can put other plain content in your layout, and other dynasnips too. In the provided layout there are calls to two other dynasnips.
32
+ Of course, you can put other plain content in your layout, and other dynasnips too. In the provided layout there are calls to three other dynasnips.
33
33
 
34
34
  The first is `page_title`, which simply places a (hopefully) meaningful string in the title element of the page. Snips can set the title to be used by defining a `:page_title` attribute. As usual, the source explains it more clearly:
35
35
 
36
36
  {raw page_title}
37
37
 
38
- The second dynasnip used is `link_to_current_snip`, which returns an HTML link to the snip that's currently being rendered. I'll let you figure out how to view the source yourself.
38
+ The second is `link_to`, which is used to create links to other snips by name; although of course you can still just write HTML, this is often more convenient and wiki-esque. Here's the source:
39
+
40
+ {raw link_to}
41
+
42
+ The third dynasnip used is `link_to_current_snip`, which returns an HTML link to the snip that's currently being rendered. I'll let you figure out how to view the source yourself.
39
43
 
40
44
 
41
45
  Other layouts
@@ -0,0 +1,214 @@
1
+ require "test_helper"
2
+ require "atom"
3
+
4
+ context "An atom feed" do
5
+ should "include snips" do
6
+ stub_app_soup({:name => "Hello", :content => "This is the content"},
7
+ {:name => "Goodbye", :content => "More content"})
8
+
9
+ feed_xml = Vanilla::AtomFeed.new(:domain => "yourdomain.example.com", :app => app).to_s
10
+ feed = Atom::Feed.load_feed(feed_xml)
11
+ assert_equal 2, feed.entries.length
12
+ end
13
+
14
+ context "when making links absolute" do
15
+ should "handle double quoted a tags" do
16
+ stub_app_soup({:name => "x", :content => %|<a href="/x">x</a>.|})
17
+
18
+ feed = get_feed
19
+ assert feed.entries.first.content =~ %r{http://yourdomain\.example\.com/x},
20
+ "double-quoted links external should work (got: #{feed.entries.first.content})"
21
+ end
22
+
23
+ should "handle single quoted a tags" do
24
+ stub_app_soup({:name => "x", :content => %|<a href='/x'>x</a>.|})
25
+
26
+ feed = get_feed
27
+ assert feed.entries.first.content =~ %r{http://yourdomain\.example\.com/x},
28
+ "single-quoted links external should work (got: #{feed.entries.first.content})"
29
+ end
30
+
31
+ should "handle double quoted a tags without slahes" do
32
+ stub_app_soup({:name => "x", :content => %|<a href="x">x</a>.|})
33
+
34
+ feed = get_feed
35
+ assert feed.entries.first.content =~ %r{http://yourdomain\.example\.com/x},
36
+ "double-quoted links external should work (got: #{feed.entries.first.content})"
37
+ end
38
+
39
+ should "handle single quoted a tags without slashes" do
40
+ stub_app_soup({:name => "x", :content => %|<a href='x'>x</a>.|})
41
+
42
+ feed = get_feed
43
+ assert feed.entries.first.content =~ %r{http://yourdomain\.example\.com/x},
44
+ "single-quoted links external should work (got: #{feed.entries.first.content})"
45
+ end
46
+
47
+ should "handle double quoted img tags" do
48
+ stub_app_soup({:name => "x", :content => %|<img src="/x.jpg" />|})
49
+
50
+ feed = get_feed
51
+ assert feed.entries.first.content =~ %r{http://yourdomain\.example\.com/x.jpg},
52
+ "single-quoted links external should work (got: #{feed.entries.first.content})"
53
+ end
54
+
55
+ should "handle single quoted img tags" do
56
+ stub_app_soup({:name => "x", :content => %|<img src='/x.jpg' />|})
57
+
58
+ feed = get_feed
59
+ assert feed.entries.first.content =~ %r{http://yourdomain\.example\.com/x.jpg},
60
+ "single-quoted links external should work (got: #{feed.entries.first.content})"
61
+ end
62
+
63
+ should "handle double quoted img tags without slashes" do
64
+ stub_app_soup({:name => "x", :content => %|<img src="x.jpg" />|})
65
+
66
+ feed = get_feed
67
+ assert feed.entries.first.content =~ %r{http://yourdomain\.example\.com/x.jpg},
68
+ "single-quoted links external should work (got: #{feed.entries.first.content})"
69
+ end
70
+
71
+ should "handle single quoted img tags without slashes" do
72
+ stub_app_soup({:name => "x", :content => %|<img src='x.jpg' />|})
73
+
74
+ feed = get_feed
75
+ assert feed.entries.first.content =~ %r{http://yourdomain\.example\.com/x.jpg},
76
+ "single-quoted links external should work (got: #{feed.entries.first.content})"
77
+ end
78
+
79
+ should "not alter urls that are already absolute" do
80
+ stub_app_soup({:name => "x", :content => %|<img src='http://x.com/' />|})
81
+
82
+ feed = get_feed
83
+ assert feed.entries.first.content =~ %r{src='http://x\.com/'},
84
+ "should not alter absolute links (got: #{feed.entries.first.content})"
85
+ end
86
+ end
87
+
88
+ should "allow inclusion of only specific snips" do
89
+ snip_a_data = {:name => "a", :content => "x"}
90
+ snip_b_data = {:name => "b", :content => "x"}
91
+ stub_app_soup(snip_a_data, snip_b_data)
92
+
93
+ feed_xml = app.atom_feed(:domain => "whatever", :snips => [app.soup["a"]]).to_s
94
+ feed = Atom::Feed.load_feed(feed_xml)
95
+ assert_equal 1, feed.entries.length
96
+ assert_equal "a", feed.entries.first.title
97
+ end
98
+
99
+ should "allow filtering of snips by matching criteria" do
100
+ stub_app_soup({:name => "a", :content => "x", :kind => "blog"},
101
+ {:name => "b", :content => "x", :kind => "draft"},
102
+ {:name => "c", :content => "x", :kind => "blog"})
103
+
104
+ feed_xml = app.atom_feed(:domain => "whatever", :matching => {:kind => "blog"}).to_s
105
+ feed = Atom::Feed.load_feed(feed_xml)
106
+ assert_equal 2, feed.entries.length
107
+ assert_same_elements ["a", "c"], feed.entries.map { |e| e.title }
108
+ end
109
+
110
+ should "set updated to be the latest updated_at of the included snips" do
111
+ snip_a_data = {:name => "a", :content => "x", :updated_at => Time.parse("2011-05-22 12:00")}
112
+ snip_b_data = {:name => "b", :content => "x", :updated_at => Time.parse("2011-05-23 12:34")}
113
+ snip_c_data = {:name => "c", :content => "x", :updated_at => Time.parse("2011-05-24 12:34")}
114
+ stub_app_soup(snip_a_data, snip_b_data, snip_c_data)
115
+
116
+ feed_xml = app.atom_feed(:domain => "whatever", :snips => [app.soup["a"], app.soup["b"]]).to_s
117
+ feed = Atom::Feed.load_feed(feed_xml)
118
+ assert_equal Time.parse("2011-05-23 12:34"), feed.updated
119
+ end
120
+
121
+ should "format updated_at as an RFC-3339 date-time" do
122
+ stub_app_soup({:name => "x", :content => "y", :updated_at => Time.parse("2011-01-01 12:23 +0000").to_s})
123
+
124
+ feed_xml = app.atom_feed(:domain => "whatever", :snips => [app.soup["x"]]).to_s
125
+ assert_match %r{2011\-01\-01T12\:23\:00\+00\:00}, feed_xml
126
+ end
127
+
128
+ should "work even without any snips" do
129
+ feed_xml = app.atom_feed(:domain => "whatever", :snips => []).to_s
130
+ feed = Atom::Feed.load_feed(feed_xml)
131
+ end
132
+
133
+ context "title" do
134
+ setup do
135
+ stub_app_soup
136
+ end
137
+
138
+ should "be settable via the initialiser" do
139
+ feed_xml = app.atom_feed(:domain => "yourdomain.example.com", :title => "My Title").to_s
140
+ feed = Atom::Feed.load_feed(feed_xml)
141
+ assert_equal "My Title", feed.title
142
+ end
143
+
144
+ should "default to the domain" do
145
+ feed_xml = app.atom_feed(:domain => "yourdomain.example.com").to_s
146
+ feed = Atom::Feed.load_feed(feed_xml)
147
+ assert_equal "yourdomain.example.com", feed.title
148
+ end
149
+ end
150
+
151
+ context "entry" do
152
+ setup do
153
+ stub_app_soup({:name => "Hello", :content => "The *content*",
154
+ :render_as => "markdown", :created_at => Time.parse("2011-01-01 12:23").to_s})
155
+ end
156
+
157
+ context "titles" do
158
+ should "default to be the name of the snip" do
159
+ assert_equal "Hello", get_feed.entries.first.title
160
+ end
161
+
162
+ should "use the title of the snip if present" do
163
+ stub_app_soup({:name => "hello-mammy", :content => "x", :title => "Hello, Mammy"})
164
+ assert_equal "Hello, Mammy", get_feed.entries.first.title
165
+ end
166
+ end
167
+
168
+ context "authors" do
169
+ should "set the author to be the domain by default" do
170
+ assert_equal ["yourdomain.example.com"], get_feed.entries.first.authors.map { |a| a.name }
171
+ end
172
+
173
+ should "set the authors if the snip provides" do
174
+ stub_app_soup({:name => "a", :content => "x", :author => "james"})
175
+ assert_equal ["james"], get_feed.entries.first.authors.map { |a| a.name }
176
+ end
177
+ end
178
+
179
+ should "included rendered snip contents" do
180
+ assert_equal "<p>The <em>content</em></p>", get_feed.entries.first.content
181
+ end
182
+
183
+
184
+ should "include a link to the snip" do
185
+ assert_equal ["http://yourdomain.example.com/Hello"], get_feed.entries.first.links.map { |l| l.href }
186
+ end
187
+
188
+ should "set the ID based on the domain, timestamp and snip name" do
189
+ assert_equal "tag:yourdomain.example.com,2011-01-01:/Hello", get_feed.entries.first.id
190
+ end
191
+
192
+ should "set the published date based on the snip created_at date" do
193
+ assert_equal Time.parse("2011-01-01 12:23"), get_feed.entries.first.published
194
+ end
195
+
196
+ context "updated at" do
197
+ should "default to the published date" do
198
+ assert_equal Time.parse("2011-01-01 12:23"), get_feed.entries.first.updated
199
+ end
200
+
201
+ should "set the updated at to the snip attribute if it exists" do
202
+ stub_app_soup({:name => "Hello", :content => "the content", :updated_at => Time.parse("2011-01-02 13:45").to_s})
203
+ assert_equal Time.parse("2011-01-02 13:45"), get_feed.entries.first.updated
204
+ end
205
+ end
206
+ end
207
+
208
+ private
209
+
210
+ def get_feed
211
+ feed_xml = app.atom_feed(:domain => "yourdomain.example.com").to_s
212
+ Atom::Feed.load_feed(feed_xml)
213
+ end
214
+ end
@@ -65,6 +65,10 @@ context "Configuring a Vanilla app" do
65
65
  should "use only the specified soups" do
66
66
  assert_equal ["blah", "monkey"], TestConfigurationApp.new.config.soups
67
67
  end
68
+
69
+ teardown do
70
+ ["blah", "monkey"].each { |dir| FileUtils.rm_rf(dir) }
71
+ end
68
72
  end
69
73
 
70
74
  context "with new renderers" do
@@ -4,7 +4,7 @@ require 'vanilla/dynasnip'
4
4
  describe Dynasnip do
5
5
  context "when storing attributes" do
6
6
 
7
- class ::TestDyna < Dynasnip
7
+ class TestDyna < Dynasnip
8
8
  attribute :test_attribute, "test attribute content"
9
9
  end
10
10
 
@@ -27,16 +27,51 @@ describe Dynasnip do
27
27
  end
28
28
  end
29
29
 
30
+ context "determining name" do
31
+ module X
32
+ class TestDyna < Dynasnip
33
+ def handle(*args)
34
+ "name: #{snip_name}"
35
+ end
36
+ end
37
+ end
38
+
39
+ should "strip out modules from the name" do
40
+ assert_equal "test_dyna", X::TestDyna.snip_name
41
+ end
42
+
43
+ should "allow the snip to reference its own name" do
44
+ assert_equal "name: test_dyna", X::TestDyna.new(app).handle
45
+ end
46
+ end
47
+
48
+ context "setting name" do
49
+ class AnotherDyna < Dynasnip
50
+ end
51
+
52
+ should "be possible" do
53
+ AnotherDyna.snip_name "some_other_name"
54
+ assert_equal "some_other_name", AnotherDyna.snip_name
55
+ end
56
+ end
57
+
30
58
  context "when rendering usage" do
31
59
  class ::ShowUsage < Dynasnip
32
- usage "This is the usage"
33
60
  def handle
34
61
  usage
35
62
  end
36
63
  end
37
64
 
38
65
  should "show the usage defined in the snip" do
66
+ ShowUsage.usage "This is the usage"
67
+
39
68
  assert_equal "This is the usage", ShowUsage.new(app).handle
40
69
  end
70
+
71
+ should "automatically escape curly braces to prevent snip inclusion" do
72
+ ShowUsage.usage "like {this}"
73
+
74
+ assert_equal "like &#123;this&#125;", ShowUsage.new(app).handle
75
+ end
41
76
  end
42
77
  end
@@ -83,6 +83,10 @@ context "The SnipReference parser" do
83
83
  %|{s 'arg, comma'}| => {:snip => 's', :attribute => nil, :arguments => ['arg, comma']},
84
84
  # %|{s "arg { open"}| => {:snip => 's', :attribute => nil, :arguments => ['arg { open']},
85
85
  # %|{s "arg } close"}| => {:snip => 's', :attribute => nil, :arguments => ['arg } close']}
86
+ },
87
+
88
+ :html_quoting_arguments => {
89
+ %|{s &ldquo;arg&rdquo;}| => {:snip => 's', :attribute => nil, :arguments => ['arg']},
86
90
  }
87
91
  }
88
92
 
@@ -45,7 +45,7 @@ describe Vanilla::App do
45
45
 
46
46
  should "raise an error if the specified renderer doesn't exist" do
47
47
  snip = create_snip(:name => "blah", :render_as => "NonExistentClass")
48
- assert_raises(NameError) { app.renderer_for(snip) }
48
+ assert_raises(Vanilla::MissingRendererError) { app.renderer_for(snip) }
49
49
  end
50
50
  end
51
51
  end
@@ -1,12 +1,13 @@
1
1
  require "test_helper"
2
2
  require "atom"
3
+ require "base/feed"
3
4
 
4
5
  context "The feed dynasnip" do
5
- should "include snips" do
6
- app.soup.stubs(:all_snips).returns([
7
- snip(:name => "Hello", :content => "This is the content"),
8
- snip(:name => "Goodbye", :content => "More content")
9
- ])
6
+ should "include snips of the specified kind" do
7
+ stub_app_soup({:name => "Hello", :content => "This is the content", :kind => "blog"},
8
+ {:name => "Goodbye", :content => "More content", :kind => "blog"},
9
+ {:name => "system", :content => "not to be shown"},
10
+ Feed.snip_attributes)
10
11
 
11
12
  visit "/feed.xml"
12
13
 
@@ -15,33 +16,13 @@ context "The feed dynasnip" do
15
16
  end
16
17
 
17
18
  should "included rendered snip contents" do
18
- app.soup.stubs(:all_snips).returns([
19
- snip(:name => "Hello", :content => "This is *the* content", :render_as => "markdown")
20
- ])
19
+ stub_app_soup({:name => "Hello", :content => "This is *the* content",
20
+ :render_as => "markdown", :kind => "blog"},
21
+ Feed.snip_attributes)
21
22
 
22
23
  visit "/feed.xml"
23
24
 
24
25
  feed = Atom::Feed.load_feed(page.source)
25
26
  assert_equal "<p>This is <em>the</em> content</p>", feed.entries.first.content
26
27
  end
27
-
28
- should "ensure that all links are made absolute"
29
-
30
- context "each entry" do
31
- setup do
32
- end
33
- should "set the name to be the name of the snip"
34
- should "use the title of the snip as name if present"
35
- should "set the authors"
36
- should "include a link to the snip"
37
- should "set the ID"
38
- should "set the published date"
39
- should "set the updated at"
40
- end
41
-
42
- private
43
-
44
- def snip(attributes)
45
- Soup::Snip.new(attributes, nil)
46
- end
47
28
  end
@@ -26,6 +26,16 @@ context "The index dynasnip" do
26
26
  assert_equal %w(beta alpha gamma), links
27
27
  end
28
28
 
29
+ should "render links to snips with weird characters and spaces" do
30
+ app.soup.stubs(:all_snips).returns([
31
+ snip(:name => "fun fun", :updated_at => Time.at(20))
32
+ ])
33
+
34
+ visit "/index"
35
+ links = page.all("ol#index li a").map { |l| l.text }
36
+ assert_equal ["fun fun"], links
37
+ end
38
+
29
39
  private
30
40
 
31
41
  def snip(attributes)
@@ -0,0 +1,21 @@
1
+ require "test_helper"
2
+ require "timeout"
3
+
4
+ context "Every snip" do
5
+ should "return a 200 response code" do
6
+ snips = app.soup.all_snips
7
+ snips.each do |snip|
8
+ expected_code = 200
9
+ expected_code = 500 if %w(test bad_dynasnip).include?(snip.name)
10
+ begin
11
+ Timeout.timeout(1) do
12
+ get snip.name
13
+ assert_equal expected_code, last_response.status,
14
+ "#{snip.name} returned a #{last_response.status} response:\n#{last_response.body}"
15
+ end
16
+ rescue Timeout::Error
17
+ flunk "#{snip.name} timed out rendering"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -7,6 +7,7 @@ require 'capybara'
7
7
  require 'capybara/dsl'
8
8
 
9
9
  require File.expand_path("../../../pristine_app/application", __FILE__)
10
+ $LOAD_PATH.unshift File.expand_path("../../../pristine_app/soups", __FILE__)
10
11
 
11
12
  module TestHelper
12
13
  include Vanilla::TestHelper
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vanilla
3
3
  version: !ruby/object:Gem::Version
4
- hash: 81
4
+ hash: 87
5
5
  prerelease:
6
6
  segments:
7
7
  - 1
8
8
  - 17
9
- - 1
10
- version: 1.17.1
9
+ - 2
10
+ version: 1.17.2
11
11
  platform: ruby
12
12
  authors:
13
13
  - James Adam
@@ -15,10 +15,12 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-07-24 00:00:00 -05:00
18
+ date: 2011-11-01 00:00:00 +00:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
22
+ name: rack
23
+ prerelease: false
22
24
  version_requirements: &id001 !ruby/object:Gem::Requirement
23
25
  none: false
24
26
  requirements:
@@ -31,26 +33,26 @@ dependencies:
31
33
  - 1
32
34
  version: 0.9.1
33
35
  type: :runtime
34
- prerelease: false
35
36
  requirement: *id001
36
- name: rack
37
37
  - !ruby/object:Gem::Dependency
38
+ name: soup
39
+ prerelease: false
38
40
  version_requirements: &id002 !ruby/object:Gem::Requirement
39
41
  none: false
40
42
  requirements:
41
43
  - - ">="
42
44
  - !ruby/object:Gem::Version
43
- hash: 7
45
+ hash: 5
44
46
  segments:
45
47
  - 1
46
48
  - 0
47
- - 8
48
- version: 1.0.8
49
+ - 9
50
+ version: 1.0.9
49
51
  type: :runtime
50
- prerelease: false
51
52
  requirement: *id002
52
- name: soup
53
53
  - !ruby/object:Gem::Dependency
54
+ name: ratom
55
+ prerelease: false
54
56
  version_requirements: &id003 !ruby/object:Gem::Requirement
55
57
  none: false
56
58
  requirements:
@@ -63,10 +65,10 @@ dependencies:
63
65
  - 5
64
66
  version: 0.3.5
65
67
  type: :runtime
66
- prerelease: false
67
68
  requirement: *id003
68
- name: ratom
69
69
  - !ruby/object:Gem::Dependency
70
+ name: RedCloth
71
+ prerelease: false
70
72
  version_requirements: &id004 !ruby/object:Gem::Requirement
71
73
  none: false
72
74
  requirements:
@@ -79,10 +81,10 @@ dependencies:
79
81
  - 1
80
82
  version: 4.1.1
81
83
  type: :runtime
82
- prerelease: false
83
84
  requirement: *id004
84
- name: RedCloth
85
85
  - !ruby/object:Gem::Dependency
86
+ name: BlueCloth
87
+ prerelease: false
86
88
  version_requirements: &id005 !ruby/object:Gem::Requirement
87
89
  none: false
88
90
  requirements:
@@ -95,10 +97,10 @@ dependencies:
95
97
  - 0
96
98
  version: 1.0.0
97
99
  type: :runtime
98
- prerelease: false
99
100
  requirement: *id005
100
- name: BlueCloth
101
101
  - !ruby/object:Gem::Dependency
102
+ name: haml
103
+ prerelease: false
102
104
  version_requirements: &id006 !ruby/object:Gem::Requirement
103
105
  none: false
104
106
  requirements:
@@ -110,10 +112,10 @@ dependencies:
110
112
  - 1
111
113
  version: "3.1"
112
114
  type: :runtime
113
- prerelease: false
114
115
  requirement: *id006
115
- name: haml
116
116
  - !ruby/object:Gem::Dependency
117
+ name: parslet
118
+ prerelease: false
117
119
  version_requirements: &id007 !ruby/object:Gem::Requirement
118
120
  none: false
119
121
  requirements:
@@ -126,10 +128,10 @@ dependencies:
126
128
  - 0
127
129
  version: 1.2.0
128
130
  type: :runtime
129
- prerelease: false
130
131
  requirement: *id007
131
- name: parslet
132
132
  - !ruby/object:Gem::Dependency
133
+ name: rack-test
134
+ prerelease: false
133
135
  version_requirements: &id008 !ruby/object:Gem::Requirement
134
136
  none: false
135
137
  requirements:
@@ -142,26 +144,26 @@ dependencies:
142
144
  - 7
143
145
  version: 0.5.7
144
146
  type: :runtime
145
- prerelease: false
146
147
  requirement: *id008
147
- name: rack-test
148
148
  - !ruby/object:Gem::Dependency
149
+ name: kintama
150
+ prerelease: false
149
151
  version_requirements: &id009 !ruby/object:Gem::Requirement
150
152
  none: false
151
153
  requirements:
152
154
  - - ">="
153
155
  - !ruby/object:Gem::Version
154
- hash: 23
156
+ hash: 21
155
157
  segments:
156
158
  - 0
157
159
  - 1
158
- - 6
159
- version: 0.1.6
160
+ - 7
161
+ version: 0.1.7
160
162
  type: :development
161
- prerelease: false
162
163
  requirement: *id009
163
- name: kintama
164
164
  - !ruby/object:Gem::Dependency
165
+ name: mocha
166
+ prerelease: false
165
167
  version_requirements: &id010 !ruby/object:Gem::Requirement
166
168
  none: false
167
169
  requirements:
@@ -172,10 +174,10 @@ dependencies:
172
174
  - 0
173
175
  version: "0"
174
176
  type: :development
175
- prerelease: false
176
177
  requirement: *id010
177
- name: mocha
178
178
  - !ruby/object:Gem::Dependency
179
+ name: capybara
180
+ prerelease: false
179
181
  version_requirements: &id011 !ruby/object:Gem::Requirement
180
182
  none: false
181
183
  requirements:
@@ -186,10 +188,10 @@ dependencies:
186
188
  - 0
187
189
  version: "0"
188
190
  type: :development
189
- prerelease: false
190
191
  requirement: *id011
191
- name: capybara
192
192
  - !ruby/object:Gem::Dependency
193
+ name: launchy
194
+ prerelease: false
193
195
  version_requirements: &id012 !ruby/object:Gem::Requirement
194
196
  none: false
195
197
  requirements:
@@ -200,9 +202,7 @@ dependencies:
200
202
  - 0
201
203
  version: "0"
202
204
  type: :development
203
- prerelease: false
204
205
  requirement: *id012
205
- name: launchy
206
206
  description:
207
207
  email: james@lazyatom.com.com
208
208
  executables:
@@ -215,6 +215,7 @@ files:
215
215
  - Rakefile
216
216
  - README
217
217
  - .gemtest
218
+ - test/core/atom_feed_test.rb
218
219
  - test/core/configuration_test.rb
219
220
  - test/core/dynasnip_test.rb
220
221
  - test/core/renderers/base_renderer_test.rb
@@ -237,8 +238,10 @@ files:
237
238
  - test/pristine_app/link_to_test.rb
238
239
  - test/pristine_app/page_title_test.rb
239
240
  - test/pristine_app/raw_test.rb
241
+ - test/pristine_app/site_test.rb
240
242
  - test/pristine_app/test_helper.rb
241
243
  - lib/vanilla/app.rb
244
+ - lib/vanilla/atom_feed.rb
242
245
  - lib/vanilla/config.rb
243
246
  - lib/vanilla/console.rb
244
247
  - lib/vanilla/dynasnip.rb
@@ -261,9 +264,9 @@ files:
261
264
  - pristine_app/application.rb
262
265
  - pristine_app/config.ru
263
266
  - pristine_app/Gemfile
264
- - pristine_app/Gemfile.lock
265
267
  - pristine_app/public/vanilla.css
266
268
  - pristine_app/README
269
+ - pristine_app/soups/base/feed.rb
267
270
  - pristine_app/soups/base/layout.snip
268
271
  - pristine_app/soups/base/start.snip
269
272
  - pristine_app/soups/extras/comments.rb
@@ -272,7 +275,6 @@ files:
272
275
  - pristine_app/soups/extras/url_to.rb
273
276
  - pristine_app/soups/system/current_snip.rb
274
277
  - pristine_app/soups/system/debug.rb
275
- - pristine_app/soups/system/feed.rb
276
278
  - pristine_app/soups/system/index.rb
277
279
  - pristine_app/soups/system/link_to.rb
278
280
  - pristine_app/soups/system/link_to_current_snip.rb
@@ -296,7 +298,6 @@ files:
296
298
  - pristine_app/soups/tutorial/tutorial.snip.markdown
297
299
  - pristine_app/soups/tutorial/vanilla-rb.snip
298
300
  - pristine_app/soups/tutorial/vanilla.snip
299
- - pristine_app/tmp/restart.txt
300
301
  has_rdoc: true
301
302
  homepage: http://github.com/lazyatom/vanilla-rb
302
303
  licenses: []
@@ -328,7 +329,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
328
329
  requirements: []
329
330
 
330
331
  rubyforge_project: vanilla
331
- rubygems_version: 1.4.1
332
+ rubygems_version: 1.6.2
332
333
  signing_key:
333
334
  specification_version: 3
334
335
  summary: A bliki-type web content thing.
@@ -1,35 +0,0 @@
1
- PATH
2
- remote: /Users/james/Code/lazyatom/vanilla-rb
3
- specs:
4
- vanilla (1.17)
5
- BlueCloth (>= 1.0.0)
6
- RedCloth (>= 4.1.1)
7
- haml (>= 3.1)
8
- parslet (>= 1.2.0)
9
- rack (>= 0.9.1)
10
- rack-test (>= 0.5.7)
11
- ratom (>= 0.3.5)
12
- soup (>= 1.0.8)
13
-
14
- GEM
15
- remote: http://rubygems.org/
16
- specs:
17
- BlueCloth (1.0.1)
18
- RedCloth (4.2.7)
19
- blankslate (2.1.2.4)
20
- haml (3.1.1)
21
- libxml-ruby (2.0.5)
22
- parslet (1.2.0)
23
- blankslate (~> 2.0)
24
- rack (1.2.2)
25
- rack-test (0.5.7)
26
- rack (>= 1.0)
27
- ratom (0.6.8)
28
- libxml-ruby (>= 1.1.2)
29
- soup (1.0.8)
30
-
31
- PLATFORMS
32
- ruby
33
-
34
- DEPENDENCIES
35
- vanilla!
@@ -1,30 +0,0 @@
1
- require "vanilla/dynasnip"
2
-
3
- class Feed < Dynasnip
4
- def handle(*args)
5
- Atom::Feed.new do |f|
6
- f.title = "blah"
7
- f.updated = Time.now
8
- f.id = "tag:x,2008-06-01:kind/x"
9
- f.entries = entries
10
- end.to_xml
11
- end
12
-
13
- private
14
-
15
- def entries
16
- app.soup.all_snips.map do |snip|
17
- Atom::Entry.new do |e|
18
- # e.published = snip.created_at
19
- # e.updated = snip.updated_at || snip.created_at
20
- e.content = Atom::Content::Html.new(app.render(snip))
21
- # e.title = snip.name
22
- # e.authors = [Atom::Person.new(:name => snip.author || domain)]
23
- # e.links << Atom::Link.new(:href => "http://#{domain}#{url_to(snip.name)}")
24
- # e.id = "tag:#{domain},#{(snip.created_at.to_s || Date.today.to_s).split[0]}:/#{snip.name}"
25
- end
26
- end
27
- end
28
-
29
- self
30
- end
File without changes