Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 49 additions & 17 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ bundle exec rake rerdoc
# Show documentation coverage
bundle exec rake rdoc:coverage
bundle exec rake coverage

# Start live-reloading preview server (port 4000)
bundle exec rake rdoc:server

# Or via CLI with custom port
bundle exec rdoc --server=8080
```

**Output Directory:** `_site/` (GitHub Pages compatible)
Expand Down Expand Up @@ -176,6 +182,7 @@ lib/rdoc/
│ ├── c.rb # C extension parser
│ ├── prism_ruby.rb # Prism-based Ruby parser
│ └── ...
├── server.rb # Live-reloading preview server (rdoc --server)
├── generator/ # Documentation generators
│ ├── aliki.rb # HTML generator (default theme)
│ ├── darkfish.rb # HTML generator (deprecated, will be removed in v8.0)
Expand Down Expand Up @@ -232,6 +239,30 @@ exe/
- **Parsers:** Ruby, C, Markdown, RD, Prism-based Ruby (experimental)
- **Generators:** HTML/Aliki (default), HTML/Darkfish (deprecated), RI, POT (gettext), JSON, Markup

### Live Preview Server (`RDoc::Server`)

The server (`lib/rdoc/server.rb`) provides `rdoc --server` for live documentation preview.

**Architecture:**
- Uses Ruby's built-in `TCPServer` (`socket` stdlib) — no WEBrick or external dependencies
- Creates a persistent `RDoc::Generator::Aliki` instance with `file_output = false` (renders to strings)
- Thread-per-connection HTTP handling with `Connection: close` (no keep-alive)
- Background watcher thread polls file mtimes every 1 second
- Live reload via inline JS polling `/__status` endpoint

**Key files:**
- `lib/rdoc/server.rb` — HTTP server, routing, caching, file watcher
- `lib/rdoc/rdoc.rb` — `start_server` method, server branch in `document`
- `lib/rdoc/options.rb` — `--server[=PORT]` option
- `lib/rdoc/generator/darkfish.rb` — `refresh_store_data` (extracted for server reuse)
- `lib/rdoc/store.rb` — `remove_file` (for deleted file handling)
- `lib/rdoc/task.rb` — `rdoc:server` Rake task

**Known limitations:**
- Reopened classes: deleting a file that partially defines a class removes the entire class from the store (save the other file to restore)
- Template/CSS changes require server restart (only source files are watched)
- Full page cache invalidation on any change (rendering is fast, so this is acceptable)

## Common Workflows

Do NOT commit anything. Ask the developer to review the changes after tasks are finished.
Expand Down Expand Up @@ -319,20 +350,17 @@ When editing markup reference documentation, such as `doc/markup_reference/markd

When making changes to theme CSS or templates (e.g., Darkfish or Aliki themes):

1. **Generate documentation**: Run `bundle exec rake rerdoc` to create baseline
2. **Start HTTP server**: Run `cd _site && python3 -m http.server 8000` (use different port if 8000 is in use)
3. **Investigate with Playwright**: Ask the AI assistant to take screenshots and inspect the documentation visually
- Example: "Navigate to the docs at localhost:8000 and screenshot the RDoc module page"
1. **Start the live-reloading server**: Run `bundle exec rdoc --server` (or `bundle exec rake rdoc:server`)
2. **Make changes**: Edit files in `lib/rdoc/generator/template/<theme>/` or source code
3. **Browser auto-refreshes**: The server detects file changes and refreshes the browser automatically
4. **Investigate with Playwright**: Ask the AI assistant to take screenshots and inspect the documentation visually
- Example: "Navigate to the docs at localhost:4000 and screenshot the RDoc module page"
- See "Playwright MCP for Testing Generated Documentation" section below for details
4. **Make changes**: Edit files in `lib/rdoc/generator/template/<theme>/` as needed
5. **Regenerate**: Run `bundle exec rake rerdoc` to rebuild documentation with changes
6. **Verify with Playwright**: Take new screenshots and compare to original issues
7. **Lint changes** (if modified):
5. **Lint changes** (if modified):
- ERB templates: `npx @herb-tools/linter "lib/rdoc/generator/template/**/*.rhtml"`
- CSS files: `npm run lint:css -- --fix`
8. **Stop server**: Kill the HTTP server process when done

