Skip to content

Commit cf9d9f0

Browse files
committed
Group contents
1 parent 2fb3f17 commit cf9d9f0

File tree

3 files changed

+127
-4
lines changed

3 files changed

+127
-4
lines changed

src/hexdocs/components/iframe.gleam

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ fn init(_) {
5555
}
5656

5757
fn update(model: Model, msg: Msg) -> #(Model, Effect(msg)) {
58-
case echo msg {
58+
case msg {
5959
UserChangedTitle(title) -> #(Model(..model, title:), effect.none())
6060
IFrameStateChanged(state) -> #(Model(..model, state:), effect.none())
6161
UserChangedTo(to) -> #(

src/hexdocs/services/hexdocs.gleam

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,14 @@ pub type Document {
2222
ref: String,
2323
title: String,
2424
type_: String,
25+
headers: List(Header),
2526
)
2627
}
2728

29+
pub type Header {
30+
Header(ref: String, title: String)
31+
}
32+
2833
pub fn packages() {
2934
let endpoint = endpoints.packages()
3035
let assert Ok(request) = request.from_uri(endpoint)
@@ -58,13 +63,91 @@ pub fn typesense_decoder() {
5863
use ref <- decode.field("ref", decode.string)
5964
use title <- decode.field("title", decode.string)
6065
use type_ <- decode.field("type", decode.string)
61-
Document(doc:, id:, package:, proglang:, ref:, title:, type_:)
66+
Document(
67+
doc:,
68+
id:,
69+
package:,
70+
proglang:,
71+
ref:,
72+
title:,
73+
type_:,
74+
headers: [],
75+
)
6276
|> decode.success
6377
})
6478
decode.success(document)
6579
})
6680
})
67-
decode.success(#(found, hits))
81+
let grouped_results = group_headers(hits)
82+
let removed_count = list.length(hits) - list.length(grouped_results)
83+
let max_results = list.take(grouped_results, config.per_page())
84+
decode.success(#(found - removed_count, max_results))
85+
}
86+
87+
fn group_headers(documents: List(Document)) -> List(Document) {
88+
// Convert to indexed tuples
89+
let indexed_docs = list.index_map(documents, fn(doc, index) { #(index, doc) })
90+
91+
// First pass: separate parents and children with their indexes
92+
let #(parents, children) =
93+
list.partition(indexed_docs, fn(indexed_doc) {
94+
let #(_index, doc) = indexed_doc
95+
!list.any(indexed_docs, fn(other_indexed) {
96+
let #(_other_index, other) = other_indexed
97+
string.starts_with(doc.ref, other.ref <> "-")
98+
&& doc.package == other.package
99+
&& doc.id != other.id
100+
})
101+
})
102+
103+
// Second pass: attach children to parents and compute min index
104+
let grouped_with_index =
105+
list.map(parents, fn(parent_indexed) {
106+
let #(parent_index, parent) = parent_indexed
107+
108+
let matching_headers =
109+
list.filter_map(children, fn(child_indexed) {
110+
let #(_child_index, child) = child_indexed
111+
case
112+
string.starts_with(child.ref, parent.ref <> "-")
113+
&& child.package == parent.package
114+
{
115+
True -> {
116+
let cleaned_title =
117+
string.replace(child.title, " - " <> parent.title, "")
118+
Ok(#(child_indexed, Header(ref: child.ref, title: cleaned_title)))
119+
}
120+
False -> Error(Nil)
121+
}
122+
})
123+
124+
let child_indexes =
125+
list.map(matching_headers, fn(header_data) {
126+
let #(#(child_index, _child), _header) = header_data
127+
child_index
128+
})
129+
130+
let min_index = list.fold(child_indexes, parent_index, int.min)
131+
let headers =
132+
list.map(matching_headers, fn(header_data) {
133+
let #(_child_indexed, header) = header_data
134+
header
135+
})
136+
137+
#(min_index, Document(..parent, headers: headers))
138+
})
139+
140+
// Sort by index and return documents
141+
grouped_with_index
142+
|> list.sort(fn(a, b) {
143+
let #(index_a, _doc_a) = a
144+
let #(index_b, _doc_b) = b
145+
int.compare(index_a, index_b)
146+
})
147+
|> list.map(fn(indexed_doc) {
148+
let #(_index, doc) = indexed_doc
149+
doc
150+
})
68151
}
69152

70153
fn new_search_query_params(
@@ -77,7 +160,8 @@ fn new_search_query_params(
77160
|> list.key_set("query_by", "title,doc,type")
78161
|> list.key_set("query_by_weights", "3,1,1")
79162
|> list.key_set("page", int.to_string(page))
80-
|> list.key_set("per_page", int.to_string(config.per_page()))
163+
// We multiply per 2 because we group results
164+
|> list.key_set("per_page", int.to_string(config.per_page() * 2))
81165
|> list.key_set("highlight_fields", "none")
82166
|> add_filter_by_packages_param(packages)
83167
|> uri.query_to_string

src/hexdocs/view/search.gleam

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,45 @@ fn result_card(model: Model, document: hexdocs.Document) {
495495
[class("text-slate-800 dark:text-slate-300 leading-normal mt-2")],
496496
hexdocs.snippet(document.doc, model.search_input),
497497
),
498+
case list.is_empty(document.headers) {
499+
True -> element.none()
500+
False ->
501+
html.div([class("mt-4 space-y-3")], [
502+
html.h4(
503+
[class("text-slate-600 dark:text-slate-400 text-sm font-medium")],
504+
[html.text("Related sections:")],
505+
),
506+
html.ul([class("space-y-1")], {
507+
list.map(document.headers, fn(header: hexdocs.Header) {
508+
let header_display_url =
509+
"/"
510+
<> string.replace(document.package, "-", "/")
511+
<> "/"
512+
<> header.ref
513+
let header_link_url = config.hexdocs_url() <> header_display_url
514+
515+
html.li(
516+
[
517+
class(
518+
"list-disc list-inside text-slate-400 dark:text-slate-500",
519+
),
520+
],
521+
[
522+
html.a(
523+
[
524+
attribute.href(header_link_url),
525+
class(
526+
"text-blue-600 dark:text-blue-400 text-sm hover:underline",
527+
),
528+
],
529+
[html.text(header.title)],
530+
),
531+
],
532+
)
533+
})
534+
}),
535+
])
536+
},
498537
html.div([class("mt-4 flex flex-wrap gap-3")], [
499538
html.button(
500539
[

0 commit comments

Comments
 (0)