1
0
Fork 0
mirror of https://github.com/lise-henry/crowbook synced 2024-05-26 07:56:09 +02:00
crowbook/src/lib/latex.rs
2018-06-01 17:50:35 +02:00

618 lines
24 KiB
Rust

// Copyright (C) 2016, 2017, 2018 Élisabeth HENRY.
//
// This file is part of Crowbook.
//
// Crowbook is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation, either version 2.1 of the License, or
// (at your option) any later version.
//
// Caribon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received ba copy of the GNU Lesser General Public License
// along with Crowbook. If not, see <http://www.gnu.org/licenses/>.
use book::{Book, compile_str};
use number::Number;
use error::{Error, Result, Source};
use token::Token;
use token::Data;
use zipper::Zipper;
use resource_handler::ResourceHandler;
use renderer::Renderer;
use parser::Parser;
use book_renderer::BookRenderer;
use syntax::Syntax;
use crowbook_text_processing::escape;
use std::iter::Iterator;
use std::fs;
use std::fs::File;
use std::io;
use std::io::Read;
use std::fmt::Write;
use std::borrow::Cow;
/// LaTeX renderer
pub struct LatexRenderer<'a> {
book: &'a Book,
current_chapter: Number,
handler: ResourceHandler,
source: Source,
escape: bool,
first_letter: bool,
first_paragraph: bool,
is_short: bool,
proofread: bool,
syntax: Option<Syntax>,
hyperref: bool,
enum_level: usize,
}
impl<'a> LatexRenderer<'a> {
/// Creates new LatexRenderer
pub fn new(book: &'a Book) -> LatexRenderer<'a> {
let mut handler = ResourceHandler::new();
handler.set_images_mapping(true);
let syntax = if book.options.get_str("rendering.highlight").unwrap() == "syntect"
&& book.features.codeblock {
Some(Syntax::new(book.options
.get_str("tex.highlight.theme")
.unwrap_or_else(|_| book.options.get_str("rendering.highlight.theme").unwrap())))
} else {
None
};
LatexRenderer {
book: book,
current_chapter: Number::Default,
handler: handler,
source: Source::empty(),
escape: true,
first_letter: false,
first_paragraph: true,
is_short: book.options.get_str("tex.class").unwrap() == "article",
proofread: false,
syntax: syntax,
hyperref: book.options.get_bool("tex.hyperref").unwrap(),
enum_level: 0,
}
}
/// Set proofreading to true
#[doc(hidden)]
pub fn proofread(mut self) -> Self {
self.proofread = true;
self
}
/// Render pdf to a file
pub fn render_pdf(&mut self, to: &mut io::Write) -> Result<String> {
let content = self.render_book()?;
debug!("{}", lformat!("Attempting to run LaTeX on generated file"));
let mut zipper = Zipper::new(&self.book.options.get_path("crowbook.temp_dir")
.unwrap())?;
zipper.write("result.tex", content.as_bytes(), false)?;
// write image files
for (source, dest) in self.handler.images_mapping() {
let mut f = fs::canonicalize(source)
.and_then(|f| File::open(f))
.map_err(|_| {
Error::file_not_found(&self.source, lformat!("image"), source.to_owned())
})?;
let mut content = vec![];
f.read_to_end(&mut content)
.map_err(|e| {
Error::render(&self.source,
lformat!("error while reading image file: {error}", error = e))
})?;
zipper.write(dest, &content, true)?;
}
zipper.generate_pdf(self.book.options.get_str("tex.command").unwrap(),
"result.tex",
to)
}
/// Render latex in a string
pub fn render_book(&mut self) -> Result<String> {
let mut content = String::new();
// set tex numbering and toc display to book's parameters
let numbering = self.book.options.get_i32("rendering.num_depth").unwrap() - 1;
write!(content,
"\\setcounter{{tocdepth}}{{{}}}
\\setcounter{{secnumdepth}}{{{}}}\n",
numbering,
numbering)?;
if self.book.options.get_bool("rendering.inline_toc").unwrap() {
content.push_str("\\tableofcontents\n");
}
for (i, chapter) in self.book.chapters.iter().enumerate() {
self.handler.add_link(chapter.filename.as_str(), format!("chapter-{}", i));
}
for (i, chapter) in self.book.chapters.iter().enumerate() {
let n = chapter.number;
self.current_chapter = n;
let v = &chapter.content;
self.source = Source::new(chapter.filename.as_str());
let mut offset = 0;
if !v.is_empty() && v[0].is_header() {
content.push_str(&self.render_token(&v[0])?);
offset = 1;
}
write!(content,
"\\label{{chapter-{}}}\n",
i)?;
content.push_str(&self.render_vec(&v[offset..])?);
}
self.source = Source::empty();
let tex_lang = String::from(match self.book.options.get_str("lang").unwrap() {
"af" => "afrikaans",
"sq" => "albanian",
"eu" => "basque",
"bg" => "bulgarian",
"ca" => "catalan",
"hr" => "croatian",
"cs" => "czech",
"da" => "danish",
"nl" => "dutch",
"en" => "english",
"eo" => "esperanto",
"et" => "estonian",
"fi" => "finnish",
"fr" => "francais",
"gl" => "galician",
"el" => "greek",
"de" => "ngerman",
"he" => "hebrew",
"hu" => "hungarian",
"it" => "italian",
"is" => "icelandic",
"id" => "indonesian",
"ga" => "irish",
"la" => "latin",
"ms" => "malay",
"nn" => "norsk",
"pl" => "polish",
"pt" => "portuguese",
"ro" => "romanian",
"ru" => "russian",
"gd" => "scottish",
"sr" => "serbian",
"sk" => "slovak",
"sl" => "slovene",
"es" => "spanish",
"sw" => "swedish",
"tr" => "turkish",
"uk" => "ukrainian",
"cy" => "welsh",
_ => {
warn!("{}", lformat!("LaTeX: can't find a tex equivalent for lang '{lang}', \
fallbacking on english",
lang = self.book.options.get_str("lang").unwrap()));
"english"
}
});
let template = compile_str(self.book.get_template("tex.template")?.as_ref(),
&self.book.source,
"tex.template")?;
let mut data = self.book.get_metadata(|s| self.render_vec(&Parser::new().parse_inline(s)?))?
.insert_str("content", content)
.insert_str("class", self.book.options.get_str("tex.class").unwrap())
.insert_bool("tex_title", self.book.options.get_bool("tex.title").unwrap())
.insert_str("papersize", self.book.options.get_str("tex.paper.size").unwrap())
.insert_bool("stdpage", self.book.options.get_bool("tex.stdpage").unwrap())
.insert_bool("use_url", self.book.features.url)
.insert_bool("use_tables", self.book.features.table)
.insert_bool("use_codeblocks", self.book.features.codeblock)
.insert_bool("use_images", self.book.features.image)
.insert_str("tex_lang", tex_lang);
if let Ok(tex_tmpl_add) = self.book.options.get_str("tex.template.add") {
data = data.insert_str("additional_code", tex_tmpl_add);
}
if let Ok(tex_font_size) = self.book.options.get_i32("tex.font.size") {
data = data
.insert_bool("has_tex_size", true)
.insert_str("tex_size", format!("{}", tex_font_size));
}
// If class isn't book, set open_any to true, so margins are symetric.
let mut book = false;
if self.book.options.get_str("tex.class").unwrap() == "book" {
data = data.insert_bool("book", true);
book = true;
}
data = data
.insert_str("margin_left", self.book.options.get_str("tex.margin.left").unwrap_or(if book { "1.5cm" } else { "2cm" }))
.insert_str("margin_right", self.book.options.get_str("tex.margin.right").unwrap_or(if book { "2cm" } else { "2cm" }))
.insert_str("margin_bottom", self.book.options.get_str("tex.margin.bottom").unwrap())
.insert_str("margin_top", self.book.options.get_str("tex.margin.top").unwrap());
if let Ok(chapter_name) = self.book.options.get_str("rendering.chapter") {
data = data.insert_str("chapter_name", chapter_name);
}
if let Ok(part_name) = self.book.options.get_str("rendering.part") {
data = data.insert_str("part_name", part_name);
}
if self.book.options.get_bool("rendering.initials") == Ok(true) {
data = data.insert_bool("initials", true);
}
// Insert xelatex if tex.command is set to xelatex
if self.book.options.get_str("tex.command") == Ok("xelatex") {
data = data.insert_bool("xelatex", true);
}
let data = data.build();
let mut res: Vec<u8> = vec![];
template.render_data(&mut res, &data)?;
match String::from_utf8(res) {
Err(_) => panic!(lformat!("generated LaTeX was not valid utf-8")),
Ok(res) => Ok(res),
}
}
}
impl<'a> Renderer for LatexRenderer<'a> {
fn render_token(&mut self, token: &Token) -> Result<String> {
match *token {
Token::Str(ref text) => {
let content = if self.escape {
self.book.clean(escape::tex(text.as_str()), true)
} else {
Cow::Borrowed(text.as_str())
};
if self.first_letter {
self.first_letter = false;
if self.book.options.get_bool("rendering.initials").unwrap() {
let mut chars = content.chars().peekable();
let initial = chars.next()
.ok_or_else(|| Error::parser(&self.book.source,
lformat!("empty str token, could not find \
initial")))?;
let mut first_word = String::new();
loop {
let c = if let Some(next_char) = chars.peek() {
*next_char
} else {
break;
};
if !c.is_whitespace() {
first_word.push(c);
chars.next();
} else {
break;
}
}
let rest = chars.collect::<String>();
if initial.is_alphanumeric() {
Ok(format!("\\lettrine{{{}}}{{{}}}{}", initial, first_word, rest))
} else {
Ok(format!("{}{}{}", initial, first_word, rest))
}
} else {
Ok(content.into_owned())
}
} else {
Ok(content.into_owned())
}
}
Token::Paragraph(ref vec) => {
if self.first_paragraph {
self.first_paragraph = false;
if !vec.is_empty() && vec[0].is_str() {
// Only use initials if first element is a Token::str
self.first_letter = true;
}
}
Ok(format!("{}\n\n", self.render_vec(vec)?))
}
Token::Header(n, ref vec) => {
let mut content = String::new();
if n == 1 {
self.first_paragraph = true;
if self.current_chapter == Number::Hidden {
if !self.is_short {
return Ok(r#"\chapter*{}"#.to_owned());
} else {
return Ok(r#"\section*{}"#.to_owned());
}
} else if let Number::Specified(n) = self.current_chapter {
content.push_str(r"\setcounter{chapter}{");
write!(content, "{}", n - 1)?;
content.push_str("}\n");
}
}
match n {
1 => {
if !self.is_short {
if self.current_chapter.is_part() {
if self.book.options.get_bool("rendering.part.reset_counter").unwrap() {
content.push_str(r"\setcounter{chapter}{0}");
}
content.push_str(r"\part");
} else {
content.push_str(r"\chapter");
}
} else {
// Chapters or parts aren't handlled for class article
content.push_str(r"\section");
}
}
2 => content.push_str(r"\section"),
3 => content.push_str(r"\subsection"),
4 => content.push_str(r"\subsubsection"),
_ => content.push_str(r"\paragraph"),
}
if !self.current_chapter.is_numbered() {
content.push_str("*");
}
content.push_str(r"{");
content.push_str(&self.render_vec(vec)?);
content.push_str("}\n");
Ok(content)
}
Token::Emphasis(ref vec) => Ok(format!("\\emph{{{}}}", self.render_vec(vec)?)),
Token::Strong(ref vec) => Ok(format!("\\mdstrong{{{}}}", self.render_vec(vec)?)),
Token::Code(ref vec) => Ok(format!("\\mdcode{{{}}}",
insert_breaks(&self.render_vec(vec)?))),
Token::Superscript(ref vec) => Ok(format!("\\textsuperscript{{{}}}", self.render_vec(vec)?)),
Token::Subscript(ref vec) => Ok(format!("\\textsubscript{{{}}}", self.render_vec(vec)?)),
Token::BlockQuote(ref vec) => {
Ok(format!("\\begin{{mdblockquote}}\n{}\n\\end{{mdblockquote}}\n",
self.render_vec(vec)?))
}
Token::CodeBlock(ref language, ref vec) => {
self.escape = false;
let mut res = self.render_vec(vec)?;
// Remove trailing newline
if res.ends_with('\n') {
res.pop();
}
self.escape = true;
res = if let Some(ref syntax) = self.syntax {
syntax.to_tex(&res, language)?
} else {
format!("\\begin{{spverbatim}}
{code}
\\end{{spverbatim}}",
code = res)
};
res = format!("\\begin{{mdcodeblock}}
{}
\\end{{mdcodeblock}}", res);
Ok(res)
}
Token::Rule => Ok(String::from("\\mdrule\n")),
Token::SoftBreak => Ok(String::from(" ")),
Token::HardBreak => Ok(String::from("\\mdhardbreak\n")),
Token::List(ref vec) => {
Ok(format!("\\begin{{itemize}}\n{}\\end{{itemize}}",
self.render_vec(vec)?))
}
Token::OrderedList(n , ref vec) => {
self.enum_level += 1;
let n = n as i32;
let set_counter = if n == 1 {
String::new()
} else {
let counter = match self.enum_level {
1 => "enumi",
2 => "enumii",
3 => "enumiii",
4 => "enumiv",
_ => return Err(Error::render(&self.source,
lformat!("found {n} indented ordered lists, LaTeX only allows for 4",
n = self.enum_level))),
};
format!("\\setcounter{{{counter}}}{{{n}}}\n",
counter = counter,
n = n - 1)
};
let result = format!("\\begin{{enumerate}}
{number}{inner}
\\end{{enumerate}}\n",
number = set_counter,
inner = self.render_vec(vec)?);
self.enum_level -= 1;
Ok(result)
},
Token::Item(ref vec) => Ok(format!("\\item {}\n", self.render_vec(vec)?)),
Token::Link(ref url, _, ref vec) => {
let content = self.render_vec(vec)?;
if self.hyperref && self.handler.contains_link(url) {
Ok(format!("\\hyperref[{}]{{{}}}", escape::tex(self.handler.get_link(url)), content))
} else {
let url = escape::tex(url.as_str());
if &content == &url {
Ok(format!("\\url{{{}}}", content))
} else if self.book.options.get_bool("tex.links_as_footnotes").unwrap() {
Ok(format!("\\href{{{}}}{{{}}}\\protect\\footnote{{\\url{{{}}}}}",
url,
content,
url))
} else {
Ok(format!("\\href{{{}}}{{{}}}", url, content))
}
}
}
Token::StandaloneImage(ref url, _, _) => {
if ResourceHandler::is_local(url) {
let img = self.handler.map_image(&self.source, url.as_str())?;
Ok(format!("\\mdstandaloneimage{{{}}}\n",
img))
} else {
debug!("{}", lformat!("LaTeX ({source}): image '{url}' doesn't seem to be \
local; ignoring it.",
source = self.source,
url = url));
Ok(String::new())
}
}
Token::Image(ref url, _, _) => {
if ResourceHandler::is_local(url) {
Ok(format!("\\mdimage{{{}}}",
self.handler.map_image(&self.source, url.as_str())?))
} else {
debug!("{}", lformat!("LaTeX ({source}): image '{url}' doesn't seem to be \
local; ignoring it.",
source = self.source,
url = url));
Ok(String::new())
}
}
Token::Footnote(ref vec) => {
Ok(format!("\\protect\\footnote{{{}}}", self.render_vec(vec)?))
}
Token::Table(n, ref vec) => {
let mut cols = String::new();
for _ in 0..n {
cols.push_str("|X");
}
cols.push_str("|");
Ok(format!("\\begin{{mdtable}}{{{}}}
\\hline
{}
\\hline
\\end{{mdtable}}\n\n",
cols,
self.render_vec(vec)?))
}
Token::TableRow(ref vec) |
Token::TableHead(ref vec) => {
let mut res: String = vec.iter()
.map(|v| self.render_token(v))
.collect::<Result<Vec<_>>>()?
.join(" & ");
res.push_str("\\\\ \n");
if let Token::TableHead(_) = *token {
res.push_str("\\hline\n");
}
Ok(res)
}
Token::TableCell(ref vec) => self.render_vec(vec),
Token::Annotation(ref annotation, ref vec) => {
let content = self.render_vec(vec)?;
if self.proofread {
match *annotation {
Data::GrammarError(ref s) => {
Ok(format!("\\underline{{{}}}\\protect\\footnote{{{}}}",
content,
escape::tex(s.as_str())))
},
Data::Repetition(ref colour) => {
if !self.escape && colour == "red" {
Ok(format!("\\underline{{{}}}",
content))
} else {
Ok(content)
}
}
_ => unreachable!(),
}
} else {
Ok(content)
}
}
Token::__NonExhaustive => unreachable!(),
}
}
}
pub struct Latex;
pub struct ProofLatex;
pub struct Pdf;
pub struct ProofPdf;
impl BookRenderer for Latex {
fn auto_path(&self, book_name: &str) -> Result<String> {
Ok(format!("{}.tex", book_name))
}
fn render(&self, book: &Book, to: &mut io::Write) -> Result<()> {
let mut latex = LatexRenderer::new(book);
let result = latex.render_book()?;
to.write_all(result.as_bytes())
.map_err(|e| {
Error::render(&book.source,
lformat!("problem when writing LaTeX: {error}", error = e))
})?;
Ok(())
}
}
impl BookRenderer for ProofLatex {
fn auto_path(&self, book_name: &str) -> Result<String> {
Ok(format!("{}.proof.tex", book_name))
}
fn render(&self, book: &Book, to: &mut io::Write) -> Result<()> {
let mut latex = LatexRenderer::new(book).proofread();
let result = latex.render_book()?;
to.write_all(result.as_bytes())
.map_err(|e| {
Error::render(&book.source,
lformat!("problem when writing LaTeX: {error}", error = e))
})?;
Ok(())
}
}
impl BookRenderer for Pdf {
fn auto_path(&self, book_name: &str) -> Result<String> {
Ok(format!("{}.pdf", book_name))
}
fn render(&self, book: &Book, to: &mut io::Write) -> Result<()> {
LatexRenderer::new(book)
.render_pdf(to)?;
Ok(())
}
}
impl BookRenderer for ProofPdf {
fn auto_path(&self, book_name: &str) -> Result<String> {
Ok(format!("{}.proof.pdf", book_name))
}
fn render(&self, book: &Book, to: &mut io::Write) -> Result<()> {
LatexRenderer::new(book)
.proofread()
.render_pdf(to)?;
Ok(())
}
}
/// Insert possible breaks after characters '-', '/', '_', '.', ... to avoid code exploding
/// the page
pub fn insert_breaks(text: &str) -> String {
let mut result = String::with_capacity(text.len());
for c in text.chars() {
match c {
'.' | '_' | ')' | '(' | '-' | '/' | ':' => {
result.push(c);
result.push_str("\\allowbreak{}");
},
_ => result.push(c),
}
}
result
}