html-native 0.1.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2ec4658fd7a7e2a040535c411ca661cc80291d705b59099c93b6eecd184fb35e
4
+ data.tar.gz: e96476a674a70ad4e28ba9ab223057bbc3bbdcab00ae91d1ff39c1788e383131
5
+ SHA512:
6
+ metadata.gz: 85deca5f040b4268a9b3cf533483f2fbe91de270e5b6dbaf35817cea6363e49e008863481d24eea85c9c18123715caf21cfbe08f2f2c872cd844866ab95c956f
7
+ data.tar.gz: 7eca06fcf63a3159af0e2de5c4502ad5d969707de4c3f0221df07ca73b9d0b49e224edae049d8c6a5f59c1f390664c56221c9b74bc499ee4e213817c100ac6e6
@@ -0,0 +1,60 @@
1
+ require "html-native/constants"
2
+ require "html-native/builder"
3
+
4
+ module HTMLComponent
5
+
6
+ # Excluded currently because it makes checking in `Builder` ugly
7
+ # Makes `include` and `extend` work exactly the same.
8
+ # It's a dirty hack based on laziness, and strict use of `extend` is preferred.
9
+ # def self.included(base)
10
+ # base.extend(self)
11
+ # end
12
+
13
+ # Generates generation methods for each HTML5-valid tag. These methods have the
14
+ # name of the tag. Note that this interferes with the builtin `p` method.
15
+ TAG_LIST.each do |tag|
16
+ HTMLComponent.define_method(tag) do |attrs = {}, &block|
17
+ attrs ||= {}
18
+ if block
19
+ body = block.call
20
+ Builder.new("<#{tag}#{attributes_list(tag, attrs)}>") + body + "</#{tag}>"
21
+ else
22
+ Builder.new("<#{tag}#{attributes_list(tag, attrs)}/>")
23
+ end
24
+ end
25
+ end
26
+
27
+ def self.singleton(&block)
28
+ Module.new do
29
+ extend HTMLComponent
30
+ define_singleton_method :render, &block
31
+ end
32
+ end
33
+
34
+ # Checks if the attribute is valid for a given tag.
35
+ #
36
+ # For example, `class` and `hidden` are valid for everything, but `autoplay`
37
+ # is valid for only `video` and `audio` tags, and invalid for all other tags.
38
+ def valid_attribute?(tag, attribute)
39
+ if LIMITED_ATTRIBUTES.key?(attribute.to_sym)
40
+ return LIMITED_ATTRIBUTES[attribute.to_sym].include?(tag.to_sym)
41
+ end
42
+ return !FORBIDDEN_ATTRIBUTES.include?(attribute)
43
+ end
44
+
45
+ private
46
+
47
+ # Given a tag and a set of attributes as a hash, format the attributes to
48
+ # HTML-valid form. If an attribute doesn't have a value or the value is
49
+ # empty, it's treated as a boolean attribute and formatted as such.
50
+ def attributes_list(tag, attrs)
51
+ formatted = attrs.filter{|opt, value| valid_attribute?(tag, opt)}.map do |k,v|
52
+ if v&.to_s.empty?
53
+ k.to_s
54
+ else
55
+ "#{k}=\"#{v}\"" # render this appropriately for numeric fields (might already)
56
+ end
57
+ end.join(" ")
58
+ formatted.empty? ? "" : " " + formatted
59
+ end
60
+ end
@@ -0,0 +1,27 @@
1
+ require "html-native"
2
+ module HTMLComponent
3
+ class Builder
4
+ def initialize(strings = [])
5
+ if strings.kind_of? String
6
+ @strings = [strings]
7
+ else
8
+ @strings = strings.dup
9
+ end
10
+ end
11
+
12
+ def +(string)
13
+ if string.kind_of? Builder
14
+ @strings << string
15
+ elsif string.kind_of? HTMLComponent
16
+ @strings << string.render
17
+ else
18
+ @strings << string.to_s
19
+ end
20
+ self
21
+ end
22
+
23
+ def to_s
24
+ @strings.join
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,197 @@
1
+ require "html-native"
2
+ require "html-native/builder"
3
+
4
+ module Enumerable
5
+ def component_map
6
+ if block_given?
7
+ result = HTMLComponent::Builder.new
8
+ each do |e|
9
+ result += yield(e)
10
+ end
11
+ result
12
+ else
13
+ to_enum(:component_map)
14
+ end
15
+ end
16
+
17
+ def to_ol(attributes: {})
18
+ OrderedListComponent.new(self, attributes: attributes)
19
+ end
20
+
21
+ def to_ul(attributes: {})
22
+ UnorderedListComponent.new(self, attributes: attributes)
23
+ end
24
+ end
25
+
26
+ class OrderedListComponent
27
+ include HTMLComponent
28
+
29
+ def initialize(data, attributes: {})
30
+ @list_data = data
31
+ @list_attributes = attributes[:list] || {}
32
+ @item_attributes = attributes[:item] || {}
33
+ end
34
+
35
+ def render(&block)
36
+ ol(@list_attributes) do
37
+ @list_data.component_map do |l|
38
+ li(@item_attributes) do
39
+ block_given? ? yield(l) : l.to_s
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ class UnorderedListComponent
47
+ include HTMLComponent
48
+
49
+ def initialize(data, attributes: {})
50
+ @list_data = data
51
+ @list_attributes = attributes[:list] || {}
52
+ @item_attributes = attributes[:item] || {}
53
+ end
54
+
55
+ def render(&block)
56
+ ul(@list_attributes) do
57
+ @list_data.component_map do |l|
58
+ li(@item_attributes) do
59
+ block_given? ? yield(l) : l.to_s
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ class ListComponent
67
+ def initialize(data, attributes: {}, ordered: false)
68
+ @list = ordered ? OrderedListComponent.new(data, attributes) :
69
+ UnorderedListComponent.new(data, attributes)
70
+ end
71
+
72
+ def render(&block)
73
+ @list.render(&block)
74
+ end
75
+ end
76
+
77
+ class TableRowComponent
78
+ include HTMLComponent
79
+
80
+ def initialize(data, attributes: {})
81
+ @data = data
82
+ @row_attributes = attributes[:row] || {}
83
+ @cell_attributes = attributes[:cell] || {}
84
+ end
85
+
86
+ def render(&block)
87
+ tr(@row_attributes) do
88
+ @data.component_map do |c|
89
+ td(@cell_attributes) {block_given? ? yield(c) : c}
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ # This needs some reworking, since it's not intuitive
96
+ class TableComponent
97
+ include HTMLComponent
98
+
99
+ def initialize(header, rows, attributes: {})
100
+ @header = header
101
+ @rows = rows
102
+ @table_attributes = attributes[:table] || {}
103
+ @header_attributes = attributes[:header] || {}
104
+ @header_cell_attributes = attributes[:header_cell] || {}
105
+ @row_attributes = attributes[:row] || {}
106
+ @cell_attributes = attributes[:cell] || {}
107
+ end
108
+
109
+ # header options:
110
+ # array - use as header
111
+ # symbol - if :from_data, then use first row, if :none, set @header to nil
112
+ def self.from_array(data, attributes: {}, header: :none)
113
+ head = rows = nil
114
+ if header == :from_data
115
+ head = data[0]
116
+ rows = data[1..]
117
+ else
118
+ head = header.kind_of?(Array) ? header : nil
119
+ rows = data
120
+ end
121
+ new(head, rows, attributes: attributes)
122
+ end
123
+
124
+ def self.from_hash(data, attributes: {}, vertical: true)
125
+ if vertical
126
+ rowcount = data.values.map(&:length).max
127
+ rows = [] * rowcount
128
+ data.each do |k,col|
129
+ rowcount.times do |i|
130
+ rows[i] << (i < col.size ? col[i] : nil)
131
+ end
132
+ end
133
+ new(data.keys, rows, attributes: attributes)
134
+ else
135
+ rows = data.map do |k, v|
136
+ [k] + v
137
+ end
138
+ new(nil, rows, attributes: attributes)
139
+ end
140
+ end
141
+
142
+ def render(&block)
143
+ table(@table_attributes) do
144
+ if @header
145
+ tr(@row_attributes.merge(@header_attributes)) do
146
+ @header.component_map do |h|
147
+ th(@cell_attributes.merge(@header_cell_attributes)) {h}
148
+ end
149
+ end
150
+ else
151
+ Builder.new
152
+ end +
153
+ @rows.component_map do |row|
154
+ TableRowComponent.new(row, attributes: {row: @row_attributes, cell: @cell_attributes}).render(&block)
155
+ end
156
+ end
157
+ end
158
+ end
159
+
160
+ class DropdownComponent
161
+ include HTMLComponent
162
+
163
+ def initialize(choices, name, attributes: {})
164
+ @choices = choices
165
+ @name = name
166
+ @menu_attributes = attributes[:menu]
167
+ @item_attributes = attributes[:item]
168
+ end
169
+
170
+ def render(&block)
171
+ select(@menu_attributes.merge({name: @name, id: "#{@name}-dropdown"})) do
172
+ @choices.component_map do |c|
173
+ option(@item_attributes.merge({value: c})) {block_given? ? yield(c) : c}
174
+ end
175
+ end
176
+ end
177
+ end
178
+
179
+ class RadioGroupComponent
180
+ include HTMLComponent
181
+
182
+ def initialize(choices, name, attributes: {}, labelled: true)
183
+ @choices = choices
184
+ @name = name
185
+ @button_attributes = attributes[:button]
186
+ @label_attributes = attributes[:label]
187
+ @labelled = labelled
188
+ end
189
+
190
+ def render(&block)
191
+ @choices.component_map do |c|
192
+ id = "#{@name}-#{c}"
193
+ input({type: "radio", id: id, name: @name, value: c}) +
194
+ (@labelled ? (label({for: id}) {block_given? ? yield(c) : c}) : nil)
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,140 @@
1
+ module HTMLComponent
2
+ TAG_LIST = [
3
+ :html,
4
+ :base, :head, :link, :meta, :style, :title,
5
+ :body,
6
+ :address, :article, :aside, :footer, :h1, :h2, :h3, :h4, :h5, :h6, :header,
7
+ :hgroup, :main, :nav, :section,
8
+ :blockquote, :dd, :div, :dl, :dt, :figcaption, :figure, :hr, :li, :ol, :p, :pre, :ul,
9
+ :a, :abbr, :b, :bdi, :bdo, :br, :cite, :code, :data, :dfn, :em, :i, :kbd, :mark, :q,
10
+ :rb, :rp, :rt, :rtc, :ruby, :s, :samp, :small, :span, :strong, :sub, :sup, :time,
11
+ :u, :var, :wbr,
12
+ :area, :audio, :img, :map, :track, :video,
13
+ :embed, :iframe, :object, :param, :picture, :portal, :source,
14
+ :svg, :math,
15
+ :canvas, :noscript, :script,
16
+ :del, :ins,
17
+ :caption, :col, :colgroup, :table, :tbody, :td, :tfoot, :th, :thead, :tr,
18
+ :button, :datalist, :fieldset, :form, :input, :label, :legend, :meter, :optgroup,
19
+ :option, :output, :progress, :select, :textarea,
20
+ :details, :dialog, :menu, :summary,
21
+ :slot, :template
22
+ ] + (1..6).map{|i| :"h#{i}"}
23
+
24
+ def self.tags
25
+ TAG_LIST.dup
26
+ end
27
+
28
+ LIMITED_ATTRIBUTES = {
29
+ accept: [:form, :input],
30
+ "accept-charset": [:form],
31
+ action: [:form],
32
+ align: [:caption, :col, :colgroup, :hr, :iframe, :img, :table,
33
+ :tbody, :td, :tfoot, :th, :thead, :tr],
34
+ allow: [:iframe],
35
+ alt: [:area, :img, :input],
36
+ async: [:script],
37
+ autocomplete: [:form, :input, :select, :textarea],
38
+ autofocus: [:button, :input, :select, :textarea],
39
+ autoplay: [:audio, :video],
40
+ buffered: [:audio, :video],
41
+ capture: [:input],
42
+ charset: [:meta, :script],
43
+ checked: [:input],
44
+ cite: [:blockquote, :del, :ins, :q],
45
+ cols: [:textarea],
46
+ colspan: [:td, :th],
47
+ content: [:meta],
48
+ controls: [:audio, :video],
49
+ coords: [:area],
50
+ crossorigin: [:audio, :img, :link, :script, :video],
51
+ csp: [:iframe],
52
+ data: [:object],
53
+ datatime: [:del, :ins, :time],
54
+ decoding: [:img],
55
+ default: [:track],
56
+ defer: [:script],
57
+ dirname: [:input, :textarea],
58
+ disabled: [:button, :fieldset, :input, :optgroup,
59
+ :option, :select, :textarea],
60
+ download: [:a, :area],
61
+ enctype: [:form],
62
+ enterkeyhint: [:textarea],
63
+ "for": [:label, :output],
64
+ form: [:button, :fieldset, :input, :label, :meter, :object,
65
+ :output, :progress, :select, :textarea],
66
+ formaction: [:input, :button],
67
+ formentype: [:button, :input],
68
+ formmethod: [:button, :input],
69
+ formnovalidate: [:button, :input],
70
+ formtarget: [:button, :input],
71
+ headers: [:td, :th],
72
+ height: [:canvas, :embed, :iframe, :img, :input, :object, :video],
73
+ high: [:meter],
74
+ href: [:a, :area, :base, :link],
75
+ hreflang: [:a, :area, :link],
76
+ "http-equiv": [:meta],
77
+ importance: [:iframe, :img, :link, :script],
78
+ integrity: [:link, :script],
79
+ inputmode: [:textarea],
80
+ ismap: [:img],
81
+ kind: [:track],
82
+ label: [:optgroup, :option, :track],
83
+ language: [:script],
84
+ loading: [:img, :iframe],
85
+ list: [:input],
86
+ loop: [:audio, :video],
87
+ low: [:meter],
88
+ max: [:input, :meter, :progress],
89
+ maxlength: [:input, :textarea],
90
+ minlength: [:input, :textarea],
91
+ media: [:a, :area, :link, :source, :style],
92
+ method: [:form],
93
+ min: [:input, :select],
94
+ multiple: [:input, :select],
95
+ muted: [:audio, :video],
96
+ name: [:button, :form, :fieldset, :iframe, :input, :object,
97
+ :output, :select, :textarea, :map, :meta, :param],
98
+ novalidate: [:form],
99
+ open: [:details],
100
+ optimum: [:meter],
101
+ pattern: [:input],
102
+ ping: [:a, :area],
103
+ placeholder: [:input, :textarea],
104
+ poster: [:video],
105
+ preload: [:audio, :video],
106
+ readonly: [:input, :textarea],
107
+ referrerpolicy: [:a, :area, :iframe, :img, :link, :script],
108
+ rel: [:a, :area, :link],
109
+ required: [:input, :select, :textarea],
110
+ reversed: [:ol],
111
+ rows: [:textarea],
112
+ rowspan: [:td, :th],
113
+ sandbox: [:iframe],
114
+ scope: [:th],
115
+ scoped: [:style],
116
+ selected: [:option],
117
+ shape: [:a, :area],
118
+ size: [:input, :select],
119
+ sizes: [:link, :img, :source],
120
+ span: [:col, :colgroup],
121
+ src: [:audio, :embed, :iframe, :img, :input, :script, :source, :track, :video],
122
+ srcdoc: [:iframe],
123
+ srclang: [:track],
124
+ srcset: [:img, :source],
125
+ start: [:ol],
126
+ step: [:input],
127
+ summary: [:table],
128
+ target: [:a, :area, :base, :form],
129
+ type: [:button, :input, :embed, :object, :script, :source, :style, :menu],
130
+ usemap: [:img, :input, :object],
131
+ value: [:button, :data, :input, :li, :meter, :option, :progress, :param],
132
+ width: [:canvas, :embed, :iframe, :img, :input, :object, :video],
133
+ wrap: [:textarea]
134
+ }
135
+
136
+ # These attributes are deprecated or outright forbidden. However, some people
137
+ # might still try to use them. These attributes are expressly disallowed
138
+ # during generation, and won't be included, even if provided.
139
+ FORBIDDEN_ATTRIBUTES = [:background, :bgcolor, :border, :color, :manifest]
140
+ end
metadata ADDED
@@ -0,0 +1,46 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: html-native
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kellen Watt
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-01-25 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: An html generation DSL designed for fluid code creation.
14
+ email:
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/html-native.rb
20
+ - lib/html-native/builder.rb
21
+ - lib/html-native/collections.rb
22
+ - lib/html-native/constants.rb
23
+ homepage: https://github.com/KellenWatt/html-native
24
+ licenses:
25
+ - MIT
26
+ metadata: {}
27
+ post_install_message:
28
+ rdoc_options: []
29
+ require_paths:
30
+ - lib
31
+ required_ruby_version: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ required_rubygems_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ requirements: []
42
+ rubygems_version: 3.1.4
43
+ signing_key:
44
+ specification_version: 4
45
+ summary: Ruby-native html generation
46
+ test_files: []