|
| 1 | +# _plugins/auto_nav.rb |
| 2 | +# ------------------------------------------------------------------ |
| 3 | +# Just‑the‑Docs auto‑navigation generator |
| 4 | +# |
| 5 | +# Scans the site’s root (excluding _site, _config.yml, _data, _posts, etc.) |
| 6 | +# and builds a `navigation.yml` file in `_data/` that matches the folder |
| 7 | +# structure and uses the file title (first H1) or the filename as fallback. |
| 8 | +# ------------------------------------------------------------------ |
| 9 | +module AutoNav |
| 10 | + class Generator < Jekyll::Generator |
| 11 | + safe true |
| 12 | + priority :low |
| 13 | + |
| 14 | + # Called by Jekyll |
| 15 | + def generate(site) |
| 16 | + nav = [] |
| 17 | + |
| 18 | + Dir.glob('**/*', File::FNM_DOTMATCH).each do |path| |
| 19 | + next if skip_path?(path) |
| 20 | + |
| 21 | + # Convert Windows backslashes to forward slashes |
| 22 | + path = path.tr('\\', '/') |
| 23 | + next unless File.extname(path) == '.md' |
| 24 | + |
| 25 | + # Build a relative URL |
| 26 | + url = File.basename(path, '.md') |
| 27 | + url = '/' if url == 'index' |
| 28 | + url = File.join('/', File.dirname(path), url, '.html') |
| 29 | + url = url.gsub('//', '/') |
| 30 | + |
| 31 | + # Grab the first H1 or fallback to filename |
| 32 | + title = extract_title(path) || File.basename(path, '.md').capitalize |
| 33 | + |
| 34 | + # Build a hierarchical structure |
| 35 | + add_to_nav(nav, path, title, url) |
| 36 | + end |
| 37 | + |
| 38 | + # Write the navigation file |
| 39 | + File.open('_data/navigation.yml', 'w') { |f| f.write(nav.to_yaml) } |
| 40 | + |
| 41 | + Jekyll.logger.info "AutoNav:", "Generated navigation.yml with #{nav.size} top‑level items." |
| 42 | + end |
| 43 | + |
| 44 | + private |
| 45 | + |
| 46 | + def skip_path?(path) |
| 47 | + # Skip hidden files, Jekyll defaults, and directories that start with _ |
| 48 | + File.directory?(path) || path.start_with?('_') || path =~ /\A\.{1,2}\z/ |
| 49 | + end |
| 50 | + |
| 51 | + # Return the first <h1> text from the markdown file |
| 52 | + def extract_title(md_path) |
| 53 | + content = File.read(md_path) |
| 54 | + # simple regex: line starting with # followed by space |
| 55 | + line = content.lines.find { |l| l.start_with?('# ') } |
| 56 | + line ? line.sub(/^# /, '').strip : nil |
| 57 | + end |
| 58 | + |
| 59 | + # Insert item into the nav hierarchy |
| 60 | + def add_to_nav(nav, path, title, url) |
| 61 | + parts = path.split('/').reject { |p| p.start_with?('_') } |
| 62 | + parts.pop # drop .md extension |
| 63 | + parts.map! { |p| p.sub(/\.md$/, '') } |
| 64 | + current = nav |
| 65 | + |
| 66 | + parts.each_with_index do |part, idx| |
| 67 | + item = current.find { |i| i['title'] == part } |
| 68 | + if item.nil? |
| 69 | + # new node |
| 70 | + item = { 'title' => part } |
| 71 | + item['sub_navigation'] = [] if idx < parts.size - 1 |
| 72 | + current << item |
| 73 | + end |
| 74 | + current = item['sub_navigation'] if idx < parts.size - 1 |
| 75 | + end |
| 76 | + |
| 77 | + # Final leaf |
| 78 | + leaf = { 'title' => title, 'url' => url } |
| 79 | + current << leaf |
| 80 | + end |
| 81 | + end |
| 82 | +end |
0 commit comments