rux 1.2.0 → 1.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fba8846934ea39d67f4705ce5a0ec7a477e65eccc5fc5b63a6ff1f6a2233e9ca
4
- data.tar.gz: a27256e117f07fe67fe286bc0730b265bccd91e98e18c6944aa968862b8c0e4b
3
+ metadata.gz: f1e472f55eed0269091c2f5efa3a1d27fb1d16ed1959c32a84633be8b192940d
4
+ data.tar.gz: 46e848d6871ff5ea01638d6e50e24968ad3aa9867324580417151287ed932b3d
5
5
  SHA512:
6
- metadata.gz: 62c36cfb6e05f6379f659e89c6bc3bea79144ff6eae9de80491597686b967daabe6c84b5e55dbf24ef104b9e7267aa186c370c9fae18ba8c1fcf038a3805db20
7
- data.tar.gz: 3c7c4a4b5f7ac00a344358df1e02f7bf5850aa76b0974f466b6759973f6b8400700cfc2050e6c9e1debff42ac8ad56eba9e5b018261bfa4e6dfd4b344dbbca70
6
+ metadata.gz: 1681c3f2cedc43ee87559663ada3f92d9061ed004cb4ea100a588d847b7de3cf2e4a490ec23f5d0cd653602d3f0fee1c97045c9c786e38249663a73534ab559c
7
+ data.tar.gz: 94c7c21726d2b38750162fb36ceb1f8ff9321fe99425efb078b51a042568b5914e5821341bb45fa5b18f1f6df1040f2fa706edd7ce32b834207a0046e8ac0479
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ # 1.4.0
2
+ * Implement React-style contexts for passing arguments across arbitrary levels of component nesting.
3
+ - Define a new context via `Rux.create_context`.
4
+ - Read that context later via `Rux.use_context`.
5
+
6
+ # 1.3.0
7
+ * Automatically add generated files to an ignore file, eg. .gitignore.
8
+ - Pass the --ignore-path=PATH flag to ruxc to indicate the file to update.
9
+ * Add the ruxlex executable that prints parser tokens for debugging purposes.
10
+ * Preserve Ruby comments in generated files.
11
+ * Fix the `as:` argument, which was being improperly generated in earlier versions.
12
+ * General parser improvements.
13
+ - Allows fragments to be nested within other tags.
14
+ - Allows tags after ruby code in branch structures like `if..else`.
15
+ * Allows HTML attributes to start with `@`.
16
+
1
17
  # 1.2.0
2
18
  * Improve output safety.
3
19
  - HTML tags are now automatically escaped when they come from Ruby code.
data/Gemfile CHANGED
@@ -7,3 +7,10 @@ group :development, :test do
7
7
  gem 'rake'
8
8
  gem 'rspec'
9
9
  end
10
+
11
+ group :development do
12
+ gem 'appraisal'
13
+ gem 'appraisal-run'
14
+ end
15
+
16
+ gem 'csv'
data/README.md CHANGED
@@ -268,6 +268,51 @@ Notice that the rux attribute "first-name" is passed to `MyComponent#initialize`
268
268
 
269
269
  Attributes on regular tags, i.e. non-component tags like `<div>` and `<span>`, are not modified. In other words, `<div data-foo="foo">` does _not_ become `<div data_foo="foo">` because that would be very annoying.
270
270
 
