1
0
Fork 0
mirror of https://github.com/lise-henry/crowbook synced 2024-05-23 21:36:15 +02:00

experimental support for odt

This commit is contained in:
Elisabeth Henry 2016-02-21 01:49:58 +01:00
parent d474bce3d6
commit 40b32226cc
9 changed files with 246 additions and 29 deletions

View File

@ -19,11 +19,10 @@ extern crate crowbook;
extern crate clap;
use crowbook::{Book};
use std::env;
use clap::{App,Arg};
fn main() {
let mut app = App::new("crowbook")
let app = App::new("crowbook")
.version(env!("CARGO_PKG_VERSION"))
.about("Render a markdown book in Epub, PDF or HTML.")
.after_help("Command line options allow to override options defined in <BOOK> configuration file.
@ -110,11 +109,11 @@ will thus generate baz.pdf in directory foo and not in current directory.")
_ => unreachable!()
} {
if let Err(err) = match format {
"epub" => book.render_epub(file),
"epub" => book.render_epub(),
"tex" => book.render_tex(file),
"html" => book.render_html(file),
"pdf" => book.render_pdf(file),
"odt" => book.render_odt(file),
"odt" => book.render_odt(),
_ => unreachable!()
} {
println!("{}", err);

View File

@ -278,7 +278,7 @@ impl Book {
}
/// Render book to epub according to book options
pub fn render_epub(&self, file: &str) -> Result<()> {
pub fn render_epub(&self) -> Result<()> {
if self.verbose {
println!("Attempting to generate epub...");
}
@ -287,20 +287,21 @@ impl Book {
if self.verbose {
println!("{}", result);
}
println!("Successfully generated epub file: {}", file);
println!("Successfully generated epub file: {}", self.output_epub.as_ref().unwrap());
Ok(())
}
/// Render book to epub according to book options
pub fn render_odt(&self, file: &str) -> Result<()> {
/// Render book to odt according to book options
pub fn render_odt(&self) -> Result<()> {
if self.verbose {
println!("Attempting to generate Odt...");
}
let mut odt = OdtRenderer::new(&self);
let result = try!(odt.render_book());
let mut f = try!(File::create(file).map_err(|_| Error::Render("could not create ODT file")));
try!(f.write_all(&result.as_bytes()).map_err(|_| Error::Render("problem when writing to ODT file")));
println!("Successfully generated ODT file: {}", file);
if self.verbose {
println!("{}", result);
}
println!("Successfully generated odt file: {}", self.output_odt.as_ref().unwrap());
Ok(())
}
@ -334,9 +335,9 @@ impl Book {
pub fn render_all(&self) -> Result<()> {
let mut did_some_stuff = false;
if let Some(ref file) = self.output_epub {
if self.output_epub.is_some() {
did_some_stuff = true;
try!(self.render_epub(file));
try!(self.render_epub());
}
if let Some(ref file) = self.output_html {

View File

@ -61,7 +61,7 @@ impl<'a> EpubRenderer<'a> {
let mut zipper = try!(Zipper::new(&self.book.temp_dir));
// Write mimetype
try!(zipper.write("mimetype", b"application/epub+zip"));
try!(zipper.write("mimetype", b"application/epub+zip", true));
// Write chapters
for (i, &(n, ref v)) in self.book.chapters.iter().enumerate() {
@ -80,30 +80,29 @@ impl<'a> EpubRenderer<'a> {
}
let chapter = try!(self.render_chapter(v));
try!(zipper.write(&filenamer(i), &chapter.as_bytes()));
}
try!(zipper.write(&filenamer(i), &chapter.as_bytes(), true)); }
// Write CSS file
try!(zipper.write("stylesheet.css",
&try!(self.book.get_template("epub_css")).as_bytes()));
&try!(self.book.get_template("epub_css")).as_bytes(), true));
// Write titlepage
try!(zipper.write("title_page.xhtml", &try!(self.render_titlepage()).as_bytes()));
try!(zipper.write("title_page.xhtml", &try!(self.render_titlepage()).as_bytes(), true));
// Write file for ibook (why?)
try!(zipper.write("META-INF/com.apple.ibooks.display-options.xml", IBOOK.as_bytes()));
try!(zipper.write("META-INF/com.apple.ibooks.display-options.xml", IBOOK.as_bytes(), true));
// Write container.xml
try!(zipper.write("META-INF/container.xml", CONTAINER.as_bytes()));
try!(zipper.write("META-INF/container.xml", CONTAINER.as_bytes(), true));
// Write nav.xhtml
try!(zipper.write("nav.xhtml", &try!(self.render_nav()).as_bytes()));
try!(zipper.write("nav.xhtml", &try!(self.render_nav()).as_bytes(), true));
// Write content.opf
try!(zipper.write("content.opf", &try!(self.render_opf()).as_bytes()));
try!(zipper.write("content.opf", &try!(self.render_opf()).as_bytes(), true));
// Write toc.ncx
try!(zipper.write("toc.ncx", &try!(self.render_toc()).as_bytes()));
try!(zipper.write("toc.ncx", &try!(self.render_toc()).as_bytes(), true));
// Write the cover (if needs be)
if let Some(ref cover) = self.book.cover {
@ -111,10 +110,10 @@ impl<'a> EpubRenderer<'a> {
let mut f = try!(File::open(s).map_err(|_| Error::FileNotFound(String::from(s))));
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));
try!(zipper.write(s, &content, true));
// also write cover.xhtml
try!(zipper.write("cover.xhtml", &try!(self.render_cover()).as_bytes()));
try!(zipper.write("cover.xhtml", &try!(self.render_cover()).as_bytes(), true));
}
if let Some(ref epub_file) = self.book.output_epub {

View File

@ -48,7 +48,7 @@ impl<'a> LatexRenderer<'a> {
let tex_file = format!("{}.tex", base_file.to_str().unwrap());
let content = try!(self.render_book());
let mut zipper = try!(Zipper::new(&self.book.temp_dir));
try!(zipper.write(&tex_file, &content.as_bytes()));
try!(zipper.write(&tex_file, &content.as_bytes(), false));
zipper.generate_pdf(&self.book.tex_command, &tex_file, pdf_file)
} else {
Err(Error::Render("no output pdf file specified in book config"))

146
src/lib/odt.rs Normal file
View File

@ -0,0 +1,146 @@
use escape::escape_html;
use token::Token;
use book::{Book, Number};
use error::{Error,Result};
use templates::odt;
use zipper::Zipper;
use mustache;
/// Rendererer for ODT
/// (Experimental)
pub struct OdtRenderer<'a> {
book: &'a Book,
current_numbering: bool,
current_hide: bool,
current_chapter: i32,
automatic_styles: String,
}
impl<'a> OdtRenderer<'a> {
/// Create a new OdtRenderer
pub fn new(book: &'a Book) -> OdtRenderer {
OdtRenderer {
book: book,
current_chapter: 1,
current_numbering: book.numbering,
current_hide: false,
automatic_styles: String::from("
<style:style style:name=\"T1\" style:family=\"text\">
<style:text-properties fo:font-style=\"italic\" style:font-style-asian=\"italic\" style:font-style-complex=\"italic\"/>
</style:style>
<style:style style:name=\"T2\" style:family=\"text\">
<style:text-properties fo:font-weight=\"bold\" style:font-weight-asian=\"bold\" style:font-weight-complex=\"bold\"/>
</style:style>"),
}
}
/// Render book
pub fn render_book(&mut self) -> Result<String> {
let content = try!(self.render_content());
let mut zipper = try!(Zipper::new(&self.book.temp_dir));
// Write template.odt there
try!(zipper.write("template.odt", odt::ODT, false));
// unzip it
try!(zipper.unzip("template.odt"));
// Complete it with content.xml
try!(zipper.write("content.xml", &content.as_bytes(), false));
// Zip and copy
if let Some(ref file) = self.book.output_odt {
zipper.generate_odt(file)
} else {
panic!("odt.render_book called while book.output_odt is not set");
}
}
/// Render content.xml
pub fn render_content(&mut self) -> Result<String> {
let mut content = String::new();
for &(n, ref v) in &self.book.chapters {
self.current_hide = false;
match n {
Number::Unnumbered => self.current_numbering = false,
Number::Default => self.current_numbering = self.book.numbering,
Number::Specified(n) => {
self.current_numbering = self.book.numbering;
self.current_chapter = n;
},
Number::Hidden => {
self.current_numbering = false;
self.current_hide = true;
},
}
for token in v {
content.push_str(&self.parse_token(token));
}
}
let template = mustache::compile_str(odt::CONTENT);
let data = self.book.get_mapbuilder()
.insert_str("content", content)
.insert_str("automatic_styles", &self.automatic_styles)
.build();
let mut res:Vec<u8> = vec!();
template.render_data(&mut res, &data);
match String::from_utf8(res) {
Err(_) => Err(Error::Render("generated content.xml was not utf-8 valid")),
Ok(res) => Ok(res)
}
}
/// Transform a vector of `Token`s to Odt format
pub fn render_vec(&mut self, tokens:&[Token]) -> String {
let mut res = String::new();
for token in tokens {
res.push_str(&self.parse_token(&token));
}
res
}
pub fn parse_token(&mut self, token: &Token) -> String {
match *token {
Token::Str(ref text) => escape_html(&*text),
Token::Paragraph(ref vec) => format!("<text:p text:style-name=\"Text_20_body\">{}</text:p>\n", self.render_vec(vec)),
Token::Header(n, ref vec) => {
if n == 1 && self.current_hide {
return String::new();
}
let s = if n == 1 && self.current_numbering {
let chapter = self.current_chapter;
self.current_chapter += 1;
self.book.get_header(chapter, &self.render_vec(vec)).unwrap()
} else {
self.render_vec(vec)
};
format!("<text:h text:style-name=\"Heading_20_{}\">\n{}</text:h>\n",
n, s)
},
Token::Emphasis(ref vec) => format!("<text:span text:style-name=\"T1\">{}</text:span>", self.render_vec(vec)),
Token::Strong(ref vec) => format!("<text:span text:style-name=\"T2\">{}</text:span>", self.render_vec(vec)),
Token::List(ref vec) => format!("<text:list>\n{}</text:list>\n", self.render_vec(vec)),
Token::OrderedList(_, ref vec) => {
println!("Ordered list not currently implemented for ODT, fallbacking to unordered one");
format!("<text:list>\n{}</text:list>\n", self.render_vec(vec))
},
Token::Item(ref vec) => format!("<text:list-item>\n<text:p>{}</text:p></text:list-item>", self.render_vec(vec)),
Token::Link(ref url, _, ref vec) => format!("<text:a xlink:type=\"simple\" xlink:href=\"{}\">{}</text:a>", url, self.render_vec(vec)),
Token::Code(ref vec) => format!("<text:span text:style-name=\"Preformatted_20_Text\">{}</text:span>", self.render_vec(vec)),
Token::BlockQuote(ref vec) | Token::CodeBlock(_, ref vec) => {
println!("warning: not currently implemented");
format!("<text:p text:style-name=\"Text_20_Body\">{}</text:p>\n", self.render_vec(vec))
},
Token::SoftBreak | Token::HardBreak => String::from(" "),
Token::Rule => format!("<text:p /><text:p>***</text:p><text:p />"),
Token::Image(_,_,_) => {
println!("warning: images not currently implemented for odt");
String::from(" ")
},
}
}
}

View File

@ -43,3 +43,8 @@ pub mod epub3 {
pub static OPF:&'static str = include_str!("../../templates/epub3/content.opf");
pub static TITLE:&'static str = include_str!("../../templates/epub3/titlepage.xhtml");
}
pub mod odt {
pub static CONTENT:&'static str = include_str!("../../templates/odt/content.xml");
pub static ODT:&'static [u8] = include_bytes!("../../templates/odt/template.odt");
}

View File

@ -52,7 +52,7 @@ impl Zipper {
}
/// writes a content to a temporary file
pub fn write(&mut self, file: &str, content: &[u8]) -> Result<()> {
pub fn write(&mut self, file: &str, content: &[u8], add_args: bool) -> Result<()> {
let dest_file = self.path.join(file);
let dest_dir = dest_file.parent().expect("This file should have a parent, it has just been joined to a directory!");
if !fs::metadata(dest_dir).is_ok() { // dir does not exist, create it
@ -65,7 +65,9 @@ impl Zipper {
if let Ok(mut f) = File::create(&dest_file) {
if f.write_all(content).is_ok() {
self.args.push(String::from(file));
if add_args {
self.args.push(String::from(file));
}
Ok(())
} else {
Err(Error::Zipper(format!("could not write to temporary file {}", file)))
@ -75,7 +77,25 @@ impl Zipper {
}
}
/// run command and copy file name to current dir
/// 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())));
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)
.output()
.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())));
try!(output);
fs::remove_file(self.path.join(file))
.map_err(|_| Error::Zipper(format!("failed to remove file {}", file)))
}
/// 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())));
@ -92,6 +112,16 @@ impl Zipper {
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
/// zip all files in zipper's tmp dir to a given file name and return odt file
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(".");
self.run_command(command, 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> {
let mut command = Command::new(command);

37
templates/odt/content.xml Normal file
View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8" ?>
<office:document-content
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"
xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"
xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0"
xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"
xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0"
xmlns:dr3d="urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0"
xmlns:math="http://www.w3.org/1998/Math/MathML"
xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0"
xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0"
xmlns:ooo="http://openoffice.org/2004/office"
xmlns:ooow="http://openoffice.org/2004/writer"
xmlns:oooc="http://openoffice.org/2004/calc"
xmlns:dom="http://www.w3.org/2001/xml-events"
xmlns:xforms="http://www.w3.org/2002/xforms"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
office:version="1.2">
<office:automatic-styles>
{{{automatic_styles}}}
</office:automatic-styles>
<office:body>
<office:text>
<text:p text:style-name="Title">{{{title}}}</text:p>
<text:p text:style-name="Heading_20_2.author">{{{author}}}</text:p>
{{{content}}}
</office:text>
</office:body>
</office:document-content>

BIN
templates/odt/template.odt Normal file

Binary file not shown.