dorian 2.4.0 → 2.5.0

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 +129 -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 +739 -81
  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
@@ -487,12 +940,14 @@ class Dorian
487
940
  end.join
488
941
  when :json
489
942
  pretty? ? JSON.pretty_generate(content) : content.to_json
490
- when :jsonl, :yamll
491
- map(content, &:to_json).join("\n")
943
+ when :jsonl
944
+ "#{map(content, &:to_json).join("\n")}\n"
492
945
  when :raw
493
946
  content
494
947
  when :yaml
495
948
  content.to_yaml
949
+ when :yamll
950
+ "#{map(map(content, &:to_yaml), &:to_json).join("\n")}\n"
496
951
  else
497
952
  abort "#{output.inspect} not supported"
498
953
  end
@@ -508,12 +963,16 @@ class Dorian
508
963
  end
509
964
  when :json
510
965
  JSON.parse(content).to_deep_struct
511
- when :jsonl, :yamll
966
+ when :jsonl
512
967
  map(content.lines) { |line| JSON.parse(line) }.to_deep_struct
513
968
  when :raw
514
969
  content
515
970
  when :yaml
516
971
  YAML.safe_load(content).to_deep_struct
972
+ when :yamll
973
+ map(content.lines) do |line|
974
+ YAML.safe_load(JSON.parse(line))
975
+ end.to_deep_struct
517
976
  else
518
977
  abort "#{input.inspect} not supported"
519
978
  end
@@ -722,9 +1181,9 @@ class Dorian
722
1181
  end
723
1182
  end
724
1183
 
725
- def lines(input)
1184
+ def lines(input, strip: :rstrip)
726
1185
  if input.is_a?(String)
727
- input.lines.map(&:rstrip)
1186
+ input.lines.map(&strip)
728
1187
  elsif deep?
729
1188
  deep_lines(input)
730
1189
  else
@@ -827,7 +1286,7 @@ class Dorian
827
1286
  end
828
1287
 
829
1288
  def match?(element, ruby: @ruby)
830
- !!evaluates(ruby:, it: element, stdout: false, returns: true).returned
1289
+ !!evaluates(ruby:, it: element, stdout: false, returns: true)
831
1290
  end
832
1291
 
833
1292
  def token(file)
@@ -934,77 +1393,65 @@ class Dorian
934
1393
  Tiktoken.encoding_for_model("gpt-4o")
935
1394
  end
936
1395
 
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
-
1396
+ def match_filetypes?(path, filetypes: arguments)
1397
+ return true if filetypes.none?
1398
+ return true unless filetypes.intersect?(%w[rb ruby])
999
1399
  return false if Dir.exist?(path)
1000
- return true if ruby_filenames.include?(path)
1001
- return true if ruby_extensions.include?(File.extname(path))
1400
+ return true if RUBY_FILENAMES.include?(path)
1401
+ return true if RUBY_EXTENSIONS.include?(File.extname(path))
1002
1402
  return false unless File.exist?(path)
1003
1403
 
1004
- first_line =
1005
- File.open(path, &:gets).to_s.encode("UTF-8", invalid: :replace)
1404
+ first_line = File.open(path, &:gets).to_s
1405
+ first_line = first_line.encode("UTF-8", invalid: :replace)
1006
1406
 
1007
- /\A#!.*ruby\z/.match?(first_line)
1407
+ return true if /\A#!.*ruby\z/.match?(first_line)
1408
+
1409
+ false
1410
+ end
1411
+
1412
+ def pluck(element)
1413
+ element = element.from_deep_struct
1414
+
1415
+ keys =
1416
+ if arguments.any?
1417
+ arguments
1418
+ elsif element.is_a?(Hash)
1419
+ element.keys
1420
+ elsif element.is_a?(Array)
1421
+ (0...(element.size)).map(&:to_s)
1422
+ else
1423
+ "it"
1424
+ end
1425
+
1426
+ results =
1427
+ keys.map do |argument|
1428
+ if element.is_a?(Array) && argument.to_i.to_s == argument
1429
+ element[argument.to_i]
1430
+ elsif element.is_a?(Hash) && element.key?(argument)
1431
+ { argument => element[argument] }
1432
+ else
1433
+ evaluates(ruby: argument, it: element.to_deep_struct)
1434
+ end
1435
+ end
1436
+
1437
+ if results.all?(Hash)
1438
+ results.inject(&:merge).to_deep_struct
1439
+ else
1440
+ results
1441
+ .map { |result| result.is_a?(Hash) ? result.values.first : result }
1442
+ .to_deep_struct
1443
+ end
1444
+ end
1445
+
1446
+ def table(data)
1447
+ is_hashes = data.first.from_deep_struct.is_a?(Hash)
1448
+ headings = is_hashes ? data.first.to_h.keys : nil
1449
+ rows = is_hashes ? data.map(&:values) : data.map { |row| wrap(row) }
1450
+ if headings
1451
+ puts Terminal::Table.new(headings:, rows:)
1452
+ else
1453
+ puts Terminal::Table.new(rows:)
1454
+ end
1008
1455
  end
