Skip to content
Draft
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
14 changes: 3 additions & 11 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ jobs:
ruby-versions:
uses: ruby/actions/.github/workflows/ruby_versions.yml@master
with:
# 2.7 breaks `test_parse_statements_nodoc_identifier_alias_method`
min_version: 3.0
min_version: 3.2
versions: '["mswin"]'
engine: cruby

test:
needs: ruby-versions
Expand All @@ -26,14 +26,6 @@ jobs:
ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }}
os: [ubuntu-latest, macos-latest, windows-latest]
exclude:
- os: windows-latest
ruby: truffleruby
- os: windows-latest
ruby: truffleruby-head
- os: windows-latest
ruby: jruby
- os: windows-latest
ruby: jruby-head
- os: macos-latest
ruby: mswin
- os: ubuntu-latest
Expand Down Expand Up @@ -68,7 +60,7 @@ jobs:
strategy:
fail-fast: false
matrix:
prism_version: ['1.0.0', '1.3.0', '1.7.0', 'head']
prism_version: ['1.6.0', '1.7.0', 'head']
runs-on: ubuntu-latest
env:
RUBYOPT: --enable-frozen_string_literal
Expand Down
27 changes: 27 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,30 @@ Please refer to `AGENTS.md` for comprehensive project documentation, including:
- CI/CD information

All project-specific instructions and guidelines are maintained in `AGENTS.md`.

## Design Context

**Personality:** Minimal, focused, fast. A well-organized reference tool — never decorative, never in the way.

**References:** Elixir HexDocs, Tailwind CSS v2 docs. **Anti-references:** busy enterprise docs, heavy drop shadows, ornamental borders.

### Design Principles

1. **Types are equal partners** — Type signatures are as important as method names. Immediately visible, not hidden metadata.
2. **Hierarchy through typography, not decoration** — Font size, weight, color. No badges, pills, or ornamental borders for structural info. Let whitespace do the heavy lifting.
3. **Code is the content** — Method names, params, and types all use `--font-code`. Don't mix prose typography into code contexts.
4. **Scan-first design** — Method entries parseable at a glance: name → type → description. Each layer visually distinct.
5. **Respect the design system** — Use CSS custom properties exclusively. No hardcoded values. Dark mode and themes must work automatically.

### Design Tokens (Aliki Theme)

| Token | Light | Dark | Usage |
|-------|-------|------|-------|
| `--color-text-primary` | `#1c1917` | `#fafaf9` | Method names, headings |
| `--color-text-secondary` | `#57534e` | `#e7e5e4` | Type signatures, descriptions |
| `--color-text-tertiary` | `#78716c` | `#a8a29e` | De-emphasized metadata |
| `--font-code` | ui-monospace stack | same | All code: names, params, types |
| `--font-size-lg` | 18px | same | Method headings |
| `--font-size-sm` | 14px | same | Type signatures |
| `--font-size-xs` | 12px | same | Metadata, labels |
| `--space-1` to `--space-6` | 4px–24px | same | All spacing |
4 changes: 1 addition & 3 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,5 @@ elsif ENV['PRISM_VERSION']
end

platforms :ruby do
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.2')
gem 'mini_racer' # For testing the searcher.js file
end
gem 'mini_racer' # For testing the searcher.js file
end
4 changes: 3 additions & 1 deletion lib/rdoc/code_object/any_method.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class RDoc::AnyMethod < RDoc::MethodAttr
# RDoc 4.1
# Added is_alias_for

MARSHAL_VERSION = 3 # :nodoc:
MARSHAL_VERSION = 4 # :nodoc:

##
# Don't rename \#initialize to \::new
Expand Down Expand Up @@ -166,6 +166,7 @@ def marshal_dump
@parent.class,
@section.title,
is_alias_for,
@type_signature,
]
end

Expand Down Expand Up @@ -204,6 +205,7 @@ def marshal_load(array)
@parent_title = array[13]
@section_title = array[14]
@is_alias_for = array[15]
@type_signature = array[16]

array[8].each do |new_name, document|
add_alias RDoc::Alias.new(nil, @name, new_name, RDoc::Comment.from_document(document), singleton: @singleton)
Expand Down
6 changes: 4 additions & 2 deletions lib/rdoc/code_object/attr.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class RDoc::Attr < RDoc::MethodAttr
# Added parent name and class
# Added section title

MARSHAL_VERSION = 3 # :nodoc:
MARSHAL_VERSION = 4 # :nodoc:

##
# Is the attribute readable ('R'), writable ('W') or both ('RW')?
Expand Down Expand Up @@ -108,7 +108,8 @@ def marshal_dump
@file.relative_name,
@parent.full_name,
@parent.class,
@section.title
@section.title,
@type_signature,
]
end

Expand Down Expand Up @@ -140,6 +141,7 @@ def marshal_load(array)
@parent_name = array[8]
@parent_class = array[9]
@section_title = array[10]
@type_signature = array[11]

@file = RDoc::TopLevel.new array[7] if version > 1

Expand Down
6 changes: 6 additions & 0 deletions lib/rdoc/code_object/method_attr.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ class RDoc::MethodAttr < RDoc::CodeObject

attr_accessor :call_seq

##
# RBS type signature from inline #: annotations

attr_accessor :type_signature

##
# The call_seq or the param_seq with method name, if there is no call_seq.

Expand Down Expand Up @@ -86,6 +91,7 @@ def initialize(text, name, singleton: false)
@block_params = nil
@call_seq = nil
@params = nil
@type_signature = nil
end

##
Expand Down
5 changes: 5 additions & 0 deletions lib/rdoc/generator/template/aliki/_head.rhtml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@
defer
></script>

