use super::markdown::{Chapter, parse_chapter};
use std::sync::OnceLock;
#[derive(Debug, Clone, Copy)]
pub struct EmbeddedChapter {
pub stem: &'static str,
pub source: &'static str,
}
macro_rules! chapter {
($stem:literal) => {
EmbeddedChapter {
stem: $stem,
source: include_str!(concat!("../../../docs/manual/", $stem, ".md")),
}
};
}
pub const CONTENTS_STEM: &str = "README";
const CONTENTS_LABEL: &str = "Contents";
pub const CHAPTERS: &[EmbeddedChapter] = &[
chapter!("README"),
chapter!("01-getting-started"),
chapter!("02-catalogue-and-viewing"),
chapter!("03-loading-and-tier-list"),
chapter!("04-editing-tiers"),
chapter!("05-solving"),
chapter!("06-materials-and-refractive-index"),
chapter!("07-new-design-worked-example"),
chapter!("08-deep-solve-optimize-adopt"),
chapter!("09-rendering-and-export"),
chapter!("10-remote-worker-setup"),
chapter!("11-saving-and-file-formats"),
chapter!("12-troubleshooting-and-limitations"),
chapter!("13-solid-inspection-view"),
chapter!("14-retarget-snapshot-compare-tilt-curves"),
chapter!("15-planning-a-rough"),
chapter!("16-concave-tiers"),
chapter!("17-preferences-and-accessibility"),
chapter!("18-history-and-variants"),
chapter!("19-command-palette-and-keyboard"),
chapter!("20-cutting-mode"),
chapter!("21-angle-sweeps"),
chapter!("22-tutorials"),
chapter!("23-render-jobs"),
chapter!("appendix-a-glossary"),
chapter!("appendix-b-keyboard-shortcuts"),
chapter!("appendix-c-render-materials"),
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SectionText {
pub plain: String,
pub lower: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ManualChapter {
pub stem: &'static str,
pub label: String,
pub parsed: Chapter,
pub texts: Vec<SectionText>,
}
impl ManualChapter {
#[must_use]
pub fn from_source(stem: &'static str, source: &str) -> Self {
let parsed = parse_chapter(source);
let label = if stem == CONTENTS_STEM {
CONTENTS_LABEL.to_owned()
} else if parsed.title.is_empty() {
stem.to_owned()
} else {
parsed.title.clone()
};
let texts = (0..parsed.sections.len())
.map(|index| {
let plain = parsed.section_plain_text(index);
SectionText {
lower: plain.to_lowercase(),
plain,
}
})
.collect();
Self {
stem,
label,
parsed,
texts,
}
}
}
#[must_use]
pub fn chapters() -> &'static [ManualChapter] {
static MANUAL: OnceLock<Vec<ManualChapter>> = OnceLock::new();
MANUAL.get_or_init(|| {
CHAPTERS
.iter()
.map(|chapter| ManualChapter::from_source(chapter.stem, chapter.source))
.collect()
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NavRow {
pub label: String,
pub topic: String,
pub level: u8,
pub open: bool,
pub current: bool,
}
#[must_use]
pub fn nav_rows(chapters: &[ManualChapter], chapter: usize, section: usize) -> Vec<NavRow> {
let mut rows = Vec::new();
for (index, entry) in chapters.iter().enumerate() {
let open = index == chapter;
rows.push(NavRow {
label: entry.label.clone(),
topic: entry.stem.to_owned(),
level: 0,
open,
current: open && section == 0,
});
if !open {
continue;
}
for (at, part) in entry.parsed.sections.iter().enumerate() {
if !(2..=3).contains(&part.level) || part.title.is_empty() {
continue;
}
rows.push(NavRow {
label: part.title.clone(),
topic: format!("{}#{}", entry.stem, part.slug),
level: part.level - 1,
open: false,
current: at == section,
});
}
}
rows
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gui::help::markdown::BlockKind;
#[test]
fn every_chapter_file_of_the_manual_folder_is_embedded() {
let folder = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("docs/manual");
let mut missing = Vec::new();
for entry in std::fs::read_dir(&folder).expect("the manual folder can be read") {
let path = entry.expect("a directory entry can be read").path();
if path.extension().is_some_and(|extension| extension == "md") {
let stem = path
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or_default()
.to_owned();
if !CHAPTERS.iter().any(|chapter| chapter.stem == stem) {
missing.push(stem);
}
}
}
assert!(
missing.is_empty(),
"add a chapter!(\"...\") line to CHAPTERS in src/gui/help/manual.rs for: {missing:?}"
);
}
#[test]
fn the_contents_page_comes_first_and_stems_are_unique() {
assert_eq!(CHAPTERS[0].stem, CONTENTS_STEM);
let mut stems: Vec<&str> = CHAPTERS.iter().map(|chapter| chapter.stem).collect();
stems.sort_unstable();
stems.dedup();
assert_eq!(stems.len(), CHAPTERS.len());
}
#[test]
fn every_chapter_starts_with_a_title_and_has_sections() {
for chapter in chapters() {
assert!(
!chapter.parsed.title.is_empty(),
"{} has no heading",
chapter.stem
);
let first = &chapter.parsed.blocks[0];
assert_eq!(first.kind, BlockKind::Heading, "{}", chapter.stem);
assert_eq!(first.level, 1, "{}", chapter.stem);
assert_eq!(chapter.texts.len(), chapter.parsed.sections.len());
}
assert_eq!(chapters()[0].label, "Contents");
}
#[test]
fn no_embedded_chapter_leaves_raw_markdown_headings_in_its_text() {
for chapter in chapters() {
for block in &chapter.parsed.blocks {
if matches!(block.kind, BlockKind::Paragraph | BlockKind::ListItem) {
assert!(
!block.text.starts_with("# ") && !block.text.starts_with("## "),
"{}: {:?}",
chapter.stem,
block.text
);
}
}
}
}
#[test]
fn the_chapter_list_expands_the_open_chapter_only() {
let manual = [
ManualChapter::from_source("a", "# One\n\n## A1\n\n### A1x\n\n#### Deep\n"),
ManualChapter::from_source("b", "# Two\n\n## B1\n"),
];
let rows = nav_rows(&manual, 0, 2);
let shown: Vec<(&str, &str, u8, bool, bool)> = rows
.iter()
.map(|r| {
(
r.label.as_str(),
r.topic.as_str(),
r.level,
r.open,
r.current,
)
})
.collect();
assert_eq!(
shown,
[
("One", "a", 0, true, false),
("A1", "a#a1", 1, false, false),
("A1x", "a#a1x", 2, false, true),
("Two", "b", 0, false, false),
]
);
let second = nav_rows(&manual, 1, 0);
assert_eq!(second.len(), 3);
assert!(second[1].current && second[1].open);
}
}