use crate::utils::{ItemOrOther, MarkdownItemIterator, Span};
use crate::{Doc, Readme};
use thiserror::Error;
pub const MARKER_RDME: &str = "<!-- cargo-rdme -->";
const MARKER_RDME_START: &str = "<!-- cargo-rdme start -->";
const MARKER_RDME_END: &str = "<!-- cargo-rdme end -->";
#[derive(PartialEq, Eq, Clone, Debug)]
struct Heading<'a> {
level: u8,
text: &'a str,
}
#[derive(PartialEq, Eq, Clone, Debug)]
enum ReadmeLine<'a> {
Heading(Heading<'a>, Span),
MarkerCargoRdme(Span),
MarkerCargoRdmeStart(Span),
MarkerCargoRdmeEnd(Span),
}
fn readme_line_iterator(readme: &Readme) -> MarkdownItemIterator<ReadmeLine> {
use pulldown_cmark::{Event, Options, Parser, Tag};
let source = readme.as_string();
let parser = Parser::new_ext(source, Options::all());
let is_line_start =
|start| start == 0 || source[0..start].chars().rev().find(|&c| c != ' ') == Some('\n');
let mut depth = 0;
let iter = parser.into_offset_iter().filter_map(move |(event, range)| match event {
Event::Start(Tag::Heading { level, .. }) => Some((
range.clone().into(),
ReadmeLine::Heading(
Heading { level: level as u8, text: &source[range.start..range.end] },
range.into(),
),
)),
Event::Html(ref html) if is_line_start(range.start) => {
let trimmed_line = html.strip_suffix('\r').unwrap_or_else(|| html.as_ref()).trim();
match trimmed_line {
MARKER_RDME if depth == 0 => {
Some((range.clone().into(), ReadmeLine::MarkerCargoRdme(range.into())))
}
MARKER_RDME_START if depth == 0 => {
depth += 1;
Some((range.clone().into(), ReadmeLine::MarkerCargoRdmeStart(range.into())))
}
MARKER_RDME_END if depth <= 1 => {
depth -= 1;
Some((range.clone().into(), ReadmeLine::MarkerCargoRdmeEnd(range.into())))
}
MARKER_RDME_START => {
depth += 1;
None
}
MARKER_RDME_END => {
depth -= 1;
None
}
_ => None,
}
}
_ => None,
});
MarkdownItemIterator::new(source, iter)
}
fn doc_heading_iterator(doc: &Doc) -> MarkdownItemIterator<Heading> {
use pulldown_cmark::{Event, Options, Parser, Tag};
let source = doc.as_string();
let parser = Parser::new_ext(source, Options::all());
let iter = parser.into_offset_iter().filter_map(move |(event, range)| match event {
Event::Start(Tag::Heading { level, .. }) => Some((
range.clone().into(),
Heading { level: level as u8, text: &source[range.start..range.end] },
)),
_ => None,
});
MarkdownItemIterator::new(source, iter)
}
#[derive(Error, Eq, PartialEq, Debug)]
pub enum InjectDocError {
#[error("unexpected end marker at line {line_number}")]
UnexpectedMarkerCargoRdmeEnd { line_number: usize },
#[error("unmatched start marker")]
UnmatchedMarkerCargoRdmeStart,
}
fn bump_heading_level(doc: &Doc, level_bump: u8) -> Doc {
let mut new_doc = String::with_capacity(doc.as_string().len() + 256);
for item in doc_heading_iterator(doc).complete() {
match item {
ItemOrOther::Item(Heading { text, .. }) => {
(0..level_bump).for_each(|_| new_doc.push('#'));
new_doc.push_str(text);
}
ItemOrOther::Other(other) => {
new_doc.push_str(other);
}
}
}
Doc::from_str(new_doc)
}
pub struct NewReadme {
pub readme: Readme,
pub had_marker: bool,
}
pub fn inject_doc_in_readme(
readme: &Readme,
doc: &Doc,
heading_base_level: Option<u8>,
) -> Result<NewReadme, InjectDocError> {
fn inject(new_readme: &mut String, doc: &Doc) {
new_readme.push_str(MARKER_RDME_START);
new_readme.push_str("\n\n");
doc.lines().for_each(|line| {
new_readme.push_str(line);
new_readme.push('\n');
});
new_readme.push('\n');
new_readme.push_str(MARKER_RDME_END);
new_readme.push('\n');
}
let mut new_readme: String =
String::with_capacity(readme.as_string().len() + doc.as_string().len() + 1024);
let mut inside_markers = false;
let mut last_heading_level: u8 = 0;
let mut had_marker = false;
for item in readme_line_iterator(readme).complete() {
match (inside_markers, item) {
(true, ItemOrOther::Item(ReadmeLine::MarkerCargoRdmeEnd(_))) => {
inside_markers = false;
}
(true, _) => (),
(false, ItemOrOther::Item(ReadmeLine::MarkerCargoRdmeEnd(span))) => {
let line_number =
1 + readme.as_string()[0..span.start].chars().filter(|&c| c == '\n').count();
return Err(InjectDocError::UnexpectedMarkerCargoRdmeEnd { line_number });
}
(false, ItemOrOther::Item(ReadmeLine::Heading(Heading { level, text }, _))) => {
new_readme.push_str(text);
last_heading_level = level;
}
(false, ItemOrOther::Other(other)) => new_readme.push_str(other),
(false, ItemOrOther::Item(ReadmeLine::MarkerCargoRdme(_))) => {
let level_bump = heading_base_level.unwrap_or(last_heading_level);
let doc = bump_heading_level(doc, level_bump);
inject(&mut new_readme, &doc);
had_marker = true;
}
(false, ItemOrOther::Item(ReadmeLine::MarkerCargoRdmeStart(_))) => {
let level_bump = heading_base_level.unwrap_or(last_heading_level);
let doc = bump_heading_level(doc, level_bump);
inject(&mut new_readme, &doc);
inside_markers = true;
had_marker = true;
}
}
}
match inside_markers {
true => Err(InjectDocError::UnmatchedMarkerCargoRdmeStart),
false => {
let new_readme = NewReadme { readme: Readme::from_str(new_readme), had_marker };
Ok(new_readme)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn test_readme_line_iterator() {
let str = indoc! { "
marker test <!-- cargo-rdme -->.
Starting with whitespace.
<!-- cargo-rdme start -->
<!-- cargo-rdme end -->
<!-- cargo-rdme -->
<!-- cargo-rdme end --> <- Does not count.
<!-- cargo-rdme start -->
<!-- cargo-rdme end -->\r
<!-- cargo-rdme start -->
<!-- cargo-rdme end --> \r
<!-- cargo-rdme start -->
<!-- cargo-rdme end --> "
};
let readme = Readme::from_str(str);
let mut iter = readme_line_iterator(&readme).items();
assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeStart(_))));
assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeEnd(_))));
assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdme(_))));
assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeStart(_))));
assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeEnd(_))));
assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeStart(_))));
assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeEnd(_))));
assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeStart(_))));
assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeEnd(_))));
assert_eq!(iter.next(), None);
}
#[test]
fn test_readme_line_iterator_nested() {
let str = indoc! { "
A
<!-- cargo-rdme start -->
B
<!-- cargo-rdme -->
C
<!-- cargo-rdme start -->
D
<!-- cargo-rdme end -->
E
<!-- cargo-rdme end -->
F"
};
let readme = Readme::from_str(str);
let mut iter = readme_line_iterator(&readme).items();
assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeStart(_))));
assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeEnd(_))));
assert_eq!(iter.next(), None);
}
#[test]
fn test_inject_doc_single_marker() {
let readme_str = indoc! { r#"
This is a really nice crate.
<!-- cargo-rdme -->
Hope you enjoy!
"#
};
let doc_str = indoc! { r#"
# The crate
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
"#
};
let expected = indoc! { r#"
This is a really nice crate.
<!-- cargo-rdme start -->
# The crate
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
<!-- cargo-rdme end -->
Hope you enjoy!
"#
};
let readme = Readme::from_str(readme_str);
let doc = Doc::from_str(doc_str);
let new_readme = inject_doc_in_readme(&readme, &doc, None).unwrap();
assert_eq!(new_readme.readme.markdown.as_string(), expected);
assert!(new_readme.had_marker);
}
#[test]
fn test_inject_doc_start_end_marker() {
let readme_str = indoc! { r#"
This is a really nice crate.
<!-- cargo-rdme start -->
Li Europan lingues es membres del sam familie. Lor separat existentie es un myth.
Por scientie, musica, sport etc, litot Europa usa li sam vocabular.
<!-- cargo-rdme end -->
Hope you enjoy!
"#
};
let doc_str = indoc! { r#"
# The crate
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
"#
};
let expected = indoc! { r#"
This is a really nice crate.
<!-- cargo-rdme start -->
# The crate
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
<!-- cargo-rdme end -->
Hope you enjoy!
"#
};
let readme = Readme::from_str(readme_str);
let doc = Doc::from_str(doc_str);
let new_readme = inject_doc_in_readme(&readme, &doc, None).unwrap();
assert_eq!(new_readme.readme.markdown.as_string(), expected);
assert!(new_readme.had_marker);
}
#[test]
fn test_inject_doc_unmatched_start_marker() {
let readme_str = indoc! { r#"
This is a really nice crate.
<!-- cargo-rdme -->
<!-- cargo-rdme start -->
Hope you enjoy!
"#
};
let doc_str = indoc! { r#"
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua.
"#
};
let readme = Readme::from_str(readme_str);
let doc = Doc::from_str(doc_str);
let result = inject_doc_in_readme(&readme, &doc, None);
assert_eq!(result.err(), Some(InjectDocError::UnmatchedMarkerCargoRdmeStart));
}
#[test]
fn test_inject_doc_unexpected_end_marker() {
let readme_str = indoc! { r#"
This is a really nice crate.
<!-- cargo-rdme -->
<!-- cargo-rdme end -->
Hope you enjoy!
"#
};
let doc_str = indoc! { r#"
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua.
"#
};
let readme = Readme::from_str(readme_str);
let doc = Doc::from_str(doc_str);
let result = inject_doc_in_readme(&readme, &doc, None);
assert_eq!(
result.err(),
Some(InjectDocError::UnexpectedMarkerCargoRdmeEnd { line_number: 5 })
);
}
#[test]
fn test_bump_heading_level() {
let doc_str = indoc! { r#"
# Foo
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua.
## Bar
Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
"#
};
let doc = Doc::from_str(doc_str);
let new_readme = bump_heading_level(&doc, 0);
assert_eq!(new_readme.markdown.as_string(), doc_str);
let expected = indoc! { r#"
### Foo
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua.
#### Bar
Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
"#
};
let new_readme = bump_heading_level(&doc, 2);
assert_eq!(new_readme.markdown.as_string(), expected);
}
#[test]
fn test_inject_doc_bump_heading_level() {
let readme_str = indoc! { r#"
# The crate
This is a really nice crate.
<!-- cargo-rdme -->
Hope you enjoy!
"#
};
let doc_str = indoc! { r#"
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
# Foo
Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
"#
};
let expected = indoc! { r#"
# The crate
This is a really nice crate.
<!-- cargo-rdme start -->
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
## Foo
Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
<!-- cargo-rdme end -->
Hope you enjoy!
"#
};
let readme = Readme::from_str(readme_str);
let doc = Doc::from_str(doc_str);
let new_readme = inject_doc_in_readme(&readme, &doc, None).unwrap();
assert_eq!(new_readme.readme.markdown.as_string(), expected);
assert!(new_readme.had_marker);
}
#[test]
fn test_inject_doc_bump_heading_level_ignore_within_markers() {
let readme_str = indoc! { r#"
# The crate
This is a really nice crate.
<!-- cargo-rdme start -->
### The crate
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
<!-- cargo-rdme end -->
Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
<!-- cargo-rdme -->
Hope you enjoy!
"#
};
let doc_str = indoc! { r#"
# Foo
Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
"#
};
let expected = indoc! { r#"
# The crate
This is a really nice crate.
<!-- cargo-rdme start -->
## Foo
Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
<!-- cargo-rdme end -->
Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
<!-- cargo-rdme start -->
## Foo
Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
<!-- cargo-rdme end -->
Hope you enjoy!
"#
};
let readme = Readme::from_str(readme_str);
let doc = Doc::from_str(doc_str);
let new_readme = inject_doc_in_readme(&readme, &doc, None).unwrap();
assert_eq!(new_readme.readme.markdown.as_string(), expected);
assert!(new_readme.had_marker);
}
#[test]
fn test_inject_doc_bump_heading_level_ignore_code_blocks() {
let readme_str = indoc! { r#"
# The crate
This is a really nice crate.
You should try it!
```
### This is code
```
<!-- cargo-rdme -->
"#
};
let doc_str = indoc! { r#"
# Foo
Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
"#
};
let expected = indoc! { r#"
# The crate
This is a really nice crate.
You should try it!
```
### This is code
```
<!-- cargo-rdme start -->
## Foo
Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
<!-- cargo-rdme end -->
"#
};
let readme = Readme::from_str(readme_str);
let doc = Doc::from_str(doc_str);
let new_readme = inject_doc_in_readme(&readme, &doc, None).unwrap();
assert_eq!(new_readme.readme.markdown.as_string(), expected);
assert!(new_readme.had_marker);
}
#[test]
fn test_inject_doc_with_zero_heading_base_level() {
let readme_str = indoc! { r#"
# The crate
This is a really nice crate.
<!-- cargo-rdme -->
Hope you enjoy!
"#
};
let doc_str = indoc! { r#"
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
# Foo
Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
"#
};
let expected = indoc! { r#"
# The crate
This is a really nice crate.
<!-- cargo-rdme start -->
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
# Foo
Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
<!-- cargo-rdme end -->
Hope you enjoy!
"#
};
let readme = Readme::from_str(readme_str);
let doc = Doc::from_str(doc_str);
let new_readme = inject_doc_in_readme(&readme, &doc, Some(0)).unwrap();
assert_eq!(new_readme.readme.markdown.as_string(), expected);
assert!(new_readme.had_marker);
}
}