|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +# Rake task to generate /public/llms.txt from doc views and routes. |
| 4 | +# |
| 5 | +# Usage: |
| 6 | +# rake llms:generate |
| 7 | +# rake llms:generate[https://rubyui.com] # custom base URL |
| 8 | +# |
| 9 | +# Run this after adding/removing components to keep llms.txt in sync. |
| 10 | + |
| 11 | +namespace :llms do |
| 12 | + desc "Generate public/llms.txt from doc view files" |
| 13 | + task :generate, [:base_url] do |_t, args| |
| 14 | + base_url = (args[:base_url] || "https://rubyui.com").chomp("/") |
| 15 | + docs_dir = File.expand_path("../../app/views/docs", __dir__) |
| 16 | + |
| 17 | + # Category mapping: filename => [category, subcategory] |
| 18 | + categories = { |
| 19 | + # Form & Input |
| 20 | + "button" => "Form & Input", |
| 21 | + "input" => "Form & Input", |
| 22 | + "masked_input" => "Form & Input", |
| 23 | + "textarea" => "Form & Input", |
| 24 | + "checkbox" => "Form & Input", |
| 25 | + "checkbox_group" => "Form & Input", |
| 26 | + "radio_button" => "Form & Input", |
| 27 | + "select" => "Form & Input", |
| 28 | + "switch" => "Form & Input", |
| 29 | + "calendar" => "Form & Input", |
| 30 | + "date_picker" => "Form & Input", |
| 31 | + "combobox" => "Form & Input", |
| 32 | + "form" => "Form & Input", |
| 33 | + # Layout & Navigation |
| 34 | + "accordion" => "Layout & Navigation", |
| 35 | + "breadcrumb" => "Layout & Navigation", |
| 36 | + "tabs" => "Layout & Navigation", |
| 37 | + "sidebar" => "Layout & Navigation", |
| 38 | + "separator" => "Layout & Navigation", |
| 39 | + "pagination" => "Layout & Navigation", |
| 40 | + "link" => "Layout & Navigation", |
| 41 | + "collapsible" => "Layout & Navigation", |
| 42 | + # Overlays & Dialogs |
| 43 | + "dialog" => "Overlays & Dialogs", |
| 44 | + "alert_dialog" => "Overlays & Dialogs", |
| 45 | + "sheet" => "Overlays & Dialogs", |
| 46 | + "popover" => "Overlays & Dialogs", |
| 47 | + "tooltip" => "Overlays & Dialogs", |
| 48 | + "hover_card" => "Overlays & Dialogs", |
| 49 | + "context_menu" => "Overlays & Dialogs", |
| 50 | + "dropdown_menu" => "Overlays & Dialogs", |
| 51 | + "command" => "Overlays & Dialogs", |
| 52 | + # Feedback & Status |
| 53 | + "alert" => "Feedback & Status", |
| 54 | + "progress" => "Feedback & Status", |
| 55 | + "skeleton" => "Feedback & Status", |
| 56 | + "badge" => "Feedback & Status", |
| 57 | + # Display & Media |
| 58 | + "avatar" => "Display & Media", |
| 59 | + "card" => "Display & Media", |
| 60 | + "table" => "Display & Media", |
| 61 | + "chart" => "Display & Media", |
| 62 | + "carousel" => "Display & Media", |
| 63 | + "aspect_ratio" => "Display & Media", |
| 64 | + "typography" => "Display & Media", |
| 65 | + "codeblock" => "Display & Media", |
| 66 | + "clipboard" => "Display & Media", |
| 67 | + "shortcut_key" => "Display & Media", |
| 68 | + "theme_toggle" => "Display & Media" |
| 69 | + } |
| 70 | + |
| 71 | + # Parse title and description from a doc view .rb file |
| 72 | + def extract_header(file_path) |
| 73 | + content = File.read(file_path) |
| 74 | + if content =~ /Docs::Header\.new\(title:\s*(?:"([^"]+)"|'([^']+)'|(\w+))\s*,\s*description:\s*"([^"]+)"/ |
| 75 | + title = $1 || $2 || $3 |
| 76 | + description = $4 |
| 77 | + # If title is a variable reference (like `component`), look for the assignment |
| 78 | + if title =~ /\A[a-z_]+\z/ |
| 79 | + content.match(/#{title}\s*=\s*"([^"]+)"/) { |m| title = m[1] } |
| 80 | + end |
| 81 | + [title, description] |
| 82 | + end |
| 83 | + end |
| 84 | + |
| 85 | + # Collect components |
| 86 | + components_by_category = Hash.new { |h, k| h[k] = [] } |
| 87 | + Dir.glob(File.join(docs_dir, "*.rb")).each do |file| |
| 88 | + next if File.basename(file) == "base.rb" |
| 89 | + |
| 90 | + slug = File.basename(file, ".rb") |
| 91 | + result = extract_header(file) |
| 92 | + next unless result |
| 93 | + |
| 94 | + title, description = result |
| 95 | + category = categories[slug] || "Miscellaneous" |
| 96 | + components_by_category[category] << { slug: slug, title: title, description: description } |
| 97 | + end |
| 98 | + |
| 99 | + # Build llms.txt |
| 100 | + lines = [] |
| 101 | + lines << "# RubyUI" |
| 102 | + lines << "" |
| 103 | + lines << "> RubyUI is a UI component library for Ruby developers, built on top of Phlex, TailwindCSS, and Stimulus JS. Components are inspired by shadcn/ui and use compatible theming with CSS variables. Install via the ruby_ui gem into any Rails application. Components are written in pure Ruby using Phlex (up to 12x faster than ERB) and use custom Stimulus.js controllers for interactivity." |
| 104 | + lines << "" |
| 105 | + |
| 106 | + # Getting Started section |
| 107 | + lines << "## Getting Started" |
| 108 | + lines << "" |
| 109 | + lines << "- [Introduction](#{base_url}/docs/introduction): Overview of RubyUI, core ingredients (Phlex, TailwindCSS, Stimulus JS), and design philosophy." |
| 110 | + lines << "- [Installation](#{base_url}/docs/installation): How to install RubyUI in your Rails application." |
| 111 | + lines << "- [Installation with Rails Bundler](#{base_url}/docs/installation/rails_bundler): Setup using Rails with bundler and JS bundling." |
| 112 | + lines << "- [Installation with Rails Importmaps](#{base_url}/docs/installation/rails_importmaps): Setup using Rails with importmaps." |
| 113 | + lines << "- [Theming](#{base_url}/docs/theming): Guide to customizing colors and design tokens using CSS variables." |
| 114 | + lines << "- [Dark Mode](#{base_url}/docs/dark_mode): How to implement dark mode support." |
| 115 | + lines << "- [Customizing Components](#{base_url}/docs/customizing_components): How to customize and extend RubyUI components." |
| 116 | + lines << "" |
| 117 | + |
| 118 | + # Components section |
| 119 | + lines << "## Components" |
| 120 | + lines << "" |
| 121 | + |
| 122 | + category_order = ["Form & Input", "Layout & Navigation", "Overlays & Dialogs", "Feedback & Status", "Display & Media", "Miscellaneous"] |
| 123 | + category_order.each do |category| |
| 124 | + next unless components_by_category.key?(category) |
| 125 | + |
| 126 | + items = components_by_category[category].sort_by { |c| c[:title] } |
| 127 | + lines << "### #{category}" |
| 128 | + lines << "" |
| 129 | + items.each do |comp| |
| 130 | + lines << "- [#{comp[:title]}](#{base_url}/docs/#{comp[:slug]}): #{comp[:description]}" |
| 131 | + end |
| 132 | + lines << "" |
| 133 | + end |
| 134 | + |
| 135 | + output_path = File.expand_path("../../public/llms.txt", __dir__) |
| 136 | + File.write(output_path, lines.join("\n")) |
| 137 | + puts "Generated #{output_path} (#{components_by_category.values.sum(&:length)} components)" |
| 138 | + end |
| 139 | +end |
0 commit comments