1
0
Fork 0
mirror of https://github.com/lise-henry/crowbook synced 2024-04-19 20:43:50 +02:00

Update dependencies and formatting.

This commit is contained in:
Florian Bottke 2022-01-15 19:21:03 +01:00
parent f84c98438e
commit b8d35ddc32
41 changed files with 4206 additions and 3268 deletions

2349
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,34 +1,44 @@
use crowbook_intl::{Extractor, Localizer};
use crowbook_intl::{Localizer, Extractor};
use std::path::Path;
use std::env;
use std::path::Path;
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=lang/fr.po");
// Extract and localize src/lib
let mut extractor = Extractor::new();
extractor.add_messages_from_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/src/lib")).unwrap();
extractor
.add_messages_from_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/src/lib"))
.unwrap();
// Uncomment to update crowbook.pot
//extractor.write_pot_file(concat!(env!("CARGO_MANIFEST_DIR"), "/lang/lib/crowbook.pot")).unwrap();
let mut localizer = Localizer::new(&extractor);
localizer.add_lang("fr", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/lang/lib/fr.po"))).unwrap();
let dest_path = Path::new(&env::var("OUT_DIR").unwrap())
.join("localize_macros.rs");
localizer
.add_lang(
"fr",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/lang/lib/fr.po")),
)
.unwrap();
let dest_path = Path::new(&env::var("OUT_DIR").unwrap()).join("localize_macros.rs");
localizer.write_macro_file(dest_path).unwrap();
// Extract and localize src/bin
let mut extractor = Extractor::new();
extractor.add_messages_from_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/src/bin")).unwrap();
extractor
.add_messages_from_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/src/bin"))
.unwrap();
// Uncomment to update crowbook.pot
//extractor.write_pot_file(concat!(env!("CARGO_MANIFEST_DIR"), "/lang/bin/crowbook.pot")).unwrap();
let mut localizer = Localizer::new(&extractor);
localizer.add_lang("fr", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/lang/bin/fr.po"))).unwrap();
let dest_path = Path::new(&env::var("OUT_DIR").unwrap())
.join("localize_macros_bin.rs");
localizer
.add_lang(
"fr",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/lang/bin/fr.po")),
)
.unwrap();
let dest_path = Path::new(&env::var("OUT_DIR").unwrap()).join("localize_macros_bin.rs");
localizer.write_macro_file(dest_path).unwrap();
}

View File

@ -15,15 +15,14 @@
// You should have received a copy of the GNU Lesser General Public License
// along with Crowbook. If not, see <http://www.gnu.org/licenses/>.
use crowbook::Book;
use clap::{App, Arg, ArgMatches, AppSettings};
use clap::{App, AppSettings, Arg, ArgMatches};
use console::style;
use crowbook::Book;
use std::env;
use std::fs;
use std::io::{self, Write};
use std::process::exit;
use std::fs;
use std::env;
static BIRD: &str = "🐦 ";
static ERROR: &str = "💣 ";
@ -34,9 +33,7 @@ pub fn print_warning(msg: &str, emoji: bool) {
if emoji {
eprint!("{}", style(WARNING).yellow());
}
eprintln!("{} {}",
style(lformat!("WARNING")).bold().yellow(),
msg);
eprintln!("{} {}", style(lformat!("WARNING")).bold().yellow(), msg);
}
/// Prints an error
@ -44,9 +41,7 @@ pub fn print_error(s: &str, emoji: bool) {
if emoji {
eprint!("{}", style(ERROR).red());
}
eprintln!("{} {}",
style(lformat!("ERROR")).bold().red(),
s);
eprintln!("{} {}", style(lformat!("ERROR")).bold().red(), s);
}
/// Prints an error on stderr and exit the program
@ -55,7 +50,6 @@ pub fn print_error_and_exit(s: &str, emoji: bool) -> ! {
exit(0);
}
/// Display version number
pub fn display_header(emoji: bool) {
if emoji {
@ -81,15 +75,19 @@ pub fn get_lang() -> Option<String> {
None
}
/// Gets the book options in a (key, value) list, or print an error
pub fn get_book_options<'a>(matches: &'a ArgMatches) -> Vec<(&'a str, &'a str)> {
let mut output = vec![];
if let Some(iter) = matches.values_of("set") {
let v: Vec<_> = iter.collect();
if v.len() % 2 != 0 {
print_error_and_exit(&lformat!("An odd number of arguments was passed to --set, but it takes \
a list of key value pairs."), false);
print_error_and_exit(
&lformat!(
"An odd number of arguments was passed to --set, but it takes \
a list of key value pairs."
),
false,
);
}
for i in 0..v.len() / 2 {
@ -104,7 +102,6 @@ pub fn get_book_options<'a>(matches: &'a ArgMatches) -> Vec<(&'a str, &'a str)>
output
}
/// Sets the book options according to command line arguments
/// Also print these options to a string, so it can be used at
/// the creation of a book to check that parameters are OK and
@ -128,7 +125,10 @@ pub fn set_book_options(book: &mut Book, matches: &ArgMatches) -> String {
pub fn create_book(matches: &ArgMatches) -> ! {
let mut f: Box<dyn Write> = if let Some(book) = matches.value_of("BOOK") {
if fs::metadata(book).is_ok() {
print_error_and_exit(&lformat!("Could not create file {}: it already exists!", book), false);
print_error_and_exit(
&lformat!("Could not create file {}: it already exists!", book),
false,
);
}
Box::new(fs::File::create(book).unwrap())
} else {
@ -141,7 +141,9 @@ pub fn create_book(matches: &ArgMatches) -> ! {
let s = set_book_options(&mut book, matches);
f.write_all(s.as_bytes()).unwrap();
} else {
f.write_all(lformat!("author: Your name
f.write_all(
lformat!(
"author: Your name
title: Your title
lang: en
@ -156,16 +158,22 @@ lang: en
# output: [pdf, epub, html]
# Uncomment and fill to set cover image (for EPUB)
# cover: some_cover.png\n").as_bytes())
.unwrap();
# cover: some_cover.png\n"
)
.as_bytes(),
)
.unwrap();
}
f.write_all(lformat!("\n## List of chapters\n").as_bytes()).unwrap();
f.write_all(lformat!("\n## List of chapters\n").as_bytes())
.unwrap();
for file in values {
f.write_all(format!("+ {}\n", file).as_bytes()).unwrap();
}
if let Some(s) = matches.value_of("BOOK") {
println!("{}",
lformat!("Created {}, now you'll have to complete it!", s));
println!(
"{}",
lformat!("Created {}, now you'll have to complete it!", s)
);
}
exit(0);
} else {
@ -213,7 +221,6 @@ ARGS:
");
}
let app = App::new("crowbook")
.version(env!("CARGO_PKG_VERSION"))
.author("Élisabeth Henry <liz.henry@ouvaton.org>")
@ -225,42 +232,51 @@ ARGS:
.arg(Arg::from_usage("-n, --no-fancy").help(NO_FANCY.as_str()))
.arg(Arg::from_usage("-v, --verbose").help(VERBOSE.as_str()))
.arg(Arg::from_usage("-a, --autograph").help(AUTOGRAPH.as_str()))
.arg(Arg::from_usage("-q, --quiet")
.help(QUIET.as_str())
.conflicts_with("verbose"))
.arg(
Arg::from_usage("-q, --quiet")
.help(QUIET.as_str())
.conflicts_with("verbose"),
)
.arg(Arg::from_usage("-h, --help").help(HELP.as_str()))
.arg(Arg::from_usage("-V, --version").help(VERSION.as_str()))
.arg(Arg::from_usage("-p, --proofread").help(PROOFREAD.as_str()))
.arg(Arg::from_usage("-c, --create [FILES]...").help(CREATE.as_str()))
.arg(Arg::from_usage("-o, --output [FILE]")
.help(OUTPUT.as_str())
.requires("to"))
.arg(Arg::from_usage("-t, --to [FORMAT]")
.help(TO.as_str())
.possible_values(&["epub",
"pdf",
"html",
"tex",
"odt",
"html.dir",
"proofread.html",
"proofread.html.dir",
"proofread.pdf",
"proofread.tex"]))
.arg(Arg::from_usage("--set [KEY_VALUES]")
.help(SET.as_str())
.min_values(2))
.arg(
Arg::from_usage("-o, --output [FILE]")
.help(OUTPUT.as_str())
.requires("to"),
)
.arg(
Arg::from_usage("-t, --to [FORMAT]")
.help(TO.as_str())
.possible_values(&[
"epub",
"pdf",
"html",
"tex",
"odt",
"html.dir",
"proofread.html",
"proofread.html.dir",
"proofread.pdf",
"proofread.tex",
]),
)
.arg(
Arg::from_usage("--set [KEY_VALUES]")
.help(SET.as_str())
.min_values(2),
)
.arg(Arg::from_usage("-l --list-options").help(LIST_OPTIONS.as_str()))
.arg(Arg::from_usage("--list-options-md")
.help(LIST_OPTIONS_MD.as_str())
.hidden(true))
.arg(Arg::from_usage("-L --lang [LANG]")
.help(LANG.as_str()))
.arg(
Arg::from_usage("--list-options-md")
.help(LIST_OPTIONS_MD.as_str())
.hidden(true),
)
.arg(Arg::from_usage("-L --lang [LANG]").help(LANG.as_str()))
.arg(Arg::from_usage("--print-template [TEMPLATE]").help(PRINT_TEMPLATE.as_str()))
.arg(Arg::from_usage("--stats -S").help(STATS.as_str()))
.arg(Arg::with_name("BOOK")
.index(1)
.help(BOOK.as_str()))
.arg(Arg::with_name("BOOK").index(1).help(BOOK.as_str()))
.template(TEMPLATE.as_str());
// Write help and version now since it `app` is moved when `get_matches` is run
@ -277,11 +293,15 @@ ARGS:
(matches, help, version)
}
/// Pre-check the matches to see if there isn't illegal options not detected by clap
fn pre_check(matches: &ArgMatches) {
if matches.is_present("files") && !matches.is_present("create") {
print_error_and_exit(&lformat!("A list of additional files is only valid with the --create \
option."), false);
print_error_and_exit(
&lformat!(
"A list of additional files is only valid with the --create \
option."
),
false,
);
}
}

View File

@ -2,25 +2,22 @@ extern crate crowbook;
extern crate crowbook_intl_runtime;
extern crate yaml_rust;
#[cfg(feature= "binary")]
extern crate simplelog;
#[cfg(feature = "binary")]
extern crate clap;
#[cfg(feature = "binary")]
extern crate tempdir;
#[cfg(feature= "binary")]
extern crate console;
#[cfg(feature = "binary")]
extern crate simplelog;
#[cfg(feature = "binary")]
extern crate tempdir;
#[macro_use]
mod localize_macros;
#[cfg(feature = "binary")]
mod real_main;
#[cfg(feature = "binary")]
mod helpers;
#[cfg(feature = "binary")]
mod real_main;
#[cfg(feature = "binary")]
#[cfg(feature = "binary")]
#[macro_use]
extern crate lazy_static;

View File

@ -17,19 +17,19 @@
use crate::helpers::*;
use yaml_rust::Yaml;
use console;
use crowbook::{Result, Book, BookOptions};
use crowbook_intl_runtime::set_lang;
use crowbook::Stats;
use tempdir::TempDir;
use clap::ArgMatches;
use std::process::exit;
use std::io;
use std::io::Read;
use console;
use crowbook::Stats;
use crowbook::{Book, BookOptions, Result};
use crowbook_intl_runtime::set_lang;
use simplelog::{ConfigBuilder, LevelFilter, SimpleLogger, TermLogger, WriteLogger};
use std::env;
use std::fs::File;
use simplelog::{ConfigBuilder, TermLogger, LevelFilter, SimpleLogger, WriteLogger};
use std::io;
use std::io::Read;
use std::process::exit;
use tempdir::TempDir;
use yaml_rust::Yaml;
/// Render a book to specific format
fn render_format(book: &mut Book, emoji: bool, matches: &ArgMatches, format: &str) {
@ -49,13 +49,10 @@ fn render_format(book: &mut Book, emoji: bool, matches: &ArgMatches, format: &st
let res = book.options.get_path(&key);
let result = match(file, res, stdout) {
(Some(file), _, _) |
(None, Ok(file), false) => book.render_format_to_file(format, file),
let result = match (file, res, stdout) {
(Some(file), _, _) | (None, Ok(file), false) => book.render_format_to_file(format, file),
(None, Err(_), _) |
(None, _, true)
=> book.render_format_to(format, &mut io::stdout()),
(None, Err(_), _) | (None, _, true) => book.render_format_to(format, &mut io::stdout()),
};
match result {
@ -65,15 +62,10 @@ fn render_format(book: &mut Book, emoji: bool, matches: &ArgMatches, format: &st
}
pub fn try_main() -> Result<()> {
let lang = get_lang()
.or_else(|| {
match env::var("LANG") {
Ok(val) => {
Some(val)
},
Err(_) => None,
}
});
let lang = get_lang().or_else(|| match env::var("LANG") {
Ok(val) => Some(val),
Err(_) => None,
});
if let Some(val) = lang {
if val.starts_with("fr") {
set_lang("fr");
@ -120,9 +112,10 @@ pub fn try_main() -> Result<()> {
println!("{}", s);
exit(0);
}
Err(_) => print_error_and_exit(&lformat!("{} is not a valid template name.",
template),
emoji),
Err(_) => print_error_and_exit(
&lformat!("{} is not a valid template name.", template),
emoji,
),
}
}
@ -141,14 +134,16 @@ pub fn try_main() -> Result<()> {
}
if !matches.is_present("BOOK") {
print_error_and_exit(&lformat!("You must pass the file of a book configuration \
print_error_and_exit(
&lformat!(
"You must pass the file of a book configuration \
file.\n\n{}\n\nFor more information try --help.",
matches.usage()),
emoji);
matches.usage()
),
emoji,
);
}
// ok to unwrap since clap checks it's there
let s = matches.value_of("BOOK").unwrap();
@ -172,14 +167,19 @@ pub fn try_main() -> Result<()> {
};
let log_config = builder.build();
let error_dir = TempDir::new("crowbook").unwrap();
let error_path = "error.log";
if fancy_ui {
let errors = File::create(error_dir.path().join(error_path)).unwrap();
let _ = WriteLogger::init(verbosity, log_config, errors);
} else {
if TermLogger::init(verbosity, log_config.clone(), simplelog::TerminalMode::Stderr).is_err() {
if TermLogger::init(
verbosity,
log_config.clone(),
simplelog::TerminalMode::Stderr,
)
.is_err()
{
// If it failed, not much we can do, we just won't display log
let _ = SimpleLogger::init(verbosity, log_config);
}
@ -192,8 +192,13 @@ pub fn try_main() -> Result<()> {
let mut autograph = String::new();
match io::stdin().read_to_string(&mut autograph) {
Ok(_) => {
book.options.set_yaml(Yaml::String("autograph".to_string()), Yaml::String(autograph)).unwrap();
},
book.options
.set_yaml(
Yaml::String("autograph".to_string()),
Yaml::String(autograph),
)
.unwrap();
}
Err(_) => print_error(&lformat!("could not read autograph from stdin"), emoji),
}
}
@ -214,10 +219,11 @@ pub fn try_main() -> Result<()> {
book.load_file(s)
} else {
book.read_config(io::stdin())
}.map(|_| ());
}
.map(|_| ());
match res {
Ok(..) => {},
Ok(..) => {}
Err(err) => {
book.set_error(&format!("{}", err));
return Err(err);
@ -234,7 +240,7 @@ pub fn try_main() -> Result<()> {
}
if let Some(format) = matches.value_of("to") {
render_format(&mut book, emoji,& matches, format);
render_format(&mut book, emoji, &matches, format);
} else {
book.render_all();
}
@ -244,10 +250,12 @@ pub fn try_main() -> Result<()> {
let mut file = File::open(error_dir.path().join(error_path)).unwrap();
file.read_to_string(&mut errors).unwrap();
if !errors.is_empty() {
print_warning(&lformat!("Crowbook exited successfully, but the following errors occurred:"),
emoji);
print_warning(
&lformat!("Crowbook exited successfully, but the following errors occurred:"),
emoji,
);
// Non-efficient dedup algorithm but we need to keep the order
let mut lines: Vec<String> = vec!();
let mut lines: Vec<String> = vec![];
for line in errors.lines().into_iter() {
let mut contains = false;
for l in &lines {
@ -272,7 +280,6 @@ pub fn try_main() -> Result<()> {
}
}
Ok(())
}

View File

@ -15,34 +15,34 @@
// You should have received a copy of the GNU Lesser General Public License
// along with Crowbook. If not, see <http://www.gnu.org/licenses/>.
use crate::error::{Error, Result, Source};
use crate::cleaner::{Cleaner, CleanerParams, French, Off, Default};
use crate::bookoptions::BookOptions;
use crate::parser::Parser;
use crate::parser::Features;
use crate::epub::{Epub};
use crate::html_single::{HtmlSingle, ProofHtmlSingle};
use crate::html_dir::{HtmlDir, ProofHtmlDir};
use crate::html_if::{HtmlIf};
use crate::latex::{Latex, ProofLatex, Pdf, ProofPdf};
use crate::odt::{Odt};
use crate::templates::{epub, html, epub3, latex, html_dir, highlight, html_single, html_if};
use crate::number::Number;
use crate::resource_handler::ResourceHandler;
use crate::lang;
use crate::misc;
use crate::book_renderer::BookRenderer;
use crate::chapter::Chapter;
use crate::token::Token;
use crate::text_view::view_as_text;
use crate::book_bars::Bars;
use crate::book_renderer::BookRenderer;
use crate::bookoptions::BookOptions;
use crate::chapter::Chapter;
use crate::cleaner::{Cleaner, CleanerParams, Default, French, Off};
use crate::epub::Epub;
use crate::error::{Error, Result, Source};
use crate::html_dir::{HtmlDir, ProofHtmlDir};
use crate::html_if::HtmlIf;
use crate::html_single::{HtmlSingle, ProofHtmlSingle};
use crate::lang;
use crate::latex::{Latex, Pdf, ProofLatex, ProofPdf};
use crate::misc;
use crate::number::Number;
use crate::odt::Odt;
use crate::parser::Features;
use crate::parser::Parser;
use crate::resource_handler::ResourceHandler;
use crate::templates::{epub, epub3, highlight, html, html_dir, html_if, html_single, latex};
use crate::text_view::view_as_text;
use crate::token::Token;
#[cfg(feature = "proofread")]
use crate::repetition_check::RepetitionDetector;
use crate::grammalecte::GrammalecteChecker;
#[cfg(feature = "proofread")]
use crate::grammar_check::GrammarChecker;
#[cfg(feature = "proofread")]
use crate::grammalecte::GrammalecteChecker;
use crate::repetition_check::RepetitionDetector;
// Dummy grammarchecker thas does nothing to let the compiler compile
#[cfg(not(feature = "proofread"))]
struct GrammarChecker {}
@ -71,21 +71,20 @@ impl RepetitionDetector {
}
}
use std::fs::File;
use std::io::{Write, Read};
use std::path::{Path, PathBuf};
use std::borrow::Cow;
use std::iter::IntoIterator;
use std::collections::HashMap;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt;
use std::fs::File;
use std::io::{Read, Write};
use std::iter::IntoIterator;
use std::path::{Path, PathBuf};
use rayon::prelude::*;
use mustache;
use mustache::{MapBuilder, Template};
use yaml_rust::{YamlLoader, Yaml};
use numerals::roman::Roman;
use rayon::prelude::*;
use yaml_rust::{Yaml, YamlLoader};
/// Type of header (part or chapter)
#[derive(Copy, Clone, Debug)]
@ -96,7 +95,6 @@ pub enum Header {
Part,
}
/// Header data (for chapter or part)
#[derive(Debug, Clone)]
pub struct HeaderData {
@ -126,7 +124,6 @@ pub enum CrowbarState {
Error,
}
impl fmt::Display for HeaderData {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.text)
@ -209,17 +206,45 @@ impl Book {
features: Features::new(),
bars: Bars::new(),
};
book.add_format("html", lformat!("HTML (standalone page)"), Box::new(HtmlSingle{}))
.add_format("proofread.html", lformat!("HTML (standalone page/proofreading)"), Box::new(ProofHtmlSingle{}))
.add_format("html.dir", lformat!("HTML (multiple pages)"), Box::new(HtmlDir{}))
.add_format("proofread.html.dir", lformat!("HTML (multiple pages/proofreading)"), Box::new(ProofHtmlDir{}))
.add_format("tex", lformat!("LaTeX"), Box::new(Latex{}))
.add_format("proofread.tex", lformat!("LaTeX (proofreading)"), Box::new(ProofLatex{}))
.add_format("pdf", lformat!("PDF"), Box::new(Pdf{}))
.add_format("proofread.pdf", lformat!("PDF (proofreading)"), Box::new(ProofPdf{}))
.add_format("epub", lformat!("EPUB"), Box::new(Epub{}))
.add_format("odt", lformat!("ODT"), Box::new(Odt{}))
.add_format("html.if", lformat!("HTML (interactive fiction)"), Box::new(HtmlIf{}));
book.add_format(
"html",
lformat!("HTML (standalone page)"),
Box::new(HtmlSingle {}),
)
.add_format(
"proofread.html",
lformat!("HTML (standalone page/proofreading)"),
Box::new(ProofHtmlSingle {}),
)
.add_format(
"html.dir",
lformat!("HTML (multiple pages)"),
Box::new(HtmlDir {}),
)
.add_format(
"proofread.html.dir",
lformat!("HTML (multiple pages/proofreading)"),
Box::new(ProofHtmlDir {}),
)
.add_format("tex", lformat!("LaTeX"), Box::new(Latex {}))
.add_format(
"proofread.tex",
lformat!("LaTeX (proofreading)"),
Box::new(ProofLatex {}),
)
.add_format("pdf", lformat!("PDF"), Box::new(Pdf {}))
.add_format(
"proofread.pdf",
lformat!("PDF (proofreading)"),
Box::new(ProofPdf {}),
)
.add_format("epub", lformat!("EPUB"), Box::new(Epub {}))
.add_format("odt", lformat!("ODT"), Box::new(Odt {}))
.add_format(
"html.if",
lformat!("HTML (interactive fiction)"),
Box::new(HtmlIf {}),
);
book
}
@ -228,8 +253,6 @@ impl Book {
self.bar_finish(Crowbar::Main, CrowbarState::Error, msg)
}
/// Adds a progress bar where where info should be written.
///
/// See [indicatif doc](https://docs.rs/indicatif) for more information.
@ -259,10 +282,12 @@ impl Book {
/// "Some dummy implementation",
/// Box::new(Dummy{}));
/// ```
pub fn add_format<S: Into<String>>(&mut self,
format: &'static str,
description: S,
renderer: Box<dyn BookRenderer>) -> &mut Self {
pub fn add_format<S: Into<String>>(
&mut self,
format: &'static str,
description: S,
renderer: Box<dyn BookRenderer>,
) -> &mut Self {
self.formats.insert(format, (description.into(), renderer));
self
}
@ -282,15 +307,21 @@ impl Book {
/// assert_eq!(book.options.get_str("title").unwrap(), "Bar");
/// ```
pub fn set_options<'a, I>(&mut self, options: I) -> &mut Book
where I: IntoIterator<Item = &'a (&'a str, &'a str)>
where
I: IntoIterator<Item = &'a (&'a str, &'a str)>,
{
// set options
for &(key, value) in options {
if let Err(err) = self.options.set(key, value) {
error!("{}", lformat!("Error initializing book: could not set {key} to {value}: {error}",
key = key,
value = value,
error = err));
error!(
"{}",
lformat!(
"Error initializing book: could not set {key} to {value}: {error}",
key = key,
value = value,
error = err
)
);
}
}
// set cleaner according to lang and autoclean settings
@ -317,10 +348,9 @@ impl Book {
self.source = Source::new(filename.as_str());
self.options.source = Source::new(filename.as_str());
let f = File::open(path.as_ref())
.map_err(|_| {
Error::file_not_found(Source::empty(), lformat!("book"), filename.clone())
})?;
let f = File::open(path.as_ref()).map_err(|_| {
Error::file_not_found(Source::empty(), lformat!("book"), filename.clone())
})?;
// Set book path to book's directory
if let Some(parent) = path.as_ref().parent() {
self.root = parent.to_owned();
@ -332,11 +362,15 @@ impl Book {
Ok(book) => Ok(book),
Err(err) => {
if err.is_config_parser() && path.as_ref().ends_with(".md") {
let err = Error::default(Source::empty(),
lformat!("could not parse {file} as a book \
let err = Error::default(
Source::empty(),
lformat!(
"could not parse {file} as a book \
file.\nMaybe you meant to run crowbook \
with the --single argument?",
file = misc::normalize(path)));
file = misc::normalize(path)
),
);
Err(err)
} else {
Err(err)
@ -360,7 +394,7 @@ impl Book {
/// let mut book = Book::new();
/// book.load_markdown_file("foo.md"); // not unwraping since foo.md doesn't exist
/// ```
pub fn load_markdown_file<P:AsRef<Path>>(&mut self, path: P) -> Result<&mut Self> {
pub fn load_markdown_file<P: AsRef<Path>>(&mut self, path: P) -> Result<&mut Self> {
let filename = format!("{}", path.as_ref().display());
self.source = Source::new(filename.as_str());
@ -374,12 +408,7 @@ impl Book {
// Add the file as chapter with hidden title
// hideous line, but basically transforms foo/bar/baz.md to baz.md
let relative_path = Path::new(path
.as_ref()
.components()
.last()
.unwrap()
.as_os_str());
let relative_path = Path::new(path.as_ref().components().last().unwrap().as_os_str());
// Update grammar checker according to options
self.add_chapter(Number::Hidden, &relative_path.to_string_lossy(), false)?;
@ -424,9 +453,10 @@ impl Book {
self.options.source = self.source.clone();
match YamlLoader::load_from_str(yaml) {
Err(err) => {
return Err(Error::config_parser(&self.source,
lformat!("YAML block was not valid YAML: {error}",
error = err)))
return Err(Error::config_parser(
&self.source,
lformat!("YAML block was not valid YAML: {error}", error = err),
))
}
Ok(mut docs) => {
if docs.len() == 1 && docs[0].as_hash().is_some() {
@ -440,9 +470,13 @@ impl Book {
unreachable!();
}
} else {
return Err(Error::config_parser(&self.source,
lformat!("YAML part of the book is not a \
valid hashmap")));
return Err(Error::config_parser(
&self.source,
lformat!(
"YAML part of the book is not a \
valid hashmap"
),
));
}
}
}
@ -482,11 +516,18 @@ impl Book {
fn get_filename<'a>(source: &Source, s: &'a str) -> Result<&'a str> {
let words: Vec<&str> = (&s[1..]).split_whitespace().collect();
if words.len() > 1 {
return Err(Error::config_parser(source,
lformat!("chapter filenames must not contain \
whitespace")));
return Err(Error::config_parser(
source,
lformat!(
"chapter filenames must not contain \
whitespace"
),
));
} else if words.len() < 1 {
return Err(Error::config_parser(source, lformat!("no chapter name specified")));
return Err(Error::config_parser(
source,
lformat!("no chapter name specified"),
));
}
Ok(words[0])
}
@ -494,10 +535,12 @@ impl Book {
self.bar_set_message(Crowbar::Main, &lformat!("setting options"));
let mut s = String::new();
source.read_to_string(&mut s)
.map_err(|err| Error::config_parser(Source::empty(),
lformat!("could not read source: {error}",
error = err)))?;
source.read_to_string(&mut s).map_err(|err| {
Error::config_parser(
Source::empty(),
lformat!("could not read source: {error}", error = err),
)
})?;
// Parse the YAML block, that is, until first chapter
let mut yaml = String::new();
@ -510,7 +553,7 @@ impl Book {
loop {
if let Some(next_line) = lines.peek() {
if next_line.starts_with(|c| match c {
'-' | '+' | '!' | '@' => true,
'-' | '+' | '!' | '@' => true,
_ => c.is_digit(10),
}) {
break;
@ -588,7 +631,8 @@ impl Book {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
} if line.starts_with("--") {
}
if line.starts_with("--") {
// Subchapter
let mut level = 0;
for b in line.bytes() {
@ -616,29 +660,35 @@ impl Book {
self.add_chapter(Number::Hidden, file, false)?;
} else if line.starts_with(|c: char| c.is_digit(10)) {
// chapter with specific number
let parts: Vec<_> = line.splitn(2, |c: char| c == '.' || c == ':' || c == '+')
let parts: Vec<_> = line
.splitn(2, |c: char| c == '.' || c == ':' || c == '+')
.collect();
if parts.len() != 2 {
return Err(Error::config_parser(&self.source,
lformat!("ill-formatted line specifying \
chapter number")));
return Err(Error::config_parser(
&self.source,
lformat!(
"ill-formatted line specifying \
chapter number"
),
));
}
let file = get_filename(&self.source, parts[1])?;
let number = parts[0].parse::<i32>()
.map_err(|err| {
Error::config_parser(&self.source,
lformat!("error parsing chapter number: {error}",
error = err))})?;
let number = parts[0].parse::<i32>().map_err(|err| {
Error::config_parser(
&self.source,
lformat!("error parsing chapter number: {error}", error = err),
)
})?;
self.add_chapter(Number::Specified(number), file, true)?;
} else if line.starts_with('@') {
/* Part */
let subline = &line[1..];
if subline.starts_with(|c: char| c.is_whitespace()) {
let subline = subline.trim();
let ast = Parser::from(&self)
.parse_inline(subline)?;
let ast = vec!(Token::Header(1, ast));
self.chapters.push(Chapter::new(Number::DefaultPart, String::new(), ast));
let ast = Parser::from(&self).parse_inline(subline)?;
let ast = vec![Token::Header(1, ast)];
self.chapters
.push(Chapter::new(Number::DefaultPart, String::new(), ast));
} else if subline.starts_with('-') {
/* Unnumbered part */
let file = get_filename(&self.source, subline)?;
@ -649,28 +699,40 @@ impl Book {
self.add_chapter(Number::DefaultPart, file, true)?;
} else if subline.starts_with(|c: char| c.is_digit(10)) {
/* Specified part*/
let parts: Vec<_> = subline.splitn(2, |c: char| c == '.' || c == ':' || c == '+')
let parts: Vec<_> = subline
.splitn(2, |c: char| c == '.' || c == ':' || c == '+')
.collect();
if parts.len() != 2 {
return Err(Error::config_parser(&self.source,
lformat!("ill-formatted line specifying \
part number")));
return Err(Error::config_parser(
&self.source,
lformat!(
"ill-formatted line specifying \
part number"
),
));
}
let file = get_filename(&self.source, parts[1])?;
let number = parts[0].parse::<i32>()
.map_err(|err| {
Error::config_parser(&self.source,
lformat!("error parsing part number: {error}",
error = err))})?;
let number = parts[0].parse::<i32>().map_err(|err| {
Error::config_parser(
&self.source,
lformat!("error parsing part number: {error}", error = err),
)
})?;
self.add_chapter(Number::SpecifiedPart(number), file, true)?;
} else {
return Err(Error::config_parser(&self.source,
lformat!("found invalid part definition in the chapter list")));
return Err(Error::config_parser(
&self.source,
lformat!("found invalid part definition in the chapter list"),
));
}
} else {
return Err(Error::config_parser(&self.source,
lformat!("found invalid chapter definition in \
the chapter list")));
return Err(Error::config_parser(
&self.source,
lformat!(
"found invalid chapter definition in \
the chapter list"
),
));
}
}
@ -683,10 +745,10 @@ impl Book {
/// Determine whether proofreading is activated or not
fn is_proofread(&self) -> bool {
self.options.get_bool("proofread").unwrap() &&
(self.options.get("output.proofread.html").is_ok() ||
self.options.get("output.proofread.html.dir").is_ok() ||
self.options.get("output.proofread.pdf").is_ok())
self.options.get_bool("proofread").unwrap()
&& (self.options.get("output.proofread.html").is_ok()
|| self.options.get("output.proofread.html.dir").is_ok()
|| self.options.get("output.proofread.pdf").is_ok())
}
/// Initialize the grammar checker and repetetion detector if they needs to be
@ -700,7 +762,10 @@ impl Book {
match checker {
Ok(checker) => self.checker = Some(checker),
Err(e) => {
error!("{}", lformat!("{error}. Proceeding without using languagetool.", error = e))
error!(
"{}",
lformat!("{error}. Proceeding without using languagetool.", error = e)
)
}
}
}
@ -711,7 +776,10 @@ impl Book {
match checker {
Ok(checker) => self.grammalecte = Some(checker),
Err(e) => {
error!("{}", lformat!("{error}. Proceeding without using grammalecte.", error = e))
error!(
"{}",
lformat!("{error}. Proceeding without using grammalecte.", error = e)
)
}
}
}
@ -762,7 +830,8 @@ impl Book {
/// .render_all(); // renders foo.tex in /tmp
/// ```
pub fn render_all(&mut self) -> () {
let mut keys: Vec<_> = self.formats
let mut keys: Vec<_> = self
.formats
.keys()
.filter(|fmt| {
if !self.is_proofread() {
@ -789,11 +858,9 @@ impl Book {
self.add_spinner_to_multibar(key);
}
keys.par_iter()
.enumerate()
.for_each(|(i, fmt)| {
self.render_format_with_bar(fmt, i);
});
keys.par_iter().enumerate().for_each(|(i, fmt)| {
self.render_format_with_bar(fmt, i);
});
self.bar_finish(Crowbar::Main, CrowbarState::Success, &lformat!("Finished"));
@ -803,7 +870,7 @@ impl Book {
// }
}
/// Renders the book to the given format and reports to progress bar if set
/// Renders the book to the given format and reports to progress bar if set
pub fn render_format_with_bar(&self, format: &str, bar: usize) -> () {
let mut key = String::from("output.");
key.push_str(format);
@ -811,64 +878,83 @@ impl Book {
self.bar_set_message(Crowbar::Spinner(bar), &lformat!("rendering..."));
let result = self.render_format_to_file_with_bar(format, path, bar);
if let Err(err) = result {
self.bar_finish(Crowbar::Spinner(bar),
CrowbarState::Error,
&format!("{}", err));
error!("{}", lformat!("Error rendering {name}: {error}", name = format, error = err));
self.bar_finish(
Crowbar::Spinner(bar),
CrowbarState::Error,
&format!("{}", err),
);
error!(
"{}",
lformat!(
"Error rendering {name}: {error}",
name = format,
error = err
)
);
}
}
}
pub fn render_format_to_file_with_bar<P:Into<PathBuf>>(&self,
format: &str,
path: P,
bar: usize) -> Result<()> {
debug!("{}", lformat!("Attempting to generate {format}...",
format = format));
pub fn render_format_to_file_with_bar<P: Into<PathBuf>>(
&self,
format: &str,
path: P,
bar: usize,
) -> Result<()> {
debug!(
"{}",
lformat!("Attempting to generate {format}...", format = format)
);
let path = path.into();
match self.formats.get(format) {
Some(&(ref description, ref renderer)) => {
let path = if path.ends_with("auto") {
let file = if let Some(s) = self.source
let file = if let Some(s) = self
.source
.file
.as_ref()
.and_then(|f| Path::new(f).file_stem()) {
.and_then(|f| Path::new(f).file_stem())
{
s.to_string_lossy().into_owned()
} else {
return Err(Error::default(&self.source, lformat!("output to {format} set to auto but can't find book file name to infer it",
format = description)));
};
let file = renderer.auto_path(&file)
.map_err(|_| Error::default(&self.source,
lformat!("the {format} renderer does not support auto for output path",
format = description)))?;
};
let file = renderer.auto_path(&file).map_err(|_| {
Error::default(
&self.source,
lformat!(
"the {format} renderer does not support auto for output path",
format = description
),
)
})?;
path.with_file_name(file)
} else {
path
};
renderer.render_to_file(self, &path)?;
let path = misc::normalize(path);
let msg = lformat!("Succesfully generated {format}: {path}",
format = description,
path = &path);
let msg = lformat!(
"Succesfully generated {format}: {path}",
format = description,
path = &path
);
info!("{}", &msg);
self.bar_finish(Crowbar::Spinner(bar),
CrowbarState::Success,
&lformat!("generated {path}",
path = path));
self.bar_finish(
Crowbar::Spinner(bar),
CrowbarState::Success,
&lformat!("generated {path}", path = path),
);
Ok(())
},
None => {
Err(Error::default(Source::empty(),
lformat!("unknown format {format}",
format = format)))
}
None => Err(Error::default(
Source::empty(),
lformat!("unknown format {format}", format = format),
)),
}
}
/// Render book to specified format according to book options, and write the results
/// in the `Write` object.
///
@ -881,38 +967,46 @@ impl Book {
/// * `render_format`, which won't do anything if `output.{format}` isn't specified
/// in the book configuration file.
pub fn render_format_to<T: Write>(&mut self, format: &str, f: &mut T) -> Result<()> {
debug!("{}", lformat!("Attempting to generate {format}...",
format = format));
debug!(
"{}",
lformat!("Attempting to generate {format}...", format = format)
);
let bar = self.add_spinner_to_multibar(format);
match self.formats.get(format) {
Some(&(ref description, ref renderer)) => {
match renderer.render(self, f) {
Ok(_) => {
self.bar_finish(Crowbar::Spinner(bar),
CrowbarState::Success,
&lformat!("generated {format}",
format = format));
self.bar_finish(Crowbar::Main, CrowbarState::Success, &lformat!("Finished"));
info!("{}", lformat!("Succesfully generated {format}",
format = description));
Ok(())
},
Err(e) => {
self.bar_finish(Crowbar::Spinner(bar),
CrowbarState::Error,
&lformat!("{error}", error = e));
self.bar_finish(Crowbar::Main, CrowbarState::Error, &lformat!("ERROR"));
Err(e)
}
Some(&(ref description, ref renderer)) => match renderer.render(self, f) {
Ok(_) => {
self.bar_finish(
Crowbar::Spinner(bar),
CrowbarState::Success,
&lformat!("generated {format}", format = format),
);
self.bar_finish(Crowbar::Main, CrowbarState::Success, &lformat!("Finished"));
info!(
"{}",
lformat!("Succesfully generated {format}", format = description)
);
Ok(())
}
Err(e) => {
self.bar_finish(
Crowbar::Spinner(bar),
CrowbarState::Error,
&lformat!("{error}", error = e),
);
self.bar_finish(Crowbar::Main, CrowbarState::Error, &lformat!("ERROR"));
Err(e)
}
},
None => {
self.bar_finish(Crowbar::Spinner(bar),
CrowbarState::Error,
&lformat!("unknown format"));
Err(Error::default(Source::empty(),
lformat!("unknown format {format}",
format = format)))
self.bar_finish(
Crowbar::Spinner(bar),
CrowbarState::Error,
&lformat!("unknown format"),
);
Err(Error::default(
Source::empty(),
lformat!("unknown format {format}", format = format),
))
}
}
}
@ -934,40 +1028,44 @@ impl Book {
/// * `render_format`, which won't do anything if `output.{format}` isn't specified
/// in the book configuration file.
pub fn render_format_to_file<P:Into<PathBuf>>(&mut self,
format: &str,
path: P) -> Result<()> {
pub fn render_format_to_file<P: Into<PathBuf>>(&mut self, format: &str, path: P) -> Result<()> {
let bar = self.add_spinner_to_multibar(format);
let path = path.into();
let normalized = misc::normalize(&path);
self.render_format_to_file_with_bar(format, path, bar)?;
self.bar_finish(Crowbar::Spinner(bar),
CrowbarState::Success,
&lformat!("generated {path}",
path = normalized));
self.bar_finish(
Crowbar::Spinner(bar),
CrowbarState::Success,
&lformat!("generated {path}", path = normalized),
);
self.bar_finish(Crowbar::Main, CrowbarState::Success, &lformat!("Finished"));
Ok(())
}
/// Adds a chapter to the book.
///
/// This method is the backend used both by `add_chapter` and `add_chapter_from_source`.
pub fn add_chapter_from_named_source<R: Read>(&mut self,
number: Number,
file: &str,
mut source: R,
mut add_title_if_empty: bool)
-> Result<&mut Self> {
self.bar_set_message(Crowbar::Main, &lformat!("Processing {file}...", file = file));
pub fn add_chapter_from_named_source<R: Read>(
&mut self,
number: Number,
file: &str,
mut source: R,
mut add_title_if_empty: bool,
) -> Result<&mut Self> {
self.bar_set_message(
Crowbar::Main,
&lformat!("Processing {file}...", file = file),
);
let mut content = String::new();
source.read_to_string(&mut content)
.map_err(|_| {
Error::parser(&self.source,
lformat!("file {file} contains invalid UTF-8",
file = misc::normalize(file)))
})?;
source.read_to_string(&mut content).map_err(|_| {
Error::parser(
&self.source,
lformat!(
"file {file} contains invalid UTF-8",
file = misc::normalize(file)
),
)
})?;
// Ignore YAML blocks (or not)
self.parse_yaml(&mut content);
@ -987,12 +1085,16 @@ impl Book {
Path::new("")
};
if offset.starts_with("..") {
debug!("{}", lformat!("Warning: book contains chapter '{file}' in a directory above \
debug!(
"{}",
lformat!(
"Warning: book contains chapter '{file}' in a directory above \
the book file, this might cause problems",
file = misc::normalize(file)));
file = misc::normalize(file)
)
);
}
// For offset: if nothing is specified, it is the filename's directory
// If base_path.{images/links} is specified, override it for one of them.
// If base_path is specified, override it for both.
@ -1031,37 +1133,67 @@ impl Book {
if let Some(ref checker) = self.checker {
self.bar_set_message(Crowbar::Second, &lformat!("Running languagetool"));
info!("{}", lformat!("Trying to run languagetool on {file}, this might take a \
info!(
"{}",
lformat!(
"Trying to run languagetool on {file}, this might take a \
while...",
file = &normalized));
file = &normalized
)
);
if let Err(err) = checker.check_chapter(&mut tokens) {
error!("{}", lformat!("Error running languagetool on {file}: {error}",
file = &normalized,
error = err));
error!(
"{}",
lformat!(
"Error running languagetool on {file}: {error}",
file = &normalized,
error = err
)
);
}
}
if let Some(ref checker) = self.grammalecte {
self.bar_set_message(Crowbar::Second, &lformat!("Running grammalecte"));
info!("{}", lformat!("Trying to run grammalecte on {file}, this might take a \
info!(
"{}",
lformat!(
"Trying to run grammalecte on {file}, this might take a \
while...",
file = &normalized));
file = &normalized
)
);
if let Err(err) = checker.check_chapter(&mut tokens) {
error!("{}", lformat!("Error running grammalecte on {file}: {error}",
file = &normalized,
error = err));
error!(
"{}",
lformat!(
"Error running grammalecte on {file}: {error}",
file = &normalized,
error = err
)
);
}
}
if let Some(ref detector) = self.detector {
self.bar_set_message(Crowbar::Second, &lformat!("Detecting repetitions"));
info!("{}", lformat!("Trying to run repetition detector on {file}, this might take a \
info!(
"{}",
lformat!(
"Trying to run repetition detector on {file}, this might take a \
while...",
file = &normalized));
file = &normalized
)
);
if let Err(err) = detector.check_chapter(&mut tokens) {
error!("{}", lformat!("Error running repetition detector on {file}: {error}",
file = &normalized,
error = err));
error!(
"{}",
lformat!(
"Error running repetition detector on {file}: {error}",
file = &normalized,
error = err
)
);
}
}
}
@ -1095,9 +1227,8 @@ impl Book {
lformat!("this subchapter contains a heading that, when adjusted, is not in the right range ({} instead of [0-6])", new)));
}
*n = new;
},
_ => {},
}
_ => {}
}
}
}
@ -1105,7 +1236,6 @@ impl Book {
Ok(self)
}
/// Adds a chapter, as a file name, to the book
///
/// `Book` will then parse the file and store the AST (i.e., a vector
@ -1118,21 +1248,30 @@ impl Book {
///
/// **Returns** an error if `file` does not exist, could not be read, of if there was
/// some error parsing it.
pub fn add_chapter(&mut self, number: Number, file: &str, add_title_if_empty: bool) -> Result<&mut Self> {
self.bar_set_message(Crowbar::Main,
&lformat!("Parsing {file}",
file = misc::normalize(file)));
debug!("{}", lformat!("Parsing chapter: {file}...",
file = misc::normalize(file)));
pub fn add_chapter(
&mut self,
number: Number,
file: &str,
add_title_if_empty: bool,
) -> Result<&mut Self> {
self.bar_set_message(
Crowbar::Main,
&lformat!("Parsing {file}", file = misc::normalize(file)),
);
debug!(
"{}",
lformat!("Parsing chapter: {file}...", file = misc::normalize(file))
);
// try to open file
let path = self.root.join(file);
let f = File::open(&path)
.map_err(|_| {
Error::file_not_found(&self.source,
lformat!("book chapter"),
format!("{}", path.display()))
})?;
let f = File::open(&path).map_err(|_| {
Error::file_not_found(
&self.source,
lformat!("book chapter"),
format!("{}", path.display()),
)
})?;
self.add_chapter_from_named_source(number, file, f, add_title_if_empty)
}
@ -1148,11 +1287,15 @@ impl Book {
/// * `content`: the content of the chapter.
///
/// **Returns** an error if there was some errror parsing `content`.
pub fn add_chapter_from_source<R: Read>(&mut self, number: Number, source: R, add_title_if_empty: bool) -> Result<&mut Self> {
pub fn add_chapter_from_source<R: Read>(
&mut self,
number: Number,
source: R,
add_title_if_empty: bool,
) -> Result<&mut Self> {
self.add_chapter_from_named_source(number, "", source, add_title_if_empty)
}
/// Either clean a string or does nothing,
/// according to book `lang` and `autoclean` options
#[doc(hidden)]
@ -1160,8 +1303,6 @@ impl Book {
self.cleaner.clean(text.into())
}
/// Returns a template
///
/// Returns the default one if no option was set, or the one set by the user.
@ -1192,37 +1333,40 @@ impl Book {
"html.if.new_game" => html_if::NEW_GAME,
"tex.template" => latex::TEMPLATE,
_ => {
return Err(Error::config_parser(&self.source,
lformat!("invalid template '{template}'",
template = template)))
return Err(Error::config_parser(
&self.source,
lformat!("invalid template '{template}'", template = template),
))
}
};
if let Ok(ref s) = option {
let mut f = File::open(s)
.map_err(|_| {
Error::file_not_found(&self.source,
format!("template '{template}'", template = template),
s.to_owned())
})?;
let mut f = File::open(s).map_err(|_| {
Error::file_not_found(
&self.source,
format!("template '{template}'", template = template),
s.to_owned(),
)
})?;
let mut res = String::new();
f.read_to_string(&mut res)
.map_err(|_| {
Error::config_parser(&self.source,
lformat!("file '{file}' could not be read", file = s))
})?;
f.read_to_string(&mut res).map_err(|_| {
Error::config_parser(
&self.source,
lformat!("file '{file}' could not be read", file = s),
)
})?;
Ok(Cow::Owned(res))
} else {
Ok(Cow::Borrowed(fallback))
}
}
/// Sets the chapter_template once and for all
fn set_chapter_template(&mut self) -> Result<()> {
let template =
compile_str(self.options.get_str("rendering.chapter.template").unwrap(),
&self.source,
"rendering.chapter.template")?;
let template = compile_str(
self.options.get_str("rendering.chapter.template").unwrap(),
&self.source,
"rendering.chapter.template",
)?;
self.chapter_template = Some(template);
Ok(())
}
@ -1231,14 +1375,24 @@ impl Book {
#[doc(hidden)]
pub fn get_header_number(&self, header: Header, n: i32) -> Result<String> {
let boolean = match header {
Header::Part => self.options.get_bool("rendering.part.roman_numerals").unwrap(),
Header::Chapter => self.options.get_bool("rendering.chapter.roman_numerals").unwrap(),
Header::Part => self
.options
.get_bool("rendering.part.roman_numerals")
.unwrap(),
Header::Chapter => self
.options
.get_bool("rendering.chapter.roman_numerals")
.unwrap(),
};
let number = if boolean {
if n <= 0 {
return Err(Error::render(Source::empty(),
lformat!("can not use roman numerals with zero or negative chapter numbers ({n})",
n = n)));
return Err(Error::render(
Source::empty(),
lformat!(
"can not use roman numerals with zero or negative chapter numbers ({n})",
n = n
),
));
}
format!("{:X}", Roman::from(n as i16))
} else {
@ -1247,11 +1401,17 @@ impl Book {
Ok(number)
}
/// Returns the string corresponding to a number, title, and the numbering template for chapter
#[doc(hidden)]
pub fn get_header<F>(&self, header: Header, n: i32, title: String, mut f: F) -> Result<HeaderData>
where F: FnMut(&str) -> Result<String>
pub fn get_header<F>(
&self,
header: Header,
n: i32,
title: String,
mut f: F,
) -> Result<HeaderData>
where
F: FnMut(&str) -> Result<String>,
{
let header_type = match header {
Header::Part => "part",
@ -1259,19 +1419,17 @@ impl Book {
};
let mut data = self.get_metadata(&mut f)?;
if !title.is_empty() {
data = data.insert_bool(format!("has_{}_title", header_type),
true);
data = data.insert_bool(format!("has_{}_title", header_type), true);
}
let number = self.get_header_number(header, n)?;
let header_name = self
.options
.get_str(&format!("rendering.{}", header_type))
.map(|s| s.to_owned())
.unwrap_or_else(|_| lang::get_str(self.options.get_str("lang").unwrap(),
header_type));
.unwrap_or_else(|_| lang::get_str(self.options.get_str("lang").unwrap(), header_type));
data = data.insert_str(format!("{}_title", header_type),
title.clone())
data = data
.insert_str(format!("{}_title", header_type), title.clone())
.insert_str(header_type, header_name.clone())
.insert_str("number", number.clone());
let data = data.build();
@ -1285,11 +1443,13 @@ impl Book {
if let Some(ref template) = *opt_template {
template.render_data(&mut res, &data)?;
} else {
let template =
compile_str(self.options.get_str(&format!("rendering.{}.template", header_type))
.unwrap(),
&self.source,
&format!("rendering.{}.template", header_type))?;
let template = compile_str(
self.options
.get_str(&format!("rendering.{}.template", header_type))
.unwrap(),
&self.source,
&format!("rendering.{}.template", header_type),
)?;
template.render_data(&mut res, &data)?;
}
@ -1307,7 +1467,8 @@ impl Book {
/// Returns the string corresponding to a number, title, and the numbering template for chapter
#[doc(hidden)]
pub fn get_chapter_header<F>(&self, n: i32, title: String, f: F) -> Result<HeaderData>
where F: FnMut(&str) -> Result<String>
where
F: FnMut(&str) -> Result<String>,
{
self.get_header(Header::Chapter, n, title, f)
}
@ -1315,7 +1476,8 @@ impl Book {
/// Returns the string corresponding to a number, title, and the numbering template for part
#[doc(hidden)]
pub fn get_part_header<F>(&self, n: i32, title: String, f: F) -> Result<HeaderData>
where F: FnMut(&str) -> Result<String>
where
F: FnMut(&str) -> Result<String>,
{
self.get_header(Header::Part, n, title, f)
}
@ -1329,13 +1491,15 @@ impl Book {
/// This method treats the metadata as Markdown and thus calls `f` to render it.
#[doc(hidden)]
pub fn get_metadata<F>(&self, mut f: F) -> Result<MapBuilder>
where F: FnMut(&str) -> Result<String>
where
F: FnMut(&str) -> Result<String>,
{
let mut mapbuilder = MapBuilder::new();
mapbuilder = mapbuilder.insert_str("crowbook_version", env!("CARGO_PKG_VERSION"));
mapbuilder =
mapbuilder.insert_bool(format!("lang_{}", self.options.get_str("lang").unwrap()),
true);
mapbuilder = mapbuilder.insert_bool(
format!("lang_{}", self.options.get_str("lang").unwrap()),
true,
);
// Add metadata to mapbuilder
for key in self.options.get_metadata() {
@ -1360,11 +1524,15 @@ impl Book {
}
}
Err(err) => {
return Err(Error::render(&self.source,
lformat!("could not render `{key}` for \
return Err(Error::render(
&self.source,
lformat!(
"could not render `{key}` for \
metadata:\n{error}",
key = &key,
error = err)));
key = &key,
error = err
),
));
}
}
} else {
@ -1391,8 +1559,11 @@ impl Book {
/// or
/// ...
fn parse_yaml(&mut self, content: &mut String) {
if !(content.starts_with("---\n") || content.contains("\n---\n") ||
content.starts_with("---\r\n") || content.contains("\n---\r\n")) {
if !(content.starts_with("---\n")
|| content.contains("\n---\n")
|| content.starts_with("---\r\n")
|| content.contains("\n---\r\n"))
{
// Content can't contain YAML, so aborting early
return;
}
@ -1412,27 +1583,41 @@ impl Book {
Ok(docs) => {
// Use this yaml block to set options only if 1) it is valid
// 2) the option is activated
if docs.len() == 1 && docs[0].as_hash().is_some() &&
self.options.get_bool("input.yaml_blocks") == Ok(true) {
if docs.len() == 1
&& docs[0].as_hash().is_some()
&& self.options.get_bool("input.yaml_blocks") == Ok(true)
{
let hash = docs[0].as_hash().unwrap();
for (key, value) in hash {
match self.options
match self
.options
//todo: remove clone
.set_yaml(key.clone(), value.clone()) {
.set_yaml(key.clone(), value.clone())
{
Ok(opt) => {
if let Some(old_value) = opt {
debug!("{}", lformat!("Inline YAML block \
debug!(
"{}",
lformat!(
"Inline YAML block \
replaced {:?} \
previously set to \
{:?} to {:?}",
key,
old_value,
value));
key,
old_value,
value
)
);
} else {
debug!("{}", lformat!("Inline YAML block \
debug!(
"{}",
lformat!(
"Inline YAML block \
set {:?} to {:?}",
key,
value));
key,
value
)
);
}
}
Err(e) => {
@ -1445,20 +1630,35 @@ impl Book {
}
}
} else {
debug!("{}", lformat!("Ignoring YAML \
debug!(
"{}",
lformat!(
"Ignoring YAML \
block:\n---\n{block}---",
block = &yaml_block));
block = &yaml_block
)
);
}
valid_block = true;
}
Err(err) => {
error!("{}", lformat!("Found something that looked like a \
error!(
"{}",
lformat!(
"Found something that looked like a \
YAML block:\n{block}",
block = &yaml_block));
error!("{}", lformat!("... but it didn't parse correctly as \
block = &yaml_block
)
);
error!(
"{}",
lformat!(
"... but it didn't parse correctly as \
YAML('{error}'), so treating it like \
Markdown.",
error = err));
error = err
)
);
}
}
break;
@ -1487,13 +1687,18 @@ impl Book {
self.init_checker();
}
// Update the cleaner according to autoclean and lang options
fn update_cleaner(&mut self) {
let params = CleanerParams {
smart_quotes: self.options.get_bool("input.clean.smart_quotes").unwrap(),
ligature_dashes: self.options.get_bool("input.clean.ligature.dashes").unwrap(),
ligature_guillemets: self.options.get_bool("input.clean.ligature.guillemets").unwrap(),
ligature_dashes: self
.options
.get_bool("input.clean.ligature.dashes")
.unwrap(),
ligature_guillemets: self
.options
.get_bool("input.clean.ligature.guillemets")
.unwrap(),
};
if self.options.get_bool("input.clean").unwrap() {
let lang = self.options.get_str("lang").unwrap().to_lowercase();
@ -1509,17 +1714,22 @@ impl Book {
}
}
/// Calls mustache::compile_str but catches panics and returns a result
pub fn compile_str<O>(template: &str, source: O, template_name: &str) -> Result<mustache::Template>
where O: Into<Source>
where
O: Into<Source>,
{
let input: String = template.to_owned();
let result = mustache::compile_str(&input);
match result {
Ok(result) => Ok(result),
Err(err) => Err(Error::template(source, lformat!("could not compile '{template}': {error}",
template = template_name,
error = err))),
Err(err) => Err(Error::template(
source,
lformat!(
"could not compile '{template}': {error}",
template = template_name,
error = err
),
)),
}
}

View File

@ -20,11 +20,11 @@
use crate::book::{Book, Crowbar, CrowbarState};
use indicatif::{ProgressBar, ProgressStyle, MultiProgress};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use std::mem;
use std::sync::Arc;
use std::thread;
use std::mem;
/// Store the progress bars needed for the book
pub struct Bars {
@ -56,7 +56,6 @@ impl Bars {
}
}
/// Return the style of a bar
impl Book {
@ -67,20 +66,25 @@ impl Book {
self.bars.emoji = emoji;
let multibar = Arc::new(MultiProgress::new());
self.bars.multibar = Some(multibar.clone());
let b = self.bars.multibar
let b = self
.bars
.multibar
.as_ref()
.unwrap()
.add(ProgressBar::new_spinner());
b.enable_steady_tick(200);
self.bars.mainbar = Some(b);
// let sty = ProgressStyle::default_spinner()
// .tick_chars("🕛🕐🕑🕒🕓🕔🕔🕕🕖🕗🕘🕘🕙🕚V")
// .tick_chars("/|\\-V")
// .template("{spinner:.dim.bold.yellow} {prefix} {wide_msg}");
// let sty = ProgressStyle::default_spinner()
// .tick_chars("🕛🕐🕑🕒🕓🕔🕔🕕🕖🕗🕘🕘🕙🕚V")
// .tick_chars("/|\\-V")
// .template("{spinner:.dim.bold.yellow} {prefix} {wide_msg}");
self.bar_set_style(Crowbar::Main, CrowbarState::Running);
self.bars.guard = Some(thread::spawn(move || {
if let Err(_) = multibar.join() {
error!("{}", lformat!("could not display fancy UI, try running crowbook with --no-fancy"));
error!(
"{}",
lformat!("could not display fancy UI, try running crowbook with --no-fancy")
);
}
}));
}
@ -89,9 +93,27 @@ impl Book {
pub fn bar_finish(&self, bar: Crowbar, state: CrowbarState, msg: &str) {
self.bar_set_style(bar, state);
let pb = match bar {
Crowbar::Main => if let Some(ref bar) = self.bars.mainbar { bar } else { return; },
Crowbar::Second => if let Some(ref bar) = self.bars.secondbar { bar } else { return; },
Crowbar::Spinner(i) => if i < self.bars.spinners.len() { &self.bars.spinners[i] } else { return; },
Crowbar::Main => {
if let Some(ref bar) = self.bars.mainbar {
bar
} else {
return;
}
}
Crowbar::Second => {
if let Some(ref bar) = self.bars.secondbar {
bar
} else {
return;
}
}
Crowbar::Spinner(i) => {
if i < self.bars.spinners.len() {
&self.bars.spinners[i]
} else {
return;
}
}
};
match bar {
@ -101,7 +123,7 @@ impl Book {
}
/// Adds a secondary progress bar to display progress of book parsing
pub fn add_second_bar(&mut self, msg: &str, len: u64) {
pub fn add_second_bar(&mut self, msg: &str, len: u64) {
if let Some(ref multibar) = self.bars.multibar {
let bar = multibar.add(ProgressBar::new(len));
self.bar_set_style(Crowbar::Second, CrowbarState::Running);
@ -140,9 +162,27 @@ impl Book {
pub fn bar_set_message(&self, bar: Crowbar, msg: &str) {
let bar = match bar {
Crowbar::Main => if let Some(ref bar) = self.bars.mainbar { bar } else { return; },
Crowbar::Second => if let Some(ref bar) = self.bars.secondbar { bar } else { return; },
Crowbar::Spinner(i) => if i < self.bars.spinners.len() { &self.bars.spinners[i] } else { return; },
Crowbar::Main => {
if let Some(ref bar) = self.bars.mainbar {
bar
} else {
return;
}
}
Crowbar::Second => {
if let Some(ref bar) = self.bars.secondbar {
bar
} else {
return;
}
}
Crowbar::Spinner(i) => {
if i < self.bars.spinners.len() {
&self.bars.spinners[i]
} else {
return;
}
}
};
bar.set_message(msg);
}
@ -150,15 +190,32 @@ impl Book {
/// Sets the style of a bar
fn bar_set_style(&self, bar: Crowbar, state: CrowbarState) -> () {
let pb = match bar {
Crowbar::Main => if let Some(ref bar) = self.bars.mainbar { bar } else { return; },
Crowbar::Second => if let Some(ref bar) = self.bars.secondbar { bar } else { return; },
Crowbar::Spinner(i) => if i < self.bars.spinners.len() { &self.bars.spinners[i] } else { return; },
Crowbar::Main => {
if let Some(ref bar) = self.bars.mainbar {
bar
} else {
return;
}
}
Crowbar::Second => {
if let Some(ref bar) = self.bars.secondbar {
bar
} else {
return;
}
}
Crowbar::Spinner(i) => {
if i < self.bars.spinners.len() {
&self.bars.spinners[i]
} else {
return;
}
}
};
let emoji = self.bars.emoji;
let mut style = match bar {
Crowbar::Second => ProgressStyle::default_bar(),
_ => ProgressStyle::default_spinner()
_ => ProgressStyle::default_spinner(),
};
let color = match state {
@ -167,10 +224,10 @@ impl Book {
CrowbarState::Error => "red",
};
let tick_chars = match (bar, emoji) {
(Crowbar::Main, false) | (Crowbar::Spinner(_), false) => "-\\|/",
(Crowbar::Main, false) | (Crowbar::Spinner(_), false) => "-\\|/",
(Crowbar::Main, true) => "🕛🕐🕑🕒🕓🕔🕔🕕🕖🕗🕘🕘🕙🕚",
(Crowbar::Spinner(_), true) => "◐◓◑◒",
(_, _) => ""
(_, _) => "",
};
let end_tick = match (state, emoji) {
(CrowbarState::Running, _) => "V",
@ -181,24 +238,27 @@ impl Book {
};
match bar {
Crowbar::Second => {
style = style.template("{bar:40.cyan/blue} {percent:>7} {wide_msg}")
style = style
.template("{bar:40.cyan/blue} {percent:>7} {wide_msg}")
.progress_chars("##-");
},
}
bar => {
style = style.tick_chars(&format!("{}{}", tick_chars, end_tick));
match bar {
Crowbar::Spinner(_) => {
style = style
.template(&format!("{{spinner:.bold.{color}}} {{prefix}} {{wide_msg}}",
color = color));
},
style = style.template(&format!(
"{{spinner:.bold.{color}}} {{prefix}} {{wide_msg}}",
color = color
));
}
_ => {
style = style
.template(&format!("{{spinner:.bold.{color}}} {{prefix}}{{wide_msg}}",
color = color));
},
style = style.template(&format!(
"{{spinner:.bold.{color}}} {{prefix}}{{wide_msg}}",
color = color
));
}
};
},
}
}
pb.set_style(style);
}
@ -212,9 +272,7 @@ impl Drop for Book {
if let Some(ref bar) = self.bars.mainbar {
bar.finish();
let guard = mem::replace(&mut self.bars.guard, None);
guard.unwrap()
.join()
.unwrap();
guard.unwrap().join().unwrap();
}
}
}

View File

@ -20,38 +20,30 @@
use crate::book::{Book, Crowbar, CrowbarState};
/// Dummy bars implementation
pub struct Bars {
}
pub struct Bars {}
impl Bars {
pub fn new() -> Bars {
Bars {
}
Bars {}
}
}
impl Book {
pub fn private_add_progress_bar(&mut self, _: bool) {
}
pub fn private_add_progress_bar(&mut self, _: bool) {}
/// Sets a finished message to the progress bar, if it is set
pub fn bar_finish(&self, _: Crowbar, _: CrowbarState, _: &str) {
}
pub fn bar_finish(&self, _: Crowbar, _: CrowbarState, _: &str) {}
/// Adds a secondary progress bar to display progress of book parsing
pub fn add_second_bar(&mut self, _: &str, _: u64) {
}
pub fn add_second_bar(&mut self, _: &str, _: u64) {}
/// Increment second bar
pub fn inc_second_bar(&self) {
}
pub fn inc_second_bar(&self) {}
/// Adds a spinner labeled key to the multibar, and set mainbar to "rendering"
pub fn add_spinner_to_multibar(&mut self, _: &str) -> usize {
0
}
pub fn bar_set_message(&self, _: Crowbar, _: &str) {
}
pub fn bar_set_message(&self, _: Crowbar, _: &str) {}
}

View File

@ -15,11 +15,11 @@
// You should have received a copy of the GNU Lesser General Public License
// along with Crowbook. If not, see <http://www.gnu.org/licenses/>.
use crate::error::{Error, Result, Source};
use crate::book::Book;
use crate::error::{Error, Result, Source};
use std::io::Write;
use std::fs::File;
use std::io::Write;
use std::path::Path;
/// Trait that must be implemented by the various renderers to render a whole book.
@ -27,8 +27,10 @@ use std::path::Path;
pub trait BookRenderer: Sync {
/// Path destination when output is set to auto
fn auto_path(&self, _book_file: &str) -> Result<String> {
Err(Error::default(Source::empty(),
lformat!("This renderer does not support the auto output")))
Err(Error::default(
Source::empty(),
lformat!("This renderer does not support the auto output"),
))
}
/// Render the book and write the result to the specified writer
@ -42,16 +44,26 @@ pub trait BookRenderer: Sync {
// Not optimal but avoid creating an empty file if it fails
let mut content = vec![];
self.render(book, &mut content)?;
let mut file = File::create(path)
.map_err(|err| Error::default(Source::empty(),
lformat!("could not create file '{file}': {err}",
file = path.display(),
err = err)))?;
file.write_all(&content)
.map_err(|err| Error::default(Source::empty(),
lformat!("could not write book content to file '{file}': {err}",
file = path.display(),
err = err)))?;
let mut file = File::create(path).map_err(|err| {
Error::default(
Source::empty(),
lformat!(
"could not create file '{file}': {err}",
file = path.display(),
err = err
),
)
})?;
file.write_all(&content).map_err(|err| {
Error::default(
Source::empty(),
lformat!(
"could not write book content to file '{file}': {err}",
file = path.display(),
err = err
),
)
})?;
Ok(())
}
}

View File

@ -51,7 +51,10 @@ impl BookOption {
pub fn as_str(&self) -> Result<&str> {
match *self {
BookOption::String(ref s) => Ok(s),
_ => Err(Error::book_option(Source::empty(), lformat!("{:?} is not a string", self))),
_ => Err(Error::book_option(
Source::empty(),
lformat!("{:?} is not a string", self),
)),
}
}
@ -59,7 +62,10 @@ impl BookOption {
pub fn as_str_vec(&self) -> Result<&[String]> {
match *self {
BookOption::StringVec(ref v) => Ok(v),
_ => Err(Error::book_option(Source::empty(), lformat!("{:?} is not a string vector", self))),
_ => Err(Error::book_option(
Source::empty(),
lformat!("{:?} is not a string vector", self),
)),
}
}
@ -67,7 +73,10 @@ impl BookOption {
pub fn as_path(&self) -> Result<&str> {
match *self {
BookOption::Path(ref s) => Ok(s),
_ => Err(Error::book_option(Source::empty(), lformat!("{:?} is not a path", self))),
_ => Err(Error::book_option(
Source::empty(),
lformat!("{:?} is not a path", self),
)),
}
}
@ -75,7 +84,10 @@ impl BookOption {
pub fn as_bool(&self) -> Result<bool> {
match *self {
BookOption::Bool(b) => Ok(b),
_ => Err(Error::book_option(Source::empty(), lformat!("{:?} is not a bool", self))),
_ => Err(Error::book_option(
Source::empty(),
lformat!("{:?} is not a bool", self),
)),
}
}
@ -83,7 +95,10 @@ impl BookOption {
pub fn as_char(&self) -> Result<char> {
match *self {
BookOption::Char(c) => Ok(c),
_ => Err(Error::book_option(Source::empty(), lformat!("{:?} is not a char", self))),
_ => Err(Error::book_option(
Source::empty(),
lformat!("{:?} is not a char", self),
)),
}
}
@ -91,8 +106,10 @@ impl BookOption {
pub fn as_i32(&self) -> Result<i32> {
match *self {
BookOption::Int(i) => Ok(i),
_ => Err(Error::book_option(Source::empty(), lformat!("{:?} is not an i32", self))),
_ => Err(Error::book_option(
Source::empty(),
lformat!("{:?} is not an i32", self),
)),
}
}
@ -100,7 +117,10 @@ impl BookOption {
pub fn as_f32(&self) -> Result<f32> {
match *self {
BookOption::Float(f) => Ok(f),
_ => Err(Error::book_option(Source::empty(), lformat!("{:?} is not a f32", self))),
_ => Err(Error::book_option(
Source::empty(),
lformat!("{:?} is not a f32", self),
)),
}
}
}

View File

@ -1,13 +1,13 @@
use crate::error::{Error, Result, Source};
use crate::bookoption::BookOption;
use crate::book::Book;
use crate::bookoption::BookOption;
use crate::error::{Error, Result, Source};
use crate::style;
use yaml_rust::{Yaml, YamlLoader};
use std::collections::HashMap;
use std::path::{PathBuf, Path};
use std::env;
use std::mem;
use std::path::{Path, PathBuf};
use yaml_rust::{Yaml, YamlLoader};
lazy_static! {
static ref OPTIONS: String = format!("\
@ -354,7 +354,6 @@ crowbook.verbose:alias # {removed}
);
}
/// Contains the options of a book.
///
/// This structure offers some facilities to check the content of an option.
@ -432,7 +431,7 @@ impl BookOptions {
"meta" => {
options.metadata.push(key.to_owned());
options.valid_strings.push(key);
},
}
"str" => options.valid_strings.push(key),
"strvec" => options.valid_str_vecs.push(key),
"bool" => options.valid_bools.push(key),
@ -445,18 +444,24 @@ impl BookOptions {
options.valid_paths.push(key);
}
"alias" => {
options.deprecated.insert(key.to_owned(), default_value.map(|s| s.to_owned()));
options
.deprecated
.insert(key.to_owned(), default_value.map(|s| s.to_owned()));
continue;
}
_ => {
panic!(lformat!("Ill-formatted OPTIONS string: unrecognized type \
panic!(lformat!(
"Ill-formatted OPTIONS string: unrecognized type \
'{option_type}'",
option_type = option_type.unwrap()))
option_type = option_type.unwrap()
))
}
}
if key == "crowbook.temp_dir" {
// "temp_dir" has a special default value that depends on the environment
options.set(key, &env::temp_dir().to_string_lossy()).unwrap();
options
.set(key, &env::temp_dir().to_string_lossy())
.unwrap();
continue;
}
if let Some(value) = default_value {
@ -469,7 +474,6 @@ impl BookOptions {
options
}
/// Sets an option from a Yaml tuple
///
/// # Arguments
@ -486,153 +490,213 @@ impl BookOptions {
let key: String = if let Yaml::String(key) = key {
key
} else {
return Err(Error::book_option(&self.source,
lformat!("Expected a String as a key, found {:?}", key)));
return Err(Error::book_option(
&self.source,
lformat!("Expected a String as a key, found {:?}", key),
));
};
if self.valid_str_vecs.contains(&key.as_ref()) {
// Value is a list of string
if let Yaml::Array(array) = value {
let mut inner:Vec<String> = vec!();
let mut inner: Vec<String> = vec![];
for value in array.into_iter() {
if let Yaml::String(value) = value {
inner.push(value);
} else {
return Err(Error::book_option(&self.source,
lformat!("Expected only string in the list for key {}, found {:?}",
&key,
&value)));
return Err(Error::book_option(
&self.source,
lformat!(
"Expected only string in the list for key {}, found {:?}",
&key,
&value
),
));
}
}
// special case
if &key == "output" {
for format in &inner {
self.set_yaml(Yaml::String(format!("output.{}", format)),
Yaml::String(String::from("auto")))
.map_err(|_| Error::book_option(&self.source,
lformat!("The output format {format} for key {key} is not recognized",
key = key,
format = format)))?;
self.set_yaml(
Yaml::String(format!("output.{}", format)),
Yaml::String(String::from("auto")),
)
.map_err(|_| {
Error::book_option(
&self.source,
lformat!(
"The output format {format} for key {key} is not recognized",
key = key,
format = format
),
)
})?;
}
}
Ok(self.options.insert(key, BookOption::StringVec(inner)))
} else {
Err(Error::book_option(&self.source,
lformat!("Expected a list as value for key {}, found {:?}",
&key,
&value)))
Err(Error::book_option(
&self.source,
lformat!(
"Expected a list as value for key {}, found {:?}",
&key,
&value
),
))
}
} else if self.valid_strings.contains(&key.as_ref()) {
// value is a string
if let Yaml::String(value) = value {
Ok(self.options.insert(key, BookOption::String(value)))
} else {
Err(Error::book_option(&self.source,
lformat!("Expected a string as value for key {}, found \
Err(Error::book_option(
&self.source,
lformat!(
"Expected a string as value for key {}, found \
{:?}",
&key,
&value)))
&key,
&value
),
))
}
} else if self.valid_paths.contains(&key.as_ref()) {
// value is a path
if let Yaml::String(value) = value {
if &key == "import" {
// special case, not a real option
let tmp = self.root
.join(&value);
let file = tmp
.to_str()
.ok_or_else(|| Error::book_option(&self.source,
lformat!("'{value}''s path contains invalid \
let tmp = self.root.join(&value);
let file = tmp.to_str().ok_or_else(|| {
Error::book_option(
&self.source,
lformat!(
"'{value}''s path contains invalid \
UTF-8 code",
value = &value)))?;
value = &value
),
)
})?;
let mut book = Book::new();
book.load_file(file)?;
let options = mem::replace(&mut book.options, BookOptions::new());
self.merge(options)?;
Ok(None)
} else {
} else {
Ok(self.options.insert(key, BookOption::Path(value)))
}
} else {
Err(Error::book_option(&self.source,
lformat!("expected a string as value for key '{}', found \
Err(Error::book_option(
&self.source,
lformat!(
"expected a string as value for key '{}', found \
{:?}",
&key,
&value)))
&key,
&value
),
))
}
} else if self.valid_chars.contains(&key.as_ref()) {
// value is a char
if let Yaml::String(value) = value {
let chars: Vec<_> = value.chars().collect();
if chars.len() != 1 {
return Err(Error::book_option(&self.source,
lformat!("could not parse '{value}' as a \
return Err(Error::book_option(
&self.source,
lformat!(
"could not parse '{value}' as a \
char: does not contain exactly one \
char",
value = &value)));
value = &value
),
));
}
Ok(self.options.insert(key.to_owned(), BookOption::Char(chars[0])))
Ok(self
.options
.insert(key.to_owned(), BookOption::Char(chars[0])))
} else {
Err(Error::book_option(&self.source,
lformat!("expected a string as value containing a char \
Err(Error::book_option(
&self.source,
lformat!(
"expected a string as value containing a char \
for key '{}', found {:?}",
&key,
&value)))
&key,
&value
),
))
}
} else if self.valid_bools.contains(&key.as_ref()) {
// value is a bool
if let Yaml::Boolean(value) = value {
Ok(self.options.insert(key, BookOption::Bool(value)))
} else {
Err(Error::book_option(&self.source,
lformat!("expected a boolean as value for key '{}', \
Err(Error::book_option(
&self.source,
lformat!(
"expected a boolean as value for key '{}', \
found {:?}",
&key,
&value)))
&key,
&value
),
))
}
} else if self.valid_ints.contains(&key.as_ref()) {
// value is an int
if let Yaml::Integer(value) = value {
Ok(self.options.insert(key, BookOption::Int(value as i32)))
} else {
Err(Error::book_option(&self.source,
lformat!("expected an integer as value for key '{}', \
Err(Error::book_option(
&self.source,
lformat!(
"expected an integer as value for key '{}', \
found {:?}",
&key,
&value)))
&key,
&value
),
))
}
} else if self.valid_floats.contains(&key.as_ref()) {
// value is a float
if let Yaml::Real(value) = value {
match value.parse::<f32>() {
Ok(value) => Ok(self.options.insert(key, BookOption::Float(value))),
Err(_) => {
Err(Error::book_option(&self.source,
lformat!("could not parse '{value}' as a float \
Err(_) => Err(Error::book_option(
&self.source,
lformat!(
"could not parse '{value}' as a float \
for key '{key}'",
value = &value,
key = &key)))
}
value = &value,
key = &key
),
)),
}
} else {
Err(Error::book_option(&self.source,
lformat!("expected a float as value for key '{}', found \
Err(Error::book_option(
&self.source,
lformat!(
"expected a float as value for key '{}', found \
{:?}",
&key,
&value)))
&key,
&value
),
))
}
} else if self.deprecated.contains_key(&key) {
let opt = self.deprecated[&key].clone();
if let Some(new_key) = opt {
warn!("{}", lformat!("'{old_key}' has been deprecated, you should \
warn!(
"{}",
lformat!(
"'{old_key}' has been deprecated, you should \
now use '{new_key}'",
old_key = &key,
new_key = &new_key));
old_key = &key,
new_key = &new_key
)
);
self.set_yaml(Yaml::String(new_key), value)
} else {
Err(Error::book_option(self.source.clone(),
lformat!("key '{key}' has been deprecated.", key = &key)))
Err(Error::book_option(
self.source.clone(),
lformat!("key '{key}' has been deprecated.", key = &key),
))
}
} else if key.starts_with("metadata.") {
// key is a custom metadata
@ -641,16 +705,22 @@ impl BookOptions {
self.metadata.push(key.clone());
Ok(self.options.insert(key, BookOption::String(value)))
} else {
Err(Error::book_option(&self.source,
lformat!("expected a string as value for key '{}', found \
Err(Error::book_option(
&self.source,
lformat!(
"expected a string as value for key '{}', found \
{:?}",
&key,
&value)))
&key,
&value
),
))
}
} else {
// key not recognized
Err(Error::book_option(self.source.clone(),
lformat!("unrecognized key '{key}'", key = &key)))
Err(Error::book_option(
self.source.clone(),
lformat!("unrecognized key '{key}'", key = &key),
))
}
}
@ -688,16 +758,24 @@ impl BookOptions {
let yaml_value = yaml_docs.into_iter().next().unwrap();
self.set_yaml(Yaml::String(key.to_owned()), yaml_value)
} else {
Err(Error::book_option(&self.source,
lformat!("value '{value}' for key '{key}' does not \
Err(Error::book_option(
&self.source,
lformat!(
"value '{value}' for key '{key}' does not \
contain one and only one YAML value",
value = value,
key = key)))
value = value,
key = key
),
))
}
} else {
Err(Error::book_option(&self.source,
lformat!("could not parse '{value}' as a valid YAML value",
value = value)))
Err(Error::book_option(
&self.source,
lformat!(
"could not parse '{value}' as a valid YAML value",
value = value
),
))
}
}
@ -711,8 +789,10 @@ impl BookOptions {
#[doc(hidden)]
pub fn get(&self, key: &str) -> Result<&BookOption> {
self.options.get(key).ok_or_else(|| {
Error::invalid_option(&self.source,
lformat!("option '{key}' is not present", key = key))
Error::invalid_option(
&self.source,
lformat!("option '{key}' is not present", key = key),
)
})
}
@ -752,10 +832,10 @@ impl BookOptions {
}
let new_path: PathBuf = match key {
"resources.base_path.links" |
"resources.base_path.images" |
"resources.base_path.files" |
"resources.pase_path.templates" => {
"resources.base_path.links"
| "resources.base_path.images"
| "resources.base_path.files"
| "resources.pase_path.templates" => {
// If resources.base_path is set, return it, else return itself
let base_path = self.get_path("resources.base_path");
if base_path.is_ok() {
@ -770,16 +850,16 @@ impl BookOptions {
Path::new(&base).join(path)
}
"output.epub" |
"output.html" |
"output.html.dir" |
"output.pdf" |
"output.tex" |
"output.odt" |
"output.proofread.html" |
"output.proofread.html.dir" |
"output.proofread.pdf" |
"output.html.if" => {
"output.epub"
| "output.html"
| "output.html.dir"
| "output.pdf"
| "output.tex"
| "output.odt"
| "output.proofread.html"
| "output.proofread.html.dir"
| "output.proofread.pdf"
| "output.html.if" => {
// Translate according to output.base_path
let base = self.get_path("output.base_path").unwrap();
Path::new(&base).join(path)
@ -796,9 +876,10 @@ impl BookOptions {
if let Some(path) = new_path.to_str() {
Ok(path.to_owned())
} else {
Err(Error::book_option(&self.source,
lformat!("'{key}''s path contains invalid UTF-8 code",
key = key)))
Err(Error::book_option(
&self.source,
lformat!("'{key}''s path contains invalid UTF-8 code", key = key),
))
}
}
@ -845,7 +926,6 @@ impl BookOptions {
self.get(key)?.as_f32()
}
/// Merges the other list of options to the first one
///
/// If option is already set in self, don't add it, unless it was the default.
@ -876,17 +956,23 @@ impl BookOptions {
let path = other.get_path(key).unwrap();
let new_path = ::std::env::current_dir()
.map_err(|_| {
Error::default(Source::empty(),
lformat!("could not get current directory!"))
Error::default(
Source::empty(),
lformat!("could not get current directory!"),
)
})?
.join(&path);
let new_path = if let Some(path) = new_path.to_str() {
path.to_owned()
} else {
return Err(Error::book_option(Source::new(other.root.to_str().unwrap()),
lformat!("'{key}''s path contains invalid \
return Err(Error::book_option(
Source::new(other.root.to_str().unwrap()),
lformat!(
"'{key}''s path contains invalid \
UTF-8 code",
key = key)));
key = key
),
));
};
self.options.insert(key.clone(), BookOption::Path(new_path));
} else {
@ -896,7 +982,6 @@ impl BookOptions {
Ok(())
}
/// Returns a description of all options valid to pass to a book.
///
/// # Arguments
@ -948,14 +1033,16 @@ impl BookOptions {
lformat!("not set")
};
if md {
out.push_str(&lformat!("- **`{key}`**
out.push_str(&lformat!(
"- **`{key}`**
- **type**: {option_type}
- **default value**: `{default}`
- {comment}\n",
key = key.unwrap(),
option_type = o_type,
default = def,
comment = comment));
key = key.unwrap(),
option_type = o_type,
default = def,
comment = comment
));
} else {
out.push_str(&format!("{key}
{type} {option_type} ({msg} {default})
@ -972,10 +1059,12 @@ impl BookOptions {
}
/// OPTIONS to a vec of tuples (comment, key, type, default value)
fn options_to_vec
()
-> Vec<(&'static str, Option<&'static str>, Option<&'static str>, Option<&'static str>)>
{
fn options_to_vec() -> Vec<(
&'static str,
Option<&'static str>,
Option<&'static str>,
Option<&'static str>,
)> {
let mut out = vec![];
for line in OPTIONS.lines() {
let line = line.trim();

View File

@ -17,10 +17,9 @@
//! This module contains the `Cleaner` traits and various implementations of it.
use std::borrow::Cow;
use crowbook_text_processing::clean;
use crowbook_text_processing::FrenchFormatter;
use std::borrow::Cow;
/// Contains cleaning parameters
pub struct CleanerParams {
@ -58,9 +57,7 @@ pub struct Default {
impl Default {
/// New Default cleaner
pub fn new(params: CleanerParams) -> Default {
Default {
params: params,
}
Default { params: params }
}
}
@ -99,12 +96,12 @@ impl French {
};
this.formatter.typographic_quotes(this.params.smart_quotes);
this.formatter.ligature_dashes(this.params.ligature_dashes);
this.formatter.ligature_guillemets(this.params.ligature_guillemets);
this.formatter
.ligature_guillemets(this.params.ligature_guillemets);
this
}
}
impl Cleaner for French {
/// Puts non breaking spaces before/after `:`, `;`, `?`, `!`, `«`, `»`, `—`
fn clean<'a>(&self, s: Cow<'a, str>) -> Cow<'a, str> {

View File

@ -15,32 +15,35 @@
// 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 crate::error::{Error, Result, Source};
use crate::token::Token;
use crate::html::HtmlRenderer;
use crate::book::{Book, compile_str};
use crate::book::Header;
use crate::book::{compile_str, Book};
use crate::book_renderer::BookRenderer;
use crate::error::{Error, Result, Source};
use crate::html::HtmlRenderer;
use crate::lang;
use crate::parser::Parser;
use crate::renderer::Renderer;
use crate::resource_handler;
use crate::templates::epub::*;
use crate::templates::epub3;
use crate::resource_handler;
use crate::renderer::Renderer;
use crate::parser::Parser;
use crate::lang;
use crate::book_renderer::BookRenderer;
use crate::text_view::view_as_text;
use crate::token::Token;
use mustache::Template;
use crowbook_text_processing::escape;
use epub_builder::{EpubBuilder, EpubVersion, EpubContent, ZipCommand, ZipLibrary, ZipCommandOrLibrary, ReferenceType};
use epub_builder::{
EpubBuilder, EpubContent, EpubVersion, ReferenceType, ZipCommand, ZipCommandOrLibrary,
ZipLibrary,
};
use mustache::Template;
use std::io::Write;
use std::convert::{AsRef, AsMut};
use mime_guess;
use std::borrow::Cow;
use std::convert::{AsMut, AsRef};
use std::fs;
use std::fs::File;
use std::path::Path;
use std::borrow::Cow;
use std::io::Write;
use std::mem;
use mime_guess;
use std::path::Path;
/// Renderer for Epub
///
@ -55,10 +58,12 @@ pub struct EpubRenderer<'a> {
impl<'a> EpubRenderer<'a> {
/// Creates a new Epub renderer
pub fn new(book: &'a Book) -> Result<EpubRenderer<'a>> {
let mut html = HtmlRenderer::new(book,
book.options
.get_str("epub.highlight.theme")
.unwrap_or_else(|_| book.options.get_str("rendering.highlight.theme").unwrap()))?;
let mut html = HtmlRenderer::new(
book,
book.options
.get_str("epub.highlight.theme")
.unwrap_or_else(|_| book.options.get_str("rendering.highlight.theme").unwrap()),
)?;
html.handler.set_images_mapping(true);
html.handler.set_base64(false);
Ok(EpubRenderer {
@ -73,12 +78,20 @@ impl<'a> EpubRenderer<'a> {
pub fn render_book(&mut self, to: &mut dyn Write) -> Result<String> {
// Initialize the EPUB builder
let mut zip = ZipCommand::new_in(self.html.book.options.get_path("crowbook.temp_dir")?)?;
zip.command(self.html.book.options.get_str("crowbook.zip.command")
.unwrap());
zip.command(
self.html
.book
.options
.get_str("crowbook.zip.command")
.unwrap(),
);
let wrapper = if zip.test().is_ok() {
ZipCommandOrLibrary::Command(zip)
} else {
warn!("{}", lformat!("Could not run zip command, falling back to zip library"));
warn!(
"{}",
lformat!("Could not run zip command, falling back to zip library")
);
ZipCommandOrLibrary::Library(ZipLibrary::new()?)
};
let mut maker = EpubBuilder::new(wrapper)?;
@ -89,11 +102,16 @@ impl<'a> EpubRenderer<'a> {
let lang = self.html.book.options.get_str("lang").unwrap();
let toc_extras = self.html.book.options.get_bool("epub.toc.extras").unwrap();
maker.metadata("lang", lang)?;
maker.metadata("author", escape::html(self.html.book.options.get_str("author").unwrap()))?;
maker.metadata("title", escape::html(self.html.book.options.get_str("title").unwrap()))?;
maker.metadata(
"author",
escape::html(self.html.book.options.get_str("author").unwrap()),
)?;
maker.metadata(
"title",
escape::html(self.html.book.options.get_str("title").unwrap()),
)?;
maker.metadata("generator", "crowbook")?;
maker.metadata("toc_name", lang::get_str(lang,
"toc"))?;
maker.metadata("toc_name", lang::get_str(lang, "toc"))?;
if let Ok(subject) = self.html.book.options.get_str("subject") {
maker.metadata("subject", subject)?;
}
@ -116,7 +134,6 @@ impl<'a> EpubRenderer<'a> {
// }
// /* If toc will be rendered inline, add it... to the toc (yeah it's meta) */
// if self.html.book.options.get_bool("rendering.inline_toc").unwrap() == true {
// self.html.toc.add(1,
@ -125,16 +142,17 @@ impl<'a> EpubRenderer<'a> {
// "toc"));
// }
for (i, chapter) in self.html.book.chapters.iter().enumerate() {
self.html.handler.add_link(chapter.filename.as_str(), filenamer(i));
self.html
.handler
.add_link(chapter.filename.as_str(), filenamer(i));
}
// Write cover.xhtml (if needs be)
if self.html.book.options.get_path("cover").is_ok() {
let cover = self.render_cover()?;
let mut content = EpubContent::new("cover.xhtml", cover.as_bytes())
.reftype(ReferenceType::Cover);
let mut content =
EpubContent::new("cover.xhtml", cover.as_bytes()).reftype(ReferenceType::Cover);
if toc_extras {
content = content.title(lang::get_str(lang, "cover"));
}
@ -152,16 +170,22 @@ impl<'a> EpubRenderer<'a> {
maker.add_content(content)?;
}
if self.html.book.options.get_bool("rendering.inline_toc").unwrap() {
if self
.html
.book
.options
.get_bool("rendering.inline_toc")
.unwrap()
{
maker.inline_toc();
}
// Write chapters
let template_chapter =
compile_str(self.html.book.get_template("epub.chapter.xhtml")?.as_ref(),
&self.html.book.source,
"epub.chapter.xhtml")?;
let template_chapter = compile_str(
self.html.book.get_template("epub.chapter.xhtml")?.as_ref(),
&self.html.book.source,
"epub.chapter.xhtml",
)?;
let mut rendered = vec![];
for (i, chapter) in self.html.book.chapters.iter().enumerate() {
let n = chapter.number;
@ -191,11 +215,13 @@ impl<'a> EpubRenderer<'a> {
self.html.source = Source::empty();
// Render the CSS file and write it
let template_css =
compile_str(self.html.book.get_template("epub.css").unwrap().as_ref(),
&self.html.book.source,
"epub.css")?;
let mut data = self.html
let template_css = compile_str(
self.html.book.get_template("epub.css").unwrap().as_ref(),
&self.html.book.source,
"epub.css",
)?;
let mut data = self
.html
.book
.get_metadata(|s| self.render_vec(&Parser::new().parse_inline(s)?))?
.insert_bool(self.html.book.options.get_str("lang").unwrap(), true);
@ -214,10 +240,12 @@ impl<'a> EpubRenderer<'a> {
let f = fs::canonicalize(source)
.and_then(|f| File::open(f))
.map_err(|_| {
Error::file_not_found(&self.html.source,
lformat!("image or cover"),
source.to_owned())
})?;
Error::file_not_found(
&self.html.source,
lformat!("image or cover"),
source.to_owned(),
)
})?;
if cover.as_ref() == Ok(source) {
// Treat cover specially so it is properly tagged
maker.add_cover_image(dest, &f, self.get_format(dest))?;
@ -228,19 +256,30 @@ impl<'a> EpubRenderer<'a> {
// Write additional resources
if let Ok(list) = self.html.book.options.get_str_vec("resources.files") {
let base_path_files =
self.html.book.options.get_path("resources.base_path.files").unwrap();
let base_path_files = self
.html
.book
.options
.get_path("resources.base_path.files")
.unwrap();
let list = resource_handler::get_files(list, &base_path_files)?;
let data_path = Path::new(self.html.book.options.get_relative_path("resources.out_path")?);
let data_path = Path::new(
self.html
.book
.options
.get_relative_path("resources.out_path")?,
);
for path in list {
let abs_path = Path::new(&base_path_files).join(&path);
let f = fs::canonicalize(&abs_path)
.and_then(|f| File::open(f))
.map_err(|_| {
Error::file_not_found(&self.html.book.source,
lformat!("additional resource from resources.files"),
abs_path.to_string_lossy().into_owned())
})?;
Error::file_not_found(
&self.html.book.source,
lformat!("additional resource from resources.files"),
abs_path.to_string_lossy().into_owned(),
)
})?;
maker.add_resource(data_path.join(&path), &f, self.get_format(path.as_ref()))?;
}
}
@ -253,13 +292,16 @@ impl<'a> EpubRenderer<'a> {
/// Render the titlepgae
fn render_titlepage(&mut self) -> Result<String> {
let epub3 = self.html.book.options.get_i32("epub.version").unwrap() == 3;
let template = compile_str(if epub3 { epub3::TITLE } else { TITLE },
&self.html.book.source,
"title page")?;
let data = self.html
let template = compile_str(
if epub3 { epub3::TITLE } else { TITLE },
&self.html.book.source,
"title page",
)?;
let data = self
.html
.book
.get_metadata(|s| self.render_vec(&Parser::new().parse_inline(s)?))?
.build();
.build();
let mut res: Vec<u8> = vec![];
template.render_data(&mut res, &data)?;
match String::from_utf8(res) {
@ -273,26 +315,36 @@ impl<'a> EpubRenderer<'a> {
if let Ok(cover) = self.html.book.options.get_path("cover") {
// Check that cover can be found
if fs::metadata(&cover).is_err() {
return Err(Error::file_not_found(&self.html.book.source, lformat!("cover"), cover));
return Err(Error::file_not_found(
&self.html.book.source,
lformat!("cover"),
cover,
));
}
let epub3 = self.html.book.options.get_i32("epub.version").unwrap() == 3;
let template = compile_str(if epub3 { epub3::COVER } else { COVER },
&self.html.book.source,
"cover.xhtml")?;
let data = self.html
let template = compile_str(
if epub3 { epub3::COVER } else { COVER },
&self.html.book.source,
"cover.xhtml",
)?;
let data = self
.html
.book
.get_metadata(|s| self.render_vec(&Parser::new().parse_inline(s)?))?
.insert_str("cover",
self.html
.handler
.map_image(&self.html.source, Cow::Owned(cover))?
.into_owned())
.insert_str(
"cover",
self.html
.handler
.map_image(&self.html.source, Cow::Owned(cover))?
.into_owned(),
)
.build();
let mut res: Vec<u8> = vec![];
template.render_data(&mut res, &data)?;
match String::from_utf8(res) {
Err(_) => panic!(lformat!("generated HTML for cover.xhtml was not utf-8 valid")),
Err(_) => panic!(lformat!(
"generated HTML for cover.xhtml was not utf-8 valid"
)),
Ok(res) => Ok(res),
}
} else {
@ -300,7 +352,6 @@ impl<'a> EpubRenderer<'a> {
}
}
/// Render a chapter
///
/// Return chapter content and raw title
@ -324,13 +375,15 @@ impl<'a> EpubRenderer<'a> {
header = Header::Chapter;
}
self.chapter_title = self.html
.book
.get_header(header, number, "".to_owned(), |s| {
self.render_vec(&Parser::new().parse_inline(s)?)
})?
self.chapter_title = self
.html
.book
.get_header(header, number, "".to_owned(), |s| {
self.render_vec(&Parser::new().parse_inline(s)?)
})?
.text;
self.chapter_title_raw = self.html
self.chapter_title_raw = self
.html
.book
.get_header(header, number, "".to_owned(), |s| {
Ok(view_as_text(&Parser::new().parse_inline(s)?))
@ -339,21 +392,26 @@ impl<'a> EpubRenderer<'a> {
}
self.toc.push(self.chapter_title.clone());
let data = self.html
let data = self
.html
.book
.get_metadata(|s| self.render_vec(&Parser::new().parse_inline(s)?))?
.insert_str("content", content)
.insert_str("chapter_title_raw",
self.chapter_title_raw.clone())
.insert_str("chapter_title",
mem::replace(&mut self.chapter_title, String::new()))
.insert_str("chapter_title_raw", self.chapter_title_raw.clone())
.insert_str(
"chapter_title",
mem::replace(&mut self.chapter_title, String::new()),
)
.build();
self.chapter_title = String::new();
let mut res: Vec<u8> = vec![];
template.render_data(&mut res, &data)?;
match String::from_utf8(res) {
Err(_) => panic!(lformat!("generated HTML was not utf-8 valid")),
Ok(res) => Ok((res, mem::replace(&mut self.chapter_title_raw, String::new())))
Ok(res) => Ok((
res,
mem::replace(&mut self.chapter_title_raw, String::new()),
)),
}
}
@ -364,10 +422,15 @@ impl<'a> EpubRenderer<'a> {
self.chapter_title = self.html.render_vec(vec)?;
self.chapter_title_raw = view_as_text(vec);
} else {
warn!("{}", lformat!("EPUB ({source}): detected two chapter titles inside the \
warn!(
"{}",
lformat!(
"EPUB ({source}): detected two chapter titles inside the \
same markdown file, in a file where chapter titles are \
not even rendered.",
source = self.html.source));
source = self.html.source
)
);
}
} else {
let header;
@ -379,34 +442,40 @@ impl<'a> EpubRenderer<'a> {
header = Header::Chapter;
number = self.html.current_chapter[1] + 1;
};
let res = self.html.book.get_header(header,
number,
self.html.render_vec(vec)?,
|s| {
self.render_vec(&(Parser::new()
.parse_inline(s)?))
});
let res = self
.html
.book
.get_header(header, number, self.html.render_vec(vec)?, |s| {
self.render_vec(&(Parser::new().parse_inline(s)?))
});
let s = res?;
if self.chapter_title.is_empty() {
self.chapter_title = s.text;
self.chapter_title_raw = self.html
self.chapter_title_raw = self
.html
.book
.get_header(header,
number,
view_as_text(vec),
|s| {
Ok(view_as_text(&(Parser::new()
.parse_inline(s)?)))
})?
.get_header(header, number, view_as_text(vec), |s| {
Ok(view_as_text(&(Parser::new().parse_inline(s)?)))
})?
.text;
} else {
warn!("{}", lformat!("EPUB ({source}): detected two chapters inside the same \
warn!(
"{}",
lformat!(
"EPUB ({source}): detected two chapters inside the same \
markdown file.",
source = self.html.source));
warn!("{}", lformat!("EPUB ({source}): conflict between: {title1} and {title2}",
source = self.html.source,
title1 = self.chapter_title,
title2 = s));
source = self.html.source
)
);
warn!(
"{}",
lformat!(
"EPUB ({source}): conflict between: {title1} and {title2}",
source = self.html.source,
title1 = self.chapter_title,
title2 = s
)
);
}
}
Ok(())
@ -418,9 +487,14 @@ impl<'a> EpubRenderer<'a> {
match opt {
Some(s) => s.to_string(),
None => {
error!("{}", lformat!("EPUB: could not guess the format of {file} based on \
error!(
"{}",
lformat!(
"EPUB: could not guess the format of {file} based on \
extension. Assuming png.",
file = s));
file = s
)
);
String::from("png")
}
}
@ -434,8 +508,12 @@ impl<'a> EpubRenderer<'a> {
/// See http://lise-henry.github.io/articles/rust_inheritance.html
#[doc(hidden)]
pub fn static_render_token<T>(this: &mut T, token: &Token) -> Result<String>
where T: AsMut<EpubRenderer<'a>>+AsRef<EpubRenderer<'a>> +
AsMut<HtmlRenderer<'a>>+AsRef<HtmlRenderer<'a>> + Renderer
where
T: AsMut<EpubRenderer<'a>>
+ AsRef<EpubRenderer<'a>>
+ AsMut<HtmlRenderer<'a>>
+ AsRef<HtmlRenderer<'a>>
+ Renderer,
{
match *token {
Token::Str(ref text) => {
@ -450,10 +528,15 @@ impl<'a> EpubRenderer<'a> {
if html.book.options.get_bool("rendering.initials").unwrap() {
// Use initial
let mut chars = content.chars();
let initial = chars.next()
.ok_or_else(|| Error::parser(&html.book.source,
lformat!("empty str token, could not find \
initial")))?;
let initial = chars.next().ok_or_else(|| {
Error::parser(
&html.book.source,
lformat!(
"empty str token, could not find \
initial"
),
)
})?;
let mut new_content = if initial.is_alphanumeric() {
format!("<span class = \"initial\">{}</span>", initial)
} else {
@ -474,7 +557,7 @@ impl<'a> EpubRenderer<'a> {
content = escape::nb_spaces_html(content);
}
Ok(content.into_owned())
},
}
Token::Header(1, ref vec) => {
{
let epub: &mut EpubRenderer = this.as_mut();
@ -487,32 +570,38 @@ impl<'a> EpubRenderer<'a> {
.book
.options
.get_i32("epub.version")
.unwrap() == 3;
.unwrap()
== 3;
Ok(format!("<a {} href = \"#note-dest-{}\"><sup id = \
Ok(format!(
"<a {} href = \"#note-dest-{}\"><sup id = \
\"note-source-{}\">[{}]</sup></a>",
if epub3 { "epub:type = \"noteref\"" } else { "" },
reference,
reference,
reference))
if epub3 { "epub:type = \"noteref\"" } else { "" },
reference,
reference,
reference
))
}
Token::FootnoteDefinition(ref reference, ref vec) => {
let epub3 = (this.as_ref() as &HtmlRenderer)
.book
.options
.get_i32("epub.version")
.unwrap() == 3;
.unwrap()
== 3;
let inner_content = this.render_vec(vec)?;
let html: &mut HtmlRenderer = this.as_mut();
let note_number = format!("<p class = \"note-number\">
let note_number = format!(
"<p class = \"note-number\">
<a href = \"#note-source-{}\">[{}]</a>
</p>\n",
reference,
reference);
reference, reference
);
let inner = if epub3 {
format!("<aside epub:type = \"footnote\" id = \"note-dest-{}\">{}</aside>",
reference,
inner_content)
format!(
"<aside epub:type = \"footnote\" id = \"note-dest-{}\">{}</aside>",
reference, inner_content
)
} else {
format!("<a id = \"note-dest-{}\" />{}", reference, inner_content)
};
@ -525,14 +614,12 @@ impl<'a> EpubRenderer<'a> {
}
}
/// Generate a file name given an int
fn filenamer(i: usize) -> String {
format!("chapter_{:03}.xhtml", i)
}
derive_html!{EpubRenderer<'a>, EpubRenderer::static_render_token}
derive_html! {EpubRenderer<'a>, EpubRenderer::static_render_token}
pub struct Epub {}
@ -542,8 +629,7 @@ impl BookRenderer for Epub {
}
fn render(&self, book: &Book, to: &mut dyn Write) -> Result<()> {
EpubRenderer::new(book)?
.render_book(to)?;
EpubRenderer::new(book)?.render_book(to)?;
Ok(())
}
}

View File

@ -15,13 +15,13 @@
// 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 mustache;
use epub_builder;
use mustache;
use std::error;
use std::result;
use std::fmt;
use std::borrow::Cow;
use std::error;
use std::fmt;
use std::result;
use std::string::FromUtf8Error;
#[derive(Debug, PartialEq, Clone)]
@ -145,11 +145,15 @@ impl Error {
/// * source: the source of the error.
/// * msg: description of why the file was needed.
/// * file: file name that wasn't found.
pub fn file_not_found<S1: Into<Cow<'static, str>>, S2: Into<Cow<'static, str>>, O: Into<Source>>
(source: O,
msg: S1,
file: S2)
-> Error {
pub fn file_not_found<
S1: Into<Cow<'static, str>>,
S2: Into<Cow<'static, str>>,
O: Into<Source>,
>(
source: O,
msg: S1,
file: S2,
) -> Error {
Error {
source: source.into(),
inner: Inner::FileNotFound(msg.into(), file.into()),
@ -280,15 +284,15 @@ impl Error {
impl error::Error for Error {
fn description(&self) -> &str {
match self.inner {
Inner::Default(ref s) |
Inner::Parser(ref s) |
Inner::Zipper(ref s) |
Inner::BookOption(ref s) |
Inner::ConfigParser(ref s) |
Inner::InvalidOption(ref s) |
Inner::Render(ref s) |
Inner::Template(ref s) |
Inner::GrammarCheck(ref s) => s.as_ref(),
Inner::Default(ref s)
| Inner::Parser(ref s)
| Inner::Zipper(ref s)
| Inner::BookOption(ref s)
| Inner::ConfigParser(ref s)
| Inner::InvalidOption(ref s)
| Inner::Render(ref s)
| Inner::Template(ref s)
| Inner::GrammarCheck(ref s) => s.as_ref(),
Inner::FileNotFound(..) => "File not found",
}
@ -309,31 +313,40 @@ impl fmt::Display for Error {
match self.inner {
Inner::Default(ref s) => write!(f, "{}", s),
Inner::GrammarCheck(ref s) => {
write!(f,
"{}",
lformat!("Error while trying to check grammar: {error}",
error = s))
write!(
f,
"{}",
lformat!("Error while trying to check grammar: {error}", error = s)
)
}
Inner::Parser(ref s) => {
write!(f,
"{}",
lformat!("Error parsing markdown: {error}", error = s))
write!(
f,
"{}",
lformat!("Error parsing markdown: {error}", error = s)
)
}
Inner::ConfigParser(ref s) => {
f.write_str(&lformat!("Error parsing configuration file: "))?;
f.write_str(s)
}
Inner::FileNotFound(ref description, ref file) => {
write!(f,
"{}",
lformat!("Could not find file '{file}' for {description}",
file = file,
description = description))
write!(
f,
"{}",
lformat!(
"Could not find file '{file}' for {description}",
file = file,
description = description
)
)
}
Inner::Template(ref s) => {
write!(f,
"{}",
lformat!("Error compiling template: {template}", template = s))
write!(
f,
"{}",
lformat!("Error compiling template: {template}", template = s)
)
}
Inner::Render(ref s) => {
f.write_str(&lformat!("Error during rendering: "))?;
@ -359,37 +372,38 @@ impl fmt::Display for Error {
/// Crowbook's Result type, used by many methods that can fail
pub type Result<T> = result::Result<T, Error>;
/// Implement our Error from mustache::Error
impl From<mustache::Error> for Error {
fn from(err: mustache::Error) -> Error {
Error::template(Source::empty(),
format!("{}", err))
Error::template(Source::empty(), format!("{}", err))
}
}
/// Implement our error from epub_maker::Error
impl From<epub_builder::Error> for Error {
fn from(err: epub_builder::Error) -> Error {
Error::render(Source::empty(),
lformat!("error during EPUB generation: {error}",
error = err))
Error::render(
Source::empty(),
lformat!("error during EPUB generation: {error}", error = err),
)
}
}
impl From<FromUtf8Error> for Error {
fn from(err: FromUtf8Error) -> Error {
Error::render(Source::empty(),
lformat!("UTF-8 error: {error}",
error = err))
Error::render(
Source::empty(),
lformat!("UTF-8 error: {error}", error = err),
)
}
}
impl From<fmt::Error> for Error {
fn from(err: fmt::Error) -> Error {
Error::default(Source::empty(),
lformat!("format error: {error}",
error = err))
Error::default(
Source::empty(),
lformat!("format error: {error}", error = err),
)
}
}

View File

@ -15,19 +15,18 @@
// You should have received a copy of the GNU Lesser General Public License
// along with Crowbook. If not, see <http://www.gnu.org/licenses/>.
use serde_json;
use reqwest;
use url::form_urlencoded;
use rayon::prelude::*;
use reqwest;
use serde_json;
use url::form_urlencoded;
use std::io::Read;
use crate::text_view::view_as_text;
use crate::text_view::insert_annotation;
use crate::token::Token;
use crate::token::Data;
use crate::error::{Error, Result, Source};
use crate::text_view::insert_annotation;
use crate::text_view::view_as_text;
use crate::token::Data;
use crate::token::Token;
/// Represents a grammar error from Grammalecte
///
@ -68,25 +67,37 @@ impl GrammalecteChecker {
pub fn new<S: Into<String>>(port: usize, lang: S) -> Result<GrammalecteChecker> {
let lang = lang.into();
if !lang.starts_with("fr") {
return Err(Error::grammar_check(Source::empty(), lformat!("grammalecte only works with 'fr' lang")));
return Err(Error::grammar_check(
Source::empty(),
lformat!("grammalecte only works with 'fr' lang"),
));
}
let checker = GrammalecteChecker {
port: port,
client: reqwest::blocking::Client::new(),
};
let res = checker.client
let res = checker
.client
.get(&format!("http://localhost:{}", port))
.send()
.map_err(|e| {
Error::grammar_check(Source::empty(),
lformat!("could not connect to grammalecte server: {error}",
error = e))
Error::grammar_check(
Source::empty(),
lformat!(
"could not connect to grammalecte server: {error}",
error = e
),
)
})?;
if !res.status().is_success() {
return Err(Error::grammar_check(Source::empty(),
lformat!("server didn't respond with a OK status \
code")));
return Err(Error::grammar_check(
Source::empty(),
lformat!(
"server didn't respond with a OK status \
code"
),
));
}
Ok(checker)
}
@ -95,40 +106,51 @@ impl GrammalecteChecker {
fn check(&self, text: &str) -> Result<GrammalecteCheck> {
let query: String = form_urlencoded::Serializer::new(String::new())
.append_pair("text", text)
.append_pair("options", "{\"apos\": false, \"nbsp\": false, \"esp\": false}")
.append_pair(
"options",
"{\"apos\": false, \"nbsp\": false, \"esp\": false}",
)
.finish();
let mut res = self.client.post(&format!("http://localhost:{}/gc_text/fr", self.port))
let mut res = self
.client
.post(&format!("http://localhost:{}/gc_text/fr", self.port))
.body(query)
.send()
.map_err(|e| {
Error::grammar_check(Source::empty(),
lformat!("could not send request to server: {error}",
error = e))
Error::grammar_check(
Source::empty(),
lformat!("could not send request to server: {error}", error = e),
)
})?;
if !res.status().is_success() {
return Err(Error::grammar_check(Source::empty(),
lformat!("server didn't respond with a OK status \
code")));
return Err(Error::grammar_check(
Source::empty(),
lformat!(
"server didn't respond with a OK status \
code"
),
));
}
let mut s = String::new();
res.read_to_string(&mut s)
.map_err(|e| {
Error::grammar_check(Source::empty(),
lformat!("could not read response: {error}", error = e))
})?;
let reponse: GrammalecteCheck = serde_json::from_str(&s)
.map_err(|e| {
Error::default(Source::empty(),
lformat!("could not decode JSON: {error}", error = e))
})?;
res.read_to_string(&mut s).map_err(|e| {
Error::grammar_check(
Source::empty(),
lformat!("could not read response: {error}", error = e),
)
})?;
let reponse: GrammalecteCheck = serde_json::from_str(&s).map_err(|e| {
Error::default(
Source::empty(),
lformat!("could not decode JSON: {error}", error = e),
)
})?;
Ok(reponse)
}
}
impl GrammalecteChecker {
/// Check the grammar in a vector of tokens.
///

View File

@ -15,18 +15,18 @@
// You should have received a copy of the GNU Lesser General Public License
// along with Crowbook. If not, see <http://www.gnu.org/licenses/>.
use serde_json;
use reqwest;
use url::form_urlencoded;
use rayon::prelude::*;
use reqwest;
use serde_json;
use url::form_urlencoded;
use std::io::Read;
use crate::text_view::view_as_text;
use crate::text_view::insert_annotation;
use crate::token::Token;
use crate::token::Data;
use crate::error::{Error, Result, Source};
use crate::text_view::insert_annotation;
use crate::text_view::view_as_text;
use crate::token::Data;
use crate::token::Token;
/// Represents a grammar error from language tool
///
@ -66,18 +66,27 @@ impl GrammarChecker {
client: reqwest::blocking::Client::new(),
};
let res = checker.client
let res = checker
.client
.get(&format!("http://localhost:{}/v2/languages", port))
.send()
.map_err(|e| {
Error::grammar_check(Source::empty(),
lformat!("could not connect to language tool server: {error}",
error = e))
Error::grammar_check(
Source::empty(),
lformat!(
"could not connect to language tool server: {error}",
error = e
),
)
})?;
if !res.status().is_success() {
return Err(Error::grammar_check(Source::empty(),
lformat!("server didn't respond with a OK status \
code")));
return Err(Error::grammar_check(
Source::empty(),
lformat!(
"server didn't respond with a OK status \
code"
),
));
}
Ok(checker)
}
@ -89,62 +98,71 @@ impl GrammarChecker {
.append_pair("text", text)
.finish();
let mut res = self.client.post(&format!("http://localhost:{}/v2/check", self.port))
let mut res = self
.client
.post(&format!("http://localhost:{}/v2/check", self.port))
.body(query)
.send()
.map_err(|e| {
Error::grammar_check(Source::empty(),
lformat!("could not send request to server: {error}",
error = e))
Error::grammar_check(
Source::empty(),
lformat!("could not send request to server: {error}", error = e),
)
})?;
if !res.status().is_success() {
return Err(Error::grammar_check(Source::empty(),
lformat!("server didn't respond with a OK status \
code")));
return Err(Error::grammar_check(
Source::empty(),
lformat!(
"server didn't respond with a OK status \
code"
),
));
}
let mut s = String::new();
res.read_to_string(&mut s)
.map_err(|e| {
Error::grammar_check(Source::empty(),
lformat!("could not read response: {error}", error = e))
})?;
let reponse: GrammarCheck = serde_json::from_str(&s)
.map_err(|e| {
Error::default(Source::empty(),
lformat!("could not decode JSON: {error}", error = e))
})?;
res.read_to_string(&mut s).map_err(|e| {
Error::grammar_check(
Source::empty(),
lformat!("could not read response: {error}", error = e),
)
})?;
let reponse: GrammarCheck = serde_json::from_str(&s).map_err(|e| {
Error::default(
Source::empty(),
lformat!("could not decode JSON: {error}", error = e),
)
})?;
Ok(reponse)
}
}
impl GrammarChecker {
/// Check the grammar in a vector of tokens.
///
/// This modifies the AST
pub fn check_chapter(&self, tokens: &mut Vec<Token>) -> Result<()> {
let res = tokens.par_iter_mut()
.map(|token| {
match *token {
Token::Paragraph(ref mut v) |
Token::Header(_, ref mut v) |
Token::BlockQuote(ref mut v) |
Token::List(ref mut v) |
Token::OrderedList(_, ref mut v) => {
let check = self.check(&view_as_text(v))?;
for error in check.matches {
insert_annotation(v,
&Data::GrammarError(error.message.clone()),
error.offset,
error.length);
}
Ok(())
},
_ => Ok(()),
let res = tokens
.par_iter_mut()
.map(|token| match *token {
Token::Paragraph(ref mut v)
| Token::Header(_, ref mut v)
| Token::BlockQuote(ref mut v)
| Token::List(ref mut v)
| Token::OrderedList(_, ref mut v) => {
let check = self.check(&view_as_text(v))?;
for error in check.matches {
insert_annotation(
v,
&Data::GrammarError(error.message.clone()),
error.offset,
error.length,
);
}
Ok(())
}
_ => Ok(()),
})
.find_any(|r| r.is_err());
if let Some(err) = res {

View File

@ -15,29 +15,29 @@
// 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 crate::error::{Result, Error, Source};
use crate::token::Token;
use crate::token::Data;
use crate::book::{Book, compile_str};
use crate::book::Header;
use crate::book::HeaderData;
use crate::number::Number;
use crate::resource_handler::ResourceHandler;
use crate::renderer::Renderer;
use crate::parser::Parser;
use crate::syntax::Syntax;
use crate::book::{compile_str, Book};
use crate::error::{Error, Result, Source};
use crate::lang;
use crate::number::Number;
use crate::parser::Parser;
use crate::renderer::Renderer;
use crate::resource_handler::ResourceHandler;
use crate::syntax::Syntax;
use crate::token::Data;
use crate::token::Token;
use std::borrow::Cow;
use std::convert::{AsMut, AsRef};
use std::fmt::Write;
use crowbook_text_processing::escape;
use numerals::roman::Roman;
use epub_builder::Toc;
use epub_builder::TocElement;
use mustache::Template;
use mustache::MapBuilder;
use mustache::Template;
use numerals::roman::Roman;
#[derive(Debug, PartialEq, Copy, Clone)]
/// If/how to highlight code
@ -47,7 +47,6 @@ pub enum Highlight {
Syntect,
}
/// Base structure for rendering HTML files
///
/// Used by EpubRenderer, HtmlSingleRenderer, HtmlDirRenderer
@ -123,12 +122,14 @@ impl<'a> HtmlRenderer<'a> {
} else {
(Highlight::None, None)
}
},
}
"none" => (Highlight::None, None),
"highlight.js" => (Highlight::Js, None),
value => {
error!("{}", lformat!("rendering.highlight set to '{}', not a valid value",
value));
error!(
"{}",
lformat!("rendering.highlight set to '{}', not a valid value", value)
);
(Highlight::None, None)
}
}
@ -159,17 +160,16 @@ impl<'a> HtmlRenderer<'a> {
proofread: false,
syntax: syntax,
highlight: highlight,
part_template_html: compile_str(book.options
.get_str("html.part.template")
.unwrap(),
Source::empty(),
"html.part.template")?,
chapter_template_html: compile_str(book.options
.get_str("html.chapter.template")
.unwrap(),
Source::empty(),
"html.chapter.template")?,
part_template_html: compile_str(
book.options.get_str("html.part.template").unwrap(),
Source::empty(),
"html.part.template",
)?,
chapter_template_html: compile_str(
book.options.get_str("html.chapter.template").unwrap(),
Source::empty(),
"html.chapter.template",
)?,
};
html.handler.set_images_mapping(true);
html.handler.set_base64(true);
@ -195,7 +195,7 @@ impl<'a> HtmlRenderer<'a> {
Number::Specified(n) => {
self.current_numbering = book_numbering;
self.current_chapter[1] = n - 1;
},
}
Number::SpecifiedPart(n) => {
self.current_numbering = book_numbering;
self.current_chapter[0] = n - 1;
@ -203,7 +203,7 @@ impl<'a> HtmlRenderer<'a> {
Number::Hidden => {
self.current_numbering = 0;
self.current_hide = true;
},
}
} // _ => panic!("Parts are not supported yet"),
self.current_part = n.is_part();
@ -212,7 +212,8 @@ impl<'a> HtmlRenderer<'a> {
/// Renders a chapter to HTML
pub fn render_html<T>(this: &mut T, tokens: &[Token], render_end_notes: bool) -> Result<String>
where T: AsMut<HtmlRenderer<'a>> + AsRef<HtmlRenderer<'a>> + Renderer
where
T: AsMut<HtmlRenderer<'a>> + AsRef<HtmlRenderer<'a>> + Renderer,
{
let mut res = String::new();
for token in tokens {
@ -228,11 +229,7 @@ impl<'a> HtmlRenderer<'a> {
/// Renders a title (without `<h1>` tags), increasing header number beforehand
#[doc(hidden)]
pub fn render_title(&mut self, n: i32, vec: &[Token]) -> Result<HeaderData> {
let n = if self.current_part {
n -1
} else {
n
};
let n = if self.current_part { n - 1 } else { n };
self.inc_header(n);
let number = self.current_chapter[n as usize];
@ -244,11 +241,10 @@ impl<'a> HtmlRenderer<'a> {
} else {
Header::Chapter
};
self.book
.get_header(header, number, c_title, |s| {
let mut parser = Parser::from(&self.book);
self.render_vec(&parser.parse_inline(s)?)
})
self.book.get_header(header, number, c_title, |s| {
let mut parser = Parser::from(&self.book);
self.render_vec(&parser.parse_inline(s)?)
})
} else if self.current_numbering >= n {
let numbers = self.get_numbers();
Ok(HeaderData {
@ -294,11 +290,10 @@ impl<'a> HtmlRenderer<'a> {
Ok(String::from_utf8(res)?)
}
} else {
Ok(format!("<h{} id = \"link-{}\">{}</h{}>\n",
n,
self.link_number,
data.text,
n))
Ok(format!(
"<h{} id = \"link-{}\">{}</h{}>\n",
n, self.link_number, data.text, n
))
}
}
@ -314,7 +309,13 @@ impl<'a> HtmlRenderer<'a> {
let n = n as usize;
assert!(n < self.current_chapter.len());
self.current_chapter[n] += 1;
let begin = if n == 0 && !self.book.options.get_bool("rendering.part.reset_counter").unwrap() {
let begin = if n == 0
&& !self
.book
.options
.get_bool("rendering.part.reset_counter")
.unwrap()
{
n + 2
} else {
n + 1
@ -334,35 +335,48 @@ impl<'a> HtmlRenderer<'a> {
if i == self.current_chapter.len() - 1 {
break;
}
let bools: Vec<_> = self.current_chapter[i + 1..].iter().map(|x| *x != 0).collect();
let bools: Vec<_> = self.current_chapter[i + 1..]
.iter()
.map(|x| *x != 0)
.collect();
if !bools.contains(&true) {
break;
}
}
if i != 1 || !self.book.options.get_bool("rendering.chapter.roman_numerals").unwrap() {
if i != 1
|| !self
.book
.options
.get_bool("rendering.chapter.roman_numerals")
.unwrap()
{
write!(output, "{}.", self.current_chapter[i]).unwrap(); //todo
} else if self.current_chapter[i] >= 1 {
write!(output,
"{:X}.",
Roman::from(self.current_chapter[i] as i16)).unwrap();
write!(output, "{:X}.", Roman::from(self.current_chapter[i] as i16)).unwrap();
} else {
error!("{}", lformat!("can not use roman numerals with zero or negative chapter numbers ({n})",
n = self.current_chapter[i]));
error!(
"{}",
lformat!(
"can not use roman numerals with zero or negative chapter numbers ({n})",
n = self.current_chapter[i]
)
);
}
}
output
}
/// Display side notes if option is to true
#[doc(hidden)]
pub fn render_side_notes(&mut self, res: &mut String) {
if self.book.options.get_bool("html.side_notes").unwrap() {
for (note_number, footnote) in self.footnotes.drain(..) {
write!(res,
"<div class = \"sidenote\">\n{} {}\n</div>\n",
note_number,
footnote).unwrap();
write!(
res,
"<div class = \"sidenote\">\n{} {}\n</div>\n",
note_number, footnote
)
.unwrap();
}
}
}
@ -371,7 +385,6 @@ impl<'a> HtmlRenderer<'a> {
#[doc(hidden)]
pub fn render_end_notes(&mut self, res: &mut String) {
if !self.footnotes.is_empty() {
// for (note_number, footnote) in self.footnotes.drain(..) {
// res.push_str(&format!("<div class = \"note\">
// <p>{}</p>
@ -381,16 +394,18 @@ impl<'a> HtmlRenderer<'a> {
// footnote));
// }
write!(res,
"<div class = \"notes\">
write!(
res,
"<div class = \"notes\">
<h2 class = \"notes\">{}</h2>\n",
lang::get_str(self.book.options.get_str("lang").unwrap(),
"notes")).unwrap();
lang::get_str(self.book.options.get_str("lang").unwrap(), "notes")
)
.unwrap();
res.push_str("<table class = \"notes\">\n");
for (note_number, footnote) in self.footnotes.drain(..) {
write!(res,
"<tr class = \"notes\">
write!(
res,
"<tr class = \"notes\">
<td class = \"note-number\">
{}
</td>
@ -398,8 +413,9 @@ impl<'a> HtmlRenderer<'a> {
{}
</td>
</tr>\n",
note_number,
footnote).unwrap();
note_number, footnote
)
.unwrap();
}
res.push_str("</table>\n");
res.push_str("</div>\n");
@ -414,30 +430,33 @@ impl<'a> HtmlRenderer<'a> {
/// See http://lise-henry.github.io/articles/rust_inheritance.html
#[doc(hidden)]
pub fn static_render_token<T>(this: &mut T, token: &Token) -> Result<String>
where T: AsMut<HtmlRenderer<'a>> + AsRef<HtmlRenderer<'a>> + Renderer
where
T: AsMut<HtmlRenderer<'a>> + AsRef<HtmlRenderer<'a>> + Renderer,
{
match *token {
Token::Annotation(ref annotation, ref v) => {
let content = this.as_mut().render_vec(v)?;
if this.as_ref().proofread {
match *annotation {
Data::GrammarError(ref s) => {
Ok(format!("<span title = \"{}\" class = \"grammar-error\">{}</span>",
escape::quotes(s.as_str()),
content))
}
Data::GrammarError(ref s) => Ok(format!(
"<span title = \"{}\" class = \"grammar-error\">{}</span>",
escape::quotes(s.as_str()),
content
)),
Data::Repetition(ref colour) => {
if !this.as_ref().verbatim {
Ok(format!("<span class = \"repetition\" \
Ok(format!(
"<span class = \"repetition\" \
style = \"text-decoration-line: underline; \
text-decoration-style: wavy; \
text-decoration-color: {colour}\">{content}</span>",
colour = colour,
content = content))
colour = colour,
content = content
))
} else {
Ok(content)
}
},
}
_ => unreachable!(),
}
} else {
@ -454,7 +473,13 @@ impl<'a> HtmlRenderer<'a> {
this.as_mut().first_letter = false;
}
if this.as_ref().book.options.get_bool("html.escape_nb_spaces").unwrap() {
if this
.as_ref()
.book
.options
.get_bool("html.escape_nb_spaces")
.unwrap()
{
content = escape::nb_spaces_html(content);
}
Ok(content.into_owned())
@ -467,8 +492,14 @@ impl<'a> HtmlRenderer<'a> {
this.as_mut().first_letter = true;
}
}
let class = if this.as_ref().first_letter &&
this.as_ref().book.options.get_bool("rendering.initials").unwrap() {
let class = if this.as_ref().first_letter
&& this
.as_ref()
.book
.options
.get_bool("rendering.initials")
.unwrap()
{
" class = \"first-para\""
} else {
""
@ -476,47 +507,63 @@ impl<'a> HtmlRenderer<'a> {
let content = this.render_vec(vec)?;
this.as_mut().current_par += 1;
let par = this.as_ref().current_par;
Ok(format!("<p id = \"para-{}\"{}>{}</p>\n", par, class, content))
Ok(format!(
"<p id = \"para-{}\"{}>{}</p>\n",
par, class, content
))
}
Token::Header(n, ref vec) => {
let data = this.as_mut().render_title(n, vec)?;
if n <= this.as_ref().book.options.get_i32("rendering.num_depth").unwrap() {
let url = format!("{}#link-{}",
this.as_ref().filename,
this.as_ref().link_number);
if n <= this
.as_ref()
.book
.options
.get_i32("rendering.num_depth")
.unwrap()
{
let url = format!(
"{}#link-{}",
this.as_ref().filename,
this.as_ref().link_number
);
if !this.as_ref().current_part {
this.as_mut().toc.add(TocElement::new(url, data.text.clone())
.level(n));
this.as_mut()
.toc
.add(TocElement::new(url, data.text.clone()).level(n));
} else {
this.as_mut().toc.add(TocElement::new(url, data.text.clone())
.level(n - 1));
this.as_mut()
.toc
.add(TocElement::new(url, data.text.clone()).level(n - 1));
}
}
Ok(this.as_mut().render_title_full(n, data)?)
}
Token::TaskItem(checked, ref vec) =>
Ok(format!("<input type = \"checkbox\" disabled = \"\" {}/>{}",
if checked { "checked = \"\"" } else { "" },
this.render_vec(vec)?)),
Token::TaskItem(checked, ref vec) => Ok(format!(
"<input type = \"checkbox\" disabled = \"\" {}/>{}",
if checked { "checked = \"\"" } else { "" },
this.render_vec(vec)?
)),
Token::Emphasis(ref vec) => Ok(format!("<em>{}</em>", this.render_vec(vec)?)),
Token::Strong(ref vec) => Ok(format!("<b>{}</b>", this.render_vec(vec)?)),
Token::Strikethrough(ref vec) => Ok(format!("<del>{}</del>", this.render_vec(vec)?)),
Token::Code(ref s) => Ok(format!("<code>{}</code>", escape::html(s))),
Token::Subscript(ref vec) => Ok(format!("<sub>{}</sub>", this.render_vec(vec)?)),
Token::Superscript(ref vec) => Ok(format!("<sup>{}</sup>", this.render_vec(vec)?)),
Token::BlockQuote(ref vec) => {
Ok(format!("<blockquote>{}</blockquote>\n", this.render_vec(vec)?))
}
Token::BlockQuote(ref vec) => Ok(format!(
"<blockquote>{}</blockquote>\n",
this.render_vec(vec)?
)),
Token::CodeBlock(ref language, ref s) => {
let output = if let Some(ref syntax) = this.as_ref().syntax {
syntax.to_html(s, language)?
} else if language.is_empty() {
format!("<pre><code>{}</code></pre>\n", s)
} else {
format!("<pre><code class = \"language-{}\">{}</code></pre>\n",
language,
escape::html(s))
format!(
"<pre><code class = \"language-{}\">{}</code></pre>\n",
language,
escape::html(s)
)
};
Ok(output)
}
@ -524,22 +571,22 @@ impl<'a> HtmlRenderer<'a> {
Token::SoftBreak => Ok(String::from(" ")),
Token::HardBreak => Ok(String::from("<br />\n")),
Token::List(ref vec) => Ok(format!("<ul>\n{}</ul>\n", this.render_vec(vec)?)),
Token::OrderedList(n, ref vec) => {
Ok(format!("<ol{}>\n{}</ol>\n",
if n == 1 {
String::new()
} else {
format!(" start = \"{}\"", n)
},
this.render_vec(vec)?))
}
Token::OrderedList(n, ref vec) => Ok(format!(
"<ol{}>\n{}</ol>\n",
if n == 1 {
String::new()
} else {
format!(" start = \"{}\"", n)
},
this.render_vec(vec)?
)),
Token::Item(ref vec) => Ok(format!("<li>{}</li>\n", this.render_vec(vec)?)),
Token::DescriptionList(ref v) => {
Ok(format!("<dl>
Token::DescriptionList(ref v) => Ok(format!(
"<dl>
{}
</dl>",
this.render_vec(v)?))
},
this.render_vec(v)?
)),
Token::DescriptionItem(ref v) => Ok(this.render_vec(v)?),
Token::DescriptionTerm(ref v) => Ok(format!("<dt>{}</dt>\n", this.render_vec(v)?)),
Token::DescriptionDetails(ref v) => Ok(format!("<dd>{}</dd>\n", this.render_vec(v)?)),
@ -551,43 +598,45 @@ impl<'a> HtmlRenderer<'a> {
url
};
Ok(format!("<a href = \"{}\"{}>{}</a>",
url,
if title.is_empty() {
String::new()
} else {
format!(" title = \"{}\"", title)
},
this.render_vec(vec)?))
Ok(format!(
"<a href = \"{}\"{}>{}</a>",
url,
if title.is_empty() {
String::new()
} else {
format!(" title = \"{}\"", title)
},
this.render_vec(vec)?
))
}
Token::Image(ref url, ref title, ref alt) |
Token::StandaloneImage(ref url, ref title, ref alt) => {
Token::Image(ref url, ref title, ref alt)
| Token::StandaloneImage(ref url, ref title, ref alt) => {
let content = this.render_vec(alt)?;
let html: &mut HtmlRenderer = this.as_mut();
let url = html.handler.map_image(&html.source, url.as_str())?;
if token.is_image() {
Ok(format!("<img src = \"{}\" title = \"{}\" alt = \"{}\" />",
url,
title,
content))
Ok(format!(
"<img src = \"{}\" title = \"{}\" alt = \"{}\" />",
url, title, content
))
} else {
Ok(format!("<div class = \"image\">
Ok(format!(
"<div class = \"image\">
<img src = \"{}\" title = \"{}\" alt = \
\"{}\" />
</div>",
url,
title,
content))
url, title, content
))
}
}
Token::Table(_, ref vec) => {
Ok(format!("<div class = \"table\">
Token::Table(_, ref vec) => Ok(format!(
"<div class = \"table\">
<table>\n{}
</table>
</div>\n",
this.render_vec(vec)?))
}
this.render_vec(vec)?
)),
Token::TableRow(ref vec) => Ok(format!("<tr>\n{}</tr>\n", this.render_vec(vec)?)),
Token::TableCell(ref vec) => {
let tag = if this.as_ref().table_head { "th" } else { "td" };
@ -599,23 +648,24 @@ impl<'a> HtmlRenderer<'a> {
this.as_mut().table_head = false;
Ok(format!("<tr>\n{}</tr>\n", s))
}
Token::FootnoteReference(ref reference) => {
Ok(format!("<a href = \"#note-dest-{}\"><sup id = \
Token::FootnoteReference(ref reference) => Ok(format!(
"<a href = \"#note-dest-{}\"><sup id = \
\"note-source-{}\">[{}]</sup></a>",
reference,
reference,
reference))
},
reference, reference, reference
)),
Token::FootnoteDefinition(ref reference, ref vec) => {
let note_number = format!("<p class = \"note-number\">
let note_number = format!(
"<p class = \"note-number\">
<a href = \"#note-source-{}\">[{}]</a>
</p>\n",
reference,
reference);
reference, reference
);
let inner = format!("<aside id = \"note-dest-{}\">{}</aside>",
reference,
this.render_vec(vec)?);
let inner = format!(
"<aside id = \"note-dest-{}\">{}</aside>",
reference,
this.render_vec(vec)?
);
this.as_mut().footnotes.push((note_number, inner));
Ok(String::new())
}
@ -627,8 +677,7 @@ impl<'a> HtmlRenderer<'a> {
fn templatize(&mut self, s: &str) -> Result<String> {
let mapbuilder = self.book.get_metadata(|s| Ok(s.to_owned()))?;
let data = mapbuilder.build();
let template =
compile_str(s, &self.book.source, "")?;
let template = compile_str(s, &self.book.source, "")?;
let mut res = vec![];
template.render_data(&mut res, &data)?;
Ok(String::from_utf8_lossy(&res).into_owned())
@ -637,13 +686,16 @@ impl<'a> HtmlRenderer<'a> {
/// Renders the toc name
#[doc(hidden)]
pub fn get_toc_name(&mut self) -> Result<String> {
let data = self.book
let data = self
.book
.get_metadata(|s| self.render_vec(&Parser::new().parse_inline(s)?))?
.build();
let template = self.book.options.get_str("rendering.inline_toc.name").unwrap();
let template = compile_str(template,
&self.book.source,
"rendering.inline_toc.name")?;
let template = self
.book
.options
.get_str("rendering.inline_toc.name")
.unwrap();
let template = compile_str(template, &self.book.source, "rendering.inline_toc.name")?;
let mut res = vec![];
template.render_data(&mut res, &data)?;
Ok(String::from_utf8_lossy(&res).into_owned())
@ -673,24 +725,27 @@ impl<'a> HtmlRenderer<'a> {
self.templatize(json)
}
/// Renders a footer, which can include a "Generated by Crowboook" link
/// or a customized text
#[doc(hidden)]
pub fn get_footer<T>(this: &mut T) -> Result<String>
where T: AsMut<HtmlRenderer<'a>> + AsRef<HtmlRenderer<'a>> + Renderer
where
T: AsMut<HtmlRenderer<'a>> + AsRef<HtmlRenderer<'a>> + Renderer,
{
let content = if let Ok(footer) = this.as_ref().book.options.get_str("html.footer") {
match this.as_mut().templatize(footer) {
Ok(content) => content,
Err(err) => {
return Err(Error::render(&this.as_ref().book.source,
lformat!("rendering 'html.footer' \
return Err(Error::render(
&this.as_ref().book.source,
lformat!(
"rendering 'html.footer' \
template:\n{error}",
error = err)))
error = err
),
))
}
}
} else {
String::new()
};
@ -706,20 +761,22 @@ impl<'a> HtmlRenderer<'a> {
/// Renders a header
#[doc(hidden)]
pub fn get_header<T>(this: &mut T) -> Result<String>
where T: AsMut<HtmlRenderer<'a>> + AsRef<HtmlRenderer<'a>> + Renderer
where
T: AsMut<HtmlRenderer<'a>> + AsRef<HtmlRenderer<'a>> + Renderer,
{
if let Ok(top) = this.as_ref().book.options.get_str("html.header") {
match this.as_mut().templatize(top) {
Ok(content) => {
let tokens = Parser::from(&this.as_ref().book).parse(&content)?;
Ok(format!("<div id = \"top\">{}</div>",
this.render_vec(&tokens)?))
}
Err(err) => {
Err(Error::render(&this.as_ref().book.source,
lformat!("rendering 'html.header' template:\n{error}",
error = err)))
Ok(format!(
"<div id = \"top\">{}</div>",
this.render_vec(&tokens)?
))
}
Err(err) => Err(Error::render(
&this.as_ref().book.source,
lformat!("rendering 'html.header' template:\n{error}", error = err),
)),
}
} else {
Ok(String::new())
@ -745,7 +802,6 @@ impl<'a> Renderer for HtmlRenderer<'a> {
}
}
/// This macro automatically generates AsRef and AsMut implementations
/// for a type, to itself and to HtmlRenderer. Type must have a .html element
/// and use a <'a> lifetime parameter.

View File

@ -15,28 +15,27 @@
// 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 crate::error::{Error, Result, Source};
use crate::html::HtmlRenderer;
use crate::html::Highlight;
use crate::book::{Book, compile_str};
use crate::token::Token;
use crate::templates::img;
use crate::resource_handler;
use crate::renderer::Renderer;
use crate::parser::Parser;
use crate::book::{compile_str, Book};
use crate::book_renderer::BookRenderer;
use crate::error::{Error, Result, Source};
use crate::html::Highlight;
use crate::html::HtmlRenderer;
use crate::parser::Parser;
use crate::renderer::Renderer;
use crate::resource_handler;
use crate::templates::img;
use crate::text_view::view_as_text;
use crate::token::Token;
use std::io;
use std::io::Read;
use std::borrow::Cow;
use std::convert::{AsMut, AsRef};
use std::fmt::Write;
use std::fs;
use std::fs::File;
use std::io;
use std::io::Read;
use std::path::Path;
use std::path::PathBuf;
use std::borrow::Cow;
use std::convert::{AsRef, AsMut};
/// Multiple files HTML renderer
///
@ -48,10 +47,12 @@ pub struct HtmlDirRenderer<'a> {
impl<'a> HtmlDirRenderer<'a> {
/// Creates a new HtmlDirRenderer
pub fn new(book: &'a Book) -> Result<HtmlDirRenderer<'a>> {
let mut html = HtmlRenderer::new(book,
book.options
.get_str("html.highlight.theme")
.unwrap_or_else(|_| book.options.get_str("rendering.highlight.theme").unwrap()))?;
let mut html = HtmlRenderer::new(
book,
book.options
.get_str("html.highlight.theme")
.unwrap_or_else(|_| book.options.get_str("rendering.highlight.theme").unwrap()),
)?;
html.handler.set_images_mapping(true);
html.handler.set_base64(false);
Ok(HtmlDirRenderer { html: html })
@ -67,25 +68,39 @@ impl<'a> HtmlDirRenderer<'a> {
pub fn render_book(&mut self, dest_path: &Path) -> Result<()> {
// Add internal files to resource handler
for (i, chapter) in self.html.book.chapters.iter().enumerate() {
self.html.handler.add_link(chapter.filename.as_str(), filenamer(i));
self.html
.handler
.add_link(chapter.filename.as_str(), filenamer(i));
}
if let Ok(metadata) = fs::metadata(&dest_path) {
if metadata.is_file() {
return Err(Error::render(&self.html.book.source,
lformat!("{path} already exists and is not a \
return Err(Error::render(
&self.html.book.source,
lformat!(
"{path} already exists and is not a \
directory",
path = dest_path.display())));
path = dest_path.display()
),
));
} else if metadata.is_dir() {
debug!("{}", lformat!("{path} already exists, deleting it",
path = dest_path.display()));
fs::remove_dir_all(&dest_path)
.map_err(|e| {
Error::render(&self.html.book.source,
lformat!("error deleting directory {path}: {error}",
path = dest_path.display(),
error = e))
})?;
debug!(
"{}",
lformat!(
"{path} already exists, deleting it",
path = dest_path.display()
)
);
fs::remove_dir_all(&dest_path).map_err(|e| {
Error::render(
&self.html.book.source,
lformat!(
"error deleting directory {path}: {error}",
path = dest_path.display(),
error = e
),
)
})?;
}
}
@ -93,19 +108,27 @@ impl<'a> HtmlDirRenderer<'a> {
.recursive(true)
.create(&dest_path)
.map_err(|e| {
Error::render(&self.html.book.source,
lformat!("could not create HTML directory {path}: {error}",
path = dest_path.display(),
error = e))
Error::render(
&self.html.book.source,
lformat!(
"could not create HTML directory {path}: {error}",
path = dest_path.display(),
error = e
),
)
})?;
// Write CSS
self.write_css()?;
// Write print.css
self.write_file("print.css",
self.html.book.get_template("html.css.print")
.unwrap()
.as_bytes())?;
self.write_file(
"print.css",
self.html
.book
.get_template("html.css.print")
.unwrap()
.as_bytes(),
)?;
// Write index.html and chapter_xxx.html
self.write_html()?;
// Write menu.svg
@ -113,18 +136,22 @@ impl<'a> HtmlDirRenderer<'a> {
// Write highlight files if they are needed
if self.html.highlight == Highlight::Js {
self.write_file("highlight.js",
self.html
.book
.get_template("html.highlight.js")
.unwrap()
.as_bytes())?;
self.write_file("highlight.css",
self.html
.book
.get_template("html.highlight.css")
.unwrap()
.as_bytes())?;
self.write_file(
"highlight.js",
self.html
.book
.get_template("html.highlight.js")
.unwrap()
.as_bytes(),
)?;
self.write_file(
"highlight.css",
self.html
.book
.get_template("html.highlight.css")
.unwrap()
.as_bytes(),
)?;
}
// Write all images (including cover)
@ -132,42 +159,60 @@ impl<'a> HtmlDirRenderer<'a> {
let mut f = fs::canonicalize(source)
.and_then(|f| File::open(f))
.map_err(|_| {
Error::file_not_found(&self.html.book.source,
lformat!("image or cover"),
source.clone())
Error::file_not_found(
&self.html.book.source,
lformat!("image or cover"),
source.clone(),
)
})?;
let mut content = vec![];
f.read_to_end(&mut content)
.map_err(|e| {
Error::render(&self.html.book.source,
lformat!("error while reading image file {file}: {error}",
file = source,
error = e))
})?;
f.read_to_end(&mut content).map_err(|e| {
Error::render(
&self.html.book.source,
lformat!(
"error while reading image file {file}: {error}",
file = source,
error = e
),
)
})?;
self.write_file(dest, &content)?;
}
// Write additional files
if let Ok(list) = self.html.book.options.get_str_vec("resources.files") {
let files_path = self.html.book.options.get_path("resources.base_path.files").unwrap();
let data_path =
Path::new(self.html.book.options.get_relative_path("resources.out_path").unwrap());
let files_path = self
.html
.book
.options
.get_path("resources.base_path.files")
.unwrap();
let data_path = Path::new(
self.html
.book
.options
.get_relative_path("resources.out_path")
.unwrap(),
);
let list = resource_handler::get_files(list, &files_path)?;
for path in list {
let abs_path = Path::new(&files_path).join(&path);
let mut f = fs::canonicalize(&abs_path)
.and_then(|f| File::open(f))
.map_err(|_| {
Error::file_not_found(&self.html.book.source,
lformat!("additional resource from resources.files"),
abs_path.to_string_lossy().into_owned())
Error::file_not_found(
&self.html.book.source,
lformat!("additional resource from resources.files"),
abs_path.to_string_lossy().into_owned(),
)
})?;
let mut content = vec![];
f.read_to_end(&mut content)
.map_err(|e| {
Error::render(&self.html.book.source,
lformat!("error while reading resource file: {error}", error = e))
})?;
f.read_to_end(&mut content).map_err(|e| {
Error::render(
&self.html.book.source,
lformat!("error while reading resource file: {error}", error = e),
)
})?;
self.write_file(data_path.join(&path).to_str().unwrap(), &content)?;
}
}
@ -194,23 +239,23 @@ impl<'a> HtmlDirRenderer<'a> {
title = self.html.render_vec(vec)?;
title_raw = view_as_text(vec);
} else {
title = self.html
title = self
.html
.book
.get_chapter_header(self.html.current_chapter[1] + 1,
self.html.render_vec(vec)?,
|s| {
self.render_vec(&Parser::new()
.parse_inline(s)?)
})?
.get_chapter_header(
self.html.current_chapter[1] + 1,
self.html.render_vec(vec)?,
|s| self.render_vec(&Parser::new().parse_inline(s)?),
)?
.text;
title_raw = self.html
title_raw = self
.html
.book
.get_chapter_header(self.html.current_chapter[1] + 1,
view_as_text(vec),
|s| {
Ok(view_as_text(&Parser::new()
.parse_inline(s)?))
})?
.get_chapter_header(
self.html.current_chapter[1] + 1,
view_as_text(vec),
|s| Ok(view_as_text(&Parser::new().parse_inline(s)?)),
)?
.text;
}
break;
@ -230,45 +275,49 @@ impl<'a> HtmlDirRenderer<'a> {
let toc = self.html.toc.render(false);
// render all chapters
let template =
compile_str(self.html.book.get_template("html.dir.template")?.as_ref(),
&self.html.book.source,
"html.dir.template")?;
let template = compile_str(
self.html.book.get_template("html.dir.template")?.as_ref(),
&self.html.book.source,
"html.dir.template",
)?;
for (i, content) in chapters.into_iter().enumerate() {
let prev_chapter = if i > 0 {
format!("<p class = \"prev_chapter\">
format!(
"<p class = \"prev_chapter\">
<a href = \"{}\">
« {}
</a>
</p>",
filenamer(i - 1),
titles[i - 1])
filenamer(i - 1),
titles[i - 1]
)
} else {
String::new()
};
let next_chapter = if i < titles.len() - 1 {
format!("<p class = \"next_chapter\">
format!(
"<p class = \"next_chapter\">
<a href = \"{}\">
{} »
</a>
</p>",
filenamer(i + 1),
titles[i + 1])
filenamer(i + 1),
titles[i + 1]
)
} else {
String::new()
};
// Render each HTML document
let mut mapbuilder = self.html
let mut mapbuilder = self
.html
.book
.get_metadata(|s| self.render_vec(&Parser::new().parse_inline(s)?))?
.insert_str("content", content?)
.insert_str("chapter_title",
titles[i].clone())
.insert_str("chapter_title", titles[i].clone())
.insert_str("json_data", self.html.get_json_ld()?)
.insert_str("chapter_title_raw",
titles_raw[i].clone())
.insert_str("chapter_title_raw", titles_raw[i].clone())
.insert_str("toc", toc.clone())
.insert_str("prev_chapter", prev_chapter)
.insert_str("next_chapter", next_chapter)
@ -278,8 +327,14 @@ impl<'a> HtmlDirRenderer<'a> {
.insert_bool(self.html.book.options.get_str("lang").unwrap(), true);
if let Ok(favicon) = self.html.book.options.get_path("html.icon") {
let favicon = self.html.handler.map_image(&self.html.book.source, favicon)?;
mapbuilder = mapbuilder.insert_str("favicon", format!("<link rel = \"icon\" href = \"{}\">", favicon));
let favicon = self
.html
.handler
.map_image(&self.html.book.source, favicon)?;
mapbuilder = mapbuilder.insert_str(
"favicon",
format!("<link rel = \"icon\" href = \"{}\">", favicon),
);
}
if self.html.highlight == Highlight::Js {
mapbuilder = mapbuilder.insert_bool("highlight_code", true);
@ -293,15 +348,22 @@ impl<'a> HtmlDirRenderer<'a> {
let mut content = if let Ok(cover) = self.html.book.options.get_path("cover") {
// checks first that cover exists
if fs::metadata(&cover).is_err() {
return Err(Error::file_not_found(&self.html.book.source, lformat!("cover"), cover));
return Err(Error::file_not_found(
&self.html.book.source,
lformat!("cover"),
cover,
));
}
format!("<div id = \"cover\">
format!(
"<div id = \"cover\">
<img class = \"cover\" alt = \"{}\" src = \"{}\" />
</div>",
self.html.book.options.get_str("title").unwrap(),
self.html.handler.map_image(&self.html.book.source, Cow::Owned(cover))?
.as_ref())
self.html.book.options.get_str("title").unwrap(),
self.html
.handler
.map_image(&self.html.book.source, Cow::Owned(cover))?
.as_ref()
)
} else {
String::new()
};
@ -311,45 +373,57 @@ impl<'a> HtmlDirRenderer<'a> {
self.render_vec(&Parser::new().parse_inline(self.html.book.options.get_str(key)?)?)
};
format!("<h2 class = 'author'>{author}</h2>
format!(
"<h2 class = 'author'>{author}</h2>
<h1 class = 'title'>{title}</h1>
<h2 class = 'subtitle'>{subtitle}</h2>
<div class = \"autograph\">
{autograph}
</div>
{content}",
author = f("author")?,
title = f("title")?,
autograph = f("autograph").unwrap_or_else(|_| String::new()),
content = content,
subtitle = f("subtitle").unwrap_or_else(|_| String::new()))
author = f("author")?,
title = f("title")?,
autograph = f("autograph").unwrap_or_else(|_| String::new()),
content = content,
subtitle = f("subtitle").unwrap_or_else(|_| String::new())
)
};
// Insert toc inline if option is set
if self.html.book.options.get_bool("rendering.inline_toc").unwrap() {
write!(content,
"<h1>{}</h1>
if self
.html
.book
.options
.get_bool("rendering.inline_toc")
.unwrap()
{
write!(
content,
"<h1>{}</h1>
<div id = \"toc\">
{}
</div>
",
self.html.get_toc_name()?,
&toc)?;
self.html.get_toc_name()?,
&toc
)?;
}
if titles.len() > 1 {
write!(content,
"<p class = \"next_chapter\">
write!(
content,
"<p class = \"next_chapter\">
<a href = \"{}\">
{} »
</a>
</p>",
filenamer(0),
titles[0])?;
filenamer(0),
titles[0]
)?;
}
// Render index.html and write it too
let mut mapbuilder = self.html
let mut mapbuilder = self
.html
.book
.get_metadata(|s| self.render_vec(&Parser::new().parse_inline(s)?))?
.insert_str("content", content)
@ -359,17 +433,24 @@ impl<'a> HtmlDirRenderer<'a> {
.insert_str("script", self.html.book.get_template("html.js").unwrap())
.insert_bool(self.html.book.options.get_str("lang").unwrap(), true);
if let Ok(favicon) = self.html.book.options.get_path("html.icon") {
let favicon = self.html.handler.map_image(&self.html.book.source, favicon)?;
mapbuilder = mapbuilder.insert_str("favicon", format!("<link rel = \"icon\" href = \"{}\">", favicon));
let favicon = self
.html
.handler
.map_image(&self.html.book.source, favicon)?;
mapbuilder = mapbuilder.insert_str(
"favicon",
format!("<link rel = \"icon\" href = \"{}\">", favicon),
);
}
if self.html.highlight == Highlight::Js {
mapbuilder = mapbuilder.insert_bool("highlight_code", true);
}
let data = mapbuilder.build();
let template =
compile_str(self.html.book.get_template("html.dir.template")?.as_ref(),
&self.html.book.source,
"html.dir.template")?;
let template = compile_str(
self.html.book.get_template("html.dir.template")?.as_ref(),
&self.html.book.source,
"html.dir.template",
)?;
let mut res = vec![];
template.render_data(&mut res, &data)?;
self.write_file("index.html", &res)?;
@ -380,15 +461,13 @@ impl<'a> HtmlDirRenderer<'a> {
// Render the CSS file and write it
fn write_css(&self) -> Result<()> {
// Render the CSS
let template_css = compile_str(self.html
.book
.get_template("html.css")?
.as_ref(),
&self.html.book.source,
"html.css")?;
let template_css = compile_str(
self.html.book.get_template("html.css")?.as_ref(),
&self.html.book.source,
"html.css",
)?;
let mut data = self.html.book.get_metadata(|s| Ok(s.to_owned()))?;
data = data.insert_str("colors",
self.html.book.get_template("html.css.colors")?);
data = data.insert_str("colors", self.html.book.get_template("html.css.colors")?);
if let Ok(html_css_add) = self.html.book.options.get_str("html.css.add") {
data = data.insert_str("additional_code", html_css_add);
}
@ -404,7 +483,11 @@ impl<'a> HtmlDirRenderer<'a> {
// Write content to a file
fn write_file(&self, file: &str, content: &[u8]) -> Result<()> {
let dir_name = if self.html.proofread {
self.html.book.options.get_path("output.proofread.html.dir").unwrap()
self.html
.book
.options
.get_path("output.proofread.html.dir")
.unwrap()
} else {
self.html.book.options.get_path("output.html.dir").unwrap()
};
@ -420,25 +503,36 @@ impl<'a> HtmlDirRenderer<'a> {
.recursive(true)
.create(&dest_dir)
.map_err(|e| {
Error::render(&self.html.book.source,
lformat!("could not create directory in {path}: {error}",
path = dest_dir.display(),
error = e))
Error::render(
&self.html.book.source,
lformat!(
"could not create directory in {path}: {error}",
path = dest_dir.display(),
error = e
),
)
})?;
}
let mut f = File::create(&dest_file).map_err(|e| {
Error::render(&self.html.book.source,
lformat!("could not create file {file}: {error}",
file = dest_file.display(),
error = e))
Error::render(
&self.html.book.source,
lformat!(
"could not create file {file}: {error}",
file = dest_file.display(),
error = e
),
)
})?;
io::Write::write_all(&mut f, content)
.map_err(|e| {
Error::render(&self.html.book.source,
lformat!("could not write to file {file}: {error}",
file = dest_file.display(),
error = e))
})
io::Write::write_all(&mut f, content).map_err(|e| {
Error::render(
&self.html.book.source,
lformat!(
"could not write to file {file}: {error}",
file = dest_file.display(),
error = e
),
)
})
}
}
@ -447,7 +541,7 @@ fn filenamer(i: usize) -> String {
format!("chapter_{:03}.html", i)
}
derive_html!{HtmlDirRenderer<'a>, HtmlRenderer::static_render_token}
derive_html! {HtmlDirRenderer<'a>, HtmlRenderer::static_render_token}
pub struct HtmlDir {}
pub struct ProofHtmlDir {}
@ -458,13 +552,14 @@ impl BookRenderer for HtmlDir {
}
fn render(&self, _: &Book, _: &mut dyn io::Write) -> Result<()> {
Err(Error::render(Source::empty(),
lformat!("can only render HTML directory to a path, not to a stream")))
Err(Error::render(
Source::empty(),
lformat!("can only render HTML directory to a path, not to a stream"),
))
}
fn render_to_file(&self, book: &Book, path: &Path) -> Result<()> {
HtmlDirRenderer::new(book)?
.render_book(path)?;
HtmlDirRenderer::new(book)?.render_book(path)?;
Ok(())
}
}
@ -475,14 +570,14 @@ impl BookRenderer for ProofHtmlDir {
}
fn render(&self, _: &Book, _: &mut dyn io::Write) -> Result<()> {
Err(Error::render(Source::empty(),
lformat!("can only render HTML directory to a path, not to a stream")))
Err(Error::render(
Source::empty(),
lformat!("can only render HTML directory to a path, not to a stream"),
))
}
fn render_to_file(&self, book: &Book, path: &Path) -> Result<()> {
HtmlDirRenderer::new(book)?
.proofread()
.render_book(path)?;
HtmlDirRenderer::new(book)?.proofread().render_book(path)?;
Ok(())
}
}

View File

@ -15,14 +15,14 @@
// 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 crate::error::{Error, Result, Source};
use crate::html::HtmlRenderer;
use crate::html::Highlight;
use crate::book::{Book, compile_str};
use crate::token::Token;
use crate::renderer::Renderer;
use crate::book::{compile_str, Book};
use crate::book_renderer::BookRenderer;
use crate::error::{Error, Result, Source};
use crate::html::Highlight;
use crate::html::HtmlRenderer;
use crate::parser::Parser;
use crate::renderer::Renderer;
use crate::token::Token;
use rustc_serialize::base64::{self, ToBase64};
@ -43,20 +43,20 @@ pub struct HtmlIfRenderer<'a> {
impl<'a> HtmlIfRenderer<'a> {
/// Creates a new HtmlIfRenderer
pub fn new(book: &'a Book) -> Result<HtmlIfRenderer<'a>> {
let mut html = HtmlRenderer::new(book,
book.options
.get_str("html.highlight.theme")
.unwrap_or_else(|_| book.options.get_str("rendering.highlight.theme").unwrap()))?;
let mut html = HtmlRenderer::new(
book,
book.options
.get_str("html.highlight.theme")
.unwrap_or_else(|_| book.options.get_str("rendering.highlight.theme").unwrap()),
)?;
html.handler.set_images_mapping(true);
html.handler.set_base64(true);
Ok(
HtmlIfRenderer {
html: html,
n_fn: 0,
curr_init: String::new(),
fn_defs: String::new(),
}
)
Ok(HtmlIfRenderer {
html: html,
n_fn: 0,
curr_init: String::new(),
fn_defs: String::new(),
})
}
/// Parse embedded javascript code
@ -73,58 +73,57 @@ impl<'a> HtmlIfRenderer<'a> {
let end = begin + len;
gen_code.push_str(&code[i..begin]);
let mut md_part = &code[begin+2..end];
let mut md_part = &code[begin + 2..end];
let rendered = self.render_vec(&Parser::new().parse(md_part)?)?;
while let Some(b) = md_part.find("{{") {
md_part = &md_part[b+2..];
md_part = &md_part[b + 2..];
if let Some(l) = md_part.find("}}") {
variables.push(md_part[..l].to_owned());
}
}
gen_code.push_str("crowbook_return_variable += \"");
gen_code.push_str(&rendered
.replace('"', "\\\"")
.replace('\n', "\\\n"));
gen_code.push_str(&rendered.replace('"', "\\\"").replace('\n', "\\\n"));
gen_code.push('"');
for var in &variables {
gen_code.push_str(&format!(".replace(/{{{{{var}}}}}/, {var})",
var = var));
gen_code.push_str(&format!(".replace(/{{{{{var}}}}}/, {var})", var = var));
}
gen_code.push(';');
i = end + 2;
} else {
gen_code.push_str(&code[i..begin+2]);
} else {
gen_code.push_str(&code[i..begin + 2]);
i = begin + 2;
}
}
gen_code.push_str(&code[i..]);
if contains_md {
gen_code = format!("var crowbook_return_variable = \"\";
gen_code = format!(
"var crowbook_return_variable = \"\";
{}
return crowbook_return_variable.replace(/<\\/ul><ul>/g, '');\n",
gen_code);
gen_code
);
}
let container = if !contains_md {
"p"
} else {
"div"
};
let container = if !contains_md { "p" } else { "div" };
let id = self.n_fn;
self.fn_defs
.push_str(&format!("function fn_{id}() {{
self.fn_defs.push_str(&format!(
"function fn_{id}() {{
{code}
}}\n",
id = id,
code = gen_code));
self.curr_init
.push_str(&format!(" result = fn_{id}();
id = id,
code = gen_code
));
self.curr_init.push_str(&format!(
" result = fn_{id}();
if (result != undefined) {{
document.getElementById(\"result_{id}\").innerHTML = result;
}}\n",
id = id));
let content = format!("<{container} id = \"result_{id}\"></{container}>\n",
id = (self.n_fn),
container = container);
id = id
));
let content = format!(
"<{container} id = \"result_{id}\"></{container}>\n",
id = (self.n_fn),
container = container
);
self.n_fn += 1;
Ok(content)
}
@ -137,37 +136,46 @@ return crowbook_return_variable.replace(/<\\/ul><ul>/g, '');\n",
/// See http://lise-henry.github.io/articles/rust_inheritance.html
#[doc(hidden)]
pub fn static_render_token<T>(this: &mut T, token: &Token) -> Result<String>
where T: AsMut<HtmlIfRenderer<'a>>+AsRef<HtmlIfRenderer<'a>> +
AsMut<HtmlRenderer<'a>>+AsRef<HtmlRenderer<'a>> + Renderer
where
T: AsMut<HtmlIfRenderer<'a>>
+ AsRef<HtmlIfRenderer<'a>>
+ AsMut<HtmlRenderer<'a>>
+ AsRef<HtmlRenderer<'a>>
+ Renderer,
{
match *token {
Token::CodeBlock(ref language, ref code) if language == "" => {
let html_if: &mut HtmlIfRenderer = this.as_mut();
let content = html_if.parse_inner_code(code)?;
Ok(content)
},
Token::CodeBlock(ref language, ref code) if language.starts_with(|c| c == '<' || c == '>') => {
}
Token::CodeBlock(ref language, ref code)
if language.starts_with(|c| c == '<' || c == '>') =>
{
let html_if: &mut HtmlIfRenderer = this.as_mut();
let code = format!("if (passageCount(state.current_id) {expr}) {{
let code = format!(
"if (passageCount(state.current_id) {expr}) {{
{code};
}}\n",
code = code,
expr = language);
code = code,
expr = language
);
let content = html_if.parse_inner_code(&code)?;
Ok(content)
},
}
Token::CodeBlock(ref language, ref code) if language.parse::<u32>().is_ok() => {
let html_if: &mut HtmlIfRenderer = this.as_mut();
let code = format!("if (passageCount(state.current_id) == {n}) {{
let code = format!(
"if (passageCount(state.current_id) == {n}) {{
{code};
}}\n",
code = code,
n = language.parse::<u32>().unwrap());
code = code,
n = language.parse::<u32>().unwrap()
);
let content = html_if.parse_inner_code(&code)?;
Ok(content)
},
_ => HtmlRenderer::static_render_token(this, token)
}
_ => HtmlRenderer::static_render_token(this, token),
}
}
@ -180,13 +188,22 @@ return crowbook_return_variable.replace(/<\\/ul><ul>/g, '');\n",
let render_notes_chapter = true;
for (i, chapter) in self.html.book.chapters.iter().enumerate() {
self.html.handler.add_link(chapter.filename.as_str(),
format!("#chapter-{}", i));
self.html
.handler
.add_link(chapter.filename.as_str(), format!("#chapter-{}", i));
}
let pre_code = self.html.book.options.get_str("html.if.new_turn")
let pre_code = self
.html
.book
.options
.get_str("html.if.new_turn")
.unwrap_or("");
let post_code = self.html.book.options.get_str("html.if.end_turn")
let post_code = self
.html
.book
.options
.get_str("html.if.end_turn")
.unwrap_or("");
for (i, chapter) in self.html.book.chapters.iter().enumerate() {
@ -201,14 +218,14 @@ return crowbook_return_variable.replace(/<\\/ul><ul>/g, '');\n",
if self.html.current_hide || self.html.current_numbering == 0 {
title = self.html.render_vec(vec)?;
} else {
title = self.html
title = self
.html
.book
.get_chapter_header(self.html.current_chapter[1] + 1,
self.html.render_vec(vec)?,
|s| {
self.render_vec(&Parser::new()
.parse_inline(s)?)
})?
.get_chapter_header(
self.html.current_chapter[1] + 1,
self.html.render_vec(vec)?,
|s| self.render_vec(&Parser::new().parse_inline(s)?),
)?
.text;
}
break;
@ -230,16 +247,19 @@ return crowbook_return_variable.replace(/<\\/ul><ul>/g, '');\n",
chapter_content.push_str(&self.parse_inner_code(post_code)?);
}
chapters.push(format!("<div id = \"chapter-{}\" class = \"chapter\">
chapters.push(format!(
"<div id = \"chapter-{}\" class = \"chapter\">
{}
</div>",
i,
chapter_content));
self.fn_defs.push_str(&format!("initFns.push(function () {{
i, chapter_content
));
self.fn_defs.push_str(&format!(
"initFns.push(function () {{
state.visited.push(state.current_id);
{code}
}})\n",
code = self.curr_init));
code = self.curr_init
));
self.curr_init = String::new();
}
@ -250,17 +270,17 @@ return crowbook_return_variable.replace(/<\\/ul><ul>/g, '');\n",
}
self.html.render_end_notes(&mut content);
// Render the CSS
let template_css = compile_str(self.html.book.get_template("html.css")?
.as_ref(),
&self.html.book.source,
"html.css")?;
let mut data = self.html
let template_css = compile_str(
self.html.book.get_template("html.css")?.as_ref(),
&self.html.book.source,
"html.css",
)?;
let mut data = self
.html
.book
.get_metadata(|s| self.render_vec(&Parser::new().parse_inline(s)?))?
.insert_str("colors",
self.html.book.get_template("html.css.colors")?);
.insert_str("colors", self.html.book.get_template("html.css.colors")?);
if let Ok(html_css_add) = self.html.book.options.get_str("html.css.add") {
data = data.insert_str("additional_code", html_css_add);
}
@ -270,23 +290,33 @@ return crowbook_return_variable.replace(/<\\/ul><ul>/g, '');\n",
let css = String::from_utf8_lossy(&res);
// Render the JS
let template_js =
compile_str(self.html.book.get_template("html.if.js")?.as_ref(),
&self.html.book.source,
"html.standalone.js")?;
let data = self.html.book.get_metadata(|s| Ok(s.to_owned()))?
let template_js = compile_str(
self.html.book.get_template("html.if.js")?.as_ref(),
&self.html.book.source,
"html.standalone.js",
)?;
let data = self
.html
.book
.get_metadata(|s| Ok(s.to_owned()))?
.insert_bool("one_chapter", true)
.insert_str("js_prelude", mem::replace(&mut self.fn_defs, String::new()))
.insert_str("new_game", self.html.book.get_template("html.if.new_game").unwrap())
.insert_str("common_script",
self.html.book.get_template("html.js").unwrap().as_ref())
.insert_str(
"new_game",
self.html.book.get_template("html.if.new_game").unwrap(),
)
.insert_str(
"common_script",
self.html.book.get_template("html.js").unwrap().as_ref(),
)
.build();
let mut res: Vec<u8> = vec![];
template_js.render_data(&mut res, &data)?;
let js = String::from_utf8_lossy(&res);
// Render the HTML document
let mut mapbuilder = self.html
let mut mapbuilder = self
.html
.book
.get_metadata(|s| self.render_vec(&Parser::new().parse_inline(s)?))?
.insert_str("content", content)
@ -294,38 +324,55 @@ return crowbook_return_variable.replace(/<\\/ul><ul>/g, '');\n",
.insert_bool(self.html.book.options.get_str("lang").unwrap(), true)
.insert_bool("one_chapter", true)
.insert_str("style", css.as_ref())
.insert_str("print_style",
self.html.book.get_template("html.css.print").unwrap())
.insert_str(
"print_style",
self.html.book.get_template("html.css.print").unwrap(),
)
.insert_str("footer", HtmlRenderer::get_footer(self)?)
.insert_str("header", HtmlRenderer::get_header(self)?)
.insert_bool("has_toc", false);
if let Ok(favicon) = self.html.book.options.get_path("html.icon") {
let favicon = self.html.handler.map_image(&self.html.book.source, favicon)?;
mapbuilder = mapbuilder.insert_str("favicon", format!("<link rel = \"icon\" href = \"{}\">", favicon));
}
let favicon = self
.html
.handler
.map_image(&self.html.book.source, favicon)?;
mapbuilder = mapbuilder.insert_str(
"favicon",
format!("<link rel = \"icon\" href = \"{}\">", favicon),
);
}
if self.html.highlight == Highlight::Js {
let highlight_js = self.html.book.get_template("html.highlight.js")?
let highlight_js = self
.html
.book
.get_template("html.highlight.js")?
.as_bytes()
.to_base64(base64::STANDARD);
let highlight_js = format!("data:text/javascript;base64,{}", highlight_js);
mapbuilder = mapbuilder.insert_bool("highlight_code", true)
.insert_str("highlight_css",
self.html.book.get_template("html.highlight.css")?)
mapbuilder = mapbuilder
.insert_bool("highlight_code", true)
.insert_str(
"highlight_css",
self.html.book.get_template("html.highlight.css")?,
)
.insert_str("highlight_js", highlight_js);
}
let data = mapbuilder.build();
let template = compile_str(self.html.book.get_template("html.standalone.template")?
.as_ref(),
&self.html.book.source,
"html.standalone.template")?;
let template = compile_str(
self.html
.book
.get_template("html.standalone.template")?
.as_ref(),
&self.html.book.source,
"html.standalone.template",
)?;
let mut res = vec![];
template.render_data(&mut res, &data)?;
Ok(String::from_utf8_lossy(&res).into_owned())
}
}
derive_html!{HtmlIfRenderer<'a>, HtmlIfRenderer::static_render_token}
derive_html! {HtmlIfRenderer<'a>, HtmlIfRenderer::static_render_token}
pub struct HtmlIf {}
@ -337,11 +384,15 @@ impl BookRenderer for HtmlIf {
fn render(&self, book: &Book, to: &mut dyn io::Write) -> Result<()> {
let mut html = HtmlIfRenderer::new(book)?;
let result = html.render_book()?;
to.write_all(result.as_bytes())
.map_err(|e| {
Error::render(&book.source,
lformat!("problem when writing interactive fiction: {error}", error = e))
})?;
to.write_all(result.as_bytes()).map_err(|e| {
Error::render(
&book.source,
lformat!(
"problem when writing interactive fiction: {error}",
error = e
),
)
})?;
Ok(())
}
}

View File

@ -15,21 +15,21 @@
// 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 crate::error::{Error, Result, Source};
use crate::html::HtmlRenderer;
use crate::html::Highlight;
use crate::book::{Book, compile_str};
use crate::token::Token;
use crate::templates::img;
use crate::renderer::Renderer;
use crate::book::{compile_str, Book};
use crate::book_renderer::BookRenderer;
use crate::error::{Error, Result, Source};
use crate::html::Highlight;
use crate::html::HtmlRenderer;
use crate::parser::Parser;
use crate::renderer::Renderer;
use crate::templates::img;
use crate::token::Token;
use rustc_serialize::base64::{self, ToBase64};
use std::convert::{AsMut, AsRef};
use std::io;
use std::fmt::Write;
use std::io;
/// Single file HTML renderer
///
@ -41,10 +41,12 @@ pub struct HtmlSingleRenderer<'a> {
impl<'a> HtmlSingleRenderer<'a> {
/// Creates a new HtmlSingleRenderer
pub fn new(book: &'a Book) -> Result<HtmlSingleRenderer<'a>> {
let mut html = HtmlRenderer::new(book,
book.options
.get_str("html.highlight.theme")
.unwrap_or_else(|_| book.options.get_str("rendering.highlight.theme").unwrap()))?;
let mut html = HtmlRenderer::new(
book,
book.options
.get_str("html.highlight.theme")
.unwrap_or_else(|_| book.options.get_str("rendering.highlight.theme").unwrap()),
)?;
html.handler.set_images_mapping(true);
html.handler.set_base64(true);
Ok(HtmlSingleRenderer { html: html })
@ -64,8 +66,12 @@ impl<'a> HtmlSingleRenderer<'a> {
/// See http://lise-henry.github.io/articles/rust_inheritance.html
#[doc(hidden)]
pub fn static_render_token<T>(this: &mut T, token: &Token) -> Result<String>
where T: AsMut<HtmlSingleRenderer<'a>>+AsRef<HtmlSingleRenderer<'a>> +
AsMut<HtmlRenderer<'a>>+AsRef<HtmlRenderer<'a>> + Renderer
where
T: AsMut<HtmlSingleRenderer<'a>>
+ AsRef<HtmlSingleRenderer<'a>>
+ AsMut<HtmlRenderer<'a>>
+ AsRef<HtmlRenderer<'a>>
+ Renderer,
{
HtmlRenderer::static_render_token(this, token)
}
@ -85,12 +91,17 @@ impl<'a> HtmlSingleRenderer<'a> {
let mut titles = vec![];
let mut chapters = vec![];
let render_notes_chapter =
self.html.book.options.get_bool("html.standalone.one_chapter").unwrap();
let render_notes_chapter = self
.html
.book
.options
.get_bool("html.standalone.one_chapter")
.unwrap();
for (i, chapter) in self.html.book.chapters.iter().enumerate() {
self.html.handler.add_link(chapter.filename.as_str(),
format!("#chapter-{}", i));
self.html
.handler
.add_link(chapter.filename.as_str(), format!("#chapter-{}", i));
}
for (i, chapter) in self.html.book.chapters.iter().enumerate() {
@ -105,14 +116,14 @@ impl<'a> HtmlSingleRenderer<'a> {
if self.html.current_hide || self.html.current_numbering == 0 {
title = self.html.render_vec(vec)?;
} else {
title = self.html
title = self
.html
.book
.get_chapter_header(self.html.current_chapter[1] + 1,
self.html.render_vec(vec)?,
|s| {
self.render_vec(&Parser::new()
.parse_inline(s)?)
})?
.get_chapter_header(
self.html.current_chapter[1] + 1,
self.html.render_vec(vec)?,
|s| self.render_vec(&Parser::new().parse_inline(s)?),
)?
.text;
}
break;
@ -124,71 +135,97 @@ impl<'a> HtmlSingleRenderer<'a> {
}
titles.push(title);
chapters.push(format!("<div id = \"chapter-{}\" class = \"chapter\">
chapters.push(format!(
"<div id = \"chapter-{}\" class = \"chapter\">
{}
</div>",
i,
HtmlRenderer::render_html(self, v, render_notes_chapter)?));
i,
HtmlRenderer::render_html(self, v, render_notes_chapter)?
));
}
self.html.source = Source::empty();
for (i, chapter) in chapters.iter().enumerate() {
if self.html.book.options.get_bool("html.standalone.one_chapter").unwrap() && i != 0 {
write!(content,
"<p onclick = \"javascript:showChapter({})\" class = \
if self
.html
.book
.options
.get_bool("html.standalone.one_chapter")
.unwrap()
&& i != 0
{
write!(
content,
"<p onclick = \"javascript:showChapter({})\" class = \
\"chapterControls prev_chapter chapter-{}\">
<a href = \"#chapter-{}\">
« {}
</a>
</p>",
i - 1,
i,
i - 1,
titles[i - 1])?;
i - 1,
i,
i - 1,
titles[i - 1]
)?;
}
content.push_str(chapter);
if self.html.book.options.get_bool("html.standalone.one_chapter").unwrap() &&
i < titles.len() - 1 {
write!(content,
"<p onclick = \"javascript:showChapter({})\" class = \
if self
.html
.book
.options
.get_bool("html.standalone.one_chapter")
.unwrap()
&& i < titles.len() - 1
{
write!(
content,
"<p onclick = \"javascript:showChapter({})\" class = \
\"chapterControls next_chapter chapter-{}\">
<a href = \"#chapter-{}\">
{} »
</a>
</p>",
i + 1,
i,
i + 1,
titles[i + 1])?;
i + 1,
i,
i + 1,
titles[i + 1]
)?;
}
}
self.html.render_end_notes(&mut content);
let toc = self.html.toc.render(false);
// If display_toc, display the toc inline
if self.html.book.options.get_bool("rendering.inline_toc").unwrap() {
content = format!("<div id = \"toc\">
if self
.html
.book
.options
.get_bool("rendering.inline_toc")
.unwrap()
{
content = format!(
"<div id = \"toc\">
<h1>{title}</h1>
{toc}
</div>
{content}",
title = self.html.get_toc_name()?,
toc = &toc,
content = content);
title = self.html.get_toc_name()?,
toc = &toc,
content = content
);
}
// Render the CSS
let template_css = compile_str(self.html.book.get_template("html.css")?
.as_ref(),
&self.html.book.source,
"html.css")?;
let mut data = self.html
let template_css = compile_str(
self.html.book.get_template("html.css")?.as_ref(),
&self.html.book.source,
"html.css",
)?;
let mut data = self
.html
.book
.get_metadata(|s| self.render_vec(&Parser::new().parse_inline(s)?))?
.insert_str("colors",
self.html.book.get_template("html.css.colors")?);
.insert_str("colors", self.html.book.get_template("html.css.colors")?);
if let Ok(html_css_add) = self.html.book.options.get_str("html.css.add") {
data = data.insert_str("additional_code", html_css_add);
}
@ -198,34 +235,55 @@ impl<'a> HtmlSingleRenderer<'a> {
let css = String::from_utf8_lossy(&res);
// Render the JS
let template_js =
compile_str(self.html.book.get_template("html.standalone.js")?.as_ref(),
&self.html.book.source,
"html.standalone.js")?;
let data = self.html.book.get_metadata(|s| Ok(s.to_owned()))?
let template_js = compile_str(
self.html.book.get_template("html.standalone.js")?.as_ref(),
&self.html.book.source,
"html.standalone.js",
)?;
let data = self
.html
.book
.get_metadata(|s| Ok(s.to_owned()))?
.insert_str("book_svg", book_svg.clone())
.insert_str("pages_svg", pages_svg.clone())
.insert_bool("one_chapter",
self.html.book.options.get_bool("html.standalone.one_chapter").unwrap())
.insert_str("common_script",
self.html.book.get_template("html.js").unwrap().as_ref())
.insert_bool(
"one_chapter",
self.html
.book
.options
.get_bool("html.standalone.one_chapter")
.unwrap(),
)
.insert_str(
"common_script",
self.html.book.get_template("html.js").unwrap().as_ref(),
)
.build();
let mut res: Vec<u8> = vec![];
template_js.render_data(&mut res, &data)?;
let js = String::from_utf8_lossy(&res);
// Render the HTML document
let mut mapbuilder = self.html
let mut mapbuilder = self
.html
.book
.get_metadata(|s| self.render_vec(&Parser::new().parse_inline(s)?))?
.insert_str("content", content)
.insert_str("script", js)
.insert_bool(self.html.book.options.get_str("lang").unwrap(), true)
.insert_bool("one_chapter",
self.html.book.options.get_bool("html.standalone.one_chapter").unwrap())
.insert_bool(
"one_chapter",
self.html
.book
.options
.get_bool("html.standalone.one_chapter")
.unwrap(),
)
.insert_str("style", css.as_ref())
.insert_str("print_style",
self.html.book.get_template("html.css.print").unwrap())
.insert_str(
"print_style",
self.html.book.get_template("html.css.print").unwrap(),
)
.insert_str("menu_svg", menu_svg)
.insert_str("book_svg", book_svg)
.insert_str("pages_svg", pages_svg)
@ -233,36 +291,51 @@ impl<'a> HtmlSingleRenderer<'a> {
.insert_str("footer", HtmlRenderer::get_footer(self)?)
.insert_str("header", HtmlRenderer::get_header(self)?);
if let Ok(favicon) = self.html.book.options.get_path("html.icon") {
let favicon = self.html.handler.map_image(&self.html.book.source, favicon)?;
mapbuilder = mapbuilder.insert_str("favicon", format!("<link rel = \"icon\" href = \"{}\">", favicon));
}
let favicon = self
.html
.handler
.map_image(&self.html.book.source, favicon)?;
mapbuilder = mapbuilder.insert_str(
"favicon",
format!("<link rel = \"icon\" href = \"{}\">", favicon),
);
}
if !self.html.toc.is_empty() {
mapbuilder = mapbuilder.insert_bool("has_toc", true);
mapbuilder = mapbuilder.insert_str("toc", toc)
}
if self.html.highlight == Highlight::Js {
let highlight_js = self.html.book.get_template("html.highlight.js")?
let highlight_js = self
.html
.book
.get_template("html.highlight.js")?
.as_bytes()
.to_base64(base64::STANDARD);
let highlight_js = format!("data:text/javascript;base64,{}", highlight_js);
mapbuilder = mapbuilder.insert_bool("highlight_code", true)
.insert_str("highlight_css",
self.html.book.get_template("html.highlight.css")?)
mapbuilder = mapbuilder
.insert_bool("highlight_code", true)
.insert_str(
"highlight_css",
self.html.book.get_template("html.highlight.css")?,
)
.insert_str("highlight_js", highlight_js);
}
let data = mapbuilder.build();
let template = compile_str(self.html.book.get_template("html.standalone.template")?
.as_ref(),
&self.html.book.source,
"html.standalone.template")?;
let template = compile_str(
self.html
.book
.get_template("html.standalone.template")?
.as_ref(),
&self.html.book.source,
"html.standalone.template",
)?;
let mut res = vec![];
template.render_data(&mut res, &data)?;
Ok(String::from_utf8_lossy(&res).into_owned())
}
}
derive_html!{HtmlSingleRenderer<'a>, HtmlSingleRenderer::static_render_token}
derive_html! {HtmlSingleRenderer<'a>, HtmlSingleRenderer::static_render_token}
pub struct HtmlSingle {}
pub struct ProofHtmlSingle {}
@ -275,11 +348,12 @@ impl BookRenderer for HtmlSingle {
fn render(&self, book: &Book, to: &mut dyn io::Write) -> Result<()> {
let mut html = HtmlSingleRenderer::new(book)?;
let result = html.render_book()?;
to.write_all(result.as_bytes())
.map_err(|e| {
Error::render(&book.source,
lformat!("problem when writing HTML: {error}", error = e))
})?;
to.write_all(result.as_bytes()).map_err(|e| {
Error::render(
&book.source,
lformat!("problem when writing HTML: {error}", error = e),
)
})?;
Ok(())
}
}
@ -290,15 +364,14 @@ impl BookRenderer for ProofHtmlSingle {
}
fn render(&self, book: &Book, to: &mut dyn io::Write) -> Result<()> {
let mut html = HtmlSingleRenderer::new(book)?
.proofread();
let mut html = HtmlSingleRenderer::new(book)?.proofread();
let result = html.render_book()?;
to.write_all(result.as_bytes())
.map_err(|e| {
Error::render(&book.source,
lformat!("problem when writing HTML: {error}", error = e))
})?;
to.write_all(result.as_bytes()).map_err(|e| {
Error::render(
&book.source,
lformat!("problem when writing HTML: {error}", error = e),
)
})?;
Ok(())
}
}

View File

@ -1,5 +1,5 @@
use yaml_rust::{YamlLoader, Yaml};
use yaml_rust::yaml::Hash;
use yaml_rust::{Yaml, YamlLoader};
static EN: &'static str = include_str!("../../lang/document/en.yaml");
static ES: &'static str = include_str!("../../lang/document/es.yaml");
@ -19,24 +19,28 @@ pub fn get_hash(lang: &str) -> Hash {
if let Yaml::Hash(hash) = elem {
hash
} else {
panic!(lformat!("Yaml file for language {lang} didn't contain a hash",
lang = lang));
panic!(lformat!(
"Yaml file for language {lang} didn't contain a hash",
lang = lang
));
}
}
/// Get a string for a given language
pub fn get_str(lang: &str, s: &str) -> String {
let hash = get_hash(lang);
let yaml = hash.get(&Yaml::String(s.to_owned()))
.expect(&lformat!("Could not find translation for {key} in language {lang}",
key = s,
lang = lang));
let yaml = hash.get(&Yaml::String(s.to_owned())).expect(&lformat!(
"Could not find translation for {key} in language {lang}",
key = s,
lang = lang
));
if let &Yaml::String(ref result) = yaml {
result.clone()
} else {
panic!(lformat!("Yaml for {key} in lang {lang} is not a String!",
key = s,
lang = lang));
panic!(lformat!(
"Yaml for {key} in lang {lang} is not a String!",
key = s,
lang = lang
));
}
}

View File

@ -15,28 +15,27 @@
// 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 crate::book::{Book, compile_str};
use crate::number::Number;
use crate::error::{Error, Result, Source};
use crate::token::Token;
use crate::token::Data;
use crate::zipper::Zipper;
use crate::resource_handler::ResourceHandler;
use crate::renderer::Renderer;
use crate::parser::Parser;
use crate::book::{compile_str, Book};
use crate::book_renderer::BookRenderer;
use crate::error::{Error, Result, Source};
use crate::number::Number;
use crate::parser::Parser;
use crate::renderer::Renderer;
use crate::resource_handler::ResourceHandler;
use crate::syntax::Syntax;
use crate::token::Data;
use crate::token::Token;
use crate::zipper::Zipper;
use crowbook_text_processing::escape;
use std::iter::Iterator;
use std::borrow::Cow;
use std::fmt::Write;
use std::fs;
use std::fs::File;
use std::io;
use std::io::Read;
use std::fmt::Write;
use std::borrow::Cow;
use std::iter::Iterator;
/// LaTeX renderer
pub struct LatexRenderer<'a> {
@ -60,10 +59,13 @@ impl<'a> 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())))
&& 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
};
@ -94,8 +96,7 @@ impl<'a> LatexRenderer<'a> {
pub fn render_pdf(&mut self, to: &mut dyn 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())?;
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
@ -104,20 +105,22 @@ impl<'a> LatexRenderer<'a> {
.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))
})?;
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)
zipper.generate_pdf(
self.book.options.get_str("tex.command").unwrap(),
"result.tex",
to,
)
}
/// Render latex in a string
@ -126,18 +129,20 @@ impl<'a> LatexRenderer<'a> {
// 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}}{{{}}}
write!(
content,
"\\setcounter{{tocdepth}}{{{}}}
\\setcounter{{secnumdepth}}{{{}}}\n",
numbering,
numbering)?;
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));
self.handler
.add_link(chapter.filename.as_str(), format!("chapter-{}", i));
}
for (i, chapter) in self.book.chapters.iter().enumerate() {
@ -150,14 +155,11 @@ impl<'a> LatexRenderer<'a> {
content.push_str(&self.render_token(&v[0])?);
offset = 1;
}
write!(content,
"\\label{{chapter-{}}}\n",
i)?;
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",
@ -199,22 +201,40 @@ impl<'a> LatexRenderer<'a> {
"uk" => "ukrainian",
"cy" => "welsh",
_ => {
warn!("{}", lformat!("LaTeX: can't find a tex equivalent for lang '{lang}', \
warn!(
"{}",
lformat!(
"LaTeX: can't find a tex equivalent for lang '{lang}', \
fallbacking on english",
lang = self.book.options.get_str("lang").unwrap()));
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)?))?
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(
"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_taskitem", self.book.features.taskitem)
.insert_bool("use_tables", self.book.features.table)
@ -238,10 +258,28 @@ impl<'a> LatexRenderer<'a> {
book = true;
}
data = data
.insert_str("margin_left", self.book.options.get_str("tex.margin.left").unwrap_or(if book { "2.5cm" } else { "2cm" }))
.insert_str("margin_right", self.book.options.get_str("tex.margin.right").unwrap_or(if book { "1.5cm" } 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());
.insert_str(
"margin_left",
self.book
.options
.get_str("tex.margin.left")
.unwrap_or(if book { "2.5cm" } else { "2cm" }),
)
.insert_str(
"margin_right",
self.book
.options
.get_str("tex.margin.right")
.unwrap_or(if book { "1.5cm" } 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);
@ -273,7 +311,7 @@ impl<'a> Renderer for LatexRenderer<'a> {
let content = if self.escape {
let mut escaped = escape::tex(self.book.clean(text.as_str()));
if self.book.options.get_bool("tex.escape_nb_spaces").unwrap() {
escaped = escape::nb_spaces_tex(escaped)
escaped = escape::nb_spaces_tex(escaped)
}
escaped
} else {
@ -283,10 +321,15 @@ impl<'a> Renderer for LatexRenderer<'a> {
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 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() {
@ -305,7 +348,10 @@ impl<'a> Renderer for LatexRenderer<'a> {
let rest = chars.collect::<String>();
if initial.is_alphanumeric() {
Ok(format!("\\lettrine{{{}}}{{{}}}{}", initial, first_word, rest))
Ok(format!(
"\\lettrine{{{}}}{{{}}}{}",
initial, first_word, rest
))
} else {
Ok(format!("{}{}{}", initial, first_word, rest))
}
@ -346,7 +392,12 @@ impl<'a> Renderer for LatexRenderer<'a> {
1 => {
if !self.is_short {
if self.current_chapter.is_part() {
if self.book.options.get_bool("rendering.part.reset_counter").unwrap() {
if self
.book
.options
.get_bool("rendering.part.reset_counter")
.unwrap()
{
content.push_str(r"\setcounter{chapter}{0}");
}
content.push_str(r"\part");
@ -371,53 +422,69 @@ impl<'a> Renderer for LatexRenderer<'a> {
content.push_str("}\n");
Ok(content)
}
Token::TaskItem(checked, ref vec) =>
Ok(format!("[{}] {}",
if checked { r"$\boxtimes$" } else { r"$\square$" },
self.render_vec(vec)?)),
Token::TaskItem(checked, ref vec) => Ok(format!(
"[{}] {}",
if checked {
r"$\boxtimes$"
} else {
r"$\square$"
},
self.render_vec(vec)?
)),
Token::Emphasis(ref vec) => Ok(format!("\\emph{{{}}}", self.render_vec(vec)?)),
Token::Strong(ref vec) => Ok(format!("\\mdstrong{{{}}}", self.render_vec(vec)?)),
Token::Strikethrough(ref vec) => Ok(format!("\\sout{{{}}}", self.render_vec(vec)?)),
Token::Code(ref s) => Ok(format!("\\mdcode{{{}}}",
insert_breaks(&escape::tex(s)))),
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::Code(ref s) => Ok(format!("\\mdcode{{{}}}", insert_breaks(&escape::tex(s)))),
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 code) => {
let mut res:String;
let mut res: String;
res = if let Some(ref syntax) = self.syntax {
syntax.to_tex(code, language)?
} else {
format!("\\begin{{spverbatim}}
format!(
"\\begin{{spverbatim}}
{code}
\\end{{spverbatim}}",
code = code)
code = code
)
};
res = format!("\\begin{{mdcodeblock}}
res = format!(
"\\begin{{mdcodeblock}}
{}
\\end{{mdcodeblock}}", res);
\\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::DescriptionList(ref v) => {
Ok(format!("\\begin{{description}}
Token::DescriptionList(ref v) => Ok(format!(
"\\begin{{description}}
{}
\\end{{description}}",
self.render_vec(v)?))
},
self.render_vec(v)?
)),
Token::DescriptionItem(ref v) => Ok(self.render_vec(v)?),
Token::DescriptionTerm(ref v) => Ok(format!("\\item[{}]\n", self.render_vec(v)?.replace('\n', " "))),
Token::DescriptionTerm(ref v) => Ok(format!(
"\\item[{}]\n",
self.render_vec(v)?.replace('\n', " ")
)),
Token::DescriptionDetails(ref v) => Ok(self.render_vec(v)?),
Token::List(ref vec) => {
Ok(format!("\\begin{{itemize}}\n{}\\end{{itemize}}",
self.render_vec(vec)?))
}
Token::OrderedList(n , ref vec) => {
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 {
@ -428,37 +495,56 @@ impl<'a> Renderer for LatexRenderer<'a> {
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))),
_ => {
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)
format!(
"\\setcounter{{{counter}}}{{{n}}}\n",
counter = counter,
n = n - 1
)
};
let result = format!("\\begin{{enumerate}}
let result = format!(
"\\begin{{enumerate}}
{number}{inner}
\\end{{enumerate}}\n",
number = set_counter,
inner = self.render_vec(vec)?);
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))
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 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))
}
@ -467,54 +553,64 @@ impl<'a> Renderer for LatexRenderer<'a> {
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))
Ok(format!("\\mdstandaloneimage{{{}}}\n", img))
} else {
debug!("{}", lformat!("LaTeX ({source}): image '{url}' doesn't seem to be \
debug!(
"{}",
lformat!(
"LaTeX ({source}): image '{url}' doesn't seem to be \
local; ignoring it.",
source = self.source,
url = url));
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())?))
Ok(format!(
"\\mdimage{{{}}}",
self.handler.map_image(&self.source, url.as_str())?
))
} else {
debug!("{}", lformat!("LaTeX ({source}): image '{url}' doesn't seem to be \
debug!(
"{}",
lformat!(
"LaTeX ({source}): image '{url}' doesn't seem to be \
local; ignoring it.",
source = self.source,
url = url));
source = self.source,
url = url
)
);
Ok(String::new())
}
}
Token::FootnoteReference(ref reference) => {
Ok(format!("\\footnotemark[{}]", reference))
},
Token::FootnoteDefinition(ref reference, ref v) => {
Ok(format!("\\footnotetext[{}]{{{}}}",
reference,
self.render_vec(v)?))
}
Token::FootnoteReference(ref reference) => Ok(format!("\\footnotemark[{}]", reference)),
Token::FootnoteDefinition(ref reference, ref v) => Ok(format!(
"\\footnotetext[{}]{{{}}}",
reference,
self.render_vec(v)?
)),
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}}{{{}}}
Ok(format!(
"\\begin{{mdtable}}{{{}}}
\\hline
{}
\\hline
\\end{{mdtable}}\n\n",
cols,
self.render_vec(vec)?))
cols,
self.render_vec(vec)?
))
}
Token::TableRow(ref vec) |
Token::TableHead(ref vec) => {
let mut res: String = vec.iter()
Token::TableRow(ref vec) | Token::TableHead(ref vec) => {
let mut res: String = vec
.iter()
.map(|v| self.render_token(v))
.collect::<Result<Vec<_>>>()?
.join(" & ");
@ -529,15 +625,14 @@ impl<'a> Renderer for LatexRenderer<'a> {
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::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))
Ok(format!("\\underline{{{}}}", content))
} else {
Ok(content)
}
@ -559,7 +654,6 @@ 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))
@ -568,11 +662,12 @@ impl BookRenderer for Latex {
fn render(&self, book: &Book, to: &mut dyn 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))
})?;
to.write_all(result.as_bytes()).map_err(|e| {
Error::render(
&book.source,
lformat!("problem when writing LaTeX: {error}", error = e),
)
})?;
Ok(())
}
}
@ -585,11 +680,12 @@ impl BookRenderer for ProofLatex {
fn render(&self, book: &Book, to: &mut dyn 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))
})?;
to.write_all(result.as_bytes()).map_err(|e| {
Error::render(
&book.source,
lformat!("problem when writing LaTeX: {error}", error = e),
)
})?;
Ok(())
}
}
@ -600,8 +696,7 @@ impl BookRenderer for Pdf {
}
fn render(&self, book: &Book, to: &mut dyn io::Write) -> Result<()> {
LatexRenderer::new(book)
.render_pdf(to)?;
LatexRenderer::new(book).render_pdf(to)?;
Ok(())
}
}
@ -612,9 +707,7 @@ impl BookRenderer for ProofPdf {
}
fn render(&self, book: &Book, to: &mut dyn io::Write) -> Result<()> {
LatexRenderer::new(book)
.proofread()
.render_pdf(to)?;
LatexRenderer::new(book).proofread().render_pdf(to)?;
Ok(())
}
}
@ -625,10 +718,10 @@ 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),
}
}

View File

@ -113,46 +113,45 @@ extern crate lazy_static;
#[macro_use]
extern crate serde_derive;
pub use parser::Parser;
pub use book::Book;
pub use book_renderer::BookRenderer;
pub use bookoption::BookOption;
pub use bookoptions::BookOptions;
pub use error::{Result, Error, Source};
pub use token::Token;
pub use token::Data;
pub use number::Number;
pub use resource_handler::ResourceHandler;
pub use renderer::Renderer;
pub use book_renderer::BookRenderer;
pub use chapter::Chapter;
pub use error::{Error, Result, Source};
pub use number::Number;
pub use parser::Parser;
pub use renderer::Renderer;
pub use resource_handler::ResourceHandler;
pub use stats::Stats;
pub use token::Data;
pub use token::Token;
#[macro_use]
#[doc(hidden)]
mod localize_macros;
#[macro_use]
mod html;
mod html_dir;
mod error;
mod book;
mod book_renderer;
mod bookoptions;
mod chapter;
mod cleaner;
mod epub;
mod error;
mod html_dir;
mod html_if;
mod html_single;
mod lang;
mod latex;
mod number;
mod odt;
mod parser;
mod token;
mod cleaner;
mod chapter;
mod number;
mod resource_handler;
mod bookoptions;
mod lang;
mod renderer;
mod book_renderer;
mod html_single;
mod html_if;
mod syntax;
mod resource_handler;
mod stats;
mod syntax;
mod token;
#[cfg(feature = "binary")]
mod style;
@ -168,20 +167,18 @@ mod book_bars_stubs;
#[cfg(not(feature = "indicatif"))]
use book_bars_stubs as book_bars;
mod zipper;
mod templates;
mod bookoption;
mod misc;
mod templates;
mod text_view;
mod zipper;
#[cfg(feature = "proofread")]
mod grammar_check;
#[cfg(feature = "proofread")]
mod grammalecte;
#[cfg(feature = "proofread")]
mod grammar_check;
#[cfg(feature = "proofread")]
mod repetition_check;
#[cfg(test)]
mod tests;

View File

@ -20,17 +20,15 @@
use crate::token::Token;
use std;
use std::path::{Path, PathBuf};
use std::io::Result;
use std::path::{Path, PathBuf};
/// Try to canonicalize a path using std::fs::canonicalize, and returns the
/// unmodified path if it fails (e.g. if the path doesn't exist (yet))
pub fn normalize<P: AsRef<Path>>(path: P) -> String {
try_normalize(path.as_ref())
.unwrap_or_else(|_| format!("{}", path.as_ref().display()))
try_normalize(path.as_ref()).unwrap_or_else(|_| format!("{}", path.as_ref().display()))
}
fn try_normalize<P: AsRef<Path>>(path: P) -> Result<String> {
let full_path = std::fs::canonicalize(path.as_ref())?;
let mut cwd = std::env::current_dir()?;
@ -61,5 +59,5 @@ pub fn insert_title(tokens: &mut Vec<Token>) {
return;
}
}
tokens.insert(0, Token::Header(1, vec!()));
tokens.insert(0, Token::Header(1, vec![]));
}

View File

@ -31,14 +31,11 @@ pub enum Number {
SpecifiedPart(i32),
}
impl Number {
/// Returns true if self is a part
pub fn is_part(&self) -> bool {
match *self {
Number::UnnumberedPart |
Number::DefaultPart |
Number::SpecifiedPart(..) => true,
Number::UnnumberedPart | Number::DefaultPart | Number::SpecifiedPart(..) => true,
_ => false,
}
}

View File

@ -1,11 +1,11 @@
use crate::token::Token;
use crate::book::{Book, compile_str};
use crate::number::Number;
use crate::error::Result;
use crate::templates::odt;
use crate::zipper::Zipper;
use crate::parser::Parser;
use crate::book::{compile_str, Book};
use crate::book_renderer::BookRenderer;
use crate::error::Result;
use crate::number::Number;
use crate::parser::Parser;
use crate::templates::odt;
use crate::token::Token;
use crate::zipper::Zipper;
use crowbook_text_processing::escape;
@ -30,7 +30,8 @@ impl<'a> OdtRenderer<'a> {
current_chapter: 1,
current_numbering: book.options.get_i32("rendering.num_depth").unwrap(),
current_hide: false,
automatic_styles: String::from("
automatic_styles: String::from(
"
<style:style style:name=\"T1\" \
style:family=\"text\">
<style:text-properties \
@ -45,7 +46,8 @@ impl<'a> OdtRenderer<'a> {
fo:font-weight=\"bold\" \
style:font-weight-asian=\"bold\" \
style:font-weight-complex=\"bold\"/>
</style:style>"),
</style:style>",
),
}
}
@ -61,8 +63,7 @@ impl<'a> OdtRenderer<'a> {
pub fn render_book(&mut self, to: &mut dyn Write) -> Result<String> {
let content = self.render_content()?;
let mut zipper =
Zipper::new(&self.book.options.get_path("crowbook.temp_dir").unwrap())?;
let mut zipper = Zipper::new(&self.book.options.get_path("crowbook.temp_dir").unwrap())?;
// Write template.odt there
zipper.write("template.odt", odt::ODT, false)?;
@ -71,22 +72,40 @@ impl<'a> OdtRenderer<'a> {
// Complete it with content.xml
zipper.write("content.xml", content.as_bytes(), false)?;
// Zip and copy
zipper.generate_odt(self.book.options.get_str("crowbook.zip.command").unwrap(),
to)
zipper.generate_odt(
self.book.options.get_str("crowbook.zip.command").unwrap(),
to,
)
}
/// Render content.xml
fn render_content(&mut self) -> Result<String> {
// Print a warning for the features that aren't supported in ODT.
let mut missing = vec![];
if self.book.features.image { missing.push(lformat!("images")); }
if self.book.features.blockquote { missing.push(lformat!("blockquotes")); }
if self.book.features.codeblock { missing.push(lformat!("codeblocks")); }
if self.book.features.ordered_list { missing.push(lformat!("ordered lists")); }
if self.book.features.footnote { missing.push(lformat!("footnotes")); }
if self.book.features.table { missing.push(lformat!("tables")); }
if self.book.features.superscript { missing.push(lformat!("superscript")); }
if self.book.features.subscript { missing.push(lformat!("subscript")); }
if self.book.features.image {
missing.push(lformat!("images"));
}
if self.book.features.blockquote {
missing.push(lformat!("blockquotes"));
}
if self.book.features.codeblock {
missing.push(lformat!("codeblocks"));
}
if self.book.features.ordered_list {
missing.push(lformat!("ordered lists"));
}
if self.book.features.footnote {
missing.push(lformat!("footnotes"));
}
if self.book.features.table {
missing.push(lformat!("tables"));
}
if self.book.features.superscript {
missing.push(lformat!("superscript"));
}
if self.book.features.subscript {
missing.push(lformat!("subscript"));
}
if !missing.is_empty() {
let missing = missing.join(", ");
@ -94,7 +113,6 @@ impl<'a> OdtRenderer<'a> {
features = missing));
}
let mut content = String::new();
for chapter in &self.book.chapters {
@ -115,7 +133,7 @@ impl<'a> OdtRenderer<'a> {
self.current_numbering = 0;
self.current_hide = true;
}
}
}
if n.is_part() {
error!("{}", lformat!("Parts are not supported yet in ODT"));
}
@ -125,10 +143,14 @@ impl<'a> OdtRenderer<'a> {
}
}
let template = compile_str(odt::CONTENT,
&self.book.source,
"could not compile template for content.xml")?;
let data = self.book.get_metadata(|s| Ok(s.to_owned()))?
let template = compile_str(
odt::CONTENT,
&self.book.source,
"could not compile template for content.xml",
)?;
let data = self
.book
.get_metadata(|s| Ok(s.to_owned()))?
.insert_str("content", content)
.insert_str("automatic_styles", self.automatic_styles.clone())
.build();
@ -155,8 +177,10 @@ impl<'a> OdtRenderer<'a> {
match *token {
Token::Str(ref text) => escape::html(self.book.clean(text.as_str())).into_owned(),
Token::Paragraph(ref vec) => {
format!("<text:p text:style-name=\"Text_20_body\">{}</text:p>\n",
self.render_vec(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 {
@ -165,80 +189,88 @@ impl<'a> OdtRenderer<'a> {
let s = if n == 1 && self.current_numbering >= 1 {
let chapter = self.current_chapter;
self.current_chapter += 1;
let res = self.book.get_chapter_header(chapter, self.render_vec(vec), |s| {
Ok(self.render_vec(&Parser::new().parse_inline(s)?))
});
let res = self
.book
.get_chapter_header(chapter, self.render_vec(vec), |s| {
Ok(self.render_vec(&Parser::new().parse_inline(s)?))
});
res.unwrap().text
} else {
self.render_vec(vec)
};
format!("<text:h text:style-name=\"Heading_20_{}\">\n{}</text:h>\n",
n,
s)
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))
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))
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) => {
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))
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))
format!(
"<text:a xlink:type=\"simple\" xlink:href=\"{}\">{}</text:a>",
url,
self.render_vec(vec)
)
}
Token::Code(ref s) => {
format!("<text:span text:style-name=\"Preformatted_20_Text\">{}</text:span>",
s)
format!(
"<text:span text:style-name=\"Preformatted_20_Text\">{}</text:span>",
s
)
}
Token::Subscript(ref vec) | Token::Superscript(ref vec) => self.render_vec(vec),
Token::BlockQuote(ref vec) => format!("<text:p text:style-name=\"Text_20_Body\">{}</text:p>\n",
self.render_vec(vec)),
Token::BlockQuote(ref vec) => format!(
"<text:p text:style-name=\"Text_20_Body\">{}</text:p>\n",
self.render_vec(vec)
),
Token::CodeBlock(_, ref s) => {
format!("<text:p text:style-name=\"Text_20_Body\">{}</text:p>\n",
s)
format!("<text:p text:style-name=\"Text_20_Body\">{}</text:p>\n", s)
}
Token::SoftBreak | Token::HardBreak => String::from(" "),
Token::Rule => String::from("<text:p /><text:p>***</text:p><text:p />"),
Token::Image(_, _, _) |
Token::StandaloneImage(_, _, _) => {
Token::Image(_, _, _) | Token::StandaloneImage(_, _, _) => String::from(" "),
Token::Table(_, _) | Token::TableHead(_) | Token::TableRow(_) | Token::TableCell(_) => {
String::from(" ")
}
Token::Table(_, _) |
Token::TableHead(_) |
Token::TableRow(_) |
Token::TableCell(_) => {
String::from(" ")
}
Token::FootnoteReference(..) | Token::FootnoteDefinition(..) => {
String::new()
}
Token::FootnoteReference(..) | Token::FootnoteDefinition(..) => String::new(),
Token::Annotation(_, ref vec) => self.render_vec(vec),
Token::__NonExhaustive => unreachable!(),
Token::DescriptionList(ref v) |
Token::DescriptionItem(ref v) |
Token::DescriptionTerm(ref v) |
Token::DescriptionDetails(ref v) |
Token::Strikethrough(ref v) |
Token::TaskItem(_, ref v) => {
warn!("{}", lformat!("ODT: Description list and strikethrough not handled in this output"));
Token::DescriptionList(ref v)
| Token::DescriptionItem(ref v)
| Token::DescriptionTerm(ref v)
| Token::DescriptionDetails(ref v)
| Token::Strikethrough(ref v)
| Token::TaskItem(_, ref v) => {
warn!(
"{}",
lformat!("ODT: Description list and strikethrough not handled in this output")
);
self.render_vec(v)
}
}
}
}
pub struct Odt {}
impl BookRenderer for Odt {
@ -247,8 +279,7 @@ impl BookRenderer for Odt {
}
fn render(&self, book: &Book, to: &mut dyn Write) -> Result<()> {
OdtRenderer::new(book)
.render_book(to)?;
OdtRenderer::new(book).render_book(to)?;
Ok(())
}
}

View File

@ -233,7 +233,7 @@ impl Parser {
fn parse_node<'a>(&mut self, node: &'a AstNode<'a>) -> Result<Vec<Token>> {
let mut inner = vec![];
// Some special cases where we need to modifiy a bit the state of the parser between parsing inner content
if let NodeValue::DescriptionTerm = node.data.borrow().value {
self.ignore_paragraphs = true;
@ -242,7 +242,7 @@ impl Parser {
let mut v = self.parse_node(c)?;
inner.append(&mut v);
}
// Reset state after special cases shenanigans
// Reset state after special cases shenanigans
if let NodeValue::DescriptionTerm = node.data.borrow().value {
// There should be no paragraphs inside description terms
self.ignore_paragraphs = false;
@ -340,7 +340,7 @@ impl Parser {
NodeValue::TaskItem(checked) => {
self.features.taskitem = true;
vec![Token::TaskItem(checked, inner)]
},
}
NodeValue::Strong => vec![Token::Strong(inner)],
NodeValue::Strikethrough => {
self.features.strikethrough = true;

View File

@ -15,8 +15,8 @@
// 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 crate::token::Token;
use crate::error::Result;
use crate::token::Token;
/// Renderer trait, implemented by various renderer to render a list of `Token`s.
pub trait Renderer {
@ -25,7 +25,8 @@ pub trait Renderer {
/// Renders a vector of tokens
fn render_vec(&mut self, tokens: &[Token]) -> Result<String> {
tokens.iter()
tokens
.iter()
.map(|token| self.render_token(token))
.collect::<Result<Vec<_>>>()
.map(|vec| vec.join(""))

View File

@ -18,11 +18,11 @@
use caribon::Parser;
use crate::book::Book;
use crate::text_view::view_as_text;
use crate::text_view::insert_annotation;
use crate::token::Token;
use crate::token::Data;
use crate::error::{Error, Result, Source};
use crate::text_view::insert_annotation;
use crate::text_view::view_as_text;
use crate::token::Data;
use crate::token::Token;
/// Repetition detector
pub struct RepetitionDetector {
@ -39,11 +39,26 @@ impl RepetitionDetector {
pub fn new(book: &Book) -> RepetitionDetector {
RepetitionDetector {
lang: book.options.get_str("lang").unwrap().to_string(),
fuzzy: book.options.get_bool("proofread.repetitions.fuzzy").unwrap(),
fuzzy_threshold: book.options.get_f32("proofread.repetitions.fuzzy.threshold").unwrap(),
ignore_proper: book.options.get_bool("proofread.repetitions.ignore_proper").unwrap(),
max_distance: book.options.get_i32("proofread.repetitions.max_distance").unwrap(),
threshold: book.options.get_f32("proofread.repetitions.threshold").unwrap(),
fuzzy: book
.options
.get_bool("proofread.repetitions.fuzzy")
.unwrap(),
fuzzy_threshold: book
.options
.get_f32("proofread.repetitions.fuzzy.threshold")
.unwrap(),
ignore_proper: book
.options
.get_bool("proofread.repetitions.ignore_proper")
.unwrap(),
max_distance: book
.options
.get_i32("proofread.repetitions.max_distance")
.unwrap(),
threshold: book
.options
.get_f32("proofread.repetitions.threshold")
.unwrap(),
}
}
@ -57,33 +72,41 @@ impl RepetitionDetector {
None
};
let mut parser = Parser::new(&self.lang)
.map_err(|err| Error::default(Source::empty(),
lformat!("could not create caribon parser: {error}", error = err)))?
.map_err(|err| {
Error::default(
Source::empty(),
lformat!("could not create caribon parser: {error}", error = err),
)
})?
.with_fuzzy(fuzzy)
.with_html(false)
.with_ignore_proper(self.ignore_proper)
.with_max_distance(self.max_distance as u32);
for token in tokens.iter_mut() {
match *token {
Token::Paragraph(ref mut v) |
Token::Header(_, ref mut v) |
Token::BlockQuote(ref mut v) |
Token::List(ref mut v) |
Token::OrderedList(_, ref mut v) => {
let mut ast = parser.tokenize(&view_as_text(v))
.map_err(|err| Error::default(Source::empty(),
lformat!("error detecting repetitions: {err}",
err = err)))?;
Token::Paragraph(ref mut v)
| Token::Header(_, ref mut v)
| Token::BlockQuote(ref mut v)
| Token::List(ref mut v)
| Token::OrderedList(_, ref mut v) => {
let mut ast = parser.tokenize(&view_as_text(v)).map_err(|err| {
Error::default(
Source::empty(),
lformat!("error detecting repetitions: {err}", err = err),
)
})?;
parser.detect_local(&mut ast, self.threshold);
let repetitions = parser.ast_to_repetitions(&ast);
for repetition in &repetitions {
insert_annotation(v,
&Data::Repetition(repetition.colour.to_string()),
repetition.offset,
repetition.length);
insert_annotation(
v,
&Data::Repetition(repetition.colour.to_string()),
repetition.offset,
repetition.length,
);
}
},
}
_ => (),
}
@ -91,4 +114,3 @@ impl RepetitionDetector {
Ok(())
}
}

View File

@ -1,15 +1,15 @@
use crate::token::Token;
use crate::error::{Error, Result, Source};
use crate::token::Token;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::borrow::Cow;
use std::collections::HashMap;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use rustc_serialize::base64::{self, ToBase64};
use mime_guess;
use rustc_serialize::base64::{self, ToBase64};
use walkdir::WalkDir;
/// Resource Handler.
///
@ -58,22 +58,32 @@ impl ResourceHandler {
/// Add a local image file and get the resulting transformed
/// file name
pub fn map_image<'a, S: Into<Cow<'a, str>>>(&'a mut self,
source: &Source,
file: S)
-> Result<Cow<'a, str>> {
pub fn map_image<'a, S: Into<Cow<'a, str>>>(
&'a mut self,
source: &Source,
file: S,
) -> Result<Cow<'a, str>> {
// If image is not local, do nothing much
let file = file.into();
if !Self::is_local(file.as_ref()) {
warn!("{}", lformat!("Resources: book includes non-local image {file}, which might \
warn!(
"{}",
lformat!(
"Resources: book includes non-local image {file}, which might \
cause problem for proper inclusion.",
file = file));
file = file
)
);
return Ok(file);
}
// Check exisence of the file
if fs::metadata(file.as_ref()).is_err() {
return Err(Error::file_not_found(source, lformat!("image"), format!("{}", file)));
return Err(Error::file_not_found(
source,
lformat!("image"),
format!("{}", file),
));
}
// if image mapping is not activated do nothing else
@ -90,35 +100,51 @@ impl ResourceHandler {
// (or a base64 version of the file)
let dest_file = if !(self.base64) {
if let Some(extension) = Path::new(file.as_ref()).extension() {
format!("images/image_{}.{}",
self.images.len(),
extension.to_string_lossy())
format!(
"images/image_{}.{}",
self.images.len(),
extension.to_string_lossy()
)
} else {
warn!("{}", lformat!("Resources: book includes image {file} which doesn't have \
warn!(
"{}",
lformat!(
"Resources: book includes image {file} which doesn't have \
an extension",
file = file));
file = file
)
);
format!("images/image_{}", self.images.len())
}
} else {
let mut f = match fs::canonicalize(file.as_ref())
.and_then(|f| fs::File::open(f)) {
let mut f = match fs::canonicalize(file.as_ref()).and_then(|f| fs::File::open(f)) {
Ok(f) => f,
Err(_) => {
return Err(Error::file_not_found(source,
lformat!("image"),
format!("{}", file)));
return Err(Error::file_not_found(
source,
lformat!("image"),
format!("{}", file),
));
}
};
let mut content: Vec<u8> = vec![];
if f.read_to_end(&mut content).is_err() {
error!("{}", lformat!("Resources: could not read file {file}", file = file));
error!(
"{}",
lformat!("Resources: could not read file {file}", file = file)
);
return Ok(file);
}
let base64 = content.to_base64(base64::STANDARD);
match mime_guess::from_path(file.as_ref()).first() {
None => {
error!("{}", lformat!("Resources: could not guess mime type of file {file}",
file = file));
error!(
"{}",
lformat!(
"Resources: could not guess mime type of file {file}",
file = file
)
);
return Ok(file);
}
Some(s) => format!("data:{};base64,{}", s.to_string(), base64),
@ -136,7 +162,7 @@ impl ResourceHandler {
}
/// Add a match between an original file and a dest file
pub fn add_link<S1: Into<String>, S2:Into<String>>(&mut self, from: S1, to: S2) {
pub fn add_link<S1: Into<String>, S2: Into<String>>(&mut self, from: S1, to: S2) {
self.links.insert(from.into(), to.into());
}
@ -146,30 +172,30 @@ impl ResourceHandler {
link
} else {
// Try to get a link by changing the extension
let new_from = format!("{}", Path::new(from)
.with_extension("md")
.display());
let new_from = format!("{}", Path::new(from).with_extension("md").display());
if let Some(link) = self.links.get(&new_from) {
link
} else {
warn!("{}", lformat!("Resources: could not find an in-book match for link \
warn!(
"{}",
lformat!(
"Resources: could not find an in-book match for link \
{file}",
file = from));
file = from
)
);
from
}
}
}
/// Tell whether a file name is a local resource or net
pub fn contains_link(&self, from: &str) -> bool {
if self.links.contains_key(from) {
true
} else {
// Try to get a link by changing the extension
let new_from = format!("{}", Path::new(from)
.with_extension("md")
.display());
let new_from = format!("{}", Path::new(from).with_extension("md").display());
if self.links.contains_key(&new_from) {
true
} else {
@ -197,8 +223,8 @@ impl ResourceHandler {
}
Self::add_offset(link_offset, image_offset, v);
}
Token::Image(ref mut url, _, ref mut v) |
Token::StandaloneImage(ref mut url, _, ref mut v) => {
Token::Image(ref mut url, _, ref mut v)
| Token::StandaloneImage(ref mut url, _, ref mut v) => {
if ResourceHandler::is_local(url) {
let new_url = format!("{}", image_offset.join(&url).display());
*url = new_url;
@ -231,10 +257,14 @@ pub fn get_files(list: &[String], base: &str) -> Result<Vec<String>> {
let res = fs::metadata(&abs_path);
match res {
Err(err) => {
return Err(Error::render(Source::empty(),
lformat!("error reading file {file}: {error}",
file = abs_path.display(),
error = err)))
return Err(Error::render(
Source::empty(),
lformat!(
"error reading file {file}: {error}",
file = abs_path.display(),
error = err
),
))
}
Ok(metadata) => {
if metadata.is_file() {
@ -245,19 +275,19 @@ pub fn get_files(list: &[String], base: &str) -> Result<Vec<String>> {
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
.map(|e| {
PathBuf::from(e.path()
.strip_prefix(base)
.unwrap())
});
.map(|e| PathBuf::from(e.path().strip_prefix(base).unwrap()));
for file in files {
out.push(file.to_string_lossy().into_owned());
}
} else {
return Err(Error::render(Source::empty(),
lformat!("error in epub rendering: {path} is \
return Err(Error::render(
Source::empty(),
lformat!(
"error in epub rendering: {path} is \
neither a file nor a directory",
path = &path)));
path = &path
),
));
}
}
}

View File

@ -30,7 +30,6 @@ pub fn element(msg: &str) -> StyledObject<&str> {
style(msg).yellow().bold()
}
pub fn field(msg: &str) -> StyledObject<&str> {
style(msg).cyan().bold()
}
@ -44,8 +43,7 @@ pub fn value(msg: &str) -> StyledObject<&str> {
}
pub fn fill(msg: &str, indent: &str) -> String {
let (_, width) = Term::stdout()
.size();
let (_, width) = Term::stdout().size();
let wrapper = Wrapper::new(width as usize)
.initial_indent(indent)
.subsequent_indent(indent);

View File

@ -18,9 +18,21 @@
//! Functions for not setting styles on text, not using console to make it look prettier.
//! (stub implementation when optional dependency disabled)
pub fn header(msg: &str) -> &str { msg }
pub fn element(msg: &str) -> &str { msg }
pub fn field(msg: &str) -> &str { msg }
pub fn tipe(msg: &str) -> &str { msg }
pub fn value(msg: &str) -> &str { msg }
pub fn fill(msg: &str, indent: &str) -> String { format!("{}{}", indent, msg) }
pub fn header(msg: &str) -> &str {
msg
}
pub fn element(msg: &str) -> &str {
msg
}
pub fn field(msg: &str) -> &str {
msg
}
pub fn tipe(msg: &str) -> &str {
msg
}
pub fn value(msg: &str) -> &str {
msg
}
pub fn fill(msg: &str, indent: &str) -> String {
format!("{}{}", indent, msg)
}

View File

@ -19,21 +19,20 @@ use crate::error::Result;
use crowbook_text_processing::escape;
#[cfg(feature="syntect")]
#[cfg(feature = "syntect")]
use syntect;
/// Wrapper around syntect, so it can be more easily optionally compiled.
#[cfg(feature="syntect")]
#[cfg(feature = "syntect")]
pub struct Syntax {
syntax_set: syntect::parsing::SyntaxSet,
theme: syntect::highlighting::Theme,
}
#[cfg(not(feature="syntect"))]
#[cfg(not(feature = "syntect"))]
pub struct Syntax {}
#[cfg(feature="syntect")]
#[cfg(feature = "syntect")]
impl Syntax {
/// Creates a new Syntax wrapper
pub fn new(theme_name: &str) -> Syntax {
@ -41,14 +40,25 @@ impl Syntax {
let theme = match theme_set.themes.remove(theme_name) {
Some(theme) => theme,
None => {
error!("{}", lformat!("could not set syntect theme to {theme}, defaulting to \"InspiredGitHub\"",
theme = theme_name));
info!("{}", lformat!("valid theme names are: {themes}",
themes = theme_set.themes
.keys()
.map(|s| s.to_owned())
.collect::<Vec<_>>()
.join(", ")));
error!(
"{}",
lformat!(
"could not set syntect theme to {theme}, defaulting to \"InspiredGitHub\"",
theme = theme_name
)
);
info!(
"{}",
lformat!(
"valid theme names are: {themes}",
themes = theme_set
.themes
.keys()
.map(|s| s.to_owned())
.collect::<Vec<_>>()
.join(", ")
)
);
theme_set.themes.remove("InspiredGitHub").unwrap()
}
};
@ -61,20 +71,26 @@ impl Syntax {
/// Convert a string containing code to HTML
pub fn to_html(&self, code: &str, language: &str) -> Result<String> {
let language = strip_language(language);
let syntax = self.syntax_set.find_syntax_by_token(language)
let syntax = self
.syntax_set
.find_syntax_by_token(language)
.unwrap_or_else(|| self.syntax_set.find_syntax_plain_text());
let mut h = syntect::easy::HighlightLines::new(syntax, &self.theme);
let regions = h.highlight(code, &self.syntax_set);
let bg = syntect::html::IncludeBackground::No;
Ok(format!("<pre>{}</pre>",
syntect::html::styled_line_to_highlighted_html(&regions[..], bg)))
Ok(format!(
"<pre>{}</pre>",
syntect::html::styled_line_to_highlighted_html(&regions[..], bg)
))
}
pub fn to_tex(&self, code: &str, language: &str) -> Result<String> {
let language = strip_language(language);
use crate::latex::insert_breaks;
use syntect::highlighting::{Color, FontStyle};
let syntax = self.syntax_set.find_syntax_by_token(language)
let syntax = self
.syntax_set
.find_syntax_by_token(language)
.unwrap_or_else(|| self.syntax_set.find_syntax_plain_text());
let mut h = syntect::easy::HighlightLines::new(syntax, &self.theme);
let regions = h.highlight(code, &self.syntax_set);
@ -83,18 +99,21 @@ impl Syntax {
for (style, text) in regions {
let mut content = escape::tex(text).into_owned();
content = insert_breaks(&content);
content = content.replace('\n', "\\\\{}\n")
content = content
.replace('\n', "\\\\{}\n")
.replace(' ', "\\hphantom{ }\\allowbreak{}");
content = format!("\\texttt{{{}}}", content);
if style.foreground != Color::BLACK {
let r = style.foreground.r as f32 / 255.0;
let g = style.foreground.g as f32 / 255.0;
let b = style.foreground.b as f32 / 255.0;
content = format!("\\textcolor[rgb]{{{r}, {g}, {b}}}{{{text}}}",
r = r,
g = g,
b = b,
text = content);
content = format!(
"\\textcolor[rgb]{{{r}, {g}, {b}}}{{{text}}}",
r = r,
g = g,
b = b,
text = content
);
}
if style.font_style.contains(FontStyle::BOLD) {
content = format!("\\textbf{{{}}}", content);
@ -117,28 +136,31 @@ fn strip_language(language: &str) -> &str {
let splits: Vec<_> = language
.split(|c: char| match c {
',' => true,
_ => false
_ => false,
})
.collect();
splits[0].trim()
}
#[cfg(not(feature="syntect"))]
#[cfg(not(feature = "syntect"))]
impl Syntax {
pub fn new( _: &str) -> Syntax {
pub fn new(_: &str) -> Syntax {
error!("{}", lformat!("crowbook was compiled without syntect support, syntax highlighting will be disabled"));
Syntax {}
}
pub fn to_html(&self, code: &str, language: &str) -> Result<String> {
Ok(format!("<pre><code class = \"language-{lang}\">{code}</code></pre>",
code = escape::html(code),
lang = language))
Ok(format!(
"<pre><code class = \"language-{lang}\">{code}</code></pre>",
code = escape::html(code),
lang = language
))
}
pub fn to_tex(&self, code: &str, _: &str) -> Result<String> {
Ok(format!("\\begin{{spverbatim}}{}\\end{{spverbatim}}\n",
code))
Ok(format!(
"\\begin{{spverbatim}}{}\\end{{spverbatim}}\n",
code
))
}
}

View File

@ -21,12 +21,13 @@ pub mod highlight {
}
pub mod html {
pub static CSS: &str = concat!(include_str!("../../templates/epub/stylesheet.css"),
include_str!("../../templates/html/template.css"));
pub static CSS: &str = concat!(
include_str!("../../templates/epub/stylesheet.css"),
include_str!("../../templates/html/template.css")
);
pub static CSS_COLORS: &str = include_str!("../../templates/html/colors.css");
pub static PRINT_CSS: &str = include_str!("../../templates/html/print.css");
pub static JS: &str = include_str!("../../templates/html/script.js");
}
pub mod img {

View File

@ -1,5 +1,5 @@
use crate::book::Book;
use super::test_eq;
use crate::book::Book;
#[test]
fn load_config() {
@ -15,7 +15,9 @@ epub.version: 3";
book.read_config(config.as_bytes()).unwrap();
test_eq(book.options.get_str("author").unwrap(), "Author");
test_eq(book.options.get_str("title").unwrap(), "Some title");
test_eq(book.options.get_str("description").unwrap(),
"A long description");
test_eq(
book.options.get_str("description").unwrap(),
"A long description",
);
assert_eq!(book.options.get_i32("epub.version").unwrap(), 3);
}

View File

@ -1,9 +1,12 @@
/// Equivalent to assert_eq! but with prettier output
pub fn test_eq(actual: &str, expected: &str) {
if actual != expected {
panic!(format!("\nexpected:\n{:?}\nactual:\n{:?}\n", expected, actual));
panic!(format!(
"\nexpected:\n{:?}\nactual:\n{:?}\n",
expected, actual
));
}
}
mod parser;
mod book;
mod parser;

View File

@ -177,4 +177,3 @@ fn table_simple() {
let result = format!("{:?}", parse_from_str(doc));
test_eq(&result, expected);
}

View File

@ -15,33 +15,33 @@
// 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 std::mem;
use std::default::Default;
use std::mem;
use crate::token::Token;
use crate::token::Data;
use crate::token::Token;
pub fn traverse_token<F1, F2, R>(token: &Token, f: &F1, add: &F2) -> R
where F1: Fn(&str) -> R,
R: Default,
F2: Fn(R, R) -> R
where
F1: Fn(&str) -> R,
R: Default,
F2: Fn(R, R) -> R,
{
match *token {
Token::Str(ref s) | Token::Code(ref s) | Token::CodeBlock(_, ref s) => f(s),
Token::SoftBreak => f(" "),
Token::Rule |
Token::HardBreak => f("\n"),
Token::Rule | Token::HardBreak => f("\n"),
Token::Image(..) |
Token::StandaloneImage(..) |
Token::FootnoteDefinition(..) |
Token::FootnoteReference(..) |
Token::Table(..) |
Token::TableHead(..) |
Token::TableRow(..) |
Token::TableCell(..) => f(""),
Token::Image(..)
| Token::StandaloneImage(..)
| Token::FootnoteDefinition(..)
| Token::FootnoteReference(..)
| Token::Table(..)
| Token::TableHead(..)
| Token::TableRow(..)
| Token::TableCell(..) => f(""),
_ => traverse_vec(token.inner().unwrap(), f, add),
}
@ -50,17 +50,17 @@ pub fn traverse_token<F1, F2, R>(token: &Token, f: &F1, add: &F2) -> R
/// Traverse a vector of tokens
#[doc(hidden)]
pub fn traverse_vec<F1, F2, R>(tokens: &[Token], f: &F1, add: &F2) -> R
where F1: Fn(&str) -> R,
F2: Fn(R, R) -> R,
R: Default
where
F1: Fn(&str) -> R,
F2: Fn(R, R) -> R,
R: Default,
{
tokens.iter()
tokens
.iter()
.map(|t| traverse_token(t, f, add))
.fold(R::default(), add)
}
/// Returns the content of an AST as raw text, without any formatting
/// Useful for tools like grammar checks
pub fn view_as_text(tokens: &[Token]) -> String {
@ -71,14 +71,14 @@ pub fn count_length(tokens: &[Token]) -> usize {
traverse_vec(tokens, &|s| s.chars().count(), &|s1, s2| s1 + s2)
}
/// Insert an annotation at begin and end pos begin+len in the text_view
#[doc(hidden)]
pub fn insert_annotation(tokens: &mut Vec<Token>,
annotation: &Data,
pos: usize,
length: usize)
-> Option<usize> {
pub fn insert_annotation(
tokens: &mut Vec<Token>,
annotation: &Data,
pos: usize,
length: usize,
) -> Option<usize> {
let mut pos = pos;
let mut found_left = None;
let mut found_right = None;
@ -128,10 +128,15 @@ pub fn insert_annotation(tokens: &mut Vec<Token>,
if found_left.is_none() {
true
} else {
warn!("{}", lformat!("ignored annotation {:?} as it \
warn!(
"{}",
lformat!(
"ignored annotation {:?} as it \
wasn't compatible with the \
Markdown structure",
annotation));
annotation
)
);
return None;
}
} else {
@ -141,7 +146,6 @@ pub fn insert_annotation(tokens: &mut Vec<Token>,
} else {
false
}
}
};
@ -186,9 +190,10 @@ pub fn insert_annotation(tokens: &mut Vec<Token>,
tokens.insert(i + 1, inline_token);
}
}
let annot = Token::Annotation(annotation.clone(),
vec![Token::Str(chars_right.into_iter()
.collect())]);
let annot = Token::Annotation(
annotation.clone(),
vec![Token::Str(chars_right.into_iter().collect())],
);
if pos_left == 0 {
tokens.insert(i, annot)
} else if i == tokens.len() {
@ -220,7 +225,6 @@ pub fn insert_annotation(tokens: &mut Vec<Token>,
i + 1
};
if !tokens[j].is_str() {
// do nothing
} else {
@ -256,19 +260,25 @@ pub fn insert_annotation(tokens: &mut Vec<Token>,
} else if found_left.is_none() && found_right.is_none() {
return Some(pos);
} else {
warn!("{}", lformat!("ignored annotation {:?} as it wasn't compatible \
warn!(
"{}",
lformat!(
"ignored annotation {:?} as it wasn't compatible \
with the Markdown structure",
annotation));
annotation
)
);
return None;
}
}
#[test]
fn test_text_view() {
let ast = vec![Token::Str("123".to_owned()),
Token::Emphasis(vec![Token::Str("456".to_owned())]),
Token::Str("789".to_owned())];
let ast = vec![
Token::Str("123".to_owned()),
Token::Emphasis(vec![Token::Str("456".to_owned())]),
Token::Str("789".to_owned()),
];
assert_eq!(view_as_text(&ast), "123456789");
}

View File

@ -119,34 +119,39 @@ impl Token {
/// Returns the inner list of tokens contained in this token (if any)
pub fn inner(&self) -> Option<&[Token]> {
match *self {
Rule | SoftBreak | HardBreak | Str(_) |
CodeBlock(_, _) | Code(_) | FootnoteReference(_) => None,
Rule
| SoftBreak
| HardBreak
| Str(_)
| CodeBlock(_, _)
| Code(_)
| FootnoteReference(_) => None,
Paragraph(ref v) |
Header(_, ref v) |
Emphasis(ref v) |
Strong(ref v) |
BlockQuote(ref v) |
Subscript(ref v) |
Superscript(ref v) |
List(ref v) |
OrderedList(_, ref v) |
Item(ref v) |
DescriptionList(ref v) |
DescriptionItem(ref v) |
DescriptionTerm(ref v) |
DescriptionDetails(ref v) |
Table(_, ref v) |
TableHead(ref v) |
TableRow(ref v) |
TableCell(ref v) |
FootnoteDefinition(_, ref v) |
Link(_, _, ref v) |
Image(_, _, ref v) |
StandaloneImage(_, _, ref v) |
Strikethrough(ref v) |
TaskItem(_, ref v) |
Annotation(_, ref v) => Some(v),
Paragraph(ref v)
| Header(_, ref v)
| Emphasis(ref v)
| Strong(ref v)
| BlockQuote(ref v)
| Subscript(ref v)
| Superscript(ref v)
| List(ref v)
| OrderedList(_, ref v)
| Item(ref v)
| DescriptionList(ref v)
| DescriptionItem(ref v)
| DescriptionTerm(ref v)
| DescriptionDetails(ref v)
| Table(_, ref v)
| TableHead(ref v)
| TableRow(ref v)
| TableCell(ref v)
| FootnoteDefinition(_, ref v)
| Link(_, _, ref v)
| Image(_, _, ref v)
| StandaloneImage(_, _, ref v)
| Strikethrough(ref v)
| TaskItem(_, ref v)
| Annotation(_, ref v) => Some(v),
__NonExhaustive => unreachable!(),
}
@ -155,34 +160,39 @@ impl Token {
/// Returns the inner list of tokens contained in this token (if any) (mutable version)
pub fn inner_mut(&mut self) -> Option<&mut Vec<Token>> {
match *self {
Rule | SoftBreak | HardBreak | Str(_) |
CodeBlock(_, _) | Code(_) | FootnoteReference(_) => None,
Rule
| SoftBreak
| HardBreak
| Str(_)
| CodeBlock(_, _)
| Code(_)
| FootnoteReference(_) => None,
Paragraph(ref mut v) |
Annotation(_, ref mut v) |
Header(_, ref mut v) |
Emphasis(ref mut v) |
Strong(ref mut v) |
BlockQuote(ref mut v) |
Subscript(ref mut v) |
Superscript(ref mut v) |
List(ref mut v) |
OrderedList(_, ref mut v) |
Item(ref mut v) |
DescriptionList(ref mut v) |
DescriptionItem(ref mut v) |
DescriptionTerm(ref mut v) |
DescriptionDetails(ref mut v) |
Table(_, ref mut v) |
TableHead(ref mut v) |
TableRow(ref mut v) |
TableCell(ref mut v) |
FootnoteDefinition(_, ref mut v) |
Link(_, _, ref mut v) |
Image(_, _, ref mut v) |
Strikethrough(ref mut v) |
TaskItem(_, ref mut v) |
StandaloneImage(_, _, ref mut v) => Some(v),
Paragraph(ref mut v)
| Annotation(_, ref mut v)
| Header(_, ref mut v)
| Emphasis(ref mut v)
| Strong(ref mut v)
| BlockQuote(ref mut v)
| Subscript(ref mut v)
| Superscript(ref mut v)
| List(ref mut v)
| OrderedList(_, ref mut v)
| Item(ref mut v)
| DescriptionList(ref mut v)
| DescriptionItem(ref mut v)
| DescriptionTerm(ref mut v)
| DescriptionDetails(ref mut v)
| Table(_, ref mut v)
| TableHead(ref mut v)
| TableRow(ref mut v)
| TableCell(ref mut v)
| FootnoteDefinition(_, ref mut v)
| Link(_, _, ref mut v)
| Image(_, _, ref mut v)
| Strikethrough(ref mut v)
| TaskItem(_, ref mut v)
| StandaloneImage(_, _, ref mut v) => Some(v),
__NonExhaustive => unreachable!(),
}
@ -224,19 +234,27 @@ impl Token {
pub fn is_code(&self) -> bool {
match *self {
Token::CodeBlock(..) | Token::Code(..) => true,
_ => false
_ => false,
}
}
/// Returns true if token is a container (paragraph, quote, emphasis, ..., but not links, images, and so on).
pub fn is_container(&self) -> bool {
match *self {
Token::Paragraph(..)
| Token::Header(..) | Token::Emphasis(..) | Token::Strong(..)
| Token::List(..) | Token::OrderedList(..) | Token::Table(..)
| Token::TableHead(..) | Token::TableRow(..) | Token::FootnoteDefinition(..)
| Token::TableCell(..) | Token::Annotation(..) | Token::Item(..)
| Token::BlockQuote(..) => true,
Token::Paragraph(..)
| Token::Header(..)
| Token::Emphasis(..)
| Token::Strong(..)
| Token::List(..)
| Token::OrderedList(..)
| Token::Table(..)
| Token::TableHead(..)
| Token::TableRow(..)
| Token::FootnoteDefinition(..)
| Token::TableCell(..)
| Token::Annotation(..)
| Token::Item(..)
| Token::BlockQuote(..) => true,
_ => false,
}
}

View File

@ -17,13 +17,13 @@
use crate::error::{Error, Result};
use std::path::{Path, PathBuf};
use std::fs::{self, DirBuilder, File};
use std::io;
use std::io::Write;
use std::process::Command;
use std::fs::{self, File, DirBuilder};
use uuid;
use std::ops::Drop;
use std::path::{Path, PathBuf};
use std::process::Command;
use uuid;
/// Struct used to create zip (using filesystem and zip command)
pub struct Zipper {
@ -45,8 +45,10 @@ impl Zipper {
.recursive(true)
.create(&zipper_path)
.map_err(|_| {
Error::zipper(lformat!("could not create temporary directory in {path}",
path = path))
Error::zipper(lformat!(
"could not create temporary directory in {path}",
path = path
))
})?;
Ok(Zipper {
@ -56,15 +58,17 @@ impl Zipper {
}
/// writes a content to a temporary file
pub fn write<P:AsRef<Path>>(&mut self, path: P, content: &[u8], add_args: bool) -> Result<()> {
pub fn write<P: AsRef<Path>>(&mut self, path: P, content: &[u8], add_args: bool) -> Result<()> {
let path = path.as_ref();
let file = format!("{}", path.display());
if path.starts_with("..") || path.is_absolute() {
return Err(Error::zipper(lformat!("file {file} refers to an absolute or a parent \
return Err(Error::zipper(lformat!(
"file {file} refers to an absolute or a parent \
path.
This is forbidden because we are supposed \
to create a temporary file in a temporary dir.",
file = file)));
file = file
)));
}
let dest_file = self.path.join(path);
let dest_dir = dest_file.parent().unwrap();
@ -74,12 +78,13 @@ This is forbidden because we are supposed \
.recursive(true)
.create(&dest_dir)
.map_err(|_| {
Error::zipper(lformat!("could not create temporary directory in {path}",
path = dest_dir.display()))
Error::zipper(lformat!(
"could not create temporary directory in {path}",
path = dest_dir.display()
))
})?;
}
if let Ok(mut f) = File::create(&dest_file) {
if f.write_all(content).is_ok() {
if add_args {
@ -87,11 +92,16 @@ This is forbidden because we are supposed \
}
Ok(())
} else {
Err(Error::zipper(lformat!("could not write to temporary file {file}",
file = file)))
Err(Error::zipper(lformat!(
"could not write to temporary file {file}",
file = file
)))
}
} else {
Err(Error::zipper(lformat!("could not create temporary file {file}", file = file)))
Err(Error::zipper(lformat!(
"could not create temporary file {file}",
file = file
)))
}
}
@ -102,9 +112,11 @@ This is forbidden because we are supposed \
.arg(file)
.output()
.map_err(|e| {
Error::zipper(lformat!("failed to execute unzip on {file}: {error}",
file = file,
error = e))
Error::zipper(lformat!(
"failed to execute unzip on {file}: {error}",
file = file,
error = e
))
});
output?;
@ -114,46 +126,67 @@ This is forbidden because we are supposed \
}
/// run command and copy content of file output (supposed to result from the command) to current dir
pub fn run_command(&mut self,
mut command: Command,
command_name: &str,
in_file: &str,
out: &mut dyn Write)
-> Result<String> {
let res_output = command.args(&self.args)
pub fn run_command(
&mut self,
mut command: Command,
command_name: &str,
in_file: &str,
out: &mut dyn Write,
) -> Result<String> {
let res_output = command
.args(&self.args)
.current_dir(&self.path)
.output()
.map_err(|e| {
debug!("{}", lformat!("output for command {name}:\n{error}",
name = command_name,
error = e));
Error::zipper(lformat!("failed to run command '{name}'",
name = command_name))
debug!(
"{}",
lformat!(
"output for command {name}:\n{error}",
name = command_name,
error = e
)
);
Error::zipper(lformat!(
"failed to run command '{name}'",
name = command_name
))
});
let output = res_output?;
if output.status.success() {
let mut file = File::open(self.path.join(in_file))
.map_err(|_| {
debug!("{}", lformat!("could not open result of command '{command}'\n\
let mut file = File::open(self.path.join(in_file)).map_err(|_| {
debug!(
"{}",
lformat!(
"could not open result of command '{command}'\n\
Command output:\n\
{output}'",
command = command_name,
output = String::from_utf8_lossy(&output.stdout)));
Error::zipper(lformat!("could not open result of command '{command}'",
command = command_name))
})?;
io::copy(&mut file, out)
.map_err(|_| Error::zipper(lformat!("error copying file '{file}'",
file = in_file)))?;
command = command_name,
output = String::from_utf8_lossy(&output.stdout)
)
);
Error::zipper(lformat!(
"could not open result of command '{command}'",
command = command_name
))
})?;
io::copy(&mut file, out).map_err(|_| {
Error::zipper(lformat!("error copying file '{file}'", file = in_file))
})?;
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
} else {
debug!("{}",
lformat!("{command} didn't return succesfully: {output}",
command = command_name,
output = String::from_utf8_lossy(&output.stdout)));
Err(Error::zipper(lformat!("{command} didn't return succesfully",
command = command_name)))
debug!(
"{}",
lformat!(
"{command} didn't return succesfully: {output}",
command = command_name,
output = String::from_utf8_lossy(&output.stdout)
)
);
Err(Error::zipper(lformat!(
"{command} didn't return succesfully",
command = command_name
)))
}
}
@ -166,17 +199,16 @@ This is forbidden because we are supposed \
self.run_command(command, command_name, "result.odt", odt_file)
}
/// generate a pdf file into given file name
pub fn generate_pdf(&mut self,
command_name: &str,
tex_file: &str,
pdf_file: &mut dyn Write)
-> Result<String> {
pub fn generate_pdf(
&mut self,
command_name: &str,
tex_file: &str,
pdf_file: &mut dyn Write,
) -> Result<String> {
// first pass
let mut command = Command::new(command_name);
command.current_dir(&self.path)
.arg(tex_file);
command.current_dir(&self.path).arg(tex_file);
let _ = command.output();
// second pass
@ -193,9 +225,11 @@ This is forbidden because we are supposed \
impl Drop for Zipper {
fn drop(&mut self) {
if let Err(err) = fs::remove_dir_all(&self.path) {
println!("Error in zipper: could not delete temporary directory {}, error: {}",
self.path.to_string_lossy(),
err);
println!(
"Error in zipper: could not delete temporary directory {}, error: {}",
self.path.to_string_lossy(),
err
);
}
}
}

View File

@ -4,8 +4,12 @@ use std::io;
#[test]
fn test_book() {
let mut book = Book::new();
book.load_file(&format!("{}/{}", env!("CARGO_MANIFEST_DIR"), "tests/test.book"))
.unwrap();
book.load_file(&format!(
"{}/{}",
env!("CARGO_MANIFEST_DIR"),
"tests/test.book"
))
.unwrap();
book.render_format_to("html", &mut io::sink()).unwrap();
book.render_format_to("tex", &mut io::sink()).unwrap();
}