ruby2js 4.1.7 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -15,7 +15,7 @@ module Ruby2JS
15
15
  EXPRESSIONS = [ :array, :float, :hash, :int, :lvar, :nil, :send, :attr,
16
16
  :str, :sym, :dstr, :dsym, :cvar, :ivar, :zsuper, :super, :or, :and,
17
17
  :block, :const, :true, :false, :xnode, :taglit, :self,
18
- :op_asgn, :and_asgn, :or_asgn, :taglit, :gvar ]
18
+ :op_asgn, :and_asgn, :or_asgn, :taglit, :gvar, :csend ]
19
19
 
20
20
  handle :autoreturn do |*statements|
21
21
  return if statements == [nil]