#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_docs)]
pub use carta_ast as ast;
pub use carta_ast::Document;
pub use carta_core::{
AnyReader, AnyWriter, BytesReader, BytesWriter, DocxOptions, EpubOptions, Error, Extension,
Extensions, MathMethod, MediaBag, MediaItem, Output, Reader, ReaderOptions, Result, TocStyle,
WrapMode, Writer, WriterOptions, media, presets, walk,
};
use std::sync::Arc;
mod format_spec;
mod registry;
#[cfg(feature = "standalone")]
mod standalone;
pub use format_spec::parse_format_spec;
pub use registry::{
any_reader_for, any_writer_for, input_format_names, output_format_names, reader_for,
supported_input_formats, supported_output_formats, writer_for,
};
#[cfg(feature = "highlight")]
pub use carta_core::HighlightOptions;
#[cfg(feature = "highlight")]
pub use carta_highlight::{Highlighter, Theme, builtin_style, languages, styles};
#[cfg(feature = "write-html")]
pub use carta_writers::{Resource, inline_resources};
pub fn convert_text(
from: &str,
to: &str,
input: &str,
reader_options: &ReaderOptions,
writer_options: &WriterOptions,
) -> Result<String> {
match convert(from, to, input.as_bytes(), reader_options, writer_options)? {
Output::Text(text) => Ok(text),
Output::Bytes(_) => {
let (base, _) = parse_format_spec(to)?;
Err(Error::BinaryFormat(base))
}
}
}
pub fn convert(
from: &str,
to: &str,
input: &[u8],
reader_options: &ReaderOptions,
writer_options: &WriterOptions,
) -> Result<Output> {
let (document, media) = read_document(from, input, reader_options)?;
render_document(to, document, media, writer_options)
}
pub fn read_document(
from: &str,
input: &[u8],
reader_options: &ReaderOptions,
) -> Result<(Document, MediaBag)> {
let (from_base, from_ext) = format_spec::parse_reader_format_spec(from)?;
let reader = any_reader_for(&from_base)?;
let mut reader_options = reader_options.clone();
reader_options.extensions = from_ext.union(reader_options.extensions);
reader_options.greedy_paragraphs |= from_base.starts_with("markdown");
reader.read_media(input, &reader_options)
}
pub fn render_document(
to: &str,
document: Document,
media: MediaBag,
writer_options: &WriterOptions,
) -> Result<Output> {
stacker::maybe_grow(RENDER_STACK, RENDER_STACK, || {
render_body(to, document, media, writer_options)
})
}
const RENDER_STACK: usize = carta_core::DEEP_STACK;
fn render_body(
to: &str,
document: Document,
media: MediaBag,
writer_options: &WriterOptions,
) -> Result<Output> {
let (to_base, to_ext) = parse_format_spec(to)?;
let writer = any_writer_for(&to_base)?;
let mut writer_options = writer_options.clone();
writer_options.extensions = to_ext.union(writer_options.extensions);
writer_options.media = Arc::new(media);
#[cfg(feature = "standalone")]
let document = {
let mut document = document;
standalone::merge_metadata(&mut document, &writer_options);
document
};
let writer = match writer {
AnyWriter::Text(writer) => writer,
AnyWriter::Bytes(writer) => {
return writer.write(&document, &writer_options).map(Output::Bytes);
}
};
#[cfg(feature = "standalone")]
let wraps_standalone = writer_options.standalone || writer_options.template.is_some();
#[cfg(feature = "standalone")]
let mut toc_source = standalone::TocSource::Document;
let mut document = document;
let body = if writer_options.number_sections && writer.numbers_sections_in_body() {
#[cfg(feature = "standalone")]
if wraps_standalone && writer_options.toc && matches!(writer.toc_style(), TocStyle::List) {
toc_source = standalone::TocSource::Prebuilt(carta_core::sections::build_toc(
&document.blocks,
writer_options
.toc_depth
.unwrap_or(standalone::DEFAULT_TOC_DEPTH),
writer_options.number_sections,
writer.toc_link_anchors(),
));
}
carta_core::sections::number_sections(&mut document.blocks);
writer.write(&document, &writer_options)?
} else {
writer.write(&document, &writer_options)?
};
#[cfg(feature = "standalone")]
if wraps_standalone {
return standalone::render(
writer.as_ref(),
&document,
body,
&writer_options,
&to_base,
toc_source,
)
.map(Output::Text);
}
Ok(Output::Text(body))
}
#[cfg(feature = "standalone")]
#[cfg_attr(docsrs, doc(cfg(feature = "standalone")))]
pub fn merge_metadata(document: &mut Document, writer_options: &WriterOptions) {
standalone::merge_metadata(document, writer_options);
}
#[cfg(feature = "metadata-file")]
#[cfg_attr(docsrs, doc(cfg(feature = "metadata-file")))]
pub fn parse_metadata_file(
content: &str,
json: bool,
) -> Result<std::collections::BTreeMap<String, ast::MetaValue>> {
if json {
carta_readers::metadata::parse_json(content)
} else {
carta_readers::metadata::parse_yaml(content)
}
}
pub fn format_extensions(format: Option<&str>) -> Result<Vec<(Extension, bool)>> {
let (base, extensions) = format_spec::parse_reader_format_spec(format.unwrap_or("markdown"))?;
if !registry::reader_recognizes(&base) && !registry::writer_recognizes(&base) {
return Err(Error::UnsupportedFormat(base));
}
let supported = format_spec::supported_extensions(&base);
let mut entries: Vec<(Extension, bool)> = Extension::ALL
.iter()
.filter(|&&extension| supported.is_none_or(|set| set.contains(extension)))
.map(|&extension| (extension, extensions.contains(extension)))
.collect();
entries.sort_by_key(|(extension, _)| extension.name());
Ok(entries)
}