grsx 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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +20 -0
  3. data/.gitignore +5 -0
  4. data/.rspec +2 -0
  5. data/.travis.yml +6 -0
  6. data/Appraisals +17 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Dockerfile +8 -0
  9. data/Gemfile +7 -0
  10. data/Gemfile.lock +274 -0
  11. data/Guardfile +70 -0
  12. data/LICENSE.txt +21 -0
  13. data/README.md +437 -0
  14. data/Rakefile +6 -0
  15. data/bin/console +14 -0
  16. data/bin/setup +8 -0
  17. data/bin/test +43 -0
  18. data/docker-compose.yml +29 -0
  19. data/gemfiles/.bundle/config +2 -0
  20. data/gemfiles/rails_6_1.gemfile +8 -0
  21. data/gemfiles/rails_6_1.gemfile.lock +260 -0
  22. data/gemfiles/rails_7_0.gemfile +7 -0
  23. data/gemfiles/rails_7_0.gemfile.lock +265 -0
  24. data/gemfiles/rails_7_1.gemfile +7 -0
  25. data/gemfiles/rails_7_1.gemfile.lock +295 -0
  26. data/gemfiles/rails_7_2.gemfile +7 -0
  27. data/gemfiles/rails_7_2.gemfile.lock +290 -0
  28. data/gemfiles/rails_8_0.gemfile +8 -0
  29. data/gemfiles/rails_8_0.gemfile.lock +344 -0
  30. data/gemfiles/rails_8_1.gemfile +8 -0
  31. data/gemfiles/rails_8_1.gemfile.lock +313 -0
  32. data/gemfiles/rails_master.gemfile +7 -0
  33. data/gemfiles/rails_master.gemfile.lock +296 -0
  34. data/grsx.gemspec +43 -0
  35. data/lib/generators/grsx/phlex_component/phlex_component_generator.rb +40 -0
  36. data/lib/generators/grsx/phlex_component/templates/component.rb.tt +12 -0
  37. data/lib/generators/grsx/phlex_component/templates/component.rbx.tt +6 -0
  38. data/lib/grsx/component_resolver.rb +64 -0
  39. data/lib/grsx/configuration.rb +14 -0
  40. data/lib/grsx/lexer.rb +325 -0
  41. data/lib/grsx/nodes/abstract_attr.rb +12 -0
  42. data/lib/grsx/nodes/abstract_element.rb +13 -0
  43. data/lib/grsx/nodes/abstract_node.rb +31 -0
  44. data/lib/grsx/nodes/component_element.rb +69 -0
  45. data/lib/grsx/nodes/component_prop.rb +29 -0
  46. data/lib/grsx/nodes/declaration.rb +15 -0
  47. data/lib/grsx/nodes/expression.rb +15 -0
  48. data/lib/grsx/nodes/expression_group.rb +15 -0
  49. data/lib/grsx/nodes/fragment.rb +30 -0
  50. data/lib/grsx/nodes/html_attr.rb +13 -0
  51. data/lib/grsx/nodes/html_element.rb +49 -0
  52. data/lib/grsx/nodes/newline.rb +9 -0
  53. data/lib/grsx/nodes/raw.rb +23 -0
  54. data/lib/grsx/nodes/root.rb +19 -0
  55. data/lib/grsx/nodes/text.rb +15 -0
  56. data/lib/grsx/nodes/util.rb +9 -0
  57. data/lib/grsx/nodes.rb +20 -0
  58. data/lib/grsx/parser.rb +238 -0
  59. data/lib/grsx/phlex_compiler.rb +223 -0
  60. data/lib/grsx/phlex_component.rb +361 -0
  61. data/lib/grsx/phlex_runtime.rb +70 -0
  62. data/lib/grsx/prop_inspector.rb +52 -0
  63. data/lib/grsx/rails/engine.rb +24 -0
  64. data/lib/grsx/rails/phlex_reloader.rb +25 -0
  65. data/lib/grsx/template.rb +12 -0
  66. data/lib/grsx/version.rb +3 -0
  67. data/lib/grsx.rb +35 -0
  68. metadata +324 -0
