Skip to content

Commit 23ddeee

Browse files
committed
feat(doc): collect fragment IDs into index.json
Walk `*[id]` in document order after `add_missing_ids` and expose the result as `fragments` on `Json*Doc`. Skip `quick_links` sidebar section ID.
1 parent c83255c commit 23ddeee

3 files changed

Lines changed: 73 additions & 3 deletions

File tree

crates/rari-doc/src/html/modifier.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,3 +238,44 @@ pub fn add_missing_ids(html: &mut Html) -> Result<(), DocError> {
238238
}
239239
Ok(())
240240
}
241+
242+
/// Collects all fragment IDs from elements with an `id` attribute, in document order.
243+
///
244+
/// Skips the `quick_links` sidebar section id, which is extracted into the page
245+
/// sidebar by `split_sections` and not part of the body addressable via `#fragment`.
246+
/// IDs have already been lowercased by `post_process_html` at this point.
247+
pub fn collect_fragment_ids(html: &Html) -> Vec<String> {
248+
let selector = Selector::parse("*[id]").unwrap();
249+
html.select(&selector)
250+
.filter_map(|el| el.attr("id"))
251+
.filter(|id| !id.trim().is_empty() && *id != "quick_links")
252+
.map(String::from)
253+
.collect()
254+
}
255+
256+
#[cfg(test)]
257+
mod tests {
258+
use super::*;
259+
260+
#[test]
261+
fn collects_ids_in_document_order() {
262+
let html = Html::parse_fragment(
263+
r#"<h2 id="examples">Examples</h2>
264+
<dl><dt id="term-a">A</dt><dd>x</dd><dt id="term-b">B</dt></dl>
265+
<h3 id="subsection">Subsection</h3>"#,
266+
);
267+
assert_eq!(
268+
collect_fragment_ids(&html),
269+
vec!["examples", "term-a", "term-b", "subsection"],
270+
);
271+
}
272+
273+
#[test]
274+
fn skips_quick_links_sidebar_id() {
275+
let html = Html::parse_fragment(
276+
r#"<section id="quick_links"></section>
277+
<h2 id="examples">Examples</h2>"#,
278+
);
279+
assert_eq!(collect_fragment_ids(&html), vec!["examples"]);
280+
}
281+
}

