dorian 2.4.0 → 2.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +15 -0
  3. data/.github/workflows/ci.yml +38 -0
  4. data/.gitignore +30 -0
  5. data/.node-version +1 -0
  6. data/.prettierignore +2 -0
  7. data/.rubocop.yml +133 -0
  8. data/.ruby-version +1 -0
  9. data/.tool-versions +3 -0
  10. data/Gemfile +20 -0
  11. data/Gemfile.lock +186 -0
  12. data/LICENSE +19 -0
  13. data/README.md +24 -0
  14. data/VERSION +1 -1
  15. data/bin/bundle +123 -0
  16. data/bin/bundle-audit +31 -0
  17. data/bin/bundler-audit +31 -0
  18. data/bin/rspec +31 -0
  19. data/bin/rubocop +31 -0
  20. data/dorian.gemspec +35 -0
  21. data/lib/dorian/bin.rb +742 -82
  22. data/package-lock.json +39 -0
  23. data/package.json +11 -0
  24. data/samples/books.json +33 -0
  25. data/samples/books.jsonl +3 -0
  26. data/samples/config.yml +27 -0
  27. data/samples/config_2.yml +22 -0
  28. data/samples/maths.js +1 -0
  29. data/samples/numbers.raw +10 -0
  30. data/samples/people.csv +6 -0
  31. data/samples/people.yml +28 -0
  32. data/samples/query.sql +7 -0
  33. data/samples/user.json +31 -0
  34. data/spec/dorian_spec.rb +276 -0
  35. data/spec/spec_helper.rb +3 -0
  36. data/vendor/groovy-beautify/LICENSE.MD +21 -0
  37. data/vendor/groovy-beautify/README.md +65 -0
  38. data/vendor/groovy-beautify/dist/__tests__/index.test.d.ts +1 -0
  39. data/vendor/groovy-beautify/dist/cjs/index.js +573 -0
  40. data/vendor/groovy-beautify/dist/cjs/package.json +3 -0
  41. data/vendor/groovy-beautify/dist/es/index.js +682 -0
  42. data/vendor/groovy-beautify/dist/es/package.json +3 -0
  43. data/vendor/groovy-beautify/dist/formatter/FormatRule.d.ts +14 -0
  44. data/vendor/groovy-beautify/dist/formatter/Formatter.d.ts +11 -0
  45. data/vendor/groovy-beautify/dist/formatter/GroovyFormatRules.d.ts +3 -0
  46. data/vendor/groovy-beautify/dist/formatter/__tests__/formatter.test.d.ts +1 -0
  47. data/vendor/groovy-beautify/dist/index.d.ts +5 -0
  48. data/vendor/groovy-beautify/dist/parser/GroovyParseRules.d.ts +3 -0
  49. data/vendor/groovy-beautify/dist/parser/ParseRule.d.ts +23 -0
  50. data/vendor/groovy-beautify/dist/parser/Parser.d.ts +29 -0
  51. data/vendor/groovy-beautify/dist/parser/__tests__/parser.test.d.ts +1 -0
  52. data/vendor/groovy-beautify/dist/parser/types.d.ts +6 -0
  53. data/vendor/groovy-beautify/dist/utils/text.d.ts +4 -0
  54. data/vendor/groovy-beautify/package.json +55 -0
  55. data/vendor/prettier/LICENSE +4379 -0
  56. data/vendor/prettier/README.md +109 -0
  57. data/vendor/prettier/bin/prettier.cjs +71 -0
  58. data/vendor/prettier/doc.d.ts +243 -0
  59. data/vendor/prettier/doc.js +1545 -0
  60. data/vendor/prettier/doc.mjs +1503 -0
  61. data/vendor/prettier/index.cjs +825 -0
  62. data/vendor/prettier/index.d.ts +941 -0
  63. data/vendor/prettier/index.mjs +25899 -0
  64. data/vendor/prettier/internal/cli.mjs +4366 -0
  65. data/vendor/prettier/package.json +198 -0
  66. data/vendor/prettier/plugins/acorn.d.ts +6 -0
  67. data/vendor/prettier/plugins/acorn.js +6618 -0
  68. data/vendor/prettier/plugins/acorn.mjs +6456 -0
  69. data/vendor/prettier/plugins/angular.d.ts +8 -0
  70. data/vendor/prettier/plugins/angular.js +2435 -0
  71. data/vendor/prettier/plugins/angular.mjs +2375 -0
  72. data/vendor/prettier/plugins/babel.d.ts +18 -0
  73. data/vendor/prettier/plugins/babel.js +14271 -0
  74. data/vendor/prettier/plugins/babel.mjs +13986 -0
  75. data/vendor/prettier/plugins/estree.d.ts +1 -0
  76. data/vendor/prettier/plugins/estree.js +9487 -0
  77. data/vendor/prettier/plugins/estree.mjs +9279 -0
  78. data/vendor/prettier/plugins/flow.d.ts +5 -0
  79. data/vendor/prettier/plugins/flow.js +51477 -0
  80. data/vendor/prettier/plugins/flow.mjs +51219 -0
  81. data/vendor/prettier/plugins/glimmer.d.ts +5 -0
  82. data/vendor/prettier/plugins/glimmer.js +8534 -0
  83. data/vendor/prettier/plugins/glimmer.mjs +8421 -0
  84. data/vendor/prettier/plugins/graphql.d.ts +5 -0
  85. data/vendor/prettier/plugins/graphql.js +2421 -0
  86. data/vendor/prettier/plugins/graphql.mjs +2347 -0
  87. data/vendor/prettier/plugins/html.d.ts +8 -0
  88. data/vendor/prettier/plugins/html.js +8182 -0
  89. data/vendor/prettier/plugins/html.mjs +8077 -0
  90. data/vendor/prettier/plugins/markdown.d.ts +7 -0
  91. data/vendor/prettier/plugins/markdown.js +9068 -0
  92. data/vendor/prettier/plugins/markdown.mjs +8977 -0
  93. data/vendor/prettier/plugins/meriyah.d.ts +5 -0
  94. data/vendor/prettier/plugins/meriyah.js +5953 -0
  95. data/vendor/prettier/plugins/meriyah.mjs +5800 -0
  96. data/vendor/prettier/plugins/postcss.d.ts +7 -0
  97. data/vendor/prettier/plugins/postcss.js +9244 -0
  98. data/vendor/prettier/plugins/postcss.mjs +9046 -0
  99. data/vendor/prettier/plugins/typescript.d.ts +5 -0
  100. data/vendor/prettier/plugins/typescript.js +38058 -0
  101. data/vendor/prettier/plugins/typescript.mjs +37732 -0
  102. data/vendor/prettier/plugins/yaml.d.ts +5 -0
  103. data/vendor/prettier/plugins/yaml.js +7425 -0
  104. data/vendor/prettier/plugins/yaml.mjs +7329 -0
  105. data/vendor/prettier/standalone.d.ts +33 -0
  106. data/vendor/prettier/standalone.js +3984 -0
  107. data/vendor/prettier/standalone.mjs +3938 -0
  108. data/vendor/sql-formatter.js +18762 -0
  109. metadata +205 -2
