Files

EPUB

E-books made from chapters of HTML or Markdown, with a title, author, cover and table of contents, and their details read back.

Version 1.0.0 · 2 functions · 3 crates

#ebooks#export#files#markdown

Functions

What use epub.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use epub_write, epub_info from epub (Quillo 0.6).

epub_write(path: str, book: any, chapters: list) → intor an error

Writes an EPUB at path (folders are made) and gives the number of chapters. book is { title, author, lang, description, cover, css }: lang like "en" (the default), cover the path of a JPEG or PNG, css a style sheet (a plain one otherwise). Each chapter is { title, html }: html is the chapter's body (from markdown(...) too); when it has no <h1>, the title is put above it.

epub_info(path: str) → anyor an error

What an EPUB file says about itself: { title, author, lang, items, files }: items counts the pages in its reading order (chapters, and a contents page if it has one).

Get it

$ quillo install epub               # epub.qwer into this folder$ quillo install epub --example     # and its example, epub.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/epub.qwer$ curl -O https://quillolang.org/plugins/files/epub.qwe$ curl -O https://quillolang.org/plugins/files/epub_sample.jpg

Then, in your program, all of it or only what you call:

main.qwe
use epub.qweruse epub_write, epub_info from epub      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
epub-builder = { version = "0.8", default-features = false, features = ["zip-library"] }zip = { version = "2", default-features = false, features = ["deflate"] }roxmltree = "0.20"

Example

epub.qwe
// A short e-book made from chapters written in Markdown, ready to download.////   quillo run epub.qwe      then open http://localhost:8080/book.epubuse "epub.qwer"let chapters = [    { title: "Beans", text: "Good coffee starts with **fresh beans**, roasted no more than a month ago." },    { title: "Water", text: "Use water just off the boil: about 94 °C.\n\n- Soft water tastes sweeter\n- Hard water dulls the cup" },    { title: "Time", text: "Four minutes for a French press. Then pour it all, or it goes bitter." },]fn make_book() = epub_write("data/coffee.epub",    { title: "Coffee at Home", author: "Quillo Café", lang: "en", description: "Three short lessons.", cover: "epub_sample.jpg" },    chapters | map((c) => { title: c.title, html: str(markdown(c.text)) }))route GET /book.epub {    make_book()?    file("data/coffee.epub")}route GET /info {    make_book()?    epub_info("data/coffee.epub")}
$ quillo run epub.qwe$ curl 'localhost:8080/info'$ curl 'localhost:8080/book.epub'

Source

