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:
parent
f84c98438e
commit
b8d35ddc32
File diff suppressed because it is too large
Load Diff
34
build.rs
34
build.rs
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
846
src/lib/book.rs
846
src/lib/book.rs
|
@ -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
|
||||
),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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> {
|
||||
|
|
354
src/lib/epub.rs
354
src/lib/epub.rs
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
104
src/lib/error.rs
104
src/lib/error.rs
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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 {
|
||||
|
|
394
src/lib/html.rs
394
src/lib/html.rs
|
@ -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.
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
393
src/lib/latex.rs
393
src/lib/latex.rs
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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![]));
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
173
src/lib/odt.rs
173
src/lib/odt.rs
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(""))
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(®ions[..], bg)))
|
||||
Ok(format!(
|
||||
"<pre>{}</pre>",
|
||||
syntect::html::styled_line_to_highlighted_html(®ions[..], 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
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -177,4 +177,3 @@ fn table_simple() {
|
|||
let result = format!("{:?}", parse_from_str(doc));
|
||||
test_eq(&result, expected);
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
140
src/lib/token.rs
140
src/lib/token.rs
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue