Skip to main content

base_markdown/markdown/
mod.rs

1// Released under the MIT License.
2// Copyright, 2024, by Bnchi.
3// Copyright, 2024, by Titus Wormer.
4// Copyright, 2026, by Samuel Williams.
5
6//! API.
7//!
8//! JS equivalent: https://github.com/syntax-tree/mdast-util-to-markdown/blob/main/lib/index.js.
9
10// Formatting rules are adapted from `mdast-util-to-markdown`; keep the parser's stricter lints on the surrounding API.
11#![allow(clippy::pedantic)]
12
13pub use self::configure::{IndentOptions, LineWrapping, Options};
14use self::state::{Info, State};
15use crate::{message::Message, Document, NodeId, ParseOptions};
16use alloc::string::String;
17
18extern crate alloc;
19mod configure;
20mod construct_name;
21mod escape;
22mod handle;
23mod state;
24mod util;
25
26/// Normalize a Markdown document.
27///
28/// Parses `CommonMark`, GFM, frontmatter, and math ([`ParseOptions::normalize`]), then serializes the result with default formatting and soft source line breaks unwrapped. Explicit hard breaks, code content, and block boundaries are preserved. The output uses LF line endings and ends with one when non-empty. Tools that generate Markdown can pass their output through this function to produce exactly what `bake-markdown`'s `markdown:normalize` task writes.
29///
30/// ```
31/// use base_markdown::normalize_markdown;
32///
33/// assert_eq!(normalize_markdown("* A wrapped\r\nline.").unwrap(), "- A wrapped line.\n");
34/// ```
35///
36/// # Errors
37/// Returns unsupported content shapes from serialization.
38pub fn normalize_markdown(source: &str) -> Result<String, Message> {
39    normalize_markdown_with_options(source, &ParseOptions::normalize())
40}
41
42/// Normalize a Markdown document using explicit parser options.
43///
44/// Use this for syntax beyond the GFM, frontmatter, and math support of [`normalize_markdown`], or to disable individual constructs. The output is serialized as by [`normalize_markdown`].
45///
46/// # Errors
47/// Returns unsupported content shapes from serialization.
48pub fn normalize_markdown_with_options(
49    source: &str,
50    parse_options: &ParseOptions,
51) -> Result<String, Message> {
52    let document = Document::parse(source, parse_options)
53        .expect("the built-in Markdown parser produces a valid document");
54
55    to_markdown_with_options(
56        &document,
57        &Options {
58            line_wrapping: LineWrapping::Unwrap,
59            ..Options::default()
60        },
61    )
62}
63
64/// Serialize an arena document to Markdown.
65///
66/// The output always uses LF line endings: CRLF and lone CR line endings in node values, such as code, HTML, and text, are written as LF.
67///
68/// # Errors
69/// Returns invalid formatting options, unsupported syntax metadata, or unsupported content shapes.
70pub fn to_markdown<S: AsRef<str>>(document: &Document<S>) -> Result<String, Message> {
71    to_markdown_with_options(document, &Options::default())
72}
73
74/// Serialize an arena document with formatting options.
75///
76/// # Errors
77/// Returns invalid formatting options, unsupported syntax metadata, or unsupported content shapes.
78pub fn to_markdown_with_options<S: AsRef<str>>(
79    document: &Document<S>,
80    options: &Options,
81) -> Result<String, Message> {
82    to_markdown_node(document, document.root(), options)
83}
84
85/// Serialize a document subtree with formatting options.
86///
87/// The output always uses LF line endings. Non-empty block subtrees end with a line ending; inline (phrasing) subtrees, such as text, emphasis, or a link, are returned without an added line ending.
88///
89/// # Errors
90/// Returns an invalid node index, invalid formatting options, unsupported syntax metadata, or unsupported content shapes.
91pub fn to_markdown_node<S: AsRef<str>>(
92    document: &Document<S>,
93    node: NodeId,
94    options: &Options,
95) -> Result<String, Message> {
96    if document.node(node).is_none() {
97        return Err(state::invalid_node());
98    }
99    let kind = &document.node(node).unwrap().kind;
100    let inline = state::is_phrasing(kind)
101        || matches!(kind, crate::stream::EventKind::Opaque { block: false, .. });
102    let mut state = State::new(options);
103    state.serialization_root = Some(node);
104    let mut result = state.handle(document, node, &Info::new("\n", "\n"))?;
105    // Markdown treats CR, LF, and CRLF alike, so a single line ending keeps output stable across platforms.
106    if result.contains('\r') {
107        result = result.replace("\r\n", "\n").replace('\r', "\n");
108    }
109    // Only block output is line-terminated, matching cmark's renderer.
110    if !inline && !result.is_empty() && !result.ends_with('\n') {
111        result.push('\n');
112    }
113    Ok(result)
114}