use crate::converter;
use crate::markdown;
use crate::markdown::CowStr;
use crate::mdbook;
use crate::typst;
use crate::ParserEvent;
use core::cmp::min;
use core::num::NonZeroU8;
use std::collections::VecDeque;
#[cfg(feature = "builder")]
mod builder;
#[cfg(feature = "builder")]
pub use builder::Conversion;
#[derive(Debug)]
pub struct ConvertAuthors<'a, T> {
authors: Vec<CowStr<'a>>,
iter: T,
}
impl<'a, T> ConvertAuthors<'a, T>
where
T: Iterator<Item = ParserEvent<'a>>,
{
#[allow(dead_code)]
fn new(iter: T) -> Self {
Self {
authors: vec![],
iter,
}
}
}
impl<'a, T> Iterator for ConvertAuthors<'a, T>
where
T: Iterator<Item = ParserEvent<'a>>,
{
type Item = ParserEvent<'a>;
fn next(&mut self) -> Option<Self::Item> {
match self.iter.next() {
Some(ParserEvent::Mdbook(mdbook::Event::Start(mdbook::Tag::AuthorList))) => {
self.authors = vec![];
self.next()
}
Some(ParserEvent::Mdbook(mdbook::Event::Author(a))) => {
self.authors.push(a);
self.next()
}
Some(ParserEvent::Mdbook(mdbook::Event::End(mdbook::Tag::AuthorList))) => {
if !self.authors.is_empty() {
let markup_array = format!(
"({})",
self.authors
.iter()
.map(|x| format!("\"{}\"", x))
.collect::<Vec<_>>()
.join(",")
);
return Some(ParserEvent::Typst(typst::Event::DocumentSet(
"author".into(),
markup_array.into(),
)));
}
self.next()
}
x => x,
}
}
}
converter!(
ConvertTitle,
ParserEvent<'a> => ParserEvent<'a>,
|this: &mut Self| {
match this.iter.next() {
Some(ParserEvent::Mdbook(mdbook::Event::Title(title))) => {
Some(ParserEvent::Typst(typst::Event::DocumentSet(
"title".into(),
format!("\"{}\"", title.as_ref()).into()),
))
},
x => x,
}
});
#[derive(Debug)]
pub struct ConvertChapter<'a, T> {
chapter_info: VecDeque<Option<(String, std::path::PathBuf)>>,
buf: VecDeque<ParserEvent<'a>>,
iter: T,
}
fn chapter_path_to_label(path: &std::path::Path) -> String {
path.to_string_lossy()
.trim_end_matches(".md")
.replace(['/', '\\'], "-")
.to_lowercase()
}
impl<'a, T> ConvertChapter<'a, T>
where
T: Iterator<Item = ParserEvent<'a>>,
{
#[allow(dead_code)]
fn new(iter: T) -> Self {
Self {
chapter_info: VecDeque::new(),
buf: VecDeque::new(),
iter,
}
}
fn current_chapter_label(&self) -> Option<&str> {
self.chapter_info
.back()
.and_then(|info| info.as_ref().map(|(label, _)| label.as_str()))
}
fn current_chapter_path(&self) -> Option<&std::path::Path> {
self.chapter_info
.back()
.and_then(|info| info.as_ref().map(|(_, path)| path.as_path()))
}
fn resolve_relative_url(&self, url_str: &str) -> Option<String> {
if url::Url::parse(url_str).is_ok() {
return None;
}
if url_str.starts_with('#') {
return None;
}
if !url_str.contains(".md") {
return None;
}
let current_path = self.current_chapter_path()?;
let current_dir = current_path.parent().unwrap_or(std::path::Path::new(""));
let (path_part, anchor_part) = if let Some(hash_pos) = url_str.find('#') {
(&url_str[..hash_pos], Some(&url_str[hash_pos..]))
} else {
(url_str, None)
};
let relative_path = std::path::Path::new(path_part);
let mut resolved = current_dir.to_path_buf();
for component in relative_path.components() {
use std::path::Component;
match component {
Component::ParentDir => {
resolved.pop();
}
Component::CurDir => {
}
Component::Normal(name) => {
resolved.push(name);
}
_ => {}
}
}
let mut result = resolved.to_string_lossy().replace('\\', "/");
if let Some(anchor) = anchor_part {
result.push_str(anchor);
}
Some(result)
}
}
impl<'a, T> Iterator for ConvertChapter<'a, T>
where
T: Iterator<Item = ParserEvent<'a>>,
{
type Item = ParserEvent<'a>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(buffered) = self.buf.pop_front() {
#[cfg(feature = "tracing")]
tracing::trace!("returning buffered: {:?}", buffered);
return Some(buffered);
}
match self.iter.next() {
Some(ParserEvent::Mdbook(mdbook::Event::Start(mdbook::Tag::Chapter(
_,
name,
ref source,
_,
)))) => {
#[cfg(feature = "tracing")]
tracing::trace!("chapter start: {}", name);
let depth = self.chapter_info.len();
let chapter_info: Option<(String, std::path::PathBuf)> =
source.as_ref().and_then(|s| match s {
mdbook::ChapterSource::Path(path) => {
Some((chapter_path_to_label(path), path.clone()))
}
mdbook::ChapterSource::Url(_) => None,
});
let label = chapter_info.as_ref().map(|(l, _)| l.clone());
let tag = typst::Tag::Heading(
NonZeroU8::new((1 + depth).try_into().expect("nonzero")).expect("nonzero"),
typst::TableOfContents::Include,
typst::Bookmarks::Include,
label.clone().map(CowStr::from),
);
let start_event = ParserEvent::Typst(typst::Event::Start(tag.clone()));
let end_event = ParserEvent::Typst(typst::Event::End(tag));
self.buf
.push_back(ParserEvent::Typst(typst::Event::Text(name.clone())));
self.buf.push_back(end_event);
self.chapter_info.push_back(chapter_info);
#[cfg(feature = "tracing")]
tracing::trace!("returning: {:?}", start_event);
Some(start_event)
}
Some(ParserEvent::Mdbook(mdbook::Event::End(mdbook::Tag::Chapter(_, _name, _, _)))) => {
#[cfg(feature = "tracing")]
tracing::trace!("chapter end: {}", _name);
let _ = self.chapter_info.pop_back();
Some(ParserEvent::Typst(typst::Event::FunctionCall(
None,
"pagebreak".into(),
vec!["weak: true".into()],
)))
}
Some(ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(
markdown::Event::Start(markdown::Tag::Heading {
level,
id,
classes,
attrs,
}),
))) => {
#[cfg(feature = "tracing")]
tracing::trace!("heading start: {}", level);
use markdown::HeadingLevel;
let depth = self.chapter_info.len();
let new_level = match level {
HeadingLevel::H1 => {
HeadingLevel::try_from(min(1 + depth, 6)).expect("valid heading level")
}
HeadingLevel::H2 => {
HeadingLevel::try_from(min(2 + depth, 6)).expect("valid heading level")
}
HeadingLevel::H3 => {
HeadingLevel::try_from(min(3 + depth, 6)).expect("valid heading level")
}
HeadingLevel::H4 => {
HeadingLevel::try_from(min(4 + depth, 6)).expect("valid heading level")
}
HeadingLevel::H5 => {
HeadingLevel::try_from(min(5 + depth, 6)).expect("valid heading level")
}
HeadingLevel::H6 => HeadingLevel::H6,
};
let new_id = match (self.current_chapter_label(), &id) {
(Some(chapter_label), Some(existing_id)) => {
Some(CowStr::from(format!(
"!{}-{}",
chapter_label,
existing_id.to_lowercase()
)))
}
(Some(chapter_label), None) => {
Some(CowStr::from(chapter_label.to_string()))
}
(None, _) => id.clone(),
};
self.buf
.push_back(ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(
markdown::Event::Start(markdown::Tag::Heading {
level: new_level,
id: new_id,
classes,
attrs,
}),
)));
self.next()
}
Some(ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(
markdown::Event::End(markdown::TagEnd::Heading(level)),
))) => {
#[cfg(feature = "tracing")]
tracing::trace!("heading end: {}", level);
use markdown::HeadingLevel;
let depth = self.chapter_info.len();
let new_level = match level {
HeadingLevel::H1 => {
HeadingLevel::try_from(min(1 + depth, 6)).expect("valid heading level")
}
HeadingLevel::H2 => {
HeadingLevel::try_from(min(2 + depth, 6)).expect("valid heading level")
}
HeadingLevel::H3 => {
HeadingLevel::try_from(min(3 + depth, 6)).expect("valid heading level")
}
HeadingLevel::H4 => {
HeadingLevel::try_from(min(4 + depth, 6)).expect("valid heading level")
}
HeadingLevel::H5 => {
HeadingLevel::try_from(min(5 + depth, 6)).expect("valid heading level")
}
HeadingLevel::H6 => HeadingLevel::H6,
};
self.buf
.push_back(ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(
markdown::Event::End(markdown::TagEnd::Heading(new_level)),
)));
self.next()
}
Some(ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(
markdown::Event::Start(markdown::Tag::Link {
link_type,
dest_url,
title,
id,
}),
))) => {
let new_url = if let Some(anchor) = dest_url.strip_prefix('#') {
if let Some(chapter_label) = self.current_chapter_label() {
CowStr::from(format!("#{}-{}", chapter_label, anchor.to_lowercase()))
} else {
dest_url.clone()
}
} else if let Some(resolved) = self.resolve_relative_url(&dest_url) {
CowStr::from(resolved)
} else {
dest_url.clone()
};
self.buf
.push_back(ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(
markdown::Event::Start(markdown::Tag::Link {
link_type,
dest_url: new_url,
title,
id,
}),
)));
self.next()
}
Some(ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(
markdown::Event::InlineHtml(html),
))) => {
if let Some(anchor_id) = extract_html_anchor_id(&html) {
if let Some(chapter_label) = self.current_chapter_label() {
let label = format!("{}-{}", chapter_label, anchor_id.to_lowercase());
self.buf
.push_back(ParserEvent::Typst(typst::Event::Label(label.into())));
return self.next();
}
}
Some(ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(
markdown::Event::InlineHtml(html),
)))
}
Some(ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(
markdown::Event::Html(html),
))) => {
if let Some(anchor_id) = extract_html_anchor_id(&html) {
if let Some(chapter_label) = self.current_chapter_label() {
let label = format!("{}-{}", chapter_label, anchor_id.to_lowercase());
self.buf
.push_back(ParserEvent::Typst(typst::Event::Label(label.into())));
return self.next();
}
}
Some(ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(
markdown::Event::Html(html),
)))
}
x => x,
}
}
}
fn extract_html_anchor_id(html: &str) -> Option<String> {
use html5ever::tokenizer::{
BufferQueue, Tag, TagKind, Token, TokenSink, TokenSinkResult, Tokenizer, TokenizerOpts,
};
use std::cell::RefCell;
struct AnchorIdExtractor {
id: RefCell<Option<String>>,
}
impl TokenSink for AnchorIdExtractor {
type Handle = ();
fn process_token(&self, token: Token, _line_number: u64) -> TokenSinkResult<()> {
if let Token::TagToken(Tag {
kind: TagKind::StartTag,
name,
attrs,
..
}) = token
{
if name.as_ref() == "a" {
for attr in attrs {
if attr.name.local.as_ref() == "id" {
*self.id.borrow_mut() = Some(attr.value.to_string());
break;
}
}
}
}
TokenSinkResult::Continue
}
}
let sink = AnchorIdExtractor {
id: RefCell::new(None),
};
let tokenizer = Tokenizer::new(sink, TokenizerOpts::default());
let buffer = BufferQueue::default();
buffer.push_back(html.into());
let _ = tokenizer.feed(&buffer);
tokenizer.end();
tokenizer.sink.id.into_inner()
}
converter!(
ConvertChapterToPagebreak,
ParserEvent<'a> => ParserEvent<'a>,
|this: &mut Self| {
match this.iter.next() {
Some(ParserEvent::Mdbook(mdbook::Event::Start(mdbook::Tag::Chapter(_, _, _, _)))) => this.next(),
Some(ParserEvent::Mdbook(mdbook::Event::End(mdbook::Tag::Chapter(_, _, _, _)))) => {
Some(ParserEvent::Typst(typst::Event::FunctionCall(
None,
"pagebreak".into(),
vec!["weak: true".into()],
)))
},
x => x,
}
});
#[cfg(test)]
mod tests {
use super::*;
use crate::markdown::to::typst::ConvertHeadings;
use crate::mdbook::MdbookIter;
use std::path::PathBuf;
fn chapter_start<'a>(name: &'a str, path: &str) -> mdbook::Event<'a> {
mdbook::Event::Start(mdbook::Tag::Chapter(
mdbook::ChapterStatus::Active,
name.into(),
Some(mdbook::ChapterSource::Path(PathBuf::from(path))),
None,
))
}
fn chapter_end<'a>(name: &'a str) -> mdbook::Event<'a> {
mdbook::Event::End(mdbook::Tag::Chapter(
mdbook::ChapterStatus::Active,
name.into(),
None,
None,
))
}
fn heading_start<'a>(level: markdown::HeadingLevel) -> mdbook::Event<'a> {
mdbook::Event::MarkdownContentEvent(markdown::Event::Start(markdown::Tag::Heading {
level,
id: None,
classes: vec![],
attrs: vec![],
}))
}
fn heading_end(level: markdown::HeadingLevel) -> mdbook::Event<'static> {
mdbook::Event::MarkdownContentEvent(markdown::Event::End(markdown::TagEnd::Heading(level)))
}
fn text<'a>(s: &'a str) -> mdbook::Event<'a> {
mdbook::Event::MarkdownContentEvent(markdown::Event::Text(s.into()))
}
#[test]
fn chapter_path_to_label_simple() {
let path = PathBuf::from("intro.md");
assert_eq!(chapter_path_to_label(&path), "intro");
}
#[test]
fn chapter_path_to_label_nested() {
let path = PathBuf::from("chapters/getting-started.md");
assert_eq!(chapter_path_to_label(&path), "chapters-getting-started");
}
#[test]
fn heading_in_chapter_gets_prefixed_label() {
let events = vec![
chapter_start("Introduction", "intro.md"),
heading_start(markdown::HeadingLevel::H2),
text("Getting Started"),
heading_end(markdown::HeadingLevel::H2),
chapter_end("Introduction"),
];
let iter = MdbookIter(events.into_iter());
let converted = ConvertChapter::new(iter);
let mapped = converted.map(|e| match e {
ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(m)) => ParserEvent::Markdown(m),
x => x,
});
let with_headings = ConvertHeadings::new(mapped);
let result: Vec<_> = with_headings.collect();
let heading_label = result.iter().find_map(|e| match e {
ParserEvent::Typst(typst::Event::Start(typst::Tag::Heading(
level,
_,
_,
Some(label),
))) if level.get() == 3 => Some(label.as_ref()),
_ => None,
});
assert_eq!(heading_label, Some("intro-getting-started"));
}
#[test]
fn different_chapters_same_heading_get_unique_labels() {
let events = vec![
chapter_start("Chapter 1", "chapter1.md"),
heading_start(markdown::HeadingLevel::H2),
text("Introduction"),
heading_end(markdown::HeadingLevel::H2),
chapter_end("Chapter 1"),
chapter_start("Chapter 2", "chapter2.md"),
heading_start(markdown::HeadingLevel::H2),
text("Introduction"),
heading_end(markdown::HeadingLevel::H2),
chapter_end("Chapter 2"),
];
let iter = MdbookIter(events.into_iter());
let converted = ConvertChapter::new(iter);
let mapped = converted.map(|e| match e {
ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(m)) => ParserEvent::Markdown(m),
x => x,
});
let with_headings = ConvertHeadings::new(mapped);
let result: Vec<_> = with_headings.collect();
let heading_labels: Vec<_> = result
.iter()
.filter_map(|e| match e {
ParserEvent::Typst(typst::Event::Start(typst::Tag::Heading(
level,
_,
_,
Some(label),
))) if level.get() == 3 => Some(label.as_ref().to_string()),
_ => None,
})
.collect();
assert_eq!(heading_labels.len(), 2);
assert!(heading_labels.contains(&"chapter1-introduction".to_string()));
assert!(heading_labels.contains(&"chapter2-introduction".to_string()));
}
#[test]
fn nested_chapter_path() {
let events = vec![
chapter_start("Basics", "guides/basics.md"),
heading_start(markdown::HeadingLevel::H2),
text("Overview"),
heading_end(markdown::HeadingLevel::H2),
chapter_end("Basics"),
];
let iter = MdbookIter(events.into_iter());
let converted = ConvertChapter::new(iter);
let mapped = converted.map(|e| match e {
ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(m)) => ParserEvent::Markdown(m),
x => x,
});
let with_headings = ConvertHeadings::new(mapped);
let result: Vec<_> = with_headings.collect();
let heading_label = result.iter().find_map(|e| match e {
ParserEvent::Typst(typst::Event::Start(typst::Tag::Heading(
level,
_,
_,
Some(label),
))) if level.get() == 3 => Some(label.as_ref()),
_ => None,
});
assert_eq!(heading_label, Some("guides-basics-overview"));
}
fn link_start<'a>(url: &'a str) -> mdbook::Event<'a> {
mdbook::Event::MarkdownContentEvent(markdown::Event::Start(markdown::Tag::Link {
link_type: markdown::LinkType::Inline,
dest_url: url.into(),
title: "".into(),
id: "".into(),
}))
}
fn link_end() -> mdbook::Event<'static> {
mdbook::Event::MarkdownContentEvent(markdown::Event::End(markdown::TagEnd::Link))
}
#[test]
fn same_page_anchor_link_gets_prefixed() {
let events = vec![
chapter_start("Installation", "ch01-01-installation.md"),
link_start("#troubleshooting"),
text("Troubleshooting"),
link_end(),
chapter_end("Installation"),
];
let iter = MdbookIter(events.into_iter());
let converted = ConvertChapter::new(iter);
let result: Vec<_> = converted.collect();
let link_url = result.iter().find_map(|e| match e {
ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(markdown::Event::Start(
markdown::Tag::Link { dest_url, .. },
))) => Some(dest_url.as_ref()),
_ => None,
});
assert_eq!(link_url, Some("#ch01-01-installation-troubleshooting"));
}
#[test]
fn cross_page_anchor_link_combines_path_and_anchor() {
use crate::markdown::to::typst::ConvertLinks;
let md = "[See section](./ch02-02-advanced.md#configuration)";
let parser = markdown::Parser::new(md);
let i = ConvertLinks::new(crate::markdown::MarkdownIter(parser));
let result: Vec<_> = i.collect();
let link_url = result.iter().find_map(|e| match e {
ParserEvent::Typst(typst::Event::Start(typst::Tag::Link(_, url))) => Some(url.as_ref()),
_ => None,
});
assert_eq!(link_url, Some("<ch02-02-advanced-configuration>"));
}
fn heading_start_with_id<'a>(level: markdown::HeadingLevel, id: &'a str) -> mdbook::Event<'a> {
mdbook::Event::MarkdownContentEvent(markdown::Event::Start(markdown::Tag::Heading {
level,
id: Some(id.into()),
classes: vec![],
attrs: vec![],
}))
}
#[test]
fn heading_with_existing_id_preserves_id() {
let events = vec![
chapter_start("Installation", "ch01-01-installation.md"),
heading_start_with_id(markdown::HeadingLevel::H2, "troubleshooting"),
text("Troubleshooting Guide for Common Issues"),
heading_end(markdown::HeadingLevel::H2),
chapter_end("Installation"),
];
let iter = MdbookIter(events.into_iter());
let converted = ConvertChapter::new(iter);
let mapped = converted.map(|e| match e {
ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(m)) => ParserEvent::Markdown(m),
x => x,
});
let with_headings = ConvertHeadings::new(mapped);
let result: Vec<_> = with_headings.collect();
let heading_label = result.iter().find_map(|e| match e {
ParserEvent::Typst(typst::Event::Start(typst::Tag::Heading(
level,
_,
_,
Some(label),
))) if level.get() == 3 => Some(label.as_ref()),
_ => None,
});
assert_eq!(heading_label, Some("ch01-01-installation-troubleshooting"));
}
#[test]
fn extract_html_anchor_id_basic() {
assert_eq!(
super::extract_html_anchor_id(r#"<a id="foo"></a>"#).as_deref(),
Some("foo")
);
}
#[test]
fn extract_html_anchor_id_single_quotes() {
assert_eq!(
super::extract_html_anchor_id(r#"<a id='bar'></a>"#).as_deref(),
Some("bar")
);
}
#[test]
fn extract_html_anchor_id_with_other_attrs() {
assert_eq!(
super::extract_html_anchor_id(r##"<a class="link" id="baz" href="#"></a>"##).as_deref(),
Some("baz")
);
}
#[test]
fn extract_html_anchor_id_not_anchor() {
assert_eq!(
super::extract_html_anchor_id(r#"<div id="foo"></div>"#),
None
);
assert_eq!(
super::extract_html_anchor_id(r#"<span id="foo"></span>"#),
None
);
}
#[test]
fn extract_html_anchor_id_no_id() {
assert_eq!(
super::extract_html_anchor_id(r##"<a href="#foo"></a>"##),
None
);
}
fn inline_html<'a>(html: &'a str) -> mdbook::Event<'a> {
mdbook::Event::MarkdownContentEvent(markdown::Event::InlineHtml(html.into()))
}
#[test]
fn html_anchor_gets_chapter_prefix() {
let events = vec![
chapter_start("Installation", "ch01-01-installation.md"),
inline_html(r#"<a id="troubleshooting"></a>"#),
chapter_end("Installation"),
];
let iter = MdbookIter(events.into_iter());
let converted = ConvertChapter::new(iter);
let result: Vec<_> = converted.collect();
let label = result.iter().find_map(|e| match e {
ParserEvent::Typst(typst::Event::Label(l)) => Some(l.as_ref()),
_ => None,
});
assert_eq!(label, Some("ch01-01-installation-troubleshooting"));
}
#[test]
fn html_anchor_matches_link_target() {
use crate::markdown::to::typst::ConvertLinks;
let anchor_events = vec![
chapter_start("Installation", "ch01-01-installation.md"),
inline_html(r#"<a id="troubleshooting"></a>"#),
chapter_end("Installation"),
];
let iter = MdbookIter(anchor_events.into_iter());
let converted = ConvertChapter::new(iter);
let result: Vec<_> = converted.collect();
let anchor_label = result.iter().find_map(|e| match e {
ParserEvent::Typst(typst::Event::Label(l)) => Some(l.as_ref().to_string()),
_ => None,
});
let md = "[text](ch01-01-installation.html#troubleshooting)";
let parser = markdown::Parser::new(md);
let i = ConvertLinks::new(crate::markdown::MarkdownIter(parser));
let result: Vec<_> = i.collect();
let link_target = result.iter().find_map(|e| match e {
ParserEvent::Typst(typst::Event::Start(typst::Tag::Link(_, url))) => {
let s = url.as_ref();
if s.starts_with('<') && s.ends_with('>') {
Some(s[1..s.len() - 1].to_string())
} else {
None
}
}
_ => None,
});
assert_eq!(
anchor_label,
Some("ch01-01-installation-troubleshooting".to_string())
);
assert_eq!(
link_target,
Some("ch01-01-installation-troubleshooting".to_string())
);
assert_eq!(anchor_label, link_target);
}
#[test]
fn relative_sibling_link_resolved() {
let events = vec![
chapter_start("About", "about/index.md"),
link_start("about-this-handbook.md"),
text("About This Handbook"),
link_end(),
chapter_end("About"),
];
let iter = MdbookIter(events.into_iter());
let converted = ConvertChapter::new(iter);
let result: Vec<_> = converted.collect();
let link_url = result.iter().find_map(|e| match e {
ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(markdown::Event::Start(
markdown::Tag::Link { dest_url, .. },
))) => Some(dest_url.as_ref().to_string()),
_ => None,
});
assert_eq!(link_url, Some("about/about-this-handbook.md".to_string()));
}
#[test]
fn relative_parent_link_resolved() {
let events = vec![
chapter_start("Live Images", "installation/live-images.md"),
link_start("../index.md"),
text("Index"),
link_end(),
chapter_end("Live Images"),
];
let iter = MdbookIter(events.into_iter());
let converted = ConvertChapter::new(iter);
let result: Vec<_> = converted.collect();
let link_url = result.iter().find_map(|e| match e {
ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(markdown::Event::Start(
markdown::Tag::Link { dest_url, .. },
))) => Some(dest_url.as_ref().to_string()),
_ => None,
});
assert_eq!(link_url, Some("index.md".to_string()));
}
#[test]
fn relative_link_with_anchor_resolved() {
let events = vec![
chapter_start("Live Images", "installation/live-images.md"),
link_start("../index.md#downloading-installation-media"),
text("downloading"),
link_end(),
chapter_end("Live Images"),
];
let iter = MdbookIter(events.into_iter());
let converted = ConvertChapter::new(iter);
let result: Vec<_> = converted.collect();
let link_url = result.iter().find_map(|e| match e {
ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(markdown::Event::Start(
markdown::Tag::Link { dest_url, .. },
))) => Some(dest_url.as_ref().to_string()),
_ => None,
});
assert_eq!(
link_url,
Some("index.md#downloading-installation-media".to_string())
);
}
#[test]
fn deeply_nested_relative_link_resolved() {
let events = vec![
chapter_start("Advanced", "src/guides/installation/advanced.md"),
link_start("../../about/index.md#getting-started"),
text("Getting Started"),
link_end(),
chapter_end("Advanced"),
];
let iter = MdbookIter(events.into_iter());
let converted = ConvertChapter::new(iter);
let result: Vec<_> = converted.collect();
let link_url = result.iter().find_map(|e| match e {
ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(markdown::Event::Start(
markdown::Tag::Link { dest_url, .. },
))) => Some(dest_url.as_ref().to_string()),
_ => None,
});
assert_eq!(
link_url,
Some("src/about/index.md#getting-started".to_string())
);
}
#[test]
fn external_link_not_modified() {
let events = vec![
chapter_start("Installation", "installation.md"),
link_start("https://example.com"),
text("Example"),
link_end(),
chapter_end("Installation"),
];
let iter = MdbookIter(events.into_iter());
let converted = ConvertChapter::new(iter);
let result: Vec<_> = converted.collect();
let link_url = result.iter().find_map(|e| match e {
ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(markdown::Event::Start(
markdown::Tag::Link { dest_url, .. },
))) => Some(dest_url.as_ref().to_string()),
_ => None,
});
assert_eq!(link_url, Some("https://example.com".to_string()));
}
#[test]
fn dot_slash_prefix_resolved() {
let events = vec![
chapter_start("About", "about/index.md"),
link_start("./about-this-handbook.md"),
text("About This Handbook"),
link_end(),
chapter_end("About"),
];
let iter = MdbookIter(events.into_iter());
let converted = ConvertChapter::new(iter);
let result: Vec<_> = converted.collect();
let link_url = result.iter().find_map(|e| match e {
ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(markdown::Event::Start(
markdown::Tag::Link { dest_url, .. },
))) => Some(dest_url.as_ref().to_string()),
_ => None,
});
assert_eq!(link_url, Some("about/about-this-handbook.md".to_string()));
}
}