cr.rb 3.19.0 → 3.20.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/bin/cr +725 -727
  3. data/lib/cr.rb +13 -13
  4. metadata +6 -6
data/bin/cr CHANGED
@@ -1,727 +1,725 @@
1
- #!/usr/bin/env ruby
2
- # coding: utf-8
3
- # ------------------------------------------------------
4
- # File : cr.rb
5
- # Authors : ccmywish <ccmywish@qq.com>
6
- # Created on : <2021-07-08>
7
- # Last modified : <2022-04-15>
8
- #
9
- # cr:
10
- #
11
- # This file is used to explain a CRyptic command
12
- # or an acronym's real meaning in computer world or
13
- # other fields.
14
- #
15
- # ------------------------------------------------------
16
-
17
- require 'cr'
18
- require 'tomlrb'
19
- require 'fileutils'
20
-
21
- CRYPTIC_RESOLVER_HOME = File.expand_path("~/.cryptic-resolver")
22
- CRYPTIC_DEFAULT_DICTS = {
23
- computer: "https://github.com/cryptic-resolver/cryptic_computer.git",
24
- common: "https://github.com/cryptic-resolver/cryptic_common.git",
25
- science: "https://github.com/cryptic-resolver/cryptic_science.git",
26
- economy: "https://github.com/cryptic-resolver/cryptic_economy.git",
27
- medicine: "https://github.com/cryptic-resolver/cryptic_medicine.git"
28
- }
29
-
30
-
31
- ####################
32
- # helper: for color
33
- ####################
34
-
35
- def bold(str) "\e[1m#{str}\e[0m" end
36
- def underline(str) "\e[4m#{str}\e[0m" end
37
- def red(str) "\e[31m#{str}\e[0m" end
38
- def green(str) "\e[32m#{str}\e[0m" end
39
- def yellow(str) "\e[33m#{str}\e[0m" end
40
- def blue(str) "\e[34m#{str}\e[0m" end
41
- def purple(str) "\e[35m#{str}\e[0m" end
42
- def cyan(str) "\e[36m#{str}\e[0m" end
43
-
44
-
45
- ####################
46
- # core: logic
47
- ####################
48
-
49
- def is_there_any_dict?
50
- unless Dir.exist? CRYPTIC_RESOLVER_HOME
51
- Dir.mkdir CRYPTIC_RESOLVER_HOME
52
- end
53
-
54
- !Dir.empty? CRYPTIC_RESOLVER_HOME
55
- end
56
-
57
-
58
- def add_default_dicts_if_none_exists
59
- unless is_there_any_dict?
60
- puts "cr: Adding default dictionaries..."
61
-
62
- begin
63
- if RUBY_PLATFORM.include? "mingw"
64
- # Windows doesn't have fork
65
- CRYPTIC_DEFAULT_DICTS.each do |key, dict|
66
- puts "cr: Pulling cryptic_#{key}..."
67
- `git -C #{CRYPTIC_RESOLVER_HOME} clone #{dict} -q`
68
- end
69
- else
70
- # *nix
71
- CRYPTIC_DEFAULT_DICTS.each do |key, dict|
72
- fork do
73
- puts "cr: Pulling cryptic_#{key}..."
74
- `git -C #{CRYPTIC_RESOLVER_HOME} clone #{dict} -q`
75
- end
76
- end
77
- Process.waitall
78
- end
79
-
80
- rescue Interrupt
81
- puts "cr: Cancel add default dicts"
82
- exit 1
83
- end
84
-
85
- puts "cr: Add done"
86
- word_count(p: false)
87
- puts
88
- puts "#{$WordCount} words added"
89
-
90
- # Really added
91
- return true
92
- end
93
- # Not added
94
- return false
95
- end
96
-
97
-
98
- def update_dicts()
99
- return if add_default_dicts_if_none_exists
100
-
101
- word_count(p: false)
102
- old_wc = [$DefaultWordCount, $WordCount-$DefaultWordCount, $WordCount]
103
-
104
- puts "cr: Updating all dictionaries..."
105
-
106
- begin
107
- Dir.chdir CRYPTIC_RESOLVER_HOME do
108
-
109
- if RUBY_PLATFORM.include? "mingw"
110
- # Windows doesn't have fork
111
- Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
112
- puts "cr: Wait to update #{dict}..."
113
- `git -C ./#{dict} pull -q`
114
- end
115
- else
116
- # *nix
117
- Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
118
- fork do
119
- puts "cr: Wait to update #{dict}..."
120
- `git -C ./#{dict} pull -q`
121
- end
122
- end
123
- Process.waitall
124
-
125
- end # end if/else
126
- end
127
-
128
- rescue Interrupt
129
- puts "cr: Cancel update"
130
- exit 1
131
- end
132
-
133
-
134
- puts "cr: Update done"
135
-
136
- # clear
137
- $DefaultWordCount, $WordCount = 0, 0
138
- # recount
139
- word_count(p: false)
140
- new_wc = [$DefaultWordCount, $WordCount-$DefaultWordCount, $WordCount]
141
- diff = []
142
- new_wc.each_with_index do
143
- diff[_2] = _1 - old_wc[_2]
144
- end
145
-
146
- puts
147
- puts "#{diff.[]2} words added: default/#{diff.[]0} user/#{diff.[]1}"
148
-
149
- end
150
-
151
-
152
- def add_dict(repo)
153
- if repo.nil?
154
- puts bold(red("cr: Need an argument!"))
155
- exit -1
156
- end
157
-
158
- # Ensure the cr home dir exists
159
- FileUtils.mkdir_p(CRYPTIC_RESOLVER_HOME)
160
-
161
- # Simplify adding dictionary
162
- if !repo.start_with?("https://") and !repo.start_with?("git@")
163
- repo = "https://github.com/#{repo}.git"
164
- end
165
-
166
- begin
167
- puts "cr: Adding new dictionary..."
168
- `git -C #{CRYPTIC_RESOLVER_HOME} clone #{repo} -q`
169
- rescue Interrupt
170
- puts "cr: Cancel add dict"
171
- exit 1
172
- end
173
-
174
- puts "cr: Add new dictionary done"
175
-
176
- # github/com/ccmywish/ruby_knowledge(.git)
177
- dict = repo.split('/')[-1].delete_suffix('.git')
178
- count_dict_words(dict)
179
- puts
180
- puts "#$WordCount words added"
181
-
182
- end
183
-
184
-
185
- def del_dict(repo)
186
- if repo.nil?
187
- puts bold(red("cr: Need an argument!"))
188
- exit -1
189
- end
190
- Dir.chdir CRYPTIC_RESOLVER_HOME do
191
- begin
192
- # Dir.rmdir repo # Can't rm a filled dir
193
- # FileUtils.rmdir repo # Can't rm a filled dir
194
- FileUtils.rm_rf repo
195
- puts "cr: Delete dictionary #{bold(green(repo))} done"
196
- rescue Exception => e
197
- puts bold(red("cr: #{e}"))
198
- list_dictionaries
199
- end
200
- end
201
- end
202
-
203
-
204
- def load_sheet(dict, sheet_name)
205
- file = CRYPTIC_RESOLVER_HOME + "/#{dict}/#{sheet_name}.toml"
206
-
207
- if File.exist? file
208
- return Tomlrb.load_file file # gem 'tomlrb'
209
- # return TOML.load_file file # gem 'toml'
210
- else
211
- nil
212
- end
213
- end
214
-
215
-
216
- #
217
- # Pretty print the info of the given word
218
- #
219
- # A info looks like this
220
- # emacs = {
221
- # name = "Emacs"
222
- # desc = "edit macros"
223
- # more = "a feature-rich editor"
224
- # see = ["Vim"]
225
- # }
226
- #
227
- # @param info [Hash] the information of the given word (mapped to a keyword in TOML)
228
- #
229
- def pp_info(info)
230
- name = info['name'] || red("No name!") # keyword `or` is invalid here in Ruby
231
-
232
- desc = info['desc']
233
- more = info['more']
234
-
235
- if desc
236
- puts "\n #{name}: #{desc}"
237
- print "\n ",more,"\n" if more
238
- else
239
- puts "\n #{name}"
240
- print "\n ",more,"\n" if more
241
- end
242
-
243
- if see_also = info['see']
244
- print "\n", purple("SEE ALSO ")
245
- if see_also.is_a?(Array)
246
- see_also.each {|x| print underline(x),' '}
247
- else
248
- print underline(see_also),' '
249
- end
250
- puts
251
- end
252
- puts
253
- end
254
-
255
- # Print default cryptic_ dictionaries
256
- def pp_dict(dict)
257
- puts green("From: #{dict}")
258
- end
259
-
260
-
261
- #
262
- # Used for synonym jump
263
- # Because we absolutely jump to a must-have word
264
- # So we can directly lookup to it
265
- #
266
- # Notice that, we must jump to a specific word definition
267
- # So in the toml file, you must specify the precise word.
268
- # If it has multiple meanings, for example
269
- #
270
- # [blah]
271
- # same = "xdg" # this is wrong, because xdg has multiple
272
- # # definitions, and all of them specify a
273
- # # category
274
- #
275
- # [blah]
276
- # same = "XDG downloader <=>xdg.Download" # this is correct
277
- #
278
- # [blah]
279
- # name = "BlaH" # You may want to display a better name first
280
- # same = "XDG downloader <=>xdg.Download" # this is correct
281
- #
282
- #
283
- def pp_same_info(dict, word, cache_content, same_key, own_name)
284
-
285
- # If it's a synonym for anther word,
286
- # we should lookup into this dict again, but maybe with a different file
287
-
288
- # file name
289
- x = word.chr.downcase
290
-
291
- #
292
- # dictionary maintainer must obey the rule for xxx.yyy word:
293
- # xxx should be lower case
294
- # yyy can be any case
295
- #
296
- # Because yyy should clearly explain the category info, IBM is better than ibm
297
- # Implementation should not be too simple if we want to stress the function we
298
- # expect.
299
- #
300
- # 'jump to' will output to user, so this is important not only inside our sheet.
301
- #
302
- # same = "XDM downloader<=>xdm.Download"
303
- #
304
- # We split 'same' key into two parts via spaceship symbol <=>, first part will
305
- # output to user, the second part is for internal jump.
306
- #
307
-
308
- jump_to, same = same_key.split("<=>")
309
- same = jump_to if same.nil?
310
-
311
- unless own_name
312
- own_name = word
313
- end
314
- puts blue(bold(own_name)) + ' redirects to ' + blue(bold(jump_to))
315
-
316
- #
317
- # As '.' is used to delimit a word and a category, what if
318
- # we jump to a dotted word?
319
- #
320
- # [eg]
321
- # same = "e.g." # this must lead to a wrong resolution to
322
- # # word 'e', category 'g'
323
- #
324
- # All you need is to be like this:
325
- #
326
- # [eg]
327
- # same = "'e.g.'" # cr will notice the single quote
328
- #
329
-
330
- if same =~ /^'(.*)'$/
331
- same, category = $1, nil
332
- else
333
- same, category = same.split('.')
334
- end
335
-
336
- if same.chr == x
337
- # No need to load another dictionary if match
338
- sheet_content = cache_content
339
- else
340
- sheet_content = load_sheet(dict, same.chr.downcase)
341
- end
342
-
343
- if category.nil?
344
- info = sheet_content[same]
345
- else
346
- info = sheet_content[same][category]
347
- end
348
-
349
- if info.nil?
350
- puts red("Warn: Synonym jumps to the wrong place `#{same}`,
351
- Please consider fixing this in `#{x}.toml` of the dictionary `#{dict}`")
352
- # exit
353
- return false
354
- # double or more jumps
355
- elsif same_key = info['same']
356
- own_name = info['name']
357
- return pp_same_info(dict, same, cache_content, same_key, own_name)
358
- else
359
- pp_info(info)
360
- return true
361
- end
362
- end
363
-
364
-
365
-
366
- #
367
- # Lookup the given word in a sheet (a toml file) and also print.
368
- # The core idea is that:
369
- #
370
- # 1. if the word is `same` with another synonym, it will directly jump to
371
- # a word in this dictionary, but maybe a different sheet.
372
- #
373
- # 2. load the toml file and check the given word
374
- # 2.1 with no category specifier
375
- # [abcd]
376
- # 2.2 with category specifier
377
- # [abcd.tYPe]
378
- #
379
- def lookup(dict, file, word)
380
- sheet_content = load_sheet(dict, file)
381
- return false if sheet_content.nil?
382
-
383
- info = sheet_content[word]
384
- return false if info.nil?
385
-
386
- # Warn if the info is empty. For example:
387
- # emacs = { }
388
- if info.size == 0
389
- puts red("WARN: Lack of everything of the given word
390
- Please consider fixing this in the dict `#{dict}`")
391
- exit
392
- end
393
-
394
-
395
-
396
- # Word with no category specifier
397
- # We call this meaning as type 1
398
- type_1_exist_flag = false
399
-
400
- # if already jump, don't check the word itself
401
- is_jump = false
402
-
403
- # synonym info print
404
- if same_key = info['same']
405
- own_name = info['name']
406
- pp_dict(dict)
407
- pp_same_info(dict, word, sheet_content, same_key, own_name)
408
- # It's also a type 1
409
- type_1_exist_flag = true
410
- is_jump = true
411
- end
412
-
413
- # normal info print
414
- # To developer:
415
- # The word should at least has one of `desc` and `more`
416
- # But when none exists, this may not be considered wrong,
417
- # Because the type2 make the case too.
418
- #
419
- # So, just ignore it, even if it's really a mistake(insignificant)
420
- # by dictionary maintainers.
421
- #
422
- if !is_jump && (info.has_key?('desc') || info.has_key?('more'))
423
- pp_dict(dict)
424
- pp_info(info)
425
- type_1_exist_flag = true
426
- end
427
-
428
- # Meanings with category specifier
429
- # We call this meaning as type 2
430
- categories = info.keys - ["name", "desc", "more", "same", "see"]
431
-
432
- if !categories.empty?
433
-
434
- if type_1_exist_flag
435
- print blue(bold("OR")),"\n"
436
- else
437
- pp_dict(dict)
438
- end
439
-
440
- categories.each do |meaning|
441
- info0 = sheet_content[word][meaning]
442
- if same_key = info0['same']
443
- own_name = info0['name']
444
- pp_same_info(dict, word, sheet_content, same_key, own_name)
445
- else
446
- pp_info(info0)
447
- end
448
-
449
- # last meaning doesn't show this separate line
450
- print blue(bold("OR")),"\n" unless categories.last == meaning
451
- end
452
- return true
453
- elsif type_1_exist_flag
454
- return true
455
- else
456
- return false
457
- end
458
- end
459
-
460
-
461
- #
462
- # The main procedure of `cr`
463
- # 1. Search the default's first dict first
464
- # 2. Search the rest dictionaries in the cryptic dictionaries default dir
465
- #
466
- # The `search` is done via the `lookup` function. It will print
467
- # the info while finding. If `lookup` always return false then
468
- # means lacking of this word in our dictionaries. So a welcomed
469
- # contribution is printed on the screen.
470
- #
471
- def solve_word(word)
472
-
473
- add_default_dicts_if_none_exists
474
-
475
- word = word.downcase # downcase! would lead to frozen error in Ruby 2.7.2
476
- # The index is the toml file we'll look into
477
- index = word.chr
478
- case index
479
- when '0'..'9'
480
- index = '0123456789'
481
- end
482
-
483
- # Default's first should be 1st to consider
484
- first_dict = "cryptic_" + CRYPTIC_DEFAULT_DICTS.keys[0].to_s # When Ruby3, We can use DICTS.key(0)
485
-
486
- # cache lookup results
487
- results = []
488
- results << lookup(first_dict,index,word)
489
- # return if result == true # We should consider all dicts
490
-
491
- # Then else
492
- rest = Dir.children(CRYPTIC_RESOLVER_HOME)
493
- rest.delete first_dict
494
- rest.each do |dict|
495
- results << lookup(dict,index,word)
496
- # continue if result == false # We should consider all dicts
497
- end
498
-
499
- unless results.include? true
500
- puts <<-NotFound
501
- cr: Not found anything.
502
-
503
- You may use `cr -u` to update all dictionaries.
504
- Or you could contribute to:
505
-
506
- 1. computer: #{CRYPTIC_DEFAULT_DICTS[:computer]}
507
- 2. common: #{CRYPTIC_DEFAULT_DICTS[:common]}
508
- 3. science: #{CRYPTIC_DEFAULT_DICTS[:science]}
509
- 4. economy: #{CRYPTIC_DEFAULT_DICTS[:economy]}
510
- 5. medicine: #{CRYPTIC_DEFAULT_DICTS[:medicine]}
511
-
512
- NotFound
513
-
514
- else
515
- return
516
- end
517
-
518
- end
519
-
520
-
521
-
522
- #
523
- # The search word process is quite like `solve_word``
524
- # Notice:
525
- # We handle two cases
526
- #
527
- # 1. the 'pattern' is the regexp itself
528
- # 2. the 'pattern' is like '/blahblah/'
529
- #
530
- # The second is what Ruby and Perl users like to do, handle it!
531
- #
532
- def search_word(pattern)
533
-
534
- if pattern.nil?
535
- puts bold(red("cr: Need an argument!"))
536
- exit -1
537
- end
538
-
539
- add_default_dicts_if_none_exists
540
-
541
- if pattern =~ /^\/(.*)\/$/
542
- regexp = %r[#$1]
543
- else
544
- regexp = %r[#{pattern}]
545
- end
546
-
547
- found = false
548
-
549
- #
550
- # Try to match every word in all dictionaries
551
- #
552
- Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
553
- sheets = Dir.children(File.join(CRYPTIC_RESOLVER_HOME, dict)).select do
554
- _1.end_with?('.toml')
555
- end
556
-
557
- similar_words_in_a_dict = []
558
-
559
- sheets.each do |sheet|
560
- sheet_content = load_sheet(dict, File.basename(sheet,'.toml'))
561
-
562
- sheet_content.keys.each do
563
- if _1 =~ regexp
564
- found = true
565
- similar_words_in_a_dict << _1
566
- end
567
- end
568
- # end of each sheet in a dict
569
- end
570
-
571
- unless similar_words_in_a_dict.empty?
572
- pp_dict(dict)
573
- require 'ls_table'
574
- LsTable.ls(similar_words_in_a_dict) do |e|
575
- puts blue(e)
576
- end
577
- puts
578
- end
579
- end
580
- if !found
581
- puts red("cr: No words match with #{regexp.inspect}")
582
- puts
583
- end
584
- end
585
-
586
-
587
- def help
588
- word_count(p: false)
589
- user_words = $WordCount - $DefaultWordCount
590
- puts <<-HELP
591
- cr: Cryptic Resolver v#{CR_GEM_VERSION} (#{$WordCount} words: default/#{$DefaultWordCount} user/#{user_words})
592
-
593
- usage:
594
-
595
- cr emacs => Edit macros: a feature-rich editor
596
- cr -c => Print word count
597
- cr -l => List local dictionaries
598
- cr -u => Update all dictionaries
599
- cr -a repo.git => Add a new dictionary
600
- cr -a user/repo => Add a new dictionary on Github
601
- cr -d cryptic_xx => Delete a dictionary
602
- cr -s pattern => Search words matched with pattern
603
- cr -v => Print version
604
- cr -h => Print this help
605
-
606
- HELP
607
-
608
- add_default_dicts_if_none_exists
609
-
610
- end
611
-
612
-
613
- def print_version
614
- puts "cr: Cryptic Resolver version #{CR_GEM_VERSION} in Ruby "
615
- end
616
-
617
-
618
- def list_dictionaries
619
- Dir.chdir CRYPTIC_RESOLVER_HOME do
620
- Dir.children(CRYPTIC_RESOLVER_HOME).each_with_index do |dict,i|
621
- puts "#{blue(i+1)}. #{bold(green(dict))}"
622
- end
623
- end
624
- end
625
-
626
-
627
- # All dictionaries word count
628
- $WordCount = 0
629
- # Default dictionaries word count
630
- $DefaultWordCount = 0
631
-
632
- def count_dict_words(dict)
633
-
634
- dict_dir = CRYPTIC_RESOLVER_HOME + "/#{dict}"
635
-
636
- wc = 0
637
-
638
- Dir.children(dict_dir).each do |entry|
639
- next unless entry.end_with?('.toml')
640
- sheet_content = load_sheet(dict, entry.delete_suffix('.toml'))
641
- count = sheet_content.keys.count
642
-
643
- # puts "#{entry}: #{count}"
644
- wc = wc + count
645
- end
646
-
647
- $WordCount += wc
648
-
649
- return wc
650
-
651
- end
652
-
653
-
654
- def word_count(p:)
655
-
656
- # Always check before Dir.children (this method creates dir if not exists)
657
- is_there_any_dict?
658
-
659
- # real dicts in user's directory
660
- locals = []
661
- Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
662
- locals << dict
663
- end
664
-
665
- # pre-defined default
666
- defaults = CRYPTIC_DEFAULT_DICTS.keys.map do |s|
667
- "cryptic_#{s}"
668
- end
669
-
670
- # user may delete some default dicts
671
- defaults &= locals
672
-
673
- unless defaults.empty?
674
- puts(bold(green("Default dict: "))) if p
675
- defaults.each do |s|
676
- wc = count_dict_words(s)
677
- $DefaultWordCount += wc
678
- # With color, ljust not works, so we disable color
679
- puts(" #{s.ljust(17)}: #{wc}") if p
680
- end
681
- end
682
-
683
- users = locals - defaults
684
- user_words = 0
685
- unless users.empty?
686
- wc = 0
687
- puts(bold(blue("\nUser's dict:"))) if p
688
- users.each do |s|
689
- wc = count_dict_words(s)
690
- # no need to add to $WordCount,
691
- # because it's done in `count_dict_words` func
692
- puts(" #{s.ljust(17)}: #{wc}") if p
693
- end
694
-
695
- user_words = $WordCount - $DefaultWordCount
696
- end
697
-
698
- if p
699
- puts
700
- puts "#{$DefaultWordCount.to_s.rjust(4)} words in default dictionaries"
701
- puts "#{user_words.to_s.rjust(4)} words in user's dictionaries"
702
- puts "#{$WordCount.to_s.rjust(4)} words altogether"
703
- end
704
- end
705
-
706
-
707
- ####################
708
- # main: CLI Handling
709
- ####################
710
- arg = ARGV.shift
711
-
712
- case arg
713
- when nil then help
714
- when '-v' then print_version
715
- when '-h' then help
716
- when '-l' then list_dictionaries
717
- when '-c' then word_count(p: true)
718
- when '-u' then update_dicts
719
- when '-s' then search_word ARGV.shift
720
- when '-a' then add_dict ARGV.shift
721
- when '-d' then del_dict ARGV.shift
722
- when '--help'
723
- help
724
- else
725
- solve_word arg
726
- end
727
-
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+ # ------------------------------------------------------
4
+ # File : cr.rb
5
+ # Authors : ccmywish <ccmywish@qq.com>
6
+ # Created on : <2021-07-08>
7
+ # Last modified : <2022-10-20>
8
+ #
9
+ # cr:
10
+ #
11
+ # This file is used to explain a CRyptic command
12
+ # or an acronym's real meaning in computer world or
13
+ # other fields.
14
+ #
15
+ # ------------------------------------------------------
16
+
17
+ require 'cr'
18
+ require 'tomlrb'
19
+ require 'fileutils'
20
+
21
+ CRYPTIC_RESOLVER_HOME = File.expand_path("~/.cryptic-resolver")
22
+ CRYPTIC_DEFAULT_DICTS = {
23
+ common: "https://github.com/cryptic-resolver/cryptic_common.git",
24
+ computer: "https://github.com/cryptic-resolver/cryptic_computer.git",
25
+ windows: "https://github.com/cryptic-resolver/cryptic_windows.git",
26
+ electronics: "https://github.com/cryptic-resolver/cryptic_electronics.git"
27
+ }
28
+
29
+
30
+ ####################
31
+ # helper: for color
32
+ ####################
33
+
34
+ def bold(str) "\e[1m#{str}\e[0m" end
35
+ def underline(str) "\e[4m#{str}\e[0m" end
36
+ def red(str) "\e[31m#{str}\e[0m" end
37
+ def green(str) "\e[32m#{str}\e[0m" end
38
+ def yellow(str) "\e[33m#{str}\e[0m" end
39
+ def blue(str) "\e[34m#{str}\e[0m" end
40
+ def purple(str) "\e[35m#{str}\e[0m" end
41
+ def cyan(str) "\e[36m#{str}\e[0m" end
42
+
43
+
44
+ ####################
45
+ # core: logic
46
+ ####################
47
+
48
+ def is_there_any_dict?
49
+ unless Dir.exist? CRYPTIC_RESOLVER_HOME
50
+ Dir.mkdir CRYPTIC_RESOLVER_HOME
51
+ end
52
+
53
+ !Dir.empty? CRYPTIC_RESOLVER_HOME
54
+ end
55
+
56
+
57
+ def add_default_dicts_if_none_exists
58
+ unless is_there_any_dict?
59
+ puts "cr: Adding default dictionaries..."
60
+
61
+ begin
62
+ if RUBY_PLATFORM.include? "mingw"
63
+ # Windows doesn't have fork
64
+ CRYPTIC_DEFAULT_DICTS.each do |key, dict|
65
+ puts "cr: Pulling cryptic_#{key}..."
66
+ `git -C #{CRYPTIC_RESOLVER_HOME} clone #{dict} -q`
67
+ end
68
+ else
69
+ # *nix
70
+ CRYPTIC_DEFAULT_DICTS.each do |key, dict|
71
+ fork do
72
+ puts "cr: Pulling cryptic_#{key}..."
73
+ `git -C #{CRYPTIC_RESOLVER_HOME} clone #{dict} -q`
74
+ end
75
+ end
76
+ Process.waitall
77
+ end
78
+
79
+ rescue Interrupt
80
+ puts "cr: Cancel add default dicts"
81
+ exit 1
82
+ end
83
+
84
+ puts "cr: Add done"
85
+ word_count(p: false)
86
+ puts
87
+ puts "#{$WordCount} words added"
88
+
89
+ # Really added
90
+ return true
91
+ end
92
+ # Not added
93
+ return false
94
+ end
95
+
96
+
97
+ def update_dicts()
98
+ return if add_default_dicts_if_none_exists
99
+
100
+ word_count(p: false)
101
+ old_wc = [$DefaultWordCount, $WordCount-$DefaultWordCount, $WordCount]
102
+
103
+ puts "cr: Updating all dictionaries..."
104
+
105
+ begin
106
+ Dir.chdir CRYPTIC_RESOLVER_HOME do
107
+
108
+ if RUBY_PLATFORM.include? "mingw"
109
+ # Windows doesn't have fork
110
+ Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
111
+ puts "cr: Wait to update #{dict}..."
112
+ `git -C ./#{dict} pull -q`
113
+ end
114
+ else
115
+ # *nix
116
+ Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
117
+ fork do
118
+ puts "cr: Wait to update #{dict}..."
119
+ `git -C ./#{dict} pull -q`
120
+ end
121
+ end
122
+ Process.waitall
123
+
124
+ end # end if/else
125
+ end
126
+
127
+ rescue Interrupt
128
+ puts "cr: Cancel update"
129
+ exit 1
130
+ end
131
+
132
+
133
+ puts "cr: Update done"
134
+
135
+ # clear
136
+ $DefaultWordCount, $WordCount = 0, 0
137
+ # recount
138
+ word_count(p: false)
139
+ new_wc = [$DefaultWordCount, $WordCount-$DefaultWordCount, $WordCount]
140
+ diff = []
141
+ new_wc.each_with_index do
142
+ diff[_2] = _1 - old_wc[_2]
143
+ end
144
+
145
+ puts
146
+ puts "#{diff.[]2} words added: default/#{diff.[]0} user/#{diff.[]1}"
147
+
148
+ end
149
+
150
+
151
+ def add_dict(repo)
152
+ if repo.nil?
153
+ puts bold(red("cr: Need an argument!"))
154
+ exit -1
155
+ end
156
+
157
+ # Ensure the cr home dir exists
158
+ FileUtils.mkdir_p(CRYPTIC_RESOLVER_HOME)
159
+
160
+ # Simplify adding dictionary
161
+ if !repo.start_with?("https://") and !repo.start_with?("git@")
162
+ repo = "https://github.com/#{repo}.git"
163
+ end
164
+
165
+ begin
166
+ puts "cr: Adding new dictionary..."
167
+ `git -C #{CRYPTIC_RESOLVER_HOME} clone #{repo} -q`
168
+ rescue Interrupt
169
+ puts "cr: Cancel add dict"
170
+ exit 1
171
+ end
172
+
173
+ puts "cr: Add new dictionary done"
174
+
175
+ # github/com/ccmywish/ruby_knowledge(.git)
176
+ dict = repo.split('/')[-1].delete_suffix('.git')
177
+ count_dict_words(dict)
178
+ puts
179
+ puts "#$WordCount words added"
180
+
181
+ end
182
+
183
+
184
+ def del_dict(repo)
185
+ if repo.nil?
186
+ puts bold(red("cr: Need an argument!"))
187
+ exit -1
188
+ end
189
+ Dir.chdir CRYPTIC_RESOLVER_HOME do
190
+ begin
191
+ # Dir.rmdir repo # Can't rm a filled dir
192
+ # FileUtils.rmdir repo # Can't rm a filled dir
193
+ FileUtils.rm_rf repo
194
+ puts "cr: Delete dictionary #{bold(green(repo))} done"
195
+ rescue Exception => e
196
+ puts bold(red("cr: #{e}"))
197
+ list_dictionaries
198
+ end
199
+ end
200
+ end
201
+
202
+
203
+ def load_sheet(dict, sheet_name)
204
+ file = CRYPTIC_RESOLVER_HOME + "/#{dict}/#{sheet_name}.toml"
205
+
206
+ if File.exist? file
207
+ return Tomlrb.load_file file # gem 'tomlrb'
208
+ # return TOML.load_file file # gem 'toml'
209
+ else
210
+ nil
211
+ end
212
+ end
213
+
214
+
215
+ #
216
+ # Pretty print the info of the given word
217
+ #
218
+ # A info looks like this
219
+ # emacs = {
220
+ # name = "Emacs"
221
+ # desc = "edit macros"
222
+ # more = "a feature-rich editor"
223
+ # see = ["Vim"]
224
+ # }
225
+ #
226
+ # @param info [Hash] the information of the given word (mapped to a keyword in TOML)
227
+ #
228
+ def pp_info(info)
229
+ name = info['name'] || red("No name!") # keyword `or` is invalid here in Ruby
230
+
231
+ desc = info['desc']
232
+ more = info['more']
233
+
234
+ if desc
235
+ puts "\n #{name}: #{desc}"
236
+ print "\n ",more,"\n" if more
237
+ else
238
+ puts "\n #{name}"
239
+ print "\n ",more,"\n" if more
240
+ end
241
+
242
+ if see_also = info['see']
243
+ print "\n", purple("SEE ALSO ")
244
+ if see_also.is_a?(Array)
245
+ see_also.each {|x| print underline(x),' '}
246
+ else
247
+ print underline(see_also),' '
248
+ end
249
+ puts
250
+ end
251
+ puts
252
+ end
253
+
254
+ # Print default cryptic_ dictionaries
255
+ def pp_dict(dict)
256
+ puts green("From: #{dict}")
257
+ end
258
+
259
+
260
+ #
261
+ # Used for synonym jump
262
+ # Because we absolutely jump to a must-have word
263
+ # So we can directly lookup to it
264
+ #
265
+ # Notice that, we must jump to a specific word definition
266
+ # So in the toml file, you must specify the precise word.
267
+ # If it has multiple meanings, for example
268
+ #
269
+ # [blah]
270
+ # same = "xdg" # this is wrong, because xdg has multiple
271
+ # # definitions, and all of them specify a
272
+ # # category
273
+ #
274
+ # [blah]
275
+ # same = "XDG downloader <=>xdg.Download" # this is correct
276
+ #
277
+ # [blah]
278
+ # name = "BlaH" # You may want to display a better name first
279
+ # same = "XDG downloader <=>xdg.Download" # this is correct
280
+ #
281
+ #
282
+ def pp_same_info(dict, word, cache_content, same_key, own_name)
283
+
284
+ # If it's a synonym for anther word,
285
+ # we should lookup into this dict again, but maybe with a different file
286
+
287
+ # file name
288
+ x = word.chr.downcase
289
+
290
+ #
291
+ # dictionary maintainer must obey the rule for xxx.yyy word:
292
+ # xxx should be lower case
293
+ # yyy can be any case
294
+ #
295
+ # Because yyy should clearly explain the category info, IBM is better than ibm
296
+ # Implementation should not be too simple if we want to stress the function we
297
+ # expect.
298
+ #
299
+ # 'jump to' will output to user, so this is important not only inside our sheet.
300
+ #
301
+ # same = "XDM downloader<=>xdm.Download"
302
+ #
303
+ # We split 'same' key into two parts via spaceship symbol <=>, first part will
304
+ # output to user, the second part is for internal jump.
305
+ #
306
+
307
+ jump_to, same = same_key.split("<=>")
308
+ same = jump_to if same.nil?
309
+
310
+ unless own_name
311
+ own_name = word
312
+ end
313
+ puts blue(bold(own_name)) + ' redirects to ' + blue(bold(jump_to))
314
+
315
+ #
316
+ # As '.' is used to delimit a word and a category, what if
317
+ # we jump to a dotted word?
318
+ #
319
+ # [eg]
320
+ # same = "e.g." # this must lead to a wrong resolution to
321
+ # # word 'e', category 'g'
322
+ #
323
+ # All you need is to be like this:
324
+ #
325
+ # [eg]
326
+ # same = "'e.g.'" # cr will notice the single quote
327
+ #
328
+
329
+ if same =~ /^'(.*)'$/
330
+ same, category = $1, nil
331
+ else
332
+ same, category = same.split('.')
333
+ end
334
+
335
+ if same.chr == x
336
+ # No need to load another dictionary if match
337
+ sheet_content = cache_content
338
+ else
339
+ sheet_content = load_sheet(dict, same.chr.downcase)
340
+ end
341
+
342
+ if category.nil?
343
+ info = sheet_content[same]
344
+ else
345
+ info = sheet_content[same][category]
346
+ end
347
+
348
+ if info.nil?
349
+ puts red("Warn: Synonym jumps to the wrong place `#{same}`,
350
+ Please consider fixing this in `#{x}.toml` of the dictionary `#{dict}`")
351
+ # exit
352
+ return false
353
+ # double or more jumps
354
+ elsif same_key = info['same']
355
+ own_name = info['name']
356
+ return pp_same_info(dict, same, cache_content, same_key, own_name)
357
+ else
358
+ pp_info(info)
359
+ return true
360
+ end
361
+ end
362
+
363
+
364
+
365
+ #
366
+ # Lookup the given word in a sheet (a toml file) and also print.
367
+ # The core idea is that:
368
+ #
369
+ # 1. if the word is `same` with another synonym, it will directly jump to
370
+ # a word in this dictionary, but maybe a different sheet.
371
+ #
372
+ # 2. load the toml file and check the given word
373
+ # 2.1 with no category specifier
374
+ # [abcd]
375
+ # 2.2 with category specifier
376
+ # [abcd.tYPe]
377
+ #
378
+ def lookup(dict, file, word)
379
+ sheet_content = load_sheet(dict, file)
380
+ return false if sheet_content.nil?
381
+
382
+ info = sheet_content[word]
383
+ return false if info.nil?
384
+
385
+ # Warn if the info is empty. For example:
386
+ # emacs = { }
387
+ if info.size == 0
388
+ puts red("WARN: Lack of everything of the given word
389
+ Please consider fixing this in the dict `#{dict}`")
390
+ exit
391
+ end
392
+
393
+
394
+
395
+ # Word with no category specifier
396
+ # We call this meaning as type 1
397
+ type_1_exist_flag = false
398
+
399
+ # if already jump, don't check the word itself
400
+ is_jump = false
401
+
402
+ # synonym info print
403
+ if same_key = info['same']
404
+ own_name = info['name']
405
+ pp_dict(dict)
406
+ pp_same_info(dict, word, sheet_content, same_key, own_name)
407
+ # It's also a type 1
408
+ type_1_exist_flag = true
409
+ is_jump = true
410
+ end
411
+
412
+ # normal info print
413
+ # To developer:
414
+ # The word should at least has one of `desc` and `more`
415
+ # But when none exists, this may not be considered wrong,
416
+ # Because the type2 make the case too.
417
+ #
418
+ # So, just ignore it, even if it's really a mistake(insignificant)
419
+ # by dictionary maintainers.
420
+ #
421
+ if !is_jump && (info.has_key?('desc') || info.has_key?('more'))
422
+ pp_dict(dict)
423
+ pp_info(info)
424
+ type_1_exist_flag = true
425
+ end
426
+
427
+ # Meanings with category specifier
428
+ # We call this meaning as type 2
429
+ categories = info.keys - ["name", "desc", "more", "same", "see"]
430
+
431
+ if !categories.empty?
432
+
433
+ if type_1_exist_flag
434
+ print blue(bold("OR")),"\n"
435
+ else
436
+ pp_dict(dict)
437
+ end
438
+
439
+ categories.each do |meaning|
440
+ info0 = sheet_content[word][meaning]
441
+ if same_key = info0['same']
442
+ own_name = info0['name']
443
+ pp_same_info(dict, word, sheet_content, same_key, own_name)
444
+ else
445
+ pp_info(info0)
446
+ end
447
+
448
+ # last meaning doesn't show this separate line
449
+ print blue(bold("OR")),"\n" unless categories.last == meaning
450
+ end
451
+ return true
452
+ elsif type_1_exist_flag
453
+ return true
454
+ else
455
+ return false
456
+ end
457
+ end
458
+
459
+
460
+ #
461
+ # The main procedure of `cr`
462
+ # 1. Search the default's first dict first
463
+ # 2. Search the rest dictionaries in the cryptic dictionaries default dir
464
+ #
465
+ # The `search` is done via the `lookup` function. It will print
466
+ # the info while finding. If `lookup` always return false then
467
+ # means lacking of this word in our dictionaries. So a welcomed
468
+ # contribution is printed on the screen.
469
+ #
470
+ def solve_word(word)
471
+
472
+ add_default_dicts_if_none_exists
473
+
474
+ word = word.downcase # downcase! would lead to frozen error in Ruby 2.7.2
475
+ # The index is the toml file we'll look into
476
+ index = word.chr
477
+ case index
478
+ when '0'..'9'
479
+ index = '0123456789'
480
+ end
481
+
482
+ # Default's first should be 1st to consider
483
+ first_dict = "cryptic_" + CRYPTIC_DEFAULT_DICTS.keys[0].to_s # When Ruby3, We can use DICTS.key(0)
484
+
485
+ # cache lookup results
486
+ results = []
487
+ results << lookup(first_dict,index,word)
488
+ # return if result == true # We should consider all dicts
489
+
490
+ # Then else
491
+ rest = Dir.children(CRYPTIC_RESOLVER_HOME)
492
+ rest.delete first_dict
493
+ rest.each do |dict|
494
+ results << lookup(dict,index,word)
495
+ # continue if result == false # We should consider all dicts
496
+ end
497
+
498
+ unless results.include? true
499
+ puts <<-NotFound
500
+ cr: Not found anything.
501
+
502
+ You may
503
+ 1. Use `cr -u` to update all dicts
504
+ 2. Use `cr -a user/repo` to add more available dicts
505
+
506
+ See: https://github.com/cryptic-resolver
507
+
508
+ 3. Contribute to theses dicts
509
+
510
+ NotFound
511
+
512
+ else
513
+ return
514
+ end
515
+
516
+ end
517
+
518
+
519
+
520
+ #
521
+ # The search word process is quite like `solve_word``
522
+ # Notice:
523
+ # We handle two cases
524
+ #
525
+ # 1. the 'pattern' is the regexp itself
526
+ # 2. the 'pattern' is like '/blahblah/'
527
+ #
528
+ # The second is what Ruby and Perl users like to do, handle it!
529
+ #
530
+ def search_word(pattern)
531
+
532
+ if pattern.nil?
533
+ puts bold(red("cr: Need an argument!"))
534
+ exit -1
535
+ end
536
+
537
+ add_default_dicts_if_none_exists
538
+
539
+ if pattern =~ /^\/(.*)\/$/
540
+ regexp = %r[#$1]
541
+ else
542
+ regexp = %r[#{pattern}]
543
+ end
544
+
545
+ found = false
546
+
547
+ #
548
+ # Try to match every word in all dictionaries
549
+ #
550
+ Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
551
+ sheets = Dir.children(File.join(CRYPTIC_RESOLVER_HOME, dict)).select do
552
+ _1.end_with?('.toml')
553
+ end
554
+
555
+ similar_words_in_a_dict = []
556
+
557
+ sheets.each do |sheet|
558
+ sheet_content = load_sheet(dict, File.basename(sheet,'.toml'))
559
+
560
+ sheet_content.keys.each do
561
+ if _1 =~ regexp
562
+ found = true
563
+ similar_words_in_a_dict << _1
564
+ end
565
+ end
566
+ # end of each sheet in a dict
567
+ end
568
+
569
+ unless similar_words_in_a_dict.empty?
570
+ pp_dict(dict)
571
+ require 'ls_table'
572
+ LsTable.ls(similar_words_in_a_dict) do |e|
573
+ puts blue(e)
574
+ end
575
+ puts
576
+ end
577
+ end
578
+ if !found
579
+ puts red("cr: No words match with #{regexp.inspect}")
580
+ puts
581
+ end
582
+ end
583
+
584
+
585
+ def help
586
+ word_count(p: false)
587
+ user_words = $WordCount - $DefaultWordCount
588
+ puts <<-HELP
589
+ cr: Cryptic Resolver v#{CR_GEM_VERSION} (#{$WordCount} words: default/#{$DefaultWordCount} user/#{user_words})
590
+
591
+ usage:
592
+
593
+ cr emacs => Edit macros: a feature-rich editor
594
+ cr -c => Print word count
595
+ cr -l => List local dictionaries
596
+ cr -u => Update all dictionaries
597
+ cr -a repo.git => Add a new dictionary
598
+ cr -a user/repo => Add a new dictionary on Github
599
+ cr -d cryptic_xx => Delete a dictionary
600
+ cr -s pattern => Search words matched with pattern
601
+ cr -v => Print version
602
+ cr -h => Print this help
603
+
604
+ HELP
605
+
606
+ add_default_dicts_if_none_exists
607
+
608
+ end
609
+
610
+
611
+ def print_version
612
+ puts "cr: Cryptic Resolver version #{CR_GEM_VERSION} in Ruby "
613
+ end
614
+
615
+
616
+ def list_dictionaries
617
+ Dir.chdir CRYPTIC_RESOLVER_HOME do
618
+ Dir.children(CRYPTIC_RESOLVER_HOME).each_with_index do |dict,i|
619
+ puts "#{blue(i+1)}. #{bold(green(dict))}"
620
+ end
621
+ end
622
+ end
623
+
624
+
625
+ # All dictionaries word count
626
+ $WordCount = 0
627
+ # Default dictionaries word count
628
+ $DefaultWordCount = 0
629
+
630
+ def count_dict_words(dict)
631
+
632
+ dict_dir = CRYPTIC_RESOLVER_HOME + "/#{dict}"
633
+
634
+ wc = 0
635
+
636
+ Dir.children(dict_dir).each do |entry|
637
+ next unless entry.end_with?('.toml')
638
+ sheet_content = load_sheet(dict, entry.delete_suffix('.toml'))
639
+ count = sheet_content.keys.count
640
+
641
+ # puts "#{entry}: #{count}"
642
+ wc = wc + count
643
+ end
644
+
645
+ $WordCount += wc
646
+
647
+ return wc
648
+
649
+ end
650
+
651
+
652
+ def word_count(p:)
653
+
654
+ # Always check before Dir.children (this method creates dir if not exists)
655
+ is_there_any_dict?
656
+
657
+ # real dicts in user's directory
658
+ locals = []
659
+ Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
660
+ locals << dict
661
+ end
662
+
663
+ # pre-defined default
664
+ defaults = CRYPTIC_DEFAULT_DICTS.keys.map do |s|
665
+ "cryptic_#{s}"
666
+ end
667
+
668
+ # user may delete some default dicts
669
+ defaults &= locals
670
+
671
+ unless defaults.empty?
672
+ puts(bold(green("Default dicts: "))) if p
673
+ defaults.each do |s|
674
+ wc = count_dict_words(s)
675
+ $DefaultWordCount += wc
676
+ # With color, ljust not works, so we disable color
677
+ puts(" #{s.ljust(17)}: #{wc}") if p
678
+ end
679
+ end
680
+
681
+ users = locals - defaults
682
+ user_words = 0
683
+ unless users.empty?
684
+ wc = 0
685
+ puts(bold(blue("\nUser's dict:"))) if p
686
+ users.each do |s|
687
+ wc = count_dict_words(s)
688
+ # no need to add to $WordCount,
689
+ # because it's done in `count_dict_words` func
690
+ puts(" #{s.ljust(17)}: #{wc}") if p
691
+ end
692
+
693
+ user_words = $WordCount - $DefaultWordCount
694
+ end
695
+
696
+ if p
697
+ puts
698
+ puts "#{$DefaultWordCount.to_s.rjust(4)} words in default dictionaries"
699
+ puts "#{user_words.to_s.rjust(4)} words in user's dictionaries"
700
+ puts "#{$WordCount.to_s.rjust(4)} words altogether"
701
+ end
702
+ end
703
+
704
+
705
+ ####################
706
+ # main: CLI Handling
707
+ ####################
708
+ arg = ARGV.shift
709
+
710
+ case arg
711
+ when nil then help
712
+ when '-v' then print_version
713
+ when '-h' then help
714
+ when '-l' then list_dictionaries
715
+ when '-c' then word_count(p: true)
716
+ when '-u' then update_dicts
717
+ when '-s' then search_word ARGV.shift
718
+ when '-a' then add_dict ARGV.shift
719
+ when '-d' then del_dict ARGV.shift
720
+ when '--help'
721
+ help
722
+ else
723
+ solve_word arg
724
+ end
725
+