Add anchor-based TOC navigation: parse #fragment anchors, find exact page within section
This commit is contained in:
@@ -11,6 +11,7 @@ fn parse_blocks(raw_text: &str) -> Vec<ContentBlock> {
|
||||
continue;
|
||||
}
|
||||
let mut heading_level = 0u8;
|
||||
let mut anchor: Option<String> = None;
|
||||
let mut text = trimmed.to_string();
|
||||
if text.contains('\x01') {
|
||||
if let Some(pos) = text.find('\x01') {
|
||||
@@ -20,19 +21,28 @@ fn parse_blocks(raw_text: &str) -> Vec<ContentBlock> {
|
||||
.next()
|
||||
.and_then(|c| c.to_digit(10))
|
||||
.unwrap_or(1) as u8;
|
||||
let after_level = &text[pos + 2..];
|
||||
if let Some(anchor_start) = after_level.find('\x03') {
|
||||
if let Some(anchor_end) = after_level.find('\x04') {
|
||||
let anchor_str = &after_level[anchor_start + 1..anchor_end];
|
||||
anchor = Some(anchor_str.to_string());
|
||||
}
|
||||
}
|
||||
text.drain(pos..pos + 2);
|
||||
}
|
||||
text = text.replace(&['\x03', '\x04'][..], "");
|
||||
text = text.replace('\x02', "");
|
||||
}
|
||||
let text = text.trim().to_string();
|
||||
if !text.is_empty() {
|
||||
blocks.push(ContentBlock { text, heading_level });
|
||||
blocks.push(ContentBlock { text, heading_level, anchor });
|
||||
}
|
||||
}
|
||||
if blocks.is_empty() {
|
||||
blocks.push(ContentBlock {
|
||||
text: String::new(),
|
||||
heading_level: 0,
|
||||
anchor: None,
|
||||
});
|
||||
}
|
||||
blocks
|
||||
@@ -201,6 +211,17 @@ pub fn find_page_by_snippet(section: &crate::book::Section, snippet: &str) -> Op
|
||||
None
|
||||
}
|
||||
|
||||
pub fn find_page_for_anchor(section: &crate::book::Section, anchor: &str) -> Option<usize> {
|
||||
for (block_idx, block) in section.blocks.iter().enumerate() {
|
||||
if let Some(ref a) = block.anchor {
|
||||
if a == anchor {
|
||||
return find_page_for_block(section, block_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub struct ReaderAction {
|
||||
pub go_back: bool,
|
||||
pub toggle_theme: bool,
|
||||
@@ -212,6 +233,7 @@ pub struct ReaderAction {
|
||||
pub page_prev: bool,
|
||||
pub toggle_sidebar: bool,
|
||||
pub jump_to_section: Option<usize>,
|
||||
pub jump_to_anchor: Option<String>,
|
||||
}
|
||||
|
||||
pub fn reading_view(
|
||||
@@ -239,6 +261,7 @@ pub fn reading_view(
|
||||
page_prev: false,
|
||||
toggle_sidebar: false,
|
||||
jump_to_section: None,
|
||||
jump_to_anchor: None,
|
||||
};
|
||||
let mut jump_to_bookmark: Option<usize> = None;
|
||||
|
||||
@@ -271,8 +294,9 @@ pub fn reading_view(
|
||||
ui.separator();
|
||||
if sidebar_tab == 0 {
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
if let Some(section) = render_toc(ui, &book.toc, *current_section) {
|
||||
if let Some((section, anchor)) = render_toc(ui, &book.toc, *current_section) {
|
||||
action.jump_to_section = Some(section);
|
||||
action.jump_to_anchor = anchor;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -549,8 +573,8 @@ fn render_toc(
|
||||
ui: &mut egui::Ui,
|
||||
entries: &[crate::book::TocEntry],
|
||||
current_section: usize,
|
||||
) -> Option<usize> {
|
||||
let mut jump = None;
|
||||
) -> Option<(usize, Option<String>)> {
|
||||
let mut jump: Option<(usize, Option<String>)> = None;
|
||||
for entry in entries {
|
||||
let label_text = egui::RichText::new(&entry.label);
|
||||
let response = ui.add(
|
||||
@@ -559,9 +583,10 @@ fn render_toc(
|
||||
.wrap()
|
||||
);
|
||||
if response.clicked() {
|
||||
jump = Some(entry.section);
|
||||
jump = Some((entry.section, entry.anchor.clone()));
|
||||
}
|
||||
response.on_hover_text(format!("跳转到: {} (章节 {})", entry.label, entry.section));
|
||||
let anchor_info = entry.anchor.as_ref().map(|a| format!(" @{a}")).unwrap_or_default();
|
||||
response.on_hover_text(format!("跳转到: {} (章节 {}{})", entry.label, entry.section, anchor_info));
|
||||
if !entry.children.is_empty() {
|
||||
ui.indent(&entry.label, |ui| {
|
||||
if let Some(s) = render_toc(ui, &entry.children, current_section) {
|
||||
|
||||
Reference in New Issue
Block a user