Skip to content

Commit

Permalink
Merge pull request #358 from jonathanhefner/constants-search
Browse files Browse the repository at this point in the history
Add constants to search index
  • Loading branch information
jonathanhefner authored Jan 4, 2024
2 parents 22657f7 + f6de4c1 commit f1786b5
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 34 deletions.
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

0 comments on commit f1786b5

Please sign in to comment.