dmos 0.7.0

Djot HTML renderer with advanced features
Documentation
use std::borrow::Cow;

use jotdown::{Container, Event, Render};

use crate::{MetaFormat, MetaOpts};

/// Implementation of [`jotdown::Render`] that outputs all "dmos:" attributes
/// either in a format suitable for consumption with env(1) or in JSON.
pub struct MetaRenderer {
    opts: MetaOpts,
    first_written: bool,
}

impl MetaRenderer {}

impl Render<'_> for MetaRenderer {
    fn push_event<W>(&mut self, event: Event<'_>, mut out: W) -> std::fmt::Result
    where
        W: std::fmt::Write,
    {
        self.render_event(&event, &mut out)
    }
}

fn must_escape_shell(ch: char) -> bool {
    match ch {
        'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '=' | '/' | ',' | '.' | '+' | '@' | '%' => {
            false
        }
        _ => char::is_ascii(&ch),
    }
}

/// Escape characters that may have special meaning in a shell, including spaces.
fn escape_shell(s: Cow<str>) -> Cow<str> {
    if s.is_empty() || !s.contains(must_escape_shell) {
        return s;
    }

    let mut es = String::with_capacity(s.len() + 2);
    for ch in s.chars() {
        if must_escape_shell(ch) {
            es.push('\\');
        }
        es.push(ch);
    }
    es.into()
}

fn must_escape_json(ch: char) -> bool {
    matches!(ch, '"' | '\\')
}

/// Escape characters for the string to be valid JSON.
fn escape_json(s: Cow<str>) -> Cow<str> {
    if s.is_empty() || !s.contains(must_escape_json) {
        return s;
    }

    let mut es = String::with_capacity(s.len() + 2);
    for ch in s.chars() {
        if ch == '"' || ch == '\\' {
            es.push('\\');
        }
        es.push(ch);
    }
    es.into()
}

impl MetaRenderer {
    pub fn new(opts: MetaOpts) -> Self {
        MetaRenderer {
            opts,
            first_written: false,
        }
    }

    fn start<W>(&mut self, out: &mut W) -> std::fmt::Result
    where
        W: std::fmt::Write,
    {
        if matches!(self.opts.format, MetaFormat::Json) {
            out.write_str("{\n")?;
        }
        Ok(())
    }

    fn finish<W>(&mut self, out: &mut W) -> std::fmt::Result
    where
        W: std::fmt::Write,
    {
        if matches!(self.opts.format, MetaFormat::Json) {
            out.write_str("\n}\n")?;
        }
        Ok(())
    }

    fn render_event<W>(&mut self, e: &Event<'_>, out: &mut W) -> std::fmt::Result
    where
        W: std::fmt::Write,
    {
        match e {
            Event::Start(Container::Document, _) => self.start(out)?,
            Event::End(Container::Document) => self.finish(out)?,
            Event::Start(_, attr) => {
                for (k, v) in attr {
                    if k.key().is_some_and(|key| key.starts_with("dmos:")) {
                        let def = match self.opts.format {
                            MetaFormat::Shell => {
                                format!(
                                    "{}={}\n",
                                    k.key().unwrap().replace(':', "_"),
                                    escape_shell(v.to_string().into()),
                                )
                            }
                            MetaFormat::Json => {
                                if self.first_written {
                                    out.write_str(",\n")?;
                                }
                                format!(
                                    "\"{}\": \"{}\"",
                                    k.key().unwrap().replace(':', "_"),
                                    escape_json(v.to_string().into()),
                                )
                            }
                        };
                        out.write_str(&def)?;
                        self.first_written = true;
                    }
                }
            }
            _ => (),
        };
        Ok(())
    }
}