ratatui_ruby-devtools 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 +7 -0
- data/.builds/ruby-4.0.yml +38 -0
- data/.pre-commit-config.yaml +16 -0
- data/.rubocop.yml +8 -0
- data/AGENTS.md +72 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE +661 -0
- data/LICENSES/AGPL-3.0-or-later.txt +661 -0
- data/LICENSES/CC-BY-SA-4.0.txt +427 -0
- data/LICENSES/CC0-1.0.txt +121 -0
- data/LICENSES/MIT-0.txt +16 -0
- data/LICENSES/MIT.txt +18 -0
- data/README.md +199 -0
- data/REUSE.toml +18 -0
- data/Rakefile +13 -0
- data/bin/agent_rake +13 -0
- data/bin/announce +13 -0
- data/bin/console +14 -0
- data/bin/consolidate_md +13 -0
- data/bin/hbs +13 -0
- data/bin/setup +17 -0
- data/doc/contributors/documentation_style.md +121 -0
- data/doc/custom.css +22 -0
- data/exe/agent_rake +96 -0
- data/exe/announce +1120 -0
- data/exe/consolidate_md +246 -0
- data/exe/hbs +670 -0
- data/exe/scaffold +662 -0
- data/lib/ratatui_ruby/devtools/tasks/autodoc/examples.rb +133 -0
- data/lib/ratatui_ruby/devtools/tasks/autodoc/member.rb +116 -0
- data/lib/ratatui_ruby/devtools/tasks/autodoc/name.rb +33 -0
- data/lib/ratatui_ruby/devtools/tasks/autodoc.rake +21 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/cargo_lockfile.rb +38 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/changelog.rb +67 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/header.rb +43 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/history.rb +50 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/links.rb +78 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/manifest.rb +63 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/ruby_gem.rb +77 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/sem_ver.rb +63 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/unreleased_section.rb +75 -0
- data/lib/ratatui_ruby/devtools/tasks/bump.rake +80 -0
- data/lib/ratatui_ruby/devtools/tasks/cargo.rake +47 -0
- data/lib/ratatui_ruby/devtools/tasks/doc.rake +887 -0
- data/lib/ratatui_ruby/devtools/tasks/example_viewer.html.erb +172 -0
- data/lib/ratatui_ruby/devtools/tasks/license/headers_md.rb +276 -0
- data/lib/ratatui_ruby/devtools/tasks/license/headers_rb.rb +236 -0
- data/lib/ratatui_ruby/devtools/tasks/license/license_utils.rb +143 -0
- data/lib/ratatui_ruby/devtools/tasks/license/snippets_md.rb +353 -0
- data/lib/ratatui_ruby/devtools/tasks/license/snippets_rdoc.rb +186 -0
- data/lib/ratatui_ruby/devtools/tasks/license.rake +91 -0
- data/lib/ratatui_ruby/devtools/tasks/lint.rake +84 -0
- data/lib/ratatui_ruby/devtools/tasks/rdoc_config.rb +45 -0
- data/lib/ratatui_ruby/devtools/tasks/resources/build.yml.erb +54 -0
- data/lib/ratatui_ruby/devtools/tasks/resources/rubies.yml +7 -0
- data/lib/ratatui_ruby/devtools/tasks/reuse.rake +104 -0
- data/lib/ratatui_ruby/devtools/tasks/sourcehut.rake +94 -0
- data/lib/ratatui_ruby/devtools/tasks/test.rake +18 -0
- data/lib/ratatui_ruby/devtools/templates/.builds/ruby.yml.erb +47 -0
- data/lib/ratatui_ruby/devtools/templates/.gitignore.erb +18 -0
- data/lib/ratatui_ruby/devtools/templates/.pre-commit-config.yaml.erb +16 -0
- data/lib/ratatui_ruby/devtools/templates/.rubocop.yml.erb +8 -0
- data/lib/ratatui_ruby/devtools/templates/AGENTS.md.erb +65 -0
- data/lib/ratatui_ruby/devtools/templates/CHANGELOG.md.erb +18 -0
- data/lib/ratatui_ruby/devtools/templates/Gemfile.erb +32 -0
- data/lib/ratatui_ruby/devtools/templates/README.md.erb +127 -0
- data/lib/ratatui_ruby/devtools/templates/REUSE.toml.erb +33 -0
- data/lib/ratatui_ruby/devtools/templates/Rakefile.erb +29 -0
- data/lib/ratatui_ruby/devtools/templates/bin/console.erb +18 -0
- data/lib/ratatui_ruby/devtools/templates/bin/setup.erb +24 -0
- data/lib/ratatui_ruby/devtools/templates/doc/concepts/application_architecture.md.erb +16 -0
- data/lib/ratatui_ruby/devtools/templates/doc/concepts/application_testing.md.erb +49 -0
- data/lib/ratatui_ruby/devtools/templates/doc/custom.css.erb +24 -0
- data/lib/ratatui_ruby/devtools/templates/doc/getting_started/quickstart.md.erb +56 -0
- data/lib/ratatui_ruby/devtools/templates/doc/images/.gitkeep +0 -0
- data/lib/ratatui_ruby/devtools/templates/doc/index.md.erb +25 -0
- data/lib/ratatui_ruby/devtools/templates/exe/.gitkeep +0 -0
- data/lib/ratatui_ruby/devtools/templates/gemspec.erb +58 -0
- data/lib/ratatui_ruby/devtools/templates/mise.toml.erb +12 -0
- data/lib/ratatui_ruby/devtools/templates/tasks/example_viewer.html.erb +174 -0
- data/lib/ratatui_ruby/devtools/templates/tasks/resources/build.yml.erb +62 -0
- data/lib/ratatui_ruby/devtools/templates/tasks/resources/index.html.erb +46 -0
- data/lib/ratatui_ruby/devtools/templates/tasks/resources/rubies.yml.erb +9 -0
- data/lib/ratatui_ruby/devtools/templates/vendor/goodcop/base.yml +1047 -0
- data/lib/ratatui_ruby/devtools/version.rb +13 -0
- data/lib/ratatui_ruby/devtools.rb +137 -0
- data/mise.toml +7 -0
- data/sig/ratatui_ruby/devtools.rbs +15 -0
- data/vendor/goodcop/base.yml +1047 -0
- metadata +252 -0
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
require "rdoc/task"
|
|
9
|
+
|
|
10
|
+
require_relative "rdoc_config"
|
|
11
|
+
|
|
12
|
+
RDoc::Task.new do |rdoc|
|
|
13
|
+
rdoc.rdoc_dir = ENV["RDOC_OUTPUT"] || "tmp/rdoc"
|
|
14
|
+
rdoc.main = RDocConfig::MAIN
|
|
15
|
+
rdoc.title = ENV["RDOC_TITLE"] if ENV["RDOC_TITLE"]
|
|
16
|
+
rdoc.rdoc_files.include(RDocConfig::RDOC_FILES)
|
|
17
|
+
custom_css = ENV["RDOC_CUSTOM_CSS"] || "doc/custom.css"
|
|
18
|
+
rdoc.options << "--template-stylesheets=#{custom_css}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Custom RDoc HTML generator that captures headings for TOC
|
|
22
|
+
class CapturingToHtml < RDoc::Markup::ToHtml
|
|
23
|
+
attr_reader :captured_headings
|
|
24
|
+
|
|
25
|
+
def initialize(options, markup = nil)
|
|
26
|
+
super
|
|
27
|
+
@captured_headings = []
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def accept_heading(heading)
|
|
31
|
+
start_pos = @res.length
|
|
32
|
+
super
|
|
33
|
+
added = @res[start_pos..-1].join
|
|
34
|
+
if added =~ /id="([^"]+)"/
|
|
35
|
+
@captured_headings << { level: heading.level, text: heading.text, id: $1 }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
task :copy_doc_images do
|
|
41
|
+
rdoc_dir = ENV["RDOC_OUTPUT"] || "tmp/rdoc"
|
|
42
|
+
if Dir.exist?("doc/images")
|
|
43
|
+
FileUtils.mkdir_p "#{rdoc_dir}/doc/images"
|
|
44
|
+
FileUtils.cp_r Dir["doc/images/*.png"], "#{rdoc_dir}/doc/images"
|
|
45
|
+
FileUtils.cp_r Dir["doc/images/*.gif"], "#{rdoc_dir}/doc/images"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def build_tree(all_files, root_dir, max_depth = nil, current_depth = 1)
|
|
50
|
+
return {} if max_depth && current_depth > max_depth
|
|
51
|
+
|
|
52
|
+
files_by_dir = all_files.group_by { |f| File.dirname(f) }
|
|
53
|
+
dirs_at_level = files_by_dir.keys.select { |d| d.start_with?(root_dir) && d.count("/") == current_depth }
|
|
54
|
+
|
|
55
|
+
tree = {}
|
|
56
|
+
|
|
57
|
+
dirs_at_level.each do |dir|
|
|
58
|
+
dir_name = dir.split("/").last
|
|
59
|
+
files = files_by_dir[dir] || []
|
|
60
|
+
subdirs = files_by_dir.keys.select { |d| d.start_with?("#{dir}/") && d.count("/") == current_depth + 1 }
|
|
61
|
+
|
|
62
|
+
tree[dir_name] = {
|
|
63
|
+
path: dir.sub("examples/", ""),
|
|
64
|
+
files: files.map { |f|
|
|
65
|
+
{
|
|
66
|
+
name: File.basename(f),
|
|
67
|
+
path: "#{File.basename(f).gsub('.', '_')}.html",
|
|
68
|
+
full_path: f,
|
|
69
|
+
}
|
|
70
|
+
}.sort_by { |f| f[:name] },
|
|
71
|
+
subdirs: build_tree(all_files, dir, max_depth, current_depth + 1),
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
tree
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def extract_rdoc_info(content, filename)
|
|
79
|
+
require "rdoc/comment"
|
|
80
|
+
require "rdoc/markup/to_html"
|
|
81
|
+
|
|
82
|
+
options = RDoc::Options.new
|
|
83
|
+
store = RDoc::Store.new(options)
|
|
84
|
+
top_level = store.add_file filename
|
|
85
|
+
stats = RDoc::Stats.new(store, 1)
|
|
86
|
+
|
|
87
|
+
parser = RDoc::Parser::Ruby.new(top_level, content, options, stats)
|
|
88
|
+
parser.scan
|
|
89
|
+
|
|
90
|
+
lines = content.lines
|
|
91
|
+
|
|
92
|
+
# Find the first class/module defined in the file
|
|
93
|
+
# We want the one that appears earliest in the file
|
|
94
|
+
# Filter out items with nil lines
|
|
95
|
+
target_class = top_level.classes_or_modules.select(&:line).min_by(&:line)
|
|
96
|
+
|
|
97
|
+
if target_class
|
|
98
|
+
# Use the class definition line as the anchor
|
|
99
|
+
# RDoc line numbers are 1-based
|
|
100
|
+
anchor_index = target_class.line - 1
|
|
101
|
+
title = target_class.name
|
|
102
|
+
search_snippet = target_class.respond_to?(:search_snippet) ? target_class.search_snippet : ""
|
|
103
|
+
else
|
|
104
|
+
# Fallback to first line of code if no class defined
|
|
105
|
+
first_code_index = lines.find_index { |l| !l.strip.empty? && !l.strip.start_with?("#") }
|
|
106
|
+
anchor_index = first_code_index
|
|
107
|
+
title = nil
|
|
108
|
+
search_snippet = ""
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Walk upwards from the line before the anchor to find immediate comments
|
|
112
|
+
comment_lines = []
|
|
113
|
+
if anchor_index && anchor_index > 0
|
|
114
|
+
idx = anchor_index - 1
|
|
115
|
+
while idx >= 0
|
|
116
|
+
line = lines[idx].strip
|
|
117
|
+
|
|
118
|
+
# Stop at blank lines
|
|
119
|
+
break if line.empty?
|
|
120
|
+
|
|
121
|
+
# Stop if we hit something that isn't a comment (shouldn't happen if we are above code, but safety check)
|
|
122
|
+
break unless line.start_with?("#")
|
|
123
|
+
|
|
124
|
+
comment_lines.unshift(lines[idx])
|
|
125
|
+
idx -= 1
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
raw_comment = nil
|
|
130
|
+
unless comment_lines.empty?
|
|
131
|
+
# Create RDoc comment from extracted lines
|
|
132
|
+
# Strip leading # and optional space
|
|
133
|
+
cleaned_lines = comment_lines.map do |line|
|
|
134
|
+
line.strip.sub(/^#\s?/, "")
|
|
135
|
+
end
|
|
136
|
+
raw_comment = cleaned_lines.join("\n")
|
|
137
|
+
# Use first line of comment as snippet if RDoc didn't provide one
|
|
138
|
+
search_snippet = cleaned_lines.first if search_snippet.empty?
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
{
|
|
142
|
+
title:,
|
|
143
|
+
raw_comment:,
|
|
144
|
+
search_snippet:,
|
|
145
|
+
}
|
|
146
|
+
rescue => e
|
|
147
|
+
puts "Warning: Failed to extract RDoc info for #{filename}: #{e.message}"
|
|
148
|
+
{ title: nil, raw_comment: nil, search_snippet: "" }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def render_tree_html(tree_data, current_path, current_file_html, depth = 0)
|
|
152
|
+
html_parts = []
|
|
153
|
+
|
|
154
|
+
# Calculate prefix to return to examples root from current file
|
|
155
|
+
current_depth = current_path.split("/").size - 1
|
|
156
|
+
root_prefix = "../" * current_depth
|
|
157
|
+
|
|
158
|
+
tree_data.keys.sort.each do |dir_name|
|
|
159
|
+
item = tree_data[dir_name]
|
|
160
|
+
has_children = item[:files].any? || item[:subdirs].any?
|
|
161
|
+
|
|
162
|
+
if has_children
|
|
163
|
+
# Check if this directory is in the path of the current file
|
|
164
|
+
# item[:path] is relative to examples root (e.g., "app_all_events/model")
|
|
165
|
+
# current_path is also relative to examples root (e.g., "app_all_events/model/event_color_cycle")
|
|
166
|
+
# We want to open if current_path starts with this directory's path
|
|
167
|
+
is_in_path = current_path == item[:path] || current_path.start_with?("#{item[:path]}/")
|
|
168
|
+
open_attr = is_in_path ? "open" : ""
|
|
169
|
+
|
|
170
|
+
html_parts << "<li>"
|
|
171
|
+
html_parts << " <details #{open_attr}>"
|
|
172
|
+
html_parts << " <summary>#{ERB::Util.html_escape(dir_name)}</summary>"
|
|
173
|
+
html_parts << " <ul class=\"link-list nav-list\">"
|
|
174
|
+
|
|
175
|
+
# Add files
|
|
176
|
+
item[:files].each do |file|
|
|
177
|
+
# Check specific file match
|
|
178
|
+
# file[:path] is the HTML filename (e.g., "event_color_cycle.html")
|
|
179
|
+
# current_file_html is the current file's HTML name
|
|
180
|
+
is_active = is_in_path && file[:path] == current_file_html
|
|
181
|
+
|
|
182
|
+
active_class = is_active ? ' class="active"' : ""
|
|
183
|
+
file_name_display = ERB::Util.html_escape(file[:name])
|
|
184
|
+
file_name_display = "<strong>#{file_name_display}</strong>" if is_active
|
|
185
|
+
|
|
186
|
+
# Link relative to examples root
|
|
187
|
+
file_path = "#{root_prefix}#{item[:path]}/#{file[:path]}"
|
|
188
|
+
html_parts << " <li><a href=\"#{file_path}\"#{active_class}><span class=\"file\"></span>#{file_name_display}</a></li>"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Add subdirectories recursively
|
|
192
|
+
if item[:subdirs].any?
|
|
193
|
+
html_parts << render_tree_html(item[:subdirs], current_path, current_file_html, depth + 1)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
html_parts << " </ul>"
|
|
197
|
+
html_parts << " </details>"
|
|
198
|
+
html_parts << "</li>"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
html_parts.join("\n")
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
task :copy_examples do
|
|
206
|
+
puts "Copying examples..."
|
|
207
|
+
require "erb"
|
|
208
|
+
|
|
209
|
+
require "rdoc"
|
|
210
|
+
require "rdoc/markdown"
|
|
211
|
+
require "rdoc/markup/to_html"
|
|
212
|
+
|
|
213
|
+
rdoc_dir = ENV["RDOC_OUTPUT"] || "tmp/rdoc"
|
|
214
|
+
|
|
215
|
+
if Dir.exist?("examples")
|
|
216
|
+
FileUtils.rm_rf "#{rdoc_dir}/examples"
|
|
217
|
+
FileUtils.mkdir_p "#{rdoc_dir}/examples"
|
|
218
|
+
|
|
219
|
+
all_files = Dir.glob("examples/**/*.{rb,md}")
|
|
220
|
+
|
|
221
|
+
template = File.read("tasks/example_viewer.html.erb")
|
|
222
|
+
erb = ERB.new(template)
|
|
223
|
+
|
|
224
|
+
# Find the RDoc icons template
|
|
225
|
+
icons_path = Gem.find_files("rdoc/generator/template/aliki/_icons.rhtml").first
|
|
226
|
+
icons_svg = icons_path ? File.read(icons_path) : ""
|
|
227
|
+
|
|
228
|
+
# Group files by directory
|
|
229
|
+
files_by_dir = all_files.group_by { |f| File.dirname(f) }
|
|
230
|
+
|
|
231
|
+
# Create a binding context for ERB
|
|
232
|
+
class ExampleViewerContext
|
|
233
|
+
attr_reader :breadcrumb_path, :page_title, :file_content_html, :file_header_html
|
|
234
|
+
attr_reader :current_file_html, :tree_data, :doc_root_link, :icons_svg, :relative_path
|
|
235
|
+
attr_reader :toc_items
|
|
236
|
+
attr_accessor :render_tree_helper
|
|
237
|
+
|
|
238
|
+
def initialize(breadcrumb_path, page_title, file_content_html, file_header_html,
|
|
239
|
+
current_file_html, tree_data, doc_root_link, icons_svg, relative_path, toc_items
|
|
240
|
+
)
|
|
241
|
+
@breadcrumb_path = breadcrumb_path
|
|
242
|
+
@page_title = page_title
|
|
243
|
+
@file_content_html = file_content_html
|
|
244
|
+
@file_header_html = file_header_html
|
|
245
|
+
|
|
246
|
+
@current_file_html = current_file_html
|
|
247
|
+
@tree_data = tree_data
|
|
248
|
+
@doc_root_link = doc_root_link
|
|
249
|
+
@icons_svg = icons_svg
|
|
250
|
+
@relative_path = relative_path
|
|
251
|
+
@toc_items = toc_items
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def render_tree(tree_data, current_path, current_file_html)
|
|
255
|
+
render_tree_helper.call(tree_data, current_path, current_file_html)
|
|
256
|
+
# Output directly to preserve HTML tags
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def render_toc(items)
|
|
260
|
+
return "" if items.empty?
|
|
261
|
+
|
|
262
|
+
html = []
|
|
263
|
+
html << "<ul>"
|
|
264
|
+
base_level = items.first[:level]
|
|
265
|
+
|
|
266
|
+
items.each_with_index do |item, i|
|
|
267
|
+
level = item[:level]
|
|
268
|
+
text = item[:text]
|
|
269
|
+
id = item[:id]
|
|
270
|
+
|
|
271
|
+
html << "<li><a href=\"##{id}\">#{text}</a>"
|
|
272
|
+
|
|
273
|
+
next_item = items[i + 1]
|
|
274
|
+
|
|
275
|
+
if next_item
|
|
276
|
+
next_level = next_item[:level]
|
|
277
|
+
if next_level > level
|
|
278
|
+
(next_level - level).times { html << "<ul>" }
|
|
279
|
+
elsif next_level < level
|
|
280
|
+
html << "</li>"
|
|
281
|
+
(level - next_level).times { html << "</ul></li>" }
|
|
282
|
+
else # same level
|
|
283
|
+
html << "</li>"
|
|
284
|
+
end
|
|
285
|
+
else
|
|
286
|
+
# Last item. Close everything back to start.
|
|
287
|
+
html << "</li>"
|
|
288
|
+
(level - base_level).times { html << "</ul></li>" }
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
html << "</ul>"
|
|
293
|
+
html.join("\n")
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def get_binding
|
|
297
|
+
binding
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Collect search index entries
|
|
302
|
+
search_entries = []
|
|
303
|
+
|
|
304
|
+
# Generate HTML files for each file
|
|
305
|
+
all_files.each do |file_path|
|
|
306
|
+
relative_path = file_path.sub("examples/", "")
|
|
307
|
+
target_dir = "#{rdoc_dir}/examples/#{File.dirname(relative_path)}"
|
|
308
|
+
FileUtils.mkdir_p target_dir
|
|
309
|
+
|
|
310
|
+
content = File.read(file_path)
|
|
311
|
+
ext = File.extname(file_path)
|
|
312
|
+
filename = File.basename(file_path)
|
|
313
|
+
toc_items = []
|
|
314
|
+
|
|
315
|
+
if ext == ".md"
|
|
316
|
+
# Markdown files usually have their own H1, so no header needed
|
|
317
|
+
page_title = filename
|
|
318
|
+
breadcrumb_path = relative_path
|
|
319
|
+
file_header_html = ""
|
|
320
|
+
|
|
321
|
+
# Parse markdown
|
|
322
|
+
doc = RDoc::Markdown.parse(content)
|
|
323
|
+
|
|
324
|
+
# Render and capture headings
|
|
325
|
+
options = RDoc::Options.new
|
|
326
|
+
renderer = CapturingToHtml.new(options)
|
|
327
|
+
file_content_html = doc.accept(renderer)
|
|
328
|
+
toc_items = renderer.captured_headings
|
|
329
|
+
|
|
330
|
+
# For Markdown, if we assume the first header is the title and is already captured,
|
|
331
|
+
# we might not need to prepend anything.
|
|
332
|
+
# But if file_header_html is empty, the H1 is in file_content_html.
|
|
333
|
+
# CapturingToHtml should have captured it.
|
|
334
|
+
else
|
|
335
|
+
info = extract_rdoc_info(content, filename)
|
|
336
|
+
|
|
337
|
+
if info[:title]
|
|
338
|
+
page_title = info[:title]
|
|
339
|
+
breadcrumb_path = relative_path
|
|
340
|
+
else
|
|
341
|
+
page_title = filename
|
|
342
|
+
breadcrumb_path = "#{File.dirname(relative_path)}/"
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Add to search index
|
|
346
|
+
html_path = "#{File.dirname(relative_path)}/#{File.basename(file_path).gsub('.', '_')}.html"
|
|
347
|
+
search_entries << {
|
|
348
|
+
name: page_title,
|
|
349
|
+
full_name: relative_path,
|
|
350
|
+
type: info[:title] ? "class" : "file",
|
|
351
|
+
path: html_path,
|
|
352
|
+
snippet: info[:search_snippet],
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
file_header_html = "<h1 id=\"top\">#{ERB::Util.html_escape(page_title)}</h1>"
|
|
356
|
+
|
|
357
|
+
# Concatenate comment and Source Code section
|
|
358
|
+
parts = []
|
|
359
|
+
if info[:raw_comment] && !info[:raw_comment].strip.empty?
|
|
360
|
+
parts << info[:raw_comment]
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
indented_code = content.gsub(/^/, " ")
|
|
364
|
+
parts << "= Source Code\n\n#{indented_code}"
|
|
365
|
+
|
|
366
|
+
combined_doc = parts.join("\n\n")
|
|
367
|
+
|
|
368
|
+
# Parse and render
|
|
369
|
+
doc = RDoc::Markup.parse(combined_doc)
|
|
370
|
+
options = RDoc::Options.new
|
|
371
|
+
renderer = CapturingToHtml.new(options)
|
|
372
|
+
file_content_html = doc.accept(renderer)
|
|
373
|
+
toc_items = renderer.captured_headings
|
|
374
|
+
|
|
375
|
+
# Add Page Title to TOC
|
|
376
|
+
toc_items.unshift({ level: 1, text: page_title, id: "top" })
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Calculate link to doc root
|
|
380
|
+
|
|
381
|
+
# Calculate link to doc root
|
|
382
|
+
depth = relative_path.split("/").size - 1
|
|
383
|
+
doc_root_link = "#{'../' * (depth + 1)}index.html"
|
|
384
|
+
|
|
385
|
+
# Build tree structure for sidebar
|
|
386
|
+
current_file_html = "#{File.basename(file_path).gsub('.', '_')}.html"
|
|
387
|
+
tree_data = build_tree(all_files, "examples", nil)
|
|
388
|
+
|
|
389
|
+
context = ExampleViewerContext.new(breadcrumb_path, page_title, file_content_html, file_header_html,
|
|
390
|
+
current_file_html, tree_data, doc_root_link, icons_svg, relative_path, toc_items)
|
|
391
|
+
context.render_tree_helper = lambda { |tree, path, file|
|
|
392
|
+
render_tree_html(tree, path, file)
|
|
393
|
+
}
|
|
394
|
+
html = erb.result(context.get_binding)
|
|
395
|
+
|
|
396
|
+
html_file = "#{target_dir}/#{File.basename(file_path).gsub('.', '_')}.html"
|
|
397
|
+
File.write(html_file, html)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Write search index for examples
|
|
401
|
+
FileUtils.mkdir_p "#{rdoc_dir}/examples/js"
|
|
402
|
+
search_data = { index: search_entries }
|
|
403
|
+
File.write("#{rdoc_dir}/examples/js/search_data.js", "var search_data = #{JSON.generate(search_data)};")
|
|
404
|
+
|
|
405
|
+
# Copy RDoc search JS files to examples
|
|
406
|
+
rdoc_js_dir = Gem.find_files("rdoc/generator/template/aliki/js").first
|
|
407
|
+
if rdoc_js_dir && Dir.exist?(rdoc_js_dir)
|
|
408
|
+
%w[search_navigation.js search_ranker.js search_controller.js aliki.js].each do |js_file|
|
|
409
|
+
src = File.join(rdoc_js_dir, js_file)
|
|
410
|
+
FileUtils.cp(src, "#{rdoc_dir}/examples/js/#{js_file}") if File.exist?(src)
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Generate index.html files for each directory
|
|
415
|
+
files_by_dir.each do |dir, files|
|
|
416
|
+
target_dir = "#{rdoc_dir}/examples/#{dir}".sub("examples/", "")
|
|
417
|
+
FileUtils.mkdir_p target_dir
|
|
418
|
+
|
|
419
|
+
# Get parent directory
|
|
420
|
+
if dir == "examples"
|
|
421
|
+
parent_link = nil
|
|
422
|
+
doc_root_link = "../index.html"
|
|
423
|
+
else
|
|
424
|
+
parent_dir = File.dirname(dir).sub("examples/", "")
|
|
425
|
+
parent_link = (parent_dir == ".") ? "../index.html" : "../index.html"
|
|
426
|
+
depth = dir.sub("examples/", "").split("/").size
|
|
427
|
+
doc_root_link = "#{'../' * (depth + 1)}index.html"
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# Find subdirectories
|
|
431
|
+
subdirs = files_by_dir.keys.select { |d| File.dirname(d) == dir && d != dir }
|
|
432
|
+
|
|
433
|
+
# Build combined list of folders and files with icons
|
|
434
|
+
items = []
|
|
435
|
+
subdirs.each { |d| items << { type: :dir, name: File.basename(d), path: "#{File.basename(d)}/index.html", icon: "📁" } }
|
|
436
|
+
files.each { |f| items << { type: :file, name: File.basename(f), path: "#{File.basename(f).gsub('.', '_')}.html", icon: "📄" } }
|
|
437
|
+
|
|
438
|
+
# Sort alphabetically
|
|
439
|
+
sorted_items = items.sort_by { |i| i[:name].downcase }
|
|
440
|
+
|
|
441
|
+
index_html = <<~HTML
|
|
442
|
+
<!DOCTYPE html>
|
|
443
|
+
<html lang="en">
|
|
444
|
+
<head>
|
|
445
|
+
<meta charset="UTF-8">
|
|
446
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
447
|
+
<title>Examples</title>
|
|
448
|
+
<link href="#{doc_root_link.sub('index.html', '')}css/rdoc.css" rel="stylesheet">
|
|
449
|
+
<link href="#{doc_root_link.sub('index.html', '')}custom.css" rel="stylesheet">
|
|
450
|
+
<script>
|
|
451
|
+
var rdoc_rel_prefix = "#{doc_root_link.sub('index.html', '')}";
|
|
452
|
+
</script>
|
|
453
|
+
</head>
|
|
454
|
+
<body class="file">
|
|
455
|
+
<header class="top-navbar">
|
|
456
|
+
<div class="navbar-brand">Examples</div>
|
|
457
|
+
<div class="navbar-search navbar-search-desktop"></div>
|
|
458
|
+
<button id="theme-toggle" class="theme-toggle" aria-label="Switch to dark mode" type="button" onclick="cycleColorMode()">
|
|
459
|
+
<span class="theme-toggle-icon" aria-hidden="true">🌙</span>
|
|
460
|
+
</button>
|
|
461
|
+
</header>
|
|
462
|
+
|
|
463
|
+
<nav id="navigation" role="navigation">
|
|
464
|
+
<div id="fileindex-section" class="nav-section">
|
|
465
|
+
<h3>Navigation</h3>
|
|
466
|
+
<ul class="nav-list">
|
|
467
|
+
<li><a href="#{doc_root_link}">← Back to Docs</a></li>
|
|
468
|
+
#{parent_link ? "<li><a href=\"#{parent_link}\">↑ Up to parent directory</a></li>" : ''}
|
|
469
|
+
</ul>
|
|
470
|
+
</div>
|
|
471
|
+
</nav>
|
|
472
|
+
|
|
473
|
+
<main role="main">
|
|
474
|
+
<div class="content">
|
|
475
|
+
<ul class="file-list">
|
|
476
|
+
#{sorted_items.map { |item| "<li><a href=\"#{item[:path]}\"><span class=\"icon\">#{item[:icon]}</span>#{item[:name]}#{(item[:type] == :dir) ? '/' : ''}</a></li>" }.join("\n ")}
|
|
477
|
+
</ul>
|
|
478
|
+
</div>
|
|
479
|
+
<div class="footer"><a href="#{doc_root_link}">← Back to docs</a></div>
|
|
480
|
+
</main>
|
|
481
|
+
|
|
482
|
+
<script>
|
|
483
|
+
const modes = ['auto', 'light', 'dark'];
|
|
484
|
+
const icons = { auto: '🌓', light: '☀️', dark: '🌙' };
|
|
485
|
+
|
|
486
|
+
function setColorMode(mode) {
|
|
487
|
+
if (mode === 'auto') {
|
|
488
|
+
document.documentElement.removeAttribute('data-theme');
|
|
489
|
+
const systemTheme = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
|
|
490
|
+
document.documentElement.setAttribute('data-theme', systemTheme);
|
|
491
|
+
} else {
|
|
492
|
+
document.documentElement.setAttribute('data-theme', mode);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const icon = icons[mode];
|
|
496
|
+
const toggle = document.getElementById('theme-toggle');
|
|
497
|
+
if (toggle) {
|
|
498
|
+
toggle.querySelector('.theme-toggle-icon').textContent = icon;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
localStorage.setItem('rdoc-theme', mode);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function cycleColorMode() {
|
|
505
|
+
const current = localStorage.getItem('rdoc-theme') || 'auto';
|
|
506
|
+
const currentIndex = modes.indexOf(current);
|
|
507
|
+
const nextMode = modes[(currentIndex + 1) % modes.length];
|
|
508
|
+
setColorMode(nextMode);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const savedMode = localStorage.getItem('rdoc-theme') || 'auto';
|
|
512
|
+
setColorMode(savedMode);
|
|
513
|
+
</script>
|
|
514
|
+
</body>
|
|
515
|
+
</html>
|
|
516
|
+
HTML
|
|
517
|
+
|
|
518
|
+
File.write("#{target_dir}/index.html", index_html)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
# Generate root index.html
|
|
522
|
+
root_files = all_files.select { |f| File.dirname(f) == "examples" }
|
|
523
|
+
root_subdirs = files_by_dir.keys.select { |d| File.dirname(d) == "examples" && d != "examples" }
|
|
524
|
+
|
|
525
|
+
# Build combined list of root folders and files with icons
|
|
526
|
+
root_items = []
|
|
527
|
+
root_subdirs.each { |d| root_items << { type: :dir, name: File.basename(d), path: "#{File.basename(d)}/index.html", icon: "📁" } }
|
|
528
|
+
root_files.each { |f| root_items << { type: :file, name: File.basename(f), path: "#{File.basename(f).gsub('.', '_')}.html", icon: "📄" } }
|
|
529
|
+
|
|
530
|
+
# Sort alphabetically
|
|
531
|
+
sorted_root_items = root_items.sort_by { |i| i[:name].downcase }
|
|
532
|
+
|
|
533
|
+
root_index_html = <<~HTML
|
|
534
|
+
<!DOCTYPE html>
|
|
535
|
+
<html lang="en">
|
|
536
|
+
<head>
|
|
537
|
+
<meta charset="UTF-8">
|
|
538
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
539
|
+
<title>Examples</title>
|
|
540
|
+
<link href="../css/rdoc.css" rel="stylesheet">
|
|
541
|
+
<link href="../custom.css" rel="stylesheet">
|
|
542
|
+
<script>
|
|
543
|
+
var rdoc_rel_prefix = "../";
|
|
544
|
+
</script>
|
|
545
|
+
</head>
|
|
546
|
+
<body class="file">
|
|
547
|
+
<header class="top-navbar">
|
|
548
|
+
<div class="navbar-brand">Examples</div>
|
|
549
|
+
<div class="navbar-search navbar-search-desktop"></div>
|
|
550
|
+
<button id="theme-toggle" class="theme-toggle" aria-label="Switch to dark mode" type="button" onclick="cycleColorMode()">
|
|
551
|
+
<span class="theme-toggle-icon" aria-hidden="true">🌙</span>
|
|
552
|
+
</button>
|
|
553
|
+
</header>
|
|
554
|
+
|
|
555
|
+
<nav id="navigation" role="navigation">
|
|
556
|
+
<div id="fileindex-section" class="nav-section">
|
|
557
|
+
<h3>Navigation</h3>
|
|
558
|
+
<ul class="nav-list">
|
|
559
|
+
<li><a href="../index.html">← Back to Docs</a></li>
|
|
560
|
+
</ul>
|
|
561
|
+
</div>
|
|
562
|
+
</nav>
|
|
563
|
+
|
|
564
|
+
<main role="main">
|
|
565
|
+
<div class="content">
|
|
566
|
+
<ul class="file-list">
|
|
567
|
+
#{sorted_root_items.map { |item| "<li><a href=\"#{item[:path]}\"><span class=\"icon\">#{item[:icon]}</span>#{item[:name]}#{(item[:type] == :dir) ? '/' : ''}</a></li>" }.join("\n ")}
|
|
568
|
+
</ul>
|
|
569
|
+
</div>
|
|
570
|
+
<div class="footer"><a href="../index.html">← Back to docs</a></div>
|
|
571
|
+
</main>
|
|
572
|
+
|
|
573
|
+
<script>
|
|
574
|
+
const modes = ['auto', 'light', 'dark'];
|
|
575
|
+
const icons = { auto: '🌓', light: '☀️', dark: '🌙' };
|
|
576
|
+
|
|
577
|
+
function setColorMode(mode) {
|
|
578
|
+
if (mode === 'auto') {
|
|
579
|
+
document.documentElement.removeAttribute('data-theme');
|
|
580
|
+
const systemTheme = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
|
|
581
|
+
document.documentElement.setAttribute('data-theme', systemTheme);
|
|
582
|
+
} else {
|
|
583
|
+
document.documentElement.setAttribute('data-theme', mode);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const icon = icons[mode];
|
|
587
|
+
const toggle = document.getElementById('theme-toggle');
|
|
588
|
+
if (toggle) {
|
|
589
|
+
toggle.querySelector('.theme-toggle-icon').textContent = icon;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
localStorage.setItem('rdoc-theme', mode);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function cycleColorMode() {
|
|
596
|
+
const current = localStorage.getItem('rdoc-theme') || 'auto';
|
|
597
|
+
const currentIndex = modes.indexOf(current);
|
|
598
|
+
const nextMode = modes[(currentIndex + 1) % modes.length];
|
|
599
|
+
setColorMode(nextMode);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const savedMode = localStorage.getItem('rdoc-theme') || 'auto';
|
|
603
|
+
setColorMode(savedMode);
|
|
604
|
+
</script>
|
|
605
|
+
</body>
|
|
606
|
+
</html>
|
|
607
|
+
HTML
|
|
608
|
+
|
|
609
|
+
File.write("#{rdoc_dir}/examples/index.html", root_index_html)
|
|
610
|
+
end
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
Rake::Task[:rdoc].enhance do
|
|
614
|
+
Rake::Task[:copy_doc_images].invoke
|
|
615
|
+
Rake::Task[:copy_examples].invoke
|
|
616
|
+
Rake::Task[:rewrite_examples_link].invoke
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
Rake::Task[:rerdoc].enhance do
|
|
620
|
+
Rake::Task[:copy_doc_images].invoke
|
|
621
|
+
Rake::Task[:copy_examples].invoke
|
|
622
|
+
Rake::Task[:rewrite_examples_link].invoke
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
task :rewrite_examples_link do
|
|
626
|
+
require "nokogiri"
|
|
627
|
+
|
|
628
|
+
rdoc_dir = ENV["RDOC_OUTPUT"] || "tmp/rdoc"
|
|
629
|
+
|
|
630
|
+
# Build a mapping of example READMEs to their H1 titles and categories
|
|
631
|
+
examples_by_category = { "Apps" => [], "Widgets" => [] }
|
|
632
|
+
|
|
633
|
+
Dir.glob("examples/*/README.md").each do |readme_path|
|
|
634
|
+
dir_name = File.dirname(readme_path).sub("examples/", "")
|
|
635
|
+
|
|
636
|
+
# Skip verify examples entirely
|
|
637
|
+
next if dir_name.start_with?("verify_")
|
|
638
|
+
|
|
639
|
+
content = File.read(readme_path)
|
|
640
|
+
if content =~ /^#\s+(.+)$/
|
|
641
|
+
title = $1.strip.sub(/ Example$/, "") # Remove trailing " Example"
|
|
642
|
+
rdoc_path = "examples/#{dir_name}/README_md.html"
|
|
643
|
+
|
|
644
|
+
# Categorize by prefix
|
|
645
|
+
category = if dir_name.start_with?("app_")
|
|
646
|
+
"Apps"
|
|
647
|
+
elsif dir_name.start_with?("widget_")
|
|
648
|
+
title = title.sub(/ Widget$/, "") # Also strip trailing " Widget" for widgets
|
|
649
|
+
"Widgets"
|
|
650
|
+
else
|
|
651
|
+
nil
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
if category
|
|
655
|
+
examples_by_category[category] << { title:, rdoc_path:, dir_name: }
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
# Sort each category alphabetically by title
|
|
661
|
+
examples_by_category.each_value { |list| list.sort_by! { |e| e[:title] } }
|
|
662
|
+
|
|
663
|
+
# Process all HTML files
|
|
664
|
+
Dir.glob("#{rdoc_dir}/**/*.html").each do |file|
|
|
665
|
+
content = File.read(file)
|
|
666
|
+
modified = false
|
|
667
|
+
|
|
668
|
+
doc = Nokogiri::HTML(content)
|
|
669
|
+
|
|
670
|
+
# Find the examples details section to remove from Pages
|
|
671
|
+
examples_detail = doc.css("details summary").find { |s| s.text.strip.downcase == "examples" }&.parent
|
|
672
|
+
|
|
673
|
+
# Find the classindex-section to insert Examples section before it
|
|
674
|
+
classindex_section = doc.at_css("#classindex-section")
|
|
675
|
+
|
|
676
|
+
if examples_detail && classindex_section
|
|
677
|
+
# Remove examples from Pages section
|
|
678
|
+
examples_detail.remove
|
|
679
|
+
|
|
680
|
+
# Build the new Examples section as a top-level nav-section
|
|
681
|
+
current_depth = file.sub("#{rdoc_dir}/", "").count("/")
|
|
682
|
+
prefix = "../" * current_depth
|
|
683
|
+
|
|
684
|
+
examples_section = Nokogiri::XML::Node.new("div", doc)
|
|
685
|
+
examples_section["id"] = "exampleindex-section"
|
|
686
|
+
examples_section["class"] = "nav-section"
|
|
687
|
+
|
|
688
|
+
examples_section.inner_html = <<~HTML
|
|
689
|
+
<details class="nav-section-collapsible" open>
|
|
690
|
+
<summary class="nav-section-header">
|
|
691
|
+
<span class="nav-section-icon">
|
|
692
|
+
<svg><use href="#icon-layers"></use></svg>
|
|
693
|
+
</span>
|
|
694
|
+
<span class="nav-section-title">Examples</span>
|
|
695
|
+
<span class="nav-section-chevron">
|
|
696
|
+
<svg><use href="#icon-chevron"></use></svg>
|
|
697
|
+
</span>
|
|
698
|
+
</summary>
|
|
699
|
+
<ul class="link-list nav-list">
|
|
700
|
+
</ul>
|
|
701
|
+
</details>
|
|
702
|
+
HTML
|
|
703
|
+
|
|
704
|
+
# Build the category structure
|
|
705
|
+
examples_ul = examples_section.at_css("ul.link-list")
|
|
706
|
+
|
|
707
|
+
examples_by_category.each do |category_name, examples|
|
|
708
|
+
next if examples.empty?
|
|
709
|
+
|
|
710
|
+
cat_li = Nokogiri::XML::Node.new("li", doc)
|
|
711
|
+
cat_details = Nokogiri::XML::Node.new("details", doc)
|
|
712
|
+
# Subcategories closed by default
|
|
713
|
+
cat_summary = Nokogiri::XML::Node.new("summary", doc)
|
|
714
|
+
cat_summary.content = category_name
|
|
715
|
+
cat_details.add_child(cat_summary)
|
|
716
|
+
|
|
717
|
+
cat_ul = Nokogiri::XML::Node.new("ul", doc)
|
|
718
|
+
cat_ul["class"] = "link-list nav-list"
|
|
719
|
+
|
|
720
|
+
examples.each do |example|
|
|
721
|
+
li = Nokogiri::XML::Node.new("li", doc)
|
|
722
|
+
a = Nokogiri::XML::Node.new("a", doc)
|
|
723
|
+
a["href"] = "#{prefix}#{example[:rdoc_path]}"
|
|
724
|
+
a.content = example[:title]
|
|
725
|
+
li.add_child(a)
|
|
726
|
+
cat_ul.add_child(li)
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
cat_details.add_child(cat_ul)
|
|
730
|
+
cat_li.add_child(cat_details)
|
|
731
|
+
examples_ul.add_child(cat_li)
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
# Insert Examples section before Classes and Modules
|
|
735
|
+
classindex_section.add_previous_sibling(examples_section)
|
|
736
|
+
|
|
737
|
+
# --- GUIDES SECTION ---
|
|
738
|
+
# Build dynamic hierarchical tree from doc/ folder structure
|
|
739
|
+
guides_tree = build_guides_tree
|
|
740
|
+
|
|
741
|
+
# Find and remove the doc details section from Pages
|
|
742
|
+
doc_detail = doc.css("details summary").find { |s| s.text.strip.downcase == "doc" }&.parent
|
|
743
|
+
doc_detail&.remove
|
|
744
|
+
|
|
745
|
+
# Create the Guides section
|
|
746
|
+
guides_section = Nokogiri::XML::Node.new("div", doc)
|
|
747
|
+
guides_section["id"] = "guidesindex-section"
|
|
748
|
+
guides_section["class"] = "nav-section"
|
|
749
|
+
|
|
750
|
+
guides_section.inner_html = <<~HTML
|
|
751
|
+
<details class="nav-section-collapsible" open>
|
|
752
|
+
<summary class="nav-section-header">
|
|
753
|
+
<span class="nav-section-icon">
|
|
754
|
+
<svg><use href="#icon-file"></use></svg>
|
|
755
|
+
</span>
|
|
756
|
+
<span class="nav-section-title">Guides</span>
|
|
757
|
+
<span class="nav-section-chevron">
|
|
758
|
+
<svg><use href="#icon-chevron"></use></svg>
|
|
759
|
+
</span>
|
|
760
|
+
</summary>
|
|
761
|
+
<ul class="link-list nav-list">
|
|
762
|
+
</ul>
|
|
763
|
+
</details>
|
|
764
|
+
HTML
|
|
765
|
+
|
|
766
|
+
# Get current file path relative to rdoc_dir (e.g. "doc/getting_started/quickstart_md.html")
|
|
767
|
+
current_file_rel = file.sub("#{rdoc_dir}/", "")
|
|
768
|
+
|
|
769
|
+
guides_ul = guides_section.at_css("ul.link-list")
|
|
770
|
+
build_guides_nav(guides_ul, guides_tree, doc, prefix, current_file_rel, "doc")
|
|
771
|
+
|
|
772
|
+
# Insert Guides section before Examples
|
|
773
|
+
examples_section.add_previous_sibling(guides_section)
|
|
774
|
+
|
|
775
|
+
content = doc.to_html
|
|
776
|
+
modified = true
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
# Also rewrite examples_md.html to examples/index.html
|
|
780
|
+
if content.include?("examples_md.html")
|
|
781
|
+
content = content.gsub(/href="([^"]*?)examples_md\.html"/, 'href="\1examples/index.html"')
|
|
782
|
+
modified = true
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
File.write(file, content) if modified
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
# Delete the now-unused examples_md.html
|
|
789
|
+
examples_page = "#{rdoc_dir}/examples_md.html"
|
|
790
|
+
FileUtils.rm_f(examples_page)
|
|
791
|
+
|
|
792
|
+
puts "Created Examples and Guides sections in sidebar"
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
# Build a hierarchical tree structure from doc/**/*.md files
|
|
796
|
+
def build_guides_tree
|
|
797
|
+
tree = { files: [], subdirs: {} }
|
|
798
|
+
|
|
799
|
+
Dir.glob("doc/**/*.md").each do |md_path|
|
|
800
|
+
# Skip images folder
|
|
801
|
+
next if md_path.include?("/images/")
|
|
802
|
+
|
|
803
|
+
relative = md_path.sub("doc/", "")
|
|
804
|
+
parts = relative.split("/")
|
|
805
|
+
filename = parts.pop
|
|
806
|
+
|
|
807
|
+
# Get title from H1
|
|
808
|
+
content = File.read(md_path)
|
|
809
|
+
title = if content =~ /^#\s+(.+)$/
|
|
810
|
+
$1.strip
|
|
811
|
+
else
|
|
812
|
+
filename.sub(/\.md$/, "").tr("_-", " ").split.map(&:capitalize).join(" ")
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
# Convert to RDoc path
|
|
816
|
+
rdoc_path = "doc/#{relative.gsub('.', '_')}.html"
|
|
817
|
+
|
|
818
|
+
# Navigate to correct position in tree
|
|
819
|
+
current = tree
|
|
820
|
+
parts.each do |dir|
|
|
821
|
+
current[:subdirs][dir] ||= { files: [], subdirs: {} }
|
|
822
|
+
current = current[:subdirs][dir]
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
current[:files] << { title:, rdoc_path:, filename: }
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
# Sort files in each level alphabetically by title
|
|
829
|
+
sort_guides_tree(tree)
|
|
830
|
+
tree
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
def sort_guides_tree(node)
|
|
834
|
+
node[:files].sort_by! { |f| f[:title] }
|
|
835
|
+
node[:subdirs].each_value { |subdir| sort_guides_tree(subdir) }
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
# Recursively build navigation elements from the tree
|
|
839
|
+
# current_file_rel: path of current HTML file relative to rdoc_dir (e.g. "doc/getting_started/quickstart_md.html")
|
|
840
|
+
# current_tree_path: path in the tree we're building (e.g. "doc", "doc/getting_started")
|
|
841
|
+
def build_guides_nav(parent_ul, tree, doc, prefix, current_file_rel, current_tree_path)
|
|
842
|
+
# Add files at this level first
|
|
843
|
+
tree[:files].each do |file|
|
|
844
|
+
# Check if this file is the current page
|
|
845
|
+
is_current = (file[:rdoc_path] == current_file_rel)
|
|
846
|
+
|
|
847
|
+
li = Nokogiri::XML::Node.new("li", doc)
|
|
848
|
+
a = Nokogiri::XML::Node.new("a", doc)
|
|
849
|
+
a["href"] = "#{prefix}#{file[:rdoc_path]}"
|
|
850
|
+
if is_current
|
|
851
|
+
a["class"] = "active"
|
|
852
|
+
strong = Nokogiri::XML::Node.new("strong", doc)
|
|
853
|
+
strong.content = file[:title]
|
|
854
|
+
a.add_child(strong)
|
|
855
|
+
else
|
|
856
|
+
a.content = file[:title]
|
|
857
|
+
end
|
|
858
|
+
li.add_child(a)
|
|
859
|
+
parent_ul.add_child(li)
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
# Add subdirectories as collapsible details
|
|
863
|
+
tree[:subdirs].each do |dir_name, subtree|
|
|
864
|
+
subdir_path = "#{current_tree_path}/#{dir_name}"
|
|
865
|
+
|
|
866
|
+
# Check if current file is inside this subdirectory
|
|
867
|
+
# current_file_rel might be "doc/getting_started/quickstart_md.html"
|
|
868
|
+
# subdir_path would be "doc/getting_started"
|
|
869
|
+
is_current_in_subdir = current_file_rel.start_with?("#{subdir_path}/")
|
|
870
|
+
|
|
871
|
+
li = Nokogiri::XML::Node.new("li", doc)
|
|
872
|
+
details = Nokogiri::XML::Node.new("details", doc)
|
|
873
|
+
# Open if current file is inside this subdir
|
|
874
|
+
details["open"] = "open" if is_current_in_subdir
|
|
875
|
+
summary = Nokogiri::XML::Node.new("summary", doc)
|
|
876
|
+
summary.content = dir_name.tr("_-", " ").split.map(&:capitalize).join(" ")
|
|
877
|
+
details.add_child(summary)
|
|
878
|
+
|
|
879
|
+
subdir_ul = Nokogiri::XML::Node.new("ul", doc)
|
|
880
|
+
subdir_ul["class"] = "link-list nav-list"
|
|
881
|
+
build_guides_nav(subdir_ul, subtree, doc, prefix, current_file_rel, subdir_path)
|
|
882
|
+
|
|
883
|
+
details.add_child(subdir_ul)
|
|
884
|
+
li.add_child(details)
|
|
885
|
+
parent_ul.add_child(li)
|
|
886
|
+
end
|
|
887
|
+
end
|