data/lib/dorian/bin.rb CHANGED
@@ -6,14 +6,82 @@ require "dorian/eval"
6
6
  require "dorian/progress"
7
7
  require "dorian/to_struct"
8
8
  require "git"
9
+ require "hexapdf"
9
10
  require "json"
11
+ require "mini_racer"
10
12
  require "net/http"
11
13
  require "parallel"
14
+ require "shellwords"
15
+ require "syntax_tree"
16
+ require "syntax_tree/erb"
17
+ require "syntax_tree/haml"
18
+ require "syntax_tree/xml"
19
+ require "syntax_tree/json"
20
+ require "tempfile"
21
+ require "terminal-table"
12
22
  require "uri"
13
23
  require "yaml"
14
24
 
15
25
  class Dorian
16
26
  class Bin
27
+ RUBY_EXTENSIONS = %w[
28
+ .rb
29
+ .arb
30
+ .axlsx
31
+ .builder
32
+ .fcgi
33
+ .gemfile
34
+ .gemspec
35
+ .god
36
+ .jb
37
+ .jbuilder
38
+ .mspec
39
+ .opal
40
+ .pluginspec
41
+ .podspec
42
+ .rabl
43
+ .rake
44
+ .rbuild
45
+ .rbw
46
+ .rbx
47
+ .ru
48
+ .ruby
49
+ .schema
50
+ .spec
51
+ .thor
52
+ .watchr
53
+ ].freeze
54
+
55
+ RUBY_FILENAMES = %w[
56
+ .irbrc
57
+ .pryrc
58
+ .simplecov
59
+ Appraisals
60
+ Berksfile
61
+ Brewfile
62
+ Buildfile
63
+ Capfile
64
+ Cheffile
65
+ Dangerfile
66
+ Deliverfile
67
+ Fastfile
68
+ Gemfile
69
+ Guardfile
70
+ Jarfile
71
+ Mavenfile
72
+ Podfile
73
+ Puppetfile
74
+ Rakefile
75
+ rakefile
76
+ Schemafile
77
+ Snapfile
78
+ Steepfile
79
+ Thorfile
80
+ Vagabondfile
81
+ Vagrantfile
82
+ buildfile
83
+ ].freeze
84
+
17
85
  VERSION = File.read(File.expand_path("../../VERSION", __dir__))
18
86
 
19
87
  DEFAULT_IO = :raw
@@ -51,8 +119,7 @@ class Dorian
51
119
  alias: :o
52
120
  },
53
121
  parallel: {
54
- alias: :p,
55
- default: true
122
+ alias: :p
56
123
  },
