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.
- 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: []
|