dorian 2.4.0 → 2.5.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.
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