1
0
mirror of https://github.com/lise-henry/crowbook synced 2024-11-18 00:13:55 +01:00

no longer change current directory, as it generated problems in multithreaded envs

This commit is contained in:
Elisabeth Henry 2016-02-26 17:33:33 +01:00
parent 431907cf39
commit bd2971ab61
8 changed files with 180 additions and 136 deletions

@ -13,8 +13,7 @@ use number::Number;
use std::fs::File;
use std::io::{self, Write,Read};
use std::env;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::borrow::Cow;
use std::collections::HashMap;
@ -28,13 +27,13 @@ title:str:Untitled # The title of the book
lang:str:en # The language of the book
subject:str # Subject of the book (used for EPUB metadata)
description:str # Description of the book (used for EPUB metadata)
cover:str # File name of the cover of the book
cover:path # File name of the cover of the book
# Output options
output.epub:str # Output file name for EPUB rendering
output.html:str # Output file name for HTML rendering
output.tex:str # Output file name for LaTeX rendering
output.pdf:str # Output file name for PDF rendering
output.odt:str # Output file name for ODT rendering
output.epub:path # Output file name for EPUB rendering
output.html:path # Output file name for HTML rendering
output.tex:path # Output file name for LaTeX rendering
output.pdf:path # Output file name for PDF rendering
output.odt:path # Output file name for ODT rendering
# Misc options
@ -45,22 +44,22 @@ autoclean:bool:true # Toggles cleaning of input markdown (not us
verbose:bool:false # Toggle verbose mode
side_notes:bool:false # Display footnotes as side notes in HTML/Epub
nb_char:char:'' # The non-breaking character to use for autoclean when lang is set to fr
temp_dir:str:. # Path where to create a temporary directory
temp_dir:path:. # Path where to create a temporary directory
numbering_template:str:{{number}}. {{title}} # Format of numbered titles
# HTML options
html.template:str # Path of an HTML template
html.css:str # Path of a stylesheet to use with HTML rendering
html.template:path # Path of an HTML template
html.css:path # Path of a stylesheet to use with HTML rendering
# EPUB options
epub.version:int:2 # The EPUB version to generate
epub.css:str # Path of a stylesheet to use with EPUB rendering
epub.template:str # Path of an epub template for chapter
epub.css:path # Path of a stylesheet to use with EPUB rendering
epub.template:path # Path of an epub template for chapter
# LaTeX options
tex.links_as_footnotes:bool:true # If set to true, will add foontotes to URL of links in LaTeX/PDF output
tex.command:str:pdflatex # LaTeX flavour to use for generating PDF
tex.template:str # Path of a LaTeX template file
tex.template:path # Path of a LaTeX template file
";
@ -103,11 +102,13 @@ pub struct Book {
valid_bools: Vec<&'static str>,
valid_chars: Vec<&'static str>,
valid_strings: Vec<&'static str>,
valid_paths: Vec<&'static str>,
valid_ints: Vec<&'static str>,
/// root path of the book
root: PathBuf,
}
impl Book {
/// Creates a new, empty `Book` with default options
pub fn new() -> Book {
@ -118,6 +119,8 @@ impl Book {
valid_chars:vec!(),
valid_ints:vec!(),
valid_strings:vec!(),
valid_paths:vec!(),
root: PathBuf::new(),
};
for (_, key, option_type, default_value) in Book::options_to_vec() {
if key.is_none() {
@ -129,6 +132,7 @@ impl Book {
"bool" => book.valid_bools.push(key),
"int" => book.valid_ints.push(key),
"char" => book.valid_chars.push(key),
"path" => book.valid_paths.push(key),
_ => panic!(format!("Ill-formatted OPTIONS string: unrecognized type '{}'", option_type.unwrap())),
}
if let Some(value) = default_value {
@ -138,48 +142,36 @@ impl Book {
book
}
/// Returns a description of all options valid to pass to a book.
/// Creates a new book from a file
///
/// # arguments
/// * `md`: whether the output should be formatted in Markdown
pub fn description(md: bool) -> String {
let mut out = String::new();
let mut previous_is_comment = true;
for (comment, key, o_type, default) in Book::options_to_vec() {
if key.is_none() {
if !previous_is_comment {
out.push_str("\n");
previous_is_comment = true;
}
out.push_str(&format!("### {} ###\n", comment));
continue;
}
previous_is_comment = false;
let o_type = match o_type.unwrap() {
"bool" => "boolean",
"int" => "integer",
"char" => "char",
"str" => "string",
_ => unreachable!()
};
let def = if let Some(value) = default {
value
} else {
"not set"
};
if md {
out.push_str(&format!("- **`{}`**
- **type**: {}
- **default value**: `{}`
- {}\n", key.unwrap(), o_type, def, comment));
} else {
out.push_str(&format!("- {} (type: {}) (default: {}) {}\n", key.unwrap(), o_type, def,comment));
}
/// Note that this method also changes the current directory to the one of this file
///
/// # Arguments
/// * `filename`: the path of file to load.
/// * `verbose`: sets the book to verbose mode even if the file's doesn't specify it
/// or specifies `verbose: false`
pub fn new_from_file(filename: &str, verbose: bool) -> Result<Book> {
let mut book = Book::new();
if verbose {
book.set_option("verbose", "true").unwrap();
}
out
let path = Path::new(filename);
let mut f = try!(File::open(&path).map_err(|_| Error::FileNotFound(String::from(filename))));
// Set book path to book's directory
if let Some(parent) = path.parent() {
book.root = parent.to_owned();
}
let mut s = String::new();
try!(f.read_to_string(&mut s).map_err(|_| Error::ConfigParser("file contains invalid UTF-8, could not parse it",
filename.to_owned())));
try!(book.set_from_config(&s));
Ok(book)
}
/// Sets an option
///
/// # Arguments
@ -205,6 +197,9 @@ impl Book {
if self.valid_strings.contains(&key) {
self.options.insert(key.to_owned(), BookOption::String(value.to_owned()));
Ok(())
} else if self.valid_paths.contains(&key) {
self.options.insert(key.to_owned(), BookOption::Path(value.to_owned()));
Ok(())
} else if self.valid_chars.contains(&key) {
let words: Vec<_> = value.trim().split('\'').collect();
if words.len() != 3 {
@ -333,40 +328,6 @@ impl Book {
}
/// Creates a new book from a file
///
/// Note that this method also changes the current directory to the one of this file
///
/// # Arguments
/// * `filename`: the path of file to load.
/// * `verbose`: sets the book to verbose mode even if the file's doesn't specify it
/// or specifies `verbose: false`
pub fn new_from_file(filename: &str, verbose: bool) -> Result<Book> {
let path = Path::new(filename);
let mut f = try!(File::open(&path).map_err(|_| Error::FileNotFound(String::from(filename))));
// change current directory
if let Some(parent) = path.parent() {
if !parent.to_string_lossy().is_empty() {
if !env::set_current_dir(&parent).is_ok() {
return Err(Error::ConfigParser("could not change current directory to the one of the config file",
format!("{}", parent.display())));
}
}
}
let mut s = String::new();
try!(f.read_to_string(&mut s).map_err(|_| Error::ConfigParser("file contains invalid UTF-8, could not parse it",
String::from(filename))));
let mut book = Book::new();
if verbose {
book.set_option("verbose", "true").unwrap();
}
try!(book.set_from_config(&s));
Ok(book)
}
/// Adds a chapter, as a file name, to the book
///
@ -383,12 +344,56 @@ impl Book {
pub fn add_chapter(&mut self, number: Number, file: &str) -> Result<()> {
self.debug(&format!("Parsing chapter: {}...", file));
let mut parser = Parser::new();
let file = self.root.join(file);
let v = try!(parser.parse_file(file));
self.chapters.push((number, v));
Ok(())
}
/// Returns a description of all options valid to pass to a book.
///
/// # arguments
/// * `md`: whether the output should be formatted in Markdown
pub fn description(md: bool) -> String {
let mut out = String::new();
let mut previous_is_comment = true;
for (comment, key, o_type, default) in Book::options_to_vec() {
if key.is_none() {
if !previous_is_comment {
out.push_str("\n");
previous_is_comment = true;
}
out.push_str(&format!("### {} ###\n", comment));
continue;
}
previous_is_comment = false;
let o_type = match o_type.unwrap() {
"bool" => "boolean",
"int" => "integer",
"char" => "char",
"str" => "string",
_ => unreachable!()
};
let def = if let Some(value) = default {
value
} else {
"not set"
};
if md {
out.push_str(&format!("- **`{}`**
- **type**: {}
- **default value**: `{}`
- {}\n", key.unwrap(), o_type, def, comment));
} else {
out.push_str(&format!("- {} (type: {}) (default: {}) {}\n", key.unwrap(), o_type, def,comment));
}
}
out
}
/// Adds a chapter, as a string, to the book
///
/// `Book` will then parse the string and store the AST (i.e., a vector
@ -458,11 +463,31 @@ impl Book {
self.options.get(key).ok_or(Error::InvalidOption(format!("option {} is not present", key)))
}
/// gets a string option as str
/// Gets a string option
pub fn get_str(&self, key: &str) -> Result<&str> {
try!(self.get_option(key)).as_str()
}
/// Get a path option
///
/// Adds book's root path before it
pub fn get_path(&self, key: &str) -> Result<String> {
let path: &str = try!(try!(self.get_option(key)).as_path());
let new_path:PathBuf = self.root.join(path);
if let Some(path) = new_path.to_str() {
Ok(path.to_owned())
} else {
Err(Error::BookOption(format!("'{}''s path contains invalid UTF-8 code", key)))
}
}
/// Get a path option
///
/// Don't add book's root path before it
pub fn get_relative_path(&self, key: &str) -> Result<&str> {
try!(self.get_option(key)).as_path()
}
/// gets a bool option
pub fn get_bool(&self, key: &str) -> Result<bool> {
try!(self.get_option(key)).as_bool()
@ -485,7 +510,7 @@ impl Book {
let mut latex = LatexRenderer::new(&self);
let result = try!(latex.render_pdf());
self.debug(&result);
self.println(&format!("Successfully generated pdf file: {}", self.get_str("output.pdf").unwrap()));
self.println(&format!("Successfully generated pdf file: {}", self.get_path("output.pdf").unwrap()));
Ok(())
}
@ -495,7 +520,7 @@ impl Book {
let mut epub = EpubRenderer::new(&self);
let result = try!(epub.render_book());
self.debug(&result);
self.println(&format!("Successfully generated epub file: {}", self.get_str("output.epub").unwrap()));
self.println(&format!("Successfully generated epub file: {}", self.get_path("output.epub").unwrap()));
Ok(())
}
@ -505,7 +530,7 @@ impl Book {
let mut odt = OdtRenderer::new(&self);
let result = try!(odt.render_book());
self.debug(&result);
self.println(&format!("Successfully generated odt file: {}", self.get_str("output.odt").unwrap()));
self.println(&format!("Successfully generated odt file: {}", self.get_path("output.odt").unwrap()));
Ok(())
}
@ -539,22 +564,22 @@ impl Book {
try!(self.render_epub());
}
if let Ok(file) = self.get_str("output.html") {
if let Ok(file) = self.get_path("output.html") {
did_some_stuff = true;
let mut f = try!(File::create(file).map_err(|_| Error::Render("could not create HTML file")));
try!(self.render_html(&mut f));
}
if let Ok(file) = self.get_str("output.tex") {
if let Ok(file) = self.get_path("output.tex") {
did_some_stuff = true;
let mut f = try!(File::create(file).map_err(|_| Error::Render("could not create LaTeX file")));
try!(self.render_tex(&mut f));
}
if self.get_str("output.pdf").is_ok() {
if self.get_option("output.pdf").is_ok() {
did_some_stuff = true;
try!(self.render_pdf());
}
if self.get_str("output.odt").is_ok() {
if self.get_option("output.odt").is_ok() {
did_some_stuff = true;
try!(self.render_odt());
}

@ -3,10 +3,11 @@ use error::{Error,Result};
/// Structure for storing a book option
#[derive(Debug, PartialEq)]
pub enum BookOption {
String(String), /// Stores a string
String(String), // Stores a string
Bool(bool), // stores a boolean
Char(char), // stores a char
Int(i32), // stores an int
Path(String), // Stores a path
}
impl BookOption {
@ -18,6 +19,14 @@ impl BookOption {
}
}
/// Returns the BookOption as a &str, but only if it is a path
pub fn as_path(&self) -> Result<&str> {
match *self {
BookOption::Path(ref s) => Ok(s),
_ => Err(Error::BookOption(format!("{:?} is not a path", self)))
}
}
/// Retuns the BookOption as a bool
pub fn as_bool(&self) -> Result<bool> {
match *self {

@ -55,7 +55,7 @@ impl<'a> EpubRenderer<'a> {
/// Render a book
pub fn render_book(&mut self) -> Result<String> {
let mut zipper = try!(Zipper::new(&self.book.get_str("temp_dir").unwrap()));
let mut zipper = try!(Zipper::new(&self.book.get_path("temp_dir").unwrap()));
// Write mimetype
try!(zipper.write("mimetype", b"application/epub+zip", true));
@ -104,19 +104,18 @@ impl<'a> EpubRenderer<'a> {
try!(zipper.write("toc.ncx", &try!(self.render_toc()).as_bytes(), true));
// Write the cover (if needs be)
if let Ok(cover) = self.book.get_str("cover") {
let s: &str = &*cover;
let mut f = try!(File::open(s).map_err(|_| Error::FileNotFound(String::from(s))));
if let Ok(ref s) = self.book.get_path("cover") {
let mut f = try!(File::open(s).map_err(|_| Error::FileNotFound(s.to_owned())));
let mut content = vec!();
try!(f.read_to_end(&mut content).map_err(|_| Error::Render("Error while reading cover file")));
try!(zipper.write(s, &content, true));
try!(zipper.write(self.book.get_relative_path("cover").unwrap(), &content, true));
// also write cover.xhtml
try!(zipper.write("cover.xhtml", &try!(self.render_cover()).as_bytes(), true));
}
if let Ok(epub_file) = self.book.get_str("output.epub") {
let res = try!(zipper.generate_epub(epub_file));
if let Ok(epub_file) = self.book.get_path("output.epub") {
let res = try!(zipper.generate_epub(&epub_file));
Ok(res)
} else {
Err(Error::Render("no output epub file specified in book config"))
@ -174,7 +173,7 @@ impl<'a> EpubRenderer<'a> {
if let Ok(s) = self.book.get_str("subject") {
optional.push_str(&format!("<dc:subject>{}</dc:subject>\n", s));
}
if let Ok(s) = self.book.get_str("cover") {
if let Ok(s) = self.book.get_relative_path("cover") {
optional.push_str(&format!("<meta name = \"cover\" content = \"{}\" />\n", s));
cover_xhtml.push_str(&format!("<reference type=\"cover\" title=\"Cover\" href=\"cover.xhtml\" />"));
}
@ -188,7 +187,7 @@ impl<'a> EpubRenderer<'a> {
let mut items = String::new();
let mut itemrefs = String::new();
let mut coverref = String::new();
if self.book.get_str("cover").is_ok() {
if self.book.get_option("cover").is_ok() {
items.push_str("<item id = \"cover_xhtml\" href = \"cover.xhtml\" media-type = \"application/xhtml+xml\" />\n");
coverref.push_str("<itemref idref = \"cover_xhtml\" />");
}
@ -200,7 +199,7 @@ impl<'a> EpubRenderer<'a> {
itemrefs.push_str(&format!("<itemref idref=\"{}\" />\n", to_id(&filename)));
}
// oh we must put cover in the manifest too
if let Ok(s) = self.book.get_str("cover") {
if let Ok(s) = self.book.get_relative_path("cover") {
let format = if let Some(ext) = Path::new(s).extension() {
if let Some(extension) = ext.to_str() {
match extension {
@ -247,7 +246,7 @@ impl<'a> EpubRenderer<'a> {
/// Render cover.xhtml
fn render_cover(&self) -> Result<String> {
if let Ok(ref cover) = self.book.get_str("cover") {
if let Ok(cover) = self.book.get_relative_path("cover") {
let template = mustache::compile_str(if self.book.get_i32("epub.version").unwrap() == 3 {epub3::COVER} else {COVER});
let data = self.book.get_mapbuilder("none")
.insert_str("cover", cover.clone())

@ -44,13 +44,11 @@ impl<'a> LatexRenderer<'a> {
/// Render pdf in a file
pub fn render_pdf(&mut self) -> Result<String> {
if let Ok(pdf_file) = self.book.get_str("output.pdf") {
let base_file = try!(Path::new(pdf_file).file_stem().ok_or(Error::Render("could not stem pdf filename")));
let tex_file = format!("{}.tex", base_file.to_str().unwrap());
if let Ok(pdf_file) = self.book.get_path("output.pdf") {
let content = try!(self.render_book());
let mut zipper = try!(Zipper::new(self.book.get_str("temp_dir").unwrap()));
try!(zipper.write(&tex_file, &content.as_bytes(), false));
zipper.generate_pdf(&self.book.get_str("tex.command").unwrap(), &tex_file, pdf_file)
let mut zipper = try!(Zipper::new(&self.book.get_path("temp_dir").unwrap()));
try!(zipper.write("result.tex", &content.as_bytes(), false));
zipper.generate_pdf(&self.book.get_str("tex.command").unwrap(), "result.tex", &pdf_file)
} else {
Err(Error::Render("no output pdf file specified in book config"))
}

@ -49,7 +49,7 @@ impl<'a> OdtRenderer<'a> {
pub fn render_book(&mut self) -> Result<String> {
let content = try!(self.render_content());
let mut zipper = try!(Zipper::new(self.book.get_str("temp_dir").unwrap()));
let mut zipper = try!(Zipper::new(&self.book.get_path("temp_dir").unwrap()));
// Write template.odt there
try!(zipper.write("template.odt", odt::ODT, false));
@ -58,7 +58,7 @@ impl<'a> OdtRenderer<'a> {
// Complete it with content.xml
try!(zipper.write("content.xml", &content.as_bytes(), false));
// Zip and copy
if let Ok(file) = self.book.get_str("output.odt") {
if let Ok(ref file) = self.book.get_path("output.odt") {
zipper.generate_odt(file)
} else {
panic!("odt.render_book called while book.output_odt is not set");

@ -19,6 +19,8 @@ use token::Token;
use error::{Result,Error};
use std::fs::File;
use std::path::Path;
use std::convert::AsRef;
use std::io::Read;
use std::collections::HashMap;
@ -62,11 +64,12 @@ impl Parser {
}
/// Parse a file and returns an AST or an error
pub fn parse_file(&mut self, filename: &str) -> Result<Vec<Token>> {
let mut f = try!(File::open(filename).map_err(|_| Error::FileNotFound(String::from(filename))));
pub fn parse_file<P: AsRef<Path>>(&mut self, filename: P) -> Result<Vec<Token>> {
let path: &Path = filename.as_ref();
let mut f = try!(File::open(path).map_err(|_| Error::FileNotFound(format!("{}", path.display()))));
let mut s = String::new();
try!(f.read_to_string(&mut s).map_err(|_| Error::Parser(format!("file {} contains invalid UTF-8, could not parse it", filename))));
try!(f.read_to_string(&mut s).map_err(|_| Error::Parser(format!("file {} contains invalid UTF-8, could not parse it", path.display()))));
self.parse(&s)
}

@ -80,7 +80,7 @@ impl Zipper {
/// Unzip a file and deletes it afterwards
pub fn unzip(&mut self, file: &str) -> Result<()> {
// change to dest directory to unzip file
let dir = try!(env::current_dir().map_err(|_| Error::Zipper("could not get current directory".to_owned())));
let dir = env::current_dir().unwrap();
try!(env::set_current_dir(&self.path).map_err(|_| Error::Zipper("could not change current directory".to_owned())));
let output = Command::new("unzip")
.arg(file)
@ -88,7 +88,7 @@ impl Zipper {
.map_err(|e| Error::Zipper(format!("failed to execute unzip on {}: {}", file, e)));
// change back to original current directory before try! ing anything
try!(env::set_current_dir(dir).map_err(|_| Error::Zipper("could not change back to old directory".to_owned())));
env::set_current_dir(dir).unwrap();
try!(output);
fs::remove_file(self.path.join(file))
@ -96,18 +96,15 @@ impl Zipper {
}
/// run command and copy file name (supposed to result from the command) to current dir
pub fn run_command(&mut self, mut command: Command, file: &str) -> Result<String> {
let dir = try!(env::current_dir().map_err(|_| Error::Zipper("could not get current directory".to_owned())));
try!(env::set_current_dir(&self.path).map_err(|_| Error::Zipper("could not change current directory".to_owned())));
pub fn run_command(&mut self, mut command: Command, in_file: &str, out_file: &str) -> Result<String> {
let res_output = command.args(&self.args)
.current_dir(&self.path)
.output()
.map_err(|e| Error::Zipper(format!("failed to execute process: {}", e)));
try!(env::set_current_dir(dir).map_err(|_| Error::Zipper("could not change back to old directory".to_owned())));
let output = try!(res_output);
try!(fs::copy(self.path.join(file), file).map_err(|_| {
try!(fs::copy(self.path.join(in_file), out_file).map_err(|_| {
println!("{}", &String::from_utf8_lossy(&output.stdout));
Error::Zipper(format!("could not copy file {}", file))
Error::Zipper(format!("could not copy file {} to {}", in_file, out_file))
}));
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
@ -116,34 +113,32 @@ impl Zipper {
pub fn generate_odt(&mut self, odt_file: &str) -> Result<String> {
let mut command = Command::new("zip");
command.arg("-r");
command.arg(odt_file);
command.arg("result.odt");
command.arg(".");
self.run_command(command, odt_file)
self.run_command(command, "result.odt", odt_file)
}
/// generate a pdf file into given file name
pub fn generate_pdf(&mut self, command: &str, tex_file: &str, pdf_file: &str) -> Result<String> {
// first pass
let dir = try!(env::current_dir().map_err(|_| Error::Zipper("could not get current directory".to_owned())));
try!(env::set_current_dir(&self.path).map_err(|_| Error::Zipper("could not change current directory".to_owned())));
let _ = Command::new(command)
.current_dir(&self.path)
.arg(tex_file)
.output();
try!(env::set_current_dir(dir).map_err(|_| Error::Zipper("could not change back to old directory".to_owned())));
// second pass
let mut command = Command::new(command);
command.arg(tex_file);
self.run_command(command, pdf_file)
self.run_command(command, "result.pdf", pdf_file)
}
/// generate an epub into given file name
pub fn generate_epub(&mut self, file: &str) -> Result<String> {
let mut command = Command::new("zip");
command.arg("-X");
command.arg(file);
self.run_command(command, file)
command.arg("result.epub");
self.run_command(command, "result.epub", file)
}
}

15
tests/book.rs Normal file

@ -0,0 +1,15 @@
extern crate crowbook;
use crowbook::Book;
use std::env;
#[test]
fn test_book() {
let book = Book::new_from_file(&format!("{}/{}", env!("CARGO_MANIFEST_DIR"), "tests/test.book"), false).unwrap();
book.render_all().unwrap();
}
#[test]
fn book_example() {
let book = Book::new_from_file(&format!("{}/{}", env!("CARGO_MANIFEST_DIR"), "book_example/config.book"), false).unwrap();
book.render_all().unwrap();
}