vanilla 1.2 → 1.9.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (106) hide show
  1. data/Rakefile +61 -60
  2. data/bin/vanilla +6 -35
  3. data/config.example.yml +6 -0
  4. data/config.ru +10 -0
  5. data/lib/defensio.rb +59 -0
  6. data/lib/tasks/vanilla.rake +173 -0
  7. data/lib/vanilla.rb +3 -10
  8. data/lib/vanilla/app.rb +48 -104
  9. data/lib/vanilla/console.rb +5 -19
  10. data/lib/vanilla/dynasnips/comments.rb +108 -0
  11. data/lib/vanilla/dynasnips/current_snip.rb +32 -0
  12. data/{pristine_app/soups → lib/vanilla}/dynasnips/debug.rb +3 -5
  13. data/lib/vanilla/dynasnips/edit.rb +60 -0
  14. data/lib/vanilla/dynasnips/edit_link.rb +20 -0
  15. data/{pristine_app/soups → lib/vanilla}/dynasnips/index.rb +2 -4
  16. data/{pristine_app/soups/extras → lib/vanilla/dynasnips}/kind.rb +12 -14
  17. data/{pristine_app/soups → lib/vanilla}/dynasnips/link_to.rb +0 -2
  18. data/lib/vanilla/dynasnips/link_to_current_snip.rb +16 -0
  19. data/lib/vanilla/dynasnips/login.rb +56 -0
  20. data/lib/vanilla/dynasnips/new.rb +14 -0
  21. data/lib/vanilla/dynasnips/notes.rb +42 -0
  22. data/{pristine_app/soups → lib/vanilla}/dynasnips/pre.rb +4 -6
  23. data/{pristine_app/soups/extras → lib/vanilla/dynasnips}/rand.rb +0 -2
  24. data/{pristine_app/soups → lib/vanilla}/dynasnips/raw.rb +5 -8
  25. data/{pristine_app/soups/extras → lib/vanilla/dynasnips}/url_to.rb +0 -0
  26. data/lib/vanilla/renderers/base.rb +22 -32
  27. data/lib/vanilla/renderers/bold.rb +2 -0
  28. data/lib/vanilla/renderers/erb.rb +2 -0
  29. data/lib/vanilla/renderers/markdown.rb +2 -0
  30. data/lib/vanilla/renderers/raw.rb +2 -0
  31. data/lib/vanilla/renderers/ruby.rb +5 -9
  32. data/lib/vanilla/renderers/textile.rb +2 -0
  33. data/lib/vanilla/request.rb +15 -16
  34. data/lib/vanilla/routes.rb +18 -5
  35. data/lib/vanilla/snip_reference.rb +534 -0
  36. data/lib/vanilla/snip_reference.treetop +48 -0
  37. data/lib/vanilla/snip_reference_parser.rb +99 -82
  38. data/lib/vanilla/snips/start.rb +28 -0
  39. data/lib/vanilla/snips/system.rb +77 -0
  40. data/lib/vanilla/snips/tutorial.rb +244 -0
  41. data/lib/vanilla/soup_with_timestamps.rb +21 -0
  42. data/public/hatch.png +0 -0
  43. data/public/javascripts/jquery.autogrow-textarea.js +54 -0
  44. data/public/javascripts/jquery.js +4376 -0
  45. data/public/javascripts/vanilla.js +22 -0
  46. data/spec/dynasnip_spec.rb +28 -0
  47. data/spec/renderers/base_renderer_spec.rb +40 -0
  48. data/spec/renderers/erb_renderer_spec.rb +27 -0
  49. data/spec/renderers/markdown_renderer_spec.rb +29 -0
  50. data/spec/renderers/raw_renderer_spec.rb +21 -0
  51. data/spec/renderers/ruby_renderer_spec.rb +59 -0
  52. data/spec/renderers/vanilla_app_detecting_renderer_spec.rb +35 -0
  53. data/spec/spec_helper.rb +70 -0
  54. data/spec/tmp/config.yml +2 -0
  55. data/spec/tmp/soup/current_snip.yml +15 -0
  56. data/spec/tmp/soup/system.yml +5 -0
  57. data/spec/vanilla_app_spec.rb +38 -0
  58. data/spec/vanilla_presenting_spec.rb +84 -0
  59. data/spec/vanilla_request_spec.rb +73 -0
  60. metadata +79 -170
  61. data/lib/vanilla/renderers.rb +0 -12
  62. data/lib/vanilla/renderers/haml.rb +0 -13
  63. data/lib/vanilla/static.rb +0 -28
  64. data/pristine_app/Gemfile +0 -3
  65. data/pristine_app/Gemfile.lock +0 -32
  66. data/pristine_app/README +0 -47
  67. data/pristine_app/config.ru +0 -26
  68. data/pristine_app/public/vanilla.css +0 -15
  69. data/pristine_app/soups/base/layout.snip +0 -18
  70. data/pristine_app/soups/base/start.snip +0 -19
  71. data/pristine_app/soups/dynasnips/current_snip.rb +0 -29
  72. data/pristine_app/soups/dynasnips/link_to_current_snip.rb +0 -14
  73. data/pristine_app/soups/dynasnips/page_title.rb +0 -9
  74. data/pristine_app/soups/extras/comments.rb +0 -78
  75. data/pristine_app/soups/tutorial/bad_dynasnip.snip +0 -8
  76. data/pristine_app/soups/tutorial/hello_world.snip +0 -20
  77. data/pristine_app/soups/tutorial/markdown_example.snip +0 -13
  78. data/pristine_app/soups/tutorial/snip.snip +0 -9
  79. data/pristine_app/soups/tutorial/soup.snip +0 -3
  80. data/pristine_app/soups/tutorial/test.snip +0 -30
  81. data/pristine_app/soups/tutorial/textile_example.snip +0 -11
  82. data/pristine_app/soups/tutorial/tutorial-another-snip.snip +0 -1
  83. data/pristine_app/soups/tutorial/tutorial-basic-snip-inclusion.snip +0 -1
  84. data/pristine_app/soups/tutorial/tutorial-dynasnips.snip.markdown +0 -56
  85. data/pristine_app/soups/tutorial/tutorial-layout.snip +0 -56
  86. data/pristine_app/soups/tutorial/tutorial-links.snip +0 -4
  87. data/pristine_app/soups/tutorial/tutorial-renderers.snip.markdown +0 -77
  88. data/pristine_app/soups/tutorial/tutorial.snip.markdown +0 -69
  89. data/pristine_app/soups/tutorial/vanilla-rb.snip +0 -16
  90. data/pristine_app/soups/tutorial/vanilla.snip +0 -8
  91. data/test/dynasnip_test.rb +0 -42
  92. data/test/dynasnips/link_to_current_snip_test.rb +0 -19
  93. data/test/dynasnips/link_to_test.rb +0 -27
  94. data/test/dynasnips/page_title_test.rb +0 -19
  95. data/test/renderers/base_renderer_test.rb +0 -43
  96. data/test/renderers/erb_renderer_test.rb +0 -29
  97. data/test/renderers/haml_renderer_test.rb +0 -35
  98. data/test/renderers/markdown_renderer_test.rb +0 -31
  99. data/test/renderers/raw_renderer_test.rb +0 -23
  100. data/test/renderers/ruby_renderer_test.rb +0 -59
  101. data/test/snip_inclusion_test.rb +0 -56
  102. data/test/snip_reference_parser_test.rb +0 -123
  103. data/test/test_helper.rb +0 -75
  104. data/test/vanilla_app_test.rb +0 -83
  105. data/test/vanilla_presenting_test.rb +0 -125
  106. data/test/vanilla_request_test.rb +0 -87
