erector 0.7.0 → 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.txt +3 -1
- data/VERSION.yml +1 -1
- data/lib/erector.rb +1 -0
- data/lib/erector/externals.rb +29 -0
- data/lib/erector/rails.rb +4 -1
- data/lib/erector/rails/extensions/action_controller.rb +1 -1
- data/lib/erector/rails/extensions/rails_widget.rb +12 -15
- data/lib/erector/rails/extensions/rails_widget/rails_helpers.rb +136 -0
- data/lib/erector/rails/rails_form_builder.rb +20 -0
- data/lib/erector/rails/rails_version.rb +2 -2
- data/lib/erector/rails/template_handlers/ert_handler.rb +32 -0
- data/lib/erector/rails/template_handlers/{action_view_template_handler.rb → rb_handler.rb} +2 -2
- data/lib/erector/widget.rb +42 -16
- data/lib/erector/widgets.rb +3 -1
- data/lib/erector/widgets/environment_badge.rb +29 -0
- data/lib/erector/widgets/field_table.rb +110 -0
- data/lib/erector/widgets/page.rb +158 -0
- data/spec/erector/external_spec.rb +52 -0
- data/spec/erector/widget_spec.rb +50 -6
- data/spec/erector/widgets/field_table_spec.rb +133 -0
- data/spec/erector/widgets/page_spec.rb +29 -0
- data/spec/erector/widgets/table_spec.rb +3 -3
- data/spec/spec_helper.rb +1 -1
- metadata +13 -4
- data/lib/erector/rails/extensions/rails_widget/helpers.rb +0 -110
data/lib/erector/widgets.rb
CHANGED
@@ -0,0 +1,29 @@
|
|
1
|
+
module Erector
|
2
|
+
module Widgets #:nodoc:
|
3
|
+
|
4
|
+
# Displays a colored badge in the upper-left corner
|
5
|
+
# signifying the environment the app is running in.
|
6
|
+
# Inspired by Assaf Arkin
|
7
|
+
# <http://blog.labnotes.org/2009/10/08/using-a-badge-to-distinguish-development-and-production-environments/>
|
8
|
+
# Erectorized by Alex Chaffee
|
9
|
+
class EnvironmentBadge < Erector::Widget
|
10
|
+
def content
|
11
|
+
style <<-STYLE
|
12
|
+
#environment_badge { position: fixed; left: 1em; font-weight: bold; padding: .2em 0.9em; text-transform: uppercase; display: none }
|
13
|
+
#environment_badge.staging { color: #000; background: #ffff00; border: 2px solid #cccc20; }
|
14
|
+
#environment_badge.development { color: #fff; background: #ff0000; border: 2px solid #cc2020; }
|
15
|
+
#environment_badge.staging, #environment_badge.development { border-top: none; display: block; opacity: 0.6 }
|
16
|
+
STYLE
|
17
|
+
unless environment =~ /production/
|
18
|
+
p environment, :class => environment, :id => "environment_badge"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def environment
|
23
|
+
RAILS_ENV
|
24
|
+
rescue NameError
|
25
|
+
ENV['RAILS_ENV'] || ENV['RACK_ENV']
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# A simple HTML table with three columns: label, contents, and (optionally) note.
|
2
|
+
# Each row is called a field.
|
3
|
+
# The last row can optionally contain "buttons" (actually any widget will do)
|
4
|
+
# that are all shown in the 2nd column.
|
5
|
+
# It's all surrounded with a fieldset element for a title.
|
6
|
+
#
|
7
|
+
# In ASCII art, it looks something like this:
|
8
|
+
#
|
9
|
+
# /-Meals----------------------------------------\
|
10
|
+
# | Breakfast | eggs | |
|
11
|
+
# | Lunch | sandwich | |
|
12
|
+
# | Dinner | lo mein | with shrimp! |
|
13
|
+
# | | [Save] [Cancel] | |
|
14
|
+
# \----------------------------------------------/
|
15
|
+
#
|
16
|
+
# There are two ways to create a FieldTable.
|
17
|
+
# 1. Pass a block in to the constructor.
|
18
|
+
# 2. Make a subclass.
|
19
|
+
# In both cases you'll want to call the "field" and "button" methods on the table.
|
20
|
+
# This sets up the contents which will be rendered later during FieldTable#content.
|
21
|
+
# If you make a subclass (#2) you can do this either in the constructor or in
|
22
|
+
# the content method *before* you call super.
|
23
|
+
#
|
24
|
+
# The FieldTable is surrounded by a fieldset element whose legend is the "title" instance variable.
|
25
|
+
# Inside this fieldset is a table element.
|
26
|
+
# Each field (row of the table) has a label (th), a content cell (td), and an optional note (td).
|
27
|
+
#
|
28
|
+
# If you call "button" you can pass in a block that'll get rendered inside the 2nd column of the last row. The idea here is that you might want to make an HTML form that has a bunch of buttons at the bottom (Save, Cancel, Clear) and these all go in the same cell, with no label for the row.
|
29
|
+
#
|
30
|
+
# TODO: error messages?
|
31
|
+
# @author Alex Chaffee
|
32
|
+
class FieldTable < Erector::Widget
|
33
|
+
|
34
|
+
include Erector::Inline
|
35
|
+
|
36
|
+
class Field < Erector::Widget
|
37
|
+
needs :label, :note => nil
|
38
|
+
|
39
|
+
def content
|
40
|
+
tr :class => "field_table_field" do
|
41
|
+
th do
|
42
|
+
text @label
|
43
|
+
text ":" unless @label.nil?
|
44
|
+
end
|
45
|
+
td do
|
46
|
+
super # calls the block
|
47
|
+
end
|
48
|
+
if @note
|
49
|
+
td do
|
50
|
+
text @note
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Define a field, containing a label, optional note, and block for the contents
|
58
|
+
#
|
59
|
+
# TODO: allow passing in a widget instead of a block
|
60
|
+
def field(label = nil, note = nil, &contents)
|
61
|
+
@fields << Field.new(:label => label, :note => note, &contents)
|
62
|
+
end
|
63
|
+
|
64
|
+
# If you call "button" you can pass in a block that'll get rendered inside the 2nd column of the
|
65
|
+
# last row. The idea here is that you might want to make an HTML form that has a bunch of buttons at
|
66
|
+
# the bottom (Save, Cancel, Clear) and these all go in the same cell, with no label for the row.
|
67
|
+
#
|
68
|
+
# TODO: allow passing in a widget instead of a block
|
69
|
+
def button(&button_proc)
|
70
|
+
@buttons << button_proc
|
71
|
+
end
|
72
|
+
|
73
|
+
needs :title
|
74
|
+
|
75
|
+
# Pass in a block and it'll get called with a pointer to this table, so you can call
|
76
|
+
# 'field' and 'button' to configure it
|
77
|
+
def initialize(*args)
|
78
|
+
super
|
79
|
+
@fields = []
|
80
|
+
@buttons = []
|
81
|
+
yield self if block_given? # invoke the configuration block
|
82
|
+
end
|
83
|
+
|
84
|
+
def content
|
85
|
+
fieldset :class => "field_table" do
|
86
|
+
legend @title
|
87
|
+
table :width => '100%' do
|
88
|
+
@fields.each do |f|
|
89
|
+
widget f
|
90
|
+
end
|
91
|
+
unless @buttons.empty?
|
92
|
+
tr :class => "field_table_buttons" do
|
93
|
+
td :colspan => 2, :align => "right" do
|
94
|
+
table :class => 'layout' do
|
95
|
+
tr do
|
96
|
+
@buttons.each do |button|
|
97
|
+
td :class => "field_table_button" do
|
98
|
+
button.call
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# Erector Page base class.
|
2
|
+
#
|
3
|
+
# Allows for accumulation of script and style tags (see example below) with either
|
4
|
+
# external or inline content. External references are 'uniq'd, so it's a good idea to declare
|
5
|
+
# a js script in all widgets that use it, so you don't accidentally lose the script if you remove
|
6
|
+
# the one widget that happened to declare it.
|
7
|
+
#
|
8
|
+
# At minimum, child classes must override #body_content. You can also get a "quick and dirty"
|
9
|
+
# page by passing a block to Page.new but that doesn't really buy you much.
|
10
|
+
#
|
11
|
+
# The script and style declarations are accumulated at class load time, as 'externals'.
|
12
|
+
# This technique allows all widgets to add their own requirements to the page header
|
13
|
+
# without extra logic for declaring which pages include which nested widgets.
|
14
|
+
# Unfortunately, this means that every page in the application will share the same headers,
|
15
|
+
# which may lead to conflicts.
|
16
|
+
#
|
17
|
+
# If you want something to show up in the headers for just one page type (subclass),
|
18
|
+
# then override #head_content, call super, and then emit it yourself.
|
19
|
+
#
|
20
|
+
# Author:: Alex Chaffee, alex@stinky.com
|
21
|
+
#
|
22
|
+
# = Example Usage:
|
23
|
+
#
|
24
|
+
# class MyPage < Page
|
25
|
+
# external :js, "lib/jquery.js"
|
26
|
+
# external :script, "$(document).ready(function(){...});"
|
27
|
+
# external :css, "stuff.css"
|
28
|
+
# external :style, "li.foo { color: red; }"
|
29
|
+
#
|
30
|
+
# def page_title
|
31
|
+
# "my app"
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# def body_content
|
35
|
+
# h1 "My App"
|
36
|
+
# p "welcome to my app"
|
37
|
+
# widget WidgetWithExternalStyle
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# class WidgetWithExternalStyle < Erector::Widget
|
42
|
+
# external :style, "div.custom { border: 2px solid green; }"
|
43
|
+
#
|
44
|
+
# def content
|
45
|
+
# div :class => "custom" do
|
46
|
+
# text "green is good"
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# = Thoughts:
|
52
|
+
# * It may be desirable to unify #js and #script, and #css and #style, and have the routine be
|
53
|
+
# smart enough to analyze its parameter to decide whether to make it a file or a script.
|
54
|
+
#
|
55
|
+
class Erector::Widgets::Page < Erector::Widget
|
56
|
+
|
57
|
+
needs :basic_styles => true
|
58
|
+
|
59
|
+
# Emit the Transitional doctype.
|
60
|
+
# TODO: allow selection from among different standard doctypes
|
61
|
+
def doctype
|
62
|
+
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
63
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
|
64
|
+
end
|
65
|
+
|
66
|
+
def content
|
67
|
+
rawtext doctype
|
68
|
+
# todo: allow customization of xmlns and xml:lang
|
69
|
+
html :xmlns => 'http://www.w3.org/1999/xhtml', 'xml:lang' => 'en', :lang => 'en' do
|
70
|
+
head do
|
71
|
+
head_content
|
72
|
+
end
|
73
|
+
body :class => body_class do
|
74
|
+
body_content
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# override me to provide a page title (default = name of the Page subclass)
|
80
|
+
def page_title
|
81
|
+
self.class.name
|
82
|
+
end
|
83
|
+
|
84
|
+
# override me to add a css class to the body
|
85
|
+
def body_class
|
86
|
+
end
|
87
|
+
|
88
|
+
# override me (or instantiate Page with a block)
|
89
|
+
def body_content
|
90
|
+
instance_eval(&@block) if @block
|
91
|
+
end
|
92
|
+
|
93
|
+
# emit the contents of the head element. Override and call super if you want to put more stuff in there.
|
94
|
+
def head_content
|
95
|
+
meta 'http-equiv' => 'content-type', :content => 'text/html;charset=UTF-8'
|
96
|
+
title page_title
|
97
|
+
|
98
|
+
basic_styles if @basic_styles
|
99
|
+
included_stylesheets
|
100
|
+
inline_styles
|
101
|
+
|
102
|
+
included_scripts
|
103
|
+
inline_scripts
|
104
|
+
end
|
105
|
+
|
106
|
+
def included_scripts
|
107
|
+
self.class.externals(:js).each do |file|
|
108
|
+
script :type => "text/javascript", :src => file
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def included_stylesheets
|
113
|
+
self.class.externals(:css).each do |file|
|
114
|
+
# todo: allow different media
|
115
|
+
link :rel => "stylesheet", :href => file, :type => "text/css", :media => "all"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Emit some *very* basic styles, hopefully not too controversial. Suppress
|
120
|
+
# them by setting :basic_styles => false when you construct your Page, or just
|
121
|
+
# override the basic_styles method in your subclass and make it do nothing.
|
122
|
+
# You can also redefine them since they're defined above any other styles in the HEAD.
|
123
|
+
#
|
124
|
+
# Class "right" floats right, class "left" floats left, and class "clear" clears
|
125
|
+
# any floats on both sides while being as small as possible to minimize impact
|
126
|
+
# on your layout. And images have no border.
|
127
|
+
def basic_styles
|
128
|
+
style <<-STYLE
|
129
|
+
img {border: none}
|
130
|
+
.right {float: right;}
|
131
|
+
.left {float: left;}
|
132
|
+
.clear {background: none;border: 0;clear: both;display: block;float: none;font-size: 0;margin: 0;padding: 0;position: static;overflow: hidden;visibility: hidden;width: 0;height: 0;}
|
133
|
+
STYLE
|
134
|
+
end
|
135
|
+
|
136
|
+
def inline_styles
|
137
|
+
style :type => "text/css", 'xml:space' => 'preserve' do
|
138
|
+
rawtext "\n"
|
139
|
+
self.class.externals(:style).each do |txt|
|
140
|
+
rawtext "\n"
|
141
|
+
rawtext txt
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def inline_scripts
|
147
|
+
javascript do
|
148
|
+
self.class.externals(:script).each do |txt|
|
149
|
+
rawtext "\n"
|
150
|
+
rawtext txt
|
151
|
+
end
|
152
|
+
self.class.externals(:jquery).each do |txt|
|
153
|
+
jquery_ready txt
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require File.expand_path("#{File.dirname(__FILE__)}/../spec_helper")
|
2
|
+
require 'benchmark'
|
3
|
+
|
4
|
+
describe "external declarations" do
|
5
|
+
class HotSauce < Erector::Widget
|
6
|
+
external :css, "/css/tapatio.css"
|
7
|
+
external :css, "/css/salsa_picante.css"
|
8
|
+
external :js, "/lib/jquery.js"
|
9
|
+
external :js, "/lib/picante.js"
|
10
|
+
end
|
11
|
+
|
12
|
+
class SourCream < Erector::Widget
|
13
|
+
external :css, "/css/sourcream.css"
|
14
|
+
external :js, "/lib/jquery.js"
|
15
|
+
external :js, "/lib/dairy.js"
|
16
|
+
end
|
17
|
+
|
18
|
+
it "can be fetched via the type" do
|
19
|
+
Erector::Widget.externals(:css).should == [
|
20
|
+
"/css/tapatio.css",
|
21
|
+
"/css/salsa_picante.css",
|
22
|
+
"/css/sourcream.css",
|
23
|
+
]
|
24
|
+
end
|
25
|
+
|
26
|
+
it "can be filtered via the class" do
|
27
|
+
Erector::Widget.externals(:css, HotSauce).should == [
|
28
|
+
"/css/tapatio.css",
|
29
|
+
"/css/salsa_picante.css",
|
30
|
+
]
|
31
|
+
Erector::Widget.externals(:css, SourCream).should == [
|
32
|
+
"/css/sourcream.css",
|
33
|
+
]
|
34
|
+
end
|
35
|
+
|
36
|
+
it "removes duplicates" do
|
37
|
+
Erector::Widget.externals(:js).should == [
|
38
|
+
"/lib/jquery.js",
|
39
|
+
"/lib/picante.js",
|
40
|
+
"/lib/dairy.js",
|
41
|
+
]
|
42
|
+
end
|
43
|
+
|
44
|
+
it "works with strings or symbols" do
|
45
|
+
Erector::Widget.externals("js").should == [
|
46
|
+
"/lib/jquery.js",
|
47
|
+
"/lib/picante.js",
|
48
|
+
"/lib/dairy.js",
|
49
|
+
]
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
data/spec/erector/widget_spec.rb
CHANGED
@@ -212,7 +212,7 @@ module WidgetSpec
|
|
212
212
|
:nil_attribute => nil
|
213
213
|
)
|
214
214
|
end.to_s
|
215
|
-
doc =
|
215
|
+
doc = Nokogiri::HTML(html)
|
216
216
|
div = doc.at('div')
|
217
217
|
div[:class].should == "foo bar"
|
218
218
|
div[:style].should == "display: none; color: white; float: left;"
|
@@ -430,6 +430,40 @@ module WidgetSpec
|
|
430
430
|
end
|
431
431
|
end
|
432
432
|
end
|
433
|
+
|
434
|
+
def capturing_output
|
435
|
+
output = StringIO.new
|
436
|
+
$stdout = output
|
437
|
+
yield
|
438
|
+
output.string
|
439
|
+
ensure
|
440
|
+
$stdout = STDOUT
|
441
|
+
end
|
442
|
+
|
443
|
+
describe "#comment" do
|
444
|
+
it "emits an HTML comment" do
|
445
|
+
Erector.inline do
|
446
|
+
comment "foo"
|
447
|
+
end.to_s.should == "<!--foo-->"
|
448
|
+
end
|
449
|
+
|
450
|
+
# see http://www.w3.org/TR/html4/intro/sgmltut.html#h-3.2.4
|
451
|
+
it "does not HTML-escape character references" do
|
452
|
+
Erector.inline do
|
453
|
+
comment " "
|
454
|
+
end.to_s.should == "<!-- -->"
|
455
|
+
end
|
456
|
+
|
457
|
+
# see http://www.w3.org/TR/html4/intro/sgmltut.html#h-3.2.4
|
458
|
+
# "Authors should avoid putting two or more adjacent hyphens inside comments."
|
459
|
+
it "warns if there's two hyphens in a row" do
|
460
|
+
capturing_output do
|
461
|
+
Erector.inline do
|
462
|
+
comment "he was -- awesome!"
|
463
|
+
end.to_s.should == "<!--he was -- awesome!-->"
|
464
|
+
end.should == "Warning: Authors should avoid putting two or more adjacent hyphens inside comments.\n"
|
465
|
+
end
|
466
|
+
end
|
433
467
|
|
434
468
|
describe "#nbsp" do
|
435
469
|
it "turns consecutive spaces into consecutive non-breaking spaces" do
|
@@ -594,8 +628,8 @@ module WidgetSpec
|
|
594
628
|
html = Erector.inline do
|
595
629
|
javascript(:src => "/my/js/file.js")
|
596
630
|
end.to_s
|
597
|
-
doc =
|
598
|
-
doc.at(
|
631
|
+
doc = Nokogiri::HTML(html)
|
632
|
+
doc.at("script")[:src].should == "/my/js/file.js"
|
599
633
|
end
|
600
634
|
end
|
601
635
|
|
@@ -604,7 +638,7 @@ module WidgetSpec
|
|
604
638
|
html = Erector.inline do
|
605
639
|
javascript('alert("&<>\'hello");', :src => "/my/js/file.js")
|
606
640
|
end.to_s
|
607
|
-
doc =
|
641
|
+
doc = Nokogiri::HTML(html)
|
608
642
|
script_tag = doc.at('script')
|
609
643
|
script_tag[:src].should == "/my/js/file.js"
|
610
644
|
script_tag.inner_html.should include('alert("&<>\'hello");')
|
@@ -709,15 +743,25 @@ module WidgetSpec
|
|
709
743
|
|
710
744
|
end
|
711
745
|
|
712
|
-
describe "assigning
|
746
|
+
describe "assigning instance variables" do
|
713
747
|
it "attempting to overwrite a reserved instance variable raises error" do
|
714
748
|
lambda {
|
715
749
|
Erector::Widget.new(:output => "foo")
|
716
750
|
}.should raise_error(ArgumentError)
|
717
751
|
end
|
752
|
+
|
753
|
+
it "handles instance variable names with and without '@' in the beginning" do
|
754
|
+
html = Erector.inline(:foo => "bar", '@baz' => 'quux') do
|
755
|
+
div do
|
756
|
+
p @foo
|
757
|
+
p @baz
|
758
|
+
end
|
759
|
+
end.to_s
|
760
|
+
doc = Nokogiri::HTML(html)
|
761
|
+
doc.css("p").map {|p| p.inner_html}.should == ["bar", "quux"]
|
762
|
+
end
|
718
763
|
end
|
719
764
|
|
720
|
-
|
721
765
|
context "when declaring parameters with the 'needs' macro" do
|
722
766
|
it "doesn't complain if there aren't any needs declared" do
|
723
767
|
class Thing1 < Erector::Widget
|