ruby2js 4.1.6 → 4.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/ruby2js +4 -0
- data/demo/ruby2js.rb +679 -0
- data/lib/ruby2js/converter/block.rb +14 -0
- data/lib/ruby2js/converter/return.rb +1 -1
- data/lib/ruby2js/demo.rb +28 -0
- data/lib/ruby2js/filter/esm.rb +28 -14
- data/lib/ruby2js/filter/functions.rb +24 -0
- data/lib/ruby2js/filter/lit-element.rb +2 -218
- data/lib/ruby2js/filter/lit.rb +290 -0
- data/lib/ruby2js/filter/react.rb +11 -4
- data/lib/ruby2js/filter/stimulus.rb +2 -2
- data/lib/ruby2js/filter/underscore.rb +23 -17
- data/lib/ruby2js/version.rb +2 -2
- data/lib/ruby2js.rb +11 -0
- data/lib/tasks/install/{litelement.rb → lit-webpacker.rb} +2 -2
- data/lib/tasks/install/stimulus-rollup.rb +44 -0
- data/lib/tasks/install/stimulus-sprockets.rb +16 -23
- data/lib/tasks/install/stimulus-webpacker.rb +3 -0
- data/lib/tasks/ruby2js_tasks.rake +19 -5
- data/ruby2js.gemspec +3 -1
- metadata +10 -5
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
|