data/lib/vanilla.rb CHANGED
@@ -1,14 +1,7 @@
1
- module Vanilla
2
- VERSION = "1.2"
1
+ require 'vanilla/app'
3
2
 
4
- autoload :Renderers, "vanilla/renderers"
5
- autoload :App, "vanilla/app"
6
- autoload :Dynasnip, "vanilla/dynasnip"
7
- autoload :Request, "vanilla/request"
8
- autoload :Routes, "vanilla/routes"
9
- autoload :Static, "vanilla/static"
10
- autoload :SnipReferenceParser, "vanilla/snip_reference_parser"
11
- end
3
+ # Load all the other renderer subclasses
4
+ Dir[File.join(File.dirname(__FILE__), 'vanilla', 'renderers', '*.rb')].each { |f| require f }
12
5
 
13
6
  # Load all the base dynasnip classes
14
7
  Dir[File.join(File.dirname(__FILE__), 'vanilla', 'dynasnips', '*.rb')].each do |dynasnip|
data/lib/vanilla/app.rb CHANGED
@@ -1,35 +1,26 @@
1
- require 'soup'
1
+ require 'vanilla/request'
2
+ require 'vanilla/routes'
3
+ require 'vanilla/soup_with_timestamps'
4
+
5
+ # Require the base set of renderers
6
+ require 'vanilla/renderers/base'
7
+ require 'vanilla/renderers/raw'
8
+ require 'vanilla/renderers/erb'
9
+
2
10
 
