gem_docs 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a07d76db2fbb75383d41735198a4cbf7b1d2b6515653df77e21e8c9575a40ade
4
+ data.tar.gz: 65e3554324e7c91e8466efba2d032c53d8549e64a7a460cce4ee4d7322fffa40
5
+ SHA512:
6
+ metadata.gz: 0d9090c6ac05d0d80fb556019bb56c57fbcdd3e1b691e1b38d5b54da8bca5923c00dae9a6e18f616bad189b3c61c981f5d341294c0ccb46e1afde7274569452c
7
+ data.tar.gz: 86a7c664a77405127a46e03a2233cc20958613965ca6bd3beba2a0ca1ae342192f835a26d22d18ad6f045a6d9f79eb13934942fe0f4baab4a11d17adbc9400dd
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GemDocs
4
+ module Badge
5
+ GITHUB_BADGE_RE = %r{actions/workflows.*badge.svg}
6
+ GITLAB_BADGE_RE = %r{badges/.*pipeline.svg}
7
+
8
+ Badge = Struct.new(:name, :marker, :org_block, keyword_init: true)
9
+
10
+ def self.ensure!
11
+ repo = Repo.from_gemspec
12
+ return false if repo.workflow.to_s.match?(/\A\s*\z/)
13
+
14
+ badge = make_badge(repo)
15
+ ensure_badge!(badge, repo)
16
+ end
17
+
18
+ class << self
19
+ private
20
+
21
+ def make_badge(repo)
22
+ repo = Repo.from_gemspec
23
+ org_block =
24
+ GemDocs.config.badge
25
+ .gsub('%n', repo.name)
26
+ .gsub('%h', repo.host)
27
+ .gsub('%u', repo.user)
28
+ .gsub('%r', repo.root)
29
+ .gsub('%b', repo.branch)
30
+ .gsub('%w', repo.workflow)
31
+ Badge.new(
32
+ name: 'GitHub Actions',
33
+ marker: '#badge',
34
+ org_block: org_block,
35
+ )
36
+ end
37
+
38
+ # Write the badge block to the README unless it's already there. Replace
39
+ # the #badge marker if present, otherwise add after TITLE.
40
+ def ensure_badge!(badge, repo)
41
+ content = File.read(README_ORG)
42
+ updated =
43
+ if content.lines.find { |l| l.match?(/\A\s*#{Regexp.quote(badge.marker)}/) }
44
+ insert_at_marker(badge.marker, content, badge.org_block)
45
+ elsif (repo.host.include?('github') && content.match?(GITHUB_BADGE_RE)) ||
46
+ (repo.host.include?('gitlab') && content.match?(GITLAB_BADGE_RE))
47
+ # Do nothing and return nil to indicate badge present
48
+ return
49
+ else
50
+ insert_after_header(content, badge.org_block)
51
+ end
52
+
53
+ File.write(README_ORG, updated)
54
+ end
55
+
56
+ # Insert the badge block after the org header lines, if any. If there are
57
+ # no header lines, insert at the beginning of the file.
58
+ def insert_after_header(content, block)
59
+ lines = content.lines
60
+ out_lines = +''
61
+
62
+ in_header = lines.any? { |l| l.match?(/\A\s*\#\+/) }
63
+ block_added = false
64
+ lines.each do |line|
65
+ out_lines <<
66
+ if in_header && line.match?(/\A\s*\#\+/)
67
+ line
68
+ elsif in_header && !line.match?(/\A\s*\#\+/)
69
+ in_header = false
70
+ block_added = true
71
+ if line.match?(/\A\s*\z/)
72
+ line + block + "\n\n"
73
+ else
74
+ "\n" + block + "\n\n" + line
75
+ end
76
+ elsif !in_header && !block_added
77
+ block_added = true
78
+ block + "\n\n" + line
79
+ else
80
+ line
81
+ end
82
+ end
83
+ out_lines
84
+ end
85
+
86
+ def insert_at_marker(marker, content, block)
87
+ lines = content.lines
88
+ out_lines = +''
89
+
90
+ lines.each do |line|
91
+ out_lines <<
92
+ if line.match?(/^#{marker}/)
93
+ block
94
+ else
95
+ line
96
+ end
97
+ end
98
+ out_lines
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GemDocs
4
+ class Config
5
+ attr_accessor :overview_headings
6
+ attr_accessor :headers
7
+ attr_accessor :repo_host
8
+ attr_accessor :repo_name
9
+ attr_accessor :repo_user
10
+ attr_accessor :repo_branch
11
+ attr_accessor :repo_workflow_dir
12
+ attr_accessor :repo_workflow_name
13
+ attr_accessor :badge
14
+
15
+ def initialize
16
+ # Default: support org comment markers
17
+ @overview_headings = ["Introduction"]
18
+ @headers =
19
+ <<~HEADER
20
+ #+PROPERTY: header-args:ruby :results value :colnames no :hlines yes :exports both :dir "./"
21
+ #+PROPERTY: header-args:ruby+ :wrap example :session %n_session :eval yes
22
+ #+PROPERTY: header-args:ruby+ :prologue "$:.unshift('./lib') unless $:.first == './lib'; require '%n'"
23
+ #+PROPERTY: header-args:sh :exports code :eval no
24
+ #+PROPERTY: header-args:bash :exports code :eval no
25
+ HEADER
26
+ @repo_host = nil
27
+ @repo_name = nil
28
+ @repo_user = nil
29
+ @repo_branch = nil
30
+ @repo_workflow_dir = ".github/workflows"
31
+ @repo_workflow_name = nil
32
+ @badge =
33
+ <<~BADGE
34
+ #+BEGIN_EXPORT markdown
35
+ [![CI](https://github.com/%u/%n/actions/workflows/%w/badge.svg?branch=%b)](https://github.com/%u/%n/actions/workflows/%w)
36
+ #+END_EXPORT
37
+ BADGE
38
+ end
39
+ end
40
+
41
+ def self.configure
42
+ yield(config)
43
+ end
44
+
45
+ def self.config
46
+ @config ||= Config.new
47
+ end
48
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GemDocs
4
+ module Emacs
5
+ def self.tangle
6
+ # ensure_saved
7
+ expr = <<~ELISP
8
+ (save-window-excursion
9
+ (with-current-buffer (find-file-noselect "#{README_ORG}")
10
+ (save-buffer)
11
+ (require 'ob-ruby)
12
+ (org-babel-execute-buffer)
13
+ (save-buffer)
14
+ "OK"))
15
+ ELISP
16
+
17
+ if system("emacsclient", "--quiet", "--eval", expr)
18
+ FileUtils.touch(STAMP)
19
+ else
20
+ abort "Babel execution failed"
21
+ end
22
+ end
23
+
24
+ def self.export
25
+ # ensure_saved
26
+ expr = <<~ELISP
27
+ (save-window-excursion
28
+ (with-current-buffer (find-file-noselect "#{README_ORG}")
29
+ (save-buffer)
30
+ (require 'ox-gfm)
31
+ (org-gfm-export-to-markdown)))
32
+ ELISP
33
+
34
+ system("emacsclient", "--quiet", "--eval", expr)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GemDocs
4
+ module Header
5
+ PROPERTY_RE = /^#\+PROPERTY:\s+header-args:ruby/
6
+
7
+ # @return String The overview from README per config
8
+ def self.write_header?
9
+ return false if present?
10
+
11
+ prelim, body = extract_prelim_body
12
+ new_org = prelim.join.strip + org_headers.strip + "\n\n" + body.join
13
+ File.write(README_ORG, new_org) > 0
14
+ end
15
+
16
+ def self.present?
17
+ prelim = extract_prelim_body.first
18
+ prelim.any? { |h| h.match?(PROPERTY_RE) }
19
+ end
20
+
21
+ class << self
22
+ private
23
+
24
+ # Returns the preliminary comment, blank, and header lines from README.org
25
+ def extract_prelim_body
26
+ prelim = []
27
+ body = []
28
+ in_prelim = true
29
+ File.read(README_ORG).lines.each do |line|
30
+ if in_prelim && line.match?(/^\s*[^#\n]+\s*$/)
31
+ in_prelim = false
32
+ body << line
33
+ elsif in_prelim && (line.match(/^#/) || line.match(/^\s*$/))
34
+ prelim << line
35
+ # elsif in_prelim
36
+ # in_prelim = false
37
+ # body << line
38
+ else
39
+ body << line
40
+ end
41
+ end
42
+ [prelim, body]
43
+ end
44
+
45
+ def org_headers
46
+ repo = Repo.from_gemspec
47
+ GemDocs.config.headers
48
+ .gsub('%n', repo.name)
49
+ .gsub('%h', repo.host)
50
+ .gsub('%u', repo.user)
51
+ .gsub('%r', repo.root)
52
+ .gsub('%b', repo.branch)
53
+ .gsub('%w', repo.workflow)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'strscan'
4
+ require 'debug'
5
+
6
+ module GemDocs
7
+ module Overview
8
+ BANNER = 'Gem Overview (extracted from README.org by gem_docs)'
9
+
10
+ # @return String The overview from README per config
11
+ def self.write_overview?
12
+ repo = Repo.from_gemspec
13
+ target = File.join("lib", "#{repo.name}.rb")
14
+
15
+ return false unless File.exist?(target)
16
+
17
+ overview = extract
18
+ return false unless overview
19
+
20
+ old_lib = File.read(target)
21
+ old_match = old_lib.match(overview_re)
22
+ old_comment = old_match.to_s
23
+
24
+ new_comment = <<~RUBY.chomp
25
+ # #{BANNER}
26
+ #
27
+ #{overview.lines.map { |l| l.match?(/\A\s*\z/) ? '#' : "# #{l.rstrip}" }.join("\n")}
28
+ RUBY
29
+ return false if old_comment == new_comment
30
+
31
+ new_lib =
32
+ if old_match
33
+ # There was an overview in the old_lib_content, so replace it with
34
+ # the new_lib_comment.
35
+ old_lib.sub(old_comment, new_comment)
36
+ else
37
+ scanner = StringScanner.new(old_lib)
38
+ scanner.scan_until(/^module #{repo.module_name}/)
39
+ scanner.pre_match + "\n" + new_comment + "\n" + scanner.matched + scanner.rest
40
+ end
41
+ File.write(target, new_lib) > 0
42
+ end
43
+
44
+ # Return a Regexp that capture any GemDocs-generated overview comment in
45
+ # the main module library file. The Regex requires the comment to come
46
+ # immediately before the `module <GemName>` line of the libary file, or it
47
+ # will not match.
48
+ def self.overview_re
49
+ heads = GemDocs.config.overview_headings
50
+ re_str = "#\\s*" + Regexp.quote(BANNER) + ".*"
51
+ heads.each do |h|
52
+ re_str << "\\*\\s+#{Regexp.escape(h)}.*"
53
+ end
54
+ repo = Repo.from_gemspec
55
+ re_str += "(?=\\n\\s*module\\s+#{repo.module_name})"
56
+ Regexp.new(re_str, Regexp::MULTILINE | Regexp::IGNORECASE)
57
+ end
58
+
59
+ def self.present?
60
+ repo = Repo.from_gemspec
61
+ target = File.join("lib", "#{repo.name}.rb")
62
+
63
+ return false unless File.exist?(target)
64
+
65
+ File.read(target).match?(overview_re)
66
+ end
67
+
68
+ class << self
69
+ private
70
+
71
+ # Extract the Overview from the concatenation of all the README.org
72
+ # top-level sections given in GemDocs.config.overview_headings.
73
+ def extract
74
+ text = File.read(README_ORG)
75
+ heads = GemDocs.config.overview_headings
76
+ return if heads.nil? || heads.empty?
77
+
78
+ result = +''
79
+ scanner = StringScanner.new(text)
80
+ heads.each do |h|
81
+ if scanner.scan_until(/\n(?<head>\s*\*\s+#{Regexp.escape(h)})[^\n]*\n/)
82
+ this_head = scanner.named_captures['head'] + "\n\n"
83
+ body_start = scanner.pos
84
+ body_end =
85
+ if scanner.scan_until(/\n^\s*\*[^\*\n]+/)
86
+ scanner.pos - scanner.matched.size
87
+ else
88
+ scanner.string.size - 1
89
+ end
90
+ scanner.pos = body_end
91
+ result << this_head + scanner.string[body_start..body_end]
92
+ end
93
+ end
94
+ result
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GemDocs
4
+ class Repo
5
+ attr_accessor :root, :host, :user, :name, :module_name
6
+ attr_accessor :branch, :workflow_dir, :workflow_name
7
+
8
+ def initialize(root: nil, host: nil,
9
+ user: nil,
10
+ name: nil,
11
+ module_name: nil,
12
+ branch: 'master',
13
+ workflow_dir: nil,
14
+ workflow_name: nil)
15
+ @root = root
16
+ @host = host
17
+ @user = user
18
+ @module_name = module_name
19
+ @name = name
20
+ @branch = branch
21
+ @workflow_dir = workflow_dir
22
+ @workflow_name = workflow_name
23
+ end
24
+
25
+ def workflow
26
+ workflow_name.to_s
27
+ end
28
+
29
+ class << self
30
+ def from_gemspec
31
+ spec = load_gemspec(gemspec_path)
32
+
33
+ url =
34
+ spec.metadata["source_code_uri"] ||
35
+ spec.metadata["homepage_uri"] ||
36
+ spec.homepage
37
+
38
+ abort "No repository URL found in gemspec metadata" unless url
39
+
40
+ # root = File.dirname(File.expand_path(path))
41
+ root = GemDocs.project_root
42
+ meta = parse_url(url)
43
+ name = GemDocs.config.repo_name ||
44
+ spec.name ||
45
+ meta[:name] ||
46
+ File.basename(Dir['*.gemspec'].first) ||
47
+ File.basename(root)
48
+ host = GemDocs.config.repo_host ||
49
+ meta[:host]
50
+ user = GemDocs.config.repo_user ||
51
+ meta[:user]
52
+ mname = to_module(name)
53
+ branch = GemDocs.config.repo_branch ||
54
+ repo_default_branch(root:)
55
+ wdir, wname = workflow_dir_name
56
+ new(
57
+ root: root,
58
+ host: host,
59
+ user: user,
60
+ name: name,
61
+ module_name: mname,
62
+ branch: branch,
63
+ workflow_dir: wdir,
64
+ workflow_name: wname,
65
+ )
66
+ end
67
+
68
+ def workflow_dir_name
69
+ if GemDocs.config.repo_workflow_name && GemDocs.config.repo_workflow_dir
70
+ workflow_file = File.join(
71
+ GemDocs.project_root,
72
+ GemDocs.config.repo_workflow_dir,
73
+ GemDocs.config.repo_workflow_name,
74
+ )
75
+ return [File.dirname(workflow_file), File.dirname(workflow_file)] if File.readable?(workflow_file)
76
+ end
77
+
78
+ dir = File.join(GemDocs.project_root, ".github/workflows")
79
+ return unless Dir.exist?(dir)
80
+
81
+ workflows =
82
+ Dir.children(dir)
83
+ .select { |f| f.match?(/\A.+\.ya?ml\z/) }
84
+ .sort
85
+ return if workflows.empty?
86
+
87
+ fname = workflows.find { |f| f =~ /\A[A-Za-z][^\.]*\.ya?ml\z/i } || workflows.first
88
+ # File.join(dir, fname)
89
+ [dir, fname]
90
+ end
91
+
92
+ def to_module(name)
93
+ name.split(/[-_]/).map(&:capitalize).join('')
94
+ end
95
+
96
+ private
97
+
98
+ def repo_default_branch(root:)
99
+ return "master" unless git_repo?(root:)
100
+
101
+ default_branch_from_origin ||
102
+ fallback_branch ||
103
+ "master"
104
+ end
105
+
106
+ def fallback_branch
107
+ return unless git_available?
108
+
109
+ branches = %x[git branch --list 2>/dev/null]
110
+ .lines
111
+ .map { |l| l.sub("*", "").strip }
112
+
113
+ return "master" if branches.include?("master")
114
+ return "main" if branches.include?("main")
115
+
116
+ nil
117
+ end
118
+
119
+ def default_branch_from_origin
120
+ return unless git_available?
121
+
122
+ ref = %x[git symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null].strip
123
+ return if ref.empty?
124
+
125
+ ref.split("/").last
126
+ end
127
+
128
+ def git_available?
129
+ system("git", "--version", out: File::NULL, err: File::NULL)
130
+ end
131
+
132
+ def git_repo?(root: nil)
133
+ File.directory?(File.join(root, ".git"))
134
+ end
135
+
136
+ # Return {host: <git_host>, user: <user_name>, name: <repo_name> } by parsing the given url
137
+ def parse_url(url)
138
+ host_bases = ['github', 'gitlab']
139
+ md = url.match(%r{(?<host>(?:#{host_bases.join('|')})\.com)/(?<user>[^/]+)/(?<repo_name>[^/]+)(?:\.git)?/?})
140
+ abort "Unsupported repository URL: #{url}" unless md
141
+
142
+ { host: md[:host], user: md[:user], name: md[:repo_name] }
143
+ end
144
+
145
+ def gemspec_path
146
+ candidates = nil
147
+ Dir.chdir(GemDocs.project_root) do
148
+ candidates = Dir["*.gemspec"]
149
+ abort "No gemspec found" if candidates.empty?
150
+ abort "Multiple gemspecs found: #{candidates.join(', ')}" if candidates.size > 1
151
+ end
152
+ candidates.first
153
+ end
154
+
155
+ def load_gemspec(path)
156
+ Gem::Specification.load(path) ||
157
+ abort("Failed to load gemspec: #{path}")
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GemDocs
4
+ module Skeleton
5
+ PROPERTY_RE = /^#\+PROPERTY:\s+header-args:ruby/
6
+
7
+ # @return String The overview from README per config
8
+ def self.make_readme?
9
+ return false if present?
10
+
11
+ repo = Repo.from_gemspec
12
+ content = <<~SKEL
13
+ #+TITLE: #{repo.module_name} Guide
14
+
15
+ * Introduction
16
+
17
+ * Installation
18
+
19
+ #+begin_src sh :eval no
20
+ bundle add #{repo.name}
21
+ #+end_src
22
+
23
+ If bundler is not being used to manage dependencies, install the gem by executing:
24
+
25
+ #+begin_src sh :eval no
26
+ gem install #{repo.name}
27
+ #+end_src
28
+
29
+ * Usage
30
+
31
+ * Development
32
+ After checking out the repo, run `bin/setup` to install dependencies. Then,
33
+ run `rake spec` to run the tests. You can also run `bin/console` for an
34
+ interactive prompt that will allow you to experiment.
35
+
36
+ To install this gem onto your local machine, run `bundle exec rake
37
+ install`.
38
+
39
+ * Contributing
40
+ Bug reports and pull requests are welcome on GitHub at
41
+ https://github.com/#{repo.user}/#{repo.name}.
42
+
43
+ * License
44
+ The gem is available as open source under the terms of the [MIT
45
+ License](https://opensource.org/licenses/MIT).
46
+ SKEL
47
+ File.write(README_ORG, content) > 0
48
+ end
49
+
50
+ def self.present?
51
+ File.exist?(README_ORG)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GemDocs
4
+ extend Rake::DSL
5
+
6
+ STAMP = ".tangle-stamp"
7
+
8
+ def self.install
9
+ extend Rake::DSL
10
+
11
+ # README.org → README.md when README.org is newer
12
+ file README_MD => README_ORG do
13
+ print "Exporting \"#{README_ORG}\" → "
14
+ GemDocs::Emacs.export
15
+ end
16
+
17
+ # Evaluate code blocks only when README.org changes
18
+ file STAMP => README_ORG do
19
+ print "Executing code blocks in #{README_ORG} ... "
20
+ GemDocs::Emacs.tangle
21
+ FileUtils.touch(STAMP)
22
+ end
23
+
24
+ namespace :docs do
25
+ desc "Evaluate code blocks in README.org"
26
+ task :tangle => [:skeleton, STAMP]
27
+
28
+ desc "Export README.org → README.md"
29
+ task :export => [:badge, README_MD]
30
+
31
+ desc "Extract overview from README.org and embed in lib/<gem>.rb for ri/yard"
32
+ task :overview => [:skeleton, README_ORG] do
33
+ print "Embedding overview extracted from #{GemDocs::README_ORG} into main gem file ... "
34
+ if GemDocs::Overview.write_overview?
35
+ puts "added"
36
+ else
37
+ puts "already present"
38
+ end
39
+ end
40
+
41
+ desc "Create skeleton README.org if one does not exist"
42
+ task :skeleton do
43
+ if GemDocs::Skeleton.make_readme?
44
+ puts "README.org added"
45
+ else
46
+ puts "README.org already present"
47
+ end
48
+ end
49
+
50
+ desc "Insert #+PROPERTY headers at top of README.org for code blocks"
51
+ task :header => :skeleton do
52
+ print "Inserting headers ... "
53
+ if GemDocs::Header.write_header?
54
+ puts "added"
55
+ else
56
+ puts "already present"
57
+ end
58
+ end
59
+
60
+ desc "Generate YARD HTML documentation"
61
+ task :yard => [:overview] do
62
+ puts "Generating YARD documentation ... "
63
+ GemDocs::Yard.generate
64
+ end
65
+
66
+ desc "Ensure GitHub Actions badge exists in README.org"
67
+ task :badge => :skeleton do
68
+ print "Ensuring badges are in README.org ... "
69
+
70
+ if GemDocs::Badge.ensure!
71
+ puts "added"
72
+ else
73
+ puts "already present"
74
+ end
75
+ end
76
+
77
+ desc "Run all documentation tasks (examples, readme, overview, yard, ri)"
78
+ task :all => [:skeleton, :header, :tangle, :export, :overview, :yard]
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GemDocs
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GemDocs
4
+ module Yard
5
+ # Generate HTML documentation via YARD
6
+ def self.generate(supress_out: false)
7
+ write_yardopts
8
+ Dir.chdir(GemDocs.project_root) do
9
+ redirect = supress_out ? '>/dev/null 2>&1' : ''
10
+ unless system("yard doc --no-private #{redirect}")
11
+ abort "Failed to generate YARD documentation"
12
+ end
13
+ end
14
+ end
15
+
16
+ class << self
17
+ private
18
+
19
+ # Path to the .yardopts file in the project root
20
+ def yardopts_path
21
+ File.expand_path(".yardopts", GemDocs.project_root)
22
+ end
23
+
24
+ # Contents of .yardopts
25
+ def yardopts_contents
26
+ <<~YOPTS
27
+ --markup markdown
28
+ --output-dir doc
29
+ --readme README.md
30
+ lib/**/*.rb
31
+ YOPTS
32
+ end
33
+
34
+ # Write .yardopts only if needed
35
+ def write_yardopts
36
+ if !File.exist?(yardopts_path) || File.read(yardopts_path) != yardopts_contents
37
+ File.write(yardopts_path, yardopts_contents)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
data/lib/gem_docs.rb ADDED
@@ -0,0 +1,264 @@
1
+ # -*- mode: ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ # Gem Overview (auto-generated by gem_docs)
5
+ #
6
+ # * Usage
7
+
8
+ require "rake"
9
+ require "rake/dsl_definition"
10
+ require "fileutils"
11
+
12
+ # Gem Overview (extracted from README.org by gem_docs)
13
+ #
14
+ #
15
+ #
16
+ # * Overview
17
+ #
18
+ # One of the more onerous tasks when writing a ~gem~ or other ~github~ project
19
+ # is maintaining the documentation, and keeping it consistent and up-to-date.
20
+ # One of the better options for writing the ~README~ file that gets displayed by
21
+ # ~github~ as the main documentation is to write the file in ~org-mode~,
22
+ # ~README.org~ the export it to markdown ~README.md~ for display by ~github~.
23
+ #
24
+ # Doing so gives you access to ~org-mode~'s code blocks for writing example code
25
+ # to demonstrate the ~gem~ or other documents being presented. If you do so,
26
+ # ~github~ will render your ~README.org~ file in ~HTML~ and give you a credible
27
+ # result. However, ~github~ cannot handle some ~org-mode~ features, and in
28
+ # particular, it will not render ~#+RESULTS~ blocks showing the results of code
29
+ # block execution unless you wrap the results in something like a
30
+ # ~#+begin_example~ block and manually delete the ~#+RESULTS~ markers.
31
+ # Exporting to markdown eliminates that hassle.
32
+ #
33
+ # This gem contains ~rake~ tasks to facilitate the production of documentation
34
+ # in other gems.
35
+ #
36
+ # It provides tasks for:
37
+ #
38
+ # - ensuring that proper code block setup is included in ~README.org~ to
39
+ # facilitate running ~ruby~ code blocks in a session using the current version
40
+ # of the library,
41
+ # - running the code block examples in a ~README.org~ by invoking ~emacsclient~,
42
+ # - exporting ~README.org~ to Git-flavored markdown in ~README.md~
43
+ # - ensuring a workflow or ci badge is present in the ~README.md~
44
+ # - generating yard documents for your repo, and
45
+ # - copying the introductory contents of the README as a leading comment in your
46
+ # main gem library file so it gets picked up as an overview for ~ri~ and ~yri~
47
+ #
48
+ # * Usage
49
+ #
50
+ # ** Create a skeleton README.org file
51
+ # This is a simple task that creates a bare-bones ~README.org~ file to get
52
+ # started with. It does create the file with the name of the gem and other repo
53
+ # detains filled in. If there is already a README.org file, it does nothing.
54
+ #
55
+ # #+begin_src ruby :eval no
56
+ # rake docs:skeleton
57
+ # #+end_src
58
+ #
59
+ # ** Add proper ~#+PROPERTY~ headers in ~README.org~: ~rake docs:headers~
60
+ # **
61
+ # Getting emacs code blocks to render well in your ~README.org~ takes proper
62
+ # configuration of the code block headers in Emacs.
63
+ #
64
+ # #+begin_src ruby :eval no
65
+ # rake docs:headers
66
+ # #+end_src
67
+ #
68
+ # By default, the ~gem_docs~ ~rake docs:headers~ task will add the following
69
+ # headers to the top of your ~README.org~ file. It does nothing if any ruby
70
+ # header args are already present, so remove them if you want these installed.
71
+ #
72
+ # #+begin_example
73
+ # #+PROPERTY: header-args:ruby :results value :colnames no :hlines yes :exports both :dir "./"
74
+ # #+PROPERTY: header-args:ruby+ :wrap example :session gem_docs_session
75
+ # #+PROPERTY: header-args:ruby+ :prologue "$:.unshift('./lib') unless $:.first == './lib'; require '%n'"
76
+ # #+PROPERTY: header-args:sh :exports code :eval no
77
+ # #+PROPERTY: header-args:bash :exports code :eval no
78
+ # #+end_example
79
+ #
80
+ # Here's what the ruby headers buy you:
81
+ # - ~:results value~ :: the value of the last expression in the block is rendered
82
+ # as the results of code execution. If you want the output instead for a
83
+ # particular block, just add the block argument ~:results output~ to the code
84
+ # block.
85
+ # - ~:colnames no~ :: prevents org from processing the column headers in tables
86
+ # it renders. It is better for you to control column headers, and this
87
+ # setting allows this.
88
+ # - ~:hlines yes~ :: prevents org from stripping hlines from tables, which also
89
+ # allows you to control the insertion of hlines in tables.
90
+ # - ~:exports both~ :: causes both your code and the results of evaluation to be
91
+ # exported to the ~README.md~. Your example blocks should demonstrate the use
92
+ # of the gem, and the reader will want to see both the code and its results.
93
+ # - ~:dir "./"~ :: causes each code block to execute with your gem's root
94
+ # directory as its current directory.
95
+ # - ~:wrap example~ :: this wraps the result of code block evaluation in
96
+ # ~#+begin_example~ / ~#+end_example~ so that the results are displayed
97
+ # literally.
98
+ # - ~:session gem_docs_session~ :: causes the code blocks to execute in a
99
+ # continuous session, so that variables set up in one code block are
100
+ # accessible in later code blocks. Without this, you would have to build the
101
+ # code environment anew with each code block which obscures the readability of
102
+ # your ~README~. The session name is set to '<gem_name>_session'
103
+ # automatically, where <gem_name> is the name of your gem.
104
+ # - ~:prologue "$:.unshift('./lib') unless $:.first == './lib'; require 'gem_name'"~
105
+ # :: this prologue gets executed before each code block execution and ensures
106
+ # that the version of the gem library is you current development version;
107
+ # otherwise, your code blocks could be running a version from a prior
108
+ # installation of the gem. The 'gem_name' in the require is set to the name
109
+ # of your gem automatically.
110
+ #
111
+ # The ~docs:headers~ task also turns off evaluation of shell code blocks since
112
+ # these will often be such things as demonstrating the shell commands to install
113
+ # the gem, etc. Of course, you can override this for particular code blocks.
114
+ #
115
+ # Those headers are in fact what I am using in this ~README~, and here is how
116
+ # they work:
117
+ #
118
+ # #+begin_src ruby
119
+ # result = []
120
+ # result << ['N', 'exp(N)']
121
+ # result << nil
122
+ # 0.upto(10) do |n|
123
+ # result << [n/3.0, Math.exp(n/3.0)]
124
+ # end
125
+ # result
126
+ # #+end_src
127
+ #
128
+ # #+RESULTS:
129
+ # #+begin_example
130
+ # | N | exp(N) |
131
+ # |--------------------+--------------------|
132
+ # | 0.0 | 1.0 |
133
+ # | 0.3333333333333333 | 1.3956124250860895 |
134
+ # | 0.6666666666666666 | 1.9477340410546757 |
135
+ # | 1.0 | 2.718281828459045 |
136
+ # | 1.3333333333333333 | 3.7936678946831774 |
137
+ # | 1.6666666666666667 | 5.29449005047003 |
138
+ # | 2.0 | 7.38905609893065 |
139
+ # | 2.3333333333333335 | 10.312258501325767 |
140
+ # | 2.6666666666666665 | 14.391916095149892 |
141
+ # | 3.0 | 20.085536923187668 |
142
+ # | 3.3333333333333335 | 28.03162489452614 |
143
+ # #+end_example
144
+ #
145
+ # I built the table in the output by returning an array of arrays, which
146
+ # org-mode renders as a table in the output. Notice that I added an hline to
147
+ # the output by simply adding ~nil~ to the outer array where I wanted the hline
148
+ # to occur.
149
+ #
150
+ # Apart from all the convenient markup that ~org-mode~ allows, the ability to
151
+ # easily demonstrate your gem's code in this way is the real killer feature of
152
+ # writing your ~README~ in ~org-mode~ then exporting to markdown.
153
+ #
154
+ # ** Run the Code Blocks in README.org: ~rake docs:tangle~
155
+ # You can invoke ~emacsclient~ to run all the example code blocks in your
156
+ # ~README.org~ that are set for evaluation:
157
+ #
158
+ # Note that the ~tangle~ task relies on ~emacsclient~ to evaluate the code blocks in
159
+ # ~README.org~, so your Emacs ~init~ files should start [[info:emacs#Emacs Server][the Emacs server]] in
160
+ # order to work properly.
161
+ #
162
+ # I use the following snippet in my Emacs init file:
163
+ #
164
+ # #+begin_src emacs-lisp :eval no
165
+ # (require 'server)
166
+ # (unless (server-running-p)
167
+ # (message "Starting Emacs server")
168
+ # (server-start))
169
+ # #+end_src
170
+ #
171
+ # Then, you can evaluate all the code blocks in your ~README.org~ like this:
172
+ #
173
+ # #+begin_src ruby :eval no
174
+ # rake docs:tangle
175
+ # #+end_src
176
+ #
177
+ # ** Ensure that a Badge is Present in ~README.md~: ~rake docs:badge~
178
+ # It is reassuring to consumers of your gem that your gem passes its workflow
179
+ # tests on github. This task checks to see if a "badge" indicating success or
180
+ # failure is present and, if not, inserts one at the top of the ~README.org~
181
+ # such that it will get exported to ~README.md~ when =rake docs:export= is run.
182
+ #
183
+ # If you want to place the badge somewhere else in you ~README.org~, place the
184
+ # special comment ~#badge~ where you want the badge located and the task will
185
+ # place it there.
186
+ #
187
+ # If there is already a badge present, the task will not modify the ~README.org~
188
+ # file.
189
+ #
190
+ # ** Export ~README.org~ to ~README.md~: ~rake docs:export~
191
+ # You can write the ~README~ in Emacs org-mode, using all its features
192
+ # including the execution of code blocks, and then export to git-flavored
193
+ # markdown.
194
+ #
195
+ # If your repo contains both ~README.org~ and ~README.md~, github (and gitlab)
196
+ # will render the markdown version.
197
+ #
198
+ # Github renders markdown better than it renders org files, so this helps with
199
+ # the readability of the ~README~ on github. For example, if you write the
200
+ # ~README~ in org mode without exporting to markdown, ~github~ will not render
201
+ # the ~#+RESULTS~ blocks unless you manually delete the ~#+RESULTS~ tag from the
202
+ # output. This is tedious and error-prone, so it is best that you write the
203
+ # ~README~ in ~org-mode~ and export to ~markdown~. That's what this task
204
+ # enables.
205
+ #
206
+ # Also note that when ~github~ renders your ~README.md~, it automatically adds a
207
+ # table of contents, so putting one in the ~README.org~ file is redundant. If
208
+ # you want to have one for your own purposes, just set the ~:noexport~ tag on it
209
+ # so it doesn't get put into the ~README.md~
210
+ #
211
+ # #+begin_src ruby :eval no
212
+ # rake docs:export
213
+ # #+end_src
214
+ #
215
+ # ** Generate Yard Documents: ~rake docs:yard~
216
+ # This task generates a suitable ~.yardopts~ file if none exists and then
217
+ # generates ~yard~ documents into the gem's ~doc~ directory. It also makes sure
218
+ # that ~yard~ knows about your ~README.md~ file so user's of your gem will be
219
+ # able to get an overview of how to use your gem.
220
+ #
221
+ # #+begin_src ruby :eval no
222
+ # rake docs:yard
223
+ # #+end_src
224
+ #
225
+ # ** Generate an Overview Comment for the Main gem File: ~rake docs:overview~
226
+ # Gem's typically gather into a central library file all the require's and other
227
+ # setup needed for the gem and the file is given the same name as the gem. For
228
+ # example, this gem uses the file =lib/gem_docs.rb= for this purpose. Since
229
+ # this =lib= directory is placed in the user's =LOADPATH=, a =require
230
+ # 'gem_docs'= or =require '<gemname>'= effectively initializes the gem.
231
+ #
232
+ # By convention the comment immediately above the 'module' definition in your
233
+ # main library file is used by =yard= and =ri= as the overview for the gem.
234
+ #
235
+ # #+begin_src ruby :eval no
236
+ # rake docs:overview
237
+ # #+end_src
238
+ #
239
+ # This extracts the "Introduction" section from ~README.org~ and makes it the
240
+ # overview comment in the gem's main library file. If it already exists, it
241
+ # replaces it with any newer version of the "Introduction" section, otherwise,
242
+ # it does not change the file.
243
+ module GemDocs
244
+ require_relative "gem_docs/version"
245
+ require_relative "gem_docs/config"
246
+ require_relative "gem_docs/repo"
247
+ require_relative "gem_docs/emacs"
248
+ require_relative "gem_docs/overview"
249
+ require_relative "gem_docs/yard"
250
+ require_relative "gem_docs/badge"
251
+ require_relative "gem_docs/header"
252
+ require_relative "gem_docs/skeleton"
253
+ require_relative "gem_docs/tasks"
254
+
255
+ README_ORG = "README.org"
256
+ README_MD = "README.md"
257
+
258
+ # Auto-detect project root (handles being run from subdirs)
259
+ def self.project_root
260
+ here = Dir.pwd
261
+ here = File.dirname(here) until !Dir['*.gemspec', 'Gemfile'].empty? || here == "/"
262
+ here
263
+ end
264
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gem_docs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniel E. Doherty
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2025-12-23 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rake
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: yard
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.9'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.9'
40
+ description: Shared tasks for README.org execution, GFM export, YARD integration (future),
41
+ etc.
42
+ email:
43
+ - ded@ddoherty.net
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - lib/gem_docs.rb
49
+ - lib/gem_docs/badge.rb
50
+ - lib/gem_docs/config.rb
51
+ - lib/gem_docs/emacs.rb
52
+ - lib/gem_docs/header.rb
53
+ - lib/gem_docs/overview.rb
54
+ - lib/gem_docs/repo.rb
55
+ - lib/gem_docs/skeleton.rb
56
+ - lib/gem_docs/tasks.rb
57
+ - lib/gem_docs/version.rb
58
+ - lib/gem_docs/yard.rb
59
+ homepage: https://github.com/ddoherty03/gem_docs
60
+ licenses:
61
+ - MIT
62
+ metadata:
63
+ homepage_uri: https://github.com/ddoherty03/gem_docs
64
+ source_code_uri: https://github.com/ddoherty03/gem_docs
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 3.2.0
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubygems_version: 3.6.3
80
+ specification_version: 4
81
+ summary: Documentation automation for Ruby gems
82
+ test_files: []