html-native 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/html-native.rb +60 -0
- data/lib/html-native/builder.rb +27 -0
- data/lib/html-native/collections.rb +197 -0
- data/lib/html-native/constants.rb +140 -0
- metadata +46 -0
checksums.yaml
ADDED
@@ -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
|
data/lib/html-native.rb
ADDED
@@ -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: []
|