gem_docs 0.3.1 → 0.4.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.
- checksums.yaml +4 -4
- data/lib/gem_docs/config.rb +4 -2
- data/lib/gem_docs/doctor.rb +175 -0
- data/lib/gem_docs/emacs.rb +127 -35
- data/lib/gem_docs/overview.rb +13 -15
- data/lib/gem_docs/repo.rb +6 -3
- data/lib/gem_docs/skeleton.rb +28 -4
- data/lib/gem_docs/tasks.rb +27 -7
- data/lib/gem_docs/version.rb +1 -1
- data/lib/gem_docs/yard.rb +4 -4
- data/lib/gem_docs.rb +9 -2
- metadata +6 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 23f41751e4958ef349292a6620097b7319493abce0ee282d7a0bf8d7aa064195
|
|
4
|
+
data.tar.gz: 944ffcfa8220c6991ff91282b61bf5926027d47da432c2087aec34be1a2c40ab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5666427b3018cbb1f6a923e211cbdae7a6b7c86d63e264fca9d24672e005f65f7565716c4a8cd1746d1c52b7a8f46cbaba11b4cfa1df61f1e073888c8ec05c76
|
|
7
|
+
data.tar.gz: 8924cc0a2db42fe04cf7ce40837803aa768a9814a45a8db498fe790eeb1ca3ab15180c316669adfc71c9d2f3ed950bf1812a55dc2ee6e3b6e9f649f82fe63303
|
data/lib/gem_docs/config.rb
CHANGED
|
@@ -11,15 +11,17 @@ module GemDocs
|
|
|
11
11
|
attr_accessor :repo_workflow_dir
|
|
12
12
|
attr_accessor :repo_workflow_name
|
|
13
13
|
attr_accessor :badge
|
|
14
|
+
attr_accessor :emacs_timeout
|
|
14
15
|
|
|
15
16
|
def initialize
|
|
17
|
+
@emacs_timeout = 120
|
|
16
18
|
# Default: support org comment markers
|
|
17
19
|
@overview_headings = ["Introduction"]
|
|
18
20
|
@headers =
|
|
19
21
|
<<~HEADER
|
|
20
22
|
#+PROPERTY: header-args:ruby :results value :colnames no :hlines yes :exports both :dir "./"
|
|
21
|
-
#+PROPERTY: header-args:ruby+ :wrap example :session %n_session :eval
|
|
22
|
-
#+PROPERTY: header-args:ruby+ :prologue "$:.unshift('./lib') unless $:.first == './lib'; require '%n'"
|
|
23
|
+
#+PROPERTY: header-args:ruby+ :wrap example :session %n_session :eval never-export
|
|
24
|
+
#+PROPERTY: header-args:ruby+ :prologue "$:.unshift('./lib') unless $:.first == './lib'; require '%n'; nil"
|
|
23
25
|
#+PROPERTY: header-args:ruby+ :ruby "bundle exec irb"
|
|
24
26
|
#+PROPERTY: header-args:sh :exports code :eval no
|
|
25
27
|
#+PROPERTY: header-args:bash :exports code :eval no
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module GemDocs
|
|
6
|
+
# Advisory static checks; never evaluates examples or starts Emacs.
|
|
7
|
+
class Doctor
|
|
8
|
+
def self.run
|
|
9
|
+
new.run
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@root = GemDocs.project_root
|
|
14
|
+
@suggestions = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
readme = path(README_ORG)
|
|
19
|
+
if File.file?(readme)
|
|
20
|
+
text = File.read(readme)
|
|
21
|
+
outline, ruby_blocks = scan_org(text)
|
|
22
|
+
check_overview(outline)
|
|
23
|
+
check_headers(outline) unless ruby_blocks.empty?
|
|
24
|
+
check_version(outline)
|
|
25
|
+
check_export(readme)
|
|
26
|
+
else
|
|
27
|
+
suggest(README_ORG, "File is missing. Run bundle exec rake docs:skeleton:readme to create it.")
|
|
28
|
+
check_export(readme) if File.file?(path(README_MD))
|
|
29
|
+
end
|
|
30
|
+
check_yard
|
|
31
|
+
puts "GemDocs doctor: #{@root}"
|
|
32
|
+
puts "Advisory static checks; Org's inherited and per-block settings may intentionally differ."
|
|
33
|
+
if @suggestions.empty?
|
|
34
|
+
puts "No suggestions from these checks."
|
|
35
|
+
else
|
|
36
|
+
@suggestions.each { |message| puts "- #{message}" }
|
|
37
|
+
end
|
|
38
|
+
@suggestions
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def path(name)
|
|
44
|
+
File.join(@root, name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def suggest(file, message)
|
|
48
|
+
@suggestions << "#{file}: #{message}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Exclude examples and export blocks so quoted headers/headings do not
|
|
52
|
+
# masquerade as document configuration.
|
|
53
|
+
def scan_org(text)
|
|
54
|
+
outline = +''
|
|
55
|
+
ruby_blocks = []
|
|
56
|
+
block = nil
|
|
57
|
+
text.each_line do |line|
|
|
58
|
+
if block
|
|
59
|
+
block = nil if line.match?(/^\s*#\+end_#{Regexp.escape(block)}\b/i)
|
|
60
|
+
elsif (match = line.match(/^\s*#\+begin_(\w+)\b(.*)/i))
|
|
61
|
+
block = match[1]
|
|
62
|
+
ruby_blocks << line if block.casecmp?('src') && match[2].match?(/^\s+ruby\b/i)
|
|
63
|
+
else
|
|
64
|
+
outline << line
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
[outline, ruby_blocks]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def check_overview(outline)
|
|
71
|
+
heads = Array(GemDocs.config.overview_headings)
|
|
72
|
+
return if heads.empty?
|
|
73
|
+
|
|
74
|
+
sections = outline.scan(/^\* +([^\n]+)\n(.*?)(?=^\* |\z)/m).to_h
|
|
75
|
+
missing = heads.reject { |head| sections.key?(head) }
|
|
76
|
+
unless missing.empty?
|
|
77
|
+
suggest(README_ORG, "No top-level overview section for #{missing.map(&:inspect).join(', ')}. " \
|
|
78
|
+
"Add prose under that heading, or set c.overview_headings to existing top-level headings " \
|
|
79
|
+
"(for example, ['Overview']) in GemDocs.configure.")
|
|
80
|
+
end
|
|
81
|
+
(heads - missing).each do |head|
|
|
82
|
+
next unless sections[head].strip.empty?
|
|
83
|
+
|
|
84
|
+
suggest(README_ORG, "The #{head.inspect} overview section is empty. Add a short description for YARD/ri.")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def check_headers(outline)
|
|
89
|
+
headers = outline.lines.grep(/^#\+PROPERTY:\s+header-args:ruby\+?\s/i).join
|
|
90
|
+
if headers.empty?
|
|
91
|
+
suggest(README_ORG, "Ruby examples have no file-level Ruby headers. Run bundle exec rake docs:headers " \
|
|
92
|
+
"or review local/inherited settings before adding the defaults.")
|
|
93
|
+
return
|
|
94
|
+
end
|
|
95
|
+
unless headers.match?(/:eval\s+never-export\b/)
|
|
96
|
+
suggest(README_ORG, "Ruby headers do not specify :eval never-export. Consider it to execute examples with " \
|
|
97
|
+
"docs:tangle and reuse results during export; docs:all then runs examples once.")
|
|
98
|
+
end
|
|
99
|
+
if headers.match?(/:eval\s+yes\b/)
|
|
100
|
+
suggest(README_ORG, "Ruby headers contain :eval yes, which allows execution during export. " \
|
|
101
|
+
"Remove or replace conflicting :eval settings if adopting never-export.")
|
|
102
|
+
end
|
|
103
|
+
unless headers.match?(/:session\s+(?!none\b)\S+/)
|
|
104
|
+
suggest(README_ORG, "No shared Ruby :session is declared. Add one if examples depend on earlier variables.")
|
|
105
|
+
end
|
|
106
|
+
unless headers.match?(/:exports\s+both\b/)
|
|
107
|
+
suggest(README_ORG, "Consider :exports both for Ruby examples so exported docs show code and saved results.")
|
|
108
|
+
end
|
|
109
|
+
unless headers.match?(/:wrap\s+example\b/)
|
|
110
|
+
suggest(
|
|
111
|
+
README_ORG,
|
|
112
|
+
"Consider :wrap example to preserve saved Ruby output as literal text during Markdown export.",
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
unless headers.match?(/:prologue\b/)
|
|
116
|
+
suggest(README_ORG, "No Ruby :prologue is declared. Ensure examples load the local gem before using it.")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def check_version(outline)
|
|
121
|
+
blocks = outline.scan(Skeleton::VERSION_BLOCK_RE)
|
|
122
|
+
if blocks.size != 1
|
|
123
|
+
suggest(README_ORG, "Expected one managed README version block, found #{blocks.size}. " \
|
|
124
|
+
"Remove duplicates if needed, then run bundle exec rake docs:skeleton:readme.")
|
|
125
|
+
return
|
|
126
|
+
end
|
|
127
|
+
specs = Dir.glob(path('*.gemspec'))
|
|
128
|
+
return unless specs.one?
|
|
129
|
+
|
|
130
|
+
spec = Gem::Specification.load(specs.first)
|
|
131
|
+
return unless spec
|
|
132
|
+
return if blocks.first.lines.any? { |line| line.strip == "Version: #{spec.version}" }
|
|
133
|
+
|
|
134
|
+
suggest(
|
|
135
|
+
README_ORG,
|
|
136
|
+
"README version differs from gemspec #{spec.version}. Run bundle exec rake docs:skeleton:readme.",
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def check_export(readme)
|
|
141
|
+
markdown = path(README_MD)
|
|
142
|
+
if !File.file?(markdown)
|
|
143
|
+
suggest(README_MD, "File is missing. Run bundle exec rake docs:export after refreshing example results.")
|
|
144
|
+
elsif File.open(markdown, &:gets).to_s.chomp != Emacs::README_MARKER
|
|
145
|
+
suggest(README_MD, "Protected from export: missing first-line #{Emacs::README_MARKER}. " \
|
|
146
|
+
"Keep handwritten Markdown as-is; review/back up older generated files before adding the marker to opt in.")
|
|
147
|
+
elsif File.file?(readme) && File.mtime(readme) > File.mtime(markdown)
|
|
148
|
+
suggest(README_MD, "README.org is newer; export may be stale. " \
|
|
149
|
+
"Run docs:all after example edits, or docs:export if saved results are current.")
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def check_yard
|
|
154
|
+
unless File.file?(path('.yardopts'))
|
|
155
|
+
suggest(".yardopts", "File is missing. docs:yard creates defaults; review and commit that configuration.")
|
|
156
|
+
end
|
|
157
|
+
%w[.yardoc doc].each do |name|
|
|
158
|
+
next unless File.directory?(path(name))
|
|
159
|
+
|
|
160
|
+
tracked, status = Open3.capture2('git', '-C', @root, 'ls-files', '--', name, err: File::NULL)
|
|
161
|
+
next unless status.success?
|
|
162
|
+
|
|
163
|
+
if !tracked.empty?
|
|
164
|
+
suggest("#{name}/", "Contains tracked generated YARD files. Consider untracking and ignoring that directory.")
|
|
165
|
+
else
|
|
166
|
+
_, ignored = Open3.capture2('git', '-C', @root, 'check-ignore', '-q', '--', "#{name}/", err: File::NULL)
|
|
167
|
+
suggest(".gitignore", "Add /#{name}/ to exclude generated YARD output.") unless ignored.success?
|
|
168
|
+
end
|
|
169
|
+
rescue Errno::ENOENT
|
|
170
|
+
# Git is optional; the remaining static checks still apply.
|
|
171
|
+
next
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
data/lib/gem_docs/emacs.rb
CHANGED
|
@@ -1,57 +1,149 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'tempfile'
|
|
4
|
+
|
|
3
5
|
module GemDocs
|
|
4
6
|
module Emacs
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
README_MARKER = '<!-- Generated by gem_docs -->'
|
|
9
|
+
|
|
5
10
|
def self.tangle
|
|
11
|
+
path = File.expand_path(README_ORG, GemDocs.project_root)
|
|
12
|
+
# A conservative fast path: leave header inheritance and eligibility to Org.
|
|
13
|
+
unless File.read(path).match?(/#\+begin_src\b|#\+call:|\bsrc_\w+|\bcall_\w+/i)
|
|
14
|
+
puts "Skipped: no executable code blocks"
|
|
15
|
+
return
|
|
16
|
+
end
|
|
17
|
+
|
|
6
18
|
expr = <<~ELISP
|
|
7
|
-
(
|
|
8
|
-
(
|
|
9
|
-
|
|
10
|
-
(
|
|
11
|
-
(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
(with-current-buffer (find-file-noselect #{path.dump})
|
|
20
|
+
(save-buffer)
|
|
21
|
+
(require 'ob-ruby)
|
|
22
|
+
(let ((eligible nil))
|
|
23
|
+
(let ((org-confirm-babel-evaluate nil))
|
|
24
|
+
(org-babel-map-executables nil
|
|
25
|
+
(let ((info (org-babel-get-src-block-info 'light)))
|
|
26
|
+
(when (or (null info) (org-babel-check-confirm-evaluate info))
|
|
27
|
+
(setq eligible t)))))
|
|
28
|
+
(if (not eligible)
|
|
29
|
+
"Skipped: no evaluable code blocks"
|
|
30
|
+
(let ((session (get-buffer #{session_name.dump})))
|
|
31
|
+
(when session
|
|
32
|
+
(let ((process (get-buffer-process session)))
|
|
33
|
+
(when process (set-process-query-on-exit-flag process nil)))
|
|
34
|
+
(kill-buffer session)))
|
|
35
|
+
(org-babel-execute-buffer)
|
|
36
|
+
(save-buffer)
|
|
37
|
+
"Babel execution complete")))
|
|
16
38
|
ELISP
|
|
17
39
|
|
|
18
|
-
|
|
19
|
-
|
|
40
|
+
run(expr, "Babel execution")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.export_readme # rubocop:disable Naming/PredicateMethod -- public command, not a predicate
|
|
44
|
+
check_readme_destination!
|
|
45
|
+
root = GemDocs.project_root
|
|
46
|
+
destination = File.join(root, README_MD)
|
|
47
|
+
Tempfile.create(['.gem_docs-readme-', '.md'], root) do |temporary|
|
|
48
|
+
export(README_ORG, destination: temporary.path)
|
|
49
|
+
generated = File.read(temporary.path)
|
|
50
|
+
check_readme_destination!
|
|
51
|
+
temporary.rewind
|
|
52
|
+
temporary.write("#{README_MARKER}\n\n#{generated}")
|
|
53
|
+
temporary.truncate(temporary.pos)
|
|
54
|
+
temporary.flush
|
|
55
|
+
temporary.chmod(File.exist?(destination) ? File.stat(destination).mode & 0o777 : 0o644)
|
|
56
|
+
File.rename(temporary.path, destination)
|
|
20
57
|
end
|
|
58
|
+
true
|
|
21
59
|
end
|
|
22
60
|
|
|
23
|
-
def self.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
(save-buffer)
|
|
28
|
-
(require 'ox-gfm)
|
|
29
|
-
(org-gfm-export-to-markdown))
|
|
30
|
-
(with-current-buffer (find-file-noselect "#{CHANGELOG_ORG}")
|
|
31
|
-
(save-buffer)
|
|
32
|
-
(require 'ox-gfm)
|
|
33
|
-
(org-gfm-export-to-markdown))
|
|
34
|
-
"Export README complete")
|
|
35
|
-
ELISP
|
|
61
|
+
def self.check_readme_destination!
|
|
62
|
+
destination = File.join(GemDocs.project_root, README_MD)
|
|
63
|
+
return unless File.exist?(destination) || File.symlink?(destination)
|
|
64
|
+
return if File.file?(destination) && File.open(destination, &:gets).to_s.chomp == README_MARKER
|
|
36
65
|
|
|
37
|
-
|
|
66
|
+
raise Error, "Refusing to overwrite #{destination}: missing first-line #{README_MARKER}. " \
|
|
67
|
+
"Keep handwritten Markdown unchanged. To opt into generation, review/back up the file " \
|
|
68
|
+
"and add that marker as its first line."
|
|
38
69
|
end
|
|
39
70
|
|
|
40
71
|
def self.export_changelog
|
|
41
|
-
|
|
42
|
-
(save-window-excursion
|
|
43
|
-
(with-current-buffer (find-file-noselect "#{CHANGELOG_ORG}")
|
|
44
|
-
(save-buffer)
|
|
45
|
-
(require 'ox-gfm)
|
|
46
|
-
(org-gfm-export-to-markdown))
|
|
47
|
-
"Export CHANGELOG complete")
|
|
48
|
-
ELISP
|
|
49
|
-
|
|
50
|
-
system("emacsclient", "--quiet", "--eval", expr)
|
|
72
|
+
export(CHANGELOG_ORG)
|
|
51
73
|
end
|
|
52
74
|
|
|
53
75
|
def self.session_name
|
|
54
76
|
"*#{Repo.from_gemspec.name}_session*"
|
|
55
77
|
end
|
|
78
|
+
|
|
79
|
+
class << self
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def export(filename, destination: nil)
|
|
83
|
+
path = File.expand_path(filename, GemDocs.project_root)
|
|
84
|
+
raise Error, "Cannot export missing file: #{path}" unless File.file?(path)
|
|
85
|
+
|
|
86
|
+
expr = <<~ELISP
|
|
87
|
+
(with-current-buffer (find-file-noselect #{path.dump})
|
|
88
|
+
(save-buffer)
|
|
89
|
+
(require 'ox-gfm)
|
|
90
|
+
#{destination ? "(org-export-to-file 'gfm #{destination.dump})" : '(org-gfm-export-to-markdown)'}
|
|
91
|
+
#{"Export #{filename} complete".dump})
|
|
92
|
+
ELISP
|
|
93
|
+
run(expr, "Export #{filename}")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def run(expr, operation)
|
|
97
|
+
seconds = Float(GemDocs.config.emacs_timeout)
|
|
98
|
+
raise ArgumentError,
|
|
99
|
+
"emacs_timeout must be a positive, finite number" unless seconds.positive? && seconds.finite?
|
|
100
|
+
|
|
101
|
+
# Fail on prompts rather than leaving emacsclient waiting in the background.
|
|
102
|
+
wrapped = <<~ELISP
|
|
103
|
+
(progn
|
|
104
|
+
(require 'cl-lib)
|
|
105
|
+
(save-window-excursion
|
|
106
|
+
(cl-letf (((symbol-function 'yes-or-no-p)
|
|
107
|
+
(lambda (&rest args)
|
|
108
|
+
(error "GemDocs requires interactive confirmation: %s. Run this operation in Emacs first" (car args))))
|
|
109
|
+
((symbol-function 'y-or-n-p)
|
|
110
|
+
(symbol-function 'yes-or-no-p)))
|
|
111
|
+
(with-timeout (#{seconds} (error "GemDocs timed out: #{operation}"))
|
|
112
|
+
#{expr}))))
|
|
113
|
+
ELISP
|
|
114
|
+
pid = Process.spawn("emacsclient", "--quiet", "--eval", wrapped)
|
|
115
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds
|
|
116
|
+
loop do
|
|
117
|
+
result = Process.waitpid2(pid, Process::WNOHANG)
|
|
118
|
+
if result
|
|
119
|
+
pid = nil
|
|
120
|
+
unless result.last.success?
|
|
121
|
+
raise Error,
|
|
122
|
+
"#{operation} failed (emacsclient exit #{result.last.exitstatus.inspect}); see Emacs output above"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
return true
|
|
126
|
+
end
|
|
127
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
128
|
+
raise Error,
|
|
129
|
+
"#{operation} timed out after #{seconds}s. " \
|
|
130
|
+
"The Emacs server may still be executing; inspect it before retrying."
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
sleep 0.05
|
|
134
|
+
end
|
|
135
|
+
rescue Errno::ENOENT
|
|
136
|
+
raise Error, "#{operation} failed: emacsclient was not found"
|
|
137
|
+
ensure
|
|
138
|
+
if pid
|
|
139
|
+
begin
|
|
140
|
+
Process.kill("KILL", pid)
|
|
141
|
+
rescue Errno::ESRCH
|
|
142
|
+
# The client exited between the timeout and cleanup.
|
|
143
|
+
end
|
|
144
|
+
Process.waitpid(pid)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
56
148
|
end
|
|
57
149
|
end
|
data/lib/gem_docs/overview.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'strscan'
|
|
4
|
-
require 'debug'
|
|
5
4
|
|
|
6
5
|
module GemDocs
|
|
7
6
|
module Overview
|
|
@@ -26,7 +25,7 @@ module GemDocs
|
|
|
26
25
|
#
|
|
27
26
|
#{overview.lines.map { |l| l.match?(/\A\s*\z/) ? '#' : "# #{l.rstrip}" }.join("\n")}
|
|
28
27
|
RUBY
|
|
29
|
-
return false if old_comment == new_comment
|
|
28
|
+
return false if old_comment.rstrip == new_comment
|
|
30
29
|
|
|
31
30
|
new_lib =
|
|
32
31
|
if old_match
|
|
@@ -35,10 +34,10 @@ module GemDocs
|
|
|
35
34
|
old_lib.sub(old_comment, new_comment)
|
|
36
35
|
else
|
|
37
36
|
scanner = StringScanner.new(old_lib)
|
|
38
|
-
if scanner.scan_until(/^module #{repo.module_name}/)
|
|
37
|
+
if scanner.scan_until(/^(?:class|module) #{Regexp.escape(repo.module_name)}\b/)
|
|
39
38
|
scanner.pre_match + "\n" + new_comment + "\n" + scanner.matched + scanner.rest
|
|
40
39
|
else
|
|
41
|
-
# No
|
|
40
|
+
# No matching class or module in the file. Add an empty module at the end.
|
|
42
41
|
old_lib + "\n" + new_comment + "\nmodule #{repo.module_name}\nend"
|
|
43
42
|
end
|
|
44
43
|
end
|
|
@@ -46,18 +45,15 @@ module GemDocs
|
|
|
46
45
|
end
|
|
47
46
|
|
|
48
47
|
# Return a Regexp that capture any GemDocs-generated overview comment in
|
|
49
|
-
# the main
|
|
50
|
-
#
|
|
51
|
-
# will not match.
|
|
48
|
+
# the main library file. The Regex requires the comment to come immediately
|
|
49
|
+
# before the `class <GemName>` or `module <GemName>` line, or it will not match.
|
|
52
50
|
def self.overview_re
|
|
53
|
-
heads = GemDocs.config.overview_headings
|
|
54
|
-
re_str = "#\\s*" + Regexp.quote(BANNER) + ".*"
|
|
55
|
-
heads.each do |h|
|
|
56
|
-
re_str << "\\*\\s+#{Regexp.escape(h)}.*"
|
|
57
|
-
end
|
|
58
51
|
repo = Repo.from_gemspec
|
|
59
|
-
|
|
60
|
-
|
|
52
|
+
# Match only comment/blank lines, never Ruby code between the banner and
|
|
53
|
+
# declaration. This also recognizes legacy empty or duplicated banners.
|
|
54
|
+
%r{^\#[ ]#{Regexp.escape(BANNER)}[^\n]*
|
|
55
|
+
(?:\n(?:[ \t]*\#[^\n]*|[ \t]*))*
|
|
56
|
+
(?=\n(?:class|module)\s+#{Regexp.escape(repo.module_name)}\b)}ix
|
|
61
57
|
end
|
|
62
58
|
|
|
63
59
|
def self.present?
|
|
@@ -82,7 +78,7 @@ module GemDocs
|
|
|
82
78
|
result = +''
|
|
83
79
|
scanner = StringScanner.new(text)
|
|
84
80
|
heads.each do |h|
|
|
85
|
-
if scanner.scan_until(
|
|
81
|
+
if scanner.scan_until(/^(?<head>\* +#{Regexp.escape(h)})(?=[ \t\n]|\z)[^\n]*\n/)
|
|
86
82
|
this_head = scanner.named_captures['head'] + "\n"
|
|
87
83
|
body_start = scanner.pos
|
|
88
84
|
body_end =
|
|
@@ -95,6 +91,8 @@ module GemDocs
|
|
|
95
91
|
result << this_head + scanner.string[body_start..body_end]
|
|
96
92
|
end
|
|
97
93
|
end
|
|
94
|
+
return if result.empty?
|
|
95
|
+
|
|
98
96
|
result.sub(/\A\n*/, '')
|
|
99
97
|
end
|
|
100
98
|
end
|
data/lib/gem_docs/repo.rb
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module GemDocs
|
|
4
4
|
class Repo
|
|
5
|
-
attr_accessor :root, :host, :user, :name, :module_name
|
|
5
|
+
attr_accessor :root, :host, :user, :name, :module_name, :version
|
|
6
6
|
attr_accessor :branch, :workflow_dir, :workflow_name
|
|
7
7
|
|
|
8
8
|
def initialize(root: nil, host: nil,
|
|
9
9
|
user: nil,
|
|
10
10
|
name: nil,
|
|
11
11
|
module_name: nil,
|
|
12
|
+
version: nil,
|
|
12
13
|
branch: 'master',
|
|
13
14
|
workflow_dir: nil,
|
|
14
15
|
workflow_name: nil)
|
|
@@ -16,6 +17,7 @@ module GemDocs
|
|
|
16
17
|
@host = host
|
|
17
18
|
@user = user
|
|
18
19
|
@module_name = module_name
|
|
20
|
+
@version = version
|
|
19
21
|
@name = name
|
|
20
22
|
@branch = branch
|
|
21
23
|
@workflow_dir = workflow_dir
|
|
@@ -59,6 +61,7 @@ module GemDocs
|
|
|
59
61
|
user: user,
|
|
60
62
|
name: name,
|
|
61
63
|
module_name: mname,
|
|
64
|
+
version: spec.version.to_s,
|
|
62
65
|
branch: branch,
|
|
63
66
|
workflow_dir: wdir,
|
|
64
67
|
workflow_name: wname,
|
|
@@ -72,7 +75,7 @@ module GemDocs
|
|
|
72
75
|
GemDocs.config.repo_workflow_dir,
|
|
73
76
|
GemDocs.config.repo_workflow_name,
|
|
74
77
|
)
|
|
75
|
-
return [File.dirname(workflow_file), File.
|
|
78
|
+
return [File.dirname(workflow_file), File.basename(workflow_file)] if File.readable?(workflow_file)
|
|
76
79
|
end
|
|
77
80
|
|
|
78
81
|
dir = File.join(GemDocs.project_root, ".github/workflows")
|
|
@@ -149,7 +152,7 @@ module GemDocs
|
|
|
149
152
|
abort "No gemspec found" if candidates.empty?
|
|
150
153
|
abort "Multiple gemspecs found: #{candidates.join(', ')}" if candidates.size > 1
|
|
151
154
|
end
|
|
152
|
-
candidates.first
|
|
155
|
+
File.join(GemDocs.project_root, candidates.first)
|
|
153
156
|
end
|
|
154
157
|
|
|
155
158
|
def load_gemspec(path)
|
data/lib/gem_docs/skeleton.rb
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module GemDocs
|
|
4
4
|
module Skeleton
|
|
5
|
-
|
|
6
|
-
def self.make_readme?
|
|
7
|
-
return false if readme_present?
|
|
5
|
+
VERSION_BLOCK_RE = /^# gem_docs:version begin\n.*?^# gem_docs:version end\n?/m
|
|
8
6
|
|
|
7
|
+
# Create the README or refresh its managed gemspec version block.
|
|
8
|
+
# @return [Boolean] whether the README changed
|
|
9
|
+
def self.make_readme?
|
|
9
10
|
repo = Repo.from_gemspec
|
|
11
|
+
return write_version?(File.read(README_ORG), repo.version) if readme_present?
|
|
12
|
+
|
|
10
13
|
content = <<~SKEL
|
|
11
14
|
#+TITLE: #{repo.module_name} Guide
|
|
15
|
+
#{Header.org_headers.strip}
|
|
12
16
|
|
|
13
17
|
* Introduction
|
|
14
18
|
|
|
@@ -42,8 +46,28 @@ module GemDocs
|
|
|
42
46
|
The gem is available as open source under the terms of the [MIT
|
|
43
47
|
License](https://opensource.org/licenses/MIT).
|
|
44
48
|
SKEL
|
|
45
|
-
|
|
49
|
+
write_version?(content, repo.version)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.write_version?(content, version)
|
|
53
|
+
block = <<~ORG
|
|
54
|
+
# gem_docs:version begin
|
|
55
|
+
Version: #{version}
|
|
56
|
+
# gem_docs:version end
|
|
57
|
+
ORG
|
|
58
|
+
updated = if content.match?(VERSION_BLOCK_RE)
|
|
59
|
+
content.sub(VERSION_BLOCK_RE, block)
|
|
60
|
+
else
|
|
61
|
+
# Keep the version below the initial Org metadata and near the CI badge.
|
|
62
|
+
header = content[/\A(?:#\+[^\n]*\n)*/]
|
|
63
|
+
header + "\n" + block + "\n" + content.delete_prefix(header)
|
|
64
|
+
end
|
|
65
|
+
return false if File.exist?(README_ORG) && updated == content
|
|
66
|
+
|
|
67
|
+
File.write(README_ORG, updated)
|
|
68
|
+
true
|
|
46
69
|
end
|
|
70
|
+
private_class_method :write_version?
|
|
47
71
|
|
|
48
72
|
def self.readme_present?
|
|
49
73
|
File.exist?(README_ORG)
|
data/lib/gem_docs/tasks.rb
CHANGED
|
@@ -15,6 +15,15 @@ module GemDocs
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
namespace :docs do
|
|
18
|
+
task :check_readme do
|
|
19
|
+
GemDocs::Emacs.check_readme_destination!
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc "Suggest documentation improvements without changing files or running examples"
|
|
23
|
+
task :doctor do
|
|
24
|
+
GemDocs::Doctor.run
|
|
25
|
+
end
|
|
26
|
+
|
|
18
27
|
desc "Evaluate code blocks in README.org"
|
|
19
28
|
task :tangle => ["docs:skeleton:readme"] do
|
|
20
29
|
print "Executing code blocks in #{README_ORG} ... "
|
|
@@ -22,7 +31,7 @@ module GemDocs
|
|
|
22
31
|
end
|
|
23
32
|
|
|
24
33
|
desc "Export README.org → README.md"
|
|
25
|
-
task :export => [:badge, README_MD, CHANGELOG_MD]
|
|
34
|
+
task :export => [:check_readme, :badge, README_MD, CHANGELOG_MD]
|
|
26
35
|
|
|
27
36
|
desc "Extract overview from README.org and embed in lib/<gem>.rb for ri/yard"
|
|
28
37
|
task :overview => ["docs:skeleton:readme", README_ORG] do
|
|
@@ -30,17 +39,17 @@ module GemDocs
|
|
|
30
39
|
if GemDocs::Overview.write_overview?
|
|
31
40
|
puts "added"
|
|
32
41
|
else
|
|
33
|
-
puts "already
|
|
42
|
+
puts "unchanged (already current or no matching overview)"
|
|
34
43
|
end
|
|
35
44
|
end
|
|
36
45
|
|
|
37
46
|
namespace :skeleton do
|
|
38
|
-
desc "Create
|
|
47
|
+
desc "Create README.org if missing and refresh its gem version"
|
|
39
48
|
task :readme do
|
|
40
49
|
if GemDocs::Skeleton.make_readme?
|
|
41
|
-
puts "README.org
|
|
50
|
+
puts "README.org created or version updated"
|
|
42
51
|
else
|
|
43
|
-
puts "README.org already
|
|
52
|
+
puts "README.org already up to date"
|
|
44
53
|
end
|
|
45
54
|
end
|
|
46
55
|
|
|
@@ -55,7 +64,7 @@ module GemDocs
|
|
|
55
64
|
end
|
|
56
65
|
|
|
57
66
|
desc "Insert #+PROPERTY headers at top of README.org for code blocks"
|
|
58
|
-
task :
|
|
67
|
+
task :headers => "docs:skeleton:readme" do
|
|
59
68
|
print "Inserting headers ... "
|
|
60
69
|
if GemDocs::Header.write_header?
|
|
61
70
|
puts "added"
|
|
@@ -64,6 +73,8 @@ module GemDocs
|
|
|
64
73
|
end
|
|
65
74
|
end
|
|
66
75
|
|
|
76
|
+
task :header => :headers
|
|
77
|
+
|
|
67
78
|
desc "Generate YARD HTML documentation"
|
|
68
79
|
task :yard => [:overview] do
|
|
69
80
|
puts "Generating YARD documentation ... "
|
|
@@ -81,7 +92,16 @@ module GemDocs
|
|
|
81
92
|
end
|
|
82
93
|
|
|
83
94
|
desc "Run all documentation tasks (examples, readme, overview, yard, ri)"
|
|
84
|
-
task :all => [
|
|
95
|
+
task :all => [
|
|
96
|
+
:check_readme,
|
|
97
|
+
"docs:skeleton:readme",
|
|
98
|
+
"docs:skeleton:changelog",
|
|
99
|
+
:headers,
|
|
100
|
+
:tangle,
|
|
101
|
+
:export,
|
|
102
|
+
:overview,
|
|
103
|
+
:yard
|
|
104
|
+
]
|
|
85
105
|
end
|
|
86
106
|
end
|
|
87
107
|
end
|
data/lib/gem_docs/version.rb
CHANGED
data/lib/gem_docs/yard.rb
CHANGED
|
@@ -31,11 +31,11 @@ module GemDocs
|
|
|
31
31
|
YOPTS
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
#
|
|
34
|
+
# Create defaults only when the project has no .yardopts file.
|
|
35
35
|
def write_yardopts
|
|
36
|
-
if
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
return if File.exist?(yardopts_path)
|
|
37
|
+
|
|
38
|
+
File.write(yardopts_path, yardopts_contents)
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
end
|
data/lib/gem_docs.rb
CHANGED
|
@@ -285,6 +285,7 @@ module GemDocs
|
|
|
285
285
|
require_relative "gem_docs/badge"
|
|
286
286
|
require_relative "gem_docs/header"
|
|
287
287
|
require_relative "gem_docs/skeleton"
|
|
288
|
+
require_relative "gem_docs/doctor"
|
|
288
289
|
require_relative "gem_docs/tasks"
|
|
289
290
|
|
|
290
291
|
README_ORG = "README.org"
|
|
@@ -295,7 +296,13 @@ module GemDocs
|
|
|
295
296
|
# Auto-detect project root (handles being run from subdirs)
|
|
296
297
|
def self.project_root
|
|
297
298
|
here = Dir.pwd
|
|
298
|
-
|
|
299
|
-
|
|
299
|
+
loop do
|
|
300
|
+
return here unless Dir.glob(['*.gemspec', 'Gemfile'], base: here).empty?
|
|
301
|
+
|
|
302
|
+
parent = File.dirname(here)
|
|
303
|
+
return here if parent == here
|
|
304
|
+
|
|
305
|
+
here = parent
|
|
306
|
+
end
|
|
300
307
|
end
|
|
301
308
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gem_docs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel E. Doherty
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-10-26 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rake
|
|
@@ -29,14 +29,14 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version:
|
|
32
|
+
version: 0.9.28
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version:
|
|
39
|
+
version: 0.9.28
|
|
40
40
|
description: Shared tasks for README.org code block execution, markdown export, YARD
|
|
41
41
|
integration, etc.
|
|
42
42
|
email:
|
|
@@ -48,6 +48,7 @@ files:
|
|
|
48
48
|
- lib/gem_docs.rb
|
|
49
49
|
- lib/gem_docs/badge.rb
|
|
50
50
|
- lib/gem_docs/config.rb
|
|
51
|
+
- lib/gem_docs/doctor.rb
|
|
51
52
|
- lib/gem_docs/emacs.rb
|
|
52
53
|
- lib/gem_docs/header.rb
|
|
53
54
|
- lib/gem_docs/overview.rb
|
|
@@ -76,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
76
77
|
- !ruby/object:Gem::Version
|
|
77
78
|
version: '0'
|
|
78
79
|
requirements: []
|
|
79
|
-
rubygems_version:
|
|
80
|
+
rubygems_version: 3.6.2
|
|
80
81
|
specification_version: 4
|
|
81
82
|
summary: Documentation automation for Ruby gems
|
|
82
83
|
test_files: []
|