ruby2js 4.1.5 → 4.2.1

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,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