Module: WifiWand::DocsTooling

Defined in:
lib/wifi_wand/docs_tooling.rb

Constant Summary collapse

REPO_ROOT =
File.expand_path('../..', __dir__)
WORKSPACE_PREPARATION_ERROR =

StandardError excludes process-control and VM-level exceptions like Interrupt, SystemExit, and NoMemoryError.

StandardError

Class Method Summary collapse

Class Method Details

.build_script_pathObject



27
28
29
# File 'lib/wifi_wand/docs_tooling.rb', line 27

def self.build_script_path
  File.join(REPO_ROOT, 'bin', 'build-docs')
end

.cleanup_mkdocs_workspace!Object



130
131
132
133
134
# File 'lib/wifi_wand/docs_tooling.rb', line 130

def self.cleanup_mkdocs_workspace!
  FileUtils.rm_rf(generated_docs_dir)
  FileUtils.rm_rf(generated_site_dir)
  FileUtils.rm_f(generated_config_path)
end

.clone_relative_target(absolute_target) ⇒ Object



233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/wifi_wand/docs_tooling.rb', line 233

def self.clone_relative_target(absolute_target)
  path_parts = absolute_target.split('/').reject(&:empty?)
  path_parts.each_index do |index|
    candidate = path_parts[index..].join('/')
    next unless repository_relative_candidate?(candidate)
    next unless plausible_repo_relative_target?(candidate)

    return candidate
  end

  nil
end

.copy_docs_source(relative_path) ⇒ Object



143
144
145
146
147
148
149
150
# File 'lib/wifi_wand/docs_tooling.rb', line 143

def self.copy_docs_source(relative_path)
  source = File.join(REPO_ROOT, relative_path)
  return unless File.exist?(source)

  destination = File.join(generated_docs_dir, relative_path)
  FileUtils.mkdir_p(File.dirname(destination))
  FileUtils.cp(source, destination)
end

.copy_docs_tree(relative_path, exclude: [], required: true) ⇒ Object



152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/wifi_wand/docs_tooling.rb', line 152

def self.copy_docs_tree(relative_path, exclude: [], required: true)
  source = File.join(REPO_ROOT, relative_path)
  return unless required_docs_tree_exists?(source, relative_path, required)

  destination = File.join(generated_docs_dir, relative_path)
  FileUtils.mkdir_p(File.dirname(destination))
  FileUtils.cp_r(source, File.dirname(destination))

  exclude.each do |excluded_file|
    FileUtils.rm_f(File.join(destination, excluded_file))
  end
end

.directory_index_path(directory) ⇒ Object



264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/wifi_wand/docs_tooling.rb', line 264

def self.directory_index_path(directory)
  index_path = File.join(directory, 'index.md')
  return index_path if File.exist?(index_path)

  entries = Dir
    .children(directory)
    .reject { |entry| entry.start_with?('.') }
    .sort
  body = ["# #{File.basename(directory)}", '', *entries.map { |entry| "- `#{entry}`" }, ''].join("\n")
  File.write(index_path, body)
  index_path
end

.ensure_mkdocs_available!Object



325
326
327
328
329
330
331
332
333
334
335
# File 'lib/wifi_wand/docs_tooling.rb', line 325

def self.ensure_mkdocs_available!
  return if executable?(mkdocs_command)

  source_command, rake_command = setup_guidance

  warn 'Error: mkdocs not found. Set up the documentation environment first:'
  warn "  #{source_command}"
  warn 'or:'
  warn "  #{rake_command}"
  exit 1
end

.ensure_python_available!Object



337
338
339
340
341
342
343
# File 'lib/wifi_wand/docs_tooling.rb', line 337

def self.ensure_python_available!
  return if executable?(python_command)

  warn "Error: Python executable \"#{python_command}\" not found."
  warn 'Install python3 or set WIFIWAND_DOCS_PYTHON to the Python executable path.'
  exit 1
end

.excluded_generated_link?(target, markdown_path) ⇒ Boolean

Returns:

  • (Boolean)


285
286
287
288
289
290
291
# File 'lib/wifi_wand/docs_tooling.rb', line 285

def self.excluded_generated_link?(target, markdown_path)
  target_path = target.split('#', 2).first
  return false if target_path.empty? || target_path.match?(%r{\A[a-z][a-z0-9+.-]*:}i)

  excluded_generated_path?(repo_relative_link_target(target_path)) ||
    excluded_generated_path?(generated_relative_target(target_path, markdown_path))
