phlex 0.1.0 → 0.2.1

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.

Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.rubocop.yml +4 -0
  4. data/Gemfile +15 -3
  5. data/README.md +14 -30
  6. data/Rakefile +4 -6
  7. data/bench.rb +14 -0
  8. data/config.ru +9 -0
  9. data/docs/assets/application.css +24 -0
  10. data/docs/assets/logo.png +0 -0
  11. data/docs/build.rb +22 -0
  12. data/docs/components/callout.rb +9 -0
  13. data/docs/components/code_block.rb +26 -0
  14. data/docs/components/example.rb +32 -0
  15. data/docs/components/heading.rb +9 -0
  16. data/docs/components/layout.rb +45 -0
  17. data/docs/components/markdown.rb +26 -0
  18. data/docs/components/tabs/tab.rb +26 -0
  19. data/docs/components/tabs.rb +30 -0
  20. data/docs/components/title.rb +9 -0
  21. data/docs/page_builder.rb +36 -0
  22. data/docs/pages/application_page.rb +7 -0
  23. data/docs/pages/components.rb +175 -0
  24. data/docs/pages/index.rb +40 -0
  25. data/docs/pages/templates.rb +242 -0
  26. data/fixtures/component_helper.rb +16 -0
  27. data/fixtures/dummy/app/assets/config/manifest.js +0 -0
  28. data/fixtures/dummy/app/controllers/articles_controller.rb +4 -0
  29. data/fixtures/dummy/app/views/articles/form.rb +13 -0
  30. data/fixtures/dummy/app/views/articles/index.html.erb +11 -0
  31. data/fixtures/dummy/app/views/articles/new.html.erb +1 -0
  32. data/fixtures/dummy/app/views/card.rb +13 -0
  33. data/fixtures/dummy/config/database.yml +3 -0
  34. data/fixtures/dummy/config/routes.rb +5 -0
  35. data/fixtures/dummy/config/storage.yml +3 -0
  36. data/fixtures/dummy/db/schema.rb +6 -0
  37. data/fixtures/dummy/log/.gitignore +1 -0
  38. data/fixtures/dummy/public/favicon.ico +0 -0
  39. data/fixtures/layout.rb +31 -0
  40. data/fixtures/page.rb +41 -0
  41. data/fixtures/test_helper.rb +13 -0
  42. data/lib/generators/phlex/component/USAGE +8 -0
  43. data/lib/generators/phlex/component/component_generator.rb +13 -0
  44. data/lib/generators/phlex/component/templates/component.rb.erb +8 -0
  45. data/lib/overrides/symbol/name.rb +5 -0
  46. data/lib/phlex/block.rb +18 -0
  47. data/lib/phlex/buffered.rb +19 -0
  48. data/lib/phlex/component.rb +169 -23
  49. data/lib/phlex/configuration.rb +7 -0
  50. data/lib/phlex/html.rb +65 -0
  51. data/lib/phlex/rails/tag_helpers.rb +29 -0
  52. data/lib/phlex/rails.rb +8 -0
  53. data/lib/phlex/renderable.rb +35 -0
  54. data/lib/phlex/version.rb +1 -1
  55. data/lib/phlex.rb +25 -15
  56. data/package-lock.json +1195 -0
  57. data/package.json +5 -0
  58. data/phlex_logo.png +0 -0
  59. data/tailwind.config.js +7 -0
  60. metadata +64 -21
  61. data/Gemfile.lock +0 -32
  62. data/lib/phlex/callable.rb +0 -11
  63. data/lib/phlex/context.rb +0 -39
  64. data/lib/phlex/node.rb +0 -13
  65. data/lib/phlex/page.rb +0 -3
  66. data/lib/phlex/tag/standard_element.rb +0 -15
  67. data/lib/phlex/tag/void_element.rb +0 -9
  68. data/lib/phlex/tag.rb +0 -43
  69. data/lib/phlex/tags.rb +0 -108
  70. data/lib/phlex/text.rb +0 -13
@@ -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
- include Node, Context
9
+ extend HTML
10
+ include Renderable
6
11
 
7
- module Initializer
8
- def initialize(*args, assigns: [], **kwargs, &block)
9
- assigns.each do |k, v|
10
- instance_variable_get(k) || instance_variable_set(k, v)
11
- end
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
- super(*args, **kwargs)
14
- template(&block)
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 initialize(**attributes)
19
- attributes.each { |k, v| instance_variable_set("@#{k}", v) }
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 self.inherited(child)
23
- child.prepend(Initializer)
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 render_context
27
- @_render_context ||= self
118
+ def classes(*tokens, **conditional_tokens)
119
+ { class: self.tokens(*tokens, **conditional_tokens) }
28
120
  end
29
121
 
30
- def <<(node)
31
- render_context.children << node
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 render(&block)
35
- instance_eval(&block) if block_given?
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 render_tag(tag, &block)
39
- old_render_context = render_context.dup
40
- @_render_context = tag
41
- instance_eval(&block)
42
- @_render_context = old_render_context
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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ class Configuration
5
+ # Config coming soon.
6
+ end
7
+ 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 path picture pre progress q rp rt ruby s samp script section select slot small span strong style sub summary sup svg 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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ module Rails
5
+ end
6
+ end
7
+
8
+ Phlex::Component.include(Phlex::Rails::TagHelpers)
@@ -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.is_a?(Class) && 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phlex
4
- VERSION = '0.1.0'
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/phlex.rb CHANGED
@@ -1,20 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/string/output_safety"
4
-
5
- require_relative "phlex/version"
6
- require_relative "phlex/callable"
7
- require_relative "phlex/tag"
8
- require_relative "phlex/tag/void_element"
9
- require_relative "phlex/node"
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
- class Error < StandardError; end
19
- # Your code goes here...
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