rxhp 0.0.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/lib/rxhp/attribute_validator.rb +116 -0
- data/lib/rxhp/composable_element.rb +57 -0
- data/lib/rxhp/constants.rb +19 -0
- data/lib/rxhp/data/html/attributes.rb +154 -0
- data/lib/rxhp/data/html/tags.rb +533 -0
- data/lib/rxhp/element.rb +70 -0
- data/lib/rxhp/error.rb +7 -0
- data/lib/rxhp/fragment.rb +13 -0
- data/lib/rxhp/html.rb +43 -0
- data/lib/rxhp/html_element.rb +143 -0
- data/lib/rxhp/html_self_closing_element.rb +16 -0
- data/lib/rxhp/html_singleton_element.rb +26 -0
- data/lib/rxhp/scope.rb +114 -0
- data/lib/rxhp/tags/html_tag.rb +21 -0
- data/lib/rxhp.rb +4 -0
- metadata +60 -0
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'rxhp/error'
|
2
|
+
|
3
|
+
module Rxhp
|
4
|
+
module AttributeValidator
|
5
|
+
class ValidationError < Rxhp::ScriptError
|
6
|
+
end
|
7
|
+
|
8
|
+
class UnacceptableAttributeError < ValidationError
|
9
|
+
attr_reader :element, :attribute, :value
|
10
|
+
def initialize element, attribute, value
|
11
|
+
@element, @attribute, @value = element, attribute, value
|
12
|
+
super "Class #{element.class.name} does not support #{attribute}=#{value.inspect}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class MissingRequiredAttributeError < ValidationError
|
17
|
+
attr_reader :element, :attribute
|
18
|
+
def initialize element, attribute
|
19
|
+
@element, @attribute = element, attribute
|
20
|
+
super "Element #{element.inspect} is missing required attributes: #{attribute.inspect}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def valid_attributes?
|
25
|
+
begin
|
26
|
+
self.validate_attributes!
|
27
|
+
true
|
28
|
+
rescue ValidationError
|
29
|
+
false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate_attributes!
|
34
|
+
# Check for required attributes
|
35
|
+
self.class.required_attributes.each do |matcher|
|
36
|
+
matched = self.attributes.any? do |key, value|
|
37
|
+
key = key.to_s
|
38
|
+
Rxhp::AttributeValidator.match? matcher, key, value
|
39
|
+
end
|
40
|
+
if !matched
|
41
|
+
raise MissingRequiredAttributeError.new(self, matcher)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Check other attributes are acceptable
|
46
|
+
return true if self.attributes.empty?
|
47
|
+
self.attributes.each do |key, value|
|
48
|
+
key = key.to_s
|
49
|
+
matched = self.class.acceptable_attributes.any? do |matcher|
|
50
|
+
Rxhp::AttributeValidator.match? matcher, key, value
|
51
|
+
end
|
52
|
+
|
53
|
+
if !matched
|
54
|
+
raise UnacceptableAttributeError.new(self, key, value)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
true
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.match? matcher, name, value
|
61
|
+
case matcher
|
62
|
+
when Array
|
63
|
+
matcher.any? { |x| match? x, name, value }
|
64
|
+
when Hash
|
65
|
+
matcher.any? do |name_matcher, value_matcher|
|
66
|
+
name_match = match?(name_matcher, name, nil)
|
67
|
+
value_match = match?(value_matcher, value, nil)
|
68
|
+
name_match && value_match
|
69
|
+
end
|
70
|
+
when Symbol
|
71
|
+
matcher.to_s.gsub('_', '-') == name
|
72
|
+
else
|
73
|
+
matcher === name
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.included(klass)
|
78
|
+
klass.extend(ClassMethods)
|
79
|
+
class << klass
|
80
|
+
attr_accessor :acceptable_attributes, :required_attributes
|
81
|
+
def accept_attributes matcher
|
82
|
+
acceptable_attributes.push matcher
|
83
|
+
end
|
84
|
+
alias :accept_attribute :accept_attributes
|
85
|
+
|
86
|
+
def require_attributes matcher
|
87
|
+
accept_attributes matcher
|
88
|
+
required_attributes.push matcher
|
89
|
+
end
|
90
|
+
alias :require_attribute :require_attributes
|
91
|
+
|
92
|
+
def accept_all_attributes
|
93
|
+
attribute_matches.push Object
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
klass.acceptable_attributes = []
|
98
|
+
klass.required_attributes = []
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.inherited(subklass)
|
102
|
+
subklass.class_eval do
|
103
|
+
include Rxhp::AttributeValidator
|
104
|
+
end
|
105
|
+
subklass.acceptable_attributes = subklass.superclass.acceptable_attributes.dup
|
106
|
+
subklass.required_attributes = subklass.superclass.required_attributes.dup
|
107
|
+
end
|
108
|
+
|
109
|
+
module ClassMethods
|
110
|
+
def inherited(subklass)
|
111
|
+
Rxhp::AttributeValidator.inherited(subklass)
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'rxhp/attribute_validator'
|
2
|
+
require 'rxhp/element'
|
3
|
+
require 'rxhp/fragment'
|
4
|
+
|
5
|
+
module Rxhp
|
6
|
+
# This defines an element that is only a handy way to think of a
|
7
|
+
# tree of other elements - it has no real rendering by itself, just those
|
8
|
+
# of its' children.
|
9
|
+
#
|
10
|
+
# Most of the time, those children will either be ComposableElement's in
|
11
|
+
# turn, or subclasses of Rxhp::HtmlElement
|
12
|
+
class ComposableElement < Rxhp::Element
|
13
|
+
include Rxhp::AttributeValidator
|
14
|
+
|
15
|
+
def initialize *args
|
16
|
+
super *args
|
17
|
+
validate_attributes!
|
18
|
+
end
|
19
|
+
|
20
|
+
# You don't want to implement this function in your subclasses -
|
21
|
+
# just reimplement compose instead.
|
22
|
+
#
|
23
|
+
# This calls compose, provides the 'yield' magic, and callls render on
|
24
|
+
# the output.
|
25
|
+
def render options = {}
|
26
|
+
validate_attributes!
|
27
|
+
self.compose do
|
28
|
+
# Allow 'yield' to embed all children
|
29
|
+
self.children.each do |child|
|
30
|
+
fragment child
|
31
|
+
end
|
32
|
+
end.render(options)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Implement this method - return an Rxhp::Element subclass.
|
36
|
+
def compose
|
37
|
+
raise NotImplementedError.new
|
38
|
+
end
|
39
|
+
|
40
|
+
# Automatically add a foo_bar method to Rxhp scopes when a FooBar
|
41
|
+
# subclass of this is created.
|
42
|
+
def self.inherited subclass
|
43
|
+
Rxhp::AttributeValidator.inherited(subclass)
|
44
|
+
full_name = subclass.name
|
45
|
+
parts = full_name.split('::')
|
46
|
+
klass_name = parts.pop
|
47
|
+
namespace = Kernel
|
48
|
+
parts.each do |part|
|
49
|
+
namespace = namespace.const_get(part)
|
50
|
+
end
|
51
|
+
# UpperCamelCase => under_scored
|
52
|
+
tag_name = klass_name.gsub(/(.)([A-Z])/, '\1_\2').downcase
|
53
|
+
|
54
|
+
Rxhp::Scope.define_element tag_name, subclass, namespace
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Rxhp
|
2
|
+
# Basic output formats
|
3
|
+
HTML_FORMAT = :html
|
4
|
+
TINY_HTML_FORMAT = :tiny_html
|
5
|
+
XHTML_FORMAT = :xhtml
|
6
|
+
|
7
|
+
# Doctypes
|
8
|
+
HTML_5 = "<!DOCTYPE html>\n"
|
9
|
+
HTML_4_01_TRANSITIONAL = <<EOF
|
10
|
+
<!DOCTYPE HTML PUBLIC
|
11
|
+
"-//W3C//DTD HTML 4.01 Transitional//EN"
|
12
|
+
"http://www.w3.org/TR/html4/loose.dtd">
|
13
|
+
EOF
|
14
|
+
XHTML_1_0_STRICT = <<EOF
|
15
|
+
<!DOCTYPE html PUBLIC
|
16
|
+
"-//W3C//DTD XHTML 1.0 Strict//EN"
|
17
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
18
|
+
EOF
|
19
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Rxhp
|
4
|
+
module Html
|
5
|
+
# Given ['foo', 'bar', 'baz'], match:
|
6
|
+
# - 'foo'
|
7
|
+
# - 'bar baz'
|
8
|
+
# - 'foo bar baz'
|
9
|
+
# etc.
|
10
|
+
def self.token_list tokens
|
11
|
+
token = tokens.join('|')
|
12
|
+
/^#{token}( (#{token}) )*$/
|
13
|
+
end
|
14
|
+
|
15
|
+
# All the non-javascript attributes shared by every HTML element.
|
16
|
+
GLOBAL_ATTRIBUTES = {
|
17
|
+
/^data-[^:]+$/ => Object,
|
18
|
+
|
19
|
+
:accesskey => String,
|
20
|
+
:class => String,
|
21
|
+
:contenteditable => %w'true false inherit',
|
22
|
+
:contextmenu => String,
|
23
|
+
:dir => %w'ltr rtl auto',
|
24
|
+
:draggable => %w'true false',
|
25
|
+
:dropzone => String,
|
26
|
+
:hidden => [true, false],
|
27
|
+
:id => /^[^ ]+$/,
|
28
|
+
:lang => String,
|
29
|
+
:spellcheck => %w'true false',
|
30
|
+
:style => String,
|
31
|
+
:tabindex => Integer,
|
32
|
+
:title => String,
|
33
|
+
}
|
34
|
+
|
35
|
+
# Every HTML element has these
|
36
|
+
GLOBAL_EVENT_HANDLERS = {
|
37
|
+
:onabort => String,
|
38
|
+
:onblur => String,
|
39
|
+
:oncanplay => String,
|
40
|
+
:oncanplaythrough => String,
|
41
|
+
:onchange => String,
|
42
|
+
:onclick => String,
|
43
|
+
:oncontextmenu => String,
|
44
|
+
:oncuechange => String,
|
45
|
+
:ondblclick => String,
|
46
|
+
:ondrag => String,
|
47
|
+
:ondragend => String,
|
48
|
+
:ondragenter => String,
|
49
|
+
:ondragleave => String,
|
50
|
+
:ondragover => String,
|
51
|
+
:ondragstart => String,
|
52
|
+
:ondrop => String,
|
53
|
+
:ondurationchange => String,
|
54
|
+
:onemptied => String,
|
55
|
+
:onended => String,
|
56
|
+
:onerror => String,
|
57
|
+
:onfocus => String,
|
58
|
+
:oninput => String,
|
59
|
+
:oninvalid => String,
|
60
|
+
:onkeydown => String,
|
61
|
+
:onkeypress => String,
|
62
|
+
:onkeyup => String,
|
63
|
+
:onload => String,
|
64
|
+
:onloadeddata => String,
|
65
|
+
:onloadedmetadata => String,
|
66
|
+
:onloadstart => String,
|
67
|
+
:onmousedown => String,
|
68
|
+
:onmousemove => String,
|
69
|
+
:onmouseout => String,
|
70
|
+
:onmouseover => String,
|
71
|
+
:onmouseup => String,
|
72
|
+
:onmousewheel => String,
|
73
|
+
:onpause => String,
|
74
|
+
:onplay => String,
|
75
|
+
:onplaying => String,
|
76
|
+
:onprogress => String,
|
77
|
+
:onratechange => String,
|
78
|
+
:onreset => String,
|
79
|
+
:onscroll => String,
|
80
|
+
:onseeked => String,
|
81
|
+
:onseeking => String,
|
82
|
+
:onselect => String,
|
83
|
+
:onshow => String,
|
84
|
+
:onstalled => String,
|
85
|
+
:onsubmit => String,
|
86
|
+
:onsuspend => String,
|
87
|
+
:ontimeupdate => String,
|
88
|
+
:onvolumechange => String,
|
89
|
+
:onwaiting => String,
|
90
|
+
}
|
91
|
+
|
92
|
+
# Specific attributes used by multiple elements
|
93
|
+
HREF_ATTRIBUTE = { :href => [String, URI] }
|
94
|
+
MEDIA_ATTRIBUTE = { :media => String }
|
95
|
+
REL_ATTRIBUTE = {
|
96
|
+
:rel => Rxhp::Html.token_list(%w[
|
97
|
+
alternate
|
98
|
+
author
|
99
|
+
bookmark
|
100
|
+
help
|
101
|
+
icon
|
102
|
+
license
|
103
|
+
next
|
104
|
+
nofollow
|
105
|
+
noreferrer
|
106
|
+
prefetch
|
107
|
+
prev
|
108
|
+
search
|
109
|
+
stylesheet
|
110
|
+
tag
|
111
|
+
]),
|
112
|
+
}
|
113
|
+
DATETIME_ATTRIBUTE = { :datetime => String }
|
114
|
+
CITE_ATTRIBUTE = { :cite => [String, URI] }
|
115
|
+
SRC_ATTRIBUTE = { :src => [String, URI] }
|
116
|
+
DIMENSION_ATTRIBUTES = {
|
117
|
+
:width => String,
|
118
|
+
:height => String,
|
119
|
+
}
|
120
|
+
CROSSORIGIN_ATTRIBUTE = {
|
121
|
+
:crossorigin => /^(anonymous|use-credentials)$/,
|
122
|
+
}
|
123
|
+
COMMON_MEDIA_ATTRIBUTES = {
|
124
|
+
:preload => /^(none|metadata|auto)/,
|
125
|
+
:autoplay => [true, false],
|
126
|
+
:mediagroup => String,
|
127
|
+
:loop => [true, false],
|
128
|
+
:muted => [true, false],
|
129
|
+
:controls => [true, false],
|
130
|
+
}
|
131
|
+
SPAN_ATTRIBUTE = { :span => Integer }
|
132
|
+
TABLE_CELL_ATTRIBUTES = {
|
133
|
+
:colspan => Integer,
|
134
|
+
:rowspan => Integer,
|
135
|
+
:headers => String,
|
136
|
+
},
|
137
|
+
AUTOCOMPLETE_ATTRIBUTE = { :autocomplete => ['on', 'off'] }
|
138
|
+
|
139
|
+
ENC_TYPES = %w[
|
140
|
+
application/x-www-form-urlencoded
|
141
|
+
multipart/form-data
|
142
|
+
text/plain
|
143
|
+
]
|
144
|
+
INTEGER_LIST = /^\d+(,\d+)*$/
|
145
|
+
FORM_ATTRIBUTES = {
|
146
|
+
:form => String,
|
147
|
+
:formaction => [String, URI],
|
148
|
+
:formenctype => ENC_TYPES,
|
149
|
+
:formmethod => ['get', 'post'],
|
150
|
+
:formnovalidate => [true, false],
|
151
|
+
:formtarget => String,
|
152
|
+
}
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,533 @@
|
|
1
|
+
require 'rxhp/html_element'
|
2
|
+
require 'rxhp/html_self_closing_element'
|
3
|
+
require 'rxhp/html_singleton_element'
|
4
|
+
|
5
|
+
require 'rxhp/data/html/attributes'
|
6
|
+
|
7
|
+
module Rxhp
|
8
|
+
# Definitions of all standard HTML 4.01 and HTML 5 elements.
|
9
|
+
#
|
10
|
+
# All the classes are created when this file is loaded, regardless
|
11
|
+
# of usage.
|
12
|
+
#
|
13
|
+
# There are three common superclasses:
|
14
|
+
# - HtmlElement - standard elements
|
15
|
+
# - HtmlSelfClosingElement - elements where in HTML, the closing tag is
|
16
|
+
# optional - for example, <p>, <li>, and <body>
|
17
|
+
# - HtmlSingletonElement - not only is the closing tag optional, but
|
18
|
+
# child elements are forbidden - for example, <br> and <img>
|
19
|
+
#
|
20
|
+
# 'Special' classes are defined in rxhp/tags/ - for example, the HTML
|
21
|
+
# element in Rxhp is defined to add a doctype, depending on the formatter
|
22
|
+
# settings.
|
23
|
+
module Html
|
24
|
+
TAGS = {
|
25
|
+
:a => {
|
26
|
+
:attributes => [
|
27
|
+
HREF_ATTRIBUTE,
|
28
|
+
MEDIA_ATTRIBUTE,
|
29
|
+
REL_ATTRIBUTE,
|
30
|
+
{
|
31
|
+
:target => String,
|
32
|
+
:hreflang => String,
|
33
|
+
:type => String,
|
34
|
+
},
|
35
|
+
],
|
36
|
+
},
|
37
|
+
:abbr => {},
|
38
|
+
:acronym => {},
|
39
|
+
:address => {},
|
40
|
+
:applet => {},
|
41
|
+
:area => {
|
42
|
+
:is_a => HtmlSingletonElement,
|
43
|
+
:attributes => [
|
44
|
+
HREF_ATTRIBUTE,
|
45
|
+
REL_ATTRIBUTE,
|
46
|
+
MEDIA_ATTRIBUTE,
|
47
|
+
{
|
48
|
+
:alt => String,
|
49
|
+
:coords => INTEGER_LIST,
|
50
|
+
:shape => %w(circle default poly rect),
|
51
|
+
:target => String,
|
52
|
+
:media => String,
|
53
|
+
:hreflang => String,
|
54
|
+
:type => String,
|
55
|
+
},
|
56
|
+
],
|
57
|
+
},
|
58
|
+
:article => {},
|
59
|
+
:aside => {},
|
60
|
+
:audio => {
|
61
|
+
:attributes => [
|
62
|
+
SRC_ATTRIBUTE,
|
63
|
+
CROSSORIGIN_ATTRIBUTE,
|
64
|
+
COMMON_MEDIA_ATTRIBUTES,
|
65
|
+
],
|
66
|
+
},
|
67
|
+
:b => {},
|
68
|
+
:base => {
|
69
|
+
:is_a => HtmlSingletonElement,
|
70
|
+
:attributes => [
|
71
|
+
HREF_ATTRIBUTE,
|
72
|
+
{ :target => String },
|
73
|
+
],
|
74
|
+
},
|
75
|
+
:basefont => {},
|
76
|
+
:bdo => {},
|
77
|
+
:bdi => {},
|
78
|
+
:big => {},
|
79
|
+
:blockquote => { :attributes => CITE_ATTRIBUTE },
|
80
|
+
:body => {
|
81
|
+
:is_a => HtmlSelfClosingElement,
|
82
|
+
:attributes => %w[
|
83
|
+
onafterprint
|
84
|
+
onbeforeprint
|
85
|
+
onbeforeunload
|
86
|
+
onblur
|
87
|
+
onerror
|
88
|
+
onfocus
|
89
|
+
onhashchange
|
90
|
+
onload
|
91
|
+
onmessage
|
92
|
+
onoffline
|
93
|
+
ononline
|
94
|
+
onpagehide
|
95
|
+
onpageshow
|
96
|
+
onpopstate
|
97
|
+
onresize
|
98
|
+
onscroll
|
99
|
+
onstorage
|
100
|
+
onunload
|
101
|
+
],
|
102
|
+
},
|
103
|
+
:br => {:is_a => HtmlSingletonElement},
|
104
|
+
:button => {
|
105
|
+
:attributes => [
|
106
|
+
FORM_ATTRIBUTES,
|
107
|
+
{
|
108
|
+
:autofocus => [true, false],
|
109
|
+
:disabled => [true, false],
|
110
|
+
:name => String,
|
111
|
+
:value => Object,
|
112
|
+
:type => %w[submit reset button],
|
113
|
+
},
|
114
|
+
],
|
115
|
+
},
|
116
|
+
:canvas => {
|
117
|
+
:attributes => DIMENSION_ATTRIBUTES,
|
118
|
+
},
|
119
|
+
:caption => {},
|
120
|
+
:center => {},
|
121
|
+
:cite => {},
|
122
|
+
:code => {},
|
123
|
+
:col => {
|
124
|
+
:is_a => HtmlSingletonElement,
|
125
|
+
:attributes => SPAN_ATTRIBUTE,
|
126
|
+
},
|
127
|
+
:colgroup => {
|
128
|
+
:is_a => HtmlSelfClosingElement,
|
129
|
+
:attributes => SPAN_ATTRIBUTE,
|
130
|
+
},
|
131
|
+
:command => {
|
132
|
+
:is_a => HtmlSingletonElement,
|
133
|
+
:attributes => {
|
134
|
+
:type => %w[command checkbox radio],
|
135
|
+
:label => String,
|
136
|
+
:icon => [String, URI],
|
137
|
+
:disabled => [true, false],
|
138
|
+
:radiogroup => String,
|
139
|
+
},
|
140
|
+
},
|
141
|
+
:datalist => {},
|
142
|
+
:dd => {:is_a => HtmlSelfClosingElement},
|
143
|
+
:del => {
|
144
|
+
:attributes => [
|
145
|
+
CITE_ATTRIBUTE,
|
146
|
+
DATETIME_ATTRIBUTE,
|
147
|
+
],
|
148
|
+
},
|
149
|
+
:details => { :attributes => { :open => [true, false] } },
|
150
|
+
:dfn => {},
|
151
|
+
:dir => {},
|
152
|
+
:div => {},
|
153
|
+
:dl => {},
|
154
|
+
:dt => {:is_a => HtmlSelfClosingElement},
|
155
|
+
:em => {},
|
156
|
+
:embed => {
|
157
|
+
:is_a => HtmlSingletonElement,
|
158
|
+
:attributes => {Object => Object }, # anything :/
|
159
|
+
},
|
160
|
+
:fieldset => {
|
161
|
+
:attributes => {
|
162
|
+
:disabled => [true, false],
|
163
|
+
:form => String,
|
164
|
+
:name => String,
|
165
|
+
},
|
166
|
+
},
|
167
|
+
:figcaption => {},
|
168
|
+
:figure => {},
|
169
|
+
:font => {},
|
170
|
+
:footer => {},
|
171
|
+
:form => {
|
172
|
+
:attributes => [
|
173
|
+
AUTOCOMPLETE_ATTRIBUTE,
|
174
|
+
{
|
175
|
+
'accept-charset' => String,
|
176
|
+
:action => [String, URI],
|
177
|
+
:enctype => ENC_TYPES,
|
178
|
+
:method => ['get', 'post'],
|
179
|
+
:name => String,
|
180
|
+
:novalidate => [true, false],
|
181
|
+
:target => String,
|
182
|
+
},
|
183
|
+
],
|
184
|
+
},
|
185
|
+
:frame => {},
|
186
|
+
:frameset => {},
|
187
|
+
:h1 => {},
|
188
|
+
:h2 => {},
|
189
|
+
:h3 => {},
|
190
|
+
:h4 => {},
|
191
|
+
:h5 => {},
|
192
|
+
:h6 => {},
|
193
|
+
:head => {:is_a => HtmlSelfClosingElement},
|
194
|
+
:header => {},
|
195
|
+
:hgroup => {},
|
196
|
+
:hr => {:is_a => HtmlSingletonElement},
|
197
|
+
:html => {:require => 'rxhp/tags/html_tag'},
|
198
|
+
:i => {},
|
199
|
+
:iframe => {
|
200
|
+
:attributes => [
|
201
|
+
SRC_ATTRIBUTE,
|
202
|
+
DIMENSION_ATTRIBUTES,
|
203
|
+
{
|
204
|
+
:srcdoc => String,
|
205
|
+
:name => String,
|
206
|
+
:sandbox => ['', token_list(%w[
|
207
|
+
allow-forms
|
208
|
+
allow-same-origin
|
209
|
+
allow-scripts
|
210
|
+
allow-top-navigation
|
211
|
+
])],
|
212
|
+
:seamless => [true, false],
|
213
|
+
},
|
214
|
+
],
|
215
|
+
},
|
216
|
+
:img => {
|
217
|
+
:is_a => HtmlSingletonElement,
|
218
|
+
:attributes => [
|
219
|
+
SRC_ATTRIBUTE,
|
220
|
+
DIMENSION_ATTRIBUTES,
|
221
|
+
CROSSORIGIN_ATTRIBUTE,
|
222
|
+
{
|
223
|
+
:alt => String,
|
224
|
+
:usemap => String,
|
225
|
+
:ismap => [true, false],
|
226
|
+
},
|
227
|
+
],
|
228
|
+
},
|
229
|
+
:input => {
|
230
|
+
:is_a => HtmlSingletonElement,
|
231
|
+
:attributes => [
|
232
|
+
AUTOCOMPLETE_ATTRIBUTE,
|
233
|
+
DIMENSION_ATTRIBUTES,
|
234
|
+
SRC_ATTRIBUTE,
|
235
|
+
FORM_ATTRIBUTES,
|
236
|
+
{
|
237
|
+
:accept => String,
|
238
|
+
:alt => String,
|
239
|
+
:autofocus => [true, false],
|
240
|
+
:checked => [true, false],
|
241
|
+
:dirname => String,
|
242
|
+
:disabled => [true, false],
|
243
|
+
:list => String,
|
244
|
+
:max => [Numeric, String],
|
245
|
+
:maxlength => Integer,
|
246
|
+
:min => [Numeric, String],
|
247
|
+
:multiple => [true, false],
|
248
|
+
:name => String,
|
249
|
+
:pattern => String, # ECMAScript RE != RegExp.to_s
|
250
|
+
:placeholder => String,
|
251
|
+
:required => [true, false],
|
252
|
+
:step => [Numeric, String],
|
253
|
+
:value => Object,
|
254
|
+
:type => %w[
|
255
|
+
hidden
|
256
|
+
text
|
257
|
+
search
|
258
|
+
tel
|
259
|
+
url
|
260
|
+
email
|
261
|
+
password
|
262
|
+
datetime
|
263
|
+
date
|
264
|
+
month
|
265
|
+
week
|
266
|
+
time
|
267
|
+
datetime-local
|
268
|
+
number
|
269
|
+
range
|
270
|
+
color
|
271
|
+
checkbox
|
272
|
+
radio
|
273
|
+
file
|
274
|
+
submit
|
275
|
+
image
|
276
|
+
reset
|
277
|
+
button
|
278
|
+
],
|
279
|
+
},
|
280
|
+
],
|
281
|
+
},
|
282
|
+
:ins => {
|
283
|
+
:attributes => [
|
284
|
+
CITE_ATTRIBUTE,
|
285
|
+
DATETIME_ATTRIBUTE,
|
286
|
+
],
|
287
|
+
},
|
288
|
+
:isindex => {},
|
289
|
+
:kbd => {},
|
290
|
+
:keygen => {
|
291
|
+
:is_a => HtmlSingletonElement,
|
292
|
+
:attributes => {
|
293
|
+
:autofocus => [true, false],
|
294
|
+
:challenge => String,
|
295
|
+
:disabled => [true, false],
|
296
|
+
:form => String,
|
297
|
+
:keytype => 'rsa',
|
298
|
+
:name => String,
|
299
|
+
},
|
300
|
+
},
|
301
|
+
:label => {
|
302
|
+
:attributes => {
|
303
|
+
:form => String,
|
304
|
+
:for => String,
|
305
|
+
},
|
306
|
+
},
|
307
|
+
:legend => {},
|
308
|
+
:li => {
|
309
|
+
:is_a => HtmlSelfClosingElement,
|
310
|
+
:attributes => {
|
311
|
+
:value => Integer,
|
312
|
+
},
|
313
|
+
},
|
314
|
+
:link => {
|
315
|
+
:is_a => HtmlSingletonElement,
|
316
|
+
:attributes => [
|
317
|
+
HREF_ATTRIBUTE,
|
318
|
+
REL_ATTRIBUTE,
|
319
|
+
{
|
320
|
+
:href_lang => String,
|
321
|
+
:type => String,
|
322
|
+
:sizes => [
|
323
|
+
'any',
|
324
|
+
/^\d+x\d+( \d+x\d+)*/,
|
325
|
+
],
|
326
|
+
},
|
327
|
+
],
|
328
|
+
},
|
329
|
+
:map => { :attributes => { :name => String } },
|
330
|
+
:mark => {},
|
331
|
+
:menu => {
|
332
|
+
:attributes => {
|
333
|
+
:type => ['context', 'toolbar'],
|
334
|
+
:label => String,
|
335
|
+
},
|
336
|
+
},
|
337
|
+
:meta => {
|
338
|
+
:is_a => HtmlSingletonElement,
|
339
|
+
:attributes => {
|
340
|
+
:name => String,
|
341
|
+
:content => String,
|
342
|
+
:charset => String,
|
343
|
+
'http-equiv' => %w(content-language content-type default-style refresh set-cookie),
|
344
|
+
},
|
345
|
+
},
|
346
|
+
:meter => {
|
347
|
+
:attributes => {
|
348
|
+
%w[value min max low high optimum] => [String, Numeric],
|
349
|
+
},
|
350
|
+
},
|
351
|
+
:nav => {},
|
352
|
+
:noframes => {},
|
353
|
+
:noscript => {},
|
354
|
+
:object => {
|
355
|
+
:attributes => [
|
356
|
+
DIMENSION_ATTRIBUTES,
|
357
|
+
{
|
358
|
+
:data => [String, URI],
|
359
|
+
:type => String,
|
360
|
+
:typemustmatch => [true, false],
|
361
|
+
:name => String,
|
362
|
+
:usemap => String,
|
363
|
+
:form => String,
|
364
|
+
},
|
365
|
+
],
|
366
|
+
},
|
367
|
+
:ol => {
|
368
|
+
:attributes => {
|
369
|
+
:reversed => [true, false],
|
370
|
+
:start => Integer,
|
371
|
+
:type => %w{1 a A i I},
|
372
|
+
},
|
373
|
+
},
|
374
|
+
:optgroup => {
|
375
|
+
:is_a => HtmlSelfClosingElement,
|
376
|
+
:attributes => {
|
377
|
+
:disabled => [true, false],
|
378
|
+
:label => String,
|
379
|
+
},
|
380
|
+
},
|
381
|
+
:option => {
|
382
|
+
:is_a => HtmlSelfClosingElement,
|
383
|
+
:attributes => {
|
384
|
+
:disabled => [true, false],
|
385
|
+
:label => String,
|
386
|
+
:selected => [true, false],
|
387
|
+
:value => Object,
|
388
|
+
},
|
389
|
+
},
|
390
|
+
:output => {
|
391
|
+
:attributes => {
|
392
|
+
:for => String,
|
393
|
+
:form => String,
|
394
|
+
:name => String,
|
395
|
+
},
|
396
|
+
},
|
397
|
+
:p => {:is_a => HtmlSelfClosingElement},
|
398
|
+
:param => {
|
399
|
+
:is_a => HtmlSingletonElement,
|
400
|
+
:attributes => {
|
401
|
+
:name => String,
|
402
|
+
:value => String,
|
403
|
+
},
|
404
|
+
},
|
405
|
+
:pre => {},
|
406
|
+
:progress => {
|
407
|
+
:attributes => {
|
408
|
+
:value => Object,
|
409
|
+
:max => [Numeric, String],
|
410
|
+
},
|
411
|
+
},
|
412
|
+
:rb => {},
|
413
|
+
:rt => {},
|
414
|
+
:ruby => {},
|
415
|
+
:q => { :attributes => CITE_ATTRIBUTE },
|
416
|
+
:s => {},
|
417
|
+
:samp => {},
|
418
|
+
:script => {
|
419
|
+
:attributes => [
|
420
|
+
SRC_ATTRIBUTE,
|
421
|
+
{
|
422
|
+
:async => [true, false],
|
423
|
+
:defer => [true, false],
|
424
|
+
:type => String,
|
425
|
+
:charset => String,
|
426
|
+
},
|
427
|
+
],
|
428
|
+
},
|
429
|
+
:section => {},
|
430
|
+
:select => {
|
431
|
+
:attributes => {
|
432
|
+
:autofocus => [true, false],
|
433
|
+
:disabled => [true, false],
|
434
|
+
:form => String,
|
435
|
+
:multiple => [true, false],
|
436
|
+
:name => String,
|
437
|
+
:required => [true, false],
|
438
|
+
:size => Integer,
|
439
|
+
},
|
440
|
+
},
|
441
|
+
:small => {},
|
442
|
+
:source => {
|
443
|
+
:is_a => HtmlSingletonElement,
|
444
|
+
:attributes => [
|
445
|
+
SRC_ATTRIBUTE,
|
446
|
+
MEDIA_ATTRIBUTE,
|
447
|
+
{ :type => String },
|
448
|
+
],
|
449
|
+
},
|
450
|
+
:span => {},
|
451
|
+
:strike => {},
|
452
|
+
:strong => {},
|
453
|
+
:style => {
|
454
|
+
:attributes => [
|
455
|
+
MEDIA_ATTRIBUTE,
|
456
|
+
{
|
457
|
+
:type => String,
|
458
|
+
:scoped => [true, false],
|
459
|
+
},
|
460
|
+
],
|
461
|
+
},
|
462
|
+
:sub => {},
|
463
|
+
:summary => {},
|
464
|
+
:sup => {},
|
465
|
+
:table => { :attributes => {:border => '1' } },
|
466
|
+
:tbody => {:is_a => HtmlSelfClosingElement},
|
467
|
+
:td => {
|
468
|
+
:is_a => HtmlSelfClosingElement,
|
469
|
+
:attributes => TABLE_CELL_ATTRIBUTES,
|
470
|
+
},
|
471
|
+
:textarea => {
|
472
|
+
:attributes => {
|
473
|
+
:autofocus => [true, false],
|
474
|
+
:cols => Integer,
|
475
|
+
:dirname => String,
|
476
|
+
:disabled => [true, false],
|
477
|
+
:form => String,
|
478
|
+
:maxlength => Integer,
|
479
|
+
:name => String,
|
480
|
+
:placeholder => String,
|
481
|
+
:readonly => [true, false],
|
482
|
+
:required => [true, false],
|
483
|
+
:rows => Integer,
|
484
|
+
:wrap => ['soft', 'hard'],
|
485
|
+
},
|
486
|
+
},
|
487
|
+
:tfoot => {:is_a => HtmlSelfClosingElement},
|
488
|
+
:th => {
|
489
|
+
:is_a => HtmlSelfClosingElement,
|
490
|
+
:attributes => [
|
491
|
+
TABLE_CELL_ATTRIBUTES,
|
492
|
+
{
|
493
|
+
:scope => %w(row col rowgroup colgroup),
|
494
|
+
},
|
495
|
+
],
|
496
|
+
},
|
497
|
+
:thead => {:is_a => HtmlSelfClosingElement},
|
498
|
+
:time => {
|
499
|
+
:attributes => DATETIME_ATTRIBUTE,
|
500
|
+
},
|
501
|
+
:title => {},
|
502
|
+
:tr => {:is_a => HtmlSelfClosingElement},
|
503
|
+
:track => {
|
504
|
+
:is_a => HtmlSingletonElement,
|
505
|
+
:attributes => [
|
506
|
+
SRC_ATTRIBUTE,
|
507
|
+
{
|
508
|
+
:kind => %w(subtitles captions descriptions chapters metadata),
|
509
|
+
:srclang => String,
|
510
|
+
:label => String,
|
511
|
+
:default => [true, false],
|
512
|
+
},
|
513
|
+
],
|
514
|
+
},
|
515
|
+
:tt => {},
|
516
|
+
:u => {},
|
517
|
+
:ul => {},
|
518
|
+
:var => {},
|
519
|
+
:video => {
|
520
|
+
:attributes => [
|
521
|
+
SRC_ATTRIBUTE,
|
522
|
+
CROSSORIGIN_ATTRIBUTE,
|
523
|
+
DIMENSION_ATTRIBUTES,
|
524
|
+
COMMON_MEDIA_ATTRIBUTES,
|
525
|
+
{
|
526
|
+
:poster => [String, URI],
|
527
|
+
},
|
528
|
+
],
|
529
|
+
},
|
530
|
+
:wbr => {:is_a => HtmlSingletonElement},
|
531
|
+
}
|
532
|
+
end
|
533
|
+
end
|
data/lib/rxhp/element.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'rxhp/constants'
|
2
|
+
require 'rxhp/scope'
|
3
|
+
|
4
|
+
module Rxhp
|
5
|
+
# Base class for all element-like things in RXHP.
|
6
|
+
#
|
7
|
+
# Everything in the tree is a subclass of this, a subclass of String,
|
8
|
+
# something that responds nicely to to_s, or something that will cause en
|
9
|
+
# error at render-time :p
|
10
|
+
class Element
|
11
|
+
include ::Rxhp::Scope
|
12
|
+
attr_accessor :attributes, :children
|
13
|
+
|
14
|
+
def initialize attributes = {}
|
15
|
+
@attributes = attributes
|
16
|
+
@children = Array.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def children?
|
20
|
+
!children.empty?
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return a flat HTML string for this element and all its' decendants.
|
24
|
+
#
|
25
|
+
# You probably don't want to implement this yourself - interesting
|
26
|
+
# implementations are in Rxhp::Fragment, Rxhp::HtmlElement, and
|
27
|
+
# Rxhp::ComposableElement
|
28
|
+
def render options = {}
|
29
|
+
raise NotImplementedError.new
|
30
|
+
end
|
31
|
+
|
32
|
+
# Called when something that isn't an element is found in the tree.
|
33
|
+
#
|
34
|
+
# Implemented in Rxhp::HtmlElement.
|
35
|
+
def render_string string, options
|
36
|
+
raise NotImplementedError.new
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
# Iterate over all the children, calling render.
|
41
|
+
def render_children options = {}
|
42
|
+
return if children.empty?
|
43
|
+
|
44
|
+
out = String.new
|
45
|
+
children.each do |child|
|
46
|
+
case child
|
47
|
+
when Element
|
48
|
+
out += child.render(options)
|
49
|
+
when String
|
50
|
+
out += self.render_string(child, options)
|
51
|
+
else
|
52
|
+
out += self.render_string(child.to_s, options)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
out
|
56
|
+
end
|
57
|
+
|
58
|
+
# Fill default options
|
59
|
+
def fill_options options
|
60
|
+
{
|
61
|
+
:pretty => true,
|
62
|
+
:format => Rxhp::HTML_FORMAT,
|
63
|
+
:skip_doctype => false,
|
64
|
+
:doctype => Rxhp::HTML_5,
|
65
|
+
:depth => 0,
|
66
|
+
:indent => 2,
|
67
|
+
}.merge(options)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/rxhp/error.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rxhp/element'
|
2
|
+
|
3
|
+
module Rxhp
|
4
|
+
# Fake element that only renders its' children.
|
5
|
+
#
|
6
|
+
# Can be used like an array, or if you just need something that acts like
|
7
|
+
# an element - this is used internally as the root of all render trees.
|
8
|
+
class Fragment < Element
|
9
|
+
def render options = {}
|
10
|
+
self.render_children(options)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/rxhp/html.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'rxhp/data/html/attributes'
|
2
|
+
require 'rxhp/data/html/tags'
|
3
|
+
|
4
|
+
module Rxhp
|
5
|
+
module Html
|
6
|
+
def fragment x
|
7
|
+
Rxhp::Scope.current.children.push x
|
8
|
+
end
|
9
|
+
alias :frag :fragment
|
10
|
+
alias :text :fragment
|
11
|
+
|
12
|
+
class <<self
|
13
|
+
def fragment x
|
14
|
+
Rxhp::Scope.current.children.push x
|
15
|
+
end
|
16
|
+
alias :frag :fragment
|
17
|
+
alias :text :fragment
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
Rxhp::Html::TAGS.each do |tag, data|
|
23
|
+
if data[:require]
|
24
|
+
require data[:require]
|
25
|
+
else
|
26
|
+
data = {
|
27
|
+
:is_a => Rxhp::HtmlElement,
|
28
|
+
}.merge(data)
|
29
|
+
|
30
|
+
klass_name = tag.to_s.dup
|
31
|
+
klass_name[0] = klass_name[0,1].upcase
|
32
|
+
klass = Class.new(data[:is_a])
|
33
|
+
klass.send(:define_method, :tag_name) { tag.to_s }
|
34
|
+
|
35
|
+
if data[:attributes]
|
36
|
+
klass.accept_attributes data[:attributes]
|
37
|
+
end
|
38
|
+
|
39
|
+
Rxhp::Html.const_set(klass_name, klass)
|
40
|
+
|
41
|
+
Rxhp::Scope.define_element tag, klass, Rxhp::Html
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'rxhp/element'
|
2
|
+
require 'rxhp/attribute_validator'
|
3
|
+
|
4
|
+
require 'rxhp/data/html/attributes'
|
5
|
+
|
6
|
+
module Rxhp
|
7
|
+
# Base class for 'real' elements that actually end up in the markup.
|
8
|
+
#
|
9
|
+
# For example, <span> is a subclass of this.
|
10
|
+
#
|
11
|
+
# To use:
|
12
|
+
# 1. subclass
|
13
|
+
# 2. define tag_name
|
14
|
+
# 3. call Rxhp::Scope.define_element('foo', Foo) to register the class
|
15
|
+
# ... or just add a define_tag line to html.rb.
|
16
|
+
#
|
17
|
+
# There's another two base classes for special types of html elements:
|
18
|
+
# - HtmlSelfClosingElement - elements where in HTML, the closing tag is
|
19
|
+
# optional - for example, <p>, <li>, and <body>
|
20
|
+
# - HtmlSingletonElement - not only is the closing tag optional, but
|
21
|
+
# child elements are forbidden - for example, <br> and <img>
|
22
|
+
#
|
23
|
+
# These can also be defined via Rxhp::Html#define_tag
|
24
|
+
#
|
25
|
+
# If you're making a custom element that's purely server-side (i.e. is
|
26
|
+
# just composed of HTML elements), you want to subclass ComposableElement
|
27
|
+
# instead.
|
28
|
+
class HtmlElement < Element
|
29
|
+
include Rxhp::AttributeValidator
|
30
|
+
accept_attributes Rxhp::Html::GLOBAL_ATTRIBUTES
|
31
|
+
accept_attributes Rxhp::Html::GLOBAL_EVENT_HANDLERS
|
32
|
+
|
33
|
+
def initialize *args
|
34
|
+
super *args
|
35
|
+
validate_attributes!
|
36
|
+
end
|
37
|
+
|
38
|
+
def tag_name
|
39
|
+
raise NotImplementedError.new
|
40
|
+
end
|
41
|
+
|
42
|
+
# Render the element.
|
43
|
+
#
|
44
|
+
# Pays attention to the formatter type, doctype, pretty print options,
|
45
|
+
# etc.
|
46
|
+
def render options = {}
|
47
|
+
validate_attributes!
|
48
|
+
options = fill_options(options)
|
49
|
+
|
50
|
+
open = render_open_tag(options)
|
51
|
+
inner = render_children(options)
|
52
|
+
close = render_close_tag(options)
|
53
|
+
|
54
|
+
if options[:pretty]
|
55
|
+
indent = ' ' * (options[:indent] * options[:depth])
|
56
|
+
out = "%s%s\n" % [indent, open]
|
57
|
+
out += inner if inner
|
58
|
+
out += "%s%s\n" % [indent, close] if close && !close.empty?
|
59
|
+
out
|
60
|
+
else
|
61
|
+
out = open
|
62
|
+
out += inner if inner
|
63
|
+
out += close if close
|
64
|
+
out
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Override to increase the depth count for the sake of pretty printing
|
69
|
+
def render_children options = {}
|
70
|
+
child_options = options.dup
|
71
|
+
child_options[:depth] += 1
|
72
|
+
super child_options
|
73
|
+
end
|
74
|
+
|
75
|
+
protected
|
76
|
+
|
77
|
+
# html-escape a string, paying attention to indentation too.
|
78
|
+
def render_string string, options
|
79
|
+
escaped = html_escape(string)
|
80
|
+
if options[:pretty]
|
81
|
+
indent = ' ' * (options[:indent] * options[:depth])
|
82
|
+
indent + escaped + "\n"
|
83
|
+
else
|
84
|
+
escaped
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Render the opening tag.
|
89
|
+
#
|
90
|
+
# Considers:
|
91
|
+
# - attributes
|
92
|
+
# - XHTML or HTML?
|
93
|
+
# - are there any children?
|
94
|
+
#
|
95
|
+
# #render_close_tag assumes that this will not leave a tag open in XHTML
|
96
|
+
# unless there are children.
|
97
|
+
def render_open_tag options
|
98
|
+
out = '<' + tag_name
|
99
|
+
unless attributes.empty?
|
100
|
+
attributes.each do |name,value|
|
101
|
+
case value
|
102
|
+
when false
|
103
|
+
next
|
104
|
+
when true
|
105
|
+
if options[:format] == Rxhp::XHTML_FORMAT
|
106
|
+
out += ' ' + name.to_s + '="' + name.to_s + '"'
|
107
|
+
else
|
108
|
+
out += ' ' + name
|
109
|
+
end
|
110
|
+
else
|
111
|
+
out += ' ' + name.to_s + '="' + html_escape(value.to_s) + '"'
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
if options[:format] == Rxhp::XHTML_FORMAT && !children?
|
117
|
+
out + ' />'
|
118
|
+
else
|
119
|
+
out + '>'
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Render the closing tag.
|
124
|
+
#
|
125
|
+
# Assumes that #render_open_tag would have made a self-closing tag if
|
126
|
+
# appropriate.
|
127
|
+
#
|
128
|
+
# This is overriden in:
|
129
|
+
# - HtmlSingletonElement: never any children, so never a closing tag
|
130
|
+
# - HtmlSelfClosingElement: calls this, /unless/ tiny html output is
|
131
|
+
# selected, in which case the closing tag is skipped.
|
132
|
+
def render_close_tag options
|
133
|
+
if options[:format] != Rxhp::XHTML_FORMAT || children?
|
134
|
+
'</' + tag_name + '>'
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Don't pull in ActiveSupport just for this...
|
139
|
+
def html_escape s
|
140
|
+
s.gsub('&','&').gsub('<','<').gsub('>','>').gsub('"','"')
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rxhp/html_element'
|
2
|
+
|
3
|
+
module Rxhp
|
4
|
+
# Base class for HTML elements where the closing tag is optional, but
|
5
|
+
# there can still be children.
|
6
|
+
#
|
7
|
+
# For example, </p> is optional in HTML, but required in XHTML.
|
8
|
+
# This will change whether they're included or not based on the selected
|
9
|
+
# render format.
|
10
|
+
class HtmlSelfClosingElement < HtmlElement
|
11
|
+
protected
|
12
|
+
def render_close_tag options
|
13
|
+
super if options[:format] != Rxhp::TINY_HTML_FORMAT
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rxhp/html_element'
|
2
|
+
require 'rxhp/error'
|
3
|
+
|
4
|
+
module Rxhp
|
5
|
+
# Superclass for all HTML elements that should never have children.
|
6
|
+
#
|
7
|
+
# This is enforced. Examples include '<br>', '<img>' etc
|
8
|
+
#
|
9
|
+
# There is never a close tag:
|
10
|
+
# - in HTML it would be optional (and meaningless) anyway
|
11
|
+
# - people don't expect to see them
|
12
|
+
# - in XHTML, the opening tag will have been self closing.
|
13
|
+
class HtmlSingletonElement < HtmlElement
|
14
|
+
def render *args
|
15
|
+
unless children.empty?
|
16
|
+
raise Rxhp::ScriptError.new('Singleton element has children')
|
17
|
+
end
|
18
|
+
super *args
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
def render_close_tag options
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/rxhp/scope.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
module Rxhp
|
2
|
+
autoload :Fragment, 'rxhp/fragment'
|
3
|
+
# A place for factory methods to be defined.
|
4
|
+
#
|
5
|
+
# These are methods like Rxhp::Scope#h1 which creates an Rxhp::Html::H1
|
6
|
+
# instance.
|
7
|
+
#
|
8
|
+
# The actual HTML classes are defined in rxhp/html.rb
|
9
|
+
#
|
10
|
+
module Scope
|
11
|
+
# Helper function to append a child to the current context.
|
12
|
+
# Allows you to do this:
|
13
|
+
# inner = body { 'hi' }
|
14
|
+
# html do
|
15
|
+
# fragment inner
|
16
|
+
# end
|
17
|
+
def fragment x
|
18
|
+
Rxhp::Scope.current.children.push x
|
19
|
+
end
|
20
|
+
alias :frag :fragment
|
21
|
+
alias :text :fragment
|
22
|
+
|
23
|
+
def self.current
|
24
|
+
self.stack.last || Rxhp::Fragment.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.with_parent parent
|
28
|
+
result = nil
|
29
|
+
begin
|
30
|
+
self.stack.push parent
|
31
|
+
result = yield
|
32
|
+
ensure
|
33
|
+
self.stack.pop
|
34
|
+
end
|
35
|
+
result
|
36
|
+
end
|
37
|
+
|
38
|
+
# Define the factory method.
|
39
|
+
#
|
40
|
+
# can be called like:
|
41
|
+
# tag 'content'
|
42
|
+
# tag { content }
|
43
|
+
# tag 'content', :attribute => 'value'
|
44
|
+
# tag(:attribute=>'value') do
|
45
|
+
# content
|
46
|
+
# end
|
47
|
+
# tag
|
48
|
+
# tag (:attribute => value)
|
49
|
+
def self.define_element name, klass, namespace
|
50
|
+
impl = Proc.new do |*args, &block|
|
51
|
+
# Yay for faking named parameters as a hash :p
|
52
|
+
children = nil
|
53
|
+
attributes = {}
|
54
|
+
args.each do |arg|
|
55
|
+
if arg.is_a?(Hash)
|
56
|
+
attributes = arg
|
57
|
+
else
|
58
|
+
children ||= []
|
59
|
+
children.push arg
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Create the actual element
|
64
|
+
element = klass.new(attributes)
|
65
|
+
Rxhp::Scope.current.children.push element
|
66
|
+
|
67
|
+
# Append non-block children
|
68
|
+
if children
|
69
|
+
if children.is_a? Array
|
70
|
+
children.each do |child|
|
71
|
+
element.children.push child
|
72
|
+
end
|
73
|
+
else
|
74
|
+
element.children.push children # well, child
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
if block
|
79
|
+
Rxhp::Scope.with_parent(element) do
|
80
|
+
if block.call.is_a? String
|
81
|
+
raise Rxhp::ScriptError.new(
|
82
|
+
"In a block, use the 'text' method to include Strings"
|
83
|
+
)
|
84
|
+
end
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
end
|
88
|
+
element
|
89
|
+
end
|
90
|
+
|
91
|
+
# Instance method if mixed in.
|
92
|
+
#
|
93
|
+
# Usage:
|
94
|
+
# include Rxhp::Html
|
95
|
+
# html do
|
96
|
+
# ...
|
97
|
+
# end
|
98
|
+
namespace.send(:define_method, name, impl)
|
99
|
+
# Class method for fully-qualified.
|
100
|
+
#
|
101
|
+
# Usage:
|
102
|
+
# Rxhp::Html.html do
|
103
|
+
# ...
|
104
|
+
# end
|
105
|
+
(class <<namespace; self; end).send(:define_method, name, impl)
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def self.stack
|
111
|
+
Thread.current[:rxhp_scope_stack] ||= []
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rxhp/html_self_closing_element'
|
2
|
+
require 'rxhp/scope'
|
3
|
+
|
4
|
+
module Rxhp
|
5
|
+
module Html
|
6
|
+
class Html < HtmlSelfClosingElement
|
7
|
+
def tag_name; 'html'; end
|
8
|
+
|
9
|
+
protected
|
10
|
+
def render_open_tag options
|
11
|
+
if options[:skip_doctype]
|
12
|
+
super
|
13
|
+
else
|
14
|
+
options[:doctype] + super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
Rxhp::Scope.define_element('html', Rxhp::Html::Html, Rxhp::Html)
|
data/lib/rxhp.rb
ADDED
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rxhp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Fred Emmott
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-01-21 00:00:00.000000000Z
|
13
|
+
dependencies: []
|
14
|
+
description: An object-oriented validating HTML template system
|
15
|
+
email:
|
16
|
+
- rxhp-gem@fredemmott.co.uk
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- lib/rxhp/attribute_validator.rb
|
22
|
+
- lib/rxhp/composable_element.rb
|
23
|
+
- lib/rxhp/constants.rb
|
24
|
+
- lib/rxhp/data/html/attributes.rb
|
25
|
+
- lib/rxhp/data/html/tags.rb
|
26
|
+
- lib/rxhp/element.rb
|
27
|
+
- lib/rxhp/error.rb
|
28
|
+
- lib/rxhp/fragment.rb
|
29
|
+
- lib/rxhp/html.rb
|
30
|
+
- lib/rxhp/html_element.rb
|
31
|
+
- lib/rxhp/html_self_closing_element.rb
|
32
|
+
- lib/rxhp/html_singleton_element.rb
|
33
|
+
- lib/rxhp/scope.rb
|
34
|
+
- lib/rxhp/tags/html_tag.rb
|
35
|
+
- lib/rxhp.rb
|
36
|
+
homepage: https://github.com/fredemmott/rxhp
|
37
|
+
licenses: []
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
requirements: []
|
55
|
+
rubyforge_project:
|
56
|
+
rubygems_version: 1.8.6
|
57
|
+
signing_key:
|
58
|
+
specification_version: 3
|
59
|
+
summary: An object-oriented validating HTML template system
|
60
|
+
test_files: []
|