dinsley-markaby 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/README +256 -0
  2. data/Rakefile +49 -0
  3. data/VERSION.yml +4 -0
  4. data/lib/markaby/builder.rb +289 -0
  5. data/lib/markaby/cssproxy.rb +48 -0
  6. data/lib/markaby/metaid.rb +16 -0
  7. data/lib/markaby/rails/action_controller_helpers.rb +13 -0
  8. data/lib/markaby/rails/template_handler.rb +21 -0
  9. data/lib/markaby/tags.rb +179 -0
  10. data/lib/markaby.rb +39 -0
  11. data/test/app_root/app/controllers/application_controller.rb +2 -0
  12. data/test/app_root/app/controllers/markaby_controller.rb +37 -0
  13. data/test/app_root/app/helpers/test_helper.rb +7 -0
  14. data/test/app_root/app/views/markaby/_monkeys.mab +12 -0
  15. data/test/app_root/app/views/markaby/broken.mab +7 -0
  16. data/test/app_root/app/views/markaby/create.mab +9 -0
  17. data/test/app_root/app/views/markaby/index.mab +7 -0
  18. data/test/app_root/app/views/markaby/multiple_forms.mab +7 -0
  19. data/test/app_root/config/boot.rb +115 -0
  20. data/test/app_root/config/database.yml +31 -0
  21. data/test/app_root/config/environment.rb +14 -0
  22. data/test/app_root/config/environments/in_memory.rb +0 -0
  23. data/test/app_root/config/environments/mysql.rb +0 -0
  24. data/test/app_root/config/environments/postgresql.rb +0 -0
  25. data/test/app_root/config/environments/sqlite.rb +0 -0
  26. data/test/app_root/config/environments/sqlite3.rb +0 -0
  27. data/test/app_root/config/routes.rb +4 -0
  28. data/test/app_root/lib/console_with_fixtures.rb +4 -0
  29. data/test/app_root/log/in_memory.log +4215 -0
  30. data/test/app_root/script/console +7 -0
  31. data/test/markaby_controller_test.rb +73 -0
  32. data/test/markaby_test.rb +122 -0
  33. data/test/monkeys.html +13 -0
  34. data/test/test_helper.rb +28 -0
  35. metadata +102 -0
