rxhp 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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,7 @@
1
+ module Rxhp
2
+ class Error < ::StandardError
3
+ end
4
+
5
+ class ScriptError < ::ScriptError
6
+ end
7
+ end
@@ -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('&','&amp;').gsub('<','&lt;').gsub('>','&gt;').gsub('"','&quot;')
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
@@ -0,0 +1,4 @@
1
+ require 'rxhp/constants'
2
+ require 'rxhp/error'
3
+ require 'rxhp/composable_element'
4
+ require 'rxhp/html'
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: []