57
124
  parallel_type: {
58
125
  alias: :pt,
@@ -206,6 +273,83 @@ class Dorian
206
273
  arguments.delete("ls")
207
274
  @command = :ls
208
275
  command_ls
276
+ when :strip
277
+ arguments.delete("strip")
278
+ @command = :strip
279
+ command_strip
280
+ when :rstrip
281
+ arguments.delete("rstrip")
282
+ @command = :rstrip
283
+ command_rstrip
284
+ when :lstrip
285
+ arguments.delete("lstrip")
286
+ @command = :lstrip
287
+ command_lstrip
288
+ when :merge
289
+ arguments.delete("merge")
290
+ @command = :merge
291
+ command_merge
292
+ when :pluck
293
+ arguments.delete("pluck")
294
+ @command = :pluck
295
+ command_pluck
296
+ when :shuffle
297
+ arguments.delete("shuffle")
298
+ @command = :shuffle
299
+ command_shuffle
300
+ when :rename
301
+ arguments.delete("rename")
302
+ @command = :rename
303
+ command_rename
304
+ when :replace
305
+ arguments.delete("replace")
306
+ @command = :replace
307
+ command_replace
308
+ when :sort
309
+ arguments.delete("sort")
310
+ @command = :sort
311
+ command_sort
312
+ when :table
313
+ arguments.delete("table")
314
+ @command = :table
315
+ command_table
316
+ when :then
317
+ arguments.delete("then")
318
+ @command = :then
319
+ @ruby = arguments.delete_at(0)
320
+ command_then
321
+ when :times
322
+ arguments.delete("times")
323
+ @command = :times
324
+ command_times
325
+ when :uniq
326
+ arguments.delete("uniq")
327
+ @command = :uniq
328
+ command_uniq
329
+ when :write
330
+ arguments.delete("write")
331
+ @command = :write
332
+ command_write
333
+ when :release
334
+ arguments.delete("release")
335
+ @command = :release
336
+ command_release
337
+ when :top
338
+ arguments.delete("top")
339
+ @command = :top
340
+ command_top
341
+ when :tree
342
+ arguments.delete("tree")
343
+ @command = :tree
344
+ command_tree
345
+ when :format
346
+ arguments.delete("format")
347
+ @command = :format
348
+ command_format
349
+ when :pretty
350
+ arguments.delete("pretty")
351
+ @command = :pretty
352
+ command_pretty
209
353
  else
210
354
  arguments.delete("read")
211
355
  @command = :read
@@ -213,12 +357,217 @@ class Dorian
213
357
  end
214
358
  end
215
359
 
360
+ def command_pretty
361
+ command_format
362
+ end
363
+
364
+ def command_format
365
+ context = MiniRacer::Context.new
366
+ context.attach("puts", proc { |string| puts(string) })
367
+ context.attach("warn", proc { |string| warn(string) })
368
+ context.attach("read", proc { |path| File.read(path) })
369
+ context.attach(
370
+ "write",
371
+ proc { |path, content| File.write(path, content) }
372
+ )
373
+
374
+ root = File.expand_path("../../", __dir__)
375
+
376
+ prettier_path = File.join(root, "vendor/prettier/standalone.js")
377
+ prettier_js = File.read(prettier_path)
378
+ context.eval(prettier_js)
379
+
380
+ sql_path = File.join(root, "vendor/sql-formatter.js")
381
+ sql_js = File.read(sql_path)
382
+ context.eval("self = {};")
383
+ context.eval(sql_js)
384
+ context.eval("sqlFormatter = self.sqlFormatter;")
385
+
386
+ groovy_path = File.join(root, "vendor/groovy-beautify/dist/cjs/index.js")
387
+ groovy_js = File.read(groovy_path)
388
+ context.eval("module = { exports: {} };")
389
+ context.eval("exports = module.exports;")
390
+ context.eval(groovy_js)
391
+ context.eval("groovyBeautify = module.exports;")
392
+
393
+ plugins = %w[babel estree typescript html postcss markdown]
394
+
395
+ plugins.each do |plugin|
396
+ path = File.join(root, "vendor/prettier/plugins/#{plugin}.js")
397
+ js = File.read(path)
398
+ context.eval("module = { exports: {} };")
399
+ context.eval("exports = module.exports;")
400
+ context.eval(js)
401
+ context.eval("#{plugin} = module.exports;")
402
+ end
403
+
404
+ context.eval("plugins = [#{plugins.join(", ")}];")
405
+
406
+ context.eval(<<~JS)
407
+ format = async (path, parser) => {
408
+ try {
409
+ const before = read(path);
410
+ let after;
411
+
412
+ if (parser === "sql") {
413
+ after = sqlFormatter.format(before);
414
+ } else if (parser === "groovy") {
415
+ after = groovyBeautify(before);
416
+ } else {
417
+ after = await prettier.format(before, { parser, plugins });
418
+ }
419
+
420
+ if (before != after) {
421
+ puts(path);
422
+ write(path, after);
423
+ }
424
+ } catch (e) {
425
+ warn(`failed to parse ${path}: ${e.message.split("\\n")[0]}`);
426
+ }
427
+ };
428
+ JS
429
+
430
+ if files.any?
431
+ each(files) { |file| format(file, context:) }
432
+ else
433
+ each(
434
+ Git.open(".").ls_files.map(&:first)
435
+ ) { |file| format(file, context:) }
436
+ end
437
+ end
438
+
439
+ def command_release
440
+ File.delete(*Dir["*.gem"])
441
+ system("gem build")
442
+ system("gem push *.gem")
443
+ File.delete(*Dir["*.gem"])
444
+ end
445
+
446
+ def command_top
447
+ shell = arguments[0] || File.basename(ENV.fetch("SHELL", nil)) || "bash"
448
+ limit = arguments[1] || 10
449
+
450
+ history =
451
+ case shell.to_s.downcase
452
+ when "fish"
453
+ File
454
+ .read("#{Dir.home}/.local/share/fish/fish_history")
455
+ .lines
456
+ .grep(/^- cmd: /)
457
+ .map { |line| line.split("- cmd: ", 2).last.strip }
458
+ when "bash"
459
+ File.read("#{Dir.home}/.bash_history").lines.map(&:strip)
460
+ when "zsh"
461
+ File.read("#{Dir.home}/.zsh_history").lines.map(&:strip)
462
+ else
463
+ raise NotImplementedError, shell
464
+ end
465
+
466
+ table(
467
+ history
468
+ .map { |line| line.split.first }
469
+ .tally
470
+ .to_a
471
+ .sort_by(&:last)
472
+ .reverse
473
+ .map
474
+ .with_index do |(command, command_count), index|
475
+ {
476
+ "#" => index + 1,
477
+ :count => command_count,
478
+ :percent =>
479
+ "#{(command_count * 100 / history.size.to_f).round(3)}%",
480
+ :command => command
481
+ }
482
+ end
483
+ .first(limit)
484
+ )
485
+ end
486
+
487
+ def command_tree
488
+ space = " "
489
+ right = "└── "
490
+ down = "│   "
491
+ down_and_right = "├── "
492
+
493
+ git_ls_files = ->(path) { Git.open(".").ls_files(path).map(&:first) }
494
+
495
+ group =
496
+ lambda do |files|
497
+ files
498
+ .group_by { |file| file.split("/").first }
499
+ .transform_values do |values|
500
+ group.call(
501
+ values
502
+ .map { |value| value.split("/")[1..].join("/") }
503
+ .reject(&:empty?)
504
+ )
505
+ end
506
+ end
507
+
508
+ print =
509
+ lambda do |key:, values:, index: 0, size: 1, prefix: ""|
510
+ key = "#{key}/" if values.any?
511
+ last = index + 1 == size
512
+ right_prefix = last ? right : down_and_right
513
+ puts prefix + right_prefix + key
514
+ values.each.with_index do |(value_key, value_values), value_index|
515
+ print.call(
516
+ key: value_key,
517
+ values: value_values,
518
+ index: value_index,
519
+ size: values.size,
520
+ prefix: prefix + (last ? space : down)
521
+ )
522
+ end
523
+ end
524
+
525
+ keys = (arguments + files)
526
+ keys = ["."] unless keys.any?
527
+
528
+ keys.each do |key|
529
+ files =
530
+ git_ls_files
531
+ .call(key)
532
+ .map { |file| parsed.arguments.any? ? file.sub(key, "") : file }
533
+ values = group.call(files)
534
+ key = "#{key}/" if values.any? && key != "." && key[-1] != "/"
535
+ puts key
536
+ values.each.with_index do |(value_key, value_values), value_index|
537
+ print.call(
538
+ key: value_key,
539
+ values: value_values,
540
+ index: value_index,
541
+ size: values.size
542
+ )
543
+ end
544
+ end
545
+ end
546
+
216
547
  def files
217
548
  parsed.files
218
549
  end
219
550
 
551
+ def command_times
552
+ map(everything, &:to_i).sum.times { |index| puts index + 1 }
553
+ end
554
+
220
555
  def command_eval
221
- each(everything) { |thing| outputs(evaluates(ruby: thing).returned) }
556
+ each(everything) { |thing| outputs(evaluates(ruby: thing)) }
557
+ end
558
+
559
+ def command_then
560
+ each(stdin_files + files) do |input|
561
+ outputs(evaluates(it: reads(File.read(input))), file: input)
562
+ end
563
+
564
+ each(stdin_arguments + arguments) do |input|
565
+ outputs(evaluates(it: reads(input)))
566
+ end
567
+ end
568
+
569
+ def command_table
570
+ table(map(everything) { |thing| lines(reads(thing)) }.inject(&:+))
222
571
  end
223
572
 
224
573
  def command_chat
@@ -258,6 +607,39 @@ class Dorian
258
607
  puts message
259
608
  end
260
609
 
610
+ def command_replace
611
+ from, to = arguments
612
+
613
+ each(stdin_files + stdin_arguments + files) do |file|
614
+ next if File.directory?(file)
615
+
616
+ File.write(file, File.read(file).gsub(from, to))
617
+ end
618
+ end
619
+
620
+ def command_rename
621
+ from, to = arguments
622
+ files = stdin_files + stdin_arguments + self.files
623
+ (files - directories).each { |file| rename(file, file.gsub(from, to)) }
624
+ directories.each { |dir| rename(dir, dir.gsub(from, to)) }
625
+ end
626
+
627
+ def directories
628
+ (stdin_files + files).select { |file| File.directory?(file) }
629
+ end
630
+
631
+ def rename(old, new)
632
+ return if old == new
633
+
634
+ puts "#{old} -> #{new}"
635
+ File.rename(old, new)
636
+ end
637
+
638
+ def command_write
639
+ content = read_stdin.join
640
+ each(files + arguments) { |file| File.write(file, content) }
641
+ end
642
+
261
643
  def command_read
262
644
  each(stdin_files + files) do |input|
263
645
  outputs(reads(File.read(input)), file: input)
@@ -266,6 +648,36 @@ class Dorian
266
648
  each(stdin_arguments + arguments) { |input| outputs(reads(input)) }
267
649
  end
268
650
 
651
+ def command_strip
652
+ each(stdin_files + files) do |input|
653
+ outputs(lines(reads(File.read(input)), strip: :strip), file: input)
654
+ end
655
+
656
+ each(stdin_arguments + arguments) do |input|
657
+ outputs(lines(reads(input), strip: :strip))
658
+ end
659
+ end
660
+
661
+ def command_rstrip
662
+ each(stdin_files + files) do |input|
663
+ outputs(lines(reads(File.read(input)), strip: :rstrip), file: input)
664
+ end
665
+
666
+ each(stdin_arguments + arguments) do |input|
667
+ outputs(lines(reads(input), strip: :rstrip))
668
+ end
669
+ end
670
+
671
+ def command_lstrip
672
+ each(stdin_files + files) do |input|
673
+ outputs(lines(reads(File.read(input)), strip: :lstrip), file: input)
674
+ end
675
+
676
+ each(stdin_arguments + arguments) do |input|
677
+ outputs(lines(reads(input), strip: :lstrip))
678
+ end
679
+ end
680
+
269
681
  def everything
270
682
  read_stdin_files + stdin_arguments + read_files + arguments
271
683
  end
@@ -335,7 +747,9 @@ class Dorian
335
747
 
336
748
  homefile = "#{Dir.home}/#{file}"
337
749
  dotfile = File.expand_path("#{dir.chomp("/")}/#{file}")
338
- File.delete(homefile) if File.exist?(homefile) || File.symlink?(homefile)
750
+ if File.exist?(homefile) || File.symlink?(homefile)
751
+ File.delete(homefile)
752
+ end
339
753
  FileUtils.mkdir_p(File.dirname(homefile))
340
754
  FileUtils.ln_s(dotfile, homefile, verbose: true)
341
755
  end
@@ -370,6 +784,43 @@ class Dorian
370
784
  end
371
785
  end
372
786
 
787
+ def command_merge
788
+ outputs(map(everything) { |thing| lines(reads(thing)) }.inject(&:+))
789
+ end
790
+
791
+ def command_sort
792
+ outputs(
793
+ map(everything) { |thing| lines(reads(thing)) }
794
+ .inject(&:+)
795
+ .sort_by do |line|
796
+ result = pluck(line).from_deep_struct
797
+ result.is_a?(Hash) ? result.values : result
798
+ end
799
+ )
800
+ end
801
+
802
+ def command_uniq
803
+ outputs(
804
+ map(everything) { |thing| lines(reads(thing)) }
805
+ .inject(&:+)
806
+ .uniq { |line| pluck(line) }
807
+ )
808
+ end
809
+
810
+ def command_pluck
811
+ outputs(
812
+ map(everything) do |thing|
813
+ map(lines(reads(thing))) { |line| pluck(line) }
814
+ end.inject(&:+)
815
+ )
816
+ end
817
+
818
+ def command_shuffle
819
+ outputs(
820
+ map(everything) { |thing| lines(reads(thing)) }.inject(&:+).shuffle
821
+ )
822
+ end
823
+
373
824
  def command_tally
374
825
  each(everything) do |input|
375
826
  outputs(
@@ -378,7 +829,7 @@ class Dorian
378
829
  if ruby.to_s.empty?
379
830
  element
380
831
  else
381
- evaluates(it: element, returns: true, stdout: false).returned
832
+ evaluates(it: element, returns: true, stdout: false)
382
833
  end
383
834
  end.tally
384
835
  )
@@ -393,11 +844,13 @@ class Dorian
393
844
  end
394
845
 
395
846
  def command_append
396
- outputs(everything.sum { |input| lines(reads(input)) })
847
+ outputs(map(everything) { |input| lines(reads(input)) }.inject(&:+))
397
848
  end
398
849
 
399
850
  def command_prepend
400
- outputs(everything.reverse.sum { |input| lines(reads(input)) })
851
+ outputs(
852
+ map(everything.reverse) { |input| lines(reads(input)) }.inject(&:+)
853
+ )
401
854
  end
402
855
 
403
856
  def command_select
@@ -479,20 +932,23 @@ class Dorian
479
932
 
480
933
  case output
481
934
  when :csv
482
- (headers_of(content) ? headers_of(content).to_csv : "") +
935
+ "#{headers_of(content)&.to_csv}#{
483
936
  map(content) do |element|
484
937
  CSV.generate(headers: headers_of(content)) do |csv|
485
938
  csv << wrap(element)
486
939
  end
487
940
  end.join
941
+ }"
488
942
  when :json
