eduardo-vanilla 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/README +53 -0
  2. data/Rakefile +121 -0
  3. data/bin/vanilla +9 -0
  4. data/config.example.yml +5 -0
  5. data/config.ru +9 -0
  6. data/lib/defensio.rb +59 -0
  7. data/lib/tasks/vanilla.rake +178 -0
  8. data/lib/vanilla.rb +20 -0
  9. data/lib/vanilla/app.rb +87 -0
  10. data/lib/vanilla/console.rb +3 -0
  11. data/lib/vanilla/dynasnip.rb +110 -0
  12. data/lib/vanilla/dynasnips/code.rb +16 -0
  13. data/lib/vanilla/dynasnips/comments.rb +108 -0
  14. data/lib/vanilla/dynasnips/current_snip.rb +32 -0
  15. data/lib/vanilla/dynasnips/debug.rb +13 -0
  16. data/lib/vanilla/dynasnips/edit.rb +63 -0
  17. data/lib/vanilla/dynasnips/edit_link.rb +24 -0
  18. data/lib/vanilla/dynasnips/index.rb +11 -0
  19. data/lib/vanilla/dynasnips/kind.rb +70 -0
  20. data/lib/vanilla/dynasnips/link_to.rb +14 -0
  21. data/lib/vanilla/dynasnips/link_to_current_snip.rb +16 -0
  22. data/lib/vanilla/dynasnips/login.rb +56 -0
  23. data/lib/vanilla/dynasnips/new.rb +14 -0
  24. data/lib/vanilla/dynasnips/notes.rb +42 -0
  25. data/lib/vanilla/dynasnips/pre.rb +19 -0
  26. data/lib/vanilla/dynasnips/rand.rb +27 -0
  27. data/lib/vanilla/dynasnips/raw.rb +19 -0
  28. data/lib/vanilla/dynasnips/url_to.rb +7 -0
  29. data/lib/vanilla/renderers/base.rb +78 -0
  30. data/lib/vanilla/renderers/bold.rb +9 -0
  31. data/lib/vanilla/renderers/erb.rb +16 -0
  32. data/lib/vanilla/renderers/markdown.rb +13 -0
  33. data/lib/vanilla/renderers/raw.rb +9 -0
  34. data/lib/vanilla/renderers/ruby.rb +35 -0
  35. data/lib/vanilla/renderers/textile.rb +13 -0
  36. data/lib/vanilla/request.rb +68 -0
  37. data/lib/vanilla/routes.rb +29 -0
  38. data/lib/vanilla/snip_handling.rb +33 -0
  39. data/lib/vanilla/snips/start.rb +18 -0
  40. data/lib/vanilla/snips/system.rb +76 -0
  41. data/lib/vanilla/snips/tutorial.rb +158 -0
  42. data/lib/vanilla/test_snips.rb +85 -0
  43. data/spec/dynasnip_spec.rb +31 -0
  44. data/spec/renderers/base_renderer_spec.rb +40 -0
  45. data/spec/renderers/erb_renderer_spec.rb +27 -0
  46. data/spec/renderers/markdown_renderer_spec.rb +29 -0
  47. data/spec/renderers/raw_renderer_spec.rb +21 -0
  48. data/spec/renderers/ruby_renderer_spec.rb +42 -0
  49. data/spec/renderers/vanilla_app_detecting_renderer_spec.rb +35 -0
  50. data/spec/soup_test.db +0 -0
  51. data/spec/spec_helper.rb +64 -0
  52. data/spec/vanilla_app_spec.rb +38 -0
  53. data/spec/vanilla_presenting_spec.rb +84 -0
  54. data/spec/vanilla_request_spec.rb +73 -0
  55. data/spec/vanilla_snip_finding_spec.rb +28 -0
  56. metadata +173 -0
