phlex 0.2.2 → 0.3.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +5 -0
  3. data/.rubocop.yml +6 -0
  4. data/CONTRIBUTING.md +23 -0
  5. data/Rakefile +2 -2
  6. data/bench.rb +1 -1
  7. data/docs/assets/application.css +2 -0
  8. data/docs/build.rb +4 -4
  9. data/docs/components/callout.rb +5 -5
  10. data/docs/components/code_block.rb +18 -18
  11. data/docs/components/example.rb +23 -23
  12. data/docs/components/heading.rb +5 -5
  13. data/docs/components/layout.rb +42 -41
  14. data/docs/components/markdown.rb +19 -19
  15. data/docs/components/tabs/tab.rb +18 -18
  16. data/docs/components/tabs.rb +21 -21
  17. data/docs/components/title.rb +5 -5
  18. data/docs/page_builder.rb +32 -32
  19. data/docs/pages/application_page.rb +3 -3
  20. data/docs/pages/index.rb +23 -25
  21. data/docs/pages/rails_integration.rb +58 -0
  22. data/docs/pages/templates.rb +238 -238
  23. data/docs/pages/views.rb +175 -0
  24. data/fixtures/compilation/vcall.rb +38 -0
  25. data/fixtures/dummy/app/views/articles/form.rb +9 -9
  26. data/fixtures/dummy/app/views/card.rb +8 -8
  27. data/fixtures/dummy/app/views/heading.rb +5 -5
  28. data/fixtures/dummy/config/routes.rb +1 -1
  29. data/fixtures/dummy/db/schema.rb +2 -2
  30. data/fixtures/layout.rb +24 -24
  31. data/fixtures/page.rb +34 -34
  32. data/fixtures/test_helper.rb +2 -2
  33. data/fixtures/view_helper.rb +16 -0
  34. data/lib/generators/phlex/view/USAGE +8 -0
  35. data/lib/generators/phlex/{component/templates/component.rb.erb → view/templates/view.rb.erb} +1 -1
  36. data/lib/generators/phlex/view/view_generator.rb +13 -0
  37. data/lib/install/phlex.rb +30 -0
  38. data/lib/overrides/symbol/name.rb +1 -1
  39. data/lib/phlex/block.rb +12 -12
  40. data/lib/phlex/buffered.rb +13 -13
  41. data/lib/phlex/compiler/formatter.rb +91 -0
  42. data/lib/phlex/compiler/generators/standard_element.rb +30 -0
  43. data/lib/phlex/compiler/generators/void_element.rb +29 -0
  44. data/lib/phlex/compiler/optimizers/base_optimizer.rb +34 -0
  45. data/lib/phlex/compiler/optimizers/vcall.rb +29 -0
  46. data/lib/phlex/compiler/visitors/base_visitor.rb +19 -0
  47. data/lib/phlex/compiler/visitors/component.rb +28 -0
  48. data/lib/phlex/compiler/visitors/component_method.rb +28 -0
  49. data/lib/phlex/compiler/visitors/file.rb +17 -0
  50. data/lib/phlex/compiler.rb +50 -0
  51. data/lib/phlex/configuration.rb +3 -3
  52. data/lib/phlex/engine.rb +11 -0
  53. data/lib/phlex/html.rb +128 -17
  54. data/lib/phlex/rails/tag_helpers.rb +23 -23
  55. data/lib/phlex/renderable.rb +32 -32
  56. data/lib/phlex/version.rb +1 -1
  57. data/lib/phlex/view.rb +223 -0
  58. data/lib/phlex.rb +27 -12
  59. data/lib/tasks/phlex_tasks.rake +10 -0
  60. metadata +41 -12
  61. data/CHANGELOG.md +0 -5
  62. data/docs/pages/components.rb +0 -175
  63. data/fixtures/component_helper.rb +0 -16
  64. data/lib/generators/phlex/component/USAGE +0 -8
  65. data/lib/generators/phlex/component/component_generator.rb +0 -13
  66. data/lib/phlex/component.rb +0 -196
  67. data/lib/phlex/rails.rb +0 -8