489
943
  pretty? ? JSON.pretty_generate(content) : content.to_json
490
- when :jsonl, :yamll
491
- map(content, &:to_json).join("\n")
944
+ when :jsonl
945
+ "#{map(content, &:to_json).join("\n")}\n"
492
946
  when :raw
493
947
  content
494
948
  when :yaml
495
949
  content.to_yaml
950
+ when :yamll
951
+ "#{map(map(content, &:to_yaml), &:to_json).join("\n")}\n"
496
952
  else
497
953
  abort "#{output.inspect} not supported"
498
954
  end
@@ -508,12 +964,16 @@ class Dorian
508
964
  end
509
965
  when :json
510
966
  JSON.parse(content).to_deep_struct
511
- when :jsonl, :yamll
967
+ when :jsonl
512
968
  map(content.lines) { |line| JSON.parse(line) }.to_deep_struct
513
969
  when :raw
514
970
  content
515
971
  when :yaml
516
972
  YAML.safe_load(content).to_deep_struct
973
+ when :yamll
974
+ map(content.lines) do |line|
975
+ YAML.safe_load(JSON.parse(line))
976
+ end.to_deep_struct
517
977
  else
518
978
  abort "#{input.inspect} not supported"
519
979
  end
@@ -722,9 +1182,9 @@ class Dorian
722
1182
  end
