Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add constants to search index #358

Merged
merged 1 commit into from
Jan 4, 2024
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Main (3.0.0.alpha)
* [#345](https://github.com/rails/sdoc/pull/345) Version explicit links to `api.rubyonrails.org` [@jonathanhefner](https://github.com/jonathanhefner)
* [#356](https://github.com/rails/sdoc/pull/356) Redesign "Constants" section [@jonathanhefner](https://github.com/jonathanhefner)
* [#357](https://github.com/rails/sdoc/pull/357) Support permalinking constants [@jonathanhefner](https://github.com/jonathanhefner)
* [#358](https://github.com/rails/sdoc/pull/358) Add constants to search index [@jonathanhefner](https://github.com/jonathanhefner)

2.6.1
=====
Expand Down
8 changes: 4 additions & 4 deletions lib/rdoc/generator/template/rails/resources/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ html {
}

/* Hide TIP when one of the first three search results is a method. */
.panel__results:has(.results__result:nth-child(n+1):nth-child(-n+3) .result__method:not(:empty))::before {
.panel__results:has(.results__result:nth-child(n+1):nth-child(-n+3) .result__member:not(:empty))::before {
display: none;
}

Expand Down Expand Up @@ -542,11 +542,11 @@ html {
flex-direction: column;
}

.result__method {
.result__member {
display: flex;
}

.result__method::before {
.result__member::before {
content: "\221F";
font-size: 1.25em;
margin: -0.3em 0.2em;
Expand All @@ -557,7 +557,7 @@ html {
margin: var(--space-xs) 0 0 0;
}

:is(.result__method, .result__summary):empty {
:is(.result__member, .result__summary):empty {
display: none;
}

Expand Down
4 changes: 2 additions & 2 deletions lib/rdoc/generator/template/rails/resources/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { Search } from "./search.js";
document.addEventListener("turbo:load", () => {
const searchInput = document.getElementById("search");
const searchOutput = document.getElementById("results");
const search = new Search(searchInput, searchOutput, (url, module, method, summary) =>
const search = new Search(searchInput, searchOutput, (url, module, member, summary) =>
`<div class="results__result">
<a class="ref-link result__link" href="${url}">
<code class="result__module">${module.replaceAll("::", "::<wbr>")}</code>
<code class="result__method">${method || ""}</code>
<code class="result__member">${member || ""}</code>
</a>
<p class="result__summary description">${summary || ""}</p>
</div>`
Expand Down
53 changes: 34 additions & 19 deletions lib/sdoc/search_index.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,47 @@ def to_json(*)
end

def generate(rdoc_modules)
# RDoc duplicates RDoc::MethodAttr instances when modules are aliased by
# assigning to a constant. For example, `MyBar = Foo::Bar` will duplicate
# all of Foo::Bar's RDoc::MethodAttr instances.
rdoc_objects = rdoc_modules + rdoc_modules.flat_map(&:method_list).uniq
rdoc_objects = rdoc_modules + rdoc_modules.flat_map(&:constants) + rdoc_modules.flat_map(&:method_list)

# RDoc duplicates member instances when modules are aliased by assigning to
# a constant. For example, `MyBar = Foo::Bar` will duplicate all of
# Foo::Bar's RDoc::Constant and RDoc::MethodAttr instances.
rdoc_objects.uniq!

ngram_sets = rdoc_objects.map { |rdoc_object| derive_ngrams(rdoc_object.full_name) }
ngram_bit_positions = compile_ngrams(ngram_sets)
bit_weights = compute_bit_weights(ngram_bit_positions)

entries = rdoc_objects.zip(ngram_sets).map do |rdoc_object, ngrams|
rdoc_module, rdoc_method = rdoc_object.is_a?(RDoc::ClassModule) ? [rdoc_object] : [rdoc_object.parent, rdoc_object]
description = rdoc_object.description

[
generate_fingerprint(ngrams, ngram_bit_positions),
compute_tiebreaker_bonus(rdoc_module.full_name, rdoc_method&.name, description),
rdoc_object.path,
rdoc_module.full_name,
(signature_for(rdoc_method) if rdoc_method),
*truncate_description(description, 130),
]
fingerprint = generate_fingerprint(ngrams, ngram_bit_positions)

case rdoc_object
when RDoc::ClassModule
build_entry(rdoc_object, fingerprint)
when RDoc::Constant
build_entry(rdoc_object, fingerprint, "::#{rdoc_object.name}")
when RDoc::MethodAttr
build_entry(rdoc_object, fingerprint, signature_for(rdoc_object))
end
end

{ "ngrams" => ngram_bit_positions, "weights" => bit_weights, "entries" => entries }
end

def build_entry(rdoc_object, fingerprint, member_label = nil)
rdoc_module = member_label ? rdoc_object.parent : rdoc_object
description = rdoc_object.description

[
fingerprint,
compute_tiebreaker_bonus(rdoc_module.full_name, (rdoc_object.name if member_label), description),
rdoc_object.path,
rdoc_module.full_name,
member_label,
*truncate_description(description, 130),
]
end

def derive_ngrams(name)
if name.match?(/:[^:A-Z]|#/)
# Example: "ActiveModel::Name::new" => ["ActiveModel", "Name", ":new"]
Expand Down Expand Up @@ -100,15 +115,15 @@ def compute_bit_weights(ngram_bit_positions)
Uint8Array.new(weights)
end

def compute_tiebreaker_bonus(module_name, method_name, description)
def compute_tiebreaker_bonus(module_name, member_name, description)
# Give bonus in proportion to documentation length, but scale up extremely
# slowly. Bonus is per matching ngram so it must be small enough to not
# outweigh points from other matches.
bonus = (description.length + 1) ** 0.01 / 100
# Reduce bonus in proportion to name length. This favors short names over
# long names. Notably, this will often favor methods over modules since
# method names are usually shorter than fully qualified module names.
bonus /= (method_name&.length || module_name.length) ** 0.1
# long names. Notably, this will often favor members over modules since
# member names are usually shorter than fully qualified module names.
bonus /= (member_name&.length || module_name.length) ** 0.1
end

def signature_for(rdoc_method)
Expand Down
24 changes: 15 additions & 9 deletions spec/search_index_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,22 @@

describe SDoc::SearchIndex do
describe "#generate" do
it "generates a search index for the given modules and their methods" do
it "generates a search index for the given modules and their members" do
top_level = rdoc_top_level_for <<~RUBY
# This is FooBar.
class FooBar
# This is +BAZ_QUX+.
BAZ_QUX = true

# This is #hoge_fuga.
def hoge_fuga; end
end
RUBY

ngrams = SDoc::SearchIndex.derive_ngrams("FooBar") | SDoc::SearchIndex.derive_ngrams("FooBar#hoge_fuga")
ngrams =
SDoc::SearchIndex.derive_ngrams("FooBar") |
SDoc::SearchIndex.derive_ngrams("FooBar::BAZ_QUX") |
SDoc::SearchIndex.derive_ngrams("FooBar#hoge_fuga")

search_index = SDoc::SearchIndex.generate(top_level.classes_and_modules)

Expand All @@ -20,29 +26,29 @@ def hoge_fuga; end
_(search_index["ngrams"].keys.sort).must_equal ngrams.sort
_(search_index["ngrams"].values.max).must_equal search_index["weights"].length - 1

_(search_index["entries"].length).must_equal 2
_(search_index["entries"].length).must_equal 3
search_index["entries"].each do |entry|
_(entry.length).must_be :<=, 6
_(entry[0]).must_be_kind_of Array # Fingerprint
_(entry[1]).must_be :<, 1.0 # Tiebreaker bonus
_(entry[3]).must_equal "FooBar" # Module name
end

module_entry, method_entry = search_index["entries"].sort_by { |entry| entry[4] ? 1 : 0 }
module_entry, method_entry, constant_entry = search_index["entries"].sort_by { |entry| entry[4].to_s }

# URL
_(module_entry[2]).must_equal "classes/FooBar.html"
_(constant_entry[2]).must_equal "classes/FooBar.html#constant-BAZ_QUX"
_(method_entry[2]).must_equal "classes/FooBar.html#method-i-hoge_fuga"

# Module name
_(module_entry[3]).must_equal "FooBar"
_(method_entry[3]).must_equal "FooBar"

# Method signature
# Member label
_(module_entry[4]).must_be_nil
_(constant_entry[4]).must_equal "::BAZ_QUX"
_(method_entry[4]).must_equal "#hoge_fuga()"

# Description
_(module_entry[5]).must_equal "This is <code>FooBar</code>."
_(constant_entry[5]).must_equal "This is <code>BAZ_QUX</code>."
_(method_entry[5]).must_equal "This is <code>hoge_fuga</code>."
end
end
Expand Down