ruby2js 4.1.4 → 4.2.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 +4 -4
- data/bin/ruby2js +4 -0
- data/demo/ruby2js.rb +677 -0
- data/lib/ruby2js/converter/def.rb +3 -1
- data/lib/ruby2js/converter/logical.rb +3 -3
- data/lib/ruby2js/converter/return.rb +1 -1
- data/lib/ruby2js/converter.rb +2 -2
- data/lib/ruby2js/demo.rb +28 -0
- data/lib/ruby2js/filter/active_functions.rb +9 -1
- data/lib/ruby2js/filter/camelCase.rb +2 -2
- data/lib/ruby2js/filter/esm.rb +28 -14
- data/lib/ruby2js/filter/functions.rb +11 -3
- data/lib/ruby2js/filter/lit-element.rb +2 -218
- data/lib/ruby2js/filter/lit.rb +290 -0
- data/lib/ruby2js/filter/react.rb +18 -5
- data/lib/ruby2js/filter/stimulus.rb +2 -2
- data/lib/ruby2js/filter/underscore.rb +23 -17
- data/lib/ruby2js/version.rb +2 -2
- data/lib/tasks/install/{litelement.rb → lit-webpacker.rb} +2 -2
- data/lib/tasks/install/stimulus-rollup.rb +44 -0
- data/lib/tasks/install/stimulus-sprockets.rb +16 -23
- data/lib/tasks/install/stimulus-webpacker.rb +3 -0
- data/lib/tasks/ruby2js_tasks.rake +19 -5
- data/ruby2js.gemspec +3 -1
- metadata +9 -4
data/demo/ruby2js.rb
ADDED
@@ -0,0 +1,677 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Interactive demo of conversions from Ruby to JS.
|
4
|
+
|
5
|
+
# --port and --install options require wunderbar.
|
6
|
+
#
|
7
|
+
# Installation
|
8
|
+
# ----
|
9
|
+
#
|
10
|
+
# Web server set up to run CGI programs?
|
11
|
+
# $ ruby ruby2js.rb --install=/web/docroot
|
12
|
+
#
|
13
|
+
# Want to run a standalone server?
|
14
|
+
# $ ruby ruby2js.rb --port=8080
|
15
|
+
#
|
16
|
+
# Want to run from the command line?
|
17
|
+
# $ ruby ruby2js.rb [options] [file]
|
18
|
+
#
|
19
|
+
# try --help for a list of supported options
|
20
|
+
|
21
|
+
# support running directly from a git clone
|
22
|
+
$:.unshift File.absolute_path('../../lib', __FILE__)
|
23
|
+
require 'ruby2js/demo'
|
24
|
+
require 'cgi'
|
25
|
+
require 'pathname'
|
26
|
+
|
27
|
+
def parse_request(env=ENV)
|
28
|
+
|
29
|
+
# autoregister filters
|
30
|
+
filters = {}
|
31
|
+
Dir["#{$:.first}/ruby2js/filter/*.rb"].sort.each do |file|
|
32
|
+
filter = File.basename(file, '.rb')
|
33
|
+
filters[filter] = file
|
34
|
+
end
|
35
|
+
|
36
|
+
# web/CGI query string support
|
37
|
+
selected = env['PATH_INFO'].to_s.split('/')
|
38
|
+
env['QUERY_STRING'].to_s.split('&').each do |opt|
|
39
|
+
key, value = opt.split('=', 2)
|
40
|
+
if key == 'ruby'
|
41
|
+
@ruby = CGI.unescape(value)
|
42
|
+
elsif key == 'filter'
|
43
|
+
selected = CGI.unescape(value).split(',')
|
44
|
+
elsif value
|
45
|
+
ARGV.push("--#{key}=#{CGI.unescape(value)}")
|
46
|
+
else
|
47
|
+
ARGV.push("--#{key}")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# extract options from the argument list
|
52
|
+
options = {}
|
53
|
+
options[:include] = [:class] if ARGV.delete('--include-class')
|
54
|
+
@live = ARGV.delete('--live')
|
55
|
+
wunderbar_options = []
|
56
|
+
|
57
|
+
require 'optparse'
|
58
|
+
|
59
|
+
opts = OptionParser.new
|
60
|
+
opts.banner = "Usage: #$0 [options] [file]"
|
61
|
+
|
62
|
+
opts.on('--autoexports [default]', "add export statements for top level constants") {|option|
|
63
|
+
options[:autoexports] = option ? option.to_sym : true
|
64
|
+
}
|
65
|
+
|
66
|
+
opts.on('--autoimports=mappings', "automatic import mappings, without quotes") {|mappings|
|
67
|
+
options[:autoimports] = Ruby2JS::Demo.parse_autoimports(mappings)
|
68
|
+
}
|
69
|
+
|
70
|
+
opts.on('--defs=mappings', "class and module definitions") {|mappings|
|
71
|
+
options[:defs] = Ruby2JS::Demo.parse_defs(mappings)
|
72
|
+
}
|
73
|
+
|
74
|
+
opts.on('--equality', "double equal comparison operators") {options[:comparison] = :equality}
|
75
|
+
|
76
|
+
# autoregister eslevels
|
77
|
+
Dir["#{$:.first}/ruby2js/es20*.rb"].sort.each do |file|
|
78
|
+
eslevel = File.basename(file, '.rb')
|
79
|
+
filters[eslevel] = file
|
80
|
+
|
81
|
+
opts.on("--#{eslevel}", "ECMAScript level #{eslevel}") do
|
82
|
+
@eslevel = eslevel[/\d+/]
|
83
|
+
options[:eslevel] = @eslevel.to_i
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
opts.on('--exclude METHOD,...', "exclude METHOD(s) from filters", Array) {|methods|
|
88
|
+
options[:exclude] ||= []; options[:exclude].push(*methods.map(&:to_sym))
|
89
|
+
}
|
90
|
+
|
91
|
+
opts.on('-f', '--filter NAME,...', "process using NAME filter(s)", Array) do |names|
|
92
|
+
selected.push(*names)
|
93
|
+
end
|
94
|
+
|
95
|
+
opts.on('--identity', "triple equal comparison operators") {options[:comparison] = :identity}
|
96
|
+
|
97
|
+
opts.on('--import_from_skypack', "use Skypack for internal functions import statements") do
|
98
|
+
options[:import_from_skypack] = true
|
99
|
+
end
|
100
|
+
|
101
|
+
opts.on('--include METHOD,...', "have filters process METHOD(s)", Array) {|methods|
|
102
|
+
options[:include] ||= []; options[:include].push(*methods.map(&:to_sym))
|
103
|
+
}
|
104
|
+
|
105
|
+
opts.on('--include-all', "have filters include all methods") do
|
106
|
+
options[:include_all] = true
|
107
|
+
end
|
108
|
+
|
109
|
+
opts.on('--include-only METHOD,...', "have filters only process METHOD(s)", Array) {|methods|
|
110
|
+
options[:include_only] ||= []; options[:include_only].push(*methods.map(&:to_sym))
|
111
|
+
}
|
112
|
+
|
113
|
+
opts.on('--ivars @name:value,...', "set ivars") {|ivars|
|
114
|
+
options[:ivars] ||= {}
|
115
|
+
options[:ivars].merge! ivars.split(/(?:^|,)\s*(@\w+):/)[1..-1].each_slice(2).
|
116
|
+
map {|name, value| [name.to_sym, value]}.to_h
|
117
|
+
}
|
118
|
+
|
119
|
+
opts.on('--logical', "use '||' for 'or' operators") {options[:or] = :logical}
|
120
|
+
|
121
|
+
opts.on('--nullish', "use '??' for 'or' operators") {options[:or] = :nullish}
|
122
|
+
|
123
|
+
opts.on('--require_recursive', "import all symbols defined by processing the require recursively") {options[:require_recursive] = true}
|
124
|
+
|
125
|
+
opts.on('--strict', "strict mode") {options[:strict] = true}
|
126
|
+
|
127
|
+
opts.on('--template_literal_tags tag,...', "process TAGS as template literals", Array) {|tags|
|
128
|
+
options[:template_literal_tags] ||= []; options[:template_literal_tags].push(*tags.map(&:to_sym))
|
129
|
+
}
|
130
|
+
|
131
|
+
opts.on('--underscored_private', "prefix private properties with an underscore") do
|
132
|
+
options[:underscored_private] = true
|
133
|
+
end
|
134
|
+
|
135
|
+
# shameless hack. Instead of repeating the available options, extract them
|
136
|
+
# from the OptionParser. Exclude default options and es20xx options.
|
137
|
+
options_available = opts.instance_variable_get(:@stack).last.list.
|
138
|
+
map {|opt| [opt.long.first[2..-1], opt.arg != nil]}.
|
139
|
+
reject {|name, arg| %w{equality logical}.include?(name) || name =~ /es20\d\d/}.to_h
|
140
|
+
|
141
|
+
opts.separator('')
|
142
|
+
|
143
|
+
opts.on('--port n', Integer, 'start a webserver') do |n|
|
144
|
+
wunderbar_options.push "--port=#{n}"
|
145
|
+
end
|
146
|
+
|
147
|
+
opts.on('--install path', 'install as a CGI program') do |path|
|
148
|
+
wunderbar_options.push "--install=#{path}"
|
149
|
+
end
|
150
|
+
|
151
|
+
begin
|
152
|
+
opts.parse!
|
153
|
+
rescue Exception => $load_error
|
154
|
+
raise unless defined? env and env['SERVER_PORT']
|
155
|
+
end
|
156
|
+
|
157
|
+
ARGV.push(*wunderbar_options)
|
158
|
+
ARGV.push @live if @live
|
159
|
+
require 'wunderbar' unless wunderbar_options.empty?
|
160
|
+
|
161
|
+
# load selected filters
|
162
|
+
options[:filters] = []
|
163
|
+
|
164
|
+
selected.each do |name|
|
165
|
+
begin
|
166
|
+
if filters.include? name
|
167
|
+
require filters[name]
|
168
|
+
|
169
|
+
# find the module and add it to the list of filters.
|
170
|
+
# Note: explicit filter option is used instead of
|
171
|
+
# relying on Ruby2JS::Filter::DEFAULTS as the demo
|
172
|
+
# may be run as a server and as such DEFAULTS may
|
173
|
+
# contain filters from previous requests.
|
174
|
+
Ruby2JS::Filter::DEFAULTS.each do |mod|
|
175
|
+
method = mod.instance_method(mod.instance_methods.first)
|
176
|
+
if filters[name] == method.source_location.first
|
177
|
+
options[:filters] << mod
|
178
|
+
end
|
179
|
+
end
|
180
|
+
elsif not name.empty? and name =~ /^[-\w+]$/
|
181
|
+
$load_error = "UNKNOWN filter: #{name}"
|
182
|
+
end
|
183
|
+
rescue Exception => $load_error
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
return options, selected, options_available
|
188
|
+
end
|
189
|
+
|
190
|
+
options = parse_request.first
|
191
|
+
|
192
|
+
if (not defined? Wunderbar or not env['SERVER_PORT']) and not @live
|
193
|
+
# command line support
|
194
|
+
if ARGV.length > 0
|
195
|
+
options[:file] = ARGV.first
|
196
|
+
puts Ruby2JS.convert(File.read(ARGV.first), options).to_s
|
197
|
+
else
|
198
|
+
puts Ruby2JS.convert($stdin.read, options).to_s
|
199
|
+
end
|
200
|
+
|
201
|
+
else
|
202
|
+
def walk(ast, indent='', tail='', last=true)
|
203
|
+
return unless ast
|
204
|
+
_div class: (ast.loc ? 'loc' : 'unloc') do
|
205
|
+
_ indent
|
206
|
+
_span.hidden 's(:'
|
207
|
+
_ ast.type
|
208
|
+
_span.hidden ',' unless ast.children.empty?
|
209
|
+
|
210
|
+
if ast.children.any? {|child| Parser::AST::Node === child}
|
211
|
+
ast.children.each_with_index do |child, index|
|
212
|
+
ctail = index == ast.children.length - 1 ? ')' + tail : ''
|
213
|
+
if Parser::AST::Node === child
|
214
|
+
walk(child, " #{indent}", ctail, last && !ctail.empty?)
|
215
|
+
else
|
216
|
+
_div do
|
217
|
+
_ "#{indent} #{child.inspect}"
|
218
|
+
_span.hidden "#{ctail}#{',' unless last && !ctail.empty?}"
|
219
|
+
_ ' ' if last && !ctail.empty?
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
else
|
224
|
+
ast.children.each_with_index do |child, index|
|
225
|
+
_ " #{child.inspect}"
|
226
|
+
_span.hidden ',' unless index == ast.children.length - 1
|
227
|
+
end
|
228
|
+
_span.hidden ")#{tail}#{',' unless last}"
|
229
|
+
_ ' ' if last
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# web server support
|
235
|
+
_html do
|
236
|
+
options, selected, options_available = parse_request env
|
237
|
+
_title 'Ruby2JS'
|
238
|
+
|
239
|
+
base = env['REQUEST_URI'].split('?').first
|
240
|
+
base = base[0..-env['PATH_INFO'].length] if env['PATH_INFO']
|
241
|
+
base += '/' unless base.end_with? '/'
|
242
|
+
_base href: base
|
243
|
+
|
244
|
+
_style %{
|
245
|
+
.js.editor { background-color: #ffffcc }
|
246
|
+
.ruby.editor { resize: vertical; overflow: auto; height: 200px; background-color: #ffeeee; margin-bottom: 5px; }
|
247
|
+
.ruby .cm-wrap { background-color: #ffeeee; height: 100% }
|
248
|
+
.js .cm-wrap { background-color: #ffffdd; height: 100% }
|
249
|
+
.ruby .cm-wrap .cm-content .cm-activeLine { background-color: #ffdddd; margin-right: 2px }
|
250
|
+
.js .cm-wrap .cm-content .cm-activeLine { background-color: #ffffcc; margin-right: 2px }
|
251
|
+
|
252
|
+
.unloc {background-color: yellow}
|
253
|
+
.loc {background-color: white}
|
254
|
+
.loc span.hidden, .unloc span.hidden {font-size: 0}
|
255
|
+
.container.narrow-container {padding: 0; margin: 0 3%; max-width: 91%}
|
256
|
+
.exception {background-color:#ff0; margin: 1em 0; padding: 1em; border: 4px solid red; border-radius: 1em}
|
257
|
+
|
258
|
+
#{(@live ? %q{
|
259
|
+
sl-menu { display: none }
|
260
|
+
.narrow-container pre {padding: 0 1rem}
|
261
|
+
.narrow-container h1.title, .narrow-container h2.title {margin: 0.5rem 0}
|
262
|
+
} : %q{
|
263
|
+
svg {height: 4em; width: 4em; transition: 0.5s}
|
264
|
+
svg:hover {height: 8em; width: 8em}
|
265
|
+
textarea.ruby {background-color: #ffeeee; margin-bottom: 0.4em}
|
266
|
+
pre.js {background-color: #ffffcc}
|
267
|
+
h2 {margin-top: 0.4em}
|
268
|
+
|
269
|
+
.dropdown { position: relative; display: none; }
|
270
|
+
.dropdown-content { display: none; position: absolute; background-color: #f9f9f9; min-width: 180px; box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); padding: 12px 16px; z-index: 1; }
|
271
|
+
|
272
|
+
/* below is based on bootstrap
|
273
|
+
https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css
|
274
|
+
*/
|
275
|
+
|
276
|
+
:root{--bs-base-font-size: 16px;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}
|
277
|
+
html{font-size:var(--bs-base-font-size)}
|
278
|
+
body{margin:0;font-family:var(--bs-font-sans-serif);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}
|
279
|
+
a{color:#0d6efd;text-decoration:underline}
|
280
|
+
a:hover{color:#0a58ca}
|
281
|
+
svg{vertical-align:middle}
|
282
|
+
label{display:inline-block}
|
283
|
+
input,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}
|
284
|
+
select{text-transform:none}
|
285
|
+
select{word-wrap:normal}
|
286
|
+
[type=submit]{-webkit-appearance:button}
|
287
|
+
.container{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}
|
288
|
+
.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}
|
289
|
+
.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}
|
290
|
+
|
291
|
+
.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}
|
292
|
+
.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}
|
293
|
+
.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}
|
294
|
+
.btn-primary:active{color:#fff;background-color:#0a58ca;border-color:#0a53be}
|
295
|
+
.btn-primary:active:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}
|
296
|
+
.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}
|
297
|
+
}).strip}
|
298
|
+
}
|
299
|
+
|
300
|
+
_div.container.narrow_container do
|
301
|
+
if @live
|
302
|
+
_h1.title.is_size_4 'Ruby'
|
303
|
+
|
304
|
+
_sl_dialog.option! label: "Option" do
|
305
|
+
_sl_input
|
306
|
+
_sl_button "Close", slot: "footer", type: "primary"
|
307
|
+
end
|
308
|
+
else
|
309
|
+
_a href: 'https://www.ruby2js.com/docs/' do
|
310
|
+
_ruby2js_logo
|
311
|
+
_ 'Ruby2JS'
|
312
|
+
end
|
313
|
+
|
314
|
+
def _sl_select(&block)
|
315
|
+
_select(&block)
|
316
|
+
end
|
317
|
+
|
318
|
+
def _sl_dropdown(&block)
|
319
|
+
_div.dropdown(&block)
|
320
|
+
end
|
321
|
+
|
322
|
+
def _sl_button(text, options, &block)
|
323
|
+
_button.btn text, id: options[:id]
|
324
|
+
end
|
325
|
+
|
326
|
+
def _sl_menu(&block)
|
327
|
+
_div.dropdown_content(&block)
|
328
|
+
end
|
329
|
+
|
330
|
+
def _sl_menu_item(name, args)
|
331
|
+
if args.include? :checked
|
332
|
+
_div do
|
333
|
+
_input type: 'checkbox', **args
|
334
|
+
_span name
|
335
|
+
end
|
336
|
+
else
|
337
|
+
_option name, args
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
def _sl_checkbox(name, args)
|
342
|
+
_input type: 'checkbox', **args
|
343
|
+
_label name, for: args[:id]
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
_form method: 'post' do
|
348
|
+
_div data_controller: @live && 'ruby' do
|
349
|
+
_textarea.ruby.form_control @ruby, name: 'ruby', rows: 8,
|
350
|
+
placeholder: 'Ruby source'
|
351
|
+
end
|
352
|
+
|
353
|
+
_div.options data_controller: @live && 'options' do
|
354
|
+
_input.btn.btn_primary type: 'submit', value: 'Convert',
|
355
|
+
style: "display: #{@live ? 'none' : 'inline'}"
|
356
|
+
|
357
|
+
_label 'ESLevel:', for: 'eslevel'
|
358
|
+
if @live
|
359
|
+
_sl_dropdown.eslevel! name: 'eslevel' do
|
360
|
+
_sl_button @eslevel || 'default', slot: 'trigger', caret: true
|
361
|
+
_sl_menu do
|
362
|
+
_sl_menu_item 'default', checked: !@eslevel || @eslevel == 'default'
|
363
|
+
Dir["#{$:.first}/ruby2js/es20*.rb"].sort.each do |file|
|
364
|
+
eslevel = File.basename(file, '.rb').sub('es', '')
|
365
|
+
_sl_menu_item eslevel, value: eslevel, checked: @eslevel == eslevel
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
369
|
+
else
|
370
|
+
_select name: 'eslevel', id: 'eslevel' do
|
371
|
+
_option 'default', selected: !@eslevel || @eslevel == 'default'
|
372
|
+
Dir["#{$:.first}/ruby2js/es20*.rb"].sort.each do |file|
|
373
|
+
eslevel = File.basename(file, '.rb').sub('es', '')
|
374
|
+
_option eslevel, value: eslevel, selected: @eslevel == eslevel
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
_sl_checkbox 'Show AST', id: 'ast', name: 'ast', checked: !!@ast
|
380
|
+
|
381
|
+
_sl_dropdown.filters! close_on_select: 'false' do
|
382
|
+
_sl_button 'Filters', slot: 'trigger', caret: true
|
383
|
+
_sl_menu do
|
384
|
+
Dir["#{$:.first}/ruby2js/filter/*.rb"].sort.each do |file|
|
385
|
+
filter = File.basename(file, '.rb')
|
386
|
+
next if filter == 'require'
|
387
|
+
_sl_menu_item filter, name: filter,
|
388
|
+
checked: selected.include?(filter)
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
_sl_dropdown.options! close_on_select: 'false' do
|
394
|
+
_sl_button 'Options', slot: 'trigger', caret: true
|
395
|
+
_sl_menu do
|
396
|
+
checked = options.dup
|
397
|
+
checked[:identity] = options[:comparison] == :identity
|
398
|
+
checked[:nullish] = options[:or] == :nullish
|
399
|
+
|
400
|
+
options_available.each do |option, args|
|
401
|
+
next if option == 'filter'
|
402
|
+
next if option.start_with? 'require_'
|
403
|
+
_sl_menu_item option, name: option,
|
404
|
+
checked: checked[option.to_sym],
|
405
|
+
data_args: options_available[option]
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
_script %{
|
413
|
+
// determine base URL and what filters and options are selected
|
414
|
+
let base = new URL(document.getElementsByTagName('base')[0].href).pathname;
|
415
|
+
let filters = new Set(window.location.pathname.slice(base.length).split('/'));
|
416
|
+
filters.delete('');
|
417
|
+
let options = {};
|
418
|
+
for (let match of window.location.search.matchAll(/(\\w+)(=([^&]*))?/g)) {
|
419
|
+
options[match[1]] = match[3] && decodeURIComponent(match[3]);
|
420
|
+
};
|
421
|
+
if (options.filter) options.filter.split(',').forEach(option => filters.add(option));
|
422
|
+
|
423
|
+
function updateLocation(force = false) {
|
424
|
+
let location = new URL(base, window.location);
|
425
|
+
location.pathname += Array.from(filters).join('/');
|
426
|
+
|
427
|
+
let search = [];
|
428
|
+
for (let [key, value] of Object.entries(options)) {
|
429
|
+
search.push(value === undefined ? key : `${key}=${encodeURIComponent(value)}`);
|
430
|
+
};
|
431
|
+
|
432
|
+
location.search = search.length === 0 ? "" : `${search.join('&')}`;
|
433
|
+
if (!force && window.location.toString() == location.toString()) return;
|
434
|
+
|
435
|
+
history.replaceState({}, null, location.toString());
|
436
|
+
|
437
|
+
if (document.getElementById('js').style.display === 'none') return;
|
438
|
+
|
439
|
+
// fetch updated results
|
440
|
+
let ruby = document.querySelector('textarea[name=ruby]').textContent;
|
441
|
+
let ast = document.getElementById('ast').checked;
|
442
|
+
let headers = {
|
443
|
+
'Content-Type': 'application/json',
|
444
|
+
'Accept': 'application/json'
|
445
|
+
}
|
446
|
+
|
447
|
+
fetch(location,
|
448
|
+
{method: 'POST', headers, body: JSON.stringify({ ruby, ast })}
|
449
|
+
).then(response => {
|
450
|
+
return response.json();
|
451
|
+
}).
|
452
|
+
then(json => {
|
453
|
+
document.querySelector('#js pre').textContent = json.js || json.exception;
|
454
|
+
|
455
|
+
let parsed = document.querySelector('#parsed');
|
456
|
+
if (json.parsed) parsed.querySelector('pre').outerHTML = json.parsed;
|
457
|
+
parsed.style.display = json.parsed ? "block" : "none";
|
458
|
+
|
459
|
+
let filtered = document.querySelector('#filtered');
|
460
|
+
if (json.filtered) filtered.querySelector('pre').outerHTML = json.filtered;
|
461
|
+
filtered.style.display = json.filtered ? "block" : "none";
|
462
|
+
}).
|
463
|
+
catch(console.error);
|
464
|
+
}
|
465
|
+
|
466
|
+
// show dropdowns (they only appear if JS is enabled)
|
467
|
+
let dropdowns = document.querySelectorAll('.dropdown');
|
468
|
+
for (let dropdown of dropdowns) {
|
469
|
+
dropdown.style.display = 'inline-block';
|
470
|
+
let content = dropdown.querySelector('.dropdown-content');
|
471
|
+
content.style.opacity = 0;
|
472
|
+
content.style.display = 'none';
|
473
|
+
|
474
|
+
// toggle dropdown
|
475
|
+
dropdown.querySelector('button').addEventListener('click', event => {
|
476
|
+
event.preventDefault();
|
477
|
+
content.style.transition = '0s';
|
478
|
+
content.style.display = 'block';
|
479
|
+
content.style.zIndex = 1;
|
480
|
+
content.style.opacity = 1 - content.style.opacity;
|
481
|
+
});
|
482
|
+
|
483
|
+
// make dropdown disappear when mouse moves away
|
484
|
+
let focus = false;
|
485
|
+
dropdown.addEventListener('mouseover', () => {focus = true});
|
486
|
+
dropdown.addEventListener('mouseout', event => {
|
487
|
+
if (content.style.opacity === 0) return;
|
488
|
+
focus = false;
|
489
|
+
setTimeout( () => {
|
490
|
+
if (!focus) {
|
491
|
+
content.style.transition = '0.5s';
|
492
|
+
content.style.opacity = 0;
|
493
|
+
setTimeout( () => { content.style.zIndex = -1; }, 500);
|
494
|
+
}
|
495
|
+
}, 500)
|
496
|
+
})
|
497
|
+
};
|
498
|
+
|
499
|
+
// add/remove eslevel options
|
500
|
+
document.getElementById('eslevel').addEventListener('change', event => {
|
501
|
+
let value = event.target.value;
|
502
|
+
if (value !== "default") options['es' + value] = undefined;
|
503
|
+
for (let option of event.target.querySelectorAll('option')) {
|
504
|
+
if (option.value === 'default' || option.value === value) continue;
|
505
|
+
delete options['es' + option.value];
|
506
|
+
};
|
507
|
+
updateLocation();
|
508
|
+
});
|
509
|
+
|
510
|
+
// add/remove filters based on checkbox
|
511
|
+
let dropdown = document.getElementById('filters');
|
512
|
+
for (let filter of dropdown.querySelectorAll('input[type=checkbox]')) {
|
513
|
+
filter.addEventListener('click', event => {
|
514
|
+
let name = event.target.name;
|
515
|
+
if (!filters.delete(name)) filters.add(name);
|
516
|
+
updateLocation();
|
517
|
+
});
|
518
|
+
}
|
519
|
+
|
520
|
+
// add/remove options based on checkbox
|
521
|
+
dropdown = document.getElementById('options');
|
522
|
+
for (let option of dropdown.querySelectorAll('input[type=checkbox]')) {
|
523
|
+
option.addEventListener('click', event => {
|
524
|
+
let name = event.target.name;
|
525
|
+
|
526
|
+
if (name in options) {
|
527
|
+
delete options[name];
|
528
|
+
} else if (option.dataset.args) {
|
529
|
+
options[name] = prompt(name);
|
530
|
+
} else {
|
531
|
+
options[name] = undefined;
|
532
|
+
};
|
533
|
+
|
534
|
+
updateLocation();
|
535
|
+
})
|
536
|
+
};
|
537
|
+
|
538
|
+
// allow update of option
|
539
|
+
for (let span of document.querySelectorAll('input[data-args] + span')) {
|
540
|
+
span.addEventListener('click', event => {
|
541
|
+
let name = span.previousElementSibling.name;
|
542
|
+
options[name] = prompt(name, decodeURIComponent(options[name] || ''));
|
543
|
+
span.previousElementSibling.checked = true;
|
544
|
+
updateLocation();
|
545
|
+
})
|
546
|
+
}
|
547
|
+
|
548
|
+
// refesh on "Show AST" change
|
549
|
+
document.getElementById('ast').addEventListener('click', updateLocation);
|
550
|
+
}
|
551
|
+
|
552
|
+
_div_? do
|
553
|
+
raise $load_error if $load_error
|
554
|
+
|
555
|
+
options[:eslevel] = @eslevel.to_i if @eslevel
|
556
|
+
|
557
|
+
parsed = Ruby2JS.parse(@ruby).first if @ast and @ruby
|
558
|
+
|
559
|
+
_div.parsed! style: "display: #{@ast ? 'block' : 'none'}" do
|
560
|
+
_h2.title.is_size_6 'AST'
|
561
|
+
_pre {_ {walk(parsed)}}
|
562
|
+
end
|
563
|
+
|
564
|
+
ruby = Ruby2JS.convert(@ruby, options) if @ruby
|
565
|
+
|
566
|
+
_div.filtered! style: "display: #{@ast && parsed != ruby.ast ? 'block' : 'none'}" do
|
567
|
+
_h2.title.is_size_6 'filtered AST'
|
568
|
+
_pre {walk(ruby.ast) if ruby}
|
569
|
+
end
|
570
|
+
|
571
|
+
_div.js! data_controller: @live && 'js', style: "display: #{@ruby ? 'block' : 'none'}" do
|
572
|
+
_h2.title.is_size_4 'JavaScript'
|
573
|
+
_pre.js ruby.to_s
|
574
|
+
end
|
575
|
+
end
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
def _ruby2js_logo
|
580
|
+
_svg width: '100%', height: '100%', viewBox: '0 0 278 239', version: '1.1', xlink: 'http://www.w3.org/1999/xlink', space: 'preserve', 'xmlns:serif' => 'http://www.serif.com/', style: 'fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;' do
|
581
|
+
_g transform: 'matrix(0.97805,-0.208368,0.208368,0.97805,-63.5964,16.8613)' do
|
582
|
+
_path d: 'M43.591,115.341L92.572,45.15L275.649,45.276L322,113.639L183.044,261.9L43.591,115.341Z', style: 'fill:rgb(201,38,19);'
|
583
|
+
_g.Layer1! transform: 'matrix(0.762386,0,0,0.762386,-83.8231,-163.857)' do
|
584
|
+
_g transform: 'matrix(1,0,0,1,1,0)' do
|
585
|
+
_path d: 'M253,412.902L323.007,416.982L335.779,302.024L433.521,467.281L346.795,556.198L253,412.902Z', style: 'fill:url(#_Linear1);'
|
586
|
+
end
|
587
|
+
_g transform: 'matrix(1,0,0,1,90,0)' do
|
588
|
+
_path d: 'M260.802,410.567L312.405,427.307L345.625,407.012L286.376,341.482L301.912,316.368L348.735,322.338L402.088,408.236L360.798,450.037L317.951,497.607L260.802,410.567Z', style: 'fill:url(#_Linear2);'
|
589
|
+
end
|
590
|
+
end
|
591
|
+
_g transform: 'matrix(1,0,0,1,-71.912,-102.1)' do
|
592
|
+
_path d: 'M133.132,219.333L241.936,335.629L190.73,219.333L133.132,219.333ZM205.287,219.333L255.212,345.305L306.383,219.333L205.287,219.333ZM374.878,219.333L320.94,219.333L267.853,335.345L374.878,219.333ZM211.57,207.009L302.227,207.009L256.899,159.664L211.57,207.009ZM334.854,155.614L268.834,155.614L314.068,202.862L334.854,155.614ZM176.816,155.614L198.271,204.385L244.966,155.614L176.816,155.614ZM375.017,207.009L345.969,163.438L326.802,207.009L375.017,207.009ZM137.348,207.009L184.868,207.009L166.129,164.411L137.348,207.009ZM163.588,147L348.228,147L393.912,215.526L254.956,364L116,217.43L163.588,147Z', style: 'fill:none;fill-rule:nonzero;stroke:rgb(255,248,195);stroke-width:5px;'
|
593
|
+
end
|
594
|
+
_g transform: 'matrix(0.76326,0,0,0.76326,-88.595,-169.24)' do
|
595
|
+
_g opacity: '0.44' do
|
596
|
+
_g.j! transform: 'matrix(0.46717,0,0,0.46717,186.613,178.904)' do
|
597
|
+
_path d: 'M165.65,526.474L213.863,497.296C223.164,513.788 231.625,527.74 251.92,527.74C271.374,527.74 283.639,520.13 283.639,490.53L283.639,289.23L342.842,289.23L342.842,491.368C342.842,552.688 306.899,580.599 254.457,580.599C207.096,580.599 179.605,556.07 165.65,526.469', style: 'fill:rgb(48,9,5);fill-rule:nonzero;'
|
598
|
+
end
|
599
|
+
_g.s! transform: 'matrix(0.46717,0,0,0.46717,185.613,178.904)' do
|
600
|
+
_path d: 'M375,520.13L423.206,492.219C435.896,512.943 452.389,528.166 481.568,528.166C506.099,528.166 521.741,515.901 521.741,498.985C521.741,478.686 505.673,471.496 478.606,459.659L463.809,453.311C421.094,435.13 392.759,412.294 392.759,364.084C392.759,319.68 426.59,285.846 479.454,285.846C517.091,285.846 544.156,298.957 563.608,333.212L517.511,362.814C507.361,344.631 496.369,337.442 479.454,337.442C462.115,337.442 451.119,348.437 451.119,362.814C451.119,380.576 462.115,387.766 487.486,398.762L502.286,405.105C552.611,426.674 580.946,448.662 580.946,498.139C580.946,551.426 539.08,580.604 482.836,580.604C427.86,580.604 392.336,554.386 375,520.13', style: 'fill:rgb(47,9,5);fill-rule:nonzero;'
|
601
|
+
end
|
602
|
+
end
|
603
|
+
end
|
604
|
+
_g transform: 'matrix(0.76326,0,0,0.76326,-91.6699,-173.159)' do
|
605
|
+
_g.j1! 'serif:id' => 'j', transform: 'matrix(0.46717,0,0,0.46717,186.613,178.904)' do
|
606
|
+
_path d: 'M165.65,526.474L213.863,497.296C223.164,513.788 231.625,527.74 251.92,527.74C271.374,527.74 283.639,520.13 283.639,490.53L283.639,289.23L342.842,289.23L342.842,491.368C342.842,552.688 306.899,580.599 254.457,580.599C207.096,580.599 179.605,556.07 165.65,526.469', style: 'fill:rgb(247,223,30);fill-rule:nonzero;'
|
607
|
+
end
|
608
|
+
_g.s1! 'serif:id' => 's', transform: 'matrix(0.46717,0,0,0.46717,185.613,178.904)' do
|
609
|
+
_path d: 'M375,520.13L423.206,492.219C435.896,512.943 452.389,528.166 481.568,528.166C506.099,528.166 521.741,515.901 521.741,498.985C521.741,478.686 505.673,471.496 478.606,459.659L463.809,453.311C421.094,435.13 392.759,412.294 392.759,364.084C392.759,319.68 426.59,285.846 479.454,285.846C517.091,285.846 544.156,298.957 563.608,333.212L517.511,362.814C507.361,344.631 496.369,337.442 479.454,337.442C462.115,337.442 451.119,348.437 451.119,362.814C451.119,380.576 462.115,387.766 487.486,398.762L502.286,405.105C552.611,426.674 580.946,448.662 580.946,498.139C580.946,551.426 539.08,580.604 482.836,580.604C427.86,580.604 392.336,554.386 375,520.13', style: 'fill:rgb(247,223,30);fill-rule:nonzero;'
|
610
|
+
end
|
611
|
+
end
|
612
|
+
end
|
613
|
+
_defs do
|
614
|
+
_linearGradient id: '_Linear1', x1: '0', y1: '0', x2: '1', y2: '0', gradientUnits: 'userSpaceOnUse', gradientTransform: 'matrix(110.514,-65.1883,65.1883,110.514,284.818,460.929)' do
|
615
|
+
_stop offset: '0', style: 'stop-color:rgb(97,18,10);stop-opacity:1'
|
616
|
+
_stop offset: '1', style: 'stop-color:rgb(184,34,18);stop-opacity:1'
|
617
|
+
end
|
618
|
+
_linearGradient id: '_Linear2', x1: '0', y1: '0', x2: '1', y2: '0', gradientUnits: 'userSpaceOnUse', gradientTransform: 'matrix(102.484,-65.5763,65.5763,102.484,288.352,453.55)' do
|
619
|
+
_stop offset: '0', style: 'stop-color:rgb(97,18,10);stop-opacity:1'
|
620
|
+
_stop offset: '1', style: 'stop-color:rgb(184,34,18);stop-opacity:1'
|
621
|
+
end
|
622
|
+
end
|
623
|
+
end
|
624
|
+
end
|
625
|
+
|
626
|
+
# html fetch support
|
627
|
+
_json do
|
628
|
+
options = parse_request(env).first
|
629
|
+
raise ArgumentError.new($load_error) if $load_error
|
630
|
+
|
631
|
+
converted = Ruby2JS.convert(@ruby, options)
|
632
|
+
|
633
|
+
_js converted.to_s
|
634
|
+
|
635
|
+
if @ast
|
636
|
+
parsed = Ruby2JS.parse(@ruby).first
|
637
|
+
html = Wunderbar::HtmlMarkup.new(Struct.new(:params, :env).new({}, {}))
|
638
|
+
ast = html._pre { html._ {walk(parsed)} }
|
639
|
+
_parsed ast.serialize({indent: ' '}).join()
|
640
|
+
|
641
|
+
if converted.ast != parsed
|
642
|
+
ast = html._pre { html._ {walk(converted.ast)} }
|
643
|
+
_filtered ast.serialize({indent: ' '}).join()
|
644
|
+
end
|
645
|
+
end
|
646
|
+
end
|
647
|
+
|
648
|
+
unless env['SERVER_SOFTWARE']
|
649
|
+
require 'net/http'
|
650
|
+
Thread.new do
|
651
|
+
port = env['SERVER_PORT'].to_i
|
652
|
+
|
653
|
+
# wait for server to start
|
654
|
+
60.times do
|
655
|
+
sleep 0.5
|
656
|
+
begin
|
657
|
+
status = Net::HTTP.get_response('0.0.0.0','/',port).code
|
658
|
+
break if %(200 404 500).include? status
|
659
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT
|
660
|
+
end
|
661
|
+
end
|
662
|
+
|
663
|
+
link = "http://localhost:#{port}/"
|
664
|
+
if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
|
665
|
+
system "start #{link}"
|
666
|
+
elsif RbConfig::CONFIG['host_os'] =~ /darwin/
|
667
|
+
system "open #{link}"
|
668
|
+
elsif RbConfig::CONFIG['host_os'] =~ /linux|bsd/
|
669
|
+
if ENV['WSLENV'] and not `which wslview`.empty?
|
670
|
+
system "wslview #{link}"
|
671
|
+
else
|
672
|
+
system "xdg-open #{link}"
|
673
|
+
end
|
674
|
+
end
|
675
|
+
end
|
676
|
+
end
|
677
|
+
end
|
@@ -169,7 +169,9 @@ module Ruby2JS
|
|
169
169
|
put 'function'
|
170
170
|
end
|
171
171
|
|
172
|
-
put '('
|
172
|
+
put '('
|
173
|
+
parse s(:args, *args.children.select {|arg| arg.type != :shadowarg})
|
174
|
+
put ") {#{nl}"
|
173
175
|
|
174
176
|
next_token, @next_token = @next_token, :return
|
175
177
|
@block_depth += 1 if @block_depth
|