723
1183
  end
724
1184
 
725
- def lines(input)
1185
+ def lines(input, strip: :rstrip)
726
1186
  if input.is_a?(String)
727
- input.lines.map(&:rstrip)
1187
+ input.lines.map(&strip)
728
1188
  elsif deep?
729
1189
  deep_lines(input)
730
1190
  else
@@ -827,7 +1287,7 @@ class Dorian
827
1287
  end
828
1288
 
829
1289
  def match?(element, ruby: @ruby)
830
- !!evaluates(ruby:, it: element, stdout: false, returns: true).returned
1290
+ !!evaluates(ruby:, it: element, stdout: false, returns: true)
831
1291
  end
832
1292
 
833
1293
  def token(file)
@@ -934,77 +1394,65 @@ class Dorian
934
1394
  Tiktoken.encoding_for_model("gpt-4o")
935
1395
  end
936
1396
 
937
- def match_filetypes?(path)
938
- return true unless arguments.any?
939
- return true unless arguments.intersect?(["rb", "ruby"])
940
-
941
- ruby_extensions = %w[
942
- .rb
943
- .arb
944
- .axlsx
945
- .builder
946
- .fcgi
947
- .gemfile
948
- .gemspec
949
- .god
950
- .jb
951
- .jbuilder
952
- .mspec
953
- .opal
954
- .pluginspec
955
- .podspec
956
- .rabl
957
- .rake
958
- .rbuild
959
- .rbw
960
- .rbx
961
- .ru
962
- .ruby
963
- .schema
964
- .spec
965
- .thor
966
- .watchr
967
- ]
968
-
969
- ruby_filenames = %w[
970
- .irbrc
971
- .pryrc
972
- .simplecov
973
- Appraisals
974
- Berksfile
975
- Brewfile
976
- Buildfile
977
- Capfile
978
- Cheffile
979
- Dangerfile
980
- Deliverfile
981
- Fastfile
982
- Gemfile
983
- Guardfile
984
- Jarfile
985
- Mavenfile
986
- Podfile
987
- Puppetfile
988
- Rakefile
989
- rakefile
990
- Schemafile
991
- Snapfile
992
- Steepfile
993
- Thorfile
994
- Vagabondfile
995
- Vagrantfile
996
- buildfile
997
- ]
998
-
1397
+ def match_filetypes?(path, filetypes: arguments)
1398
+ return true if filetypes.none?
1399
+ return true unless filetypes.intersect?(%w[rb ruby])
999
1400
  return false if Dir.exist?(path)