**Tip:** Keep HTTP server running during iteration. Just regenerate with `bundle exec rake rerdoc` between changes.
**Note:** The server watches source files, not template files. If you modify `.rhtml` templates or CSS in the template directory, restart the server to pick up those changes.

## Notes for AI Agents

Expand Down Expand Up @@ -386,23 +414,27 @@ Restart Claude Code after running these commands.

### Testing Generated Documentation

To test the generated documentation:
The easiest way to test documentation is with the live-reloading server:

```bash
# Generate documentation
bundle exec rake rerdoc
bundle exec rdoc --server
# Or: bundle exec rake rdoc:server
```

This starts a server at `http://localhost:4000` that auto-refreshes on file changes.

Alternatively, for testing static output:

# Start a simple HTTP server in the _site directory (use an available port)
```bash
bundle exec rake rerdoc
cd _site && python3 -m http.server 8000
```

If port 8000 is already in use, try another port (e.g., `python3 -m http.server 9000`).

Then ask the AI assistant to inspect the documentation. It will use the appropriate Playwright tools (`browser_navigate`, `browser_snapshot`, `browser_take_screenshot`, etc.) based on your request.

**Example requests:**

- "Navigate to `http://localhost:8000` and take a screenshot"
- "Navigate to `http://localhost:4000` and take a screenshot"
- "Take a screenshot of the RDoc module page"
- "Check if code blocks are rendering properly on the Markup page"
- "Compare the index page before and after my CSS changes"
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,41 @@ There are also a few community-maintained themes for RDoc:

Please follow the theme's README for usage instructions.

## Live Preview Server

RDoc includes a built-in server for previewing documentation while you edit source files. It parses your code once on startup, then watches for changes and auto-refreshes the browser.

```shell
rdoc --server
```

This starts a server at `http://localhost:4000`. You can specify a different port:

```shell
rdoc --server=8080
```

Or use the Rake task:

```shell
rake rdoc:server
```

### How It Works

- Parses all source files on startup and serves pages from memory using the Aliki theme
- A background thread polls file mtimes every second
- When a file changes, only that file is re-parsed — the browser refreshes automatically
- New files are detected and added; deleted files are removed

**No external dependencies.** The server uses Ruby's built-in `TCPServer` (`socket` stdlib) — no WEBrick or other gems required.

### Limitations

- **Reopened classes and file deletion.** If a class is defined across multiple files (e.g. `Foo` in both `a.rb` and `b.rb`), deleting one file removes the entire class from the store, including parts from the other file. Saving the remaining file triggers a re-parse that restores it.
- **Full cache invalidation.** Any file change clears all cached pages. This is simple and correct — rendering is fast (~ms per page), parsing is the expensive part and is done incrementally.
- **No HTTPS or HTTP/2.** The server is intended for local development preview only.

## Bugs

See [CONTRIBUTING.md](CONTRIBUTING.md) for information on filing a bug report. It's OK to file a bug report for anything you're having a problem with. If you can't figure out how to make RDoc produce the output you like that is probably a documentation bug.
Expand Down
2 changes: 1 addition & 1 deletion lib/rdoc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def self.home
autoload :Generator, "#{__dir__}/rdoc/generator"
autoload :Options, "#{__dir__}/rdoc/options"
autoload :Parser, "#{__dir__}/rdoc/parser"
autoload :Servlet, "#{__dir__}/rdoc/servlet"
autoload :Server, "#{__dir__}/rdoc/server"
autoload :RI, "#{__dir__}/rdoc/ri"
autoload :Stats, "#{__dir__}/rdoc/stats"
autoload :Store, "#{__dir__}/rdoc/store"
Expand Down
9 changes: 9 additions & 0 deletions lib/rdoc/generator/darkfish.rb
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,15 @@ def setup

return unless @store

refresh_store_data
end

##
# Refreshes the generator's data from the store. Called by #setup and
# can be called again after the store has been updated (e.g. in server
# mode after re-parsing changed files).