@@ -0,0 +1,87 @@
1
+ require 'vanilla'
2
+ require 'vanilla/request'
3
+
4
+ module Vanilla
5
+ class App
6
+
7
+ attr_reader :request, :response, :config
8
+
9
+ def initialize(config_file=nil)
10
+ prepare_configuration(config_file)
11
+ Soup.prepare
12
+ end
13
+
14
+ # Returns a Rack-appropriate 3-element array (via Rack::Response#finish)
15
+ def call(env)
16
+ @request = Vanilla::Request.new(env)
17
+ @response = Rack::Response.new
18
+
19
+ begin
20
+ output = formatted_render(request.snip, request.part, request.format)
21
+ rescue => e
22
+ @response.status = 500
23
+ output = e.to_s
24
+ end
25
+ response_format = request.format
26
+ response_format = 'plain' if response_format == 'raw'
27
+ @response['Content-Type'] = "text/#{response_format}"
28
+ @response.write(output)
29
+ @response.finish # returns the array
30
+ end
31
+
32
+ def formatted_render(snip, part=nil, format=nil)
33
+ case format
34
+ when 'html', nil
35
+ Renderers::Erb.new(self).render(Vanilla.snip('system'), :main_template)
36
+ when 'raw', 'css', 'js'
37
+ Renderers::Raw.new(self).render(snip, part || :content)
38
+ when 'text', 'atom', 'xml'
39
+ render(snip, part || :content)
40
+ else
41
+ raise "Unknown format '#{format}'"
42
+ end
43
+ end
44
+
45
+ # render a snip using either the renderer given, or the renderer
46
+ # specified by the snip's "render_as" property, or Render::Base
47
+ # if nothing else is given.
48
+ def render(snip, part=:content, args=[])
49
+ rendering(snip) do |renderer|
50
+ renderer.render(snip, part, args)
51
+ end
52
+ end
53
+
54
+ # Given the snip and parameters, yield an instance of the appropriate
55
+ # Vanilla::Render::Base subclass
56
+ def rendering(snip)
57
+ renderer_instance = renderer_for(snip).new(self)
58
+ yield renderer_instance
59
+ rescue Exception => e
60
+ "<pre>[Error rendering '#{snip.name}' - \"" +
61
+ e.message.gsub("<", "&lt;").gsub(">", "&gt;") + "\"]\n" +
62
+ e.backtrace.join("\n").gsub("<", "&lt;").gsub(">", "&gt;") + "</pre>"
63
+ end
64
+
65
+ # Returns the renderer class for a given snip
66
+ def renderer_for(snip)
67
+ return Renderers::Base unless snip.render_as && !snip.render_as.empty?
68
+ Vanilla::Renderers.const_get(snip.render_as)
69
+ end
70
+
71
+ # Other things can call this when a snip cannot be loaded.
72
+ def render_missing_snip(snip_name)
73
+ "[snip '#{snip_name}' cannot be found]"
74
+ end
75
+
76
+ private
77
+
78
+ def prepare_configuration(config_file)
79
+ config_file ||= "config.yml"
80
+ @config = YAML.load(File.open(config_file)) rescue {}
81
+ @config[:filename] = config_file
82
+ def @config.save!
83
+ File.open(self[:filename], 'w') { |f| f.puts self.to_yaml }
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,3 @@
1
+ require 'vanilla'
2
+ Soup.prepare
3
+ puts "The Soup is simmering."
@@ -0,0 +1,110 @@
1
+ require 'vanilla/renderers/base'
2
+ require 'enumerator'
3
+
4
+ class Dynasnip < Vanilla::Renderers::Base
5
+
6
+ def self.all
7
+ ObjectSpace.enum_for(:each_object, class << self; self; end).to_a - [self]
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
+
22
+ def self.attribute(attribute_name, attribute_value=nil)
23
+ @attributes ||= {}
24
+ @attributes[attribute_name.to_sym] = attribute_value if attribute_value
25
+ @attributes[attribute_name.to_sym]
26
+ end
27
+
28
+ def self.usage(str)
29
+ attribute :usage, escape_curly_braces(str).strip
30
+ end
31
+
32
+ def self.persist_all!(overwrite=false)
33
+ all.each do |dynasnip|
34
+ dynasnip.persist!(overwrite)
35
+ end
36
+ end
37
+
38
+ def self.build_snip
39
+ Snip.new(snip_attributes)
40
+ end
41
+
42
+ def self.snip_attributes
43
+ full_snip_attributes = {:name => snip_name, :content => self.name, :render_as => "Ruby"}
44
+ @attributes ? full_snip_attributes.merge!(@attributes) : full_snip_attributes
45
+ end
46
+
47
+ def self.persist!(overwrite=false)
48
+ if overwrite
49
+ snip = Soup[snip_name]
50
+ if snip
51
+ if snip.is_a?(Array)
52
+ snip.each { |s| s.destroy }
53
+ else
54
+ snip.destroy
55
+ end
56
+ end
57
+ end
58
+ snip = Soup[snip_name]
59
+ snip = snip[0] if snip.is_a?(Array)
60
+ if snip
61
+ snip_attributes.each do |name, value|
62
+ snip.set_value(name, value)
63
+ end
64
+ else
65
+ snip = build_snip
66
+ end
67
+ snip.save
68
+ snip
69
+ end
70
+
71
+ def method_missing(method, *args)
72
+ if snip = Vanilla.snip(snip_name)
73
+ snip.get_value(method)
74
+ elsif part = self.class.attribute(method)
75
+ part
76
+ else
77
+ super
78
+ end
79
+ end
80
+
81
+ # dynasnips gain access to the app in the same way as Render::Base
82
+ # subclasses
83
+
84
+ protected
85
+
86
+ def snip_name
87
+ self.class.snip_name
88
+ end
89
+
90
+ def snip
91
+ Snip[snip_name]
92
+ end
93
+
94
+ def show_usage
95
+ if snip.usage
96
+ Vanilla::Renderers::Markdown.render(snip_name, :usage)
97
+ else
98
+ "No usage information for #{snip_name}"
99
+ end
100
+ end
101
+
102
+ def cleaned_params
103
+ p = app.request.params.dup
104
+ p.delete(:snip)
105
+ p.delete(:format)
106
+ p.delete(:method)
107
+ p.delete(:part)
108
+ p
109
+ end
110
+ end
@@ -0,0 +1,16 @@
1
+ require 'vanilla/dynasnip'
2
+ require 'syntax/convertors/html'
3
+
4
+ class CodeHighlighter < Dynasnip
5
+ snip_name "code"
6
+
7
+ def handle(language, snip_to_render, part_to_render='content')
8
+ snip = Vanilla.snip(snip_to_render)
9
+ text = snip.__send__(part_to_render.to_sym)
10
+ convertor = Syntax::Convertors::HTML.for_syntax(language)
11
+ code = convertor.convert(text, false)
12
+ %(<span class="code ) + language + %("><code>) + code + %(</code></span>)
13
+ end
14
+
15
+ self
16
+ end
@@ -0,0 +1,108 @@
1
+ require 'vanilla/dynasnip'
2
+ require 'defensio'
3
+ require 'date'
4
+
5
+ class Comments < Dynasnip
6
+ usage %|
7
+ Embed comments within snips!
8
+
9
+ {comments <snip-name>}
10
+
11
+ This will embed a list of comments, and a comment form, in a snip
12
+ If the snip is being rendered within another snip, it will show a link to the snip,
13
+ with the number of comments.
14
+ |
15
+
16
+ def get(snip_name=nil, disable_new_comments=false)
17
+ snip_name = snip_name || app.request.params[:snip]
18
+ return usage if self.class.snip_name == snip_name
19
+ comments = Soup.sieve(:commenting_on => snip_name)
20
+ comments_html = if app.request.snip_name == snip_name
21
+ rendered_comments = render_comments(comments)
22
+ rendered_comments += comment_form.gsub('SNIP_NAME', snip_name) unless disable_new_comments
23
+ rendered_comments
24
+ else
25
+ %{<a href="#{Vanilla::Routes.url_to(snip_name)}">#{comments.length} comments for #{snip_name}</a>}
26
+ end
27
+ return comments_html
28
+ end
29
+
30
+ def post(*args)
31
+ snip_name = app.request.params[:snip]
32
+ existing_comments = Soup.sieve(:commenting_on => snip_name)
33
+ comment = app.request.params.reject { |k,v| ![:author, :email, :website, :content].include?(k) }
34
+
35
+ return "You need to add some details!" if comment.empty?
36
+
37
+ comment = check_for_spam(comment)
38
+
39
+ if comment[:spam]
40
+ "Sorry - your comment looks like spam, according to Defensio :("
41
+ else
42
+ return "No spam today, thanks anyway" unless app.request.params[:human] == 'human'
43
+ Soup << comment.merge({
44
+ :name => "#{snip_name}-comment-#{existing_comments.length + 1}",
45
+ :commenting_on => snip_name,
46
+ :created_at => Time.now
47
+ })
48
+ "Thanks for your comment! Back to {link_to #{snip_name}}"
49
+ end
50
+ end
51
+
52
+ def render_comments(comments)
53
+ "<h2>Comments</h2><ol class='comments'>" + comments.map do |comment|
54
+ rendered_comment = comment_template.gsub('COMMENT_CONTENT', app.render(comment)).
55
+ gsub('COMMENT_DATE', comment.created_at)
56
+ author = comment.author
57
+ author = "Anonymous" unless author && author != ""
58
+ if comment.website && comment.website != ""
59
+ rendered_comment.gsub!('COMMENT_AUTHOR', "<a href=\"#{comment.website}\">#{author}</a>")
60
+ else
61
+ rendered_comment.gsub!('COMMENT_AUTHOR', author)
62
+ end
63
+ rendered_comment
64
+ end.join + "</ol>"
65
+ end
66
+
67
+ def check_for_spam(comment)
68
+ snip_date = Date.parse(Soup[app.request.params[:snip]].updated_at)
69
+ Defensio.configure(app.config[:defensio])
70
+ defensio_params = {
71
+ :comment_author_email => comment[:email],
72
+ :comment_author => comment[:author],
73
+ :comment_author_url => comment[:website],
74
+ :comment_content => comment[:content],
75
+ :comment_type => "comment",
76
+ :user_ip => app.request.ip,
77
+ :article_date => snip_date.strftime("%Y/%m/%d")
78
+ }
79
+ audit = Defensio.audit_comment(defensio_params)
80
+
81
+ # Augment the comment hash
82
+ comment[:user_ip] = app.request.ip
83
+ comment[:spamminess] = audit["defensio_result"]["spaminess"]
84
+ comment[:spam] = audit["defensio_result"]["spam"]
85
+ comment[:defensio_signature] = audit["defensio_result"]["signature"]
86
+ comment[:defensio_message] = audit["defensio_result"]["message"] if audit["defensio_result"]["message"]
87
+ comment[:defensio_status] = audit["defensio_result"]["status"]
88
+ comment
89
+ end
90
+
91
+ attribute :comment_template, %{
92
+ <li>
93
+ <p>COMMENT_AUTHOR (COMMENT_DATE)</p>
94
+ <div>COMMENT_CONTENT</div>
95
+ </li>
96
+ }
97
+
98
+ attribute :comment_form, %{
99
+ <form class="comments" action="/#{snip_name}?snip=SNIP_NAME" method="POST">
100
+ <label>Name: <input type="text" name="author"></input></label>
101
+ <label>Email: <input type="text" name="email"></input></label>
102
+ <label>Website: <input type="text" name="website"></input></label>
103
+ <textarea name="content"></textarea>
104
+ <label class="human">Type 'human' if you are one: <input type="text" name="human"></input></label>
105
+ <button>Submit</button>
106
+ </form>
107
+ }
108
+ end
@@ -0,0 +1,32 @@
1
+ require 'vanilla/dynasnip'
2
+
3
+ class CurrentSnip < Dynasnip
4
+ usage %|
5
+ The current_snip dyna normally returns the result of rendering the snip named by the
6
+ 'snip' value in the parameters. This way, it can be used in templates to place the currently
7
+ requested snip, in its rendered form, within the page.
8
+
9
+ It can also be used to determine the name of the current snip in a consistent way:
10
+
11
+ {current_snip name}
12
+
13
+ will output the name of the current snip, or the name of the snip currently being edited.
14
+ |
15
+
16
+ def handle(*args)
17
+ if args[0] == 'name'
18
+ if app.request.snip_name == 'edit' # we're editing so don't use this name
19
+ app.request.params[:snip_to_edit]
20
+ else
21
+ app.request.snip_name
22
+ end
23
+ else
24
+ if app.request.snip
25
+ app.render(app.request.snip, app.request.part)
26
+ else
27
+ app.response.status = 404
28
+ "Couldn't find snip {link_to #{app.request.snip_name}}"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,13 @@
1
+ require 'vanilla/dynasnip'
2
+
3
+ # If the dynasnip is a subclass of Dynasnip, it has access to the request hash
4
+ # (or whatever - access to some object outside of the snip itself.)
5
+ class Debug < Dynasnip
6
+ def get(*args)
7
+ app.request.inspect
8
+ end
9
+
10
+ def post(*args)
11
+ "You posted! " + app.request.inspect
12
+ end
13
+ end
@@ -0,0 +1,63 @@
1
+ require 'vanilla/dynasnip'
2
+ require 'vanilla/dynasnips/login'
3
+
4
+ # The edit dyna will load the snip given in the 'snip_to_edit' part of the
5
+ # params
6
+ class EditSnip < Dynasnip
7
+ include Login::Helper
8
+
9
+ snip_name "edit"
10
+
11
+ def get(snip_name=nil)
12
+ return login_required unless logged_in?
13
+ snip = Vanilla.snip(snip_name || app.request.params[:name])
14
+ edit(snip)
15
+ end
16
+
17
+ def post(*args)
18
+ return login_required unless logged_in?
19
+ snip_attributes = cleaned_params
20
+ snip_attributes.delete(:save_button)
21
+ return 'no params' if snip_attributes.empty?
22
+ snip = Vanilla.snip(snip_attributes[:name])
23
+ snip_attributes[:updated_at] = Time.now
24
+ snip_attributes.each do |name, value|
25
+ snip.__send__(:set_value, name, value)
26
+ end
27
+ snip.save
28
+ %{Saved snip #{Vanilla::Routes.link_to snip_attributes[:name]} ok}
29
+ rescue Exception => e
30
+ snip_attributes[:created_at] ||= Time.now
31
+ Soup << snip_attributes
32
+ %{Created snip #{Vanilla::Routes.link_to snip_attributes[:name]} ok}
33
+ end
34
+
35
+ def edit(snip)
36
+ renderer = Vanilla::Renderers::Erb.new(app)
37
+ renderer.instance_eval { @snip_to_edit = snip } # hacky!
38
+ snip_in_edit_template = renderer.render_without_including_snips(Vanilla.snip('edit'), :template)
39
+ prevent_snip_inclusion(snip_in_edit_template)
40
+ end
41
+
42
+ private
43
+
44
+ def prevent_snip_inclusion(content)
45
+ content.gsub("{", "&#123;").gsub("}" ,"&#125;")
46
+ end
47
+
48
+ attribute :template, %{
49
+ <form action="<%= Vanilla::Routes.url_to 'edit' %>" method="post">
50
+ <dl class="attributes">
51
+ <% @snip_to_edit.attributes.each do |name, value| %>
52
+ <dt><%= name %></dt>
53
+ <% num_rows = (value || "").split("\n").length + 1 %>
54
+ <dd><textarea name="<%= name %>" class="<%= name %>" rows="<%= num_rows %>"><%=h value %></textarea></dd>
55
+ <% end %>
56
+ <dt><input class="attribute_name" type="text"></input></dt>
57
+ <dd><textarea></textarea></dd>
58
+ </dl>
59
+ <a href="#" id="add">Add</a>
60
+ <button name='save_button'>Save</button>
61
+ </form>
62
+ }
63
+ end
@@ -0,0 +1,24 @@
1
+ require 'vanilla/dynasnip'
2
+
3
+ class EditLink < Dynasnip
4
+ usage %|
5
+ You can use the edit_link snip to insert links for editing other snips. For example:
6
+
7
+ &#123;edit_link blah&#125;
8
+
9
+ would insert a link to an editor for the blah snip.
10
+
11
+ You can also give a custom piece of text for the link, like this:
12
+
13
+ &#123;edit_link blah,link-name&#125;|
14
+
15
+ def handle(*args)
16
+ if args.length < 2
17
+ show_usage
18
+ else
19
+ snip_name = args[0]
20
+ link_text = args[1]
21
+ Vanilla::Routes.edit_link(snip_name, link_text)
22
+ end
23
+ end
24
+ end