<script
src="<%= h asset_rel_prefix %>/js/rbs_highlighter.js?v=<%= h RDoc::VERSION %>"
defer
></script>

<script
src="<%= h asset_rel_prefix %>/js/aliki.js?v=<%= h RDoc::VERSION %>"
defer
Expand Down
13 changes: 11 additions & 2 deletions lib/rdoc/generator/template/aliki/class.rhtml
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@
<div class="method-heading attribute-method-heading">
<a href="#<%= attrib.aref %>" title="Link to this attribute">
<span class="method-name"><%= h attrib.name %></span>
<span class="attribute-access-type">[<%= attrib.rw %>]</span>
</a>
<span class="attribute-access-type">[<%= attrib.rw %>]</span></a><%- if attrib.type_signature %>
<span class="method-type-signature"><code><%= h attrib.type_signature %></code></span>
<%- end %>
</div>

<div class="method-description">
Expand Down Expand Up @@ -150,6 +151,14 @@
</a>
</div>
<%- end %>

<%- if method.type_signature %>
<div class="method-type-signature">
<%- method.type_signature.split("\n").each do |sig| %>
<code><%= h(sig).gsub('-&gt;', '&rarr;') %></code>
<%- end %>
</div>
<%- end %>
</div>

<%- if method.token_stream %>
Expand Down
33 changes: 33 additions & 0 deletions lib/rdoc/generator/template/aliki/css/rdoc.css
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,18 @@ main h6 a:hover {
font-style: italic;
}

/* RBS Type Signature Highlighting — only types and builtins get color */
.rbs-type { color: var(--code-blue); }
.rbs-builtin { color: var(--code-purple); }

a.rbs-type {
text-decoration: none;
}

a.rbs-type:hover {
text-decoration: underline;
}

/* Emphasis */
em {
text-decoration-color: var(--color-emphasis-decoration);
Expand Down Expand Up @@ -1312,6 +1324,27 @@ main .method-heading .method-args {
font-weight: var(--font-weight-normal);
}

/* Type signatures — method overloads stack vertically under the name */
main .method-header .method-type-signature {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
}

main .method-type-signature code {
font-family: var(--font-code);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);
color: var(--color-text-secondary);
background: transparent;
}

/* Attribute type sigs render inline after the [RW] badge */
main .method-heading > .method-type-signature {
display: inline;
margin-left: var(--space-2);
}

main .method-controls {
float: right;
}
Expand Down
116 changes: 116 additions & 0 deletions lib/rdoc/generator/template/aliki/js/rbs_highlighter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* Client-side RBS type signature highlighter for RDoc
*
* Highlights type names and built-in keywords in inline RBS annotations.
* Links type names to their documentation pages using the search index.
*
* NOTE: innerHTML usage is safe here — input is the element's own textContent
* (not user-supplied) and all output is escaped through escapeHtml(). This
* follows the same pattern as c_highlighter.js and bash_highlighter.js.
*/

(function() {
'use strict';

var BUILTIN_TYPES = new Set([
'void', 'untyped', 'nil', 'bool', 'self', 'top', 'bot',
'instance', 'class', 'true', 'false'
]);

var typeLookup = null;

function buildTypeLookup() {
var lookup = {};
if (!window.search_data || !window.search_data.index) return lookup;

window.search_data.index.forEach(function(entry) {
if (entry.type === 'class' || entry.type === 'module') {
lookup[entry.full_name] = entry.path;
// Also map short name if not already taken
var short = entry.name;
if (!lookup[short]) lookup[short] = entry.path;
}
});
return lookup;
}

function escapeHtml(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}

function isIdentChar(ch) {
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') || ch === '_';
}

function highlightRbs(text) {
var tokens = [];
var i = 0;
var len = text.length;
var prefix = typeof rdoc_rel_prefix !== 'undefined' ? rdoc_rel_prefix : '';

while (i < len) {
var ch = text[i];

// Uppercase identifier — possibly a type name, collect qualified name (Foo::Bar)
if (ch >= 'A' && ch <= 'Z') {
var end = i + 1;
while (end < len && isIdentChar(text[end])) end++;
// Collect :: continuations
while (end + 1 < len && text[end] === ':' && text[end + 1] === ':') {
end += 2;
while (end < len && isIdentChar(text[end])) end++;
}
var name = text.substring(i, end);
var href = typeLookup ? typeLookup[name] : null;

if (href) {
tokens.push('<a href="' + prefix + href + '" class="rbs-type">' + escapeHtml(name) + '</a>');
} else {
tokens.push('<span class="rbs-type">' + escapeHtml(name) + '</span>');
}
i = end;
continue;
}

// Lowercase identifier — check for builtin keywords
if ((ch >= 'a' && ch <= 'z') || ch === '_') {
var end = i + 1;
while (end < len && isIdentChar(text[end])) end++;
var word = text.substring(i, end);

if (BUILTIN_TYPES.has(word)) {
tokens.push('<span class="rbs-builtin">' + escapeHtml(word) + '</span>');
} else {
tokens.push(escapeHtml(word));
}
i = end;
continue;
}

tokens.push(escapeHtml(ch));
i++;
}

return tokens.join('');
}

function initHighlighting() {
typeLookup = buildTypeLookup();

document.querySelectorAll('.method-type-signature code').forEach(function(el) {
if (el.getAttribute('data-highlighted') === 'true') return;
el.innerHTML = highlightRbs(el.textContent); // eslint-disable-line no-unsanitized/property
el.setAttribute('data-highlighted', 'true');
});
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initHighlighting);
} else {
initHighlighting();
}
})();
Loading