Skip to main content

carta_core/
lib.rs

1//! Shared carta core: the conversion traits, their option types, and the common error type.
2//!
3//! [`Reader`] turns input text into a [`Document`]; [`Writer`] turns a [`Document`] back into
4//! output text. Readers and writers depend only on the AST contract and this crate, so input and
5//! output formats stay independent.
6
7use std::io;
8
9use carta_ast::{Block, Document, Inline};
10
11pub mod extensions;
12pub mod sections;
13#[cfg(feature = "template")]
14pub mod template;
15
16pub use extensions::{Extension, Extensions, presets};
17
18/// The error type returned across the conversion pipeline.
19#[derive(Debug, thiserror::Error)]
20pub enum Error {
21    #[error("JSON error: {0}")]
22    Json(#[from] serde_json::Error),
23    #[error("I/O error: {0}")]
24    Io(#[from] io::Error),
25    #[error("input is not valid UTF-8: {0}")]
26    InvalidUtf8(#[from] std::string::FromUtf8Error),
27    #[error("unsupported format: {0}")]
28    UnsupportedFormat(String),
29    #[error("format '{0}' is recognized but not enabled in this build")]
30    FormatNotEnabled(String),
31    #[error("unknown extension: {0}")]
32    UnknownExtension(String),
33    #[error(
34        "The extension '{extension}' is not supported for {format}.\nUse --list-extensions={format} to list supported extensions."
35    )]
36    UnsupportedExtension { extension: String, format: String },
37    #[error("invalid document metadata: {0}")]
38    InvalidMetadata(String),
39    #[error("template error: {0}")]
40    Template(String),
41    #[error("cannot represent this content in the target format: {0}")]
42    Unrepresentable(String),
43}
44
45#[cfg(feature = "template")]
46impl From<template::TemplateError> for Error {
47    fn from(error: template::TemplateError) -> Self {
48        Error::Template(error.to_string())
49    }
50}
51
52/// A `Result` whose error is [`Error`].
53pub type Result<T> = std::result::Result<T, Error>;
54
55/// Options controlling a [`Reader`]. Extended (not resignatured) as real options land.
56#[derive(Debug, Clone, Default)]
57#[non_exhaustive]
58pub struct ReaderOptions {
59    /// Format extensions to enable. Strict-CommonMark readers ignore this (the empty preset).
60    pub extensions: Extensions,
61    /// When set, an open paragraph is greedy: a following line that would otherwise open a block —
62    /// a blockquote, heading, list, thematic break, fenced div, or footnote definition — is folded
63    /// into the paragraph as a lazy continuation instead. Only a blank line, a fenced code block, or
64    /// an HTML block ends the paragraph. Unset, every such line interrupts the paragraph.
65    pub greedy_paragraphs: bool,
66}
67
68/// How math is presented by a format that offers a choice of renderers (the HTML family). The
69/// method decides both the inline markup inside a `span.math` and which loader a standalone document
70/// pulls in to typeset it: a MathJax (or plain) document carries the source TeX wrapped in `\(…\)` /
71/// `\[…\]`, whereas a KaTeX document carries the bare TeX, which its in-browser loader reads from the
72/// span directly.
73#[derive(Debug, Clone, PartialEq, Eq, Default)]
74pub enum MathMethod {
75    /// No renderer: the `\(…\)` / `\[…\]` markup is left for the reader to typeset (or read as
76    /// source). The default.
77    #[default]
78    Plain,
79    /// MathJax, loaded from the given script URL. The markup keeps the `\(…\)` / `\[…\]` delimiters.
80    MathJax(String),
81    /// KaTeX, loaded from the given asset base URL (the directory holding `katex.min.js` and its
82    /// stylesheet). The span carries bare TeX without delimiters.
83    Katex(String),
84}
85
86/// How a writer supplies a table of contents.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
88pub enum TocStyle {
89    /// The contents are rendered as a nested list and placed in the `toc` template variable. The
90    /// default.
91    #[default]
92    List,
93    /// The format assembles its own contents from a directive in its template, so only a boolean
94    /// `toc` flag is exposed and no list is generated.
95    Native,
96}
97
98/// How a text writer lays out the lines of a paragraph.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
100pub enum WrapMode {
101    /// Reflow inline content, breaking lines to keep them within the fill column. A soft line break
102    /// in the source is just inter-word space and is re-flowed like any other.
103    #[default]
104    Auto,
105    /// Never break a paragraph: each one is a single line, with soft breaks rendered as spaces. Lines
106    /// run as long as their content (only an explicit hard break starts a new line).
107    None,
108    /// Keep the source's own line breaks: a soft break stays a line break and content is not
109    /// reflowed, but lines are not wrapped to a column either.
110    Preserve,
111}
112
113/// Options controlling a [`Writer`]. Extended (not resignatured) as real options land.
114#[derive(Debug, Clone, Default)]
115#[non_exhaustive]
116pub struct WriterOptions {
117    /// Format extensions to enable.
118    pub extensions: Extensions,
119
120    /// How paragraphs are laid out: reflowed to the fill column, never wrapped, or with the source's
121    /// own line breaks preserved.
122    pub wrap: WrapMode,
123
124    /// The fill column a wrapping writer reflows to under [`WrapMode::Auto`]. `None` uses the
125    /// writer's built-in default width.
126    pub columns: Option<usize>,
127
128    /// Splice a hierarchical section number into each heading. A format that numbers headings with a
129    /// typesetting counter applies it through its template instead (see
130    /// [`Writer::numbers_sections_natively`]).
131    pub number_sections: bool,
132
133    /// Emit a table of contents in a standalone document.
134    pub toc: bool,
135
136    /// The deepest heading level the table of contents includes. `None` uses the conventional depth
137    /// of three.
138    pub toc_depth: Option<usize>,
139
140    /// How math is presented by a format offering a choice of renderers (the HTML family).
141    pub math_method: MathMethod,
142
143    /// Emit a complete document by wrapping the rendered body in the target format's template,
144    /// rather than a bare fragment.
145    #[cfg(feature = "template")]
146    pub standalone: bool,
147
148    /// Template source overriding the format's built-in default. Its presence implies standalone
149    /// output.
150    #[cfg(feature = "template")]
151    pub template: Option<String>,
152
153    /// Directory used to resolve template partials (`$name()$`).
154    #[cfg(feature = "template")]
155    pub template_dir: Option<std::path::PathBuf>,
156
157    /// Extension a partial (`$name()$`) inherits from the including template: the `--template`
158    /// file's own extension, so the same partial name resolves to the same kind of file whatever
159    /// the output format. An empty string means the template file had no extension (the partial is
160    /// looked up bare). Absent for a built-in default, where the format name is used instead.
161    #[cfg(feature = "template")]
162    pub template_ext: Option<String>,
163
164    /// Raw template variables, in order; a repeated key accumulates into a list. Inserted verbatim
165    /// (unescaped) at the highest precedence when building the template context.
166    #[cfg(feature = "template")]
167    pub variables: Vec<(String, String)>,
168
169    /// Metadata layered *above* the document's own (the `-M` layer): each key replaces the reader's
170    /// value for that key when the context is built.
171    #[cfg(feature = "template")]
172    pub metadata: std::collections::BTreeMap<String, carta_ast::MetaValue>,
173
174    /// Metadata layered *below* the document's own (the metadata-file layer): supplies defaults the
175    /// reader's values and `-M` override.
176    #[cfg(feature = "template")]
177    pub metadata_defaults: std::collections::BTreeMap<String, carta_ast::MetaValue>,
178
179    /// The source name an HTML-family standalone document falls back to for its `pagetitle` when no
180    /// `title` metadata is present: an input file's stem, or `-` for standard input. `None` outside
181    /// the command line, where there is no source name and the fallback is empty.
182    #[cfg(feature = "template")]
183    pub source_name: Option<String>,
184}
185
186/// Parses input text in some source format into the document model.
187pub trait Reader {
188    fn read(&self, input: &str, options: &ReaderOptions) -> Result<Document>;
189}
190
191/// Which plain-text identity variables a writer's standalone template draws on. The document's
192/// title, authors, and date are exposed as markup-free, target-escaped text for places that cannot
193/// carry markup — a web document head or a PDF document's properties. See [`Writer::meta_var_style`].
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
195pub enum MetaVarStyle {
196    /// The format exposes none of these variables.
197    #[default]
198    None,
199    /// A web document head: `pagetitle` (the title, falling back to the source name), `date-meta`
200    /// (the date), and `author-meta` (the authors, one list entry each).
201    Web,
202    /// A PDF document's properties: `title-meta` (the title) and `author-meta` (the authors joined
203    /// into one string with `; `).
204    Pdf,
205}
206
207/// Renders the document model into some target format's text.
208///
209/// The returned string carries no trailing newline; the CLI appends exactly one.
210pub trait Writer {
211    fn write(&self, document: &Document, options: &WriterOptions) -> Result<String>;
212
213    /// Render an inline sequence in this format, for interpolating inline metadata (a `title`, an
214    /// `author`) into a template variable. Wrapping the inlines in a [`Block::Plain`] yields them
215    /// with no paragraph chrome across formats; a writer whose `Plain` diverges overrides this.
216    ///
217    /// # Errors
218    /// Propagates any error from [`Writer::write`].
219    fn render_meta_inlines(&self, inlines: &[Inline], options: &WriterOptions) -> Result<String> {
220        let document = Document {
221            blocks: vec![Block::Plain(inlines.to_vec())],
222            ..Document::default()
223        };
224        Ok(self
225            .write(&document, options)?
226            .trim_end_matches('\n')
227            .to_string())
228    }
229
230    /// Render a block sequence in this format, for interpolating block metadata (an `abstract`
231    /// authored as Markdown blocks) into a template variable.
232    ///
233    /// # Errors
234    /// Propagates any error from [`Writer::write`].
235    fn render_meta_blocks(&self, blocks: &[Block], options: &WriterOptions) -> Result<String> {
236        let document = Document {
237            blocks: blocks.to_vec(),
238            ..Document::default()
239        };
240        Ok(self
241            .write(&document, options)?
242            .trim_end_matches('\n')
243            .to_string())
244    }
245
246    /// This format's own standalone template, or `None` when standalone output is identical to the
247    /// fragment (no wrapping document exists for the format).
248    fn default_template(&self) -> Option<&'static str> {
249        None
250    }
251
252    /// A standalone document this format assembles structurally, embedding the metadata and block
253    /// list in one value rather than wrapping a text body in a template — the data form is the
254    /// canonical example. Returned in place of template rendering. `None` (the default) when the
255    /// format wraps its body with a text template instead.
256    ///
257    /// # Errors
258    /// Propagates any error from rendering the document.
259    fn standalone_document(
260        &self,
261        document: &Document,
262        options: &WriterOptions,
263    ) -> Result<Option<String>> {
264        let _ = (document, options);
265        Ok(None)
266    }
267
268    /// Which plain-text identity variables this writer's standalone template draws on — the title,
269    /// authors, and date as markup-free text. The default is [`MetaVarStyle::None`]; an HTML-family
270    /// writer returns [`MetaVarStyle::Web`] and a LaTeX-family writer [`MetaVarStyle::Pdf`].
271    fn meta_var_style(&self) -> MetaVarStyle {
272        MetaVarStyle::None
273    }
274
275    /// Whether block-shaped metadata is flattened to its inline content when built into the template
276    /// context. A writer that places title, author, and date into single-line header fields — a man
277    /// page's `.TH` line cannot carry paragraph structure — sets this so a lone-paragraph value
278    /// contributes its inline text and any other block shape contributes nothing. The default `false`
279    /// renders block metadata as blocks.
280    fn flatten_block_metadata(&self) -> bool {
281        false
282    }
283
284    /// A title presentation the template language cannot express from individual variables — an
285    /// underlined title for reStructuredText, say, whose rule length depends on the rendered title
286    /// width. Exposed to the template as the `titleblock` variable. `None` (the default) when the
287    /// format builds its title presentation from individual variables instead.
288    ///
289    /// # Errors
290    /// Propagates any error from rendering the metadata.
291    fn title_block(&self, document: &Document, options: &WriterOptions) -> Result<Option<String>> {
292        let _ = (document, options);
293        Ok(None)
294    }
295
296    /// Whether this writer lays the document out as newline-terminated lines, so a non-empty `body`
297    /// template variable ends with a newline. Writers that build their markup as one string ending
298    /// at its final glyph (HTML, LaTeX, and the like) leave the default `false`.
299    fn body_ends_with_newline(&self) -> bool {
300        false
301    }
302
303    /// How this writer supplies a table of contents. The default renders a nested list into the
304    /// `toc` variable; a format whose template assembles its own contents from a directive overrides
305    /// to [`TocStyle::Native`].
306    fn toc_style(&self) -> TocStyle {
307        TocStyle::List
308    }
309
310    /// Whether a list-style table of contents attaches a back-reference anchor — an `id` on each
311    /// entry's link — so the entries can be linked to. The default includes them; a format that
312    /// cannot represent an inline identifier (so an attributed link would degrade to raw markup)
313    /// overrides to `false`. Honored only when [`toc_style`](Writer::toc_style) is [`TocStyle::List`].
314    fn toc_link_anchors(&self) -> bool {
315        true
316    }
317
318    /// Whether this format numbers sections with its own typesetting counter rather than carrying the
319    /// number in the heading text. The default splices a `header-section-number` span into each
320    /// heading; a format with a native counter (the typesetting formats) overrides to `true` and is
321    /// driven by a `numbersections` template flag instead.
322    fn numbers_sections_natively(&self) -> bool {
323        false
324    }
325
326    /// Whether this writer carries section numbers in the heading text, so the number is spliced into
327    /// each heading before rendering (and contents entries inherit it). The default leaves headings
328    /// untouched; a format that renders the number inline (HTML) overrides to `true`. A format with a
329    /// native counter relies on [`numbers_sections_natively`](Writer::numbers_sections_natively)
330    /// instead and leaves this `false`.
331    fn numbers_sections_in_body(&self) -> bool {
332        false
333    }
334}