This switches to using the tracing crate instead of log. Tracing provides a lot of nice features which we can take advantage of moving forward. This also adjusts the output fairly significantly. This includes: - Switched the environment variable from RUST_LOG to MDBOOK_LOG. - Dropped the timestamp. I experimented with various different time displays, but ultimately decided to omit it for now. I don't think I've ever found it to be useful, and it takes up a very significant amount of space. It could potentially be useful for basic profiling, but I think there are other, better mechanisms for that. We could consider leveraging tracing itself for doing some basic profiling (like using something like tracing-chrome). - Dropped the target unless MDBOOK_LOG is set. The target tends to be pretty noisy, and doesn't really convey much information unless you are debugging or otherwise trying to adjust the log output. - Added color. - Slightly reworked the way the error cause trace is displayed. - Slightly changed the way html5ever filtering is done, as well as add handlebars to the list since they both are very noisy. You can override this now by explicitly listing them as targets. I still expect that mdbook will eventually change how it displays things to the console, possibly switching away from tracing and printing things itself. However, that is a larger project for the future.
938 lines
30 KiB
Rust
938 lines
30 KiB
Rust
use anyhow::{Context, Result};
|
|
use mdbook_core::book::{Book, BookItem};
|
|
use mdbook_core::utils::{
|
|
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
|
|
take_rustdoc_include_lines,
|
|
};
|
|
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
|
|
use regex::{CaptureMatches, Captures, Regex};
|
|
use std::fs;
|
|
use std::ops::{Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeTo};
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::LazyLock;
|
|
use tracing::{error, warn};
|
|
|
|
const ESCAPE_CHAR: char = '\\';
|
|
const MAX_LINK_NESTED_DEPTH: usize = 10;
|
|
|
|
/// A preprocessor for expanding helpers in a chapter. Supported helpers are:
|
|
///
|
|
/// - `{{# include}}` - Insert an external file of any type. Include the whole file, only particular
|
|
/// lines, or only between the specified anchors.
|
|
/// - `{{# rustdoc_include}}` - Insert an external Rust file, showing the particular lines
|
|
/// specified or the lines between specified anchors, and include the rest of the file behind `#`.
|
|
/// This hides the lines from initial display but shows them when the reader expands the code
|
|
/// block and provides them to Rustdoc for testing.
|
|
/// - `{{# playground}}` - Insert runnable Rust files
|
|
/// - `{{# title}}` - Override \<title\> of a webpage.
|
|
#[derive(Default)]
|
|
#[non_exhaustive]
|
|
pub struct LinkPreprocessor;
|
|
|
|
impl LinkPreprocessor {
|
|
/// Name of this preprocessor.
|
|
pub const NAME: &'static str = "links";
|
|
|
|
/// Create a new `LinkPreprocessor`.
|
|
pub fn new() -> Self {
|
|
LinkPreprocessor
|
|
}
|
|
}
|
|
|
|
impl Preprocessor for LinkPreprocessor {
|
|
fn name(&self) -> &str {
|
|
Self::NAME
|
|
}
|
|
|
|
fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
|
|
let src_dir = ctx.root.join(&ctx.config.book.src);
|
|
|
|
book.for_each_mut(|section: &mut BookItem| {
|
|
if let BookItem::Chapter(ref mut ch) = *section {
|
|
if let Some(ref chapter_path) = ch.path {
|
|
let base = chapter_path
|
|
.parent()
|
|
.map(|dir| src_dir.join(dir))
|
|
.expect("All book items have a parent");
|
|
|
|
let mut chapter_title = ch.name.clone();
|
|
let content =
|
|
replace_all(&ch.content, base, chapter_path, 0, &mut chapter_title);
|
|
ch.content = content;
|
|
if chapter_title != ch.name {
|
|
ctx.chapter_titles
|
|
.borrow_mut()
|
|
.insert(chapter_path.clone(), chapter_title);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
Ok(book)
|
|
}
|
|
}
|
|
|
|
fn replace_all<P1, P2>(
|
|
s: &str,
|
|
path: P1,
|
|
source: P2,
|
|
depth: usize,
|
|
chapter_title: &mut String,
|
|
) -> String
|
|
where
|
|
P1: AsRef<Path>,
|
|
P2: AsRef<Path>,
|
|
{
|
|
// When replacing one thing in a string by something with a different length,
|
|
// the indices after that will not correspond,
|
|
// we therefore have to store the difference to correct this
|
|
let path = path.as_ref();
|
|
let source = source.as_ref();
|
|
let mut previous_end_index = 0;
|
|
let mut replaced = String::new();
|
|
|
|
for link in find_links(s) {
|
|
replaced.push_str(&s[previous_end_index..link.start_index]);
|
|
|
|
match link.render_with_path(path, chapter_title) {
|
|
Ok(new_content) => {
|
|
if depth < MAX_LINK_NESTED_DEPTH {
|
|
if let Some(rel_path) = link.link_type.relative_path(path) {
|
|
replaced.push_str(&replace_all(
|
|
&new_content,
|
|
rel_path,
|
|
source,
|
|
depth + 1,
|
|
chapter_title,
|
|
));
|
|
} else {
|
|
replaced.push_str(&new_content);
|
|
}
|
|
} else {
|
|
error!(
|
|
"Stack depth exceeded in {}. Check for cyclic includes",
|
|
source.display()
|
|
);
|
|
}
|
|
previous_end_index = link.end_index;
|
|
}
|
|
Err(e) => {
|
|
error!("Error updating \"{}\", {}", link.link_text, e);
|
|
for cause in e.chain().skip(1) {
|
|
warn!("Caused By: {}", cause);
|
|
}
|
|
|
|
// This should make sure we include the raw `{{# ... }}` snippet
|
|
// in the page content if there are any errors.
|
|
previous_end_index = link.start_index;
|
|
}
|
|
}
|
|
}
|
|
|
|
replaced.push_str(&s[previous_end_index..]);
|
|
replaced
|
|
}
|
|
|
|
#[derive(PartialEq, Debug, Clone)]
|
|
enum LinkType<'a> {
|
|
Escaped,
|
|
Include(PathBuf, RangeOrAnchor),
|
|
Playground(PathBuf, Vec<&'a str>),
|
|
RustdocInclude(PathBuf, RangeOrAnchor),
|
|
Title(&'a str),
|
|
}
|
|
|
|
#[derive(PartialEq, Debug, Clone)]
|
|
enum RangeOrAnchor {
|
|
Range(LineRange),
|
|
Anchor(String),
|
|
}
|
|
|
|
// A range of lines specified with some include directive.
|
|
#[derive(PartialEq, Debug, Clone)]
|
|
enum LineRange {
|
|
Range(Range<usize>),
|
|
RangeFrom(RangeFrom<usize>),
|
|
RangeTo(RangeTo<usize>),
|
|
RangeFull(RangeFull),
|
|
}
|
|
|
|
impl RangeBounds<usize> for LineRange {
|
|
fn start_bound(&self) -> Bound<&usize> {
|
|
match self {
|
|
LineRange::Range(r) => r.start_bound(),
|
|
LineRange::RangeFrom(r) => r.start_bound(),
|
|
LineRange::RangeTo(r) => r.start_bound(),
|
|
LineRange::RangeFull(r) => r.start_bound(),
|
|
}
|
|
}
|
|
|
|
fn end_bound(&self) -> Bound<&usize> {
|
|
match self {
|
|
LineRange::Range(r) => r.end_bound(),
|
|
LineRange::RangeFrom(r) => r.end_bound(),
|
|
LineRange::RangeTo(r) => r.end_bound(),
|
|
LineRange::RangeFull(r) => r.end_bound(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<Range<usize>> for LineRange {
|
|
fn from(r: Range<usize>) -> LineRange {
|
|
LineRange::Range(r)
|
|
}
|
|
}
|
|
|
|
impl From<RangeFrom<usize>> for LineRange {
|
|
fn from(r: RangeFrom<usize>) -> LineRange {
|
|
LineRange::RangeFrom(r)
|
|
}
|
|
}
|
|
|
|
impl From<RangeTo<usize>> for LineRange {
|
|
fn from(r: RangeTo<usize>) -> LineRange {
|
|
LineRange::RangeTo(r)
|
|
}
|
|
}
|
|
|
|
impl From<RangeFull> for LineRange {
|
|
fn from(r: RangeFull) -> LineRange {
|
|
LineRange::RangeFull(r)
|
|
}
|
|
}
|
|
|
|
impl<'a> LinkType<'a> {
|
|
fn relative_path<P: AsRef<Path>>(self, base: P) -> Option<PathBuf> {
|
|
let base = base.as_ref();
|
|
match self {
|
|
LinkType::Escaped => None,
|
|
LinkType::Include(p, _) => Some(return_relative_path(base, &p)),
|
|
LinkType::Playground(p, _) => Some(return_relative_path(base, &p)),
|
|
LinkType::RustdocInclude(p, _) => Some(return_relative_path(base, &p)),
|
|
LinkType::Title(_) => None,
|
|
}
|
|
}
|
|
}
|
|
fn return_relative_path<P: AsRef<Path>>(base: P, relative: P) -> PathBuf {
|
|
base.as_ref()
|
|
.join(relative)
|
|
.parent()
|
|
.expect("Included file should not be /")
|
|
.to_path_buf()
|
|
}
|
|
|
|
fn parse_range_or_anchor(parts: Option<&str>) -> RangeOrAnchor {
|
|
let mut parts = parts.unwrap_or("").splitn(3, ':').fuse();
|
|
|
|
let next_element = parts.next();
|
|
let start = if let Some(value) = next_element.and_then(|s| s.parse::<usize>().ok()) {
|
|
// subtract 1 since line numbers usually begin with 1
|
|
Some(value.saturating_sub(1))
|
|
} else if let Some("") = next_element {
|
|
None
|
|
} else if let Some(anchor) = next_element {
|
|
return RangeOrAnchor::Anchor(String::from(anchor));
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let end = parts.next();
|
|
// If `end` is empty string or any other value that can't be parsed as a usize, treat this
|
|
// include as a range with only a start bound. However, if end isn't specified, include only
|
|
// the single line specified by `start`.
|
|
let end = end.map(|s| s.parse::<usize>());
|
|
|
|
match (start, end) {
|
|
(Some(start), Some(Ok(end))) => RangeOrAnchor::Range(LineRange::from(start..end)),
|
|
(Some(start), Some(Err(_))) => RangeOrAnchor::Range(LineRange::from(start..)),
|
|
(Some(start), None) => RangeOrAnchor::Range(LineRange::from(start..start + 1)),
|
|
(None, Some(Ok(end))) => RangeOrAnchor::Range(LineRange::from(..end)),
|
|
(None, None) | (None, Some(Err(_))) => RangeOrAnchor::Range(LineRange::from(RangeFull)),
|
|
}
|
|
}
|
|
|
|
fn parse_include_path(path: &str) -> LinkType<'static> {
|
|
let mut parts = path.splitn(2, ':');
|
|
|
|
let path = parts.next().unwrap().into();
|
|
let range_or_anchor = parse_range_or_anchor(parts.next());
|
|
|
|
LinkType::Include(path, range_or_anchor)
|
|
}
|
|
|
|
fn parse_rustdoc_include_path(path: &str) -> LinkType<'static> {
|
|
let mut parts = path.splitn(2, ':');
|
|
|
|
let path = parts.next().unwrap().into();
|
|
let range_or_anchor = parse_range_or_anchor(parts.next());
|
|
|
|
LinkType::RustdocInclude(path, range_or_anchor)
|
|
}
|
|
|
|
#[derive(PartialEq, Debug, Clone)]
|
|
struct Link<'a> {
|
|
start_index: usize,
|
|
end_index: usize,
|
|
link_type: LinkType<'a>,
|
|
link_text: &'a str,
|
|
}
|
|
|
|
impl<'a> Link<'a> {
|
|
fn from_capture(cap: Captures<'a>) -> Option<Link<'a>> {
|
|
let link_type = match (cap.get(0), cap.get(1), cap.get(2)) {
|
|
(_, Some(typ), Some(title)) if typ.as_str() == "title" => {
|
|
Some(LinkType::Title(title.as_str()))
|
|
}
|
|
(_, Some(typ), Some(rest)) => {
|
|
let mut path_props = rest.as_str().split_whitespace();
|
|
let file_arg = path_props.next();
|
|
let props: Vec<&str> = path_props.collect();
|
|
|
|
match (typ.as_str(), file_arg) {
|
|
("include", Some(pth)) => Some(parse_include_path(pth)),
|
|
("playground", Some(pth)) => Some(LinkType::Playground(pth.into(), props)),
|
|
("playpen", Some(pth)) => {
|
|
warn!(
|
|
"the {{{{#playpen}}}} expression has been \
|
|
renamed to {{{{#playground}}}}, \
|
|
please update your book to use the new name"
|
|
);
|
|
Some(LinkType::Playground(pth.into(), props))
|
|
}
|
|
("rustdoc_include", Some(pth)) => Some(parse_rustdoc_include_path(pth)),
|
|
_ => None,
|
|
}
|
|
}
|
|
(Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => {
|
|
Some(LinkType::Escaped)
|
|
}
|
|
_ => None,
|
|
};
|
|
|
|
link_type.and_then(|lnk_type| {
|
|
cap.get(0).map(|mat| Link {
|
|
start_index: mat.start(),
|
|
end_index: mat.end(),
|
|
link_type: lnk_type,
|
|
link_text: mat.as_str(),
|
|
})
|
|
})
|
|
}
|
|
|
|
fn render_with_path<P: AsRef<Path>>(
|
|
&self,
|
|
base: P,
|
|
chapter_title: &mut String,
|
|
) -> Result<String> {
|
|
let base = base.as_ref();
|
|
match self.link_type {
|
|
// omit the escape char
|
|
LinkType::Escaped => Ok(self.link_text[1..].to_owned()),
|
|
LinkType::Include(ref pat, ref range_or_anchor) => {
|
|
let target = base.join(pat);
|
|
|
|
fs::read_to_string(&target)
|
|
.map(|s| match range_or_anchor {
|
|
RangeOrAnchor::Range(range) => take_lines(&s, range.clone()),
|
|
RangeOrAnchor::Anchor(anchor) => take_anchored_lines(&s, anchor),
|
|
})
|
|
.with_context(|| {
|
|
format!(
|
|
"Could not read file for link {} ({})",
|
|
self.link_text,
|
|
target.display(),
|
|
)
|
|
})
|
|
}
|
|
LinkType::RustdocInclude(ref pat, ref range_or_anchor) => {
|
|
let target = base.join(pat);
|
|
|
|
fs::read_to_string(&target)
|
|
.map(|s| match range_or_anchor {
|
|
RangeOrAnchor::Range(range) => {
|
|
take_rustdoc_include_lines(&s, range.clone())
|
|
}
|
|
RangeOrAnchor::Anchor(anchor) => {
|
|
take_rustdoc_include_anchored_lines(&s, anchor)
|
|
}
|
|
})
|
|
.with_context(|| {
|
|
format!(
|
|
"Could not read file for link {} ({})",
|
|
self.link_text,
|
|
target.display(),
|
|
)
|
|
})
|
|
}
|
|
LinkType::Playground(ref pat, ref attrs) => {
|
|
let target = base.join(pat);
|
|
|
|
let mut contents = fs::read_to_string(&target).with_context(|| {
|
|
format!(
|
|
"Could not read file for link {} ({})",
|
|
self.link_text,
|
|
target.display()
|
|
)
|
|
})?;
|
|
let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
|
|
if !contents.ends_with('\n') {
|
|
contents.push('\n');
|
|
}
|
|
Ok(format!(
|
|
"```{}{}\n{}```\n",
|
|
ftype,
|
|
attrs.join(","),
|
|
contents
|
|
))
|
|
}
|
|
LinkType::Title(title) => {
|
|
*chapter_title = title.to_owned();
|
|
Ok(String::new())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct LinkIter<'a>(CaptureMatches<'a, 'a>);
|
|
|
|
impl<'a> Iterator for LinkIter<'a> {
|
|
type Item = Link<'a>;
|
|
fn next(&mut self) -> Option<Link<'a>> {
|
|
for cap in &mut self.0 {
|
|
if let Some(inc) = Link::from_capture(cap) {
|
|
return Some(inc);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
fn find_links(contents: &str) -> LinkIter<'_> {
|
|
// lazily compute following regex
|
|
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([^}]+)\}\}")?;
|
|
static RE: LazyLock<Regex> = LazyLock::new(|| {
|
|
Regex::new(
|
|
r"(?x) # insignificant whitespace mode
|
|
\\\{\{\#.*\}\} # match escaped link
|
|
| # or
|
|
\{\{\s* # link opening parens and whitespace
|
|
\#([a-zA-Z0-9_]+) # link type
|
|
\s+ # separating whitespace
|
|
([^}]+) # link target path and space separated properties
|
|
\}\} # link closing parens",
|
|
)
|
|
.unwrap()
|
|
});
|
|
|
|
LinkIter(RE.captures_iter(contents))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_replace_all_escaped() {
|
|
let start = r"
|
|
Some text over here.
|
|
```hbs
|
|
\{{#include file.rs}} << an escaped link!
|
|
```";
|
|
let end = r"
|
|
Some text over here.
|
|
```hbs
|
|
{{#include file.rs}} << an escaped link!
|
|
```";
|
|
let mut chapter_title = "test_replace_all_escaped".to_owned();
|
|
assert_eq!(replace_all(start, "", "", 0, &mut chapter_title), end);
|
|
}
|
|
|
|
#[test]
|
|
fn test_set_chapter_title() {
|
|
let start = r"{{#title My Title}}
|
|
# My Chapter
|
|
";
|
|
let end = r"
|
|
# My Chapter
|
|
";
|
|
let mut chapter_title = "test_set_chapter_title".to_owned();
|
|
assert_eq!(replace_all(start, "", "", 0, &mut chapter_title), end);
|
|
assert_eq!(chapter_title, "My Title");
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_links_no_link() {
|
|
let s = "Some random text without link...";
|
|
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_links_partial_link() {
|
|
let s = "Some random text with {{#playground...";
|
|
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
|
let s = "Some random text with {{#include...";
|
|
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
|
let s = "Some random text with \\{{#include...";
|
|
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_links_empty_link() {
|
|
let s = "Some random text with {{#playground}} and {{#playground }} {{}} {{#}}...";
|
|
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_links_unknown_link_type() {
|
|
let s = "Some random text with {{#playgroundz ar.rs}} and {{#incn}} {{baz}} {{#bar}}...";
|
|
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_links_simple_link() {
|
|
let s = "Some random text with {{#playground file.rs}} and {{#playground test.rs }}...";
|
|
|
|
let res = find_links(s).collect::<Vec<_>>();
|
|
println!("\nOUTPUT: {res:?}\n");
|
|
|
|
assert_eq!(
|
|
res,
|
|
vec![
|
|
Link {
|
|
start_index: 22,
|
|
end_index: 45,
|
|
link_type: LinkType::Playground(PathBuf::from("file.rs"), vec![]),
|
|
link_text: "{{#playground file.rs}}",
|
|
},
|
|
Link {
|
|
start_index: 50,
|
|
end_index: 74,
|
|
link_type: LinkType::Playground(PathBuf::from("test.rs"), vec![]),
|
|
link_text: "{{#playground test.rs }}",
|
|
},
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_links_with_special_characters() {
|
|
let s = "Some random text with {{#playground foo-bar\\baz/_c++.rs}}...";
|
|
|
|
let res = find_links(s).collect::<Vec<_>>();
|
|
println!("\nOUTPUT: {res:?}\n");
|
|
|
|
assert_eq!(
|
|
res,
|
|
vec![Link {
|
|
start_index: 22,
|
|
end_index: 57,
|
|
link_type: LinkType::Playground(PathBuf::from("foo-bar\\baz/_c++.rs"), vec![]),
|
|
link_text: "{{#playground foo-bar\\baz/_c++.rs}}",
|
|
},]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_links_with_range() {
|
|
let s = "Some random text with {{#include file.rs:10:20}}...";
|
|
let res = find_links(s).collect::<Vec<_>>();
|
|
println!("\nOUTPUT: {res:?}\n");
|
|
assert_eq!(
|
|
res,
|
|
vec![Link {
|
|
start_index: 22,
|
|
end_index: 48,
|
|
link_type: LinkType::Include(
|
|
PathBuf::from("file.rs"),
|
|
RangeOrAnchor::Range(LineRange::from(9..20))
|
|
),
|
|
link_text: "{{#include file.rs:10:20}}",
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_links_with_line_number() {
|
|
let s = "Some random text with {{#include file.rs:10}}...";
|
|
let res = find_links(s).collect::<Vec<_>>();
|
|
println!("\nOUTPUT: {res:?}\n");
|
|
assert_eq!(
|
|
res,
|
|
vec![Link {
|
|
start_index: 22,
|
|
end_index: 45,
|
|
link_type: LinkType::Include(
|
|
PathBuf::from("file.rs"),
|
|
RangeOrAnchor::Range(LineRange::from(9..10))
|
|
),
|
|
link_text: "{{#include file.rs:10}}",
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_links_with_from_range() {
|
|
let s = "Some random text with {{#include file.rs:10:}}...";
|
|
let res = find_links(s).collect::<Vec<_>>();
|
|
println!("\nOUTPUT: {res:?}\n");
|
|
assert_eq!(
|
|
res,
|
|
vec![Link {
|
|
start_index: 22,
|
|
end_index: 46,
|
|
link_type: LinkType::Include(
|
|
PathBuf::from("file.rs"),
|
|
RangeOrAnchor::Range(LineRange::from(9..))
|
|
),
|
|
link_text: "{{#include file.rs:10:}}",
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_links_with_to_range() {
|
|
let s = "Some random text with {{#include file.rs::20}}...";
|
|
let res = find_links(s).collect::<Vec<_>>();
|
|
println!("\nOUTPUT: {res:?}\n");
|
|
assert_eq!(
|
|
res,
|
|
vec![Link {
|
|
start_index: 22,
|
|
end_index: 46,
|
|
link_type: LinkType::Include(
|
|
PathBuf::from("file.rs"),
|
|
RangeOrAnchor::Range(LineRange::from(..20))
|
|
),
|
|
link_text: "{{#include file.rs::20}}",
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_links_with_full_range() {
|
|
let s = "Some random text with {{#include file.rs::}}...";
|
|
let res = find_links(s).collect::<Vec<_>>();
|
|
println!("\nOUTPUT: {res:?}\n");
|
|
assert_eq!(
|
|
res,
|
|
vec![Link {
|
|
start_index: 22,
|
|
end_index: 44,
|
|
link_type: LinkType::Include(
|
|
PathBuf::from("file.rs"),
|
|
RangeOrAnchor::Range(LineRange::from(..))
|
|
),
|
|
link_text: "{{#include file.rs::}}",
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_links_with_no_range_specified() {
|
|
let s = "Some random text with {{#include file.rs}}...";
|
|
let res = find_links(s).collect::<Vec<_>>();
|
|
println!("\nOUTPUT: {res:?}\n");
|
|
assert_eq!(
|
|
res,
|
|
vec![Link {
|
|
start_index: 22,
|
|
end_index: 42,
|
|
link_type: LinkType::Include(
|
|
PathBuf::from("file.rs"),
|
|
RangeOrAnchor::Range(LineRange::from(..))
|
|
),
|
|
link_text: "{{#include file.rs}}",
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_links_with_anchor() {
|
|
let s = "Some random text with {{#include file.rs:anchor}}...";
|
|
let res = find_links(s).collect::<Vec<_>>();
|
|
println!("\nOUTPUT: {res:?}\n");
|
|
assert_eq!(
|
|
res,
|
|
vec![Link {
|
|
start_index: 22,
|
|
end_index: 49,
|
|
link_type: LinkType::Include(
|
|
PathBuf::from("file.rs"),
|
|
RangeOrAnchor::Anchor(String::from("anchor"))
|
|
),
|
|
link_text: "{{#include file.rs:anchor}}",
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_links_escaped_link() {
|
|
let s = "Some random text with escaped playground \\{{#playground file.rs editable}} ...";
|
|
|
|
let res = find_links(s).collect::<Vec<_>>();
|
|
println!("\nOUTPUT: {res:?}\n");
|
|
|
|
assert_eq!(
|
|
res,
|
|
vec![Link {
|
|
start_index: 41,
|
|
end_index: 74,
|
|
link_type: LinkType::Escaped,
|
|
link_text: "\\{{#playground file.rs editable}}",
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_playgrounds_with_properties() {
|
|
let s = "Some random text with escaped playground {{#playground file.rs editable }} and some \
|
|
more\n text {{#playground my.rs editable no_run should_panic}} ...";
|
|
|
|
let res = find_links(s).collect::<Vec<_>>();
|
|
println!("\nOUTPUT: {res:?}\n");
|
|
assert_eq!(
|
|
res,
|
|
vec![
|
|
Link {
|
|
start_index: 41,
|
|
end_index: 74,
|
|
link_type: LinkType::Playground(PathBuf::from("file.rs"), vec!["editable"]),
|
|
link_text: "{{#playground file.rs editable }}",
|
|
},
|
|
Link {
|
|
start_index: 95,
|
|
end_index: 145,
|
|
link_type: LinkType::Playground(
|
|
PathBuf::from("my.rs"),
|
|
vec!["editable", "no_run", "should_panic"],
|
|
),
|
|
link_text: "{{#playground my.rs editable no_run should_panic}}",
|
|
},
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_all_link_types() {
|
|
let s = "Some random text with escaped playground {{#include file.rs}} and \\{{#contents are \
|
|
insignifficant in escaped link}} some more\n text {{#playground my.rs editable \
|
|
no_run should_panic}} ...";
|
|
|
|
let res = find_links(s).collect::<Vec<_>>();
|
|
println!("\nOUTPUT: {res:?}\n");
|
|
assert_eq!(res.len(), 3);
|
|
assert_eq!(
|
|
res[0],
|
|
Link {
|
|
start_index: 41,
|
|
end_index: 61,
|
|
link_type: LinkType::Include(
|
|
PathBuf::from("file.rs"),
|
|
RangeOrAnchor::Range(LineRange::from(..))
|
|
),
|
|
link_text: "{{#include file.rs}}",
|
|
}
|
|
);
|
|
assert_eq!(
|
|
res[1],
|
|
Link {
|
|
start_index: 66,
|
|
end_index: 115,
|
|
link_type: LinkType::Escaped,
|
|
link_text: "\\{{#contents are insignifficant in escaped link}}",
|
|
}
|
|
);
|
|
assert_eq!(
|
|
res[2],
|
|
Link {
|
|
start_index: 133,
|
|
end_index: 183,
|
|
link_type: LinkType::Playground(
|
|
PathBuf::from("my.rs"),
|
|
vec!["editable", "no_run", "should_panic"]
|
|
),
|
|
link_text: "{{#playground my.rs editable no_run should_panic}}",
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_without_colon_includes_all() {
|
|
let link_type = parse_include_path("arbitrary");
|
|
assert_eq!(
|
|
link_type,
|
|
LinkType::Include(
|
|
PathBuf::from("arbitrary"),
|
|
RangeOrAnchor::Range(LineRange::from(RangeFull))
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_with_nothing_after_colon_includes_all() {
|
|
let link_type = parse_include_path("arbitrary:");
|
|
assert_eq!(
|
|
link_type,
|
|
LinkType::Include(
|
|
PathBuf::from("arbitrary"),
|
|
RangeOrAnchor::Range(LineRange::from(RangeFull))
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_with_two_colons_includes_all() {
|
|
let link_type = parse_include_path("arbitrary::");
|
|
assert_eq!(
|
|
link_type,
|
|
LinkType::Include(
|
|
PathBuf::from("arbitrary"),
|
|
RangeOrAnchor::Range(LineRange::from(RangeFull))
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_with_garbage_after_two_colons_includes_all() {
|
|
let link_type = parse_include_path("arbitrary::NaN");
|
|
assert_eq!(
|
|
link_type,
|
|
LinkType::Include(
|
|
PathBuf::from("arbitrary"),
|
|
RangeOrAnchor::Range(LineRange::from(RangeFull))
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_with_one_number_after_colon_only_that_line() {
|
|
let link_type = parse_include_path("arbitrary:5");
|
|
assert_eq!(
|
|
link_type,
|
|
LinkType::Include(
|
|
PathBuf::from("arbitrary"),
|
|
RangeOrAnchor::Range(LineRange::from(4..5))
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_with_one_based_start_becomes_zero_based() {
|
|
let link_type = parse_include_path("arbitrary:1");
|
|
assert_eq!(
|
|
link_type,
|
|
LinkType::Include(
|
|
PathBuf::from("arbitrary"),
|
|
RangeOrAnchor::Range(LineRange::from(0..1))
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_with_zero_based_start_stays_zero_based_but_is_probably_an_error() {
|
|
let link_type = parse_include_path("arbitrary:0");
|
|
assert_eq!(
|
|
link_type,
|
|
LinkType::Include(
|
|
PathBuf::from("arbitrary"),
|
|
RangeOrAnchor::Range(LineRange::from(0..1))
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_start_only_range() {
|
|
let link_type = parse_include_path("arbitrary:5:");
|
|
assert_eq!(
|
|
link_type,
|
|
LinkType::Include(
|
|
PathBuf::from("arbitrary"),
|
|
RangeOrAnchor::Range(LineRange::from(4..))
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_start_with_garbage_interpreted_as_start_only_range() {
|
|
let link_type = parse_include_path("arbitrary:5:NaN");
|
|
assert_eq!(
|
|
link_type,
|
|
LinkType::Include(
|
|
PathBuf::from("arbitrary"),
|
|
RangeOrAnchor::Range(LineRange::from(4..))
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_end_only_range() {
|
|
let link_type = parse_include_path("arbitrary::5");
|
|
assert_eq!(
|
|
link_type,
|
|
LinkType::Include(
|
|
PathBuf::from("arbitrary"),
|
|
RangeOrAnchor::Range(LineRange::from(..5))
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_start_and_end_range() {
|
|
let link_type = parse_include_path("arbitrary:5:10");
|
|
assert_eq!(
|
|
link_type,
|
|
LinkType::Include(
|
|
PathBuf::from("arbitrary"),
|
|
RangeOrAnchor::Range(LineRange::from(4..10))
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_with_negative_interpreted_as_anchor() {
|
|
let link_type = parse_include_path("arbitrary:-5");
|
|
assert_eq!(
|
|
link_type,
|
|
LinkType::Include(
|
|
PathBuf::from("arbitrary"),
|
|
RangeOrAnchor::Anchor("-5".to_string())
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_with_floating_point_interpreted_as_anchor() {
|
|
let link_type = parse_include_path("arbitrary:-5.7");
|
|
assert_eq!(
|
|
link_type,
|
|
LinkType::Include(
|
|
PathBuf::from("arbitrary"),
|
|
RangeOrAnchor::Anchor("-5.7".to_string())
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_with_anchor_followed_by_colon() {
|
|
let link_type = parse_include_path("arbitrary:some-anchor:this-gets-ignored");
|
|
assert_eq!(
|
|
link_type,
|
|
LinkType::Include(
|
|
PathBuf::from("arbitrary"),
|
|
RangeOrAnchor::Anchor("some-anchor".to_string())
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_with_more_than_three_colons_ignores_everything_after_third_colon() {
|
|
let link_type = parse_include_path("arbitrary:5:10:17:anything:");
|
|
assert_eq!(
|
|
link_type,
|
|
LinkType::Include(
|
|
PathBuf::from("arbitrary"),
|
|
RangeOrAnchor::Range(LineRange::from(4..10))
|
|
)
|
|
);
|
|
}
|
|
}
|