@@ -0,0 +1,361 @@
1
+ require "phlex"
2
+ require "phlex-rails"
3
+ require "digest"
4
+
5
+ module Grsx
6
+ # Base class for JSX-backed Phlex components.
7
+ #
8
+ # Define your props in initialize, write your template in a co-located .rbx
9
+ # file. Grsx compiles the .rbx into a real view_template method — no eval
10
+ # at render time.
11
+ #
12
+ # ## Basic usage
13
+ #
14
+ # # app/components/card_component.rb
15
+ # class CardComponent < Grsx::PhlexComponent
16
+ # def initialize(title:)
17
+ # @title = title
18
+ # end
19
+ # end
20
+ #
21
+ # # app/components/card_component.rbx
22
+ # <article class="card">
23
+ # <h2>{@title}</h2>
24
+ # {content}
25
+ # </article>
26
+ #
27
+ # ## Named slots
28
+ #
29
+ # class CardComponent < Grsx::PhlexComponent
30
+ # slots :header, :footer
31
+ # end
32
+ #
33
+ # # card_component.rbx
34
+ # <article>
35
+ # <header>{slot(:header)}</header>
36
+ # <main>{content}</main>
37
+ # <footer>{slot(:footer)}</footer>
38
+ # </article>
39
+ #
40
+ # # Usage
41
+ # card = CardComponent.new
42
+ # card.with_slot(:header) { render LogoComponent.new }
43
+ # render card
44
+ #
45
+ class PhlexComponent < Phlex::HTML
46
+ # Include ALL phlex-rails helper adapters so link_to, form_with,
47
+ # image_tag, url_for, etc. just work in every .rbx template without
48
+ # per-component opt-in.
49
+ #
50
+ # Some helpers (e.g. Routes) reference `Rails.application` at define
51
+ # time, which raises NameError outside a Rails boot. We rescue and
52
+ # skip — those helpers are unavailable in non-Rails contexts anyway.
53
+ Phlex::Rails::Helpers.constants.each do |helper_name|
54
+ begin
55
+ mod = Phlex::Rails::Helpers.const_get(helper_name)
56
+ include mod if mod.is_a?(Module)
57
+ rescue NameError
58
+ # Skip helpers that require Rails to be fully initialized
59
+ end
60
+ end
61
+
62
+ # --- Named slots ---
63
+
64
+ class << self
65
+ # Declare named content slots on the component.
66
+ #
67
+ # class CardComponent < Grsx::PhlexComponent
68
+ # slots :header, :footer
69
+ # end
70
+ def slots(*names)
71
+ names.each do |name|
72
+ # Define a setter: component.with_header { ... }
73
+ define_method(:"with_#{name}") do |&block|
74
+ @_slots ||= {}
75
+ @_slots[name] = block
76
+ self
77
+ end
78
+
79
+ # Define a predicate: has_header?
80
+ define_method(:"has_#{name}?") do
81
+ (@_slots ||= {}).key?(name)
82
+ end
83
+ end
84
+ end
85
+
86
+ # Declare typed props with optional defaults — auto-generates initialize.
87
+ #
88
+ # class CardComponent < Grsx::PhlexComponent
89
+ # props :title, :body, size: :md, disabled: false
90
+ # end
91
+ #
92
+ # This is exactly equivalent to:
93
+ #
94
+ # def initialize(title:, body:, size: :md, disabled: false)
95
+ # @title = title
96
+ # @body = body
97
+ # @size = size
98
+ # @disabled = disabled
99
+ # end
100
+ #
101
+ # You can still override initialize manually when you need logic
102
+ # beyond simple ivar assignment.
103
+ def props(*required_names, **defaults)
104
+ # Guard against mutable default values ([], {}) — they would be
105
+ # shared across every instance of the component, causing subtle
106
+ # cross-request state contamination. Fail loudly at class-definition
107
+ # time with guidance on the idiomatic fix.
108
+ defaults.each do |key, val|
109
+ if val.is_a?(Array) || val.is_a?(Hash)
110
+ raise ArgumentError,
111
+ "#{name}.props :#{key} has a mutable default (#{val.inspect}). " \
112
+ "Use nil as the default and set the value in initialize instead:\n" \
113
+ " props :#{key}\n" \
114
+ " def initialize(#{key}: nil)\n" \
115
+ " @#{key} = #{key} || #{val.inspect}\n" \
116
+ " end"
117
+ end
118
+ end
119
+
120
+ @_declared_props = { required: required_names.map(&:to_sym), defaults: defaults }
121
+
122
+ all_names = required_names.map(&:to_sym) + defaults.keys.map(&:to_sym)
123
+
124
+ # Generate attr_readers so callers can inspect prop values after render.
125
+ # Templates use @ivar directly; attr_reader makes the same data available
126
+ # to parent components or test code.
127
+ attr_reader(*all_names)
128
+
129
+ # Build initialize parameter list
130
+ params = required_names.map { |n| "#{n}:" }
131
+ defaults.each { |k, v| params << "#{k}: #{v.inspect}" }
132
+
133
+ # Build ivar assignment lines
134
+ assignments = all_names.map { |n| " @#{n} = #{n}" }
135
+
136
+ class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
137
+ def initialize(#{params.join(", ")})
138
+ #{assignments.join("\n")}
139
+ end
140
+ RUBY
141
+ end
142
+
143
+ # Returns the declared props, or nil if none were declared.
144
+ attr_reader :_declared_props
145
+ end
146
+
147
+ # Render a named slot. Falls back silently if no slot content was provided.
148
+ # Used in .rbx templates as {slot(:header)}.
149
+ def slot(name)
150
+ block = (@_slots ||= {})[name]
151
+ instance_exec(&block) if block
152
+ nil
153
+ end
154
+
155
+ # --- Default children slot ---
156
+
157
+ # {content} in a .rbx template compiles to `yield` — Phlex 2.x style.
158
+ # This method is a no-op; the compiler special-cases the `content` identifier.
159
+ # Kept for documentation and as a fallback.
160
+ def content
161
+ yield
162
+ end
163
+
164
+ # --- Expression output ---
165
+
166
+ # Handles all { ruby_expr } in compiled templates:
167
+ #
168
+ # render(<Comp />) already wrote to buffer → returns nil → no-op here
169
+ # Array/Enumerable → each element rendered recursively
170
+ # Phlex::SafeObject → raw() (trusted HTML, no escaping)
171
+ # nil / false / "" → silent no-op (safe for && and || patterns)
172
+ # anything else → plain(value.to_s) (CGI auto-escaped, XSS-safe)
173
+ def __rbx_expr_out(value)
174
+ case value
175
+ when nil, false, ""
176
+ nil # {condition && <Foo />}: falsy short-circuit
177
+ when Array, Enumerable
178
+ # {@items.map { |i| <Item /> }}: map returns [nil,nil,...] after render→nil
179
+ value.each { |v| __rbx_expr_out(v) }
180
+ when Phlex::SGML
181
+ # Safety net: if a user passes a component directly (e.g. {MyComp.new})
182
+ # render it normally. Our render override returns nil so this branch is
183
+ # only hit in direct-instance-passing scenarios, not {cond && <Comp />}.
184
+ render(value)
185
+ when Phlex::SGML::SafeObject
186
+ raw(value)
187
+ else
188
+ plain(value.to_s)
189
+ end
190
+ end
191
+
192
+ # Explicit escape hatch for trusted HTML strings.
193
+ #
194
+ # By default, every { expression } is CGI-escaped via plain(). Use safe()
195
+ # when you have a string that is already HTML and must not be escaped:
196
+ #
197
+ # {safe(@html_body)} # raw inject
198
+ # {@items.map { safe(i.html) }} # safe inside map
199
+ #
200
+ # WARNING: never pass user-supplied input to safe() — it bypasses all XSS
201
+ # protection. Only use for strings you have produced or sanitized yourself.
202
+ def safe(html_string)
203
+ Phlex::SGML::SafeValue.new(html_string.to_s)
204
+ end
205
+
206
+ # Override Phlex's render to always return nil.
207
+ #
208
+ # Phlex::SGML#render returns the component instance, which would cause
209
+ # __rbx_expr_out to see a Phlex::SGML and call render() a second time
210
+ # (double-render bug). Returning nil short-circuits that:
211
+ #
212
+ # {true && <Button />} → true && render(ButtonComponent.new) → nil
213
+ # __rbx_expr_out(nil) → no-op ✓
214
+ #
215
+ # {@items.map { |i| <Item /> }} → map returns [nil, nil, nil]
216
+ # __rbx_expr_out([nil, nil, nil]) → no-op ✓
217
+ def render(renderable = nil, &block)
218
+ super
219
+ nil
220
+ end
221
+
222
+ # Raised when a .rbx template fails to compile (syntax or parse error).
223
+ # Provides the source file path and the underlying error message so
224
+ # developers see their .rbx line rather than a grsx internal backtrace.
225
+ class TemplateCompileError < StandardError
226
+ attr_reader :template_path
227
+
228
+ def initialize(message, template_path:)
229
+ @template_path = template_path
230
+ super(message)
231
+ end
232
+ end
233
+
234
+ # --- Template loading ---
235
+
236
+ class << self
237
+ # Template cache: { path => { mtime: Time, code: String } }
238
+ TEMPLATE_CACHE = {}
239
+ private_constant :TEMPLATE_CACHE
240
+
241
+ def inherited(subclass)
242
+ # Capture the caller's file path BEFORE calling super so the stack
243
+ # frame is still fresh. This is more reliable than source_location
244
+ # because it works even when the subclass has no custom initialize.
245
+ defining_file = caller_locations(1, 10)
246
+ .find { |loc| loc.path != __FILE__ && !loc.path.end_with?("phlex_component.rb") }
247
+ &.path
248
+ subclass.instance_variable_set(:@_rbx_source_rb, defining_file)
249
+
250
+ super
251
+ subclass.load_rbx_template
252
+ end
253
+
254
+ # Locate, compile, and define view_template from the co-located .rbx file.
255
+ # Called once when the subclass is first defined.
256
+ def load_rbx_template
257
+ path = rbx_template_path
258
+ return unless path && File.exist?(path)
259
+
260
+ compiled = compile_template(path)
261
+ define_view_template(compiled)
262
+ @_rbx_template_path = path
263
+ end
264
+
265
+ # Recompile and redefine view_template if the .rbx file has changed.
266
+ # Called by Grsx::Rails::PhlexReloader on each dev request.
267
+ def reload_rbx_template_if_changed
268
+ path = @_rbx_template_path
269
+ return unless path
270
+
271
+ mtime = File.mtime(path)
272
+ cached = TEMPLATE_CACHE[path]
273
+ return if cached && cached[:mtime] == mtime
274
+
275
+ compiled = compile_template(path)
276
+ define_view_template(compiled)
277
+ end
278
+
279
+ # Return the path to the .rbx file for this component (nil if not found).
280
+ def rbx_template_path
281
+ @_rbx_template_path if defined?(@_rbx_template_path)
282
+
283
+ source = @_rbx_source_rb
284
+ return nil unless source
285
+
286
+ base = File.basename(source, ".rb")
287
+ dir = File.dirname(source)
288
+ candidate = File.join(dir, "#{base}.rbx")
289
+ candidate if File.exist?(candidate)
290
+ end
291
+
292
+ # All known PhlexComponent subclasses, for the dev-mode reloader.
293
+ def all_descendants
294
+ ObjectSpace.each_object(Class).select { |c| c < self }
295
+ end
296
+
297
+ # Returns the Phlex DSL Ruby code that was compiled from the .rbx template.
298
+ # Useful for debugging, introspection, and writing specs that verify
299
+ # what the compiler generates:
300
+ #
301
+ # puts MyCard.compiled_template_code
302
+ # # ⇒ div(class: "card") do
303
+ # # plain(@title)
304
+ # # end
305
+ def compiled_template_code
306
+ path = @_rbx_template_path || rbx_template_path
307
+ return nil unless path
308
+ compile_template(path)
309
+ end
310
+
311
+ private
312
+
313
+ def compile_template(path)
314
+ content = File.read(path)
315
+
316
+ # Cache by content hash, not mtime. mtime is fragile in Docker/rsync
317
+ # deployments where COPY or rsync can reset timestamps to build time
318
+ # without changing content — or vice versa, touch the file without
319
+ # changing content, causing pointless recompilation.
320
+ #
321
+ # SHA256 is deterministic and correct. We truncate to 16 hex chars
322
+ # (64 bits of collision resistance) which is more than sufficient for
323
+ # a per-process in-memory cache keyed by full path.
324
+ hash = Digest::SHA256.hexdigest(content)[0, 16]
325
+ cache_key = "#{path}:#{hash}"
326
+
327
+ return TEMPLATE_CACHE[cache_key] if TEMPLATE_CACHE.key?(cache_key)
328
+
329
+ template = Grsx::Template.new(content, path)
330
+
331
+ begin
332
+ code = Grsx.compile(template)
333
+ rescue Grsx::Lexer::SyntaxError, Grsx::Parser::ParseError => e
334
+ raise TemplateCompileError.new(
335
+ "#{File.basename(path)}: #{e.message}",
336
+ template_path: path
337
+ )
338
+ end
339
+
340
+ TEMPLATE_CACHE[cache_key] = code
341
+ code
342
+ end
343
+
344
+ def define_view_template(compiled_code)
345
+ # Pass the .rbx file path and line 1 to class_eval so that Ruby's
346
+ # backtraces point directly to the template file when errors occur.
347
+ #
348
+ # Before: view_template defined at phlex_component.rb:233 (useless)
349
+ # After: error at card_component.rbx:5:in 'view_template'
350
+ #
351
+ # @_rbx_template_path is set by load_rbx_template before we get here.
352
+ source_file = @_rbx_template_path || __FILE__
353
+ class_eval(<<~RUBY, source_file, 1)
354
+ def view_template
355
+ #{compiled_code}
356
+ end
357
+ RUBY
358
+ end
359
+ end
360
+ end
361
+ end
@@ -0,0 +1,70 @@
1
+ require "phlex"
2
+ require "phlex-rails"
3
+
4
+ module Grsx
5
+ # A Phlex::HTML subclass that serves as the execution context for compiled
6
+ # .rbx templates when Grsx.configuration.render_target == :phlex.
7
+ #
8
+ # Instead of ActionView's @output_buffer, compiled Phlex-target code calls
9
+ # Phlex element methods directly (div, span, etc.) and renders component
10
+ # instances via render().
11
+ class PhlexRuntime < Phlex::HTML
12
+ # Include ALL phlex-rails helper adapters (mirrors PhlexComponent).
13
+ Phlex::Rails::Helpers.constants.each do |helper_name|
14
+ begin
15
+ mod = Phlex::Rails::Helpers.const_get(helper_name)
16
+ include mod if mod.is_a?(Module)
17
+ rescue NameError
18
+ # Skip helpers that require Rails to be fully initialized
19
+ end
20
+ end
21
+
22
+ # Called by PhlexCompiler-generated code for inline expressions: {expr}
23
+ #
24
+ # render(<Comp />) already wrote to buffer → returns nil → no-op here
25
+ # Array/Enumerable → each element rendered recursively
26
+ # Phlex::SafeObject → raw()
27
+ # nil / false / "" → silent no-op (safe for &&, ||, ternary)
28
+ # anything else → plain(value.to_s) CGI-escaped
29
+ def __rbx_expr_out(value)
30
+ case value
31
+ when nil, false, ""
32
+ nil
33
+ when Array, Enumerable
34
+ value.each { |v| __rbx_expr_out(v) }
35
+ when Phlex::SGML
36
+ render(value)
37
+ when Phlex::SGML::SafeObject
38
+ raw(value)
39
+ else
40
+ plain(value.to_s)
41
+ end
42
+ end
43
+
44
+ # Return nil from render() so that {cond && <Comp />} doesn't double-render.
45
+ # The actual rendering is a side-effect on the buffer, not the return value.
46
+ def render(renderable = nil, &block)
47
+ super
48
+ nil
49
+ end
50
+
51
+ def initialize(assigns: {})
52
+ assigns.each { |k, v| instance_variable_set("@#{k}", v) }
53
+ end
54
+
55
+ def view_template(&block)
56
+ instance_eval(&block)
57
+ end
58
+
59
+ # Explicit escape hatch for trusted HTML strings (mirrors PhlexComponent#safe).
60
+ # WARNING: never pass user-supplied input to safe() — bypasses XSS protection.
61
+ def safe(html_string)
62
+ Phlex::SGML::SafeValue.new(html_string.to_s)
63
+ end
64
+
65
+ # Rails view_context is accessed via phlex-rails' context[:rails_view_context]
66
+ # which is set automatically during render_in. We no longer carry our own
67
+ # @view_context ivar or method_missing delegation — phlex-rails' SGML#method_missing
68
+ # provides better error hints ("Try including Phlex::Rails::Helpers::LinkTo").
69
+ end
70
+ end
@@ -0,0 +1,52 @@
1
+ module Grsx
2
+ # Walks a compiled Phlex code string (or a Grsx AST) and collects every
3
+ # @ivar name referenced in the template.
4
+ #
5
+ # This powers the `props` DSL — it lets us tell the user which props are
6
+ # actually used in a template without requiring manual declaration.
7
+ #
8
+ # Usage:
9
+ # code = Grsx.compile(template)
10
+ # names = Grsx::PropInspector.scan_code(code)
11
+ # # => [:title, :body, :user]
12
+ #
13
+ # Or directly from a parse tree:
14
+ # root = Grsx::Parser.new(tokens).parse
15
+ # names = Grsx::PropInspector.scan_tree(root)
16
+ #
17
+ class PropInspector
18
+ # Regex that matches @ivar names in the already-compiled Phlex code string.
19
+ # Matches patterns like @title, @user_name, @items123
20
+ IVAR_PATTERN = /@([a-z_][a-zA-Z0-9_]*)/.freeze
21
+
22
+ # Scan compiled Phlex code (a Ruby string) for @ivar references
23
+ # and return them as an array of symbols.
24
+ def self.scan_code(code)
25
+ code.scan(IVAR_PATTERN).map { |m| m.first.to_sym }.uniq.sort
26
+ end
27
+
28
+ # Recursively walk a Grsx AST root node and collect @ivar identifiers
29
+ # from all expression nodes. Returns a sorted, unique array of symbols.
30
+ def self.scan_tree(root)
31
+ names = []
32
+ walk(root, names)
33
+ names.sort.uniq
34
+ end
35
+
36
+ private_class_method def self.walk(node, names)
37
+ case node
38
+ when Nodes::Root
39
+ node.children.each { |c| walk(c, names) }
40
+ when Nodes::HTMLElement, Nodes::ComponentElement
41
+ node.members.each { |m| walk(m, names) }
42
+ (node.children || []).each { |c| walk(c, names) }
43
+ when Nodes::ExpressionGroup
44
+ node.members.each { |m| walk(m, names) }
45
+ when Nodes::Expression
46
+ node.content.scan(IVAR_PATTERN).each { |m| names << m.first.to_sym }
47
+ when Nodes::HTMLAttr, Nodes::ComponentProp
48
+ walk(node.value, names) if node.respond_to?(:value)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,24 @@
1
+ module Grsx
2
+ module Rails
3
+ class Engine < ::Rails::Engine
4
+ # In development, hot-reload .rbx templates for PhlexComponent subclasses
5
+ # on every request via a lightweight Rack middleware.
6
+ initializer "grsx.phlex_reloader" do |app|
7
+ dev_mode = if app.config.respond_to?(:enable_reloading)
8
+ app.config.enable_reloading
9
+ else
10
+ !app.config.cache_classes
11
+ end
12
+
13
+ if dev_mode
14
+ require "grsx/rails/phlex_reloader"
15
+ app.middleware.use Grsx::Rails::PhlexReloader
16
+ end
17
+
18
+ Grsx.configure do |config|
19
+ config.template_paths << ::Rails.root.join("app", "components")
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ module Grsx
2
+ module Rails
3
+ # Rack middleware that hot-reloads .rbx templates for all known
4
+ # PhlexComponent subclasses on every request in development.
5
+ #
6
+ # Installed automatically by the Grsx Rails engine when
7
+ # config.cache_classes / config.enable_reloading indicates dev mode.
8
+ #
9
+ # You can also install it manually:
10
+ #
11
+ # # config/application.rb
12
+ # config.middleware.use Grsx::Rails::PhlexReloader
13
+ #
14
+ class PhlexReloader
15
+ def initialize(app)
16
+ @app = app
17
+ end
18
+
19
+ def call(env)
20
+ Grsx::PhlexComponent.all_descendants.each(&:reload_rbx_template_if_changed)
21
+ @app.call(env)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,12 @@
1
+ module Grsx
2
+ class Template
3
+ attr_reader :source, :identifier
4
+
5
+ Anonymous = Class.new(String).new.freeze
6
+
7
+ def initialize(source, identifier = Anonymous)
8
+ @source = source
9
+ @identifier = identifier
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module Grsx
2
+ VERSION = "0.1.0"
3
+ end
data/lib/grsx.rb ADDED
@@ -0,0 +1,35 @@
1
+ require "grsx/version"
2
+ require "active_support/inflector"
3
+
4
+ require "grsx/rails/engine" if defined?(::Rails)
5
+
6
+ module Grsx
7
+ autoload :Lexer, "grsx/lexer"
8
+ autoload :Parser, "grsx/parser"
9
+ autoload :Nodes, "grsx/nodes"
10
+ autoload :PhlexRuntime, "grsx/phlex_runtime"
11
+ autoload :PhlexCompiler, "grsx/phlex_compiler"
12
+ autoload :PhlexComponent, "grsx/phlex_component"
13
+ autoload :PropInspector, "grsx/prop_inspector"
14
+ autoload :Configuration, "grsx/configuration"
15
+ autoload :ComponentResolver, "grsx/component_resolver"
16
+ autoload :Template, "grsx/template"
17
+
18
+ class << self
19
+ def configure
20
+ yield(configuration)
21
+ end
22
+
23
+ def configuration
24
+ @configuration ||= Configuration.new
25
+ end
26
+
27
+ # Compile a .rbx template to Phlex DSL Ruby code.
28
+ # Returns a string that can be class_eval'd inside a PhlexComponent.
29
+ def compile(template)
30
+ tokens = Lexer.new(template, configuration.element_resolver).tokenize
31
+ root = Parser.new(tokens).parse
32
+ PhlexCompiler.new(root).compile
33
+ end
34
+ end
35
+ end