yard_ghurt 1.0.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.
@@ -0,0 +1,594 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ #--
6
+ # This file is part of YardGhurt.
7
+ # Copyright (c) 2019 Jonathan Bradley Whited (@esotericpig)
8
+ #
9
+ # YardGhurt is free software: you can redistribute it and/or modify
10
+ # it under the terms of the GNU Lesser General Public License as published by
11
+ # the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # YardGhurt is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU Lesser General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU Lesser General Public License
20
+ # along with YardGhurt. If not, see <https://www.gnu.org/licenses/>.
21
+ #++
22
+
23
+
24
+ require 'rake'
25
+ require 'set'
26
+
27
+ require 'rake/tasklib'
28
+
29
+ require 'yard_ghurt/anchor_links'
30
+
31
+ module YardGhurt
32
+ ###
33
+ # Fix (find & replace) text in the GitHub Flavored Markdown (GFM) files in the YARDoc directory,
34
+ # for differences between the two formats.
35
+ #
36
+ # @example What I Use
37
+ # YardGhurt::GFMFixerTask.new() do |task|
38
+ # task.arg_names = [:dev]
39
+ # task.dry_run = false
40
+ # task.fix_code_langs = true
41
+ # task.md_files = ['index.html']
42
+ #
43
+ # task.before = Proc.new() do |task,args|
44
+ # # Delete this file as it's never used (index.html is an exact copy)
45
+ # YardGhurt.rm_exist(File.join(task.doc_dir,'file.README.html'))
46
+ #
47
+ # # Root dir of my GitHub Page for CSS/JS
48
+ # GHP_ROOT_DIR = YardGhurt.to_bool(args.dev) ? '../../esotericpig.github.io' : '../../..'
49
+ #
50
+ # task.css_styles << %Q(<link rel="stylesheet" type="text/css" href="#{GHP_ROOT_DIR}/css/prism.css" />)
51
+ # task.js_scripts << %Q(<script src="#{GHP_ROOT_DIR}/js/prism.js"></script>)
52
+ # end
53
+ # end
54
+ #
55
+ # @example Using All Options
56
+ # YardGhurt::GFMFixerTask.new(:yard_fix) do |task|
57
+ # task.anchor_db = {'tests' => 'Testing'} # #tests => #Testing
58
+ # task.arg_names << :name # Custom args
59
+ # task.css_styles << '<link rel="stylesheet" href="css/my_css.css" />' # Inserted at </head>
60
+ # task.css_styles << '<style>body{ background-color: linen; }</style>'
61
+ # task.custom_gsub = Proc.new() {|line| !line.gsub!('YardGhurt','YARD GHURT!').nil?()}
62
+ # task.custom_gsubs << [/newline/i,'Do you smell what The Rock is cooking?']
63
+ # task.deps << :yard # Custom dependencies
64
+ # task.description = 'Fix it'
65
+ # task.doc_dir = 'doc'
66
+ # task.dry_run = false
67
+ # task.exclude_code_langs = Set['ruby']
68
+ # task.fix_anchor_links = true
69
+ # task.fix_code_langs = true
70
+ # task.fix_file_links = true
71
+ # task.js_scripts << '<script src="js/my_js.js"></script>' # Inserted at </body>
72
+ # task.js_scripts << '<script>document.write("Hello World!");</script>'
73
+ # task.md_files = ['index.html']
74
+ # task.verbose = false
75
+ #
76
+ # task.before = Proc.new() {|task,args| puts "Hi, #{args.name}!"}
77
+ # task.during = Proc.new() {|task,args,file| puts "#{args.name} can haz #{file}?"}
78
+ # task.after = Proc.new() {|task,args| puts "Goodbye, #{args.name}!"}
79
+ # end
80
+ #
81
+ # @author Jonathan Bradley Whited (@esotericpig)
82
+ # @since 1.0.0
83
+ ###
84
+ class GFMFixerTask < Rake::TaskLib
85
+ # This is important so that a subsequent call to this task will not write the CSS again.
86
+ #
87
+ # @return [String] the comment tag of where to place {css_styles}
88
+ #
89
+ # @see add_css_styles!
90
+ CSS_COMMENT = "<!-- #{self} CSS - Do NOT remove this comment! -->"
91
+
92
+ # This is important so that a subsequent call to this task will not write the JS again.
93
+ #
94
+ # @return [String] the comment tag of where to place {js_scripts}
95
+ #
96
+ # @see add_js_scripts!
97
+ JS_COMMENT = "<!-- #{self} JS - Do NOT remove this comment! -->"
98
+
99
+ # @example
100
+ # task.arg_names = [:dev]
101
+ #
102
+ # # @param task [self]
103
+ # # @param args [Rake::TaskArguments] the args specified by {arg_names}
104
+ # task.after = Proc.new do |task,args|
105
+ # puts args.dev
106
+ # end
107
+ #
108
+ # @return [Proc,nil] the Proc ( +respond_to?(:call)+ ) to call at the end of this task or +nil+;
109
+ # default: +nil+
110
+ attr_accessor :after
111
+
112
+ # The anchor links to override in the database.
113
+ #
114
+ # The keys are GFM anchor IDs and the values are their equivalent YARDoc anchor IDs.
115
+ #
116
+ # @return [Hash] the custom database (key-value pairs) of GFM anchor links to YARDoc anchor links;
117
+ # default: +{}+
118
+ #
119
+ # @see build_anchor_links_db
120
+ # @see AnchorLinks#merge_anchor_ids!
121
+ attr_accessor :anchor_db
122
+
123
+ # @return [Array<Symbol>,Symbol] the custom arg(s) for this task; default: +[]+
124
+ attr_accessor :arg_names
125
+
126
+ # @example
127
+ # task.arg_names = [:dev]
128
+ #
129
+ # # @param task [self]
130
+ # # @param args [Rake::TaskArguments] the args specified by {arg_names}
131
+ # task.before = Proc.new do |task,args|
132
+ # puts args.dev
133
+ # end
134
+ #
135
+ # @return [Proc,nil] the Proc ( +respond_to?(:call)+ ) to call at the beginning of this task or +nil+;
136
+ # default: +nil+
137
+ attr_accessor :before
138
+
139
+ # @example
140
+ # task.css_styles << '<link rel="stylesheet" type="text/css" href="css/prism.css" />'
141
+ #
142
+ # @return [Array<String>] the CSS styles to add to each file; default: +[]+
143
+ attr_accessor :css_styles
144
+
145
+ # @example
146
+ # # +gsub!()+ (and other mutable methods) must be used
147
+ # # as the return value must be +true+ or +false+.
148
+ # #
149
+ # # @param line [String] the current line being processed from the current file
150
+ # #
151
+ # # @return [true,false] whether there was a change
152
+ # task.custom_gsub = Proc.new do |line|
153
+ # has_change = false
154
+ #
155
+ # has_change = !line.gsub!('dev','prod').nil?() || has_change
156
+ # # More changes...
157
+ #
158
+ # return has_change
159
+ # end
160
+ #
161
+ # @return [Proc,nil] the custom Proc ( +respond_to?(:call)+ ) to call to gsub! each line for each file
162
+ attr_accessor :custom_gsub
163
+
164
+ # @example
165
+ # task.custom_gsubs = [
166
+ # ['dev','prod'],
167
+ # [/href="#[^"]*"/,'href="#contents"']
168
+ # ]
169
+ #
170
+ # # Internal code:
171
+ # # ---
172
+ # # @custom_gsubs.each do |custom_gsub|
173
+ # # line.gsub!(custom_gsub[0],custom_gsub[1])
174
+ # # end
175
+ #
176
+ # @return [Array<[Regexp,String]>] the custom args to use in gsub on each line for each file
177
+ attr_accessor :custom_gsubs
178
+
179
+ # @example
180
+ # task.deps = :yard
181
+ # # or...
182
+ # task.deps = [:clobber,:yard]
183
+ #
184
+ # @return [Array<Symbol>,Symbol] the custom dependencies for this task; default: +[]+
185
+ attr_accessor :deps
186
+
187
+ # @return [String] the description of this task (customizable)
188
+ attr_accessor :description
189
+
190
+ # @return [String] the directory of generated YARDoc files; default: +doc+
191
+ attr_accessor :doc_dir
192
+
193
+ # @return [true,false] whether to run a dry run (no writing to the files); default: +false+
194
+ attr_accessor :dry_run
195
+
196
+ # @example
197
+ # task.arg_names = [:dev]
198
+ #
199
+ # # @param task [self]
200
+ # # @param args [Rake::TaskArguments] the args specified by {arg_names}
201
+ # # @param file [String] the current file being processed
202
+ # task.during = Proc.new do |task,args,file|
203
+ # puts args.dev
204
+ # end
205
+ #
206
+ # @return [Proc,nil] the Proc to call ( +respond_to?(:call)+ ) at the beginning of processing
207
+ # each file or +nil+; default: +nil+
208
+ attr_accessor :during
209
+
210
+ # @return [Set<String>] the case-sensitive code languages to not fix; default: +Set[ 'ruby' ]+
211
+ #
212
+ # @see fix_code_langs
213
+ attr_accessor :exclude_code_langs
214
+
215
+ # @return [true,false] whether to fix anchor links; default: +true+
216
+ attr_accessor :fix_anchor_links
217
+
218
+ # If +true+, +language-+ will be added to code classes, except for {exclude_code_langs}.
219
+ #
220
+ # For example, +code class="ruby"+ will be changed to +code class="language-ruby"+.
221
+ #
222
+ # @return [true,false] whether to fix code languages; default: +false+
223
+ attr_accessor :fix_code_langs
224
+
225
+ # If +true+, local file links (if the local file exists), will be changed to +file.{filename}.html+.
226
+ #
227
+ # This is useful for +README.md+, +LICENSE.txt+, etc.
228
+ #
229
+ # @return [true,false] whether to fix local file links; default: +true+
230
+ attr_accessor :fix_file_links
231
+
232
+ # This is an internal flag meant to be changed internally.
233
+ #
234
+ # @return [true,false] whether {CSS_COMMENT} has been seen/added; default: +false+
235
+ #
236
+ # @see add_css_styles!
237
+ attr_accessor :has_css_comment
238
+
239
+ # This is an internal flag meant to be changed internally.
240
+ #
241
+ # @return [true,false] whether {JS_COMMENT} has been seen/added; default: +false+
242
+ #
243
+ # @see add_js_scripts!
244
+ attr_accessor :has_js_comment
245
+
246
+ # @example
247
+ # task.js_scripts << '<script src="js/prism.js"></script>'
248
+ #
249
+ # @return [Array<String>] the JS scripts to add to each file; default: +[]+
250
+ attr_accessor :js_scripts
251
+
252
+ # @return [Array<String>] the (GFM) Markdown files to fix; default: +['file.README.html','index.html']+
253
+ attr_accessor :md_files
254
+
255
+ # @return [String] the name of this task (customizable); default: +yard_gfm_fix+
256
+ attr_accessor :name
257
+
258
+ # @return [true,false] whether to output each change to stdout; default: +true+
259
+ attr_accessor :verbose
260
+
261
+ alias_method :dry_run?,:dry_run
262
+ alias_method :fix_anchor_links?,:fix_anchor_links
263
+ alias_method :fix_code_langs?,:fix_code_langs
264
+ alias_method :fix_file_links?,:fix_file_links
265
+ alias_method :has_css_comment?,:has_css_comment
266
+ alias_method :has_js_comment?,:has_js_comment
267
+ alias_method :verbose?,:verbose
268
+
269
+ # @param name [Symbol] the name of this task to use on the command line with +rake+
270
+ def initialize(name=:yard_gfm_fix)
271
+ @after = nil
272
+ @anchor_db = {}
273
+ @arg_names = []
274
+ @before = nil
275
+ @css_styles = []
276
+ @custom_gsub = nil
277
+ @custom_gsubs = []
278
+ @deps = []
279
+ @description = 'Fix (find & replace) text in the YARDoc GitHub Flavored Markdown files'
280
+ @doc_dir = 'doc'
281
+ @dry_run = false
282
+ @during = nil
283
+ @exclude_code_langs = Set['ruby']
284
+ @fix_anchor_links = true
285
+ @fix_code_langs = false
286
+ @fix_file_links = true
287
+ @js_scripts = []
288
+ @md_files = ['file.README.html','index.html']
289
+ @name = name
290
+ @verbose = true
291
+
292
+ yield self if block_given?()
293
+ define()
294
+ end
295
+
296
+ # Reset certain instance vars per file.
297
+ def reset_per_file()
298
+ @anchor_links = AnchorLinks.new()
299
+ @has_css_comment = false
300
+ @has_js_comment = false
301
+ @has_verbose_anchor_links = false
302
+ end
303
+
304
+ # Define the Rake task and description using the instance variables.
305
+ def define()
306
+ desc @description
307
+ task @name,Array(@arg_names) => Array(@deps) do |task,args|
308
+ @before.call(self,args) if @before.respond_to?(:call)
309
+
310
+ @md_files.each do |md_file|
311
+ reset_per_file()
312
+ build_anchor_links_db(md_file)
313
+
314
+ @during.call(self,args,md_file) if @during.respond_to?(:call)
315
+
316
+ fix_md_file(md_file)
317
+ end
318
+
319
+ @after.call(self,args) if @after.respond_to?(:call)
320
+ end
321
+
322
+ return self
323
+ end
324
+
325
+ # Convert each HTML header tag in +md_file+ to a GFM & YARDoc anchor link
326
+ # and build a database using them.
327
+ #
328
+ # @param md_file [String] the file (no dir) to build the anchor links database with,
329
+ # will be joined to {doc_dir}
330
+ #
331
+ # @see AnchorLinks#<<
332
+ # @see AnchorLinks#merge_anchor_ids!
333
+ def build_anchor_links_db(md_file)
334
+ filename = File.join(@doc_dir,md_file)
335
+
336
+ return unless File.exist?(filename)
337
+
338
+ File.open(filename,'r') do |file|
339
+ file.each_line do |line|
340
+ next if line !~ /<h\d+>/i
341
+
342
+ line.gsub!(/<[^>]+>/,'') # Remove tags: <...>
343
+
344
+ @anchor_links << line
345
+ end
346
+ end
347
+
348
+ @anchor_links.merge_anchor_ids!(@anchor_db)
349
+ end
350
+
351
+ # Fix (find & replace) text in +md_file+. Calls all +add_*+ & +gsub_*+ methods.
352
+ #
353
+ # @param md_file [String] the file (no dir) to fix, will be joined to {doc_dir}
354
+ def fix_md_file(md_file)
355
+ filename = File.join(@doc_dir,md_file)
356
+
357
+ puts "[#{filename}]:"
358
+
359
+ if !File.exist?(filename)
360
+ puts '! File does not exist'
361
+
362
+ return
363
+ end
364
+
365
+ changes = 0
366
+ lines = []
367
+
368
+ File.open(filename,'r') do |file|
369
+ file.each_line do |line|
370
+ has_change = false
371
+
372
+ # Standard
373
+ has_change = add_css_styles!(line) || has_change
374
+ has_change = add_js_scripts!(line) || has_change
375
+ has_change = gsub_anchor_links!(line) || has_change
376
+ has_change = gsub_code_langs!(line) || has_change
377
+ has_change = gsub_local_file_links!(line) || has_change
378
+
379
+ # Custom
380
+ has_change = gsub_customs!(line) || has_change
381
+ has_change = gsub_custom!(line) || has_change
382
+
383
+ if has_change
384
+ puts "+ #{line}" if @verbose
385
+
386
+ changes += 1
387
+ end
388
+
389
+ lines << line
390
+ end
391
+ end
392
+
393
+ print '= '
394
+
395
+ if changes > 0
396
+ if @dry_run
397
+ puts 'Nothing written (dry run)'
398
+ else
399
+ File.open(filename,'w') do |file|
400
+ file.puts lines
401
+ end
402
+
403
+ puts "#{changes} changes written"
404
+ end
405
+ else
406
+ puts 'Nothing written (up-to-date)'
407
+ end
408
+ end
409
+
410
+ # Add {CSS_COMMENT} & {css_styles} to +line+ if it is +</head>+,
411
+ # unless {CSS_COMMENT} has already been found or {has_css_comment}.
412
+ #
413
+ # @param line [String] the line from the file to check if +</head>+
414
+ def add_css_styles!(line)
415
+ return false if @has_css_comment || @css_styles.empty?()
416
+
417
+ if line.strip() == CSS_COMMENT
418
+ @has_css_comment = true
419
+
420
+ return false
421
+ end
422
+
423
+ return false unless line =~ /^\s*<\/head>\s*$/i
424
+
425
+ line.slice!(0,line.length())
426
+ line << " #{CSS_COMMENT}"
427
+
428
+ @css_styles.each do |css_style|
429
+ line << "\n #{css_style}"
430
+ end
431
+
432
+ line << "\n\n </head>"
433
+
434
+ @has_css_comment = true
435
+
436
+ return true
437
+ end
438
+
439
+ # Add {JS_COMMENT} & {js_scripts} to +line+ if it is +</body>+,
440
+ # unless {JS_COMMENT} has already been found or {has_js_comment}.
441
+ #
442
+ # @param line [String] the line from the file to check if +</body>+
443
+ def add_js_scripts!(line)
444
+ return false if @has_js_comment || @js_scripts.empty?()
445
+
446
+ if line.strip() == JS_COMMENT
447
+ @has_js_comment = true
448
+
449
+ return false
450
+ end
451
+
452
+ return false unless line =~ /^\s*<\/body>\s*$/i
453
+
454
+ line.slice!(0,line.length())
455
+ line << "\n #{JS_COMMENT}"
456
+
457
+ @js_scripts.each do |js_script|
458
+ line << "\n #{js_script}"
459
+ end
460
+
461
+ line << "\n\n </body>"
462
+
463
+ @has_js_comment = true
464
+
465
+ return true
466
+ end
467
+
468
+ # Replace GFM anchor links with their equivalent YARDoc anchor links,
469
+ # using {build_anchor_links_db} & {anchor_db}, if {fix_anchor_links}.
470
+ #
471
+ # @param line [String] the line from the file to fix
472
+ def gsub_anchor_links!(line)
473
+ return false unless @fix_anchor_links
474
+
475
+ has_change = false
476
+ tag = 'href="#'
477
+
478
+ line.gsub!(Regexp.new(Regexp.quote(tag) + '[^"]*"')) do |href|
479
+ link = href[tag.length..-2]
480
+
481
+ if @anchor_links.yard_anchor_id?(link)
482
+ href
483
+ else
484
+ yard_link = @anchor_links[link]
485
+
486
+ if yard_link.nil?()
487
+ # Either the GFM link is wrong [check with @anchor_links.to_github_anchor_id()]
488
+ # or the internal code is broken [check with @anchor_links.to_s()]
489
+ puts "! YARDoc anchor link for GFM anchor link [#{link}] does not exist"
490
+
491
+ if !@has_verbose_anchor_links
492
+ if @verbose
493
+ puts ' GFM anchor link in the Markdown file is wrong?'
494
+ puts ' Please check the generated links:'
495
+ puts %Q( #{@anchor_links.to_s().strip().gsub("\n","\n ")})
496
+ else
497
+ puts " Turn on #{self.class}.verbose for more info"
498
+ end
499
+
500
+ @has_verbose_anchor_links = true
501
+ end
502
+
503
+ href
504
+ else
505
+ has_change = true
506
+
507
+ %Q(#{tag}#{yard_link}")
508
+ end
509
+ end
510
+ end
511
+
512
+ return has_change
513
+ end
514
+
515
+ # Add +language-+ to code class languages and down case them,
516
+ # if {fix_code_langs} and the language is not in {exclude_code_langs}.
517
+ #
518
+ # @param line [String] the line from the file to fix
519
+ def gsub_code_langs!(line)
520
+ return false unless @fix_code_langs
521
+
522
+ has_change = false
523
+ tag = 'code class="'
524
+
525
+ line.gsub!(Regexp.new(Regexp.quote(tag) + '[^"]*"')) do |code_class|
526
+ lang = code_class[tag.length..-2]
527
+
528
+ if lang =~ /^language\-/ || @exclude_code_langs.include?(lang)
529
+ code_class
530
+ else
531
+ has_change = true
532
+
533
+ %Q(#{tag}language-#{lang.downcase()}")
534
+ end
535
+ end
536
+
537
+ return has_change
538
+ end
539
+
540
+ # Call the custom Proc {custom_gsub} (if it responds to +:call+) on +line+,
541
+ #
542
+ # @param line [String] the line from the file to fix
543
+ def gsub_custom!(line)
544
+ return false unless @custom_gsub.respond_to?(:call)
545
+ return @custom_gsub.call(line)
546
+ end
547
+
548
+ # Call +gsub!()+ on +line+ with each {custom_gsubs},
549
+ # which is an Array of pairs of arguments:
550
+ # task.custom_gsubs = [
551
+ # ['dev','prod'],
552
+ # [/href="#[^"]*"/,'href="#contents"']
553
+ # ]
554
+ #
555
+ # @param line [String] the line from the file to fix
556
+ def gsub_customs!(line)
557
+ return false if @custom_gsubs.empty?()
558
+
559
+ has_change = false
560
+
561
+ @custom_gsubs.each do |custom_gsub|
562
+ has_change = !line.gsub!(custom_gsub[0],custom_gsub[1]).nil?() || has_change
563
+ end
564
+
565
+ return has_change
566
+ end
567
+
568
+ # Replace local file links (that exist) to be +file.{filename}.html+,
569
+ # if {fix_file_links}.
570
+ #
571
+ # @param line [String] the line from the file to fix
572
+ def gsub_local_file_links!(line)
573
+ return false unless @fix_file_links
574
+
575
+ has_change = false
576
+ tag = 'href="'
577
+
578
+ line.gsub!(Regexp.new(Regexp.quote(tag) + '[^#][^"]*"')) do |href|
579
+ link = href[tag.length..-2]
580
+
581
+ if File.exist?(link)
582
+ link = File.basename(link,'.*')
583
+ has_change = true
584
+
585
+ %Q(#{tag}file.#{link}.html")
586
+ else
587
+ href
588
+ end
589
+ end
590
+
591
+ return has_change
592
+ end
593
+ end
594
+ end