rux 1.3.0 → 1.4.1

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: e2874b5336b991b08b274df04219d2c7b97247580f0587dfc3b6860e913d8af8
4
- data.tar.gz: ba582dd96d6c58da357295e60267f4db9a6cdf47ddc99aa2740d590a1cec530d
3
+ metadata.gz: 2418beae606c99c851a3000546d6fffe20fb30e02049b41a2c2cbd8ecae427dc
4
+ data.tar.gz: 974fc8d3502737b5737f30b07c818e04918eb55b9693ea8b3d5ef629a71a1dc6
5
5
  SHA512:
6
- metadata.gz: c6c663d6d237fb6fc044dc2ebfa9c706d54eb5cdd3bc7a50011d82578ac7aab36a8f6c1ca466754f44bfed73ce708fc57bbd0ea3951681febd79f2730e7fd1d7
7
- data.tar.gz: fb52a3a10cb3db47817acc7d760210758bc733b3b74b4ebfc9d241039d8d38ce00d4622957fd26effa7593acefe4d9387ebce9bcc6bab7dd20d83e9523b62322
6
+ metadata.gz: b6ba53663a7d8fb7195962d37586beee45a9ea97f1fcbc73b0366f68c640e54ae8f11a0daafee299500c84c06526d9683ad0bb09a065f929b43096eaaae77c9d
7
+ data.tar.gz: d25ada54406d57355e85d6824ab2ac2141771e613aa9a7dabfc24f7e2732124a4e369bca0fd61025248b0661c8e80183f0016c04480253870cc3fe2f94afcb9c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ # 1.4.1
2
+ * Fix bug preventing use of curly brace-delimited block syntax, eg. `[1, 2].each { ... }`.
3
+
4
+ # 1.4.0
5
+ * Implement React-style contexts for passing arguments across arbitrary levels of component nesting.
6
+ - Define a new context via `Rux.create_context`.
7
+ - Read that context later via `Rux.use_context`.
8
+
1
9
  # 1.3.0
2
10
  * Automatically add generated files to an ignore file, eg. .gitignore.