crates/rari-doc/src/pages/build.rs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ use crate::helpers::title::{TitleFormat, page_title, render_title, transform_tit
2525
use crate::html::banner::build_banner;
2626
use crate::html::bubble_up::bubble_up_curriculum_page;
2727
use crate::html::code::{Code, code_blocks};
28-
use crate::html::modifier::{add_missing_ids, insert_self_links_for_dts, remove_empty_p};
28+
use crate::html::modifier::{
29+
add_missing_ids, collect_fragment_ids, insert_self_links_for_dts, remove_empty_p,
30+
};
2931
use crate::html::rewriter::{post_process_html, post_process_inline_sidebar};
3032
use crate::html::sections::{BuildSection, BuildSectionType, Split, split_sections};
3133
use crate::html::sidebar::{
@@ -141,6 +143,7 @@ impl BuildSection<'_> {
141143
pub struct PageContent {
142144
body: Vec<Section>,
143145
toc: Vec<TocEntry>,
146+
fragments: Vec<String>,
144147
summary: Option<String>,
145148
sidebar: Option<String>,
146149
live_samples: Option<Vec<Code>>,
@@ -208,10 +211,12 @@ fn build_content<T: PageLike>(page: &T) -> Result<PageContent, DocError> {
208211
Some(sidebars.into_iter().collect::<Result<String, _>>()?)
209212
};
210213
let toc = make_toc(&sections, matches!(page.page_type(), PageType::Curriculum));
214+
let fragments = collect_fragment_ids(&fragment);
211215
let body = sections.into_iter().map(Into::into).collect();
212216
Ok(PageContent {
213217
body,
214218
toc,
219+
fragments,
215220
summary,
216221
sidebar,
217222
live_samples,
@@ -231,6 +236,7 @@ fn build_doc(doc: &Doc) -> Result<BuiltPage, DocError> {
231236
let PageContent {
232237
body,
233238
toc,
239+
fragments,
234240
summary,
235241
sidebar,
236242
live_samples,
@@ -312,6 +318,7 @@ fn build_doc(doc: &Doc) -> Result<BuiltPage, DocError> {
312318
body,
313319
sidebar_html,
314320
toc,
321+
fragments,
315322
baseline,
316323
modified,
317324
summary,
@@ -344,6 +351,7 @@ fn build_blog_post(post: &BlogPost) -> Result<BuiltPage, DocError> {
344351
let PageContent {
345352
body,
346353
toc,
354+
fragments,
347355
live_samples,
348356
..
349357
} = build_content(post)?;
@@ -356,6 +364,7 @@ fn build_blog_post(post: &BlogPost) -> Result<BuiltPage, DocError> {
356364
locale: post.locale(),
357365
body,
358366
toc,
367+
fragments,
359368
summary: Some(post.meta.description.clone()),
360369
live_samples,
361370
..Default::default()
@@ -386,12 +395,18 @@ fn build_blog_post(post: &BlogPost) -> Result<BuiltPage, DocError> {
386395

387396
fn build_generic_page(page: &Generic) -> Result<BuiltPage, DocError> {
388397
let built = build_content(page);
389-
let PageContent { body, toc, .. } = built?;
398+
let PageContent {
399+
body,
400+
toc,
401+
fragments,
402+
..
403+
} = built?;
390404
Ok(BuiltPage::GenericPage(Box::new(JsonGenericPage {
391405
hy_data: JsonGenericHyData {
392406
sections: body,
393407
title: page.meta.title.clone(),
394408
toc,
409+
fragments,
395410
},
396411
short_title: page.meta.short_title.clone(),
397412
page_title: if let Some(suffix) = &page.meta.title_suffix {
@@ -419,7 +434,12 @@ fn build_spa(spa: &SPA) -> Result<BuiltPage, DocError> {
419434
}
420435

421436
fn build_curriculum(curriculum: &Curriculum) -> Result<BuiltPage, DocError> {
422-
let PageContent { body, toc, .. } = build_content(curriculum)?;
437+
let PageContent {
438+
body,
439+
toc,
440+
fragments,
441+
..
442+
} = build_content(curriculum)?;
423443
let sidebar = build_sidebar().ok();
424444
let group = curriculum_group(&parents(curriculum));
425445
let modules = match curriculum.meta.template {
@@ -444,6 +464,7 @@ fn build_curriculum(curriculum: &Curriculum) -> Result<BuiltPage, DocError> {
444464
body,
445465
sidebar,
446466
toc,
467+
fragments,
447468
group,
448469
modules,
449470
prev_next,

crates/rari-doc/src/pages/json.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,8 @@ pub struct JsonDoc {
273273
#[serde(rename = "titleHTML")]
274274
pub title_html: String,
275275
pub toc: Vec<TocEntry>,
276+
#[serde(skip_serializing_if = "Vec::is_empty")]
277+
pub fragments: Vec<String>,
276278
#[serde(skip_serializing_if = "Option::is_none")]
277279
pub baseline: Option<Baseline<'static>>,
278280
#[serde(rename = "browserCompat", skip_serializing_if = "Vec::is_empty")]
@@ -449,6 +451,8 @@ pub struct JsonCurriculumDoc {
449451
pub title: String,
450452
pub summary: Option<String>,
451453
pub toc: Vec<TocEntry>,
454+
#[serde(skip_serializing_if = "Vec::is_empty")]
455+
pub fragments: Vec<String>,
452456
#[serde(skip_serializing_if = "Option::is_none")]
453457
pub sidebar: Option<Vec<CurriculumSidebarEntry>>,
454458
#[serde(skip_serializing_if = "Option::is_none")]
@@ -527,6 +531,8 @@ pub struct JsonBlogPostDoc {
527531
pub title: String,
528532
#[serde(skip_serializing_if = "Vec::is_empty")]
529533
pub toc: Vec<TocEntry>,
534+
#[serde(skip_serializing_if = "Vec::is_empty")]
535+
pub fragments: Vec<String>,
530536
#[serde(skip_serializing_if = "Option::is_none")]
531537
pub live_samples: Option<Vec<Code>>,
532538
}
@@ -936,6 +942,8 @@ pub struct JsonGenericHyData {
936942
pub sections: Vec<Section>,
937943
pub title: String,
938944
pub toc: Vec<TocEntry>,
945+
#[serde(skip_serializing_if = "Vec::is_empty")]
946+
pub fragments: Vec<String>,
939947
}
940948

941949
/// Represents the outermost generic page structure in the documentation system. This is written to

0 commit comments

Comments
 (0)