#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_docs)]
use std::fmt;
use std::io;
use std::sync::Arc;
use carta_ast::{Block, Document, Inline};
#[cfg(feature = "container")]
#[cfg_attr(docsrs, doc(cfg(feature = "container")))]
pub mod container;
pub mod extensions;
pub mod media;
pub mod sections;
#[cfg(feature = "template")]
#[cfg_attr(docsrs, doc(cfg(feature = "template")))]
pub mod template;
pub mod walk;
pub use extensions::{Extension, Extensions, presets};
pub use media::{MediaBag, MediaItem};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("input is not valid UTF-8: {0}")]
InvalidUtf8(#[from] std::str::Utf8Error),
#[error("format '{0}' converts binary data; use the byte-capable API (convert)")]
BinaryFormat(String),
#[error("unsupported format: {0}")]
UnsupportedFormat(String),
#[error("format '{0}' is recognized but not enabled in this build")]
FormatNotEnabled(String),
#[error("unknown extension: {0}")]
UnknownExtension(String),
#[error(
"The extension '{extension}' is not supported for {format}.\nUse --list-extensions={format} to list supported extensions."
)]
UnsupportedExtension {
extension: String,
format: String,
},
#[error("invalid document metadata: {0}")]
InvalidMetadata(String),
#[error("template error: {0}")]
Template(String),
#[error("cannot represent this content in the target format: {0}")]
Unrepresentable(String),
#[error("container error: {0}")]
Container(String),
#[error("filter error: {0}")]
Filter(String),
}
#[cfg(feature = "template")]
impl From<template::TemplateError> for Error {
fn from(error: template::TemplateError) -> Self {
Error::Template(error.to_string())
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ReaderOptions {
pub extensions: Extensions,
pub greedy_paragraphs: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum MathMethod {
#[default]
Plain,
MathJax(String),
Katex(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TocStyle {
#[default]
List,
Native,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WrapMode {
#[default]
Auto,
None,
Preserve,
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct EpubOptions {
pub cover_image: Option<(String, Vec<u8>)>,
pub fonts: Vec<(String, Vec<u8>)>,
pub stylesheets: Vec<String>,
pub metadata_xml: Option<String>,
pub subdirectory: Option<String>,
pub split_level: Option<usize>,
pub source_date_epoch: Option<i64>,
pub locale: Option<String>,
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct WriterOptions {
pub extensions: Extensions,
pub media: Arc<MediaBag>,
pub epub: EpubOptions,
pub wrap: WrapMode,
pub columns: Option<usize>,
pub number_sections: bool,
pub toc: bool,
pub toc_depth: Option<usize>,
pub math_method: MathMethod,
#[cfg(feature = "template")]
#[cfg_attr(docsrs, doc(cfg(feature = "template")))]
pub standalone: bool,
#[cfg(feature = "template")]
#[cfg_attr(docsrs, doc(cfg(feature = "template")))]
pub template: Option<String>,
#[cfg(feature = "template")]
#[cfg_attr(docsrs, doc(cfg(feature = "template")))]
pub template_dir: Option<std::path::PathBuf>,
#[cfg(feature = "template")]
#[cfg_attr(docsrs, doc(cfg(feature = "template")))]
pub template_datadir: Option<std::path::PathBuf>,
#[cfg(feature = "template")]
#[cfg_attr(docsrs, doc(cfg(feature = "template")))]
pub template_ext: Option<String>,
#[cfg(feature = "template")]
#[cfg_attr(docsrs, doc(cfg(feature = "template")))]
pub variables: Vec<(String, String)>,
#[cfg(feature = "template")]
#[cfg_attr(docsrs, doc(cfg(feature = "template")))]
pub metadata: std::collections::BTreeMap<String, carta_ast::MetaValue>,
#[cfg(feature = "template")]
#[cfg_attr(docsrs, doc(cfg(feature = "template")))]
pub metadata_defaults: std::collections::BTreeMap<String, carta_ast::MetaValue>,
#[cfg(any(feature = "template", feature = "container"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "template", feature = "container"))))]
pub source_name: Option<String>,
}
pub trait Reader {
fn read(&self, input: &str, options: &ReaderOptions) -> Result<Document>;
fn read_media(&self, input: &str, options: &ReaderOptions) -> Result<(Document, MediaBag)> {
Ok((self.read(input, options)?, MediaBag::new()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MetaVarStyle {
#[default]
None,
Web,
Pdf,
}
pub trait Writer {
fn write(&self, document: &Document, options: &WriterOptions) -> Result<String>;
fn render_meta_inlines(&self, inlines: &[Inline], options: &WriterOptions) -> Result<String> {
let document = Document {
blocks: vec![Block::Plain(inlines.to_vec())],
..Document::default()
};
Ok(self
.write(&document, options)?
.trim_end_matches('\n')
.to_string())
}
fn render_meta_blocks(&self, blocks: &[Block], options: &WriterOptions) -> Result<String> {
let document = Document {
blocks: blocks.to_vec(),
..Document::default()
};
Ok(self
.write(&document, options)?
.trim_end_matches('\n')
.to_string())
}
fn default_template(&self) -> Option<&'static str> {
None
}
fn standalone_document(
&self,
document: &Document,
options: &WriterOptions,
) -> Result<Option<String>> {
let _ = (document, options);
Ok(None)
}
fn meta_var_style(&self) -> MetaVarStyle {
MetaVarStyle::None
}
fn flatten_block_metadata(&self) -> bool {
false
}
fn title_block(&self, document: &Document, options: &WriterOptions) -> Result<Option<String>> {
let _ = (document, options);
Ok(None)
}
fn body_ends_with_newline(&self) -> bool {
false
}
fn toc_style(&self) -> TocStyle {
TocStyle::List
}
fn toc_link_anchors(&self) -> bool {
true
}
fn numbers_sections_natively(&self) -> bool {
false
}
fn numbers_sections_in_body(&self) -> bool {
false
}
}
pub trait BytesReader {
fn read(&self, input: &[u8], options: &ReaderOptions) -> Result<Document>;
fn read_media(&self, input: &[u8], options: &ReaderOptions) -> Result<(Document, MediaBag)> {
Ok((self.read(input, options)?, MediaBag::new()))
}
}
pub trait BytesWriter {
fn write(&self, document: &Document, options: &WriterOptions) -> Result<Vec<u8>>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Output {
Text(String),
Bytes(Vec<u8>),
}
pub enum AnyReader {
Text(Box<dyn Reader>),
Bytes(Box<dyn BytesReader>),
}
impl fmt::Debug for AnyReader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let variant = match self {
AnyReader::Text(_) => "Text",
AnyReader::Bytes(_) => "Bytes",
};
f.debug_tuple(variant).finish()
}
}
impl AnyReader {
pub fn read(&self, input: &[u8], options: &ReaderOptions) -> Result<Document> {
match self {
AnyReader::Text(reader) => reader.read(std::str::from_utf8(input)?, options),
AnyReader::Bytes(reader) => reader.read(input, options),
}
}
pub fn read_media(
&self,
input: &[u8],
options: &ReaderOptions,
) -> Result<(Document, MediaBag)> {
match self {
AnyReader::Text(reader) => reader.read_media(std::str::from_utf8(input)?, options),
AnyReader::Bytes(reader) => reader.read_media(input, options),
}
}
}
pub enum AnyWriter {
Text(Box<dyn Writer>),
Bytes(Box<dyn BytesWriter>),
}
impl fmt::Debug for AnyWriter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let variant = match self {
AnyWriter::Text(_) => "Text",
AnyWriter::Bytes(_) => "Bytes",
};
f.debug_tuple(variant).finish()
}
}
impl AnyWriter {
#[must_use]
pub fn default_template(&self) -> Option<&'static str> {
match self {
AnyWriter::Text(writer) => writer.default_template(),
AnyWriter::Bytes(_) => None,
}
}
}
#[cfg(test)]
mod tests {
use super::{
AnyReader, AnyWriter, BytesReader, BytesWriter, Error, Reader, ReaderOptions, Result,
WriterOptions,
};
use carta_ast::Document;
struct FixedBytesWriter;
impl BytesWriter for FixedBytesWriter {
fn write(&self, _document: &Document, _options: &WriterOptions) -> Result<Vec<u8>> {
Ok(vec![0x00, 0xff, 0x9f])
}
}
struct RawBytesReader;
impl BytesReader for RawBytesReader {
fn read(&self, input: &[u8], _options: &ReaderOptions) -> Result<Document> {
assert_eq!(input, &[0xff, 0xfe]);
Ok(Document::default())
}
}
struct EmptyTextReader;
impl Reader for EmptyTextReader {
fn read(&self, _input: &str, _options: &ReaderOptions) -> Result<Document> {
Ok(Document::default())
}
}
#[test]
fn bytes_writer_round_trips_bytes() {
let writer = AnyWriter::Bytes(Box::new(FixedBytesWriter));
assert!(writer.default_template().is_none());
let AnyWriter::Bytes(inner) = &writer else {
panic!("expected a byte writer");
};
let output = inner
.write(&Document::default(), &WriterOptions::default())
.unwrap();
assert_eq!(output, vec![0x00, 0xff, 0x9f]);
}
#[test]
fn text_reader_rejects_invalid_utf8() {
let reader = AnyReader::Text(Box::new(EmptyTextReader));
let error = reader
.read(&[0xff, 0xfe], &ReaderOptions::default())
.unwrap_err();
assert!(matches!(error, Error::InvalidUtf8(_)), "{error:?}");
}
#[test]
fn bytes_reader_accepts_invalid_utf8() {
let reader = AnyReader::Bytes(Box::new(RawBytesReader));
assert!(
reader
.read(&[0xff, 0xfe], &ReaderOptions::default())
.is_ok()
);
}
#[test]
fn default_read_media_carries_no_resources() {
let reader = AnyReader::Text(Box::new(EmptyTextReader));
let (_, media) = reader
.read_media(b"anything", &ReaderOptions::default())
.expect("read succeeds");
assert!(media.is_empty());
}
}