271
+ ## Context
272
+
273
+ Occasionally you might find it necessary to pass values across multiple component boundaries. A grandparent component might want to pass values to its grandchildren but not the parent components (i.e. the grandparent's direct children).
274
+
275
+ For example, imagine you'd like to support multiple themes on your website, like dark and light mode. While it would be possible to add a `theme:` argument to all your components, doing so could be quite invasive - you'd have to modify every single one.
276
+
277
+ Contexts are modeled after the React concept by the same name. They allow you to set values at one level of the component hierarchy and consume them at a deeper one, without having to pass the values through every intermediate level.
278
+
279
+ First, create a new context, passing an optional default value or default block. Note that the value stored in a context can be anything, i.e. any Ruby object.
280
+
281
+ ```ruby
282
+ # with a default value
283
+ ThemeContext = Rux.create_context("dark")
284
+
285
+ # with a default block
286
+ ThemeContext = Rux.create_context { "dark" }
287
+ ```
288
+
289
+ NOTE: passing a default block takes precedence over default values. In other words, if you pass both an argument _and_ a block to `create_context`, only the block will be used to produce default values.
290
+
291
+ The `create_context` method returns a component class. Rux requires that all components start with an uppercase letter, i.e. be a Ruby constant, which is why we assigned it to the `ThemeContext` constant in the example above. You can now render the context component like you would any other component:
292
+
293
+ ```ruby
294
+ <ThemeContext value="light">
295
+ <Heading>Welcome!</Heading>
296
+ <WelcomePage />
297
+ </ThemeContext>
298
+ ```
299
+
300
+ Now that we've defined a context and rendered it, any component rendered inside `ThemeContext` (i.e. any of its children) can access the context like this:
301
+
302
+ ```ruby
303
+ class WelcomePage < ViewComponent::Base
304
+ def call
305
+ theme = Rux.use_context(ThemeContext)
306
+
307
+ <div>
308
+ <h2 style={theme == "dark" ? "color: #FFF" : "color: #000"}>
309
+ Welcome to my site!
310
+ </h2>
311
+ </div>
312
+ end
313
+ end
314
+ ```
315
+
271
316
  ## How it Works
272
317
 
273
318
  Translating rux code (Ruby + HTML tags) into Ruby code happens in three phases: lexing, parsing, and emitting. The lexer phase is implemented as a wrapper around the lexer from the [Parser gem](https://github.com/whitequark/parser) that looks for specific patterns in the token stream. When it finds an opening HTML tag, it hands off lexing to the rux lexer. When the tag ends, the lexer continues emitting Ruby tokens, and so on.
data/bin/ruxc CHANGED
@@ -5,6 +5,7 @@ $:.push(File.expand_path('./lib'))
5
5
  require 'pathname'
6
6
  require 'optparse'
7
7
  require 'rux'
8
+ require 'onload'
8
9
 
9
10
  class RuxCLI
10
11
  def self.parse(argv)
@@ -15,7 +16,8 @@ class RuxCLI
15
16
 
16
17
  options = {
17
18
  pretty: true,
18
- stdout: false
19
+ stdout: false,
20
+ ignore_path: nil,
19
21
  }
20
22
 
21
23
  if argv.first != '-h' && argv.first != '--help'
@@ -41,6 +43,14 @@ class RuxCLI
41
43
  end
42
44
  end
43
45
 
46
+ oneline(<<~DESC).tap do |desc|
47
+ Update the given git ignore file with transpiled file paths.
48
+ DESC
49
+ opts.on('-i', '--ignore-path=PATH', desc) do |ignore_path|
50
+ options[:ignore_path] = ignore_path
51
+ end
52
+ end
53
+
44
54
  opts.on('-h', '--help', 'Prints this help info') do
45
55
  puts opts
46
56
  exit
@@ -91,8 +101,17 @@ class RuxCLI
91
101
  @options[:stdout]
92
102
  end
93
103
 
104
+ def ignore_file
105
+ return nil unless ignore_path
106
+ @ignore_file ||= Onload::IgnoreFile.load(ignore_path)
107
+ end
108
+
94
109
  private
95
110
 
111
+ def ignore_path
112
+ @options[:ignore_path]
113
+ end
114
+
96
115
  def directory?
97
116
  File.directory?(in_path)
98
117
  end
@@ -101,7 +120,11 @@ end
101
120
  cli = RuxCLI.parse(ARGV)
102
121
  cli.validate
103
122
 
104
- cli.each_file do |in_file, out_file, rbi_file|
123
+ at_exit do
124
+ cli.ignore_file&.persist!
125
+ end
126
+
127
+ cli.each_file do |in_file, out_file|
105
128
  rux_file = Rux::File.new(in_file)
106
129
 
107
130
  if cli.write_to_stdout?
@@ -109,5 +132,7 @@ cli.each_file do |in_file, out_file, rbi_file|
109
132
  else
110
133
  rux_file.write(out_file, pretty: cli.pretty?)
111
134
  puts "Wrote #{out_file}"
135
+
136
+ cli.ignore_file&.add(out_file)
112
137
  end
113
138
  end
data/bin/ruxlex ADDED
@@ -0,0 +1,13 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ $:.push(File.expand_path('./lib'))
4
+
5
+ require 'rux'
6
+
7
+ code = STDIN.read
8
+ result = Rux::Lexer.lex(code)
9
+
10
+ result.each do |(type, (str, loc))|
11
+ loc_str = loc.to_range.to_s.rjust(12, " ")
12
+ puts "#{loc_str} #{type} #{str.inspect}"
13
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "use_context"
4
+ require "securerandom"
5
+
6
+ module Rux
7
+ class ContextBase
8
+ include UseContext::ContextMethods
9
+
10
+ class << self
11
+ attr_accessor :default_value, :default_value_block
12
+
13
+ def context_key
14
+ @context_key ||= "#{context_name}_context"
15
+ end
16
+
17
+ def context_name
18
+ @context_name ||= self.name || SecureRandom.hex
19
+ end
20
+ end
21
+
22
+ attr_reader :value
23
+
24
+ def initialize(**kwargs)
25
+ if kwargs.include?(:value)
26
+ @value = kwargs[:value]
27
+ else
28
+ @value = if self.class.default_value_block
29
+ self.class.default_value_block.call
30
+ else
31
+ self.class.default_value
32
+ end
33
+ end
34
+ end
35
+
36
+ def render_in(_view_context, &block)
37
+ provide_context(self.class.context_key, { value: value }) do
38
+ block.call if block
39
+ end
40
+ end
41
+ end
42
+
43
+ module Context
44
+ class << self
45
+ include UseContext::ContextMethods
46
+
47
+ def create(default_value = nil, &default_value_block)
48
+ Class.new(ContextBase).tap do |klass|
49
+ klass.default_value = default_value
50
+ klass.default_value_block = default_value_block
51
+ end
52
+ end
53
+
54
+ def use(context_class)
55
+ use_context(context_class.context_key, :value)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -32,7 +32,7 @@ module Rux
32
32
  def visit_tag(node)
33
33
  ''.tap do |result|
34
34
  block_arg = if (as = node.attrs.get('as'))
35
- visit(as)
35
+ visit(as.value)
36
36
  end
37
37
 
38
38
  block_arg ||= "rux_block_arg#{@render_stack.size}"
@@ -71,10 +71,7 @@ module Rux
71
71
  result << "|#{block_arg}| " if block_arg && node.component?
72
72
  result << "Rux.create_buffer.tap { |_rux_buf_| "
73
73
 
74
- node.children.each do |child|
75
- result << append_statement_for(child)
76
- end
77
-
74
+ result << visit_tag_children(node).join
78
75
  result << " }.to_s }"
79
76
  end
80
77
 
@@ -88,11 +85,17 @@ module Rux
88
85
  end
89
86
  end
90
87
 
88
+ def visit_tag_children(node)
89
+ node.children.map do |child|
90
+ append_statement_for(child)
91
+ end
92
+ end
93
+
91
94
  def append_statement_for(node)
92
95
  if node.is_a?(AST::TextNode)
93
- "_rux_buf_.safe_append(#{visit(node).strip});"
96
+ "_rux_buf_.safe_append(#{visit(node).strip.chomp(';')});"
94
97
  else
95
- "_rux_buf_.append(#{visit(node).strip});"
98
+ "_rux_buf_.append(#{visit(node).strip.chomp(';')});"
96
99
  end
97
100
  end
98
101
 
@@ -1,50 +1,51 @@
1
- ,[<],[a-zA-Z0-9_-_---:-:],[>],[/],(space),[=],"[""]",['],"[^""]",[^'],[{],[}],(default)
2
- start,tag_open_test,,,,,,,,,,literal_ruby_code_start,,literal_body
3
- tag_open_test,,tag_open_start[0],fragment_open,tag_close_start,,,,,,,,,
4
- tag_open_start*,,tag_open_body,,,,,,,,,,,
5
- tag_open_body,,tag_open_body,tag_open[0],tag_self_closing[0],tag_open[0],,,,,,,,
6
- tag_open*,,,tag_open_end,,attribute_spaces_body,,,,,,,,
7
- tag_open_end*,,,,,,,,,,,,,
8
- tag_close_start*,,tag_close_body,fragment_close,,,,,,,,,,
9
- tag_close_body,,tag_close_body,tag_close[0],,tag_close[0],,,,,,,,
10
- tag_close*,,,tag_close_end,tag_self_closing[0],tag_close_spaces_body,,,,,,,,
11
- tag_close_spaces_body,,,tag_close_spaces[0],,tag_close_spaces_body,,,,,,,,
12
- tag_close_spaces*,,,tag_close_end,,,,,,,,,,
13
- tag_close_end*,,,,,,,,,,,,,
14
- tag_self_closing*,,,,tag_self_closing_start,,,,,,,,,
15
- tag_self_closing_start,,,tag_self_closing_end,,,,,,,,,,
16
- tag_self_closing_end*,,,,,,,,,,,,,
17
- fragment_open*,,,,,,,,,,,,,
18
- fragment_close*,,,,,,,,,,,,,
19
- ,,,,,,,,,,,,,
20
- attribute_spaces_body,,attribute_spaces[0],attribute_spaces[0],attribute_spaces[0],attribute_spaces_body,,,,,,attribute_spaces[0],,
21
- attribute_spaces*,,attribute_name_body,tag_close_start,tag_self_closing_start,,,,,,,attribute_ruby_code_start,,
22
- attribute_name_body,,attribute_name_body,attribute_name[0],attribute_name[0],attribute_name[0],attribute_name[0],,,,,,,
23
- attribute_name*,,,tag_open_end,tag_self_closing_start,attribute_equals_spaces_body,attribute_equals,,,,,,,
24
- attribute_equals_spaces_body,,,attribute_equals_spaces[0],,attribute_equals_spaces_body,attribute_equals_spaces[0],,,,,,,attribute_equals_spaces[0]
25
- attribute_equals_spaces*,,,tag_open_end,tag_self_closing_start,,attribute_equals,,,,,,,attribute_name_body
26
- attribute_equals*,,attribute_value_uq_body,,,attribute_value_spaces_body[0],,attribute_value_dq_start,attribute_value_sq_start,,,attribute_value_ruby_code_start,,
27
- attribute_value_spaces_body,,attribute_value_spaces[0],attribute_value_spaces[0],attribute_value_spaces[0],attribute_value_spaces_body,,attribute_value_spaces[0],attribute_value_spaces[0],,,attribute_value_spaces[0],,
28
- attribute_value_spaces*,,attribute_value_uq_body,tag_open_end,tag_self_closing_start,,,attribute_value_dq_start,attribute_value_sq_start,,,attribute_value_ruby_code_start,,
29
- attribute_value_dq_start*,,,,,,,,,attribute_value_dq_body,,,,
30
- attribute_value_sq_start*,,,,,,,,,,attribute_value_sq_body,,,
31
- attribute_value_dq_body,,,,,,,attribute_dq_value[0],,attribute_value_dq_body,,,,
32
- attribute_value_sq_body,,,,,,,,attribute_sq_value[0],,attribute_value_sq_body,,,
33
- attribute_value_uq_body,,attribute_value_uq_body,attribute_uq_value[0],attrIbute_uq_value[0],attribute_uq_value[0],,,,,,,,
34
- attribute_value_dq_end*,,attribute_name,tag_open_end,tag_self_closing_start,attribute_spaces_body,,,,,,,,
35
- attribute_value_sq_end*,,attribute_name,tag_open_end,tag_self_closing_start,attribute_spaces_body,,,,,,,,
36
- attribute_value_ruby_code_start*,,,,,,,,,,,,,attribute_value_ruby_code
37
- attribute_value_ruby_code*,,,,,,,,,,,,attribute_value_ruby_code_end,
38
- attribute_value_ruby_code_end*,,attribute_name,tag_open_end,tag_self_closing_start,attribute_spaces_body,,,,,,,,
39
- attribute_dq_value*,,,,,,,attribute_value_dq_end,,,,,,
40
- attribute_sq_value*,,,,,,,,attribute_value_sq_end,,,,,
41
- attribute_uq_value*,,attribute_name,tag_open_end,tag_self_closing_start,attribute_spaces_body,,,,,,,,
42
- attribute_ruby_code_start*,,,,,,,,,,,,,attribute_ruby_code[0]
43
- attribute_ruby_code*,,,,,,,,,,,,attribute_ruby_code_end,
44
- attribute_ruby_code_end*,,attribute_name,tag_open_end,tag_self_closing_start,attribute_spaces_body,,,,,,,,
45
- ,,,,,,,,,,,,,
46
- literal_body,literal[0],,,,,,,,,,literal[0],,literal_body
47
- literal*,,,,,,,,,,,literal_ruby_code_start,,
48
- literal_ruby_code_start*,,,,,,,,,,,,,literal_ruby_code[0]
49
- literal_ruby_code*,,,,,,,,,,,,literal_ruby_code_end,
50
- literal_ruby_code_end*,,,,,,,,,,,,,
1
+ ,[<],[@-@.-.],[a-zA-Z0-9_-_---:-:],[>],[/],(space),[=],"[""]",['],"[^""]",[^'],[{],[}],(default)
2
+ start,tag_open_test,,,,,,,,,,,literal_ruby_code_start,,literal_body
3
+ tag_open_test,,,tag_open_start[0],fragment_open,tag_close_start,,,,,,,,,
4
+ tag_open_start*,,,tag_open_body,,,,,,,,,,,
5
+ tag_open_body,,,tag_open_body,tag_open[0],tag_self_closing[0],tag_open[0],,,,,,,,
6
+ tag_open*,,,,tag_open_end,,attribute_spaces_body,,,,,,,,
7
+ tag_open_end*,,,,,,,,,,,,,,
8
+ tag_close_start*,,,tag_close_body,fragment_close,,,,,,,,,,
9
+ tag_close_body,,,tag_close_body,tag_close[0],,tag_close[0],,,,,,,,
10
+ tag_close*,,,,tag_close_end,tag_self_closing[0],tag_close_spaces_body,,,,,,,,
11
+ tag_close_spaces_body,,,,tag_close_spaces[0],,tag_close_spaces_body,,,,,,,,
12
+ tag_close_spaces*,,,,tag_close_end,,,,,,,,,,
13
+ tag_close_end*,,,,,,,,,,,,,,
14
+ tag_self_closing*,,,,,tag_self_closing_start,,,,,,,,,
15
+ tag_self_closing_start,,,,tag_self_closing_end,,,,,,,,,,
16
+ tag_self_closing_end*,,,,,,,,,,,,,,
17
+ fragment_open*,,,,,,,,,,,,,,
18
+ fragment_close*,,,,,,,,,,,,,,
19
+ ,,,,,,,,,,,,,,
20
+ attribute_spaces_body,,attribute_spaces[0],attribute_spaces[0],attribute_spaces[0],attribute_spaces[0],attribute_spaces_body,,,,,,attribute_spaces[0],,
21
+ attribute_spaces*,,attribute_name_sigil,attribute_name_body,tag_open_end,tag_self_closing_start,,,,,,,attribute_ruby_code_start,,
22
+ attribute_name_sigil,,,,,,,,,,,,,,attribute_name_body
23
+ attribute_name_body,,,attribute_name_body,attribute_name[0],attribute_name[0],attribute_name[0],attribute_name[0],,,,,,,
24
+ attribute_name*,,,,tag_open_end,tag_self_closing_start,attribute_equals_spaces_body,attribute_equals,,,,,,,
25
+ attribute_equals_spaces_body,,,,attribute_equals_spaces[0],,attribute_equals_spaces_body,attribute_equals_spaces[0],,,,,,,attribute_equals_spaces[0]
26
+ attribute_equals_spaces*,,,,tag_open_end,tag_self_closing_start,,attribute_equals,,,,,,,attribute_name_sigil
27
+ attribute_equals*,,,attribute_value_uq_body,,,attribute_value_spaces_body[0],,attribute_value_dq_start,attribute_value_sq_start,,,attribute_value_ruby_code_start,,
28
+ attribute_value_spaces_body,,,attribute_value_spaces[0],attribute_value_spaces[0],attribute_value_spaces[0],attribute_value_spaces_body,,attribute_value_spaces[0],attribute_value_spaces[0],,,attribute_value_spaces[0],,
29
+ attribute_value_spaces*,,,attribute_value_uq_body,tag_open_end,tag_self_closing_start,,,attribute_value_dq_start,attribute_value_sq_start,,,attribute_value_ruby_code_start,,
30
+ attribute_value_dq_start*,,,,,,,,,,attribute_value_dq_body,,,,
31
+ attribute_value_sq_start*,,,,,,,,,,,attribute_value_sq_body,,,
32
+ attribute_value_dq_body,,,,,,,,attribute_dq_value[0],,attribute_value_dq_body,,,,
33
+ attribute_value_sq_body,,,,,,,,,attribute_sq_value[0],,attribute_value_sq_body,,,
34
+ attribute_value_uq_body,,,attribute_value_uq_body,attribute_uq_value[0],attrIbute_uq_value[0],attribute_uq_value[0],,,,,,,,
35
+ attribute_value_dq_end*,,,attribute_name,tag_open_end,tag_self_closing_start,attribute_spaces_body,,,,,,,,
36
+ attribute_value_sq_end*,,,attribute_name,tag_open_end,tag_self_closing_start,attribute_spaces_body,,,,,,,,
37
+ attribute_value_ruby_code_start*,,,,,,,,,,,,,,attribute_value_ruby_code
38
+ attribute_value_ruby_code*,,,,,,,,,,,,,attribute_value_ruby_code_end,
39
+ attribute_value_ruby_code_end*,,,attribute_name,tag_open_end,tag_self_closing_start,attribute_spaces_body,,,,,,,,
40
+ attribute_dq_value*,,,,,,,,attribute_value_dq_end,,,,,,
41
+ attribute_sq_value*,,,,,,,,,attribute_value_sq_end,,,,,
42
+ attribute_uq_value*,,,attribute_name,tag_open_end,tag_self_closing_start,attribute_spaces_body,,,,,,,,
43
+ attribute_ruby_code_start*,,,,,,,,,,,,,,attribute_ruby_code[0]
44
+ attribute_ruby_code*,,,,,,,,,,,,,attribute_ruby_code_end,
45
+ attribute_ruby_code_end*,,,attribute_name,tag_open_end,tag_self_closing_start,attribute_spaces_body,,,,,,,,
46
+ ,,,,,,,,,,,,,,
47
+ literal_body,literal[0],,,,,,,,,,,literal[0],,literal_body
48
+ literal*,,,,,,,,,,,,literal_ruby_code_start,,
49
+ literal_ruby_code_start*,,,,,,,,,,,,,,literal_ruby_code[0]
50
+ literal_ruby_code*,,,,,,,,,,,,,literal_ruby_code_end,
51
+ literal_ruby_code_end*,,,,,,,,,,,,,,
data/lib/rux/lexer.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'parser'
2
+
1
3
  module Rux
2
4
  class Lexer
3
5
  class EOFError < StandardError; end
@@ -5,6 +7,13 @@ module Rux
5
7
 
6
8
  attr_reader :source_buffer
7
9
 
10
+ class << self
11
+ def lex(str)
12
+ buffer = ::Parser::Source::Buffer.new('(source)', source: str)
13
+ new(buffer).to_a
14
+ end
15
+ end
16
+
8
17
  def initialize(source_buffer)
9
18
  @source_buffer = source_buffer
10
19
  @stack = [RubyLexer.new(source_buffer, 0)]
@@ -17,6 +26,10 @@ module Rux
17
26
  [nil, ['$eof']]
18
27
  end
19
28
 
29
+ def to_a
30
+ @generator.to_a
31
+ end
32
+
20
33
  private
21
34
 
22
35
  def each_token
data/lib/rux/parser.rb CHANGED
@@ -8,14 +8,18 @@ module Rux
8
8
  class << self
9
9
  def parse_file(path)
10
10
  buffer = ::Parser::Source::Buffer.new(path).read
11
- lexer = ::Rux::Lexer.new(buffer)
12
- new(lexer).parse
11
+ new(make_lexer(buffer)).parse
13
12
  end
14
13
 
15
14
  def parse(str)
16
15
  buffer = ::Parser::Source::Buffer.new('(source)', source: str)
17
- lexer = ::Rux::Lexer.new(buffer)
18
- new(lexer).parse
16
+ new(make_lexer(buffer)).parse
17
+ end
18
+
19
+ private
20
+
21
+ def make_lexer(buffer)
22
+ ::Rux::Lexer.new(buffer)
19
23
  end
20
24
  end
21
25
 
@@ -137,6 +141,8 @@ module Rux
137
141
  if is?(:tRUX_LITERAL, :tRUX_LITERAL_RUBY_CODE_START)
138
142
  lit = literal
139
143
  node.children << lit if lit
144
+ elsif is?(:tRUX_FRAGMENT_OPEN)
145
+ node.children << fragment
140
146
  else
141
147
  node.children << tag
142
148
  end
@@ -1,4 +1,18 @@
1
1
  module Rux
2
+ class TokenArrayProxy < Array
3
+ def initialize(rux_token_queue)
4
+ @rux_token_queue = rux_token_queue
5
+ end
6
+
7
+ def push(token)
8
+ if token[0] == :tCOMMENT
9
+ @rux_token_queue.push(token)
10
+ end
11
+
12
+ super
13
+ end
14
+ end
15
+
2
16
  class RubyLexer < ::Parser::Lexer
3
17
  # These are populated when ::Parser::Lexer loads and are therefore
4
18
  # not inherited. We have to copy them over manually.
@@ -15,6 +29,8 @@ module Rux
15
29
  @generator = to_enum(:each_token)
16
30
  @rux_token_queue = []
17
31
  @p = init_pos
32
+
33
+ self.tokens = TokenArrayProxy.new(@rux_token_queue)
18
34
  end
19
35
 
20
36
  alias_method :advance_orig, :advance
@@ -27,6 +43,7 @@ module Rux
27
43
  @ts = @te = @p = pos
28
44
  @eof = false
29
45
  @rux_token_queue.clear
46
+ clear_queue
30
47
  populate_queue
31
48
  end
32
49
 
@@ -44,13 +61,7 @@ module Rux
44
61
  end
45
62
 
46
63
  def each_token(&block)
47
- # We detect whether or not we're at the beginning of a rux tag by looking
48
- # ahead by 1 token; that's why the first element in @rux_token_queue is
49
- # yielded immediately. If the lexer _starts_ at a rux tag however,
50
- # lookahead is a lot more difficult. To mitigate, we insert a dummy skip
51
- # token here. That way, at_rux? checks the right tokens in the queue and
52
- # correctly identifies the start of a rux tag.
53
- @rux_token_queue << [:tSKIP, ['$skip', make_range(@p, @p)]]
64
+ clear_queue
54
65
 
55
66
  @eof = false
56
67
  curlies = 1
@@ -74,7 +85,7 @@ module Rux
74
85
  type, (_, pos) = token
75
86
 
76
87
  case type
77
- when :tLCURLY, :tLBRACE
88
+ when :tLCURLY, :tLBRACE, :tLAMBEG
78
89
  curlies += 1
79
90
  when :tRCURLY, :tRBRACE
80
91
  curlies -= 1
@@ -111,6 +122,18 @@ module Rux
111
122
  end
112
123
  end
113
124
 
125
+ def clear_queue
126
+ @rux_token_queue.clear
127
+
128
+ # We detect whether or not we're at the beginning of a rux tag by looking
129
+ # ahead by 1 token; that's why the first element in @rux_token_queue is
130
+ # yielded immediately. If the lexer _starts_ at a rux tag however,
131
+ # lookahead is a lot more difficult. To mitigate, we insert a dummy skip
132
+ # token here. That way, at_rux? checks the right tokens in the queue and
133
+ # correctly identifies the start of a rux tag.
134
+ @rux_token_queue << [:tSKIP, ['$skip', make_range(@p, @p)]]
135
+ end
136
+
114
137
  def at_rux?
115
138
  at_lt? && !at_inheritance?
116
139
  end
data/lib/rux/rux_lexer.rb CHANGED
@@ -123,7 +123,7 @@ module Rux
123
123
 
124
124
  unless cur_trans
125
125
  raise Rux::Lexer::TransitionError,
126
- "no transition found from #{cur_state} at position #{@p} while "\
126
+ "no transition found from #{cur_state} for #{chr.inspect} at position #{@p} while "\
127
127
  'lexing rux code'
128
128
  end
129
129
 
data/lib/rux/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Rux
2
- VERSION = '1.2.0'
2
+ VERSION = '1.4.0'
3
3
  end
data/lib/rux.rb CHANGED
@@ -20,6 +20,7 @@ module Rux
20
20
  autoload :AST, 'rux/ast'
21
21
  autoload :Buffer, 'rux/buffer'
22
22
  autoload :Component, 'rux/component'
23
+ autoload :Context, 'rux/context'
23
24
  autoload :DefaultTagBuilder, 'rux/default_tag_builder'
24
25
  autoload :DefaultVisitor, 'rux/default_visitor'
25
26
  autoload :File, 'rux/file'
@@ -38,9 +39,8 @@ module Rux
38
39
  ruby_code = visitor.visit(Parser.parse(str))
39
40
  return ruby_code unless pretty
40
41
 
41
- ::Unparser.unparse(
42
- ::Parser::CurrentRuby.parse(ruby_code)
43
- )
42
+ ast, comments = *::Parser::CurrentRuby.parse_with_comments(ruby_code)
43
+ ::Unparser.unparse(ast, comments: comments)
44
44
  end
45
45
 
46
46
  def default_visitor
@@ -66,6 +66,14 @@ module Rux
66
66
  def library_paths
67
67
  @library_paths ||= []
68
68
  end
69
+
70
+ def create_context(...)
71
+ Rux::Context.create(...)
72
+ end
73
+
74
+ def use_context(...)
75
+ Rux::Context.use(...)
76
+ end
69
77
  end
70
78
 
71
79
  self.tag_builder = self.default_tag_builder
data/rux.gemspec CHANGED
@@ -10,11 +10,14 @@ Gem::Specification.new do |s|
10
10
  s.description = s.summary = 'A jsx-inspired way to write view components.'
11
11
  s.platform = Gem::Platform::RUBY
12
12
 
13
+ s.add_dependency 'onload', '~> 1.1'
13
14
  s.add_dependency 'parser', '~> 3.0'
14
- s.add_dependency 'unparser', '~> 0.6'
15
+ s.add_dependency 'unparser', '~> 0.8'
16
+ s.add_dependency 'use_context', '~> 1.2'
15
17
 
16
18
  s.require_path = 'lib'
17
19
  s.executables << 'ruxc'
20
+ s.executables << 'ruxlex'
18
21
 
19
22
  s.files = Dir['{lib,spec}/**/*', 'Gemfile', 'LICENSE', 'CHANGELOG.md', 'README.md', 'Rakefile', 'rux.gemspec']
20
23
  end
@@ -0,0 +1,84 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'context', type: :render do
4
+ FooContext = Rux.create_context("foo")
5
+ FooContextWithBlock = Rux.create_context { "foo from block" }
6
+
7
+ class FooComponent < ViewComponent::Base
8
+ def call
9
+ context = Rux.use_context(FooContext)
10
+ Rux::SafeString.new("<p>#{context}</p>")
11
+ end
12
+ end
13
+
14
+ class FooComponentWithBlock < ViewComponent::Base
15
+ def call
16
+ context = Rux.use_context(FooContextWithBlock)
17
+ Rux::SafeString.new("<p>#{context}</p>")
18
+ end
19
+ end
20
+
21
+ describe '#context_key' do
22
+ it 'uses the class name in the key' do
23
+ expect(FooContext.context_key).to eq("FooContext_context")
24
+ expect(FooContextWithBlock.context_key).to eq("FooContextWithBlock_context")
25
+ end
26
+
27
+ it "uses a random ID if the class doesn't have a name" do
28
+ no_name = Rux.create_context
29
+ expect(no_name.context_key).to match(/[a-f0-9-]{32}_context/)
30
+ end
31
+
32
+ it 'returns the same value when called multiple times' do
33
+ no_name = Rux.create_context
34
+ key = no_name.context_key
35
+ expect(no_name.context_key).to eq(key)
36
+ end
37
+ end
38
+
39
+ describe 'rendering' do
40
+ it 'uses the given context value' do
41
+ result = render(<<~RUX)
42
+ <FooContext value="bar">
43
+ <FooComponent />
44
+ </FooContext>
45
+ RUX
46
+
47
+ expect(result).to eq("<p>bar</p>")
48
+ end
49
+
50
+ it 'uses the default value when no value is provided' do
51
+ result = render(<<~RUX)
52
+ <FooContext>
53
+ <FooComponent />
54
+ </FooContext>
55
+ RUX
56
+
57
+ expect(result).to eq("<p>foo</p>")
58
+ end
59
+
60
+ it 'calls the default block when provided' do
61
+ result = render(<<~RUX)
62
+ <FooContextWithBlock>
63
+ <FooComponentWithBlock />
64
+ </FooContextWithBlock>
65
+ RUX
66
+
67
+ expect(result).to eq("<p>foo from block</p>")
68
+ end
69
+
70
+ it 'allows context to be temporarily overridden' do
71
+ result = render(<<~RUX)
72
+ <FooContext value="bar">
73
+ <FooComponent />
74
+ <FooContext value="baz">
75
+ <FooComponent />
76
+ </FooContext>
77
+ <FooComponent />
78
+ </FooContext>
79
+ RUX
80
+
81
+ expect(result).to eq("<p>bar</p><p>baz</p><p>bar</p>")
82
+ end
83
+ end
84
+ end
@@ -94,4 +94,28 @@ describe 'attributes', type: :parser do
94
94
  Rux.tag("div", { :"data-foo" => "bar" })
95
95
  RUBY
96
96
  end
97
+
98
+ it 'yields the component instance to the block using the as: argument for the variable name' do
99
+ code = <<~RUX
100
+ <Hello as={hello}>
101
+ {hello.foo}
102
+ </Hello>
103
+ RUX
104
+ expect(compile(code)).to eq(<<~RUBY.strip)
105
+ render(Hello.new) { |hello|
106
+ Rux.create_buffer.tap { |_rux_buf_|
107
+ _rux_buf_.append(hello.foo)
108
+ }.to_s
109
+ }
110
+ RUBY
111
+ end
112
+
113
+ it 'allows attributes to start with @' do
114
+ code = <<~RUX
115
+ <div @click="alert('foo')" />
116
+ RUX
117
+ expect(compile(code)).to eq(<<~RUBY.strip)
118
+ Rux.tag("div", { :@click => "alert('foo')" })
119
+ RUBY
120
+ end
97
121
  end
@@ -44,4 +44,21 @@ describe 'fragments', type: :parser do
44
44
  }
45
45
  RUBY
46
46
  end
47
+
48
+ it 'allows fragments nested inside other tags' do
49
+ code = <<~RUX
50
+ <div>
51
+ <>{"foo"}</>
52
+ </div>
53
+ RUX
54
+ expect(compile(code)).to eq(<<~RUBY.strip)
55
+ Rux.tag("div") {
56
+ Rux.create_buffer.tap { |_rux_buf_|
57
+ _rux_buf_.append(Rux.create_buffer.tap { |_rux_buf_|
58
+ _rux_buf_.append("foo")
59
+ }.to_s)
60
+ }.to_s
61
+ }
62
+ RUBY
63
+ end
47
64
  end
data/spec/parser_spec.rb CHANGED
@@ -10,7 +10,7 @@ describe 'parsing', type: :parser do
10
10
  it 'raises an error when no state transition can be found' do
11
11
  expect { compile('<Hello <foo>') }.to(
12
12
  raise_error(Rux::Lexer::TransitionError,
13
- 'no transition found from tRUX_ATTRIBUTE_SPACES_BODY at position 7 '\
13
+ 'no transition found from tRUX_ATTRIBUTE_SPACES_BODY for "<" at position 7 '\
14
14
  'while lexing rux code')
15
15
  )
16
16
  end
@@ -63,4 +63,53 @@ describe 'parsing', type: :parser do
63
63
  }