3
11
  module Vanilla
4
12
  class App
5
- include Vanilla::Routes
6
-
13
+ include Routes
14
+
7
15
  attr_reader :request, :response, :config, :soup
8
-
9
- # Create a new Vanilla application
10
- # Configuration options:
11
- #
12
- # :soup - provide the path to the soup data
13
- # :soups - provide an array of paths to soup data
14
- # :renderers - a hash of names to classes
15
- # :default_renderer - the class to use when no renderer is provided;
16
- # defaults to 'Vanilla::Renderers::Base'
17
- # :default_layout_snip - the snip to use as a layout when rendering to HTML;
18
- # defaults to 'layout'
19
- # :root_snip - the snip to load for the root ('/') url;
20
- # defaults to 'start'
21
- def initialize(config={})
22
- @config = config
23
- if @config[:soup].nil? && @config[:soups].nil?
24
- @config.merge!(:soup => File.expand_path("soup"))
25
- end
26
- @soup = prepare_soup(config)
27
- prepare_renderers(config[:renderers])
16
+
17
+ def initialize(config_file=nil)
18
+ prepare_configuration(config_file)
19
+ @soup = SoupWithTimestamps.new(config[:soup])
28
20
  end
29
-
21
+
30
22
  # Returns a Rack-appropriate 3-element array (via Rack::Response#finish)
31
23
  def call(env)
32
- env['vanilla.app'] = self
33
24
  @request = Vanilla::Request.new(env, self)
34
25
  @response = Rack::Response.new
35
26
 
@@ -37,9 +28,9 @@ module Vanilla
37
28
  output = formatted_render(request.snip, request.part, request.format)
38
29
  rescue => e
39
30
  @response.status = 500
40
- output = e.to_s + e.backtrace.join("\n")
31
+ output = e.to_s
41
32
  end
42
- response_format = request.format
33
+ response_format = request.format
43
34
  response_format = 'plain' if response_format == 'raw'
44
35
  @response['Content-Type'] = "text/#{response_format}"
45
36
  @response.write(output)
@@ -49,16 +40,11 @@ module Vanilla
49
40
  def formatted_render(snip, part=nil, format=nil)
50
41
  case format
51
42
  when 'html', nil
52
- layout = layout_for(snip)
53
- if layout == snip
54
- "Rendering of the current layout would result in infinite recursion."
55
- else
56
- render(layout)
57
- end
43
+ Renderers::Erb.new(self).render(soup['system'], :main_template)
58
44
  when 'raw', 'css', 'js'
59
- Renderers::Raw.new(self).render(snip, part)
45
+ Renderers::Raw.new(self).render(snip, part || :content)
60
46
  when 'text', 'atom', 'xml'