def refresh_store_data
@classes = @store.all_classes_and_modules.sort
@files = @store.all_files.sort
@methods = @classes.flat_map { |m| m.method_list }.sort
Expand Down
18 changes: 18 additions & 0 deletions lib/rdoc/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ class RDoc::Options
pipe
rdoc_include
root
server_port
static_path
template
template_dir
Expand Down Expand Up @@ -328,6 +329,13 @@ class RDoc::Options

attr_reader :visibility

##
# When set to a port number, starts a live-reloading server instead of
# writing files. Defaults to +false+ (no server). Set via
# <tt>--server[=PORT]</tt>.

attr_reader :server_port

##
# Indicates if files of test suites should be skipped
attr_accessor :skip_tests
Expand Down Expand Up @@ -410,6 +418,7 @@ def init_ivars # :nodoc:
@output_decoration = true
@rdoc_include = []
@root = Pathname(Dir.pwd)
@server_port = false
@show_hash = false
@static_path = []
@tab_width = 8
Expand Down Expand Up @@ -1123,6 +1132,15 @@ def parse(argv)
opt.separator "Generic options:"
opt.separator nil

opt.on("--server[=PORT]", Integer,
"Start a web server to preview",
"documentation with live reload.",
"Defaults to port 4000.") do |port|
@server_port = port || 4000
end

opt.separator nil

opt.on("--write-options",
"Write .rdoc_options to the current",
"directory with the given options. Not all",
Expand Down
26 changes: 26 additions & 0 deletions lib/rdoc/rdoc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,19 @@ def document(options)
exit
end

if @options.server_port
@store.load_cache

parse_files @options.files

@options.default_title = "RDoc Documentation"

@store.complete @options.visibility

start_server
return
end

unless @options.coverage_report then
@last_modified = setup_output_dir @options.op_dir, @options.force_update
end
Expand Down Expand Up @@ -516,6 +529,19 @@ def generate
end
end

##
# Starts a live-reloading HTTP server for previewing documentation.
# Called from #document when <tt>--server</tt> is given.

def start_server
server = RDoc::Server.new(self, @options.server_port)

trap('INT') { server.shutdown }
trap('TERM') { server.shutdown }

server.start
end

##
# Removes a siginfo handler and replaces the previous

Expand Down
7 changes: 4 additions & 3 deletions lib/rdoc/ri.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ module RDoc::RI

class Error < RDoc::Error; end

autoload :Driver, "#{__dir__}/ri/driver"
autoload :Paths, "#{__dir__}/ri/paths"
autoload :Store, "#{__dir__}/ri/store"
autoload :Driver, "#{__dir__}/ri/driver"
autoload :Paths, "#{__dir__}/ri/paths"
autoload :Servlet, "#{__dir__}/ri/servlet"
autoload :Store, "#{__dir__}/ri/store"

end
2 changes: 1 addition & 1 deletion lib/rdoc/ri/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1521,7 +1521,7 @@ def start_server

extra_doc_dirs = @stores.map {|s| s.type == :extra ? s.path : nil}.compact

server.mount '/', RDoc::Servlet, nil, extra_doc_dirs
server.mount '/', RDoc::RI::Servlet, nil, extra_doc_dirs

trap 'INT' do server.shutdown end
trap 'TERM' do server.shutdown end
Expand Down
8 changes: 4 additions & 4 deletions lib/rdoc/servlet.rb → lib/rdoc/ri/servlet.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true
require_relative '../rdoc'
require_relative '../../rdoc'
require 'erb'
require 'time'
require 'json'
Expand All @@ -24,14 +24,14 @@
#
# server = WEBrick::HTTPServer.new Port: 8000
#
# server.mount '/', RDoc::Servlet
# server.mount '/', RDoc::RI::Servlet
#
# If you want to mount the servlet some other place than the root, provide the
# base path when mounting:
#
# server.mount '/rdoc', RDoc::Servlet, '/rdoc'
# server.mount '/rdoc', RDoc::RI::Servlet, '/rdoc'

class RDoc::Servlet < WEBrick::HTTPServlet::AbstractServlet
class RDoc::RI::Servlet < WEBrick::HTTPServlet::AbstractServlet

@server_stores = Hash.new { |hash, server| hash[server] = {} }
@cache = Hash.new { |hash, store| hash[store] = {} }
Expand Down
Loading