use regex::RegexBuilder;
use std::fmt;
use std::fs::{File, read_dir};
use std::io::Read;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use toml::Value;
use toml::de::Error as TomlError;
const MANIFEST_NAME: &str = "Cargo.toml";
const MARKER_START: &str = "<!-- cargo-sync-readme start -->";
const MARKER_END: &str = "<!-- cargo-sync-readme end -->";
const MARKER_RE: &str = "^<!-- cargo-sync-readme -->\r?$";
const MARKER_START_RE: &str = "^<!-- cargo-sync-readme start -->\r?$";
const MARKER_END_RE: &str = "^<!-- cargo-sync-readme end -->\r?$";
#[derive(Debug)]
enum CodeBlockState {
None,
InWithBackticks,
InWithTildes
}
#[derive(Debug)]
pub enum FindManifestError {
CannotFindManifest,
CannotOpenManifest(PathBuf),
TomlError(TomlError)
}
impl fmt::Display for FindManifestError {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
FindManifestError::CannotFindManifest => f.write_str("Cannot find manifest (Cargo.toml)."),
FindManifestError::CannotOpenManifest(ref path) =>
write!(f, "Cannot open manifest at path {}.", path.display()),
FindManifestError::TomlError(ref e) => write!(f, "TOML error: {}.", e)
}
}
}
#[derive(Debug)]
pub struct Manifest {
pub toml: Value,
pub parent_dir: PathBuf
}
impl Manifest {
fn new(toml: Value, path: PathBuf) -> Self {
Manifest { toml, parent_dir: path.parent().unwrap().to_owned() }
}
pub fn find_manifest<P>(dir: P) -> Result<Self, FindManifestError> where P: AsRef<Path> {
let dir = dir.as_ref();
if let Ok(mut dir_entry) = read_dir(dir) {
if let Some(file_entry) = dir_entry.find(
|entry| {
match entry {
Ok(entry) if entry.file_name() == MANIFEST_NAME => true,
_ => false
}
}) {
let path = file_entry.unwrap().path();
let mut file = File::open(&path).map_err(|_| FindManifestError::CannotOpenManifest(path.clone()))?;
let mut file_str = String::new();
let _ = file.read_to_string(&mut file_str);
let toml = file_str.parse().map_err(FindManifestError::TomlError)?;
Ok(Manifest::new(toml, path))
} else {
if let Some(parent) = dir.parent() {
Self::find_manifest(parent)
} else {
Err(FindManifestError::CannotFindManifest)
}
}
} else {
Err(FindManifestError::CannotFindManifest)
}
}
pub fn entry_point(&self, prefer_doc_from: Option<PreferDocFrom>) -> Option<PathBuf> {
match self.entry_point_from_toml(prefer_doc_from) {
Some(ep) => Some(ep.into()),
None => {
let lib_path = self.parent_dir.join("src/lib.rs");
let main_path = self.parent_dir.join("src/main.rs");
match (lib_path.is_file(), main_path.is_file()) {
(true, true) => match prefer_doc_from {
Some(PreferDocFrom::Binary) => Some(main_path),
Some(PreferDocFrom::Library) => Some(lib_path),
_ => None
}
(true, _) => Some(lib_path),
(_, true) => Some(main_path),
_ => None
}
}
}
}
pub fn readme(&self) -> PathBuf {
let readme = self.toml
.get("package")
.and_then(|p| p.get("readme"))
.and_then(Value::as_str)
.unwrap_or("README.md");
self.parent_dir.join(readme)
}
fn entry_point_from_toml(&self, prefer_from: Option<PreferDocFrom>) -> Option<String> {
let lib = self.toml.get("lib");
let bin = self.toml.get("bin");
let preference =
match prefer_from {
Some(PreferDocFrom::Binary) => bin.clone(),
Some(PreferDocFrom::Library) => lib.clone(),
_ => None
};
preference.or(lib).or(bin)
.and_then(|v| v.get("path"))
.and_then(Value::as_str)
.map(|s| s.to_owned())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PreferDocFrom {
Binary,
Library
}
impl FromStr for PreferDocFrom {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"bin" => Ok(PreferDocFrom::Binary),
"lib" => Ok(PreferDocFrom::Library),
_ => Err("not a valid preference".to_owned())
}
}
}
pub fn extract_inner_doc<P>(path: P, show_hidden_doc: bool, crlf: bool) -> String where P: AsRef<Path> {
let mut file = File::open(path.as_ref()).unwrap();
let mut content = String::new();
let mut codeblock_st = CodeBlockState::None;
let _ = file.read_to_string(&mut content);
let lines: Vec<_> = content
.lines()
.skip_while(|l| !l.starts_with("//!"))
.take_while(|l| l.starts_with("//!"))
.map(|l| {
if crlf {
format!("{}\r\n", l.trim_start_matches("//!"))
} else {
format!("{}\n", l.trim_start_matches("//!"))
}
})
.collect();
let offset = lines
.iter()
.flat_map(|line| line.find(|c: char| !c.is_whitespace()))
.min()
.unwrap_or(0);
lines
.iter()
.map(|line| if crlf && line == "\r\n" || line == "\n" { line } else { &line[offset..] })
.filter(|l| {
if show_hidden_doc {
true
} else {
strip_hidden_doc_tests(&mut codeblock_st, l)
}
})
.collect()
}
#[derive(Debug, Eq, PartialEq)]
pub enum TransformError {
CannotReadReadme(PathBuf),
MissingOrIllFormatMarkers
}
impl fmt::Display for TransformError {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
TransformError::CannotReadReadme(ref path) => write!(f, "Cannot read README at {}.", path.display()),
TransformError::MissingOrIllFormatMarkers => f.write_str("Markers not found or ill-formed; check your file again."),
}
}
}
pub fn read_readme<P>(path: P) -> Result<String, TransformError> where P: AsRef<Path> {
let path = path.as_ref();
let mut file = File::open(path).map_err(|_| TransformError::CannotReadReadme(path.to_owned()))?;
let mut content = String::new();
let _ = file.read_to_string(&mut content);
Ok(content)
}
pub fn transform_readme<C, R>(
content: C,
doc: R,
crlf: bool
) -> Result<String, TransformError>
where C: AsRef<str>,
R: AsRef<str> {
let content = content.as_ref();
let doc = doc.as_ref();
let mut marker_re_builder = RegexBuilder::new(MARKER_RE);
marker_re_builder.multi_line(true);
let marker_re = marker_re_builder.build().unwrap();
if let Some(marker_match) = marker_re.find(&content) {
let first_part = &content[0 .. marker_match.start()];
let second_part = &content[if crlf { marker_match.end() - 1 } else { marker_match.end() } ..];
Ok(reformat_with_markers(first_part, doc, second_part, crlf))
} else {
let mut marker_start_re_builder = RegexBuilder::new(MARKER_START_RE);
marker_start_re_builder.multi_line(true);
let marker_start_re = marker_start_re_builder.build().unwrap();
let mut marker_end_re_builder = RegexBuilder::new(MARKER_END_RE);
marker_end_re_builder.multi_line(true);
let marker_end_re = marker_end_re_builder.build().unwrap();
let marker_start = marker_start_re.find(&content);
let marker_end = marker_end_re.find(&content);
match (marker_start, marker_end) {
(Some(start_match), Some(end_match)) => {
let first_part = &content[0 .. start_match.start()];
let second_part = &content[if crlf { end_match.end() - 1 } else { end_match.end() } ..];
Ok(reformat_with_markers(first_part, doc, second_part, crlf))
},
_ => Err(TransformError::MissingOrIllFormatMarkers)
}
}
}
fn reformat_with_markers(first_part: &str, doc: &str, second_part: &str, crlf: bool) -> String {
if crlf {
format!("{}{}\r\n\r\n{}\r\n{}{}", first_part, MARKER_START, doc, MARKER_END, second_part)
} else {
format!("{}{}\n\n{}\n{}{}", first_part, MARKER_START, doc, MARKER_END, second_part)
}
}
fn strip_hidden_doc_tests(st: &mut CodeBlockState, line: &str) -> bool {
match st {
CodeBlockState::None => {
if line.starts_with("~~~") {
*st = CodeBlockState::InWithTildes;
} else if line.starts_with("```") {
*st = CodeBlockState::InWithBackticks;
}
true
}
CodeBlockState::InWithTildes => {
if line.starts_with("# ") {
false
} else {
if line.starts_with("~~~") {
*st = CodeBlockState::None;
}
true
}
}
CodeBlockState::InWithBackticks => {
if line.starts_with("# ") {
false
} else {
if line.starts_with("```") {
*st = CodeBlockState::None;
}
true
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_dash_starting_lines() {
let mut st = CodeBlockState::None;
assert_eq!(strip_hidden_doc_tests(&mut st, "# okay"), true);
assert_eq!(strip_hidden_doc_tests(&mut st, "```"), true);
assert_eq!(strip_hidden_doc_tests(&mut st, "foo bar zoo"), true);
assert_eq!(strip_hidden_doc_tests(&mut st, "# hello"), false);
assert_eq!(strip_hidden_doc_tests(&mut st, "#"), true);
assert_eq!(strip_hidden_doc_tests(&mut st, "# "), false);
assert_eq!(strip_hidden_doc_tests(&mut st, "# ### nope"), false);
assert_eq!(strip_hidden_doc_tests(&mut st, "~~~"), true);
assert_eq!(strip_hidden_doc_tests(&mut st, "```"), true);
assert_eq!(strip_hidden_doc_tests(&mut st, "# still okay"), true);
}
#[test]
fn simple_transform() {
let doc = "Test! <3";
let readme = "Foo\n<!-- cargo-sync-readme -->\nbar\nzoo";
let output = transform_readme(readme, doc, false);
assert_eq!(output, Ok("Foo\n<!-- cargo-sync-readme start -->\n\nTest! <3\n<!-- cargo-sync-readme end -->\nbar\nzoo".to_owned()));
}
#[test]
fn windows_line_endings() {
let doc = "Test! <3";
let readme = "Foo\r\n<!-- cargo-sync-readme -->\r\nbar\r\nzoo";
let output = transform_readme(readme, doc, true);
assert_eq!(output, Ok("Foo\r\n<!-- cargo-sync-readme start -->\r\n\r\nTest! <3\r\n<!-- cargo-sync-readme end -->\r\nbar\r\nzoo".to_owned()));
}
}