fundaia 0.22.0

Command line for the Fundaia deployment platform: projects, services, variables, deployments, logs and metrics
//! Just enough Markdown to read a changelog entry in a terminal.
//!
//! Not a Markdown implementation and not trying to be: the only Markdown this
//! tool ever renders is a file it ships itself, so what is needed is the four
//! things that file uses — a sub-heading, a bullet, bold and a code span — and
//! anything else is passed through untouched. A dependency that parses the
//! whole grammar would be a megabyte of binary and a startup budget spent on
//! syntax nobody here writes.
//!
//! Unrecognised markup is left exactly as it was written. A changelog is still
//! legible with a stray asterisk in it; one with a mangled line is not.

use owo_colors::OwoColorize;

use crate::render::theme;

/// An entry's body, indented and coloured, ready to print.
pub fn body(text: &str) -> String {
    text.lines().map(line).collect::<Vec<_>>().join("\n")
}

fn line(raw: &str) -> String {
    if raw.trim().is_empty() {
        return String::new();
    }

    if let Some(heading) = raw.strip_prefix("### ") {
        return format!("  {}", inline(heading).style(theme::strong()));
    }

    if let Some(item) = raw.strip_prefix("- ") {
        return format!("  {} {}", "·".style(theme::accent()), inline(item));
    }

    format!("  {}", inline(raw))
}

/// `**bold**`, `*emphasis*` and `` `code` ``, in one pass.
///
/// All three are the same shape — a delimiter, a run, the same delimiter — so
/// one loop handles them. A delimiter that is never closed is printed as
/// itself, which is the whole reason this is a scan and not a regular
/// expression: the fallback has to be "leave it alone".
fn inline(text: &str) -> String {
    let mut out = String::with_capacity(text.len());
    let mut rest = text;

    while let Some((before, delimiter, after)) = next_delimiter(rest) {
        out.push_str(before);

        match after.find(delimiter) {
            Some(end) => {
                let inner = &after[..end];
                out.push_str(&format!("{}", inner.style(style_of(delimiter))));
                rest = &after[end + delimiter.len()..];
            }
            // Opened and never closed: it is punctuation, not markup.
            None => {
                out.push_str(delimiter);
                rest = after;
            }
        }
    }

    out.push_str(rest);
    out
}

fn style_of(delimiter: &str) -> owo_colors::Style {
    match delimiter {
        "**" => theme::strong(),
        "*" => theme::emphasis(),
        _ => theme::accent(),
    }
}

/// The first delimiter in the text, split around it.
///
/// Scanned left to right rather than found three times and compared, because
/// `**` and `*` overlap: `find('*')` answers the same position for both, and
/// the longer one has to win there or every bold run opens an emphasis that
/// swallows it. Every delimiter is ASCII, so the byte where one is found is
/// always a character boundary and the slices below are safe.
fn next_delimiter(text: &str) -> Option<(&str, &'static str, &str)> {
    let bytes = text.as_bytes();

    for at in 0..bytes.len() {
        let delimiter = match bytes[at] {
            b'*' if bytes[at..].starts_with(b"**") => "**",
            b'*' => "*",
            b'`' => "`",
            _ => continue,
        };

        return Some((&text[..at], delimiter, &text[at + delimiter.len()..]));
    }

    None
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The rendered block with its escapes removed.
    ///
    /// Stripped rather than switched off: `set_override` governs
    /// `if_supports_color` and not the `Style` this module applies, so a test
    /// that asked for no colour would be asserting against a global that has no
    /// say here. What is under test is the *text* — which markers are gone and
    /// what is left where.
    fn plain(text: &str) -> String {
        let rendered = body(text);
        let mut out = String::with_capacity(rendered.len());
        let mut rest = rendered.as_str();

        while let Some(at) = rest.find('\u{1b}') {
            out.push_str(&rest[..at]);
            rest = match rest[at..].find('m') {
                Some(end) => &rest[at + end + 1..],
                None => "",
            };
        }

        out.push_str(rest);
        out
    }

    #[test]
    fn it_should_indent_a_paragraph() {
        assert_eq!(plain("Una línea."), "  Una línea.");
    }

    #[test]
    fn it_should_leave_a_blank_line_blank() {
        assert_eq!(plain("Uno.\n\nDos."), "  Uno.\n\n  Dos.");
    }

    #[test]
    fn it_should_draw_a_bullet_for_a_list_item() {
        assert_eq!(plain("- Algo"), "  · Algo");
    }

    #[test]
    fn it_should_drop_the_hashes_of_a_sub_heading() {
        assert_eq!(plain("### Un título"), "  Un título");
    }

    #[test]
    fn it_should_drop_the_asterisks_around_bold_text() {
        assert_eq!(plain("Esto es **importante**."), "  Esto es importante.");
    }

    #[test]
    fn it_should_drop_the_backticks_around_a_code_span() {
        assert_eq!(
            plain("Ejecuta `fundaia status`."),
            "  Ejecuta fundaia status."
        );
    }

    #[test]
    fn it_should_handle_two_code_spans_in_one_line() {
        assert_eq!(plain("`uno` y `dos`"), "  uno y dos");
    }

    #[test]
    fn it_should_drop_the_asterisks_around_emphasis() {
        assert_eq!(plain("qué había *aquí*."), "  qué había aquí.");
    }

    /// `find('*')` answers the same position for both, and the longer one has
    /// to win or every bold run opens an emphasis that swallows it.
    #[test]
    fn it_should_read_a_double_asterisk_as_bold_and_not_as_two_emphases() {
        assert_eq!(plain("**dos** y *una*"), "  dos y una");
    }

    /// The fallback that keeps a hand-written file legible.
    #[test]
    fn it_should_leave_a_delimiter_that_was_never_closed_alone() {
        assert_eq!(plain("2 ** 3 es ocho"), "  2 ** 3 es ocho");
    }

    #[test]
    fn it_should_leave_a_lone_asterisk_where_it_was_written() {
        assert_eq!(plain("un * suelto"), "  un * suelto");
    }

    #[test]
    fn it_should_leave_an_unknown_marker_where_it_was_written() {
        assert_eq!(plain("> una cita"), "  > una cita");
    }
}