3
11
  - Pass the --ignore-path=PATH flag to ruxc to indicate the file to update.
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  ![Unit Tests](https://github.com/camertron/rux/actions/workflows/unit_tests.yml/badge.svg?branch=master)
4
4
 
5
+ /rʌks/, rhymes with "ducks" and "trucks"
6
+
5
7
  Rux is a JSX-inspired way to write HTML tags in your Ruby code. It can be used to render view components in Rails via the [rux-rails gem](https://github.com/camertron/rux-rails). This repo however contains only the rux parser itself.
6
8
 
7
9
  ## Introduction
@@ -268,6 +270,51 @@ Notice that the rux attribute "first-name" is passed to `MyComponent#initialize`
268
270
 
269
271
  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
272
 
273
+ ## Context
274
+
275
+ 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).
276
+
277
+ 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.
278
+
279
+ 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.
280
+
281
+ 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.
282
+
283
+ ```ruby
284
+ # with a default value
285
+ ThemeContext = Rux.create_context("dark")
286
+
287
+ # with a default block
288
+ ThemeContext = Rux.create_context { "dark" }
289
+ ```
290
+
291
+ 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.
292
+
293
+ 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:
294
+
295
+ ```ruby
296
+ <ThemeContext value="light">
297
+ <Heading>Welcome!</Heading>
298
+ <WelcomePage />
299
+ </ThemeContext>
300
+ ```
301
+
302
+ 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:
303
+
304
+ ```ruby
305
+ class WelcomePage < ViewComponent::Base
306
+ def call
307
+ theme = Rux.use_context(ThemeContext)
308
+
309
+ <div>
310
+ <h2 style={theme == "dark" ? "color: #FFF" : "color: #000"}>
311
+ Welcome to my site!
312
+ </h2>
313
+ </div>
314
+ end
315
+ end
316
+ ```
317
+
271
318
  ## How it Works
272
319
 
273
320
  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.
@@ -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
@@ -0,0 +1,23 @@
1
+ module Rux
2
+ class DebugLexer
3
+ def initialize(...)
4
+ @lexer = Lexer.new(...)
5
+ @tokens = @lexer.to_a
6
+ @counter = -1
7
+ end
8
+
9
+ def source_buffer
10
+ @lexer.source_buffer
11
+ end
12
+
13
+ def advance
14
+ @counter += 1
15
+
16
+ if @counter >= @tokens.size
17
+ [nil, ['$eof']]
18
+ else
19
+ @tokens[@counter]
20
+ end
21
+ end
22
+ end
23
+ end
data/lib/rux/parser.rb CHANGED
@@ -19,7 +19,12 @@ module Rux
19
19
  private
20
20
 
21
21
  def make_lexer(buffer)
22
- ::Rux::Lexer.new(buffer)
22
+ if ENV["RUX_DEBUG"]
23
+ require "rux/debug_lexer"
24
+ ::Rux::DebugLexer.new(buffer)
25
+ else
26
+ ::Rux::Lexer.new(buffer)
27
+ end
23
28
  end
24
29
  end
25
30
 
@@ -50,19 +55,21 @@ module Rux
50
55
  curlies -= 1
51
56
  end
52
57
 
53
- break if curlies == 0
54
-
55
58
  if rb = ruby
56
59
  children << rb
57
60
  elsif type_of(current) == :tRUX_TAG_OPEN_START
58
61
  children << tag
59
62
  elsif type_of(current) == :tRUX_FRAGMENT_OPEN
60
63
  children << fragment
64
+ elsif type_of(current) == :tRUX_LITERAL_RUBY_CODE_END
65
+ # do nothing - let #literal_ruby_code consume the end token
61
66
  else
62
67
  raise UnexpectedTokenError,
63
68
  'expected ruby code or the start of a rux tag but found '\
64
69
  "#{type_of(current)} instead"
65
70
  end
71
+
72
+ break if curlies == 0
66
73
  end
67
74
 
68
75
  AST::ListNode.new(children)
@@ -306,7 +313,7 @@ module Rux
306
313
  def consume(*types)
307
314
  if !types.include?(type_of(current))
308
315
  raise UnexpectedTokenError,
309
- "expected [#{types.map(&:to_s).join(', ')}], got '#{type_of(current)}'"
316
+ "expected [#{types.map(&:to_s).join(', ')}], got '#{type_of(current)}' at #{pos_of(current)}"
310
317
  end
311
318
 
312
319
  @current = get_next
data/lib/rux/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Rux
2
- VERSION = '1.3.0'
2
+ VERSION = '1.4.1'
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'
@@ -65,6 +66,14 @@ module Rux
65
66
  def library_paths
66
67
  @library_paths ||= []
67
68
  end
69
+
70
+ def create_context(...)
71
+ Rux::Context.create(...)
72
+ end
73
+
74
+ def use_context(...)
75
+ Rux::Context.use(...)
76
+ end
68
77
  end
69
78
 
70
79
  self.tag_builder = self.default_tag_builder
data/rux.gemspec CHANGED
@@ -13,6 +13,7 @@ Gem::Specification.new do |s|
13
13
  s.add_dependency 'onload', '~> 1.1'
14
14
  s.add_dependency 'parser', '~> 3.0'
15
15
  s.add_dependency 'unparser', '~> 0.8'
16
+ s.add_dependency 'use_context', '~> 1.2'
16
17
 
17
18
  s.require_path = 'lib'
18
19
  s.executables << 'ruxc'
@@ -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
data/spec/render_spec.rb CHANGED
@@ -25,6 +25,24 @@ describe 'rendering', type: :render do
25
25
  expect(result).to eq("<div><p>Welcome!</p><p>Welcome!</p><p>Welcome!</p></div>")
26
26
  end
27
27
 
28
+ it 'handles curly brace block syntax' do
29
+ result = render(<<~RUBY)
30
+ [1, 2, 3].map { |i| <div>{i}</div> }
31
+ RUBY
32
+
33
+ expect(result).to eq(["<div>1</div>", "<div>2</div>", "<div>3</div>"])
34
+ end
35
+
36
+ it 'handles curly brace block syntax with wrapping tag' do
37
+ result = render(<<~RUBY)
38
+ <span>
39
+ {[1, 2, 3].map { |i| <div>{i}</div> }}
40
+ </span>
41
+ RUBY
42
+
43
+ expect(result).to eq("<span><div>1</div><div>2</div><div>3</div></span>")
44
+ end
45
+
28
46
  it 'correctly handles keyword arguments (ruby 3)' do
29
47
  result = render(<<~RUBY)
30
48
  <ArgsComponent a="a" b="b" />
@@ -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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rux
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cameron Dutro
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
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'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.2'
54
68
  description: A jsx-inspired way to write view components.
55
69
  email:
56
70
  - camertron@gmail.com
@@ -80,6 +94,8 @@ files:
80
94
  - lib/rux/ast/tag_node.rb
81
95
  - lib/rux/ast/text_node.rb
82
96
  - lib/rux/buffer.rb
97
+ - lib/rux/context.rb
98
+ - lib/rux/debug_lexer.rb
83
99
  - lib/rux/default_tag_builder.rb
84
100
  - lib/rux/default_visitor.rb
85
101
  - lib/rux/file.rb
@@ -96,6 +112,7 @@ files:
96
112
  - lib/rux/version.rb
97
113
  - lib/rux/visitor.rb
98
114
  - rux.gemspec
115
+ - spec/context_spec.rb
99
116
  - spec/parser/attributes_spec.rb
100
117
  - spec/parser/fragment_spec.rb
101
118
  - spec/parser/html_safety_spec.rb