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 +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +47 -0
- data/lib/rux/context.rb +59 -0
- data/lib/rux/debug_lexer.rb +23 -0
- data/lib/rux/parser.rb +11 -4
- data/lib/rux/version.rb +1 -1
- data/lib/rux.rb +9 -0
- data/rux.gemspec +1 -0
- data/spec/context_spec.rb +84 -0
- data/spec/render_spec.rb +18 -0
- data/spec/support/view_component/base.rb +11 -8
- metadata +18 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2418beae606c99c851a3000546d6fffe20fb30e02049b41a2c2cbd8ecae427dc
|
|
4
|
+
data.tar.gz: 974fc8d3502737b5737f30b07c818e04918eb55b9693ea8b3d5ef629a71a1dc6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|

|
|
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.
|
data/lib/rux/context.rb
ADDED
|
@@ -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
|
-
|
|
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
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
|
@@ -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.
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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.
|
|
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
|