end

.excluded_generated_path?(repo_relative_target) ⇒ Boolean

Returns:

  • (Boolean)


301
302
303
304
305
306
307
# File 'lib/wifi_wand/docs_tooling.rb', line 301

def self.excluded_generated_path?(repo_relative_target)
  return false unless repo_relative_target

  %w[dev/reports dev/prompts].any? do |prefix|
    repo_relative_target == prefix || repo_relative_target.start_with?("#{prefix}/")
  end
end

.executable?(command) ⇒ Boolean

Returns:

  • (Boolean)


71
72
73
74
75
76
77
78
79
80
# File 'lib/wifi_wand/docs_tooling.rb', line 71

def self.executable?(command)
  if command.include?(File::SEPARATOR)
    File.file?(command) && File.executable?(command)
  else
    ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).any? do |dir|
      path = File.join(dir, command)
      File.file?(path) && File.executable?(path)
    end
  end
end

.extract_rake_passthrough_args!(argv = ARGV, rake_application = Rake.application) ⇒ Object



95
96
97
98
99
100
101
102
103
104
# File 'lib/wifi_wand/docs_tooling.rb', line 95

def self.extract_rake_passthrough_args!(argv = ARGV, rake_application = Rake.application)
  separator_index = argv.index('--')
  return [] unless separator_index

  args = argv[(separator_index + 1)..]
  argv.slice!(separator_index..)
  task_like_args = args.reject { |arg| arg.start_with?('-') }
  rake_application.top_level_tasks.reject! { |task| task_like_args.include?(task) }
  args
end

.gemfile_pathObject



43
44
45
# File 'lib/wifi_wand/docs_tooling.rb', line 43

def self.gemfile_path
  File.join(REPO_ROOT, 'Gemfile')
end

.generated_config_pathObject



59
60
61
# File 'lib/wifi_wand/docs_tooling.rb', line 59

def self.generated_config_path
  File.join(REPO_ROOT, 'tmp', "mkdocs-#{Process.pid}.yml")
end

.generated_docs_dirObject



55
56
57
# File 'lib/wifi_wand/docs_tooling.rb', line 55

def self.generated_docs_dir
  File.join(REPO_ROOT, 'tmp', "mkdocs-src-#{Process.pid}")
end

.generated_exclude_docs(exclude_docs) ⇒ Object



309
310
311
312
313
314
315
# File 'lib/wifi_wand/docs_tooling.rb', line 309

def self.generated_exclude_docs(exclude_docs)
  exclude_docs
    .to_s
    .lines
    .reject { |line| %w[lib/ spec/].include?(line.strip) }
    .join
end

.generated_nav(nav) ⇒ Object



183
184
185
186
187
188
189
190
191
192
# File 'lib/wifi_wand/docs_tooling.rb', line 183

def self.generated_nav(nav)
  nav.map do |entry|
    case entry
    when Hash
      entry.transform_values { |value| value == 'index.md' ? 'README.md' : value }
    else
      entry
    end
  end
end


209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/wifi_wand/docs_tooling.rb', line 209

def self.generated_relative_link(absolute_target, markdown_path, fragment: '')
  repo_relative_target = repo_relative_link_target(absolute_target)
  return "(#{absolute_target}#{fragment})" unless repo_relative_target

  generated_target = File.join(generated_docs_dir, repo_relative_target)
  return "(#{absolute_target}#{fragment})" unless File.exist?(generated_target)

  generated_target = directory_index_path(generated_target) if File.directory?(generated_target)
  relative_target = Pathname
    .new(generated_target)
    .relative_path_from(Pathname.new(File.dirname(markdown_path)))
    .to_s
  "(#{relative_target}#{fragment})"
end

.generated_relative_target(target_path, markdown_path) ⇒ Object



293
294
295
296
297
298
299
# File 'lib/wifi_wand/docs_tooling.rb', line 293

def self.generated_relative_target(target_path, markdown_path)
  absolute_target = File.expand_path(target_path, File.dirname(markdown_path))
  Pathname
    .new(absolute_target)
    .relative_path_from(Pathname.new(generated_docs_dir))
    .to_s
end