1000
- return true if ruby_filenames.include?(path)
1001
- return true if ruby_extensions.include?(File.extname(path))
1401
+ return true if RUBY_FILENAMES.include?(path)
1402
+ return true if RUBY_EXTENSIONS.include?(File.extname(path))
1002
1403
  return false unless File.exist?(path)
1003
1404
 
1004
- first_line =
1005
- File.open(path, &:gets).to_s.encode("UTF-8", invalid: :replace)
1405
+ first_line = File.open(path, &:gets).to_s
1406
+ first_line = first_line.encode("UTF-8", invalid: :replace)
1006
1407
 
1007
- /\A#!.*ruby\z/.match?(first_line)
1408
+ return true if /\A#!.*ruby\z/.match?(first_line)
1409
+
1410
+ false
1411
+ end
1412
+
1413
+ def pluck(element)
1414
+ element = element.from_deep_struct
1415
+
1416
+ keys =
1417
+ if arguments.any?
1418
+ arguments
1419
+ elsif element.is_a?(Hash)
1420
+ element.keys
1421
+ elsif element.is_a?(Array)
1422
+ (0...(element.size)).map(&:to_s)
1423
+ else
1424
+ "it"
1425
+ end
1426
+
1427
+ results =
1428
+ keys.map do |argument|
1429
+ if element.is_a?(Array) && argument.to_i.to_s == argument
1430
+ element[argument.to_i]
1431
+ elsif element.is_a?(Hash) && element.key?(argument)
1432
+ { argument => element[argument] }
1433
+ else
1434
+ evaluates(ruby: argument, it: element.to_deep_struct)
1435
+ end
1436
+ end
1437
+
1438
+ if results.all?(Hash)
1439
+ results.inject(&:merge).to_deep_struct
1440
+ else
1441
+ results
1442
+ .map { |result| result.is_a?(Hash) ? result.values.first : result }
1443
+ .to_deep_struct
1444
+ end
1445
+ end
1446
+
1447
+ def table(data)
1448
+ is_hashes = data.first.from_deep_struct.is_a?(Hash)
1449
+ headings = is_hashes ? data.first.to_h.keys : nil
1450
+ rows = is_hashes ? data.map(&:values) : data.map { |row| wrap(row) }
1451
+ if headings
1452
+ puts Terminal::Table.new(headings:, rows:)
1453
+ else
1454
+ puts Terminal::Table.new(rows:)
1455
+ end
1008
1456
  end
