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:
parent
d474bce3d6
commit
40b32226cc
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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(" ")
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
Binary file not shown.
Loading…
Reference in New Issue