erector 0.7.0 → 0.7.1
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.
- 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
|