61
- render(snip, part)
47
+ render(snip, part || :content)
62
48
  else
63
49
  raise "Unknown format '#{format}'"
64
50
  end
@@ -73,82 +59,40 @@ module Vanilla
73
59
  end
74
60
  end
75
61
 
62
+ # Given the snip and parameters, yield an instance of the appropriate
63
+ # Vanilla::Render::Base subclass
64
+ def rendering(snip)
65
+ renderer_instance = renderer_for(snip).new(self)
66
+ yield renderer_instance
67
+ rescue Exception => e
68
+ "<pre>[Error rendering '#{snip.name}' - \"" +
69
+ e.message.gsub("<", "&lt;").gsub(">", "&gt;") + "\"]\n" +
70
+ e.backtrace.join("\n").gsub("<", "&lt;").gsub(">", "&gt;") + "</pre>"
71
+ end
72
+
76
73
  # Returns the renderer class for a given snip
77
74
  def renderer_for(snip)
78
- if snip
79
- find_renderer(snip.render_as || snip.extension)
80
- else
81
- default_renderer
82
- end
83
- end
84
-
85
- def default_layout_snip
86
- soup[config[:default_layout_snip] || 'layout']
75
+ return Renderers::Base unless snip.render_as && !snip.render_as.empty?
76
+ Vanilla::Renderers.const_get(snip.render_as)
87
77
  end
88
-
89
- def layout_for(snip)
90
- if snip
91
- renderer_for(snip).new(self).layout_for(snip)
92
- else
93
- default_layout_snip
94
- end
78
+
79
+ # Other things can call this when a snip cannot be loaded.
80
+ def render_missing_snip(snip_name)
81
+ "[snip '#{snip_name}' cannot be found]"
95
82
  end
96
-
83
+
97
84
  def snip(attributes)
98
- @soup << attributes
85
+ @soup.new_snip(attributes)
99
86
  end
100
-
101
- def register_renderer(klass, *types)
102
- types.each do |type|
103
- if klass.is_a?(String)
104
- klass = klass.split("::").inject(Object) { |o, name| o.const_get(name) }
105
- end
106
- @renderers[type.to_s] = klass
107
- end
108
- end
109
-
87
+
110
88
  private
111
-
112
- def prepare_renderers(additional_renderers={})
113
- @renderers = Hash.new(config[:default_renderer] || Vanilla::Renderers::Base)
114
- @renderers.merge!({
115
- "base" => Vanilla::Renderers::Base,
116
- "markdown" => Vanilla::Renderers::Markdown,
117
- "bold" => Vanilla::Renderers::Bold,
118
- "erb" => Vanilla::Renderers::Erb,
119
- "rb" => Vanilla::Renderers::Ruby,
120
- "ruby" => Vanilla::Renderers::Ruby,
121
- "haml" => Vanilla::Renderers::Haml,
122
- "raw" => Vanilla::Renderers::Raw,
123
- "textile" => Vanilla::Renderers::Textile
124
- })
125
- additional_renderers.each { |name, klass| register_renderer(klass, name) } if additional_renderers
126
- end
127
-
128
- def find_renderer(name)
129
- @renderers[(name ? name.downcase : nil)]
130
- end
131
-
132
- def default_renderer
133
- @renderers[nil]
134
- end
135
-
136
- def rendering(snip)
137
- renderer_instance = renderer_for(snip).new(self)
138
- yield renderer_instance
139
- rescue Exception => e
140
- snip_name = snip ? snip.name : nil
141
- "<pre>[Error rendering '#{snip_name}' - \"" +
142
- e.message.gsub("<", "&lt;").gsub(">", "&gt;") + "\"]\n" +
143
- e.backtrace.join("\n").gsub("<", "&lt;").gsub(">", "&gt;") + "</pre>"
144
- end
145
-
146
- def prepare_soup(config)
147
- if config[:soups]
148
- backends = [config[:soups]].flatten.map { |path| ::Soup::Backends::FileBackend.new(path) }
149
- ::Soup.new(::Soup::Backends::MultiSoup.new(*backends))
150
- else
151
- ::Soup.new(::Soup::Backends::FileBackend.new(config[:soup]))
89
+
90
+ def prepare_configuration(config_file)
91
+ config_file ||= "config.yml"
92
+ @config = YAML.load(File.open(config_file)) rescue {}
93
+ @config[:filename] = config_file
94
+ def @config.save!
95
+ File.open(self[:filename], 'w') { |f| f.puts self.to_yaml }
152
96
  end
