dmos 0.7.0

Djot HTML renderer with advanced features
Documentation
use serde::{de, Deserialize};

#[derive(Clone, Debug)]
pub enum MetaFormat {
    Shell,
    Json,
}

/// Options controlling the metadata generation
#[derive(Clone, Debug)]
pub struct MetaOpts {
    pub format: MetaFormat,
}

impl Default for MetaOpts {
    fn default() -> Self {
        MetaOpts {
            format: MetaFormat::Shell,
        }
    }
}

/// Options controlling the table-of-contents generation.
#[derive(Clone, Debug, Deserialize)]
#[serde(default)]
pub struct TocOpts {
    /// Whether or not to render the table of contents.
    #[serde(rename = "dmos_toc", deserialize_with = "deserialize_bool")]
    pub render: bool,
    /// If `true`, do not include the document title in the outline.
    ///
    /// The title is the first top-level section heading. The next section
    /// heading will be considered top-level for the table of content, even if
    /// it is for example a second-level heading.
    ///
    /// <div class="warning">
    ///
    /// If `true`, the table of content will be emitted **after** the document
    /// title.
    ///
    /// </div>
    ///
    /// # Example
    ///
    /// Given the following djot snippet:
    ///
    /// ```md
    /// # My title
    ///
    /// ## My section
    ///
    /// Ohai!
    /// ```
    ///
    /// Setting `skip_title` to `false` will generate:
    ///
    /// ```html
    /// <section class="toc"><h3>Table of Contents</h3>
    /// <ol>
    /// <li><a href="#My-title">My title</a>
    /// <ul>
    /// <li><a href="#My-section">My section</a>
    /// </ul>
    /// </li>
    /// </ol>
    /// </section>
    /// <section id="My-title">
    /// <h1>My title</h1>
    /// <section id="My-section">
    /// <h2>My section</h2>
    /// <p>Ohai!</p>
    /// </section>
    /// </section>
    /// ```
    ///
    /// Whereas setting `skip_title` to `true` will generate:
    ///
    /// ```html
    /// <section id="My-title">
    /// <h1>My title</h1>
    /// <section class="toc"><h3>Table of Contents</h3>
    /// <ol>
    /// <li><a href="#My-section">My section</a></li>
    /// </ol>
    /// </section>
    /// <section id="My-section">
    /// <h2>My section</h2>
    /// <p>Ohai!</p>
    /// </section>
    /// </section>
    /// ```
    #[serde(rename = "dmos_toc_skip_title", deserialize_with = "deserialize_bool")]
    pub skip_title: bool,

    /// Arbitrary HTML to output before the table of contents list element.
    ///
    /// Can be used to customize the default output or for translation of the
    /// phrase "Table of contents".
    ///
    /// Defaults to `"<section class=\"toc\"><h3>Table of Contents</h3>"`.
    ///
    /// Another common prologue is for example
    /// `"<details><summary>Table of Content</summary>"` to get a collapsible
    /// table of contents.
    ///
    /// <div class="warning">
    ///
    /// Always set a matching [`epilogue`][`TocOpts::epilogue`] -
    /// otherwise the output will be invalid HTML!
    ///
    /// </div>
    #[serde(rename = "dmos_toc_prologue")]
    pub prologue: String,

    /// Arbitrary HTML to output after the table of contents list element.
    ///
    /// Defaults to `"</section>"`.
    ///
    /// <div class="warning">
    ///
    /// Always set a matching [`prologue`][`TocOpts::prologue`] -
    /// otherwise the output will be invalid HTML!
    ///
    /// </div>
    #[serde(rename = "dmos_toc_epilogue")]
    pub epilogue: String,
}

impl Default for TocOpts {
    fn default() -> Self {
        TocOpts {
            render: false,
            skip_title: false,
            prologue: "<section class=\"toc\"><h3>Table of Contents</h3>".to_string(),
            epilogue: "</section>".to_string(),
        }
    }
}

/// Options controlling the document generation.
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default)]
pub struct RenderOpts {
    /// Add anchors to headings linking to themselves.
    ///
    /// Such anchors are a common technique to allow users to easily get the
    /// link to a specific section. They are often styled with CSS to only
    /// appear when hovering over the heading. You can see a similar mechanism
    /// in action by hovering the mouse over any section heading of the [DMOS
    /// README on sr.ht](https://git.sr.ht/~bitfehler/dmos).
    ///
    /// # Example
    ///
    /// Given the following djot snippet:
    ///
    /// ```md
    /// ## My section
    /// ```
    ///
    /// Setting `anchors` to `Some("#".to_string())` will produce the following
    /// output:
    ///
    /// ```html
    /// <section id="My-section">
    /// <h2><a href="#My-section" aria-hidden="true">#</a>My section</h2>
    /// </section>
    /// ```
    #[serde(rename = "dmos_anchor")]
    pub anchor: Option<String>,

    /// Disable emoji handling in symbol processing.
    ///
    /// Djot has the concept of [symbols][djot-sym]. It uses the `:<NAME>:`
    /// syntax popularized by Github and others, such as `:+1:` for 👍. DMOS
    /// handles these symbols and by default replaces them with the respective
    /// emoji, if one is found for the given name. If `no_emojis` is set to
    /// true, this mechanism will be disabled. If a custom symbol handler is
    /// set, it will still be executed.
    ///
    /// [djot-sym]: https://htmlpreview.github.io/?https://github.com/jgm/djot/blob/master/doc/syntax.html#symbols
    ///
    #[serde(rename = "dmos_no_emojis", deserialize_with = "deserialize_bool")]
    pub no_emojis: bool,

    /// Add custom symbol handler.
    ///
    /// In addition to the emoji handling, DMOS can use a custom symbol handler
    /// by providing an executable. DMOS will call the custom handler with the
    /// symbol name as the only argument. If the symbol handler returns with a
    /// non-zero exit code, the symbol is not processed. If it does return with
    /// an exit code of zero, the symbol is replaced with the output of the
    /// handler.
    ///
    /// DMOS will always try the built-in emoji handler first, unless it is
    /// disabled.
    ///
    /// [djot-sym]: https://htmlpreview.github.io/?https://github.com/jgm/djot/blob/master/doc/syntax.html#symbols
    ///
    #[serde(rename = "dmos_symbols")]
    pub symbols: Option<String>,

    /// Render the document without the title (i.e. the first top-level heading)
    ///
    /// Can be useful if the desired page layout requires putting the title
    /// someplace detached from the document, or for example to render the text
    /// for an RSS feed.
    #[serde(rename = "dmos_skip_title", deserialize_with = "deserialize_bool")]
    pub skip_title: bool,

    /// Generate a table of contents.
    ///
    /// Set to `Some(TocOpts::default())` to enable TOC generation. See
    /// [`TocOpts`] for more fine-grained controls.
    #[serde(flatten)]
    pub toc: TocOpts,
}

// djot attributes are always strings, so use some custom logic to deserialize bools
fn deserialize_bool<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
    D: de::Deserializer<'de>,
{
    let s: String = de::Deserialize::deserialize(deserializer)?;

    match s.as_str() {
        "true" => Ok(true),
        "false" => Ok(false),
        _ => Err(de::Error::unknown_variant(s.as_str(), &["true", "false"])),
    }
}