data/README ADDED
@@ -0,0 +1,256 @@
1
+ = Markaby (Markup as Ruby)
2
+
3
+ Markaby is a very short bit of code for writing HTML pages in pure Ruby.
4
+ It is an alternative to ERb which weaves the two languages together.
5
+ Also a replacement for templating languages which use primitive languages
6
+ that blend with HTML.
7
+
8
+ == Using Markaby as a Rails plugin
9
+
10
+ Write Rails templates in pure Ruby. Example layout:
11
+
12
+ html do
13
+ head do
14
+ title 'Products: ' + action_name
15
+ stylesheet_link_tag 'scaffold'
16
+ end
17
+
18
+ body do
19
+ p flash[:notice], :style => "color: green"
20
+
21
+ self << content_for_layout
22
+ end
23
+ end
24
+
25
+ == Using Markaby as a Ruby class
26
+
27
+ Markaby is flaming easy to call from your Ruby classes.
28
+
29
+ require 'markaby'
30
+
31
+ mab = Markaby::Builder.new
32
+ mab.html do
33
+ head { title "Boats.com" }
34
+ body do
35
+ h1 "Boats.com has great deals"
36
+ ul do
37
+ li "$49 for a canoe"
38
+ li "$39 for a raft"
39
+ li "$29 for a huge boot that floats and can fit 5 people"
40
+ end
41
+ end
42
+ end
43
+ puts mab.to_s
44
+
45
+ Markaby::Builder.new does take two arguments for passing in variables and
46
+ a helper object. You can also affix the block right on to the class.
47
+
48
+ See Markaby::Builder for all of that.
49
+
50
+ = A Note About <tt>instance_eval</tt>
51
+
52
+ The Markaby::Builder class is different from the normal Builder class,
53
+ since it uses <tt>instance_eval</tt> when running blocks. This cleans
54
+ up the appearance of the Markaby code you write. If <tt>instance_eval</tt>
55
+ was not used, the code would look like this:
56
+
57
+ mab = Markaby::Builder.new
58
+ mab.html do
59
+ mab.head { mab.title "Boats.com" }
60
+ mab.body do
61
+ mab.h1 "Boats.com has great deals"
62
+ end
63
+ end
64
+ puts mab.to_s
65
+
66
+ So, the advantage is the cleanliness of your code. The disadvantage is that
67
+ the block will run inside the Markaby::Builder object's scope. This means
68
+ that inside these blocks, <tt>self</tt> will be your Markaby::Builder object.
69
+ When you use instance variables in these blocks, they will be instance variables
70
+ of the Markaby::Builder object.
71
+
72
+ This doesn't affect Rails users, but when used in regular Ruby code, it can
73
+ be a bit disorienting. You are recommended to put your Markaby code in a
74
+ module where it won't mix with anything.
75
+
76
+ = The Six Steps of Markaby
77
+
78
+ If you dive right into Markaby, it'll probably make good sense, but you're
79
+ likely to run into a few kinks. Why not review these six steps and commit
80
+ them memory so you can really *know* what you're doing?
81
+
82
+ == 1. Element Classes
83
+
84
+ Element classes may be added by hooking methods onto container elements:
85
+
86
+ div.entry do
87
+ h2.entryTitle 'Son of WebPage'
88
+ div.entrySection %{by Anthony}
89
+ div.entryContent 'Okay, once again, the idea here is ...'
90
+ end
91
+
92
+ Which results in:
93
+
94
+ <div class="entry">
95
+ <h2 class="entryTitle">Son of WebPage</h2>
96
+ <div class="entrySection">by Anthony</div>
97
+ <div class="entryContent">Okay, once again, the idea here is ...</div>
98
+ </div>
99
+
100
+ == 2. Element IDs
101
+
102
+ IDs may be added by the use of bang methods:
103
+
104
+ div.page! {
105
+ div.content! {
106
+ h1 "A Short Short Saintly Dog"
107
+ }
108
+ }
109
+
110
+ Which results in:
111
+
112
+ <div id="page">
113
+ <div id="content">
114
+ <h1>A Short Short Saintly Dog</h1>
115
+ </div>
116
+ </div>
117
+
118
+ == 3. Validate Your XHTML 1.0 Output
119
+
120
+ If you'd like Markaby to help you assemble valid XHTML documents,
121
+ you can use the <tt>xhtml_transitional</tt> or <tt>xhtml_strict</tt>
122
+ methods in place of the normal <tt>html</tt> tag.
123
+
124
+ xhtml_strict do
125
+ head { ... }
126
+ body { ... }
127
+ end
128
+
129
+ This will add the XML instruction and the doctype tag to your document.
130
+ Also, a character set meta tag will be placed inside your <tt>head</tt>
131
+ tag.
132
+
133
+ Now, since Markaby knows which doctype you're using, it checks a big
134
+ list of valid tags and attributes before printing anything.
135
+
136
+ >> div :styl => "padding: 10px" do
137
+ >> img :src => "samorost.jpg"
138
+ >> end
139
+ InvalidHtmlError: no such attribute `styl'
140
+
141
+ Markaby will also make sure you don't use the same element ID twice!
142
+
143
+ == 4. Escape or No Escape?
144
+
145
+ Markaby uses a simple convention for escaping stuff: if a string
146
+ is an argument, it gets escaped. If the string is in a block, it
147
+ doesn't.
148
+
149
+ This is handy if you're using something like RedCloth or
150
+ RDoc inside an element. Pass the string back through the block
151
+ and it'll skip out of escaping.
152
+
153
+ div.comment { RedCloth.new(str).to_html }
154
+
155
+ But, if we have some raw text that needs escaping, pass it in
156
+ as an argument:
157
+
158
+ div.comment raw_str
159
+
160
+ One caveat: if you have other tags inside a block, the string
161
+ passed back will be ignored.
162
+
163
+ div.comment {
164
+ div.author "_why"
165
+ div.says "Torpedoooooes!"
166
+ "<div>Silence.</div>"
167
+ }
168
+
169
+ The final div above won't appear in the output. You can't mix
170
+ tag modes like that, friend.
171
+
172
+ == 5. Auto-stringification
173
+
174
+ If you end up using any of your Markaby "tags" as a string, the
175
+ tag won't be output. It'll be up to you to add the new string
176
+ back into the HTML output.
177
+
178
+ This means if you call <tt>to_s</tt>, you'll get a string back.
179
+
180
+ div.title { "Rock Bottom" + span(" by Robert Wyatt").to_s }
181
+
182
+ But, when you're adding strings in Ruby, <tt>to_s</tt> happens automatically.
183
+
184
+ div.title { "Rock Bottom" + span(" by Robert Wyatt") }
185
+
186
+ Interpolation works fine.
187
+
188
+ div.title { "Rock Bottom #{span(" by Robert Wyatt")}" }
189
+
190
+ And any other operation you might perform on a string.
191
+
192
+ div.menu! \
193
+ ['5.gets', 'bits', 'cult', 'inspect', '-h'].map do |category|
194
+ link_to category
195
+ end.
196
+ join( " | " )
197
+
198
+ == 6. The <tt>tag!</tt> Method
199
+
200
+ If you need to force a tag at any time, call <tt>tag!</tt> with the
201
+ tag name followed by the possible arguments and block. The CssProxy
202
+ won't work with this technique.
203
+
204
+ tag! :select, :id => "country_list" do
205
+ countries.each do |country|
206
+ tag! :option, country
207
+ end
208
+ end
209
+
210
+ = A Note About Rails Helpers
211
+
212
+ When used in Rails templates, the Rails helper object is passed into
213
+ Markaby::Builder. When you call helper methods inside Markaby, the output
214
+ from those methods will be output to the stream. This is incredibly
215
+ handy, since most Rails helpers output HTML tags.
216
+
217
+ head do
218
+ javascript_include_tag 'prototype'
219
+ autodiscovery_link_tag
220
+ end
221
+
222
+ However, some methods are designed to give back a String which you can use
223
+ elsewhere. That's okay! Every method returns a Fragment object, which can
224
+ be used as a string.
225
+
226
+ p { "Total is: #{number_to_human_size @file_bytes}" }
227
+
228
+ Also see the Quick Tour above, specifically the stuff about auto-stringification.
229
+
230
+ If for any reason you have trouble with fragments, you can just
231
+ call the <tt>@helpers</tt> object with the method and you'll get
232
+ the String back and nothing will be output.
233
+
234
+ p { "Total is: #{@helpers.number_to_human_size @file_bytes}" }
235
+
236
+ Conversely, you may call instance variables from your controller by using
237
+ a method and its value will be returned, nothing will be output.
238
+
239
+ # Inside imaginary ProductController
240
+ def list
241
+ @products = Product.find :all
242
+ end
243
+
244
+ # Inside app/views/product/list.mab
245
+ products.each do |product|
246
+ p product.title
247
+ end
248
+
249
+ = Credits
250
+
251
+ Markaby is a work of immense hope by Tim Fletcher and why the lucky stiff.
252
+ Thankyou for giving it a whirl.
253
+
254
+ Markaby is inspired by the HTML library within cgi.rb. Hopefully it will
255
+ turn around and take some cues.
256
+
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rcov/rcovtask'
5
+
6
+ begin
7
+ require 'jeweler'
8
+
9
+ Jeweler::Tasks.new do |s|
10
+ s.name = "markaby"
11
+ s.summary = "Markaby is a very short bit of code for writing HTML pages in pure Ruby."
12
+ s.email = "dinsley@gmail.com "
13
+ s.homepage = "http://github.com/dinsley/markaby"
14
+ s.description = "Markaby is a very short bit of code for writing HTML pages in pure Ruby."
15
+ s.authors = ["Tim Fletcher", "why_", "Daniel Insley"]
16
+ end
17
+ rescue LoadError
18
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
19
+ end
20
+
21
+ task :test do
22
+ Rake::TestTask.new do |t|
23
+ t.libs << 'lib'
24
+ t.pattern = ['test/markaby_test.rb']
25
+ t.verbose = false
26
+ end
27
+
28
+ Rake::TestTask.new do |t|
29
+ t.libs << 'lib'
30
+ t.pattern = ['test/markaby_controller_test.rb']
31
+ t.verbose = false
32
+ end
33
+ end
34
+
35
+ Rake::RDocTask.new do |rdoc|
36
+ rdoc.rdoc_dir = 'rdoc'
37
+ rdoc.title = 'Jeweler'
38
+ rdoc.options << '--line-numbers' << '--inline-source'
39
+ rdoc.rdoc_files.include('README*')
40
+ rdoc.rdoc_files.include('lib/**/*.rb')
41
+ end
42
+
43
+ Rcov::RcovTask.new do |t|
44
+ t.libs << "test"
45
+ t.test_files = FileList['markaby_test.rb', 'test/markaby_controller_test.rb']
46
+ t.verbose = true
47
+ end
48
+
49
+ task :default => :test
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 0
3
+ :patch: 5
4
+ :major: 0
@@ -0,0 +1,289 @@
1
+ require 'markaby/tags'
2
+
3
+ module Markaby
4
+ # The Markaby::Builder class is the central gear in the system. When using
5
+ # from Ruby code, this is the only class you need to instantiate directly.
6
+ #
7
+ # mab = Markaby::Builder.new
8
+ # mab.html do
9
+ # head { title "Boats.com" }
10
+ # body do
11
+ # h1 "Boats.com has great deals"
12
+ # ul do
13
+ # li "$49 for a canoe"
14
+ # li "$39 for a raft"
15
+ # li "$29 for a huge boot that floats and can fit 5 people"
16
+ # end
17
+ # end
18
+ # end
19
+ # puts mab.to_s
20
+ #
21
+ class Builder
22
+
23
+ @@default = {
24
+ :indent => 0,
25
+ :output_helpers => true,
26
+ :output_xml_instruction => true,
27
+ :output_meta_tag => true,
28
+ :auto_validation => true,
29
+ :tagset => Markaby::XHTMLTransitional,
30
+ :root_attributes => {
31
+ :xmlns => 'http://www.w3.org/1999/xhtml', :'xml:lang' => 'en', :lang => 'en'
32
+ }
33
+ }
34
+
35
+ def self.set(option, value)
36
+ @@default[option] = value
37
+ end
38
+
39
+ def self.ignored_helpers
40
+ @@ignored_helpers ||= []
41
+ end
42
+
43
+ def self.ignore_helpers(*helpers)
44
+ ignored_helpers.concat helpers
45
+ end
46
+
47
+ attr_accessor :output_helpers, :tagset
48
+
49
+ # Create a Markaby builder object. Pass in a hash of variable assignments to
50
+ # +assigns+ which will be available as instance variables inside tag construction
51
+ # blocks. If an object is passed in to +helpers+, its methods will be available
52
+ # from those same blocks.
53
+ #
54
+ # Pass in a +block+ to new and the block will be evaluated.
55
+ #
56
+ # mab = Markaby::Builder.new {
57
+ # html do
58
+ # body do
59
+ # h1 "Matching Mole"
60
+ # end
61
+ # end
62
+ # }
63
+ #
64
+ def initialize(assigns = {}, helpers = nil, &block)
65
+ @streams = [[]]
66
+ @assigns = assigns.dup
67
+ @helpers = helpers
68
+ @elements = {}
69
+
70
+ @@default.each do |k, v|
71
+ instance_variable_set("@#{k}", @assigns.delete(k) || v)
72
+ end
73
+
74
+ @assigns.each do |k, v|
75
+ instance_variable_set("@#{k}", v)
76
+ end
77
+
78
+ @builder = XmlMarkup.new(:indent => @indent, :target => @streams.last)
79
+
80
+ text(capture(&block)) if block
81
+ end
82
+
83
+ # Returns a string containing the HTML stream. Internally, the stream is stored as an Array.
84
+ def to_s
85
+ @streams.last.to_s
86
+ end
87
+
88
+ # Write a +string+ to the HTML stream without escaping it.
89
+ def text(string)
90
+ @builder << string.to_s
91
+ nil
92
+ end
93
+ alias_method :<<, :text
94
+ alias_method :concat, :text
95
+
96
+ # Captures the HTML code built inside the +block+. This is done by creating a new
97
+ # stream for the builder object, running the block and passing back its stream as a string.
98
+ #
99
+ # >> Markaby::Builder.new.capture { h1 "TEST"; h2 "CAPTURE ME" }
100
+ # => "<h1>TITLE</h1>\n<h2>CAPTURE ME</h2>\n"
101
+ #
102
+ def capture(&block)
103
+ @streams.push(@builder.target = [])
104
+ @builder.level += 1
105
+ str = instance_eval(&block)
106
+ str = @streams.last.join if @streams.last.any?
107
+ @streams.pop
108
+ @builder.level -= 1
109
+ @builder.target = @streams.last
110
+ str
111
+ end
112
+
113
+ # Create a tag named +tag+. Other than the first argument which is the tag name,
114
+ # the arguments are the same as the tags implemented via method_missing.
115
+ def tag!(tag, *args, &block)
116
+ ele_id = nil
117
+ if @auto_validation and @tagset
118
+ if !@tagset.tagset.has_key?(tag)
119
+ raise InvalidXhtmlError, "no element `#{tag}' for #{tagset.doctype}"
120
+ elsif args.last.respond_to?(:to_hash)
121
+ attrs = args.last.to_hash
122
+
123
+ if @tagset.forms.include?(tag) and attrs[:id]
124
+ attrs[:name] ||= attrs[:id]
125
+ end
126
+
127
+ attrs.each do |k, v|
128
+ atname = k.to_s.downcase.intern
129
+ unless k =~ /:/ or @tagset.tagset[tag].include? atname
130
+ raise InvalidXhtmlError, "no attribute `#{k}' on #{tag} elements"
131
+ end
132
+ if atname == :id
133
+ ele_id = v.to_s
134
+ if @elements.has_key? ele_id
135
+ raise InvalidXhtmlError, "id `#{ele_id}' already used (id's must be unique)."
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ if block
142
+ str = capture(&block)
143
+ block = proc { text(str) }
144
+ end
145
+
146
+ f = fragment { @builder.method_missing(tag, *args, &block) }
147
+ @elements[ele_id] = f if ele_id
148
+ f
149
+ end
150
+
151
+ # This method is used to intercept calls to helper methods and instance
152
+ # variables. Here is the order of interception:
153
+ #
154
+ # * If +sym+ is a helper method, the helper method is called
155
+ # and output to the stream.
156
+ # * If +sym+ is a Builder::XmlMarkup method, it is passed on to the builder object.
157
+ # * If +sym+ is also the name of an instance variable, the
158
+ # value of the instance variable is returned.
159
+ # * If +sym+ has come this far and no +tagset+ is found, +sym+ and its arguments are passed to tag!
160
+ # * If a tagset is found, though, +NoMethodError+ is raised.
161
+ #
162
+ # method_missing used to be the lynchpin in Markaby, but it's no longer used to handle
163
+ # HTML tags. See html_tag for that.
164
+ def method_missing(sym, *args, &block)
165
+ if @helpers.respond_to?(sym, true) && !self.class.ignored_helpers.include?(sym)
166
+ r = @helpers.send(sym, *args, &block)
167
+ if @output_helpers and r.respond_to? :to_str
168
+ fragment { @builder << r }
169
+ else
170
+ r
171
+ end
172
+ elsif @assigns.has_key?(sym)
173
+ @assigns[sym]
174
+ elsif @assigns.has_key?(stringy_key = sym.to_s)
175
+ # Rails' ActionView assigns hash has string keys for
176
+ # instance variables that are defined in the controller.
177
+ @assigns[stringy_key]
178
+ elsif instance_variables.include?(ivar = "@#{sym}")
179
+ instance_variable_get(ivar)
180
+ elsif !@helpers.nil? && @helpers.instance_variables.include?(ivar)
181
+ @helpers.instance_variable_get(ivar)
182
+ elsif ::Builder::XmlMarkup.instance_methods.include?(sym.to_s)
183
+ @builder.__send__(sym, *args, &block)
184
+ elsif @tagset.nil?
185
+ tag!(sym, *args, &block)
186
+ else
187
+ raise NoMethodError, "no such method `#{sym}'"
188
+ end
189
+ end
190
+
191
+ # Every HTML tag method goes through an html_tag call. So, calling <tt>div</tt> is equivalent
192
+ # to calling <tt>html_tag(:div)</tt>. All HTML tags in Markaby's list are given generated wrappers
193
+ # for this method.
194
+ #
195
+ # If the @auto_validation setting is on, this method will check for many common mistakes which
196
+ # could lead to invalid XHTML.
197
+ def html_tag(sym, *args, &block)
198
+ if @auto_validation and @tagset.self_closing.include?(sym) and block
199
+ raise InvalidXhtmlError, "the `#{sym}' element is self-closing, please remove the block"
200
+ elsif args.empty? and block.nil?
201
+ CssProxy.new(self, @streams.last, sym)
202
+ else
203
+ tag!(sym, *args, &block)
204
+ end
205
+ end
206
+
207
+ XHTMLTransitional.tags.each do |k|
208
+ class_eval %{
209
+ def #{k}(*args, &block)
210
+ html_tag(#{k.inspect}, *args, &block)
211
+ end
212
+ }
213
+ end
214
+
215
+ remove_method :head
216
+
217
+ # Builds a head tag. Adds a <tt>meta</tt> tag inside with Content-Type
218
+ # set to <tt>text/html; charset=utf-8</tt>.
219
+ def head(*args, &block)
220
+ tag!(:head, *args) do
221
+ tag!(:meta, "http-equiv" => "Content-Type", "content" => "text/html; charset=utf-8") if @output_meta_tag
222
+ instance_eval(&block)
223
+ end
224
+ end
225
+
226
+ # Builds an html tag. An XML 1.0 instruction and an XHTML 1.0 Transitional doctype
227
+ # are prepended. Also assumes <tt>:xmlns => "http://www.w3.org/1999/xhtml",
228
+ # :lang => "en"</tt>.
229
+ def xhtml_transitional(attrs = {}, &block)
230
+ self.tagset = Markaby::XHTMLTransitional
231
+ xhtml_html(attrs, &block)
232
+ end
233
+
234
+ # Builds an html tag with XHTML 1.0 Strict doctype instead.
235
+ def xhtml_strict(attrs = {}, &block)
236
+ self.tagset = Markaby::XHTMLStrict
237
+ xhtml_html(attrs, &block)
238
+ end
239
+
240
+ # Builds an html tag with XHTML 1.0 Frameset doctype instead.
241
+ def xhtml_frameset(attrs = {}, &block)
242
+ self.tagset = Markaby::XHTMLFrameset
243
+ xhtml_html(attrs, &block)
244
+ end
245
+
246
+ private
247
+
248
+ def xhtml_html(attrs = {}, &block)
249
+ instruct! if @output_xml_instruction
250
+ declare!(:DOCTYPE, :html, :PUBLIC, *tagset.doctype)
251
+ tag!(:html, @root_attributes.merge(attrs), &block)
252
+ end
253
+
254
+ def fragment
255
+ stream = @streams.last
256
+ start = stream.length
257
+ yield
258
+ length = stream.length - start
259
+ Fragment.new(stream, start, length)
260
+ end
261
+
262
+ end
263
+
264
+ # Every tag method in Markaby returns a Fragment. If any method gets called on the Fragment,
265
+ # the tag is removed from the Markaby stream and given back as a string. Usually the fragment
266
+ # is never used, though, and the stream stays intact.
267
+ #
268
+ # For a more practical explanation, check out the README.
269
+ class Fragment < ::Builder::BlankSlate
270
+ def initialize(*args)
271
+ @stream, @start, @length = args
272
+ end
273
+ def method_missing(*args, &block)
274
+ # We can't do @stream.slice!(@start, @length),
275
+ # as it would invalidate the @starts and @lengths of other Fragment instances.
276
+ @str = @stream[@start, @length].to_s
277
+ @stream[@start, @length] = [nil] * @length
278
+ def self.method_missing(*args, &block)
279
+ @str.send(*args, &block)
280
+ end
281
+ @str.send(*args, &block)
282
+ end
283
+ end
284
+
285
+ class XmlMarkup < ::Builder::XmlMarkup
286
+ attr_accessor :target, :level
287
+ end
288
+
289
+ end