eduardo-vanilla 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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