gem_docs 0.2.0 → 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 +5 -2
- data/lib/gem_docs/doctor.rb +175 -0
- data/lib/gem_docs/emacs.rb +130 -20
- data/lib/gem_docs/header.rb +11 -11
- data/lib/gem_docs/overview.rb +13 -15
- data/lib/gem_docs/repo.rb +6 -3
- data/lib/gem_docs/skeleton.rb +51 -6
- data/lib/gem_docs/tasks.rb +50 -15
- data/lib/gem_docs/version.rb +1 -1
- data/lib/gem_docs/yard.rb +4 -4
- data/lib/gem_docs.rb +27 -4
- 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,18 @@ 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"
|
|
25
|
+
#+PROPERTY: header-args:ruby+ :ruby "bundle exec irb"
|
|
23
26
|
#+PROPERTY: header-args:sh :exports code :eval no
|
|
24
27
|
#+PROPERTY: header-args:bash :exports code :eval no
|
|
25
28
|
HEADER
|
|
@@ -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,39 +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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
65
|
+
|
|
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."
|
|
69
|
+
end
|
|
31
70
|
|
|
32
|
-
|
|
71
|
+
def self.export_changelog
|
|
72
|
+
export(CHANGELOG_ORG)
|
|
33
73
|
end
|
|
34
74
|
|
|
35
75
|
def self.session_name
|
|
36
76
|
"*#{Repo.from_gemspec.name}_session*"
|
|
37
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
|
|
38
148
|
end
|
|
39
149
|
end
|
data/lib/gem_docs/header.rb
CHANGED
|
@@ -18,6 +18,17 @@ module GemDocs
|
|
|
18
18
|
prelim.any? { |h| h.match?(PROPERTY_RE) }
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
def self.org_headers
|
|
22
|
+
repo = Repo.from_gemspec
|
|
23
|
+
GemDocs.config.headers
|
|
24
|
+
.gsub('%n', repo.name)
|
|
25
|
+
.gsub('%h', repo.host)
|
|
26
|
+
.gsub('%u', repo.user)
|
|
27
|
+
.gsub('%r', repo.root)
|
|
28
|
+
.gsub('%b', repo.branch)
|
|
29
|
+
.gsub('%w', repo.workflow)
|
|
30
|
+
end
|
|
31
|
+
|
|
21
32
|
class << self
|
|
22
33
|
private
|
|
23
34
|
|
|
@@ -38,17 +49,6 @@ module GemDocs
|
|
|
38
49
|
end
|
|
39
50
|
[prelim, body]
|
|
40
51
|
end
|
|
41
|
-
|
|
42
|
-
def org_headers
|
|
43
|
-
repo = Repo.from_gemspec
|
|
44
|
-
GemDocs.config.headers
|
|
45
|
-
.gsub('%n', repo.name)
|
|
46
|
-
.gsub('%h', repo.host)
|
|
47
|
-
.gsub('%u', repo.user)
|
|
48
|
-
.gsub('%r', repo.root)
|
|
49
|
-
.gsub('%b', repo.branch)
|
|
50
|
-
.gsub('%w', repo.workflow)
|
|
51
|
-
end
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
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,15 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module GemDocs
|
|
4
4
|
module Skeleton
|
|
5
|
-
|
|
5
|
+
VERSION_BLOCK_RE = /^# gem_docs:version begin\n.*?^# gem_docs:version end\n?/m
|
|
6
6
|
|
|
7
|
-
#
|
|
7
|
+
# Create the README or refresh its managed gemspec version block.
|
|
8
|
+
# @return [Boolean] whether the README changed
|
|
8
9
|
def self.make_readme?
|
|
9
|
-
return false if present?
|
|
10
|
-
|
|
11
10
|
repo = Repo.from_gemspec
|
|
11
|
+
return write_version?(File.read(README_ORG), repo.version) if readme_present?
|
|
12
|
+
|
|
12
13
|
content = <<~SKEL
|
|
13
14
|
#+TITLE: #{repo.module_name} Guide
|
|
15
|
+
#{Header.org_headers.strip}
|
|
14
16
|
|
|
15
17
|
* Introduction
|
|
16
18
|
|
|
@@ -44,11 +46,54 @@ module GemDocs
|
|
|
44
46
|
The gem is available as open source under the terms of the [MIT
|
|
45
47
|
License](https://opensource.org/licenses/MIT).
|
|
46
48
|
SKEL
|
|
47
|
-
|
|
49
|
+
write_version?(content, repo.version)
|
|
48
50
|
end
|
|
49
51
|
|
|
50
|
-
def self.
|
|
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
|
|
69
|
+
end
|
|
70
|
+
private_class_method :write_version?
|
|
71
|
+
|
|
72
|
+
def self.readme_present?
|
|
51
73
|
File.exist?(README_ORG)
|
|
52
74
|
end
|
|
75
|
+
|
|
76
|
+
def self.make_changelog?
|
|
77
|
+
return false if changelog_present?
|
|
78
|
+
|
|
79
|
+
content = <<~SKEL
|
|
80
|
+
* COMMENT CHANGELOG tips:
|
|
81
|
+
1. Don't dump your git change logs into this file, write them yourself.
|
|
82
|
+
2. Keep entries short and user-focused,
|
|
83
|
+
3. Use non-technical language, but do speak in the vocabulary of your gem.
|
|
84
|
+
4. Don't document changes only of interest to the programmers, just those the
|
|
85
|
+
user would find useful.
|
|
86
|
+
5. Give each heading a version number and an inactive date (C-c ! is useful here).
|
|
87
|
+
|
|
88
|
+
* Version 0.3.0 [2025-12-27 Sat]
|
|
89
|
+
- First change
|
|
90
|
+
- Second change
|
|
91
|
+
SKEL
|
|
92
|
+
File.write(CHANGELOG_ORG, content) > 0
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.changelog_present?
|
|
96
|
+
File.exist?(CHANGELOG_ORG)
|
|
97
|
+
end
|
|
53
98
|
end
|
|
54
99
|
end
|
data/lib/gem_docs/tasks.rb
CHANGED
|
@@ -4,43 +4,67 @@ module GemDocs
|
|
|
4
4
|
def self.install
|
|
5
5
|
extend Rake::DSL
|
|
6
6
|
|
|
7
|
-
# README.org → README.md when README.org is newer
|
|
8
7
|
file README_MD => README_ORG do
|
|
9
8
|
print "Exporting \"#{README_ORG}\" → "
|
|
10
|
-
GemDocs::Emacs.
|
|
9
|
+
GemDocs::Emacs.export_readme
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
file CHANGELOG_MD => CHANGELOG_ORG do
|
|
13
|
+
print "Exporting \"#{CHANGELOG_ORG}\" → "
|
|
14
|
+
GemDocs::Emacs.export_changelog
|
|
11
15
|
end
|
|
12
16
|
|
|
13
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
|
+
|
|
14
27
|
desc "Evaluate code blocks in README.org"
|
|
15
|
-
task :tangle => [:skeleton] do
|
|
28
|
+
task :tangle => ["docs:skeleton:readme"] do
|
|
16
29
|
print "Executing code blocks in #{README_ORG} ... "
|
|
17
30
|
GemDocs::Emacs.tangle
|
|
18
31
|
end
|
|
19
32
|
|
|
20
33
|
desc "Export README.org → README.md"
|
|
21
|
-
task :export => [:badge, README_MD]
|
|
34
|
+
task :export => [:check_readme, :badge, README_MD, CHANGELOG_MD]
|
|
22
35
|
|
|
23
36
|
desc "Extract overview from README.org and embed in lib/<gem>.rb for ri/yard"
|
|
24
|
-
task :overview => [:skeleton, README_ORG] do
|
|
37
|
+
task :overview => ["docs:skeleton:readme", README_ORG] do
|
|
25
38
|
print "Embedding overview extracted from #{GemDocs::README_ORG} into main gem file ... "
|
|
26
39
|
if GemDocs::Overview.write_overview?
|
|
27
40
|
puts "added"
|
|
28
41
|
else
|
|
29
|
-
puts "already
|
|
42
|
+
puts "unchanged (already current or no matching overview)"
|
|
30
43
|
end
|
|
31
44
|
end
|
|
32
45
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
namespace :skeleton do
|
|
47
|
+
desc "Create README.org if missing and refresh its gem version"
|
|
48
|
+
task :readme do
|
|
49
|
+
if GemDocs::Skeleton.make_readme?
|
|
50
|
+
puts "README.org created or version updated"
|
|
51
|
+
else
|
|
52
|
+
puts "README.org already up to date"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
desc "Create skeleton CHANGELOG.org if one does not exist"
|
|
57
|
+
task :changelog do
|
|
58
|
+
if GemDocs::Skeleton.make_changelog?
|
|
59
|
+
puts "CHANGELOG.org added"
|
|
60
|
+
else
|
|
61
|
+
puts "CHANGELOG.org already present"
|
|
62
|
+
end
|
|
39
63
|
end
|
|
40
64
|
end
|
|
41
65
|
|
|
42
66
|
desc "Insert #+PROPERTY headers at top of README.org for code blocks"
|
|
43
|
-
task :
|
|
67
|
+
task :headers => "docs:skeleton:readme" do
|
|
44
68
|
print "Inserting headers ... "
|
|
45
69
|
if GemDocs::Header.write_header?
|
|
46
70
|
puts "added"
|
|
@@ -49,6 +73,8 @@ module GemDocs
|
|
|
49
73
|
end
|
|
50
74
|
end
|
|
51
75
|
|
|
76
|
+
task :header => :headers
|
|
77
|
+
|
|
52
78
|
desc "Generate YARD HTML documentation"
|
|
53
79
|
task :yard => [:overview] do
|
|
54
80
|
puts "Generating YARD documentation ... "
|
|
@@ -56,7 +82,7 @@ module GemDocs
|
|
|
56
82
|
end
|
|
57
83
|
|
|
58
84
|
desc "Ensure GitHub Actions badge exists in README.org"
|
|
59
|
-
task :badge => :skeleton do
|
|
85
|
+
task :badge => "docs:skeleton:readme" do
|
|
60
86
|
print "Ensuring badges are in README.org ... "
|
|
61
87
|
if GemDocs::Badge.ensure!
|
|
62
88
|
puts "added"
|
|
@@ -66,7 +92,16 @@ module GemDocs
|
|
|
66
92
|
end
|
|
67
93
|
|
|
68
94
|
desc "Run all documentation tasks (examples, readme, overview, yard, ri)"
|
|
69
|
-
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
|
+
]
|
|
70
105
|
end
|
|
71
106
|
end
|
|
72
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
|
@@ -37,6 +37,7 @@ require "fileutils"
|
|
|
37
37
|
# of the library,
|
|
38
38
|
# - running the code block examples in a ~README.org~ by invoking ~emacsclient~,
|
|
39
39
|
# - exporting ~README.org~ to Git-flavored markdown in ~README.md~
|
|
40
|
+
# - exporting ~CHANGELOG.org~ to Git-flavored markdown in ~CHANGELOG.md~
|
|
40
41
|
# - ensuring a workflow or ci badge is present in the ~README.md~
|
|
41
42
|
# - generating yard documents for your repo, and
|
|
42
43
|
# - copying the introductory contents of the README as a leading comment in your
|
|
@@ -49,7 +50,16 @@ require "fileutils"
|
|
|
49
50
|
# detains filled in. If there is already a README.org file, it does nothing.
|
|
50
51
|
#
|
|
51
52
|
# #+begin_src ruby :eval no
|
|
52
|
-
# rake docs:skeleton
|
|
53
|
+
# rake docs:skeleton:readme
|
|
54
|
+
# #+end_src
|
|
55
|
+
#
|
|
56
|
+
# ** Create a skeleton CHANGELOG.org file
|
|
57
|
+
# This is a simple task that creates a bare-bones ~CHANGELOG.org~ file to get
|
|
58
|
+
# started with. It only contains some tips for writing a change log and a
|
|
59
|
+
# sample heading. If there is already a CHANGELOG.org file, it does nothing.
|
|
60
|
+
#
|
|
61
|
+
# #+begin_src ruby :eval no
|
|
62
|
+
# rake docs:skeleton:changelog
|
|
53
63
|
# #+end_src
|
|
54
64
|
#
|
|
55
65
|
# ** Add proper ~#+PROPERTY~ headers in ~README.org~: ~rake docs:headers~
|
|
@@ -208,7 +218,7 @@ require "fileutils"
|
|
|
208
218
|
# If there is already a badge present, the task will not modify the ~README.org~
|
|
209
219
|
# file.
|
|
210
220
|
#
|
|
211
|
-
# ** Export ~README.org~
|
|
221
|
+
# ** Export ~README.org~ and ~CHANGELOG.org~ to Markdown: ~rake docs:export~
|
|
212
222
|
# You can write the ~README~ in Emacs org-mode, using all its features
|
|
213
223
|
# including the execution of code blocks, and then export to git-flavored
|
|
214
224
|
# markdown.
|
|
@@ -229,6 +239,10 @@ require "fileutils"
|
|
|
229
239
|
# you want to have one for your own purposes, just set the ~:noexport~ tag on it
|
|
230
240
|
# so it doesn't get put into the ~README.md~
|
|
231
241
|
#
|
|
242
|
+
# Less important, but still handy, you can also write the CHANGELOG in org mode
|
|
243
|
+
# and this task will convert it to markdown for display on github and
|
|
244
|
+
# rubygems.org.
|
|
245
|
+
#
|
|
232
246
|
# #+begin_src ruby :eval no
|
|
233
247
|
# rake docs:export
|
|
234
248
|
# #+end_src
|
|
@@ -271,15 +285,24 @@ module GemDocs
|
|
|
271
285
|
require_relative "gem_docs/badge"
|
|
272
286
|
require_relative "gem_docs/header"
|
|
273
287
|
require_relative "gem_docs/skeleton"
|
|
288
|
+
require_relative "gem_docs/doctor"
|
|
274
289
|
require_relative "gem_docs/tasks"
|
|
275
290
|
|
|
276
291
|
README_ORG = "README.org"
|
|
277
292
|
README_MD = "README.md"
|
|
293
|
+
CHANGELOG_ORG = "CHANGELOG.org"
|
|
294
|
+
CHANGELOG_MD = "CHANGELOG.md"
|
|
278
295
|
|
|
279
296
|
# Auto-detect project root (handles being run from subdirs)
|
|
280
297
|
def self.project_root
|
|
281
298
|
here = Dir.pwd
|
|
282
|
-
|
|
283
|
-
|
|
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
|
|
284
307
|
end
|
|
285
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: 3.6.
|
|
80
|
+
rubygems_version: 3.6.2
|
|
80
81
|
specification_version: 4
|
|
81
82
|
summary: Documentation automation for Ruby gems
|
|
82
83
|
test_files: []
|