use structopt::StructOpt;
use rust_embed::RustEmbed;
use pulldown_cmark::{Parser, Event};
use pulldown_cmark_to_cmark::cmark;
use mdplayscript::interface::*;
use japanese_ruby_filter::pulldown_cmark_filter::RubyFilter;
use mdbook::preprocess::{PreprocessorContext, CmdPreprocessor};
use mdbook::book::{Book, BookItem};
use mdbook_playscript::counter::{IgnorePatterns, CounterFactory};
#[derive(Debug,StructOpt)]
struct PlayScriptOpt {
#[structopt(subcommand)]
command: Option<Command>,
}
#[derive(Debug,StructOpt)]
enum Command {
Supports {
renderer: String,
},
}
fn main() {
env_logger::Builder::new()
.filter_level(log::LevelFilter::Info)
.init();
let opt = PlayScriptOpt::from_args();
let preprocessor = PlayScriptPreprocessor::new();
let result = match opt.command {
Some(Command::Supports { renderer }) => {
handle_renderer(preprocessor, &renderer)
},
_ => {
handle_preprocessing(preprocessor)
},
};
if let Err(e) = result {
eprintln!("{}", e);
std::process::exit(1);
}
}
fn handle_renderer(prep: PlayScriptPreprocessor, renderer: &str) -> ! {
if prep.supports_renderer(renderer) {
std::process::exit(0);
} else {
std::process::exit(1);
}
}
fn handle_preprocessing(prep: PlayScriptPreprocessor) -> Result<(), mdbook::errors::Error> {
let (ctx, book) = CmdPreprocessor::parse_input(std::io::stdin())?;
let book = prep.run(&ctx, book)?;
serde_json::to_writer(std::io::stdout(), &book)?;
Ok(())
}
struct PlayScriptPreprocessor {
}
impl PlayScriptPreprocessor {
fn new() -> Self {
Self {
}
}
fn supports_renderer(&self, renderer: &str) -> bool {
match renderer {
"html" => true,
_ => false,
}
}
fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> mdbook::errors::Result<Book> {
let css = Stylesheet::from_context(ctx);
css.copy(ctx);
let count_script = CountScript::from_context(ctx);
count_script.copy(ctx);
let (options, is_japanese) = match ctx.config.book.language.as_ref() {
Some(lang) if lang == "ja" => (Options::default_ja(), true),
_ => (Options::default(), false),
};
let params = Params {
title: ctx.config.book.title.clone(),
subtitle: ctx.config.get("preprocessor.playscript.subtitle")
.and_then(|v| v.as_str())
.map(|s| s.to_owned()),
authors: ctx.config.book.authors.clone(),
};
let title_conj = ctx.config.get("preprocessor.playscript.title-conjunction")
.and_then(|v| v.as_str())
.map(|s| s.to_owned());
log::info!("title-conjunction: {:?}", title_conj);
let enable_ruby = ctx.config.get("preprocessor.playscript.japanese-ruby.enable")
.and_then(|v| v.as_bool())
.unwrap_or(false);
log::info!("japanese-ruby.enable: {}", enable_ruby);
let enable_counting = ctx.config.get("preprocessor.playscript.counting.enable")
.and_then(|v| v.as_bool())
.unwrap_or(false)
&& is_japanese;
log::info!("counting.enable: {}", enable_counting);
let mut counter_factory = CounterFactory::new("scene");
let ignored = ctx.config.get("preprocessor.playscript.counting.ignore")
.and_then(|v| v.as_array());
let ignored = if let Some(v) = ignored {
IgnorePatterns::from_toml_values(v)
} else {
IgnorePatterns::new()
};
book.for_each_mut(|book_item| {
match book_item {
BookItem::Chapter(chapter) => {
let title_conj = title_conj.clone();
let len = chapter.content.len();
let mut content = String::new();
std::mem::swap(&mut chapter.content, &mut content);
let parser: Box<dyn Iterator<Item=Event<'_>>> = if enable_ruby {
Box::new(RubyFilter::new(Parser::new(&content)))
} else {
Box::new(Parser::new(&content))
};
let mut parser = MdPlayScriptBuilder::new()
.params(params.clone())
.options(options.clone())
.make_title(Box::new(move |params| make_title_fn(params, title_conj.as_ref())))
.build(parser);
let counter = counter_factory.issue();
if enable_counting {
counter.set_class_to_renderer(parser.renderer_mut());
}
let mut processed = String::with_capacity(len + len/2);
let tail = if enable_counting {
counter.generate_placeholder(&ignored, chapter.source_path.as_ref())
} else {
vec![mdbook_playscript::IgnoredPlaceholder.to_event()]
};
cmark((&mut parser).chain(tail), &mut processed, None).unwrap();
std::mem::swap(&mut chapter.content, &mut processed);
},
_ => {},
}
});
Ok(book)
}
}
fn make_title_fn(params: &Params, conj: Option<&String>) -> String {
let mut cover = r#"<div class="cover">"#.to_owned();
if let Some(title) = params.title.as_ref() {
cover += &format!(r#"<h1 class="cover-title">{}</h1>"#, title);
}
if let Some(subtitle) = params.subtitle.as_ref() {
if let Some(conj) = conj.as_ref() {
cover += &format!(r#"<p class="cover-conjunction">{}</p>"#, conj);
}
cover += &format!(r#"<p class="cover-subtitle">{}</p>"#, subtitle);
}
if !params.authors.is_empty() {
cover += r#"<div class="cover-authors">"#;
for author in params.authors.iter() {
cover += &format!(r#"<p class="cover-author">{}</p>"#, author);
}
cover += "</div>";
}
cover += "</div>";
cover
}
#[derive(RustEmbed)]
#[folder = "$CARGO_MANIFEST_DIR/public"]
struct Asset;
trait AdditionalFile {
fn filename(&self) -> &str;
fn copy(&self, ctx: &PreprocessorContext) {
let mut path = ctx.root.clone();
assert!(path.exists(), "root directory does not exist");
let filename = self.filename();
path.push(filename);
if !cfg!(debug_assertions) && path.exists() {
log::info!("Additional file already exists: {}", filename);
return;
}
let file = Asset::get(filename).unwrap();
std::fs::write(&path, &file).unwrap();
}
}
struct Stylesheet {
filename: &'static str,
}
impl Stylesheet {
fn from_context(ctx: &PreprocessorContext) -> Self {
assert_eq!(ctx.renderer.as_str(), "html");
let filename = match ctx.config.book.language.as_ref() {
Some(lang) if lang == "ja" => "mdplayscript_ja.css",
_ => "mdplayscript.css",
};
Self {
filename: filename,
}
}
}
impl AdditionalFile for Stylesheet {
fn filename(&self) -> &str {
&self.filename
}
}
struct CountScript;
impl CountScript {
fn from_context(ctx: &PreprocessorContext) -> Self {
assert_eq!(ctx.renderer.as_str(), "html");
Self
}
}
impl AdditionalFile for CountScript {
fn filename(&self) -> &str {
"playscript-count.js"
}
}