.generated_site_dirObject



63
64
65
# File 'lib/wifi_wand/docs_tooling.rb', line 63

def self.generated_site_dir
  File.join(REPO_ROOT, 'tmp', "mkdocs-site-#{Process.pid}")
end

.mkdocs_commandObject



82
83
84
85
86
87
88
89
# File 'lib/wifi_wand/docs_tooling.rb', line 82

def self.mkdocs_command
  mkdocs_override = ENV.fetch('WIFIWAND_DOCS_MKDOCS', '').strip
  return mkdocs_override unless mkdocs_override.empty?

  return venv_mkdocs_path if executable?(venv_mkdocs_path)

  'mkdocs'
end

.mkdocs_config_pathObject



51
52
53
# File 'lib/wifi_wand/docs_tooling.rb', line 51

def self.mkdocs_config_path
  File.join(REPO_ROOT, 'mkdocs.yml')
end

.plausible_repo_relative_target?(candidate) ⇒ Boolean

Returns:

  • (Boolean)


246
247
248
249
250
251
# File 'lib/wifi_wand/docs_tooling.rb', line 246

def self.plausible_repo_relative_target?(candidate)
  first_segment = candidate.split('/', 2).first
  return false unless repository_root_entries.include?(first_segment)

  candidate.include?('/') || excluded_generated_path?(candidate)
end

.prepare_mkdocs_workspace!Object



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/wifi_wand/docs_tooling.rb', line 106

def self.prepare_mkdocs_workspace!
  FileUtils.rm_rf(generated_docs_dir)
  FileUtils.mkdir_p(generated_docs_dir)

  copy_docs_source('README.md')
  copy_docs_source('RELEASE_NOTES.md')
  copy_docs_source('LICENSE.txt')
  copy_docs_source('Gemfile')
  copy_docs_tree('lib')
  copy_docs_tree('spec')
  copy_docs_tree('docs', exclude: ['index.md'])
  copy_docs_tree('dev/docs')
  copy_docs_tree('dev/reports', required: false)
  copy_docs_tree('dev/prompts', required: false)
  copy_docs_tree('logo', required: false)
  rewrite_generated_markdown_links
  write_generated_config

  generated_config_path
rescue WORKSPACE_PREPARATION_ERROR
  cleanup_mkdocs_workspace!
  raise
end

.python_commandObject



67
68
69
# File 'lib/wifi_wand/docs_tooling.rb', line 67

def self.python_command
  ENV.fetch('WIFIWAND_DOCS_PYTHON', 'python3')
end

.rake_passthrough_argsObject



91
92
93
# File 'lib/wifi_wand/docs_tooling.rb', line 91

def self.rake_passthrough_args
  @rake_passthrough_args ||= extract_rake_passthrough_args!
end

.rakefile_pathObject



39
40
41
# File 'lib/wifi_wand/docs_tooling.rb', line 39

def self.rakefile_path
  File.join(REPO_ROOT, 'Rakefile')
end


224
225
226
227
228
229
230
231
# File 'lib/wifi_wand/docs_tooling.rb', line 224

def self.repo_relative_link_target(absolute_target)
  current_repo_prefix = "#{REPO_ROOT}/"
  if absolute_target.start_with?(current_repo_prefix)
    return absolute_target.delete_prefix(current_repo_prefix)
  end

  clone_relative_target(absolute_target)
end

.repository_relative_candidate?(candidate) ⇒ Boolean

Returns:

  • (Boolean)


253
254
255
256
257
258
# File 'lib/wifi_wand/docs_tooling.rb', line 253

def self.repository_relative_candidate?(candidate)
  return false unless candidate && !candidate.empty?
  return true if excluded_generated_path?(candidate)

  File.exist?(File.join(REPO_ROOT, candidate))
end

.repository_root_entriesObject



260
261
262
# File 'lib/wifi_wand/docs_tooling.rb', line 260

def self.repository_root_entries
  @repository_root_entries ||= Dir.children(REPO_ROOT).reject { |entry| entry.start_with?('.') }
end

.required_docs_tree_exists?(source, relative_path, required) ⇒ Boolean

Returns:

  • (Boolean)


165
166
167
168
169
170
# File 'lib/wifi_wand/docs_tooling.rb', line 165

