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.
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 '('; parse args; put ") {#{nl}"
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