yard_ghurt 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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