rux 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 29ba86ed38eae6295a1f2f3a68df1b371589c72c24ef4827541e238bdc288f5d
4
+ data.tar.gz: 4db5f89366ba6bb85ef4939f33e55e71aed68baa9cc437d347f4888e1bb2e772
5
+ SHA512:
6
+ metadata.gz: b94c6b6b4b9a4d802c32135b6f47f12186428b54a3d94c74a4eb72e858c077f4f4a8548615cd07e58c1b4a20340bcaba8412789e22f0387819850f0026f41eec
7
+ data.tar.gz: 5f37c06fe42d57e38dafa661f9f22c7a0dd180375afe36d4c4d98ed38f01d29c9668b1bf2e81fec19b05278bbabf081871c448944456cfe1f6045716e9d12ff9
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :development, :test do
6
+ gem 'pry-byebug'
7
+ gem 'rake'
8
+ gem 'rspec'
9
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Cameron Dutro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,310 @@
1
+ ## rux [![Build Status](https://travis-ci.com/camertron/rux.svg?branch=master)](https://travis-ci.com/camertron/rux)
2
+
3
+ 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.
4
+
5
+ ## Introduction
6
+
7
+ A bit of background before we dive into how to use rux.
8
+
9
+ ### React and JSX
10
+
11
+ React mainstreamed the idea of composing websites from a series of components. To make it conceptually easier to transition from HTML templates to Javascript components, React also introduced an HTML-based syntax called JSX that allows developers to embed HTML into their Javascript code.
12
+
13
+ ### Rails View Components
14
+
15
+ For a long time, Rails didn't really have any support for components, preferring to rely on HTML template languages like ERB and HAML. The fine folks at Github however decided components could work well in Rails and released their [view_component framework](https://github.com/github/view_component). There was even some talk about merging view_component into Rails core as `ActionView::Component`, but unfortunately it looks like that won't be happening.
16
+
17
+ **NOTE**: I'm going to be focusing on Rails examples here using the view_component gem, but rendering views from a series of components is a framework-agnostic idea.
18
+
19
+ ### View Component Example
20
+
21
+ A view component is just a class. The actual view portion is usually stored in a secondary template file that the component renders in the context of an instance of that class. For example, here's a very basic view component that displays a person's name on the page:
22
+
23
+ ```ruby
24
+ # app/components/name_component.rb
25
+ class NameComponent < ViewComponent::Base
26
+ def initialize(first_name:, last_name:)
27
+ @first_name = first_name
28
+ @last_name = last_name
29
+ end
30
+ end
31
+ ```
32
+
33
+ ```html+erb
34
+ <%# app/components/name_component.html.erb %>
35
+ <span><%= @first_name %> <%= last_name %></span>
36
+ ```
37
+
38
+ View components have a number of very nice properties. Read about them on [viewcomponent.org](https://viewcomponent.org/) or watch Joel Hawksley's excellent 2019 [Railsconf talk](https://www.youtube.com/watch?v=y5Z5a6QdA-M).
39
+
40
+ ## HTML in Your Ruby
41
+
42
+ Rux does one thing: it lets you write HTML in your Ruby code. Here's the name component example from earlier rewritten in rux (sorry about the syntax highlighting, Github doesn't know about rux yet).
43
+
44
+ ```ruby
45
+ # app/components/name_component.rux
46
+ class NameComponent < ViewComponent::Base
47
+ def initialize(first_name:, last_name:)
48
+ @first_name = first_name
49
+ @last_name = last_name
50
+ end
51
+
52
+ def call
53
+ <span>
54
+ {@first_name} {@last_name}
55
+ </span>
56
+ end
57
+ end
58
+ ```
59
+
60
+ **NOTE**: The example above takes advantage of a feature of the view_component gem that lets you define a `call` method instead of creating a separate template file.
61
+
62
+ Next, we'll run the `ruxc` tool to translate the rux code into Ruby code, eg. `ruxc app/components/name_component.rux`. Here's the result:
63
+
64
+ ```ruby
65
+ class NameComponent < ViewComponent::Base
66
+ def initialize(first_name:, last_name:)
67
+ @first_name = first_name
68
+ @last_name = last_name
69
+ end
70
+
71
+ def call
72
+ Rux.tag("span") {
73
+ Rux.create_buffer.tap { |_rux_buf_,|
74
+ _rux_buf_ << @first_name
75
+ _rux_buf_ << " "
76
+ _rux_buf_ << @last_name
77
+ }.to_s
78
+ }
79
+ end
80
+ end
81
+ ```
82
+
83
+ As you can see, the span tag was converted to a `Rux.tag` call. The instance variables containing the first and last names are concatenated together and rendered inside the span.
84
+
85
+ ## Composing Components
86
+
87
+ Things get even more interesting when it comes to rendering components inside other components. Let's create a greeting component that makes use of the name component:
88
+
89
+ ```ruby
90
+ # app/components/greeting_component.rux
91
+ class GreetingComponent < ViewComponent::Base
92
+ def call
93
+ <div>
94
+ Hey there <NameComponent first-name="Homer" last-name="Simpson" />!
95
+ </div>
96
+ end
97
+ end
98
+ ```
99
+
100
+ The `ruxc` tool produces:
101
+
102
+ ```ruby
103
+ class GreetingComponent < ViewComponent::Base
104
+ def call
105
+ Rux.tag("div") {
106
+ Rux.create_buffer.tap { |_rux_buf_,|
107
+ _rux_buf_ << " Hey there "
108
+ _rux_buf_ << render(NameComponent.new(first_name: "Homer", last_name: "Simpson"))
109
+ _rux_buf_ << "! "
110
+ }.to_s
111
+ }
112
+ end
113
+ end
114
+ ```
115
+
116
+ The `<NameComponent>` tag was translated into an instance of the `NameComponent` class and the attributes into its keyword arguments.
117
+
118
+ **NOTE**: The `render` method is provided by `ViewComponent::Base`.
119
+
120
+ ## Embedding Ruby
121
+
122
+ Since rux code is translated into Ruby code, anything goes. You're free to put any valid Ruby statements inside the curly braces.
123
+
124
+ For example, let's say we want to change our greeting component to greet a variable number of people:
125
+
126
+ ```ruby
127
+ # app/components/greeting_component.rux
128
+ class GreetingComponent < ViewComponent::Base
129
+ def initialize(people:)
130
+ # people is an array of hashes containing :first_name and :last_name keys
131
+ @people = people
132
+ end
133
+
134
+ def call
135
+ <div>
136
+ {@people.map do |person|
137
+ <NameComponent
138
+ first-name={person[:first_name]}
139
+ last-name={person[:last_name]}
140
+ />
141
+ end}
142
+ </div>
143
+ end
144
+ end
145
+ ```
146
+
147
+ Notice we were able to embed Ruby within rux within Ruby within rux. Within Ruby. The rux parser supports unlimited levels of nesting, although you'll probably not want to go _too_ crazy.
148
+
149
+ ## Keyword Arguments Only
150
+
151
+ Any view component that will be rendered by rux must _only_ accept keyword arguments in its constructor. For example:
152
+
153
+ ```ruby
154
+ class MyComponent < ViewComponent::Base
155
+ # GOOD
156
+ def initialize(first_name:, last_name:)
157
+ end
158
+
159
+ # BAD
160
+ def initialize(first_name, last_name)
161
+ end
162
+
163
+ # BAD
164
+ def initialize(first_name, last_name = 'Simpson')
165
+ end
166
+ end
167
+ ```
168
+
169
+ In other words, positional arguments are not allowed. This is because there's no such thing as a positional HTML attribute - all HTML attributes are key/value pairs. So, in order to match up with HTML, rux components are written with keyword arguments.
170
+
171
+ Note also that the rux parser will replace dashes with underscores in rux tag attributes to adhere to both HTML and Ruby syntax conventions, since HTML attributes use dashes while Ruby keyword arguments use underscores. For example, here's how to write a rux tag for `MyComponent` above:
172
+
173
+ ```ruby
174
+ <MyComponent first-name="Homer" last-name="Simpson" />
175
+ ```
176
+
177
+ Notice that the rux attribute "first-name" is passed to `MyComponent#initialize` as "first_name".
178
+
179
+ ## How it Works
180
+
181
+ 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.
182
+
183
+ In the parsing phase, the token stream is transformed into an intermediate representation of the code known as an abstract syntax tree, or AST. It's the parser's job to work out which tags are children of other tags, associate attributes with tags, etc.
184
+
185
+ Finally it's time to generate Ruby code in the emitting phase. The rux gem makes use of the visitor pattern to walk over all the nodes in the AST and generate a big string of Ruby code. This big string is the final product that can be written to a file and executed by the Ruby interpreter.
186
+
187
+ ## Transpiling Rux to Ruby
188
+
189
+ While the `ruxc` tool is a convenient way to transpile rux to Ruby via the command line, it's also possible to do so programmatically.
190
+
191
+ ### Transpiling Strings
192
+
193
+ Let's say you have a string containing a bunch of rux code. You can transpile it to Ruby like so:
194
+
195
+ ```ruby
196
+ require 'rux'
197
+
198
+ str = 'some rux code'
199
+ Rux.to_ruby(str)
200
+ ```
201
+
202
+ **NOTE**: The `to_ruby` method accepts a visitor instance as its second argument (see below for more information about creating custom visitors). It uses the default visitor if no second argument is provided.
203
+
204
+ ### Transpiling Files
205
+
206
+ Rux comes with a handy `File` class to make transpiling files easier:
207
+
208
+ ```ruby
209
+ require 'rux'
210
+
211
+ f = Rux::File.new('path/to/some/file.rux')
212
+
213
+ # get result as a string, same as calling Rux.to_ruby
214
+ f.to_ruby
215
+
216
+ # write result to path/to/some/file.rb
217
+ f.write
218
+
219
+ # write result to the given file
220
+ f.write('somewhere/else/file.rb')
221
+
222
+ # the default file the result will be written, i.e. the location
223
+ # #write will write to
224
+ f.default_outfile
225
+ ```
226
+
227
+ ## Custom Visitors
228
+
229
+ Rux comes with a default visitor capable of emitting Ruby code that is mostly compatible with the view_component gem discussed earlier. A little bit of extra work is required to render rux components in Rails, which is why the rux-rails gem uses a modified version of the default visitor to emit Ruby code that will render correctly in Rails views. It's likely other frameworks that want to render rux components will need a custom visitor as well.
230
+
231
+ Visitors should inherit from the `Rux::Visitor` class and implement the various methods. See lib/rux/visitor.rb for details. If you're looking to tweak the default visitor, inherit from `Rux::DefaultVisitor` instead, and see lib/rux/default_visitor.rb for details.
232
+
233
+ ## Custom Tag Builders
234
+
235
+ The `Rux.tag` method emits HTML tags via the configured tag builder. You can configure a custom tag builder by setting `Rux.tag_builder` to any object that responds to the `call` method (and accepts three arguments). For example:
236
+
237
+ ```ruby
238
+ class MyTagBuilder
239
+ def call(tag_name, attributes = {}, &block)
240
+ # Should return a string, eg. '<div foo="bar"></div>'.
241
+ # When called, the block should return the tag's body contents.
242
+ end
243
+ end
244
+
245
+ Rux.tag_builder = MyTagBuilder.new
246
+ ```
247
+
248
+ Or, since the only requirement is that the tag builder respond to `#call`, you could pass a lambda:
249
+
250
+ ```ruby
251
+ Rux.tag_builder = -> (tag_name, attributes = {}, &block) do
252
+ # Should return a string, eg. '<div foo="bar"></div>'.
253
+ # When called, the block should return the tag's body contents.
254
+ end
255
+ ```
256
+
257
+ ## Custom Buffers
258
+
259
+ You may have noticed calls to `Rux.create_buffer` in the code examples above. Rux comes with a default buffer implementation, but you can configure a custom one as well. The rux-rails gem for example configures rux to use `ActiveSupport::SafeBuffer` in order to be compatible with Rails view rendering. Buffer implementations only need to define two methods: `#>>` and `#to_s`:
260
+
261
+ ```ruby
262
+ class MyBuffer
263
+ def initialize
264
+ @buffer = ''
265
+ end
266
+
267
+ def <<(thing)
268
+ # it's important to handle nils here
269
+ @buffer << (thing || '')
270
+ end
271
+
272
+ def to_s
273
+ @buffer
274
+ end
275
+ end
276
+
277
+ Rux.buffer = MyBuffer
278
+ ```
279
+
280
+ ## The Library Path
281
+
282
+ It is my hope that, in the future, Ruby and Rails devs will publish collections of view components in gem form that other devs can use in their own projects. Maybe some of those view component libraries will even be written in rux. Accordingly, I wanted a way of adding rux components to Rails' eager load system, but without actually depending on Rails.
283
+
284
+ The rux library path is a way for libraries written in rux to register themselves. The rux-rails gem automatically appends every entry in the library path to the Rails eager load and autoload paths so .rux files are automatically reloaded in development mode. Hopefully the library path enables other frameworks to do something similar.
285
+
286
+ Adding a path is done like so:
287
+
288
+ ```ruby
289
+ Rux.library_paths << 'path/to/dir/with/rux/files'
290
+ ```
291
+
292
+ ## Editor Support
293
+
294
+ Sublime Text: [https://github.com/camertron/rux-SublimeText](https://github.com/camertron/rux-SublimeText)
295
+
296
+ Atom: [https://github.com/camertron/rux-atom](https://github.com/camertron/rux-atom)
297
+
298
+ VSCode: [https://github.com/camertron/rux-vscode](https://github.com/camertron/rux-vscode)
299
+
300
+ ## Running Tests
301
+
302
+ `bundle exec rspec` should do the trick.
303
+
304
+ ## License
305
+
306
+ Licensed under the MIT license. See LICENSE for details.
307
+
308
+ ## Authors
309
+
310
+ * Cameron C. Dutro: http://github.com/camertron
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require 'bundler'
2
+ require 'rspec/core/rake_task'
3
+ require 'rubygems/package_task'
4
+
5
+ require 'rux'
6
+
7
+ Bundler::GemHelper.install_tasks
8
+
9
+ task default: :spec
10
+
11
+ desc 'Run specs'
12
+ RSpec::Core::RakeTask.new do |t|
13
+ t.pattern = './spec/**/*_spec.rb'
14
+ end
data/bin/ruxc ADDED
@@ -0,0 +1,96 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ $:.push(File.expand_path('./lib'))
4
+
5
+ require 'pathname'
6
+ require 'optparse'
7
+ require 'rux'
8
+
9
+ class RuxCLI
10
+ def self.parse(argv)
11
+ if argv.empty?
12
+ puts 'Please pass a file or directory to transpile'
13
+ exit 1
14
+ end
15
+
16
+ options = {
17
+ recursive: false,
18
+ pretty: true
19
+ }
20
+
21
+ if argv.first != '-h' && argv.first != '--help'
22
+ options[:in_path] = argv.shift
23
+ end
24
+
25
+ parser = OptionParser.new do |opts|
26
+ opts.banner = "Usage: ruxc path [options]"
27
+
28
+ oneline(<<~DESC).tap do |desc|
29
+ Prettify generated Ruby code (default: #{options[:pretty]}).
30
+ DESC
31
+ opts.on('-p', '--[no-]pretty', desc) do |pretty|
32
+ options[:pretty] = pretty
33
+ end
34
+ end
35
+
36
+ opts.on('-h', '--help', 'Prints this help info') do
37
+ puts opts
38
+ exit
39
+ end
40
+ end
41
+
42
+ parser.parse(argv)
43
+ new(options)
44
+ end
45
+
46
+ def self.oneline(str)
47
+ str.split("\n").join(' ')
48
+ end
49
+
50
+ def initialize(options)
51
+ @options = options
52
+ end
53
+
54
+ def validate
55
+ unless File.exist?(in_path)
56
+ puts "Could not find file at '#{in_path}'"
57
+ exit 1
58
+ end
59
+ end
60
+
61
+ def each_file(&block)
62
+ files = if in_path.directory?
63
+ in_path.glob(File.join('**', '*.rux'))
64
+ else
65
+ [in_path]
66
+ end
67
+
68
+ files.each do |file|
69
+ ruby_file = file.sub_ext('.rb')
70
+ yield file, ruby_file
71
+ end
72
+ end
73
+
74
+ def in_path
75
+ @in_path ||= Pathname(@options[:in_path]).expand_path
76
+ end
77
+
78
+ def pretty?
79
+ @options[:pretty]
80
+ end
81
+
82
+ private
83
+
84
+ def directory?
85
+ File.directory?(in_path)
86
+ end
87
+ end
88
+
89
+ cli = RuxCLI.parse(ARGV)
90
+ cli.validate
91
+
92
+ cli.each_file do |in_file, out_file, rbi_file|
93
+ rux_file = Rux::File.new(in_file)
94
+ rux_file.write(out_file, pretty: cli.pretty?)
95
+ puts "Wrote #{out_file}"
96
+ end