153
97
  end
154
98
  end
@@ -1,23 +1,9 @@
1
1
  require 'vanilla'
2
+ require 'irb'
2
3
 
3
- module Vanilla
4
- class RackShim
5
- def run(app)
6
- app # return it
7
- end
8
- def use(*args)
9
- # ignore
10
- end
11
- def get_binding
12
- binding
13
- end
14
- end
4
+ def app(reload=false)
5
+ @__vanilla_console_app = nil if reload
6
+ @__vanilla_console_app ||= Vanilla::App.new(ENV['VANILLA_CONFIG'])
15
7
  end
16
8
 
17
- def app(reload=false)
18
- if !@__vanilla_console_app || reload
19
- shim_binding = Vanilla::RackShim.new.get_binding
20
- @__vanilla_console_app = eval File.read("config.ru"), shim_binding
21
- end
22
- @__vanilla_console_app
23
- end
9
+ puts "The Soup is simmering."
@@ -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 = app.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="#{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 = app.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
+ app.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(app.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
@@ -1,15 +1,13 @@
1
1
  require 'vanilla/dynasnip'
2
- require 'cgi'
3
2
 
4
3
  # If the dynasnip is a subclass of Dynasnip, it has access to the request hash
5
4
  # (or whatever - access to some object outside of the snip itself.)
6
5
  class Debug < Dynasnip
7
6
  def get(*args)
8
- CGI.escapeHTML(app.request.inspect)
7
+ app.request.inspect
9
8
  end
10
-
9
+
11
10
  def post(*args)
12
- "You posted! " + CGI.escapeHTML(app.request.inspect)
11
+ "You posted! " + app.request.inspect
13
12
  end
14
- self
15
13
  end
@@ -0,0 +1,60 @@
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 = app.soup[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 = app.soup[snip_attributes[:name]]
23
+ snip_attributes.each do |name, value|
24
+ snip.__send__(:set_value, name, value)
25
+ end
26
+ snip.save
27
+ %{Saved snip #{link_to snip_attributes[:name]} ok}
28
+ rescue Exception => e
29
+ app.soup << snip_attributes
30
+ %{Created snip #{link_to snip_attributes[:name]} ok}
31
+ end
32
+
33
+ def edit(snip)
34
+ renderer = Vanilla::Renderers::Erb.new(app)
35
+ renderer.instance_eval { @snip_to_edit = snip } # hacky!
36
+ snip_in_edit_template = renderer.render_without_including_snips(app.soup['edit'], :template)
37
+ prevent_snip_inclusion(snip_in_edit_template)
38
+ end
39
+
40
+ private
41
+
42
+ def prevent_snip_inclusion(content)
43
+ content.gsub("{", "&#123;").gsub("}" ,"&#125;")
44
+ end
45
+
46
+ attribute :template, %{
47
+ <form action="<%= url_to 'edit' %>" method="post">
48
+ <dl class="attributes">
49
+ <% @snip_to_edit.attributes.each do |name, value| %>
50
+ <dt><%= name %></dt>
51
+ <dd><textarea name="<%= name %>" class="<%= name %>"><%=h value %></textarea></dd>
52
+ <% end %>
53
+ <dt><input class="attribute_name" type="text"></dt>
54
+ <dd><textarea></textarea></dd>
55
+ </dl>
56
+ <a href="#" id="add">Add</a>
57
+ <button name='save_button'>Save</button>
58
+ </form>
59
+ }
60
+ end