data/lib/phlex/view.rb ADDED
@@ -0,0 +1,223 @@
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
+ class View
9
+ extend HTML
10
+ include Renderable
11
+
12
+ class << self
13
+ attr_accessor :rendered_at_least_once
14
+
15
+ def compile
16
+ return if @compiled
17
+ return unless name
18
+ return if name.start_with? "#"
19
+
20
+ Compiler.new(self).call
21
+
22
+ @compiled = true
23
+ end
24
+
25
+ def compiled?
26
+ !!@compiled
27
+ end
28
+ end
29
+
30
+ def call(buffer = +"", view_context: nil, parent: nil, &block)
31
+ raise "The same view instance shouldn't be rendered twice" if rendered?
32
+
33
+ @_rendered = true
34
+ @_target = buffer
35
+ @_view_context = view_context
36
+ @_parent = parent
37
+ @output_buffer = self
38
+
39
+ template(&block)
40
+
41
+ self.class.rendered_at_least_once ||= true
42
+
43
+ buffer
44
+ end
45
+
46
+ def rendered?
47
+ @_rendered ||= false
48
+ end
49
+
50
+ HTML::STANDARD_ELEMENTS.each do |method_name, tag|
51
+ register_element(method_name, tag: tag)
52
+ end
53
+
54
+ HTML::VOID_ELEMENTS.each do |method_name, tag|
55
+ register_void_element(method_name, tag: tag)
56
+ end
57
+
58
+ def yield_content(&block)
59
+ return unless block_given?
60
+
61
+ original_length = @_target.length
62
+ output = yield(self)
63
+ unchanged = (original_length == @_target.length)
64
+
65
+ if unchanged
66
+ case output
67
+ when String, Symbol, Integer, Float
68
+ text(output)
69
+ end
70
+ end
71
+
72
+ nil
73
+ end
74
+
75
+ def text(content)
76
+ @_target << case content
77
+ when String then CGI.escape_html(content)
78
+ when Symbol then CGI.escape_html(content.name)
79
+ else CGI.escape_html(content.to_s)
80
+ end
81
+
82
+ nil
83
+ end
84
+
85
+ def whitespace
86
+ @_target << " "
87
+ nil
88
+ end
89
+
90
+ def comment(content = "")
91
+ @_target << "<!-- " << CGI.escape_html(content.to_s) << " -->"
92
+ nil
93
+ end
94
+
95
+ def doctype
96
+ @_target << HTML::DOCTYPE
97
+ nil
98
+ end
99
+
100
+ def raw(content)
101
+ @_target << content
102
+ nil
103
+ end
104
+
105
+ def html_safe?
106
+ true
107
+ end
108
+
109
+ def safe_append=(value)
110
+ return unless value
111
+
112
+ @_target << case value
113
+ when String then value
114
+ when Symbol then value.name
115
+ else value.to_s
116
+ end
117
+ end
118
+
119
+ def append=(value)
120
+ return unless value
121
+
122
+ if value.html_safe?
123
+ self.safe_append = value
124
+ else
125
+ @_target << case value
126
+ when String then CGI.escape_html(value)
127
+ when Symbol then CGI.escape_html(value.name)
128
+ else CGI.escape_html(value.to_s)
129
+ end
130
+ end
131
+ end
132
+
133
+ def capture(&block)
134
+ return unless block_given?
135
+
136
+ original_buffer = @_target
137
+ new_buffer = +""
138
+ @_target = new_buffer
139
+
140
+ yield
141
+
142
+ @_target = original_buffer
143
+ new_buffer.html_safe
144
+ end
145
+
146
+ def classes(*tokens, **conditional_tokens)
147
+ { class: self.tokens(*tokens, **conditional_tokens) }
148
+ end
149
+
150
+ def tokens(*tokens, **conditional_tokens)
151
+ conditional_tokens.each do |condition, token|
152
+ case condition
153
+ when Symbol then next unless send(condition)
154
+ when Proc then next unless condition.call
155
+ else raise ArgumentError,
156
+ "The class condition must be a Symbol or a Proc."
157
+ end
158
+
159
+ case token
160
+ when Symbol then tokens << token.name
161
+ when String then tokens << token
162
+ when Array then tokens.concat(t)
163
+ else raise ArgumentError,
164
+ "Conditional classes must be Symbols, Strings, or Arrays of Symbols or Strings."
165
+ end
166
+ end
167
+
168
+ tokens.compact.join(" ")
169
+ end
170
+
171
+ def helpers
172
+ @_view_context
173
+ end
174
+
175
+ def _attributes(attributes, buffer: +"")
176
+ if attributes[:href]&.start_with?(/\s*javascript/)
177
+ attributes[:href] = attributes[:href].sub(/^\s*(javascript:)+/, "")
178
+ end
179
+
180
+ _build_attributes(attributes, buffer: buffer)
181
+
182
+ unless self.class.rendered_at_least_once
183
+ Phlex::ATTRIBUTE_CACHE[attributes.hash] = buffer.freeze
184
+ end
185
+
186
+ buffer
187
+ end
188
+
189
+ def _build_attributes(attributes, buffer:)
190
+ attributes.each do |k, v|
191
+ next unless v
192
+
193
+ name = case k
194
+ when String
195
+ k
196
+ when Symbol
197
+ k.name.tr("_", "-")
198
+ else
199
+ k.to_s
200
+ end
201
+
202
+ if HTML::EVENT_ATTRIBUTES[name] || name.match?(/[<>&"']/)
203
+ raise ArgumentError, "Unsafe attribute name detected: #{k}."
204
+ end
205
+
206
+ case v
207
+ when true
208
+ buffer << " " << name
209
+ when String
210
+ buffer << " " << name << '="' << CGI.escape_html(v) << '"'
211
+ when Symbol
212
+ buffer << " " << name << '="' << CGI.escape_html(v.name) << '"'
213
+ when Hash
214
+ _build_attributes(v.transform_keys { "#{k}-#{_1.name.tr('_', '-')}" }, buffer: buffer)
215
+ else
216
+ buffer << " " << name << '="' << CGI.escape_html(v.to_s) << '"'
217
+ end
218
+ end
219
+
220
+ buffer
221
+ end
222
+ end
223
+ end
data/lib/phlex.rb CHANGED
@@ -2,29 +2,44 @@
2
2
 
3
3
  require "cgi"
4
4
  require "zeitwerk"
5
+ require "syntax_tree"
5
6
 
6
7
  loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
7
8
  loader.ignore("#{__dir__}/generators")
8
9
  loader.inflector.inflect("html" => "HTML")
10
+ loader.inflector.inflect("vcall" => "VCall")
11
+ loader.inflector.inflect("fcall" => "FCall")
9
12
  loader.setup
10
13
 
11
14
  module Phlex
12
- Error = Module.new
13
- ArgumentError = Class.new(ArgumentError) { include Error }
15
+ Error = Module.new
16
+ ArgumentError = Class.new(ArgumentError) { include Error }
17
+ NameError = Class.new(NameError) { include Error }
14
18
 
15
- extend self
19
+ extend self
16
20
 
17
- ATTRIBUTE_CACHE = {}
21
+ ATTRIBUTE_CACHE = {}
18
22
 
19
- def configuration
20
- @configuration ||= Configuration.new
21
- end
23
+ def const_missing(name)
24
+ if name == :Component
25
+ raise NameError, "👋 Phlex::Component is now Phlex::View"
26
+ else
27
+ super
28
+ end
29
+ end
22
30
 
23
- def configure
24
- yield configuration
25
- end
31
+ def configuration
32
+ @configuration ||= Configuration.new
33
+ end
34
+
35
+ def configure
36
+ yield configuration
37
+ end
26
38
  end
27
39
 
28
- if defined?(Rails::Engine)
29
- require "rails"
40
+ begin
41
+ require "rails"
42
+ require "phlex/engine"
43
+ rescue LoadError
44
+ # Rails isn't in this env, don't load the engine.
30
45
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :phlex do
4
+ desc "Install Phlex in the Rails application"
5
+ task :install do
6
+ install_file_path = File.expand_path("../install/phlex.rb", __dir__)
7
+
8
+ system "#{RbConfig.ruby} bin/rails app:template LOCATION=#{install_file_path}"
9
+ end
10
+ end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlex
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel Drapper
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-09-21 00:00:00.000000000 Z
11
+ date: 2022-09-30 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: syntax_tree
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.6'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: zeitwerk
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -24,17 +38,18 @@ dependencies:
24
38
  - - "~>"
25
39
  - !ruby/object:Gem::Version
26
40
  version: '2.6'
27
- description: A high-performance view component framework optimised for developer happiness.
41
+ description: A high-performance view framework optimised for developer happiness.
28
42
  email:
29
43
  - joel@drapper.me
30
44
  executables: []
31
45
  extensions: []
32
46
  extra_rdoc_files: []
33
47
  files:
48
+ - ".editorconfig"
34
49
  - ".rspec"
35
50
  - ".rubocop.yml"
36
- - CHANGELOG.md
37
51
  - CODE_OF_CONDUCT.md
52
+ - CONTRIBUTING.md
38
53
  - Gemfile
39
54
  - LICENSE.txt
40
55
  - README.md
@@ -56,10 +71,11 @@ files:
56
71
  - docs/components/title.rb
57
72
  - docs/page_builder.rb
58
73
  - docs/pages/application_page.rb
59
- - docs/pages/components.rb
60
74
  - docs/pages/index.rb
75
+ - docs/pages/rails_integration.rb
61
76
  - docs/pages/templates.rb
62
- - fixtures/component_helper.rb
77
+ - docs/pages/views.rb
78
+ - fixtures/compilation/vcall.rb
63
79
  - fixtures/dummy/app/assets/config/manifest.js
64
80
  - fixtures/dummy/app/controllers/articles_controller.rb
65
81
  - fixtures/dummy/app/views/articles/form.rb
@@ -76,20 +92,33 @@ files:
76
92
  - fixtures/layout.rb
77
93
  - fixtures/page.rb
78
94
  - fixtures/test_helper.rb
79
- - lib/generators/phlex/component/USAGE
80
- - lib/generators/phlex/component/component_generator.rb
81
- - lib/generators/phlex/component/templates/component.rb.erb
95
+ - fixtures/view_helper.rb
96
+ - lib/generators/phlex/view/USAGE
97
+ - lib/generators/phlex/view/templates/view.rb.erb
98
+ - lib/generators/phlex/view/view_generator.rb
99
+ - lib/install/phlex.rb
82
100
  - lib/overrides/symbol/name.rb
83
101
  - lib/phlex.rb
84
102
  - lib/phlex/block.rb
85
103
  - lib/phlex/buffered.rb
86
- - lib/phlex/component.rb
104
+ - lib/phlex/compiler.rb
105
+ - lib/phlex/compiler/formatter.rb
106
+ - lib/phlex/compiler/generators/standard_element.rb
107
+ - lib/phlex/compiler/generators/void_element.rb
108
+ - lib/phlex/compiler/optimizers/base_optimizer.rb
109
+ - lib/phlex/compiler/optimizers/vcall.rb
110
+ - lib/phlex/compiler/visitors/base_visitor.rb
111
+ - lib/phlex/compiler/visitors/component.rb
112
+ - lib/phlex/compiler/visitors/component_method.rb
113
+ - lib/phlex/compiler/visitors/file.rb
87
114
  - lib/phlex/configuration.rb
115
+ - lib/phlex/engine.rb
88
116
  - lib/phlex/html.rb
89
- - lib/phlex/rails.rb
90
117
  - lib/phlex/rails/tag_helpers.rb
91
118
  - lib/phlex/renderable.rb
92
119
  - lib/phlex/version.rb
120
+ - lib/phlex/view.rb
121
+ - lib/tasks/phlex_tasks.rake
93
122
  - package-lock.json
94
123
  - package.json
95
124
  - phlex_logo.png
@@ -122,5 +151,5 @@ requirements: []
122
151
  rubygems_version: 3.3.7
123
152
  signing_key:
124
153
  specification_version: 4
125
- summary: A framework for building view components with a Ruby DSL.
154
+ summary: A framework for building views with a Ruby DSL.
126
155
  test_files: []
data/CHANGELOG.md DELETED
@@ -1,5 +0,0 @@
1
- ## [Unreleased]
2
-
3
- ## [0.1.0] - 2022-06-01
4
-
5
- - Initial release
@@ -1,175 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Pages
4
- class Components < ApplicationPage
5
- def template
6
- render Layout.new(title: "Components in Phlex") do
7
- render Markdown.new(<<~MD)
8
- # Components
9
-
10
- ## Yielding content
11
-
12
- Your components can accept content as a block passed to the template method. You can capture the content block and pass it to the `content` method to yield it.
13
- MD
14
-
15
- render Example.new do |e|
16
- e.tab "card.rb", <<~RUBY
17
- class Card < Phlex::Component
18
- def template(&)
19
- article(class: "drop-shadow rounded p-5") {
20
- h1 "Amazing content!"
21
- content(&)
22
- }
23
- end
24
- end
25
- RUBY
26
-
27
- e.execute "Card.new.call { 'Your content here.\n' }"
28
- end
29
-
30
- render Markdown.new(<<~MD)
31
- ## Delegating content
32
-
33
- Alternatively, you can pass the content down as an argument to another component or tag.
34
- MD
35
-
36
- render Example.new do |e|
37
- e.tab "card.rb", <<~RUBY
38
- class Card < Phlex::Component
39
- def template(&)
40
- article(class: "drop-shadow rounded p-5", &)
41
- end
42
- end
43
- RUBY
44
-
45
- e.execute "Card.new.call { 'Your content here.' }"
46
- end
47
-
48
- render Markdown.new(<<~MD)
49
- ## Nested components
50
-
51
- Components can render other components and optionally pass them content as a block.
52
- MD
53
-
54
- render Example.new do |e|
55
- e.tab "example.rb", <<~RUBY
56
- class Example < Phlex::Component
57
- def template
58
- render Card.new do
59
- h1 "Hello"
60
- end
61
- end
62
- end
63
- RUBY
64
-
65
- e.tab "card.rb", <<~RUBY
66
- class Card < Phlex::Component
67
- def template(&)
68
- article(class: "drop-shadow rounded p-5", &)
69
- end
70
- end
71
- RUBY
72
-
73
- e.execute "Example.new.call"
74
- end
75
-
76
- render Markdown.new(<<~MD)
77
- If the block just wraps a string, the string is treated as _text content_.
78
- MD
79
-
80
- render Example.new do |e|
81
- e.tab "example.rb", <<~RUBY
82
- class Example < Phlex::Component
83
- def template
84
- render(Card.new) { "Hi" }
85
- end
86
- end
87
- RUBY
88
-
89
- e.tab "card.rb", <<~RUBY
90
- class Card < Phlex::Component
91
- def template(&)
92
- article(class: "drop-shadow rounded p-5", &)
93
- end
94
- end
95
- RUBY
96
-
97
- e.execute "Example.new.call"
98
- end
99
-
100
- render Markdown.new(<<~MD)
101
- ## Component attributes
102
-
103
- Besides content, components can define attributes in an initializer, which can then be rendered in the template.
104
- MD
105
-
106
- render Example.new do |e|
107
- e.tab "hello.rb", <<~RUBY
108
- class Hello < Phlex::Component
109
- def initialize(name:)
110
- @name = name
111
- end
112
-
113
- def template
114
- h1 "Hello \#{@name}!"
115
- end
116
- end
117
- RUBY
118
-
119
- e.tab "example.rb", <<~RUBY
120
- class Example < Phlex::Component
121
- def template
122
- render Hello.new(name: "Joel")
123
- end
124
- end
125
- RUBY
126
-
127
- e.execute "Example.new.call"
128
- end
129
-
130
- render Markdown.new(<<~MD)
131
- It’s usually a good idea to use instance variables directly rather than creating accessor methods for them. Otherwise it’s easy to run into naming conflicts. For example, your layout component might have the attribute `title`, to render into a `<title>` element in the document head. If you define `attr_accessor :title`, that would overwrite the `title` method for creating `<title>` elements.
132
-
133
- ## Calculations with methods
134
-
135
- Components are just Ruby classes, so you can perform calculations on component attributes by defining your own methods.
136
- MD
137
-
138
- render Example.new do |e|
139
- e.tab "status.rb", <<~RUBY
140
- class Status < Phlex::Component
141
- def initialize(status:)
142
- @status = status
143
- end
144
-
145
- def template
146
- span status_emoji
147
- end
148
-
149
- private
150
-
151
- def status_emoji
152
- case @status
153
- when :success
154
- "✅"
155
- when :failure
156
- "❌"
157
- end
158
- end
159
- end
160
- RUBY
161
-
162
- e.tab "example.rb", <<~RUBY
163
- class Example < Phlex::Component
164
- def template
165
- render Status.new(status: :success)
166
- end
167
- end
168
- RUBY
169
-
170
- e.execute "Example.new.call"
171
- end
172
- end
173
- end
174
- end
175
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ComponentHelper
4
- def self.extended(parent)
5
- parent.instance_exec do
6
- let(:output) { example.call }
7
- let(:example) { component.new }
8
- end
9
- end
10
-
11
- def component(&block)
12
- let :component do
13
- Class.new(Phlex::Component, &block)
14
- end
15
- end
16
- end
@@ -1,8 +0,0 @@
1
- Description:
2
- Generates a phlex component with the given name
3
-
4
- Example:
5
- rails generate phlex:component Sidebar
6
-
7
- This will create:
8
- app/views/components/sidebar_component.rb
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlex
4
- module Generators
5
- class ComponentGenerator < ::Rails::Generators::NamedBase
6
- source_root File.expand_path("templates", __dir__)
7
-
8
- def create_component
9
- template "component.rb", File.join("app/views", class_path, "#{file_name}.rb")
10
- end
11
- end
12
- end
13
- end