epub.qwer
//! E-books (EPUB 3) made from chapters of HTML, with a title, author, cover and table of contents, and an e-book's details read back.//! version: 1.0.0//!//! [dependencies]//! epub-builder = { version = "0.8", default-features = false, features = ["zip-library"] }//! zip = { version = "2", default-features = false, features = ["deflate"] }//! roxmltree = "0.20"use epub_builder::{EpubBuilder, EpubContent, EpubVersion, ReferenceType, ZipLibrary};use serde_json::{json, Value};use std::io::Read;const MAX_CHAPTERS: usize = 2000;const DEFAULT_CSS: &str = "body { font-family: serif; line-height: 1.5; margin: 0 5%; } h1, h2, h3 { font-family: sans-serif; line-height: 1.2; } img { max-width: 100%; } pre { white-space: pre-wrap; }";fn esc(s: &str) -> String {    s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;")}// HTML as e-readers' XHTML wants it: void elements closed, and the named entities XML does not know as numbers.fn xhtml(html: &str) -> String {    let mut out = String::with_capacity(html.len() + 64);    let mut rest = html;    while let Some(i) = rest.find('<') {        out.push_str(&rest[..i]);        rest = &rest[i..];        let Some(end) = rest.find('>') else { break };        let tag = &rest[..=end];        let name: String = tag[1..].chars().take_while(|c| c.is_ascii_alphanumeric()).collect::<String>().to_lowercase();        if matches!(name.as_str(), "br" | "hr" | "img" | "meta" | "link" | "input" | "wbr" | "col" | "source") && !tag.ends_with("/>") {            out.push_str(&tag[..tag.len() - 1]);            out.push_str("/>");        } else {            out.push_str(tag);        }        rest = &rest[end + 1..];    }    out.push_str(rest);    for (named, num) in [("&nbsp;", "&#160;"), ("&copy;", "&#169;"), ("&mdash;", "&#8212;"), ("&ndash;", "&#8211;"), ("&hellip;", "&#8230;"), ("&rsquo;", "&#8217;"), ("&lsquo;", "&#8216;"), ("&ldquo;", "&#8220;"), ("&rdquo;", "&#8221;"), ("&eacute;", "&#233;")] {        out = out.replace(named, num);    }    out}fn page(title: &str, lang: &str, body: &str) -> String {    format!("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\" xml:lang=\"{l}\" lang=\"{l}\">\n<head><meta charset=\"utf-8\"/><title>{t}</title><link rel=\"stylesheet\" type=\"text/css\" href=\"stylesheet.css\"/></head>\n<body>\n{b}\n</body>\n</html>\n", l = esc(lang), t = esc(title), b = body)}fn text(v: &Value, key: &str) -> String {    v.get(key).and_then(|x| x.as_str()).unwrap_or("").to_string()}/// Writes an EPUB at `path` (folders are made) and gives the number of chapters./// `book` is `{ title, author, lang, description, cover, css }`: `lang` like "en" (the default), `cover` the path of a JPEG or PNG, `css` a style sheet (a plain one otherwise)./// Each chapter is `{ title, html }`: `html` is the chapter's body (from `markdown(...)` too); when it has no `<h1>`, the title is put above it.pub fn epub_write(path: &str, book: serde_json::Value, chapters: Vec<serde_json::Value>) -> Result<i64, String> {    if chapters.is_empty() || chapters.len() > MAX_CHAPTERS {        return Err(format!("give 1 to {} chapters", MAX_CHAPTERS));    }    let title = text(&book, "title");    if title.is_empty() {        return Err("the book needs a title".into());    }    let lang = Some(text(&book, "lang")).filter(|l| !l.is_empty()).unwrap_or_else(|| "en".into());    let e = |e: epub_builder::Error| e.to_string();    let mut b = EpubBuilder::new(ZipLibrary::new().map_err(e)?).map_err(e)?;    b.epub_version(EpubVersion::V30);    b.set_title(title.clone());    b.set_lang(lang.clone());    b.set_generator("Quillo");    let author = text(&book, "author");    if !author.is_empty() {        b.add_author(author);    }    let desc = text(&book, "description");    if !desc.is_empty() {        b.add_description(desc);    }    let css = Some(text(&book, "css")).filter(|c| !c.is_empty()).unwrap_or_else(|| DEFAULT_CSS.into());    b.stylesheet(css.as_bytes()).map_err(e)?;    let cover = text(&book, "cover");    if !cover.is_empty() {        let data = std::fs::read(&cover).map_err(|err| format!("{}: {}", cover, err))?;        let (ext, mime) = if data.starts_with(&[0x89, b'P', b'N', b'G']) { ("png", "image/png") } else if data.starts_with(&[0xFF, 0xD8]) { ("jpg", "image/jpeg") } else { return Err(format!("{}: the cover is a JPEG or a PNG", cover)) };        b.add_cover_image(format!("cover.{}", ext), data.as_slice(), mime).map_err(e)?;    }    for (i, ch) in chapters.iter().enumerate() {        let ct = Some(text(ch, "title")).filter(|t| !t.is_empty()).unwrap_or_else(|| format!("Chapter {}", i + 1));        let mut html = xhtml(&text(ch, "html"));        if !html.to_lowercase().contains("<h1") {            html = format!("<h1>{}</h1>\n{}", esc(&ct), html);        }        let content = page(&ct, &lang, &html);        b.add_content(EpubContent::new(format!("chapter_{}.xhtml", i + 1), content.as_bytes()).title(ct).reftype(ReferenceType::Text)).map_err(e)?;    }    b.inline_toc();    if let Some(dir) = std::path::Path::new(path).parent().filter(|d| !d.as_os_str().is_empty()) {        std::fs::create_dir_all(dir).map_err(|err| format!("{}: {}", dir.display(), err))?;    }    let f = std::fs::File::create(path).map_err(|err| format!("{}: {}", path, err))?;    b.generate(f).map_err(e)?;    Ok(chapters.len() as i64)}/// What an EPUB file says about itself: `{ title, author, lang, items, files }`: `items` counts the pages in its reading order (chapters, and a contents page if it has one).pub fn epub_info(path: &str) -> Result<Value, String> {    let f = std::fs::File::open(path).map_err(|e| format!("{}: {}", path, e))?;    let mut z = zip::ZipArchive::new(f).map_err(|_| format!("{}: not an EPUB (it is not a zip)", path))?;    let mut read = |name: &str| -> Option<String> {        let e = z.by_name(name).ok()?;        let mut s = String::new();        e.take(16 * 1024 * 1024).read_to_string(&mut s).ok()?;        Some(s)    };    let container = read("META-INF/container.xml").ok_or_else(|| format!("{}: not an EPUB (no META-INF/container.xml)", path))?;    let cdoc = roxmltree::Document::parse(&container).map_err(|e| e.to_string())?;    let opf_path = cdoc.descendants().find(|n| n.has_tag_name("rootfile")).and_then(|n| n.attribute("full-path")).ok_or("the EPUB names no package file")?.to_string();    let opf = read(&opf_path).ok_or_else(|| format!("the EPUB has no {}", opf_path))?;    let doc = roxmltree::Document::parse(&opf).map_err(|e| e.to_string())?;    let dc = |name: &str| doc.descendants().find(|n| n.is_element() && n.tag_name().name() == name).and_then(|n| n.text()).map(|t| t.trim().to_string());    Ok(json!({        "title": dc("title"),        "author": dc("creator"),        "lang": dc("language"),        "items": doc.descendants().filter(|n| n.is_element() && n.tag_name().name() == "itemref").count(),        "files": z.len(),    }))}
Thread
88Useful? Like it, or add to its thread below.

Thread 0 posts

On EPUB: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.