1009
1457
 
1010
1458
  def evaluates(
@@ -1028,7 +1476,219 @@ class Dorian
1028
1476
  rails:,
1029
1477
  fast:,
1030
1478
  returns:
1031
- )
1479
+ ).returned
1480
+ end
1481
+
1482
+ def sort(object)
1483
+ object = object.from_deep_struct
1484
+
1485
+ if object.is_a?(Hash)
1486
+ object
1487
+ .to_a
1488
+ .sort_by(&:first)
1489
+ .to_h
1490
+ .transform_values { |value| sort(value) }
1491
+ elsif object.is_a?(Array)
1492
+ object.map { |element| sort(element) }
1493
+ else
1494
+ object
1495
+ end
1496
+ end
1497
+
1498
+ def filetype(path)
1499
+ ext = File.extname(path).to_s.downcase
1500
+ return :directory if Dir.exist?(path)
1501
+ return :symlink if File.symlink?(path)
1502
+ return :ruby if RUBY_FILENAMES.include?(File.basename(path))
1503
+ return :ruby if RUBY_EXTENSIONS.include?(ext)
1504
+ return :json if ext == ".json"
1505
+ return :jsonl if ext == ".jsonl"
1506
+ return :yaml if ext == ".yaml"
1507
+ return :yaml if ext == ".yml"
1508
+ return :yamll if ext == ".yamll"
1509
+ return :yamll if ext == ".ymll"
1510
+ return :csv if ext == ".csv"
1511
+ return :js if ext == ".js"
1512
+ return :js if ext == ".mjs"
1513
+ return :js if ext == ".cjs"
1514
+ return :ts if ext == ".ts"
1515
+ return :css if ext == ".css"
1516
+ return :html if ext == ".html"
1517
+ return :html if ext == ".htm"
1518
+ return :haml if ext == ".haml"
1519
+ return :slim if ext == ".slim"
1520
+ return :erb if ext == ".erb"
1521
+ return :fish if ext == ".fish"
1522
+ return :sql if ext == ".sql"
1523
+ return :tex if ext == ".tex"
1524
+ return :md if ext == ".md"
1525
+ return :md if ext == ".markdown"
1526
+ return :png if ext == ".png"
1527
+ return :jpeg if ext == ".jpg"
1528
+ return :jpeg if ext == ".jpeg"
1529
+ return :ico if ext == ".ico"
1530
+ return :webp if ext == ".webp"
1531
+ return :heic if ext == ".heic"
1532
+ return :pdf if ext == ".pdf"
1533
+ return :raw if ext == ".raw"
1534
+ return :env if path == ".env"
1535
+ return :env if path.start_with?(".env.")
1536
+ return :sh if path == "Dockerfile"
1537
+ return :sh if ext == ".sh"
1538
+ return :enc if ext == ".enc"
1539
+ return :enc if ext == ".keystore"
1540
+ return :pro if ext == ".pro"
1541
+ return :txt if ext == ".txt"
1542
+ return :bat if ext == ".bat"
1543
+ return :xcconfig if ext == ".xcconfig"
1544
+ return :pbxproj if ext == ".pbxproj"
1545
+ return :xml if ext == ".xml"
1546
+ return :xml if ext == ".plist"
1547
+ return :xml if ext == ".storyboard"
1548
+ return :xml if ext == ".xcscheme"
1549
+ return :xml if ext == ".xcworkspacedata"
1550
+ return :xml if ext == ".xcprivacy"
1551
+ return :xml if ext == ".entitlements"
1552
+ return :kotlin if ext == ".kt"
1553
+ return :groovy if ext == ".gradle"
1554
+ return :groovy if ext == ".properties"
1555
+ return :binary if ext == ".jar"
1556
+ return :objectivec if ext == ".h"
1557
+ return :objectivec if ext == ".mm"
1558
+ return :objectivec if ext == ".m"
1559
+ return unless File.exist?(path)
1560
+
1561
+ first_line = File.open(path, &:gets).to_s
1562
+ first_line = first_line.encode("UTF-8", invalid: :replace).strip
1563
+ return :ruby if first_line == "#!/usr/bin/env ruby"
1564
+ return :sh if first_line == "#!/bin/bash"
1565
+ return :sh if first_line == "#!/bin/sh"
1566
+ return :sh if first_line == "#!/bin/bash -e"
1567
+ return :sh if first_line == "#!/usr/bin/env sh"
1568
+
1569
+ nil
1570
+ end
1571
+
1572
+ def format(path, context:)
1573
+ return if File.symlink?(path)
1574
+ return if File.directory?(path)
1575
+ return unless File.exist?(path)
1576
+
1577
+ before = File.read(path)
1578
+
1579
+ case filetype(path)
1580
+ when :ruby
1581
+ after = SyntaxTree.format(before)
1582
+ when :haml
1583
+ after = SyntaxTree::Haml.format(before)
1584
+ when :erb
1585
+ after = SyntaxTree::ERB.format(before)
1586
+ when :xml
1587
+ after = SyntaxTree::XML.format(before)
1588
+ when :json
1589
+ after = JSON.pretty_generate(JSON.parse(before))
1590
+ when :jsonl
1591
+ after =
1592
+ "#{before.lines.map { |line| JSON.parse(line).to_json }.join("\n")}\n"
1593
+ when :csv
1594
+ after =
1595
+ "#{CSV.generate { |csv| CSV.parse(before).each { |row| csv << row } }}\n"
1596
+ when :yaml
1597
+ after = sort(YAML.safe_load(before)).to_yaml
1598
+ when :yamll
1599
+ after =
1600
+ "#{
1601
+ before
1602
+ .lines
1603
+ .map do |line|
1604
+ sort(YAML.safe_load(JSON.parse(line))).to_yaml.to_json
1605
+ end
1606
+ .join("\n")
1607
+ }\n"
1608
+ when :js
1609
+ context.eval("format(#{path.to_json}, 'babel')")
1610
+ when :ts
1611
+ context.eval("format(#{path.to_json}, 'typescript')")
1612
+ when :html
1613
+ context.eval("format(#{path.to_json}, 'html')")
1614
+ when :md
1615
+ context.eval("format(#{path.to_json}, 'markdown')")
1616
+ when :sql
1617
+ context.eval("format(#{path.to_json}, 'sql')")
1618
+ when :groovy
1619
+ context.eval("format(#{path.to_json}, 'groovy')")
1620
+ when :css
1621
+ context.eval("format(#{path.to_json}, 'css')")
1622
+ when :sh
1623
+ if system("command -v shfmt > /dev/null 2>&1")
1624
+ command = ["shfmt", "--indent", "4", path].shelljoin
1625
+ stdout, stderr, status = Open3.capture3(command)
1626
+ raise stderr unless stderr.empty? && status.success?
1627
+
1628
+ after = stdout
1629
+ else
1630
+ warn "run: `brew install shfmt` for #{path}"
1631
+ end
1632
+ when :pdf
1633
+ doc = HexaPDF::Document.open(path)
1634
+ doc.trailer.info.each { |key, _| doc.trailer.info.delete(key) }
1635
+ doc.write(path, update_fields: false)
1636
+ after = File.read(path)
1637
+ when :tex
1638
+ if system("command -v latexindent > /dev/null 2>&1")
1639
+ command = ["latexindent", path, "--logfile", "/dev/null"].shelljoin
1640
+ stdout, stderr, status = Open3.capture3(command)
1641
+ raise stderr unless stderr.empty? && status.success?
1642
+
1643
+ after = stdout.gsub("\t", " ")
1644
+ else
1645
+ warn "run: `brew install latexindent` for #{path}"
1646
+ end
1647
+ when :objectivec
1648
+ if system("command -v clang-format > /dev/null 2>&1")
1649
+ command = ["clang-format", path].shelljoin
1650
+ stdout, stderr, status = Open3.capture3(command)
1651
+ raise stderr unless stderr.empty? && status.success?
1652
+
1653
+ after = stdout.gsub("\t", " ")
1654
+ else
1655
+ warn "run: `brew install clang-format` for #{path}"
1656
+ end
1657
+ when :kotlin
1658
+ if system("command -v ktlint > /dev/null 2>&1")
1659
+ command = ["ktlint", "-F", path].shelljoin
1660
+ stdout, stderr, status = Open3.capture3(command)
1661
+ raise stderr unless stderr.empty? && status.success?
1662
+
1663
+ after = File.read(path)
1664
+ else
1665
+ warn "run: `brew install ktlint` for #{path}"
1666
+ end
1667
+ when :raw, :env, :enc, :txt, :pro, :binary, :slim, :fish, :bat, :xcconfig,
1668
+ :pbxproj, :jpeg, :png, :webp, :heic, :ico
1669
+ # nothing to do
1670
+ else
1671
+ case File.basename(path)
1672
+ when ".gitignore", ".node-version", ".prettierignore", ".ruby-version",
1673
+ ".tool-versions", "Gemfile.lock", "LICENSE", "VERSION", ".rspec",
1674
+ "Procfile", "Procfile.dev", "Podfile.lock", ".xcode.env", "CNAME",
1675
+ "TODO", ".gitmodules", ".asdfrc", "config", ".dotignore", ".gemrc",
1676
+ ".gitconfig", ".gitmessage", ".hushlogin", ".psqlrc", ".vimrc",
1677
+ "DIRECTORIES"
1678
+ # nothing to do
1679
+ when ".keep"
1680
+ File.write(path, "")
1681
+ else
1682
+ puts "unhandled: #{path}"
1683
+ end
1684
+ end
1685
+
1686
+ if after && before != after
1687
+ puts path
1688
+ File.write(path, after)
1689
+ end
1690
+ rescue StandardError => e
1691
+ warn "failed to parse #{path}: #{e.message}"
1032
1692
  end
1033
1693
  end
1034
1694
  end