def self.required_docs_tree_exists?(source, relative_path, required)
  return true if File.directory?(source)
  return false unless required

  raise "Required documentation source directory is missing: #{relative_path}"
end

.requirements_pathObject



47
48
49
# File 'lib/wifi_wand/docs_tooling.rb', line 47

def self.requirements_path
  File.join(REPO_ROOT, 'requirements-lock.txt')
end


277
278
279
280
281
282
283
# File 'lib/wifi_wand/docs_tooling.rb', line 277

def self.rewrite_excluded_generated_links(markdown, markdown_path)
  markdown.gsub(%r{\[([^\]]+)\]\(([^)\s]+)\)}) do
    label = Regexp.last_match(1)
    target = Regexp.last_match(2)
    excluded_generated_link?(target, markdown_path) ? label : Regexp.last_match(0)
  end
end


194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/wifi_wand/docs_tooling.rb', line 194

def self.rewrite_generated_markdown_links
  Dir.glob(File.join(generated_docs_dir, '**', '*.md')).each do |markdown_path|
    markdown = File.read(markdown_path)
    rewritten = markdown.gsub(%r{\((/[^)\s]+?)(?::\d+)?(#[^)\s]+)?\)}) do
      generated_relative_link(
        Regexp.last_match(1),
        markdown_path,
        fragment: Regexp.last_match(2).to_s
      )
    end
    rewritten = rewrite_excluded_generated_links(rewritten, markdown_path)
    File.write(markdown_path, rewritten) if rewritten != markdown
  end
end

.run_mkdocs!Object



136
137
138
139
140
141
# File 'lib/wifi_wand/docs_tooling.rb', line 136

def self.run_mkdocs!(*)
  success = system(mkdocs_command, *)
  exit($CHILD_STATUS&.exitstatus || 1) unless success
ensure
  cleanup_mkdocs_workspace!
end

.setup_environment!Object



345
346
347
348
349
# File 'lib/wifi_wand/docs_tooling.rb', line 345

def self.setup_environment!
  ensure_python_available!
  system(python_command, '-m', 'venv', venv_dir, exception: true)
  system(venv_pip_path, 'install', '-q', '-r', requirements_path, exception: true)
end

.setup_guidanceObject



317
318
319
320
321
322
323
# File 'lib/wifi_wand/docs_tooling.rb', line 317

def self.setup_guidance
  source_command = "source #{setup_script_path.shellescape}"
  rake_command = "BUNDLE_GEMFILE=#{gemfile_path.shellescape} bundle exec rake -f " \
    "#{rakefile_path.shellescape} docs:setup"

  [source_command, rake_command]
end

.setup_script_pathObject



35
36
37
# File 'lib/wifi_wand/docs_tooling.rb', line 35

def self.setup_script_path
  File.join(REPO_ROOT, 'bin', 'set-up-python-for-doc-server')
end

.start_server_script_pathObject



31
32
33
# File 'lib/wifi_wand/docs_tooling.rb', line 31

def self.start_server_script_path
  File.join(REPO_ROOT, 'bin', 'start-doc-server')
end

.venv_dirObject



15
16
17
# File 'lib/wifi_wand/docs_tooling.rb', line 15

def self.venv_dir
  ENV.fetch('WIFIWAND_DOCS_VENV_DIR', File.join(REPO_ROOT, '.docs-venv'))
end

.venv_mkdocs_pathObject



19
20
21
# File 'lib/wifi_wand/docs_tooling.rb', line 19

def self.venv_mkdocs_path
  File.join(venv_dir, 'bin', 'mkdocs')
end

.venv_pip_pathObject



23
24
25
# File 'lib/wifi_wand/docs_tooling.rb', line 23

def self.venv_pip_path
  File.join(venv_dir, 'bin', 'pip')
end

.write_generated_configObject



172
173
174
175
176
177
178
179
180
181
# File 'lib/wifi_wand/docs_tooling.rb', line 172

def self.write_generated_config
  config = YAML.load_file(mkdocs_config_path)
  config['docs_dir'] = generated_docs_dir
  config['site_dir'] = generated_site_dir
  config['exclude_docs'] = generated_exclude_docs(config['exclude_docs'])
  config['nav'] = generated_nav(config['nav'])

  FileUtils.mkdir_p(File.dirname(generated_config_path))
  File.write(generated_config_path, config.to_yaml)
end