Skip to content

Commit c9a7591

Browse files
committed
feat(sidebars): start supporting inline sidebars
1 parent b19f4fc commit c9a7591

File tree

18 files changed

+359
-210
lines changed

18 files changed

+359
-210
lines changed

Diff for: crates/rari-doc/src/build.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use crate::resolve::url_to_path_buf;
1616
pub fn build_single_page(page: &Page) {
1717
let slug = &page.slug();
1818
let locale = page.locale();
19-
let span = span!(Level::ERROR, "ctx", "{}:{}", locale, slug);
19+
let span = span!(Level::ERROR, "page", "{}:{}", locale, slug);
2020
let _enter = span.enter();
2121
let built_page = match page {
2222
Page::Doc(doc) => build_doc(doc),

Diff for: crates/rari-doc/src/docs/build.rs

+21-6
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ use super::json::{
1818
};
1919
use super::page::PageLike;
2020
use super::parents::parents;
21-
use super::sections::{split_sections, BuildSection, BuildSectionType};
2221
use super::title::{page_title, transform_title};
2322
use crate::baseline::get_baseline;
2423
use crate::error::DocError;
2524
use crate::html::modifier::add_missing_ids;
2625
use crate::html::rewriter::post_process_html;
27-
use crate::html::sidebar::render_sidebar;
26+
use crate::html::sections::{split_sections, BuildSection, BuildSectionType};
27+
use crate::html::sidebar::build_sidebars;
2828
use crate::specs::extract_specifications;
2929
use crate::templ::render::{decode_ref, render};
3030

@@ -122,6 +122,7 @@ pub struct PageContent {
122122
body: Vec<Section>,
123123
toc: Vec<TocEntry>,
124124
summary: Option<String>,
125+
sidebar: Option<String>,
125126
}
126127

127128
pub fn make_toc(sections: &[BuildSection], with_h3: bool) -> Vec<TocEntry> {
@@ -143,15 +144,29 @@ pub fn build_content<T: PageLike>(doc: &T) -> Result<PageContent, DocError> {
143144
let post_processed_html = post_process_html(&html, doc, false)?;
144145
let mut fragment = Html::parse_fragment(&post_processed_html);
145146
add_missing_ids(&mut fragment)?;
146-
let (sections, summary) = split_sections(&fragment).expect("DOOM");
147+
let (sections, summary, sidebar) = split_sections(&fragment).expect("DOOM");
147148
let toc = make_toc(&sections, matches!(doc.page_type(), PageType::Curriculum));
148149
let body = sections.into_iter().map(Into::into).collect();
149-
Ok(PageContent { body, toc, summary })
150+
Ok(PageContent {
151+
body,
152+
toc,
153+
summary,
154+
sidebar,
155+
})
150156
}
151157

152158
pub fn build_doc(doc: &Doc) -> Result<BuiltDocy, DocError> {
153-
let PageContent { body, toc, summary } = build_content(doc)?;
154-
let sidebar_html = render_sidebar(doc)?;
159+
let PageContent {
160+
body,
161+
toc,
162+
summary,
163+
sidebar,
164+
} = build_content(doc)?;
165+
let sidebar_html = if sidebar.is_some() {
166+
sidebar
167+
} else {
168+
build_sidebars(doc)?
169+
};
155170
let baseline = get_baseline(&doc.meta.browser_compat);
156171
let folder = doc
157172
.meta

Diff for: crates/rari-doc/src/docs/mod.rs

-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,5 @@ pub mod dummy;
66
pub mod json;
77
pub mod page;
88
pub mod parents;
9-
pub mod sections;
109
pub mod title;
1110
pub mod types;

Diff for: crates/rari-doc/src/error.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pub enum DocError {
2929
WalkError(#[from] ignore::Error),
3030
#[error(transparent)]
3131
JsonError(#[from] serde_json::Error),
32-
#[error("File not found in static cache: {0}")]
32+
#[error("Page not found (static cache): {0}")]
3333
NotFoundInStaticCache(PathBuf),
3434
#[error("File cache broken")]
3535
FileCacheBroken,

Diff for: crates/rari-doc/src/helpers/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pub mod css_info;
2+
pub mod subpages;

Diff for: crates/rari-doc/src/helpers/subpages.rs

+233
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
use std::cmp::Ordering;
2+
use std::collections::BTreeMap;
3+
use std::fmt::Write;
4+
use std::path::PathBuf;
5+
6+
use rari_types::fm_types::{FeatureStatus, PageType};
7+
use rari_types::globals::deny_warnings;
8+
use rari_types::locale::Locale;
9+
10+
use crate::docs::page::{Page, PageLike, PageReader};
11+
use crate::error::DocError;
12+
use crate::redirects::resolve_redirect;
13+
use crate::templ::macros::badges::{write_deprecated, write_experimental, write_non_standard};
14+
use crate::utils::COLLATOR;
15+
use crate::walker::walk_builder;
16+
17+
fn title_sorter(a: &Page, b: &Page) -> Ordering {
18+
COLLATOR.with(|c| c.compare(a.title(), b.title()))
19+
}
20+
21+
fn slug_sorter(a: &Page, b: &Page) -> Ordering {
22+
COLLATOR.with(|c| c.compare(a.slug(), b.slug()))
23+
}
24+
25+
fn title_natural_sorter(a: &Page, b: &Page) -> Ordering {
26+
natural_compare_with_floats(a.title(), b.title())
27+
}
28+
29+
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
30+
pub enum SubPagesSorter {
31+
#[default]
32+
Title,
33+
Slug,
34+
TitleNatural,
35+
}
36+
37+
impl SubPagesSorter {
38+
pub fn sorter(&self) -> fn(a: &Page, b: &Page) -> Ordering {
39+
match self {
40+
SubPagesSorter::Title => title_sorter,
41+
SubPagesSorter::Slug => slug_sorter,
42+
SubPagesSorter::TitleNatural => title_natural_sorter,
43+
}
44+
}
45+
}
46+
47+
pub fn write_li_with_badges(
48+
out: &mut impl Write,
49+
page: &impl PageLike,
50+
locale: Locale,
51+
) -> Result<(), DocError> {
52+
write!(
53+
out,
54+
"<li><a href=\"{}\">{}</a>",
55+
page.url(),
56+
html_escape::encode_safe(page.short_title().unwrap_or(page.title()))
57+
)?;
58+
if page.status().contains(&FeatureStatus::Experimental) {
59+
write_experimental(out, locale)?;
60+
}
61+
if page.status().contains(&FeatureStatus::NonStandard) {
62+
write_non_standard(out, locale)?;
63+
}
64+
if page.status().contains(&FeatureStatus::Deprecated) {
65+
write_deprecated(out, locale)?;
66+
}
67+
Ok(write!(out, "</li>")?)
68+
}
69+
70+
pub fn list_sub_pages_internal(
71+
out: &mut impl Write,
72+
url: &str,
73+
locale: Locale,
74+
depth: Option<usize>,
75+
reverse: bool,
76+
sorter: Option<SubPagesSorter>,
77+
page_types: &[PageType],
78+
) -> Result<(), DocError> {
79+
let sub_pages = get_sub_pages(url, depth, sorter.unwrap_or_default())?;
80+
81+
if reverse {
82+
for sub_page in sub_pages.into_iter().rev() {
83+
if !page_types.is_empty() && !page_types.contains(&sub_page.page_type()) {
84+
continue;
85+
}
86+
write_li_with_badges(out, &sub_page, locale)?;
87+
}
88+
} else {
89+
for sub_page in sub_pages {
90+
if !page_types.is_empty() && !page_types.contains(&sub_page.page_type()) {
91+
continue;
92+
}
93+
write_li_with_badges(out, &sub_page, locale)?;
94+
}
95+
}
96+
Ok(())
97+
}
98+
99+
pub fn list_sub_pages_grouped_internal(
100+
out: &mut String,
101+
url: &str,
102+
locale: Locale,
103+
sorter: Option<SubPagesSorter>,
104+
page_types: &[PageType],
105+
) -> Result<(), DocError> {
106+
let sub_pages = get_sub_pages(url, None, sorter.unwrap_or_default())?;
107+
108+
let mut grouped = BTreeMap::new();
109+
for sub_page in sub_pages.iter() {
110+
if !page_types.is_empty() && !page_types.contains(&sub_page.page_type()) {
111+
continue;
112+
}
113+
let title = sub_page.title();
114+
let prefix_index = if !title.is_empty() {
115+
title[1..].find('-').map(|i| i + 1)
116+
} else {
117+
None
118+
};
119+
if let Some(prefix) = prefix_index.map(|i| &title[..i]) {
120+
grouped
121+
.entry(prefix)
122+
.and_modify(|l: &mut Vec<_>| l.push(sub_page))
123+
.or_insert(vec![sub_page]);
124+
} else {
125+
grouped.insert(sub_page.title(), vec![sub_page]);
126+
}
127+
}
128+
for (prefix, group) in grouped {
129+
let keep_group = group.len() > 2;
130+
if keep_group {
131+
out.push_str("<li class=\"toggle\"><details><summary>");
132+
out.push_str(prefix);
133+
out.push_str("-*</summary><ol>");
134+
}
135+
for sub_page in group {
136+
write_li_with_badges(out, sub_page, locale)?;
137+
}
138+
if keep_group {
139+
out.push_str("</ol></details></li>");
140+
}
141+
}
142+
Ok(())
143+
}
144+
145+
pub fn get_sub_pages(
146+
url: &str,
147+
depth: Option<usize>,
148+
sorter: SubPagesSorter,
149+
) -> Result<Vec<Page>, DocError> {
150+
let redirect = resolve_redirect(url);
151+
let url = match redirect.as_ref() {
152+
Some(redirect) if deny_warnings() => {
153+
return Err(DocError::RedirectedLink {
154+
from: url.to_string(),
155+
to: redirect.to_string(),
156+
})
157+
}
158+
Some(redirect) => redirect,
159+
None => url,
160+
};
161+
let doc = Page::page_from_url_path(url)?;
162+
let full_path = doc.full_path();
163+
if let Some(folder) = full_path.parent() {
164+
let sub_folders = walk_builder(&[folder], None)?
165+
.max_depth(depth.map(|i| i + 1))
166+
.build()
167+
.filter_map(|f| f.ok())
168+
.filter(|f| {
169+
f.file_type().map(|ft| ft.is_file()).unwrap_or(false) && f.path() != full_path
170+
})
171+
.map(|f| f.into_path())
172+
.collect::<Vec<PathBuf>>();
173+
174+
let mut sub_pages = sub_folders
175+
.iter()
176+
.map(Page::read)
177+
.collect::<Result<Vec<_>, DocError>>()?;
178+
sub_pages.sort_by(sorter.sorter());
179+
return Ok(sub_pages);
180+
}
181+
Ok(vec![])
182+
}
183+
184+
fn split_into_parts(s: &str) -> Vec<(bool, &str)> {
185+
let mut parts = Vec::new();
186+
let mut start = 0;
187+
let mut end = 0;
188+
let mut in_number = false;
189+
190+
for c in s.chars() {
191+
if c.is_ascii_digit() || c == '.' {
192+
if !in_number {
193+
if start != end {
194+
parts.push((false, &s[start..end]));
195+
start = end
196+
}
197+
in_number = true;
198+
}
199+
} else if in_number {
200+
if start != end {
201+
parts.push((true, &s[start..end]));
202+
start = end
203+
}
204+
in_number = false;
205+
}
206+
end += 1
207+
}
208+
209+
if start != end {
210+
parts.push((in_number, &s[start..end]));
211+
}
212+
213+
parts
214+
}
215+
216+
fn natural_compare_with_floats(a: &str, b: &str) -> Ordering {
217+
let parts_a = split_into_parts(a);
218+
let parts_b = split_into_parts(b);
219+
220+
for (part_a, part_b) in parts_a.iter().zip(parts_b.iter()) {
221+
let order = if part_a.0 && part_b.0 {
222+
let num_a: f64 = part_a.1.parse().unwrap_or(f64::NEG_INFINITY);
223+
let num_b: f64 = part_b.1.parse().unwrap_or(f64::INFINITY);
224+
num_a.partial_cmp(&num_b).unwrap()
225+
} else {
226+
part_a.1.cmp(part_b.1)
227+
};
228+
if order != Ordering::Equal {
229+
return order;
230+
}
231+
}
232+
parts_a.len().cmp(&parts_b.len())
233+
}

Diff for: crates/rari-doc/src/html/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod links;
22
pub mod modifier;
33
pub mod rewriter;
4+
pub mod sections;
45
pub mod sidebar;

Diff for: crates/rari-doc/src/html/rewriter.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -243,14 +243,14 @@ pub fn post_process_html<T: PageLike>(
243243
el.after("</figure>", ContentType::Html);
244244
Ok(())
245245
}),
246-
];
247-
if sidebar {
248-
element_content_handlers.push(element!("*[data-rewriter=em]", |el| {
246+
element!("*[data-rewriter=em]", |el| {
249247
el.prepend("<em>", ContentType::Html);
250248
el.append("</em>", ContentType::Html);
251249
el.remove_attribute("data-rewriter");
252250
Ok(())
253-
}));
251+
}),
252+
];
253+
if sidebar {
254254
element_content_handlers.push(element!("html", |el| {
255255
el.remove_and_keep_content();
256256
Ok(())

Diff for: crates/rari-doc/src/docs/sections.rs renamed to crates/rari-doc/src/html/sections.rs

+12-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ pub struct BuildSection<'a> {
2222
pub id: Option<String>,
2323
}
2424

25-
pub fn split_sections(html: &Html) -> Result<(Vec<BuildSection<'_>>, Option<String>), DocError> {
25+
pub fn split_sections(
26+
html: &Html,
27+
) -> Result<(Vec<BuildSection<'_>>, Option<String>, Option<String>), DocError> {
2628
let root_children = html.root_element().children();
2729
let raw_sections = root_children;
2830
let summary_selector = Selector::parse("p").unwrap();
@@ -34,6 +36,7 @@ pub fn split_sections(html: &Html) -> Result<(Vec<BuildSection<'_>>, Option<Stri
3436
None
3537
}
3638
});
39+
let mut sidebar = None;
3740

3841
let (mut sections, mut last) = raw_sections.fold(
3942
(Vec::new(), None::<BuildSection>),
@@ -100,6 +103,13 @@ pub fn split_sections(html: &Html) -> Result<(Vec<BuildSection<'_>>, Option<Stri
100103
id,
101104
});
102105
}
106+
"section" if element.id() == Some("Quick_links") => {
107+
if let Some(section) = maybe_section.take() {
108+
sections.push(section);
109+
}
110+
let html = ElementRef::wrap(current).unwrap().html();
111+
sidebar = Some(html)
112+
}
103113
_ => {
104114
let (typ, query, urls) = if element.classes().any(|cls| cls == "bc-data") {
105115
(BuildSectionType::Compat, element.attr("data-query"), None)
@@ -223,5 +233,5 @@ pub fn split_sections(html: &Html) -> Result<(Vec<BuildSection<'_>>, Option<Stri
223233
if let Some(section) = last.take() {
224234
sections.push(section);
225235
}
226-
Ok((sections, summary))
236+
Ok((sections, summary, sidebar))
227237
}

0 commit comments

Comments
 (0)