phlex 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of phlex might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.rubocop.yml +4 -0
- data/Gemfile +15 -3
- data/README.md +14 -30
- data/Rakefile +4 -6
- data/bench.rb +14 -0
- data/config.ru +9 -0
- data/docs/assets/application.css +24 -0
- data/docs/assets/logo.png +0 -0
- data/docs/build.rb +22 -0
- data/docs/components/callout.rb +9 -0
- data/docs/components/code_block.rb +26 -0
- data/docs/components/example.rb +32 -0
- data/docs/components/heading.rb +9 -0
- data/docs/components/layout.rb +45 -0
- data/docs/components/markdown.rb +26 -0
- data/docs/components/tabs/tab.rb +26 -0
- data/docs/components/tabs.rb +30 -0
- data/docs/components/title.rb +9 -0
- data/docs/page_builder.rb +36 -0
- data/docs/pages/application_page.rb +7 -0
- data/docs/pages/components.rb +175 -0
- data/docs/pages/index.rb +40 -0
- data/docs/pages/templates.rb +242 -0
- data/fixtures/component_helper.rb +16 -0
- data/fixtures/dummy/app/assets/config/manifest.js +0 -0
- data/fixtures/dummy/app/controllers/articles_controller.rb +4 -0
- data/fixtures/dummy/app/views/articles/form.rb +13 -0
- data/fixtures/dummy/app/views/articles/index.html.erb +11 -0
- data/fixtures/dummy/app/views/articles/new.html.erb +1 -0
- data/fixtures/dummy/app/views/card.rb +13 -0
- data/fixtures/dummy/config/database.yml +3 -0
- data/fixtures/dummy/config/routes.rb +5 -0
- data/fixtures/dummy/config/storage.yml +3 -0
- data/fixtures/dummy/db/schema.rb +6 -0
- data/fixtures/dummy/log/.gitignore +1 -0
- data/fixtures/dummy/public/favicon.ico +0 -0
- data/fixtures/layout.rb +31 -0
- data/fixtures/page.rb +41 -0
- data/fixtures/test_helper.rb +13 -0
- data/lib/generators/phlex/component/USAGE +8 -0
- data/lib/generators/phlex/component/component_generator.rb +13 -0
- data/lib/generators/phlex/component/templates/component.rb.erb +8 -0
- data/lib/overrides/symbol/name.rb +5 -0
- data/lib/phlex/block.rb +18 -0
- data/lib/phlex/buffered.rb +19 -0
- data/lib/phlex/component.rb +169 -23
- data/lib/phlex/configuration.rb +7 -0
- data/lib/phlex/html.rb +65 -0
- data/lib/phlex/rails/tag_helpers.rb +29 -0
- data/lib/phlex/rails.rb +8 -0
- data/lib/phlex/renderable.rb +35 -0
- data/lib/phlex/version.rb +1 -1
- data/lib/phlex.rb +25 -15
- data/package-lock.json +1195 -0
- data/package.json +5 -0
- data/phlex_logo.png +0 -0
- data/tailwind.config.js +7 -0
- metadata +64 -21
- data/Gemfile.lock +0 -32
- data/lib/phlex/callable.rb +0 -11
- data/lib/phlex/context.rb +0 -39
- data/lib/phlex/node.rb +0 -13
- data/lib/phlex/page.rb +0 -3
- data/lib/phlex/tag/standard_element.rb +0 -15
- data/lib/phlex/tag/void_element.rb +0 -9
- data/lib/phlex/tag.rb +0 -43
- data/lib/phlex/tags.rb +0 -108
- data/lib/phlex/text.rb +0 -13
data/lib/phlex/component.rb
CHANGED
@@ -1,45 +1,191 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.0")
|
4
|
+
using Overrides::Symbol::Name
|
5
|
+
end
|
6
|
+
|
3
7
|
module Phlex
|
4
8
|
class Component
|
5
|
-
|
9
|
+
extend HTML
|
10
|
+
include Renderable
|
6
11
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
+
class << self
|
13
|
+
attr_accessor :rendered_at_least_once
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(buffer = +"", view_context: nil, parent: nil, &block)
|
17
|
+
raise "The same component instance shouldn't be rendered twice" if rendered?
|
18
|
+
|
19
|
+
@_rendered = true
|
20
|
+
@_target = buffer
|
21
|
+
@_view_context = view_context
|
22
|
+
@_parent = parent
|
23
|
+
@output_buffer = self
|
24
|
+
|
25
|
+
template(&block)
|
26
|
+
|
27
|
+
self.class.rendered_at_least_once ||= true
|
28
|
+
|
29
|
+
buffer
|
30
|
+
end
|
31
|
+
|
32
|
+
def rendered?
|
33
|
+
@_rendered ||= false
|
34
|
+
end
|
35
|
+
|
36
|
+
HTML::STANDARD_ELEMENTS.each do |element|
|
37
|
+
register_element(element)
|
38
|
+
end
|
39
|
+
|
40
|
+
HTML::VOID_ELEMENTS.each do |element|
|
41
|
+
register_void_element(element)
|
42
|
+
end
|
43
|
+
|
44
|
+
register_element :template_tag, tag: "template"
|
45
|
+
|
46
|
+
def content(&block)
|
47
|
+
return unless block_given?
|
48
|
+
|
49
|
+
original_length = @_target.length
|
50
|
+
output = yield(self) if block_given?
|
51
|
+
unchanged = (original_length == @_target.length)
|
52
|
+
|
53
|
+
text(output) if unchanged && output.is_a?(String)
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def text(content)
|
58
|
+
@_target << CGI.escape_html(content)
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
|
62
|
+
def whitespace
|
63
|
+
@_target << " "
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
|
67
|
+
def doctype
|
68
|
+
@_target << HTML::DOCTYPE
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
|
72
|
+
def raw(content)
|
73
|
+
@_target << content
|
74
|
+
nil
|
75
|
+
end
|
76
|
+
|
77
|
+
def html_safe?
|
78
|
+
true
|
79
|
+
end
|
80
|
+
|
81
|
+
def safe_append=(value)
|
82
|
+
return unless value
|
12
83
|
|
13
|
-
|
14
|
-
|
84
|
+
@_target << case value
|
85
|
+
when String then value
|
86
|
+
when Symbol then value.name
|
87
|
+
else value.to_s
|
15
88
|
end
|
16
89
|
end
|
17
90
|
|
18
|
-
def
|
19
|
-
|
91
|
+
def append=(value)
|
92
|
+
return unless value
|
93
|
+
|
94
|
+
if value.html_safe?
|
95
|
+
self.safe_append = value
|
96
|
+
else
|
97
|
+
@_target << case value
|
98
|
+
when String then CGI.escape_html(value)
|
99
|
+
when Symbol then CGI.escape_html(value.name)
|
100
|
+
else CGI.escape_html(value.to_s)
|
101
|
+
end
|
102
|
+
end
|
20
103
|
end
|
21
104
|
|
22
|
-
def
|
23
|
-
|
105
|
+
def capture(&block)
|
106
|
+
return unless block_given?
|
107
|
+
|
108
|
+
original_buffer = @_target
|
109
|
+
new_buffer = +""
|
110
|
+
@_target = new_buffer
|
111
|
+
|
112
|
+
yield
|
113
|
+
|
114
|
+
@_target = original_buffer
|
115
|
+
new_buffer.html_safe
|
24
116
|
end
|
25
117
|
|
26
|
-
def
|
27
|
-
|
118
|
+
def classes(*tokens, **conditional_tokens)
|
119
|
+
{ class: self.tokens(*tokens, **conditional_tokens) }
|
28
120
|
end
|
29
121
|
|
30
|
-
def
|
31
|
-
|
122
|
+
def tokens(*tokens, **conditional_tokens)
|
123
|
+
conditional_tokens.each do |condition, token|
|
124
|
+
case condition
|
125
|
+
when Symbol then next unless send(condition)
|
126
|
+
when Proc then next unless condition.call
|
127
|
+
else raise ArgumentError,
|
128
|
+
"The class condition must be a Symbol or a Proc."
|
129
|
+
end
|
130
|
+
|
131
|
+
case token
|
132
|
+
when Symbol then tokens << token.name
|
133
|
+
when String then tokens << token
|
134
|
+
when Array then tokens.concat(t)
|
135
|
+
else raise ArgumentError,
|
136
|
+
"Conditional classes must be Symbols, Strings, or Arrays of Symbols or Strings."
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
tokens.compact.join(" ")
|
32
141
|
end
|
33
142
|
|
34
|
-
def
|
35
|
-
|
143
|
+
def _attributes(attributes, buffer: +"")
|
144
|
+
if attributes[:href]&.start_with?(/\s*javascript/)
|
145
|
+
attributes[:href] = attributes[:href].sub(/^\s*(javascript:)+/, "")
|
146
|
+
end
|
147
|
+
|
148
|
+
_build_attributes(attributes, buffer: buffer)
|
149
|
+
|
150
|
+
unless self.class.rendered_at_least_once
|
151
|
+
Phlex::ATTRIBUTE_CACHE[attributes.hash] = buffer.freeze
|
152
|
+
end
|
153
|
+
|
154
|
+
buffer
|
36
155
|
end
|
37
156
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
157
|
+
def _build_attributes(attributes, buffer:)
|
158
|
+
attributes.each do |k, v|
|
159
|
+
next unless v
|
160
|
+
|
161
|
+
name = case k
|
162
|
+
when String
|
163
|
+
k
|
164
|
+
when Symbol
|
165
|
+
k.name.tr("_", "-")
|
166
|
+
else
|
167
|
+
k.to_s
|
168
|
+
end
|
169
|
+
|
170
|
+
if HTML::EVENT_ATTRIBUTES[name] || name.match?(/[<>&"']/)
|
171
|
+
raise ArgumentError, "Unsafe attribute name detected: #{k}."
|
172
|
+
end
|
173
|
+
|
174
|
+
case v
|
175
|
+
when true
|
176
|
+
buffer << " " << name
|
177
|
+
when String
|
178
|
+
buffer << " " << name << '="' << CGI.escape_html(v) << '"'
|
179
|
+
when Symbol
|
180
|
+
buffer << " " << name << '="' << CGI.escape_html(v.name) << '"'
|
181
|
+
when Hash
|
182
|
+
_build_attributes(v.transform_keys { "#{k}-#{_1}" }, buffer: buffer)
|
183
|
+
else
|
184
|
+
buffer << " " << name << '="' << CGI.escape_html(v.to_s) << '"'
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
buffer
|
43
189
|
end
|
44
190
|
end
|
45
191
|
end
|
data/lib/phlex/html.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.0")
|
4
|
+
using Overrides::Symbol::Name
|
5
|
+
end
|
6
|
+
|
7
|
+
module Phlex
|
8
|
+
module HTML
|
9
|
+
DOCTYPE = "<!DOCTYPE html>"
|
10
|
+
|
11
|
+
STANDARD_ELEMENTS = %i[a abbr address article aside b bdi bdo blockquote body button caption cite code colgroup data datalist dd del details dfn dialog div dl dt em fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header html i iframe ins kbd label legend li main map mark menuitem meter nav noscript object ol optgroup option output p picture pre progress q rp rt ruby s samp script section select slot small span strong style sub summary sup table tbody td textarea tfoot th thead time title tr u ul video wbr].freeze
|
12
|
+
|
13
|
+
VOID_ELEMENTS = %i[area embed img input link meta param track col].freeze
|
14
|
+
|
15
|
+
EVENT_ATTRIBUTES = %w[onabort onafterprint onbeforeprint onbeforeunload onblur oncanplay oncanplaythrough onchange onclick oncontextmenu oncopy oncuechange oncut ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended onerror onerror onfocus onhashchange oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart onmessage onmousedown onmousemove onmouseout onmouseover onmouseup onmousewheel onoffline ononline onpagehide onpageshow onpaste onpause onplay onplaying onpopstate onprogress onratechange onreset onresize onscroll onsearch onseeked onseeking onselect onstalled onstorage onsubmit onsuspend ontimeupdate ontoggle onunload onvolumechange onwaiting onwheel].to_h { [_1, true] }.freeze
|
16
|
+
|
17
|
+
def register_element(element, tag: element.name.tr("_", "-"))
|
18
|
+
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
19
|
+
# frozen_string_literal: true
|
20
|
+
|
21
|
+
def #{element}(content = nil, **attributes, &block)
|
22
|
+
if attributes.length > 0
|
23
|
+
if content
|
24
|
+
@_target << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[attributes.hash] || _attributes(attributes)) << ">" << CGI.escape_html(content) << "</#{tag}>"
|
25
|
+
elsif block_given?
|
26
|
+
@_target << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[attributes.hash] || _attributes(attributes)) << ">"
|
27
|
+
content(&block)
|
28
|
+
@_target << "</#{tag}>"
|
29
|
+
else
|
30
|
+
@_target << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[attributes.hash] || _attributes(attributes)) << "></#{tag}>"
|
31
|
+
end
|
32
|
+
else
|
33
|
+
if content
|
34
|
+
@_target << "<#{tag}>" << CGI.escape_html(content) << "</#{tag}>"
|
35
|
+
elsif block_given?
|
36
|
+
@_target << "<#{tag}>"
|
37
|
+
content(&block)
|
38
|
+
@_target << "</#{tag}>"
|
39
|
+
else
|
40
|
+
@_target << "<#{tag}></#{tag}>"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
RUBY
|
47
|
+
end
|
48
|
+
|
49
|
+
def register_void_element(element, tag: element.name.tr("_", "-"))
|
50
|
+
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
51
|
+
# frozen_string_literal: true
|
52
|
+
|
53
|
+
def #{element}(**attributes)
|
54
|
+
if attributes.length > 0
|
55
|
+
@_target << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[attributes.hash] || _attributes(attributes)) << " />"
|
56
|
+
else
|
57
|
+
@_target << "<#{tag} />"
|
58
|
+
end
|
59
|
+
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
RUBY
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Phlex
|
4
|
+
module Rails
|
5
|
+
module TagHelpers
|
6
|
+
def form_with(*args, **kwargs, &block)
|
7
|
+
raw @_view_context.form_with(*args, **kwargs) { |form|
|
8
|
+
capture do
|
9
|
+
yield(
|
10
|
+
Phlex::Buffered.new(form, buffer: @_target)
|
11
|
+
)
|
12
|
+
end
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
def csp_meta_tag
|
17
|
+
if (output = @_view_context.csp_meta_tag)
|
18
|
+
@_target << output
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def csrf_meta_tags
|
23
|
+
if (output = @_view_context.csrf_meta_tags)
|
24
|
+
@_target << output
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/phlex/rails.rb
CHANGED
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Phlex
|
4
|
+
module Renderable
|
5
|
+
def render(renderable, *args, **kwargs, &block)
|
6
|
+
if renderable.is_a?(Component)
|
7
|
+
if block_given? && !block.binding.receiver.is_a?(Phlex::Block)
|
8
|
+
block = Phlex::Block.new(self, &block)
|
9
|
+
end
|
10
|
+
|
11
|
+
renderable.call(@_target, view_context: @_view_context, parent: self, &block)
|
12
|
+
elsif renderable < Component
|
13
|
+
raise ArgumentError, "You tried to render the Phlex component class: #{renderable.name} but you probably meant to render an instance of that class instead."
|
14
|
+
else
|
15
|
+
@_target << @_view_context.render(renderable, *args, **kwargs, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def render_in(view_context, &block)
|
22
|
+
if block_given?
|
23
|
+
call(view_context: view_context) do |*args, **kwargs|
|
24
|
+
view_context.with_output_buffer(self) { yield(*args, **kwargs) }
|
25
|
+
end.html_safe
|
26
|
+
else
|
27
|
+
call(view_context: view_context).html_safe
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def format
|
32
|
+
:html
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/phlex/version.rb
CHANGED
data/lib/phlex.rb
CHANGED
@@ -1,20 +1,30 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
require_relative "phlex/tag/standard_element"
|
11
|
-
require_relative "phlex/tags"
|
12
|
-
require_relative "phlex/context"
|
13
|
-
require_relative "phlex/component"
|
14
|
-
require_relative "phlex/page"
|
15
|
-
require_relative "phlex/text"
|
3
|
+
require "cgi"
|
4
|
+
require "zeitwerk"
|
5
|
+
|
6
|
+
loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
|
7
|
+
loader.ignore("#{__dir__}/generators")
|
8
|
+
loader.inflector.inflect("html" => "HTML")
|
9
|
+
loader.setup
|
16
10
|
|
17
11
|
module Phlex
|
18
|
-
|
19
|
-
|
12
|
+
Error = Module.new
|
13
|
+
ArgumentError = Class.new(ArgumentError) { include Error }
|
14
|
+
|
15
|
+
extend self
|
16
|
+
|
17
|
+
ATTRIBUTE_CACHE = {}
|
18
|
+
|
19
|
+
def configuration
|
20
|
+
@configuration ||= Configuration.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def configure
|
24
|
+
yield configuration
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
if defined?(Rails::Engine)
|
29
|
+
require "rails"
|
20
30
|
end
|