64
64
  RUBY
65
65
  end
66
+
67
+ it 'preserves comments' do
68
+ code = <<~RUX
69
+ # frozen_string_literal: true
70
+
71
+ class Foo
72
+ def call
73
+ <p>Hello</p>
74
+ end
75
+ end
76
+ RUX
77
+ expect(compile(code)).to eq(<<~RUBY)
78
+ # frozen_string_literal: true
79
+ class Foo
80
+ def call
81
+ Rux.tag("p") {
82
+ Rux.create_buffer.tap { |_rux_buf_|
83
+ _rux_buf_.safe_append("Hello")
84
+ }.to_s
85
+ }
86
+ end
87
+ end
88
+ RUBY
89
+ end
90
+
91
+ it 'allows a tag after ruby code in a branch situation' do
92
+ code = <<~RUX
93
+ <div>
94
+ {if foo
95
+ <>foo</>
96
+ else
97
+ <div></div>
98
+ end}
99
+ </div>
100
+ RUX
101
+ expect(compile(code)).to eq(<<~RUBY.strip)
102
+ Rux.tag("div") {
103
+ Rux.create_buffer.tap { |_rux_buf_|
104
+ _rux_buf_.append(if foo
105
+ Rux.create_buffer.tap { |_rux_buf_|
106
+ _rux_buf_.safe_append("foo")
107
+ }.to_s
108
+ else
109
+ Rux.tag("div")
110
+ end)
111
+ }.to_s
112
+ }
113
+ RUBY
114
+ end
66
115
  end