1009
1456
 
1010
1457
  def evaluates(
@@ -1028,7 +1475,218 @@ class Dorian
1028
1475
  rails:,
1029
1476
  fast:,
1030
1477
  returns:
1031
- )
1478
+ ).returned
1479
+ end
1480
+
1481
+ def sort(object)
1482
+ object = object.from_deep_struct
1483
+
1484
+ if object.is_a?(Hash)
1485
+ object
1486
+ .to_a
1487
+ .sort_by(&:first)
1488
+ .to_h
1489
+ .transform_values { |value| sort(value) }
1490
+ elsif object.is_a?(Array)
1491
+ object.map { |element| sort(element) }
1492
+ else
1493
+ object
1494
+ end
1495
+ end
1496
+
1497
+ def filetype(path)
1498
+ ext = File.extname(path).to_s.downcase
1499
+ return :directory if Dir.exist?(path)
1500
+ return :symlink if File.symlink?(path)
1501
+ return :ruby if RUBY_FILENAMES.include?(File.basename(path))
1502
+ return :ruby if RUBY_EXTENSIONS.include?(ext)
1503
+ return :json if ext == ".json"
1504
+ return :jsonl if ext == ".jsonl"
1505
+ return :yaml if ext == ".yaml"
1506
+ return :yaml if ext == ".yml"
1507
+ return :yamll if ext == ".yamll"
1508
+ return :yamll if ext == ".ymll"
1509
+ return :csv if ext == ".csv"
1510
+ return :js if ext == ".js"
1511
+ return :js if ext == ".mjs"
1512
+ return :js if ext == ".cjs"
1513
+ return :ts if ext == ".ts"
1514
+ return :css if ext == ".css"
1515
+ return :html if ext == ".html"
1516
+ return :html if ext == ".htm"
1517
+ return :haml if ext == ".haml"
1518
+ return :slim if ext == ".slim"
1519
+ return :erb if ext == ".erb"
1520
+ return :fish if ext == ".fish"
1521
+ return :sql if ext == ".sql"
1522
+ return :tex if ext == ".tex"
1523
+ return :md if ext == ".md"
1524
+ return :md if ext == ".markdown"
1525
+ return :png if ext == ".png"
1526
+ return :jpeg if ext == ".jpg"
1527
+ return :jpeg if ext == ".jpeg"
1528
+ return :ico if ext == ".ico"
1529
+ return :webp if ext == ".webp"
1530
+ return :heic if ext == ".heic"
1531
+ return :pdf if ext == ".pdf"
1532
+ return :raw if ext == ".raw"
1533
+ return :env if path == ".env"
1534
+ return :env if path.start_with?(".env.")
1535
+ return :sh if path == "Dockerfile"
1536
+ return :sh if ext == ".sh"
1537
+ return :enc if ext == ".enc"
1538
+ return :enc if ext == ".keystore"
1539
+ return :pro if ext == ".pro"
1540
+ return :txt if ext == ".txt"
1541
+ return :bat if ext == ".bat"
1542
+ return :xcconfig if ext == ".xcconfig"
1543
+ return :pbxproj if ext == ".pbxproj"
1544
+ return :xml if ext == ".xml"
1545
+ return :xml if ext == ".plist"
1546
+ return :xml if ext == ".storyboard"
1547
+ return :xml if ext == ".xcscheme"
1548
+ return :xml if ext == ".xcworkspacedata"
1549
+ return :xml if ext == ".xcprivacy"
1550
+ return :xml if ext == ".entitlements"
1551
+ return :kotlin if ext == ".kt"
1552
+ return :groovy if ext == ".gradle"
1553
+ return :groovy if ext == ".properties"
1554
+ return :binary if ext == ".jar"
1555
+ return :objectivec if ext == ".h"
1556
+ return :objectivec if ext == ".mm"
1557
+ return :objectivec if ext == ".m"
1558
+ return unless File.exist?(path)
1559
+
1560
+ first_line = File.open(path, &:gets).to_s
1561
+ first_line = first_line.encode("UTF-8", invalid: :replace).strip
1562
+ return :ruby if first_line == "#!/usr/bin/env ruby"
1563
+ return :sh if first_line == "#!/bin/bash"
1564
+ return :sh if first_line == "#!/bin/sh"
1565
+ return :sh if first_line == "#!/bin/bash -e"
1566
+ return :sh if first_line == "#!/usr/bin/env sh"
1567
+
1568
+ nil
1569
+ end
1570
+
1571
+ def format(path, context:)
1572
+ return if File.symlink?(path)
1573
+ return if File.directory?(path)
1574
+ return unless File.exist?(path)
1575
+
1576
+ before = File.read(path)
1577
+
1578
+ case filetype(path)
1579
+ when :directory
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 = before.lines.map { |line| JSON.parse(line).to_json }.join("\n")
1592
+ when :csv
1593
+ after =
1594
+ CSV.generate { |csv| CSV.parse(before).each { |row| csv << row } }
1595
+ when :yaml
1596
+ after = sort(YAML.safe_load(before)).to_yaml
1597
+ when :yamll
1598
+ after =
1599
+ before
1600
+ .lines
1601
+ .map do |line|
1602
+ sort(YAML.safe_load(JSON.parse(line))).to_yaml.to_json
1603
+ end
1604
+ .join("\n") + "\n"
1605
+ when :js
1606
+ context.eval("format(#{path.to_json}, 'babel')")
1607
+ when :ts
1608
+ context.eval("format(#{path.to_json}, 'typescript')")
1609
+ when :html
1610
+ context.eval("format(#{path.to_json}, 'html')")
1611
+ when :md
1612
+ context.eval("format(#{path.to_json}, 'markdown')")
1613
+ when :sql
1614
+ context.eval("format(#{path.to_json}, 'sql')")
1615
+ when :groovy
1616
+ context.eval("format(#{path.to_json}, 'groovy')")
1617
+ when :css
1618
+ context.eval("format(#{path.to_json}, 'css')")
1619
+ when :sh
1620
+ if system("command -v shfmt > /dev/null 2>&1")
1621
+ command = ["shfmt", "--indent", "4", path].shelljoin
1622
+ stdout, stderr, status = Open3.capture3(command)
1623
+ raise stderr unless stderr.empty? && status.success?
1624
+
1625
+ after = stdout
1626
+ else
1627
+ warn "run: `brew install shfmt` for #{path}"
1628
+ end
1629
+ when :pdf
1630
+ doc = HexaPDF::Document.open(path)
1631
+ doc.trailer.info.each_key { |key| doc.trailer.info.delete(key) }
1632
+ doc.write(path, update_fields: false)
1633
+ after = File.read(path)
1634
+ when :tex
1635
+ if system("command -v latexindent > /dev/null 2>&1")
1636
+ command = ["latexindent", path, "--logfile", "/dev/null"].shelljoin
1637
+ stdout, stderr, status = Open3.capture3(command)
1638
+ raise stderr unless stderr.empty? && status.success?
1639
+
1640
+ after = stdout.gsub("\t", " ")
1641
+ else
1642
+ warn "run: `brew install latexindent` for #{path}"
1643
+ end
1644
+ when :objectivec
1645
+ if system("command -v clang-format > /dev/null 2>&1")
1646
+ command = ["clang-format", path].shelljoin
1647
+ stdout, stderr, status = Open3.capture3(command)
1648
+ raise stderr unless stderr.empty? && status.success?
1649
+
1650
+ after = stdout.gsub("\t", " ")
1651
+ else
1652
+ warn "run: `brew install clang-format` for #{path}"
1653
+ end
1654
+ when :kotlin
1655
+ if system("command -v ktlint > /dev/null 2>&1")
1656
+ command = ["ktlint", "-F", path].shelljoin
1657
+ stdout, stderr, status = Open3.capture3(command)
1658
+ raise stderr unless stderr.empty? && status.success?
1659
+
1660
+ after = File.read(path)
1661
+ else
1662
+ warn "run: `brew install ktlint` for #{path}"
1663
+ end
1664
+ when :raw, :directory, :symlink, :env, :enc, :txt, :pro, :binary, :slim,
1665
+ :fish, :bat, :xcconfig, :pbxproj, :jpeg, :png, :webp, :heic, :ico
1666
+ # nothing to do
1667
+ else
1668
+ case File.basename(path)
1669
+ when ".gitignore", ".node-version", ".prettierignore", ".ruby-version",
1670
+ ".tool-versions", "Gemfile.lock", "LICENSE", "VERSION", ".rspec",
1671
+ "Procfile", "Procfile.dev", "Podfile.lock", ".xcode.env", "CNAME",
1672
+ "TODO", ".gitmodules", ".asdfrc", "config", ".dotignore", ".gemrc",
1673
+ ".gitconfig", ".gitmessage", ".hushlogin", ".psqlrc", ".vimrc",
1674
+ "DIRECTORIES"
1675
+ # nothing to do
1676
+ when ".keep"
1677
+ File.write(path, "")
1678
+ else
1679
+ puts "unhandled: #{path}"
1680
+ end
1681
+ end
1682
+
1683
+ if after && before != after
1684
+ puts path
1685
+ File.write(path, after)
1686
+ end
1687
+ rescue StandardError => e
1688
+ warn "failed to parse #{path}: #{e.message}"
1689
+ binding.irb
1032
1690
  end
1033
1691
  end
1034
1692
  end