Skip to content
This repository was archived by the owner on May 11, 2026. It is now read-only.

Commit 4559c5b

Browse files
dariyeclaude
andcommitted
Extract LlmsTxtGenerator class and add tests
Refactor generation logic from rake task into a standalone LlmsTxtGenerator class in lib/ for testability. The rake task now delegates to the class. Regenerate public/llms.txt from the generator (alphabetical sort within categories, exact descriptions from source). 31 tests covering: - Header extraction (string titles, variable titles, fixtures) - Component collection (categories, counts, structure) - Content generation (sections, URLs, sorting, spec compliance) - File writing (output, idempotency, sync check) - llms.txt spec compliance (H1, blockquote, H2 sections, link format) - Route cross-validation (all 52 URL slugs verified) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0f9df93 commit 4559c5b

4 files changed

Lines changed: 482 additions & 149 deletions

File tree

lib/llms_txt_generator.rb

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

lib/tasks/llms_txt.rake

Lines changed: 10 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,25 @@
11
# frozen_string_literal: true
22

3-
# Rake task to generate /public/llms.txt from doc views and routes.
3+
require_relative "../llms_txt_generator"
4+
5+
# Rake task to generate /public/llms.txt from doc views.
46
#
57
# Usage:
68
# rake llms:generate
79
# rake llms:generate[https://rubyui.com] # custom base URL
8-
#
9-
# Run this after adding/removing components to keep llms.txt in sync.
1010

1111
namespace :llms do
1212
desc "Generate public/llms.txt from doc view files"
1313
task :generate, [:base_url] do |_t, args|
14-
base_url = (args[:base_url] || "https://rubyui.com").chomp("/")
1514
docs_dir = File.expand_path("../../app/views/docs", __dir__)
15+
output_path = File.expand_path("../../public/llms.txt", __dir__)
1616

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
17+
generator = LlmsTxtGenerator.new(
18+
docs_dir: docs_dir,
19+
base_url: args[:base_url] || "https://rubyui.com"
20+
)
13421

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)"
22+
count = generator.write(output_path)
23+
puts "Generated #{output_path} (#{count} components)"
13824
end
13925
end

0 commit comments

Comments
 (0)