data/spec/render_spec.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe 'rendering', type: :render do
4
- it 'handles a HTML tags inside ruby code' do
4
+ it 'handles HTML tags inside ruby code' do
5
5
  result = render(<<~RUBY)
6
6
  <div>
7
7
  {3.times.map do
@@ -7,8 +7,7 @@ module ViewComponent
7
7
  end
8
8
 
9
9
  def to_s
10
- @component_instance.content = @content_block.call(@component_instance) if @content_block
11
- @component_instance.call
10
+ @component_instance.render_in(nil, &@content_block)
12
11
  end
13
12
  end
14
13
 
@@ -40,14 +39,18 @@ module ViewComponent
40
39
  end
41
40
  end
42
41
 
43
- attr_accessor :content
44
-
45
42
  def render(component, &block)
46
- if block
47
- component.content = block.call(component)
48
- end
43
+ component.render_in(nil, &block)
44
+ end
45
+
46
+ def render_in(_view_context, &block)
47
+ @content_block = block
48
+ content # fill in slots
49
+ call
50
+ end
49
51
 
50
- component.call
52
+ def content
53
+ @content ||= @content_block&.call(self)
51
54
  end
52
55
 
53
56
  private
metadata CHANGED
@@ -1,15 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rux
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cameron Dutro
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-11-10 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: onload
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.1'
13
26
  - !ruby/object:Gem::Dependency
14
27
  name: parser
15
28
  requirement: !ruby/object:Gem::Requirement
@@ -30,19 +43,34 @@ dependencies:
30
43
  requirements:
31
44
  - - "~>"
32
45
  - !ruby/object:Gem::Version
33
- version: '0.6'
46
+ version: '0.8'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.8'
54
+ - !ruby/object:Gem::Dependency
55
+ name: use_context
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.2'
34
61
  type: :runtime
35
62
  prerelease: false
36
63
  version_requirements: !ruby/object:Gem::Requirement
37
64
  requirements:
38
65
  - - "~>"
39
66
  - !ruby/object:Gem::Version
40
- version: '0.6'
67
+ version: '1.2'
41
68
  description: A jsx-inspired way to write view components.
42
69
  email:
43
70
  - camertron@gmail.com
44
71
  executables:
45
72
  - ruxc
73
+ - ruxlex
46
74
  extensions: []
47
75
  extra_rdoc_files: []
48
76
  files:
@@ -52,6 +80,7 @@ files:
52
80
  - README.md
53
81
  - Rakefile
54
82
  - bin/ruxc
83
+ - bin/ruxlex
55
84
  - lib/rux.rb
56
85
  - lib/rux/ast.rb
57
86
  - lib/rux/ast/attr_node.rb
@@ -65,6 +94,7 @@ files:
65
94
  - lib/rux/ast/tag_node.rb
66
95
  - lib/rux/ast/text_node.rb
67
96
  - lib/rux/buffer.rb
97
+ - lib/rux/context.rb
68
98
  - lib/rux/default_tag_builder.rb
69
99
  - lib/rux/default_visitor.rb
70
100
  - lib/rux/file.rb
@@ -81,6 +111,7 @@ files:
81
111
  - lib/rux/version.rb
82
112
  - lib/rux/visitor.rb
83
113
  - rux.gemspec
114
+ - spec/context_spec.rb
84
115
  - spec/parser/attributes_spec.rb
85
116
  - spec/parser/fragment_spec.rb
86
117
  - spec/parser/html_safety_spec.rb
@@ -97,7 +128,6 @@ files:
97
128
  homepage: http://github.com/camertron/rux
98
129
  licenses: []
99
130
  metadata: {}
100
- post_install_message:
101
131
  rdoc_options: []
102
132
  require_paths:
103
133
  - lib
@@ -112,8 +142,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
112
142
  - !ruby/object:Gem::Version
113
143
  version: '0'
114
144
  requirements: []
115
- rubygems_version: 3.4.5
116
- signing_key:
145
+ rubygems_version: 3.6.7
117
146
  specification_version: 4
118
147
  summary: A jsx-inspired way to write view components.
119
148
  test_files: []