ruby2js 4.1.5 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/demo/ruby2js.rb ADDED
@@ -0,0 +1,679 @@
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
+ require 'wunderbar'
203
+
204
+ def walk(ast, indent='', tail='', last=true)
205
+ return unless ast
206
+ _div class: (ast.loc ? 'loc' : 'unloc') do
207
+ _ indent
208
+ _span.hidden 's(:'
209
+ _ ast.type
210
+ _span.hidden ',' unless ast.children.empty?
211
+
212
+ if ast.children.any? {|child| Parser::AST::Node === child}
213
+ ast.children.each_with_index do |child, index|
214
+ ctail = index == ast.children.length - 1 ? ')' + tail : ''
215
+ if Parser::AST::Node === child
216
+ walk(child, " #{indent}", ctail, last && !ctail.empty?)
217
+ else
218
+ _div do
219
+ _ "#{indent} #{child.inspect}"
220
+ _span.hidden "#{ctail}#{',' unless last && !ctail.empty?}"
221
+ _ ' ' if last && !ctail.empty?
222
+ end
223
+ end
224
+ end
225
+ else
226
+ ast.children.each_with_index do |child, index|
227
+ _ " #{child.inspect}"
228
+ _span.hidden ',' unless index == ast.children.length - 1
229
+ end
230
+ _span.hidden ")#{tail}#{',' unless last}"
231
+ _ ' ' if last
232
+ end
233
+ end
234
+ end
235
+
236
+ # web server support
237
+ _html do
238
+ options, selected, options_available = parse_request env
239
+ _title 'Ruby2JS'
240
+
241
+ base = env['REQUEST_URI'].split('?').first
242
+ base = base[0..-env['PATH_INFO'].length] if env['PATH_INFO']
243
+ base += '/' unless base.end_with? '/'
244
+ _base href: base
245
+
246
+ _style %{
247
+ .js.editor { background-color: #ffffcc }
248
+ .ruby.editor { resize: vertical; overflow: auto; height: 200px; background-color: #ffeeee; margin-bottom: 5px; }
249
+ .ruby .cm-wrap { background-color: #ffeeee; height: 100% }
250
+ .js .cm-wrap { background-color: #ffffdd; height: 100% }
251
+ .ruby .cm-wrap .cm-content .cm-activeLine { background-color: #ffdddd; margin-right: 2px }
252
+ .js .cm-wrap .cm-content .cm-activeLine { background-color: #ffffcc; margin-right: 2px }
253
+
254
+ .unloc {background-color: yellow}
255
+ .loc {background-color: white}
256
+ .loc span.hidden, .unloc span.hidden {font-size: 0}
257
+ .container.narrow-container {padding: 0; margin: 0 3%; max-width: 91%}
258
+ .exception {background-color:#ff0; margin: 1em 0; padding: 1em; border: 4px solid red; border-radius: 1em}
259
+
260
+ #{(@live ? %q{
261
+ sl-menu { display: none }
262
+ .narrow-container pre {padding: 0 1rem}
263
+ .narrow-container h1.title, .narrow-container h2.title {margin: 0.5rem 0}
264
+ } : %q{
265
+ svg {height: 4em; width: 4em; transition: 0.5s}
266
+ svg:hover {height: 8em; width: 8em}
267
+ textarea.ruby {background-color: #ffeeee; margin-bottom: 0.4em}
268
+ pre.js {background-color: #ffffcc}
269
+ h2 {margin-top: 0.4em}
270
+
271
+ .dropdown { position: relative; display: none; }
272
+ .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; }
273
+
274
+ /* below is based on bootstrap
275
+ https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css
276
+ */
277
+
278
+ :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}
279
+ html{font-size:var(--bs-base-font-size)}
280
+ 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}
281
+ a{color:#0d6efd;text-decoration:underline}
282
+ a:hover{color:#0a58ca}
283
+ svg{vertical-align:middle}
284
+ label{display:inline-block}
285
+ input,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}
286
+ select{text-transform:none}
287
+ select{word-wrap:normal}
288
+ [type=submit]{-webkit-appearance:button}
289
+ .container{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}
290
+ .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}
291
+ .btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}
292
+
293
+ .btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}
294
+ .btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}
295
+ .btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}
296
+ .btn-primary:active{color:#fff;background-color:#0a58ca;border-color:#0a53be}
297
+ .btn-primary:active:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}
298
+ .btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}
299
+ }).strip}
300
+ }
301
+
302
+ _div.container.narrow_container do
303
+ if @live
304
+ _h1.title.is_size_4 'Ruby'
305
+
306
+ _sl_dialog.option! label: "Option" do
307
+ _sl_input
308
+ _sl_button "Close", slot: "footer", type: "primary"
309
+ end
310
+ else
311
+ _a href: 'https://www.ruby2js.com/docs/' do
312
+ _ruby2js_logo
313
+ _ 'Ruby2JS'
314
+ end
315
+
316
+ def _sl_select(&block)
317
+ _select(&block)
318
+ end
319
+
320
+ def _sl_dropdown(&block)
321
+ _div.dropdown(&block)
322
+ end
323
+
324
+ def _sl_button(text, options, &block)
325
+ _button.btn text, id: options[:id]
326
+ end
327
+
328
+ def _sl_menu(&block)
329
+ _div.dropdown_content(&block)
330
+ end
331
+
332
+ def _sl_menu_item(name, args)
333
+ if args.include? :checked
334
+ _div do
335
+ _input type: 'checkbox', **args
336
+ _span name
337
+ end
338
+ else
339
+ _option name, args
340
+ end
341
+ end
342
+
343
+ def _sl_checkbox(name, args)
344
+ _input type: 'checkbox', **args
345
+ _label name, for: args[:id]
346
+ end
347
+ end
348
+
349
+ _form method: 'post' do
350
+ _div data_controller: @live && 'ruby' do
351
+ _textarea.ruby.form_control @ruby, name: 'ruby', rows: 8,
352
+ placeholder: 'Ruby source'
353
+ end
354
+
355
+ _div.options data_controller: @live && 'options' do
356
+ _input.btn.btn_primary type: 'submit', value: 'Convert',
357
+ style: "display: #{@live ? 'none' : 'inline'}"
358
+
359
+ _label 'ESLevel:', for: 'eslevel'
360
+ if @live
361
+ _sl_dropdown.eslevel! name: 'eslevel' do
362
+ _sl_button @eslevel || 'default', slot: 'trigger', caret: true
363
+ _sl_menu do
364
+ _sl_menu_item 'default', checked: !@eslevel || @eslevel == 'default'
365
+ Dir["#{$:.first}/ruby2js/es20*.rb"].sort.each do |file|
366
+ eslevel = File.basename(file, '.rb').sub('es', '')
367
+ _sl_menu_item eslevel, value: eslevel, checked: @eslevel == eslevel
368
+ end
369
+ end
370
+ end
371
+ else
372
+ _select name: 'eslevel', id: 'eslevel' do
373
+ _option 'default', selected: !@eslevel || @eslevel == 'default'
374
+ Dir["#{$:.first}/ruby2js/es20*.rb"].sort.each do |file|
375
+ eslevel = File.basename(file, '.rb').sub('es', '')
376
+ _option eslevel, value: eslevel, selected: @eslevel == eslevel
377
+ end
378
+ end
379
+ end
380
+
381
+ _sl_checkbox 'Show AST', id: 'ast', name: 'ast', checked: !!@ast
382
+
383
+ _sl_dropdown.filters! close_on_select: 'false' do
384
+ _sl_button 'Filters', slot: 'trigger', caret: true
385
+ _sl_menu do
386
+ Dir["#{$:.first}/ruby2js/filter/*.rb"].sort.each do |file|
387
+ filter = File.basename(file, '.rb')
388
+ next if filter == 'require'
389
+ _sl_menu_item filter, name: filter,
390
+ checked: selected.include?(filter)
391
+ end
392
+ end
393
+ end
394
+
395
+ _sl_dropdown.options! close_on_select: 'false' do
396
+ _sl_button 'Options', slot: 'trigger', caret: true
397
+ _sl_menu do
398
+ checked = options.dup
399
+ checked[:identity] = options[:comparison] == :identity
400
+ checked[:nullish] = options[:or] == :nullish
401
+
402
+ options_available.each do |option, args|
403
+ next if option == 'filter'
404
+ next if option.start_with? 'require_'
405
+ _sl_menu_item option, name: option,
406
+ checked: checked[option.to_sym],
407
+ data_args: options_available[option]
408
+ end
409
+ end
410
+ end
411
+ end
412
+ end
413
+
414
+ _script %{
415
+ // determine base URL and what filters and options are selected
416
+ let base = new URL(document.getElementsByTagName('base')[0].href).pathname;
417
+ let filters = new Set(window.location.pathname.slice(base.length).split('/'));
418
+ filters.delete('');
419
+ let options = {};
420
+ for (let match of window.location.search.matchAll(/(\\w+)(=([^&]*))?/g)) {
421
+ options[match[1]] = match[3] && decodeURIComponent(match[3]);
422
+ };
423
+ if (options.filter) options.filter.split(',').forEach(option => filters.add(option));
424
+
425
+ function updateLocation(force = false) {
426
+ let location = new URL(base, window.location);
427
+ location.pathname += Array.from(filters).join('/');
428
+
429
+ let search = [];
430
+ for (let [key, value] of Object.entries(options)) {
431
+ search.push(value === undefined ? key : `${key}=${encodeURIComponent(value)}`);
432
+ };
433
+
434
+ location.search = search.length === 0 ? "" : `${search.join('&')}`;
435
+ if (!force && window.location.toString() == location.toString()) return;
436
+
437
+ history.replaceState({}, null, location.toString());
438
+
439
+ if (document.getElementById('js').style.display === 'none') return;
440
+
441
+ // fetch updated results
442
+ let ruby = document.querySelector('textarea[name=ruby]').textContent;
443
+ let ast = document.getElementById('ast').checked;
444
+ let headers = {
445
+ 'Content-Type': 'application/json',
446
+ 'Accept': 'application/json'
447
+ }
448
+
449
+ fetch(location,
450
+ {method: 'POST', headers, body: JSON.stringify({ ruby, ast })}
451
+ ).then(response => {
452
+ return response.json();
453
+ }).
454
+ then(json => {
455
+ document.querySelector('#js pre').textContent = json.js || json.exception;
456
+
457
+ let parsed = document.querySelector('#parsed');
458
+ if (json.parsed) parsed.querySelector('pre').outerHTML = json.parsed;
459
+ parsed.style.display = json.parsed ? "block" : "none";
460
+
461
+ let filtered = document.querySelector('#filtered');
462
+ if (json.filtered) filtered.querySelector('pre').outerHTML = json.filtered;
463
+ filtered.style.display = json.filtered ? "block" : "none";
464
+ }).
465
+ catch(console.error);
466
+ }
467
+
468
+ // show dropdowns (they only appear if JS is enabled)
469
+ let dropdowns = document.querySelectorAll('.dropdown');
470
+ for (let dropdown of dropdowns) {
471
+ dropdown.style.display = 'inline-block';
472
+ let content = dropdown.querySelector('.dropdown-content');
473
+ content.style.opacity = 0;
474
+ content.style.display = 'none';
475
+
476
+ // toggle dropdown
477
+ dropdown.querySelector('button').addEventListener('click', event => {
478
+ event.preventDefault();
479
+ content.style.transition = '0s';
480
+ content.style.display = 'block';
481
+ content.style.zIndex = 1;
482
+ content.style.opacity = 1 - content.style.opacity;
483
+ });
484
+
485
+ // make dropdown disappear when mouse moves away
486
+ let focus = false;
487
+ dropdown.addEventListener('mouseover', () => {focus = true});
488
+ dropdown.addEventListener('mouseout', event => {
489
+ if (content.style.opacity === 0) return;
490
+ focus = false;
491
+ setTimeout( () => {
492
+ if (!focus) {
493
+ content.style.transition = '0.5s';
494
+ content.style.opacity = 0;
495
+ setTimeout( () => { content.style.zIndex = -1; }, 500);
496
+ }
497
+ }, 500)
498
+ })
499
+ };
500
+
501
+ // add/remove eslevel options
502
+ document.getElementById('eslevel').addEventListener('change', event => {
503
+ let value = event.target.value;
504
+ if (value !== "default") options['es' + value] = undefined;
505
+ for (let option of event.target.querySelectorAll('option')) {
506
+ if (option.value === 'default' || option.value === value) continue;
507
+ delete options['es' + option.value];
508
+ };
509
+ updateLocation();
510
+ });
511
+
512
+ // add/remove filters based on checkbox
513
+ let dropdown = document.getElementById('filters');
514
+ for (let filter of dropdown.querySelectorAll('input[type=checkbox]')) {
515
+ filter.addEventListener('click', event => {
516
+ let name = event.target.name;
517
+ if (!filters.delete(name)) filters.add(name);
518
+ updateLocation();
519
+ });
520
+ }
521
+
522
+ // add/remove options based on checkbox
523
+ dropdown = document.getElementById('options');
524
+ for (let option of dropdown.querySelectorAll('input[type=checkbox]')) {
525
+ option.addEventListener('click', event => {
526
+ let name = event.target.name;
527
+
528
+ if (name in options) {
529
+ delete options[name];
530
+ } else if (option.dataset.args) {
531
+ options[name] = prompt(name);
532
+ } else {
533
+ options[name] = undefined;
534
+ };
535
+
536
+ updateLocation();
537
+ })
538
+ };
539
+
540
+ // allow update of option
541
+ for (let span of document.querySelectorAll('input[data-args] + span')) {
542
+ span.addEventListener('click', event => {
543
+ let name = span.previousElementSibling.name;
544
+ options[name] = prompt(name, decodeURIComponent(options[name] || ''));
545
+ span.previousElementSibling.checked = true;
546
+ updateLocation();
547
+ })
548
+ }
549
+
550
+ // refesh on "Show AST" change
551
+ document.getElementById('ast').addEventListener('click', updateLocation);
552
+ }
553
+
554
+ _div_? do
555
+ raise $load_error if $load_error
556
+
557
+ options[:eslevel] = @eslevel.to_i if @eslevel
558
+
559
+ parsed = Ruby2JS.parse(@ruby).first if @ast and @ruby
560
+
561
+ _div.parsed! style: "display: #{@ast ? 'block' : 'none'}" do
562
+ _h2.title.is_size_6 'AST'
563
+ _pre {_ {walk(parsed)}}
564
+ end
565
+
566
+ ruby = Ruby2JS.convert(@ruby, options) if @ruby
567
+
568
+ _div.filtered! style: "display: #{@ast && parsed != ruby.ast ? 'block' : 'none'}" do
569
+ _h2.title.is_size_6 'filtered AST'
570
+ _pre {walk(ruby.ast) if ruby}
571
+ end
572
+
573
+ _div.js! data_controller: @live && 'js', style: "display: #{@ruby ? 'block' : 'none'}" do
574
+ _h2.title.is_size_4 'JavaScript'
575
+ _pre.js ruby.to_s
576
+ end
577
+ end
578
+ end
579
+ end
580
+
581
+ def _ruby2js_logo
582
+ _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
583
+ _g transform: 'matrix(0.97805,-0.208368,0.208368,0.97805,-63.5964,16.8613)' do
584
+ _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);'
585
+ _g.Layer1! transform: 'matrix(0.762386,0,0,0.762386,-83.8231,-163.857)' do
586
+ _g transform: 'matrix(1,0,0,1,1,0)' do
587
+ _path d: 'M253,412.902L323.007,416.982L335.779,302.024L433.521,467.281L346.795,556.198L253,412.902Z', style: 'fill:url(#_Linear1);'
588
+ end
589
+ _g transform: 'matrix(1,0,0,1,90,0)' do
590
+ _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);'
591
+ end
592
+ end
593
+ _g transform: 'matrix(1,0,0,1,-71.912,-102.1)' do
594
+ _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;'
595
+ end
596
+ _g transform: 'matrix(0.76326,0,0,0.76326,-88.595,-169.24)' do
597
+ _g opacity: '0.44' do
598
+ _g.j! transform: 'matrix(0.46717,0,0,0.46717,186.613,178.904)' do
599
+ _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;'
600
+ end
601
+ _g.s! transform: 'matrix(0.46717,0,0,0.46717,185.613,178.904)' do
602
+ _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;'
603
+ end
604
+ end
605
+ end
606
+ _g transform: 'matrix(0.76326,0,0,0.76326,-91.6699,-173.159)' do
607
+ _g.j1! 'serif:id' => 'j', transform: 'matrix(0.46717,0,0,0.46717,186.613,178.904)' do
608
+ _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;'
609
+ end
610
+ _g.s1! 'serif:id' => 's', transform: 'matrix(0.46717,0,0,0.46717,185.613,178.904)' do
611
+ _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;'
612
+ end
613
+ end
614
+ end
615
+ _defs do
616
+ _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
617
+ _stop offset: '0', style: 'stop-color:rgb(97,18,10);stop-opacity:1'
618
+ _stop offset: '1', style: 'stop-color:rgb(184,34,18);stop-opacity:1'
619
+ end
620
+ _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
621
+ _stop offset: '0', style: 'stop-color:rgb(97,18,10);stop-opacity:1'
622
+ _stop offset: '1', style: 'stop-color:rgb(184,34,18);stop-opacity:1'
623
+ end
624
+ end
625
+ end
626
+ end
627
+
628
+ # html fetch support
629
+ _json do
630
+ options = parse_request(env).first
631
+ raise ArgumentError.new($load_error) if $load_error
632
+
633
+ converted = Ruby2JS.convert(@ruby, options)
634
+
635
+ _js converted.to_s
636
+
637
+ if @ast
638
+ parsed = Ruby2JS.parse(@ruby).first
639
+ html = Wunderbar::HtmlMarkup.new(Struct.new(:params, :env).new({}, {}))
640
+ ast = html._pre { html._ {walk(parsed)} }
641
+ _parsed ast.serialize({indent: ' '}).join()
642
+
643
+ if converted.ast != parsed
644
+ ast = html._pre { html._ {walk(converted.ast)} }
645
+ _filtered ast.serialize({indent: ' '}).join()
646
+ end
647
+ end
648
+ end
649
+
650
+ unless env['SERVER_SOFTWARE']
651
+ require 'net/http'
652
+ Thread.new do
653
+ port = env['SERVER_PORT'].to_i
654
+
655
+ # wait for server to start
656
+ 60.times do
657
+ sleep 0.5
658
+ begin
659
+ status = Net::HTTP.get_response('0.0.0.0','/',port).code
660
+ break if %(200 404 500).include? status
661
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT
662
+ end
663
+ end
664
+
665
+ link = "http://localhost:#{port}/"
666
+ if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
667
+ system "start #{link}"
668
+ elsif RbConfig::CONFIG['host_os'] =~ /darwin/
669
+ system "open #{link}"
670
+ elsif RbConfig::CONFIG['host_os'] =~ /linux|bsd/
671
+ if ENV['WSLENV'] and not `which wslview`.empty?
672
+ system "wslview #{link}"
673
+ else
674
+ system "xdg-open #{link}"
675
+ end
676
+ end
677
+ end
678
+ end
679
+ end